Bootstrap tamq local tmux agent message queue
Some checks failed
tamq-ci / test (push) Failing after 36s

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a02f34-ead4-7a60-85cd-d7f08e22fa0e
This commit is contained in:
tegwick 2026-08-24 01:18:40 +02:00
parent 521bbe5afc
commit 4d915a2617
58 changed files with 1882 additions and 1 deletions

9
tests/test_ack.py Normal file
View file

@ -0,0 +1,9 @@
from tamq.store import Store
def test_acknowledge_message(tmp_path):
store = Store(tmp_path / "queue.sqlite3")
message_id = store.add("a", "b", "body")
assert store.acknowledge(message_id)
assert store.list()[0]["state"] == "acknowledged"
assert not store.acknowledge("missing")

11
tests/test_broker.py Normal file
View file

@ -0,0 +1,11 @@
from tamq.broker import BrokerIdentity, InputBroker
from tamq.store import Store
def test_broker_preserves_identity(tmp_path):
store = Store(tmp_path / "queue.sqlite3")
broker = InputBroker(store, BrokerIdentity("tmux-amq-42", "net-kingdom"))
assert broker.inspect_line("@railiance-platform: hello") is not None
row = store.list()[0]
assert row["sender_repo"] == "net-kingdom"
assert row["endpoint_id"] == "tmux-amq-42"

View file

@ -0,0 +1,21 @@
from tamq.broker import BrokerIdentity, InputBroker
from tamq.store import Store
class FakeControl:
def __init__(self):
self.calls = []
def inject(self, window, text):
self.calls.append((window, text))
def test_pending_delivery_marks_injected(tmp_path):
store = Store(tmp_path / "queue.sqlite3")
message_id = store.add("net-kingdom", "railiance-platform", "hello", endpoint="tmux-amq-42")
control = FakeControl()
count = InputBroker(store, BrokerIdentity("tmux-amq-42", "net-kingdom")).deliver_pending(control, window_for_repo=lambda repo: f"tamq:{repo}")
assert count == 1
assert control.calls == [("tamq:railiance-platform", "#net-kingdom: hello")]
assert store.list()[0]["message_id"] == message_id
assert store.list()[0]["state"] == "injected"

View file

@ -0,0 +1,10 @@
from tamq import broker
from tamq.broker import BrokerIdentity, InputBroker
from tamq.store import Store
def test_broker_rejects_unregistered_target(tmp_path, monkeypatch):
monkeypatch.setattr(broker, "validate_targets", lambda repos: (_ for _ in ()).throw(broker.RegistryError("unknown")))
store = Store(tmp_path / "queue.sqlite3")
assert InputBroker(store, BrokerIdentity("ep", "source")).inspect_line("@missing: hello") is None
assert store.list() == []

18
tests/test_cli.py Normal file
View file

@ -0,0 +1,18 @@
from tamq.cli import main
import pytest
def test_help_and_version(capsys):
with pytest.raises(SystemExit) as exc:
main(["--version"])
assert exc.value.code == 0
assert "0.1.0" in capsys.readouterr().out
assert main([]) == 0
assert "usage:" in capsys.readouterr().out
def test_attach_delegates_to_tmux(monkeypatch):
calls = []
monkeypatch.setattr("tamq.cli.subprocess.run", lambda args, check=False: calls.append((args, check)) or type("R", (), {"returncode": 0})())
assert main(["attach", "--session", "demo"]) == 0
assert calls == [(["tmux", "attach-session", "-t", "demo"], False)]

View file

@ -0,0 +1,6 @@
from tamq.cli import parse_size
def test_parse_size():
assert parse_size("100MB") == 100_000_000
assert parse_size("2KB") == 2_000

9
tests/test_completion.py Normal file
View file

@ -0,0 +1,9 @@
from tamq.cli import completion_script
def test_completion_scripts_include_commands():
for shell in ("bash", "zsh", "fish"):
script = completion_script(shell)
assert "start" in script
assert "attach" in script
assert "replay" in script

View file

@ -0,0 +1,7 @@
from tamq.cli import completion_script
def test_completion_mentions_gita():
assert "gita ls" in completion_script("bash")
assert "gita ls" in completion_script("zsh")
assert "gita ls" in completion_script("fish")

View file

@ -0,0 +1,11 @@
import json
from tamq.cli import main
def test_config_command(monkeypatch, capsys, tmp_path):
monkeypatch.setenv("TAMQ_STATE_DIR", str(tmp_path / "state"))
assert main(["config"]) == 0
output = json.loads(capsys.readouterr().out)
assert output["database"].endswith("tamq.sqlite3")
assert output["policy_profile"] == "default"

View file

@ -0,0 +1,20 @@
from tamq.config import setting
def test_toml_defaults(monkeypatch, tmp_path):
config = tmp_path / "config.toml"
config.write_text("[tamq]\npurge_before='30d'\nhistory_max_size='12MB'\n")
monkeypatch.setenv("TAMQ_CONFIG", str(config))
assert setting("purge_before", "365d") == "30d"
assert setting("history_max_size", "100MB") == "12MB"
assert setting("missing", "fallback") == "fallback"
def test_delivery_interval_is_configurable(monkeypatch, tmp_path):
config = tmp_path / "config.toml"
config.write_text("[tamq]\ndelivery_poll_interval='2.5'\n")
monkeypatch.setenv("TAMQ_CONFIG", str(config))
from tamq.service import Service
from tamq.store import Store
service = Service(store=Store(tmp_path / "state.sqlite3"))
assert service.poll_interval == 2.5

View file

@ -0,0 +1,6 @@
from tamq.config import lock_path
def test_lock_path_override(monkeypatch, tmp_path):
monkeypatch.setenv("TAMQ_LOCKFILE", str(tmp_path / "tamq.lock"))
assert lock_path() == tmp_path / "tamq.lock"

11
tests/test_config_toml.py Normal file
View file

@ -0,0 +1,11 @@
from tamq.config import socket_path, state_dir
def test_toml_paths(monkeypatch, tmp_path):
config = tmp_path / "config.toml"
config.write_text(f"[tamq]\nsocket='{tmp_path / 'socket'}'\nstate_dir='{tmp_path / 'state'}'\n")
monkeypatch.setenv("TAMQ_CONFIG", str(config))
monkeypatch.delenv("TAMQ_SOCKET", raising=False)
monkeypatch.delenv("TAMQ_STATE_DIR", raising=False)
assert socket_path() == tmp_path / "socket"
assert state_dir() == tmp_path / "state"

12
tests/test_control.py Normal file
View file

@ -0,0 +1,12 @@
from tamq.control import shell_quote
from tamq.tmux import shell_join
def test_shell_quote():
quoted = shell_quote("hello 'world'")
assert quoted.startswith("'") and quoted.endswith("'")
assert "\\'" in quoted
def test_tmux_shell_join_quotes_arguments():
assert shell_join(["tamq", "tap", "--repo", "net-kingdom", "--", "codex"]) == "tamq tap --repo net-kingdom -- codex"

View file

@ -0,0 +1,9 @@
from tamq.diagnostics import configure
def test_orwell_log_is_private(tmp_path, monkeypatch, capsys):
monkeypatch.setenv("XDG_STATE_HOME", str(tmp_path))
configure(orwell=True)
warning = capsys.readouterr().err
assert "--orwell" in warning
assert (tmp_path / "tamq" / "tamq-orwell.log").stat().st_mode & 0o077 == 0

17
tests/test_disconnect.py Normal file
View file

@ -0,0 +1,17 @@
from tamq.store import Store
def test_disconnect_endpoint(tmp_path):
store = Store(tmp_path / "queue.sqlite3")
store.register_endpoint("ep", 1, "tamq", ["a"])
store.disconnect_endpoint("ep")
assert store.endpoint("ep") is None
def test_disconnect_all_marks_active_endpoints(tmp_path):
store = Store(tmp_path / "queue.sqlite3")
store.register_endpoint("ep-1", 1, "tamq", ["a"])
store.register_endpoint("ep-2", 2, "tamq", ["b"])
store.disconnect_all()
assert store.endpoint("ep-1") is None
assert store.endpoint("ep-2") is None

7
tests/test_endpoint.py Normal file
View file

@ -0,0 +1,7 @@
from tamq.tmux import Endpoint
def test_endpoint_instance_key():
endpoint = Endpoint("tamq", 42, ["net-kingdom"], "tmux-amq-42-boot")
assert endpoint.endpoint_id == "tmux-amq-42"
assert endpoint.instance_key == "tmux-amq-42-boot"

View file

@ -0,0 +1,25 @@
import asyncio
import json
from tamq.service import Service
from tamq.store import Store
from tamq import service
def test_send_rejects_endpoint_target_not_attached(tmp_path, monkeypatch):
monkeypatch.setattr(service, "validate_targets", lambda repos: None)
async def run():
path = tmp_path / "tamq.sock"
store = Store(tmp_path / "queue.sqlite3")
store.register_endpoint("ep", 1, "tamq", ["source"])
instance = Service(path, store)
server = await asyncio.start_unix_server(instance.handle, path=str(path))
try:
reader, writer = await asyncio.open_unix_connection(str(path))
payload = {"op":"send","endpoint_id":"ep","sender_repo":"source","target_repo":"other","body":"hello"}
writer.write((json.dumps(payload) + "\n").encode()); await writer.drain()
assert json.loads(await reader.readline())["ok"] is False
writer.close(); await writer.wait_closed()
finally:
server.close(); await server.wait_closed(); instance.close()
asyncio.run(run())

15
tests/test_endpoints.py Normal file
View file

@ -0,0 +1,15 @@
from tamq.store import Store
def test_endpoint_registration(tmp_path):
store = Store(tmp_path / "queue.sqlite3")
store.register_endpoint("tmux-amq-1-boot", 1, "tamq", ["a"])
row = store.endpoints()[0]
assert row["endpoint_id"] == "tmux-amq-1-boot"
assert row["pid"] == 1
def test_endpoint_resolves_visible_pid_id(tmp_path):
store = Store(tmp_path / "queue.sqlite3")
store.register_endpoint("tmux-amq-1-boot", 1, "tamq", ["a"])
assert store.endpoint("tmux-amq-1")["endpoint_id"] == "tmux-amq-1-boot"

12
tests/test_leases.py Normal file
View file

@ -0,0 +1,12 @@
from tamq.store import Store
def test_message_lease_claim_and_release(tmp_path):
store = Store(tmp_path / "queue.sqlite3")
message_id = store.add("a", "b", "body")
lease = store.claim(message_id, "tmux-amq-1")
assert lease is not None
assert store.claim(message_id, "tmux-amq-2") is None
assert store.renew(message_id, lease)
assert store.release(message_id, lease)
assert store.list()[0]["state"] == "injected"

View file

@ -0,0 +1,9 @@
import pytest
from tamq.store import Store
def test_message_body_limit(tmp_path):
store = Store(tmp_path / "queue.sqlite3")
with pytest.raises(ValueError):
store.add("a", "b", "x" * 8193)

5
tests/test_peer_auth.py Normal file
View file

@ -0,0 +1,5 @@
from tamq.service import Service
def test_peer_auth_method_exists():
assert hasattr(Service, "_peer_allowed")

14
tests/test_policy.py Normal file
View file

@ -0,0 +1,14 @@
from tamq.policy import load_profile
def test_default_policy(tmp_path):
profile = load_profile(tmp_path / "missing.toml")
assert profile.name == "default"
assert "secrets" in profile.require_human
def test_configured_policy(tmp_path):
path = tmp_path / "config.toml"
path.write_text("[policy.profiles.dev]\nallow=['repo_inspect']\nrequire_human=['destructive']\nsafety_gated_max_attempts=2\n")
profile = load_profile(path, "dev")
assert profile.safety_gated_max_attempts == 2

7
tests/test_policy_ack.py Normal file
View file

@ -0,0 +1,7 @@
from tamq.policy import load_profile
def test_ack_mode_profile(tmp_path):
path = tmp_path / "config.toml"
path.write_text("[policy.profiles.safe]\ndelivery_ack_mode='acknowledged'\n")
assert load_profile(path, "safe").delivery_ack_mode == "acknowledged"

21
tests/test_protocol.py Normal file
View file

@ -0,0 +1,21 @@
import asyncio
import json
from tamq.service import Service
from tamq.store import Store
def test_incompatible_protocol_rejected(tmp_path):
async def run():
path = tmp_path / "tamq.sock"
service = Service(path, Store(tmp_path / "queue.sqlite3"))
server = await asyncio.start_unix_server(service.handle, path=str(path))
try:
reader, writer = await asyncio.open_unix_connection(str(path))
writer.write(b'{"op":"ping","protocol":"2.0"}\n'); await writer.drain()
response = json.loads(await reader.readline())
assert response["ok"] is False
writer.close(); await writer.wait_closed()
finally:
server.close(); await server.wait_closed(); service.close()
asyncio.run(run())

29
tests/test_purge.py Normal file
View file

@ -0,0 +1,29 @@
import json
from tamq.cli import main
from tamq.store import Store
def test_purge_dry_run_does_not_delete(tmp_path, monkeypatch, capsys):
state = tmp_path / "state"
monkeypatch.setenv("TAMQ_STATE_DIR", str(state))
store = Store(state / "tamq.sqlite3")
store.add("a", "b", "body")
store.close()
assert main(["purge"]) == 0
assert "Dry run" in capsys.readouterr().out
store = Store(state / "tamq.sqlite3")
assert len(store.list()) == 1
def test_purge_confirmed_deletes(tmp_path, monkeypatch, capsys):
state = tmp_path / "state"
monkeypatch.setenv("TAMQ_STATE_DIR", str(state))
store = Store(state / "tamq.sqlite3")
store.add("a", "b", "body")
store.db.execute("UPDATE messages SET created_at=created_at-400*86400")
store.db.commit()
store.close()
assert main(["purge", "--yes"]) == 0
assert "Purged" in capsys.readouterr().out
assert len(Store(state / "tamq.sqlite3").list()) == 0

16
tests/test_registry.py Normal file
View file

@ -0,0 +1,16 @@
from tamq import registry
def test_validate_targets(monkeypatch):
monkeypatch.setattr(registry, "registered_repositories", lambda: {"net-kingdom"})
registry.validate_targets(["net-kingdom"])
def test_reject_unknown_targets(monkeypatch):
monkeypatch.setattr(registry, "registered_repositories", lambda: {"net-kingdom"})
try:
registry.validate_targets(["missing"])
except registry.RegistryError as exc:
assert "missing" in str(exc)
else:
raise AssertionError("unknown target was accepted")

View file

@ -0,0 +1,10 @@
from tamq import registry
def test_repository_paths(monkeypatch):
class Result:
returncode = 0
stdout = "remote,net-kingdom,/home/worsch/net-kingdom\n"
stderr = ""
monkeypatch.setattr(registry.subprocess, "run", lambda *args, **kwargs: Result())
assert registry.repository_paths()["net-kingdom"] == "/home/worsch/net-kingdom"

31
tests/test_replay.py Normal file
View file

@ -0,0 +1,31 @@
import json
from tamq.cli import main
from tamq.store import Store
def test_replay_reports_batch(tmp_path, monkeypatch, capsys):
source = tmp_path / "messages.jsonl"
source.write_text(json.dumps({"message_id": "old-1", "sender_repo": "a", "target_repo": "b", "body": "hello"}) + "\n")
monkeypatch.setenv("TAMQ_STATE_DIR", str(tmp_path / "state"))
assert main(["replay", str(source)]) == 0
output = json.loads(capsys.readouterr().out)
assert output["batch_id"].startswith("replay-")
assert output["count"] == 1
def test_replay_preserves_endpoint(tmp_path, monkeypatch, capsys):
source = tmp_path / "messages.jsonl"
source.write_text(json.dumps({"sender_repo": "a", "target_repo": "b", "body": "hello", "endpoint_id": "tmux-amq-42"}) + "\n")
monkeypatch.setenv("TAMQ_STATE_DIR", str(tmp_path / "state"))
assert main(["replay", str(source)]) == 0
row = Store(tmp_path / "state" / "tamq.sqlite3").list()[0]
assert row["endpoint_id"] == "tmux-amq-42"
def test_replay_rejects_bad_json(tmp_path, monkeypatch, capsys):
source = tmp_path / "bad.jsonl"
source.write_text("not-json\n")
monkeypatch.setenv("TAMQ_STATE_DIR", str(tmp_path / "state"))
assert main(["replay", str(source)]) == 2
assert "cannot replay" in capsys.readouterr().err

19
tests/test_request.py Normal file
View file

@ -0,0 +1,19 @@
import asyncio
import json
from tamq.service import request
def test_request_round_trip(tmp_path):
async def run():
path = tmp_path / "tamq.sock"
async def handle(reader, writer):
payload = json.loads(await reader.readline())
writer.write((json.dumps({"ok": payload["op"] == "ping"}) + "\n").encode())
await writer.drain(); writer.close(); await writer.wait_closed()
server = await asyncio.start_unix_server(handle, path=str(path))
try:
assert await request({"op": "ping"}, path) == {"ok": True}
finally:
server.close(); await server.wait_closed()
asyncio.run(run())

11
tests/test_routing.py Normal file
View file

@ -0,0 +1,11 @@
from tamq.routing import parse_address_line
def test_direct_address():
assert parse_address_line("@railiance-platform: do something!").body == "do something!"
assert parse_address_line("@railiance-platform: do something!").target_repo == "railiance-platform"
def test_ordinary_input_is_unchanged():
assert parse_address_line("hello @repo: not at start") is None
assert parse_address_line("@repo:") is None

View file

@ -0,0 +1,8 @@
import json
from tamq.cli import build_parser
def test_send_accepts_endpoint_id():
args = build_parser().parse_args(["send", "--endpoint-id", "ep", "@repo:", "hello"])
assert args.endpoint_id == "ep"

24
tests/test_service.py Normal file
View file

@ -0,0 +1,24 @@
import asyncio
import json
from tamq.service import Service
from tamq.store import Store
def test_register_and_send(tmp_path):
async def run():
path = tmp_path / "tamq.sock"
service = Service(path, Store(tmp_path / "queue.sqlite3"))
server = await asyncio.start_unix_server(service.handle, path=str(path))
try:
reader, writer = await asyncio.open_unix_connection(str(path))
writer.write((json.dumps({"op": "register", "endpoint_id": "tmux-amq-42", "pid": 42, "session": "tamq", "repos": ["net-kingdom"]}) + "\n").encode()); await writer.drain()
assert json.loads(await reader.readline())["ok"] is True
writer.close(); await writer.wait_closed()
reader, writer = await asyncio.open_unix_connection(str(path))
writer.write(b'{"op":"send","sender_repo":"net-kingdom","target_repo":"railiance-platform","body":"hello"}\n'); await writer.drain()
assert json.loads(await reader.readline())["state"] == "pending"
writer.close(); await writer.wait_closed()
finally:
server.close(); await server.wait_closed(); service.close()
asyncio.run(run())

View file

@ -0,0 +1,31 @@
from tamq.service import Service
from tamq.store import Store
class FakeControl:
instances = []
def __init__(self, session):
self.session = session
self.injected = []
self.__class__.instances.append(self)
def start(self):
return None
def inject(self, window, text):
self.injected.append((window, text))
def close(self):
return None
def test_service_delivery_injects_pending_messages(tmp_path, monkeypatch):
store = Store(tmp_path / "queue.sqlite3")
store.register_endpoint("tmux-amq-9-boot", 9, "tamq", ["repo-b"])
message_id = store.add("repo-a", "repo-b", "continue")
monkeypatch.setattr("tamq.service.ControlModeClient", FakeControl)
Service(store=store)._deliver_once()
assert FakeControl.instances[-1].injected == [("tamq:repo-b", "#repo-a: continue")]
assert store.list()[0]["message_id"] == message_id
assert store.list()[0]["state"] == "injected"

View file

@ -0,0 +1,23 @@
import asyncio
import json
from tamq.service import Service
from tamq.store import Store
from tamq import service
def test_register_rejects_unknown_repo(tmp_path, monkeypatch):
monkeypatch.setattr(service, "validate_targets", lambda repos: (_ for _ in ()).throw(service.RegistryError("unknown")))
async def run():
socket = tmp_path / "tamq.sock"
instance = Service(socket, Store(tmp_path / "queue.sqlite3"))
server = await asyncio.start_unix_server(instance.handle, path=str(socket))
try:
reader, writer = await asyncio.open_unix_connection(str(socket))
writer.write((json.dumps({"op": "register", "endpoint_id": "tmux-amq-1", "pid": 1, "session": "tamq", "repos": ["missing"]}) + "\n").encode()); await writer.drain()
response = json.loads(await reader.readline())
assert response["ok"] is False
writer.close(); await writer.wait_closed()
finally:
server.close(); await server.wait_closed(); instance.close()
asyncio.run(run())

View file

@ -0,0 +1,22 @@
import asyncio
import json
from tamq import service
from tamq.service import Service
from tamq.store import Store
def test_send_rejects_unknown_target(tmp_path, monkeypatch):
monkeypatch.setattr(service, "validate_targets", lambda repos: (_ for _ in ()).throw(service.RegistryError("unknown")))
async def run():
socket = tmp_path / "tamq.sock"
instance = Service(socket, Store(tmp_path / "queue.sqlite3"))
server = await asyncio.start_unix_server(instance.handle, path=str(socket))
try:
reader, writer = await asyncio.open_unix_connection(str(socket))
writer.write(b'{"op":"send","sender_repo":"a","target_repo":"missing","body":"hello"}\n'); await writer.drain()
assert json.loads(await reader.readline())["ok"] is False
writer.close(); await writer.wait_closed()
finally:
server.close(); await server.wait_closed(); instance.close()
asyncio.run(run())

25
tests/test_shutdown.py Normal file
View file

@ -0,0 +1,25 @@
import asyncio
from tamq.service import Service
from tamq.store import Store
def test_service_shutdown_removes_socket(tmp_path, monkeypatch):
monkeypatch.setenv("TAMQ_PIDFILE", str(tmp_path / "tamq.pid"))
async def run():
socket = tmp_path / "tamq.sock"
service = Service(socket, Store(tmp_path / "queue.sqlite3"))
task = asyncio.create_task(service.run())
for _ in range(20):
if socket.exists():
break
await asyncio.sleep(0.01)
assert socket.exists()
service.server.close()
await service.server.wait_closed()
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
asyncio.run(run())

11
tests/test_stop.py Normal file
View file

@ -0,0 +1,11 @@
from tamq.cli import main
def test_stop_cleans_stale_pidfile(tmp_path, monkeypatch, capsys):
pidfile = tmp_path / "tamq.pid"
pidfile.write_text("999999")
monkeypatch.setattr("tamq.cli.pid_path", lambda: pidfile)
monkeypatch.setattr("tamq.cli.os.kill", lambda pid, sig: (_ for _ in ()).throw(ProcessLookupError))
assert main(["stop"]) == 1
assert not pidfile.exists()
assert "not running" in capsys.readouterr().err

15
tests/test_store.py Normal file
View file

@ -0,0 +1,15 @@
import json
from tamq.store import Store
def test_message_history_and_jsonl(tmp_path):
store = Store(tmp_path / "tamq.sqlite3")
message_id = store.add("net-kingdom", "railiance-platform", "hello")
rows = store.list(target="railiance-platform")
assert rows[0]["message_id"] == message_id
output = tmp_path / "messages.jsonl"
store.export(rows, output)
assert json.loads(output.read_text())["body"] == "hello"
assert store.db.execute("PRAGMA journal_mode").fetchone()[0] == "wal"
store.close()

View file

@ -0,0 +1,20 @@
from tamq import tmux
def test_tmux_manager_builds_tap_windows(monkeypatch):
calls = []
manager = tmux.TmuxManager("tamq-test")
monkeypatch.setattr(tmux, "validate_targets", lambda repos: None)
monkeypatch.setattr(tmux, "repository_paths", lambda: {"a": "/tmp/a", "b": "/tmp/b"})
def run(*args, check=True):
calls.append(args)
if args[:2] == ("list-windows", "-t"):
return "__tamq_boot"
if args[:2] == ("display-message", "-p"):
return "42"
return ""
monkeypatch.setattr(manager, "_run", run)
monkeypatch.setattr(tmux.subprocess, "run", lambda *args, **kwargs: type("R", (), {"returncode": 1})())
endpoint = manager.ensure(["a", "b"], "codex")
assert endpoint.endpoint_id == "tmux-amq-42"
assert any("tamq tap" in " ".join(call) for call in calls if call and call[0] == "send-keys")