Bootstrap tamq local tmux agent message queue
Some checks failed
tamq-ci / test (push) Failing after 36s
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:
parent
521bbe5afc
commit
4d915a2617
58 changed files with 1882 additions and 1 deletions
9
tests/test_ack.py
Normal file
9
tests/test_ack.py
Normal 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
11
tests/test_broker.py
Normal 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"
|
||||
21
tests/test_broker_delivery.py
Normal file
21
tests/test_broker_delivery.py
Normal 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"
|
||||
10
tests/test_broker_registry.py
Normal file
10
tests/test_broker_registry.py
Normal 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
18
tests/test_cli.py
Normal 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)]
|
||||
6
tests/test_cli_helpers.py
Normal file
6
tests/test_cli_helpers.py
Normal 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
9
tests/test_completion.py
Normal 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
|
||||
7
tests/test_completion_dynamic.py
Normal file
7
tests/test_completion_dynamic.py
Normal 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")
|
||||
11
tests/test_config_command.py
Normal file
11
tests/test_config_command.py
Normal 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"
|
||||
20
tests/test_config_defaults.py
Normal file
20
tests/test_config_defaults.py
Normal 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
|
||||
6
tests/test_config_paths.py
Normal file
6
tests/test_config_paths.py
Normal 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
11
tests/test_config_toml.py
Normal 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
12
tests/test_control.py
Normal 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"
|
||||
9
tests/test_diagnostics.py
Normal file
9
tests/test_diagnostics.py
Normal 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
17
tests/test_disconnect.py
Normal 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
7
tests/test_endpoint.py
Normal 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"
|
||||
25
tests/test_endpoint_authorization.py
Normal file
25
tests/test_endpoint_authorization.py
Normal 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
15
tests/test_endpoints.py
Normal 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
12
tests/test_leases.py
Normal 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"
|
||||
9
tests/test_message_limits.py
Normal file
9
tests/test_message_limits.py
Normal 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
5
tests/test_peer_auth.py
Normal 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
14
tests/test_policy.py
Normal 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
7
tests/test_policy_ack.py
Normal 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
21
tests/test_protocol.py
Normal 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
29
tests/test_purge.py
Normal 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
16
tests/test_registry.py
Normal 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")
|
||||
10
tests/test_registry_paths.py
Normal file
10
tests/test_registry_paths.py
Normal 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
31
tests/test_replay.py
Normal 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
19
tests/test_request.py
Normal 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
11
tests/test_routing.py
Normal 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
|
||||
8
tests/test_send_endpoint.py
Normal file
8
tests/test_send_endpoint.py
Normal 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
24
tests/test_service.py
Normal 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())
|
||||
31
tests/test_service_delivery.py
Normal file
31
tests/test_service_delivery.py
Normal 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"
|
||||
23
tests/test_service_registry.py
Normal file
23
tests/test_service_registry.py
Normal 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())
|
||||
22
tests/test_service_send_registry.py
Normal file
22
tests/test_service_send_registry.py
Normal 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
25
tests/test_shutdown.py
Normal 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
11
tests/test_stop.py
Normal 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
15
tests/test_store.py
Normal 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()
|
||||
20
tests/test_tmux_manager.py
Normal file
20
tests/test_tmux_manager.py
Normal 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")
|
||||
Loading…
Add table
Add a link
Reference in a new issue