Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a03397-4d51-7fd1-8ff2-946eb22ea2bc
This commit is contained in:
parent
408d32df88
commit
74b2f27997
20 changed files with 608 additions and 182 deletions
|
|
@ -1,6 +1,6 @@
|
|||
import json
|
||||
|
||||
from tamq.cli import build_parser, main
|
||||
from tamq.cli import build_parser, main, normalize_argv
|
||||
import pytest
|
||||
|
||||
|
||||
|
|
@ -33,9 +33,32 @@ def test_start_parser_supports_command_and_cmd_alias():
|
|||
parser = build_parser()
|
||||
canonical = parser.parse_args(["start", "--command", "codex --quiet", "a", "b"])
|
||||
compatibility = parser.parse_args(["start", "--cmd", "claude", "a"])
|
||||
assert canonical.agent_command == "codex --quiet"
|
||||
neutral = parser.parse_args(["start", "a", "b"])
|
||||
assert canonical.initial_command == "codex --quiet"
|
||||
assert canonical.repos == ["a", "b"]
|
||||
assert compatibility.agent_command == "claude"
|
||||
assert compatibility.initial_command == "claude"
|
||||
assert neutral.initial_command is None
|
||||
|
||||
|
||||
def test_repository_first_and_command_first_start_shorthand():
|
||||
assert normalize_argv(["flex-auth", "audit-core"]) == [
|
||||
"start",
|
||||
"flex-auth",
|
||||
"audit-core",
|
||||
]
|
||||
assert normalize_argv(["--command", "codex", "flex-auth"]) == [
|
||||
"start",
|
||||
"--command",
|
||||
"codex",
|
||||
"flex-auth",
|
||||
]
|
||||
assert normalize_argv(["status"]) == ["status"]
|
||||
assert normalize_argv(["--verbose", "flex-auth", "audit-core"]) == [
|
||||
"--verbose",
|
||||
"start",
|
||||
"flex-auth",
|
||||
"audit-core",
|
||||
]
|
||||
|
||||
|
||||
def test_detached_no_service_start_skips_socket_registration(monkeypatch, capsys):
|
||||
|
|
@ -54,7 +77,7 @@ def test_detached_no_service_start_skips_socket_registration(monkeypatch, capsys
|
|||
class Manager:
|
||||
def preflight(self, repos, command):
|
||||
assert repos == ["a", "b"]
|
||||
assert command == "codex"
|
||||
assert command is None
|
||||
return "plan"
|
||||
|
||||
def ensure_plan(self, plan, *, tap=True):
|
||||
|
|
@ -121,7 +144,7 @@ def test_registration_failure_rolls_back_created_windows(monkeypatch, capsys):
|
|||
return "plan"
|
||||
|
||||
def ensure_plan(self, plan, *, tap=True):
|
||||
assert tap is True
|
||||
assert tap is False
|
||||
return endpoint
|
||||
|
||||
def rollback(self, value):
|
||||
|
|
@ -132,8 +155,15 @@ def test_registration_failure_rolls_back_created_windows(monkeypatch, capsys):
|
|||
|
||||
monkeypatch.setattr("tamq.cli.TmuxManager", Manager)
|
||||
monkeypatch.setattr("tamq.cli.preflight_runtime_paths", lambda: None)
|
||||
monkeypatch.setattr("tamq.cli.ensure_service", lambda: True)
|
||||
monkeypatch.setattr("tamq.cli.ensure_manual_service", lambda: True)
|
||||
monkeypatch.setattr("tamq.cli.request", failed_registration)
|
||||
assert main(["start", "--detach", "a"]) == 1
|
||||
assert rolled_back == [endpoint]
|
||||
assert "endpoint registration failed: denied" in capsys.readouterr().err
|
||||
|
||||
|
||||
def test_tap_requires_explicit_command_and_service(capsys):
|
||||
assert main(["start", "--tap", "a"]) == 2
|
||||
assert "--tap requires an explicit --command" in capsys.readouterr().err
|
||||
assert main(["start", "--tap", "--no-service", "--command", "sh", "a"]) == 2
|
||||
assert "--tap cannot be combined with --no-service" in capsys.readouterr().err
|
||||
|
|
|
|||
|
|
@ -1,6 +1,24 @@
|
|||
from tamq.cli import parse_size
|
||||
from tamq.cli import ensure_manual_service, parse_size
|
||||
|
||||
|
||||
def test_parse_size():
|
||||
assert parse_size("100MB") == 100_000_000
|
||||
assert parse_size("2KB") == 2_000
|
||||
|
||||
|
||||
def test_manual_service_restarts_legacy_broker(monkeypatch):
|
||||
capabilities = iter([[], ["manual_delivery"]])
|
||||
starts = []
|
||||
stops = []
|
||||
|
||||
async def request(payload):
|
||||
assert payload == {"op": "ping"}
|
||||
return {"ok": True, "capabilities": next(capabilities)}
|
||||
|
||||
monkeypatch.setattr("tamq.cli.ensure_service", lambda: starts.append(True) or True)
|
||||
monkeypatch.setattr("tamq.cli.stop_service_process", lambda: stops.append(True) or 42)
|
||||
monkeypatch.setattr("tamq.cli.request", request)
|
||||
|
||||
assert ensure_manual_service() is True
|
||||
assert len(starts) == 2
|
||||
assert stops == [True]
|
||||
|
|
|
|||
|
|
@ -94,34 +94,59 @@ def test_isolated_installed_tool_session_smoke(tmp_path):
|
|||
started = json.loads(
|
||||
run(
|
||||
str(tamq),
|
||||
"start",
|
||||
"--detach",
|
||||
"--command",
|
||||
"alpha-agent",
|
||||
"railiance-platform",
|
||||
"activity-core",
|
||||
"--detach",
|
||||
).stdout
|
||||
)
|
||||
assert started["repos"] == ["railiance-platform", "activity-core"]
|
||||
assert started["delivery_mode"] == "manual"
|
||||
rows = run(
|
||||
*tmux,
|
||||
"list-windows",
|
||||
"-t",
|
||||
"tamq",
|
||||
"-F",
|
||||
"#{window_name}|#{pane_current_path}|#{pane_pid}",
|
||||
"#{window_name}|#{pane_current_path}|#{pane_pid}|#{pane_current_command}",
|
||||
).stdout.splitlines()
|
||||
parsed = {name: (path, pid) for name, path, pid in (row.split("|", 2) for row in rows)}
|
||||
parsed = {
|
||||
name: (path, pid, command)
|
||||
for name, path, pid, command in (row.split("|", 3) for row in rows)
|
||||
}
|
||||
assert parsed["railiance-platform"][0] == str(repo_a)
|
||||
assert parsed["activity-core"][0] == str(repo_b)
|
||||
assert all(command != "alpha-agent" for _, _, command in parsed.values())
|
||||
assert all(
|
||||
"installed-alpha-ready"
|
||||
not in run(*tmux, "capture-pane", "-p", "-t", f"tamq:{repo}").stdout
|
||||
for repo in ("railiance-platform", "activity-core")
|
||||
)
|
||||
|
||||
for repo in ("railiance-platform", "activity-core"):
|
||||
run(
|
||||
*tmux,
|
||||
"send-keys",
|
||||
"-t",
|
||||
f"tamq:{repo}",
|
||||
"printf 'TAMQ_REPO=%s\\n' \"$TAMQ_REPO\"",
|
||||
"C-m",
|
||||
)
|
||||
deadline = time.monotonic() + 5
|
||||
while time.monotonic() < deadline:
|
||||
identity_captures = {
|
||||
repo: run(*tmux, "capture-pane", "-p", "-t", f"tamq:{repo}").stdout
|
||||
for repo in ("railiance-platform", "activity-core")
|
||||
}
|
||||
if all(f"TAMQ_REPO={repo}" in output for repo, output in identity_captures.items()):
|
||||
break
|
||||
time.sleep(0.05)
|
||||
assert all(f"TAMQ_REPO={repo}" in output for repo, output in identity_captures.items())
|
||||
|
||||
repeated = json.loads(
|
||||
run(
|
||||
str(tamq),
|
||||
"start",
|
||||
"--detach",
|
||||
"--command",
|
||||
"alpha-agent",
|
||||
"railiance-platform",
|
||||
"activity-core",
|
||||
).stdout
|
||||
|
|
@ -133,7 +158,7 @@ def test_isolated_installed_tool_session_smoke(tmp_path):
|
|||
"-t",
|
||||
"tamq",
|
||||
"-F",
|
||||
"#{window_name}|#{pane_current_path}|#{pane_pid}",
|
||||
"#{window_name}|#{pane_current_path}|#{pane_pid}|#{pane_current_command}",
|
||||
).stdout.splitlines()
|
||||
assert repeated_rows == rows
|
||||
pre_delivery_status = json.loads(run(str(tamq), "status").stdout)
|
||||
|
|
@ -141,22 +166,49 @@ def test_isolated_installed_tool_session_smoke(tmp_path):
|
|||
assert [item["endpoint_id"] for item in pre_delivery_status["endpoints"]] == [
|
||||
started["instance_id"]
|
||||
]
|
||||
assert pre_delivery_status["endpoints"][0]["delivery_mode"] == "manual"
|
||||
|
||||
message_id = run(
|
||||
str(tamq), "send", "@activity-core:", "installed-message"
|
||||
).stdout.strip()
|
||||
target_before = run(
|
||||
*tmux, "capture-pane", "-p", "-t", "tamq:activity-core"
|
||||
).stdout
|
||||
run(
|
||||
*tmux,
|
||||
"send-keys",
|
||||
"-t",
|
||||
"tamq:railiance-platform",
|
||||
f"{tamq} send '@activity-core: installed-message'",
|
||||
"C-m",
|
||||
)
|
||||
deadline = time.monotonic() + 5
|
||||
history = []
|
||||
inbox = []
|
||||
while time.monotonic() < deadline:
|
||||
history = [
|
||||
inbox = [
|
||||
json.loads(line)
|
||||
for line in run(str(tamq), "history", "--repo", "activity-core").stdout.splitlines()
|
||||
for line in run(
|
||||
str(tamq), "inbox", "--repo", "activity-core", "--json"
|
||||
).stdout.splitlines()
|
||||
]
|
||||
if history and history[-1]["state"] == "injected":
|
||||
if inbox:
|
||||
break
|
||||
time.sleep(0.05)
|
||||
assert history[-1]["message_id"] == message_id
|
||||
assert history[-1]["state"] == "injected"
|
||||
assert inbox[-1]["sender_repo"] == "railiance-platform"
|
||||
assert inbox[-1]["body"] == "installed-message"
|
||||
assert inbox[-1]["state"] == "pending"
|
||||
message_id = inbox[-1]["message_id"]
|
||||
target_after = run(
|
||||
*tmux, "capture-pane", "-p", "-t", "tamq:activity-core"
|
||||
).stdout
|
||||
assert target_after == target_before
|
||||
|
||||
assert run(str(tamq), "ack", message_id).stdout.strip() == message_id
|
||||
assert run(str(tamq), "inbox", "--repo", "activity-core").stdout == ""
|
||||
all_messages = [
|
||||
json.loads(line)
|
||||
for line in run(
|
||||
str(tamq), "inbox", "--repo", "activity-core", "--all", "--json"
|
||||
).stdout.splitlines()
|
||||
]
|
||||
assert all_messages[-1]["state"] == "acknowledged"
|
||||
status = json.loads(run(str(tamq), "status").stdout)
|
||||
assert status["service"] is True
|
||||
assert [item["endpoint_id"] for item in status["endpoints"]] == [started["instance_id"]]
|
||||
|
|
@ -168,8 +220,6 @@ def test_isolated_installed_tool_session_smoke(tmp_path):
|
|||
str(tamq),
|
||||
"start",
|
||||
"--detach",
|
||||
"--command",
|
||||
"alpha-agent",
|
||||
"railiance-platform",
|
||||
"activity-core",
|
||||
).stdout
|
||||
|
|
@ -181,7 +231,7 @@ def test_isolated_installed_tool_session_smoke(tmp_path):
|
|||
"-t",
|
||||
"tamq",
|
||||
"-F",
|
||||
"#{window_name}|#{pane_current_path}|#{pane_pid}",
|
||||
"#{window_name}|#{pane_current_path}|#{pane_pid}|#{pane_current_command}",
|
||||
).stdout.splitlines() == rows
|
||||
|
||||
run(*tmux, "kill-session", "-t", "tamq")
|
||||
|
|
@ -205,6 +255,7 @@ def test_isolated_installed_tool_session_smoke(tmp_path):
|
|||
).stdout
|
||||
)
|
||||
assert recovered["instance_id"] != started["instance_id"]
|
||||
assert recovered["delivery_mode"] == "manual"
|
||||
recovered_rows = run(
|
||||
*tmux,
|
||||
"list-windows",
|
||||
|
|
@ -217,6 +268,16 @@ def test_isolated_installed_tool_session_smoke(tmp_path):
|
|||
f"railiance-platform|{repo_a}",
|
||||
f"activity-core|{repo_b}",
|
||||
]
|
||||
deadline = time.monotonic() + 5
|
||||
while time.monotonic() < deadline:
|
||||
captures = [
|
||||
run(*tmux, "capture-pane", "-p", "-t", f"tamq:{repo}").stdout
|
||||
for repo in ("railiance-platform", "activity-core")
|
||||
]
|
||||
if all("installed-alpha-ready" in output for output in captures):
|
||||
break
|
||||
time.sleep(0.05)
|
||||
assert all("installed-alpha-ready" in output for output in captures)
|
||||
assert run(str(tamq), "stop").returncode == 0
|
||||
finally:
|
||||
subprocess.run(
|
||||
|
|
|
|||
37
tests/test_manual_messaging.py
Normal file
37
tests/test_manual_messaging.py
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import json
|
||||
|
||||
from tamq.cli import main
|
||||
|
||||
|
||||
async def service_is_down():
|
||||
return False
|
||||
|
||||
|
||||
def test_manual_send_inbox_and_ack_use_window_repository_identity(tmp_path, monkeypatch, capsys):
|
||||
monkeypatch.setenv("TAMQ_STATE_DIR", str(tmp_path / "state"))
|
||||
monkeypatch.setenv("TAMQ_REPO", "flex-auth")
|
||||
monkeypatch.setattr("tamq.cli.ping", service_is_down)
|
||||
monkeypatch.setattr("tamq.cli.validate_targets", lambda repos: None)
|
||||
|
||||
assert main(["send", "@audit-core:", "review", "this"]) == 0
|
||||
message_id = capsys.readouterr().out.strip()
|
||||
|
||||
assert main(["inbox", "--repo", "audit-core", "--json"]) == 0
|
||||
row = json.loads(capsys.readouterr().out)
|
||||
assert row["message_id"] == message_id
|
||||
assert row["sender_repo"] == "flex-auth"
|
||||
assert row["target_repo"] == "audit-core"
|
||||
assert row["body"] == "review this"
|
||||
assert row["state"] == "pending"
|
||||
|
||||
assert main(["ack", message_id]) == 0
|
||||
capsys.readouterr()
|
||||
assert main(["inbox", "--repo", "audit-core"]) == 0
|
||||
assert capsys.readouterr().out == ""
|
||||
|
||||
|
||||
def test_inbox_requires_repository_outside_managed_window(tmp_path, monkeypatch, capsys):
|
||||
monkeypatch.setenv("TAMQ_STATE_DIR", str(tmp_path / "state"))
|
||||
monkeypatch.delenv("TAMQ_REPO", raising=False)
|
||||
assert main(["inbox"]) == 2
|
||||
assert "requires --repo" in capsys.readouterr().err
|
||||
|
|
@ -67,7 +67,7 @@ def test_tap_propagates_terminal_size_resize_and_raw_mouse_input(tmp_path, monke
|
|||
pytest.fail(f"timed out waiting for {marker!r}; pane contained:\n{output}")
|
||||
|
||||
try:
|
||||
manager.ensure_plan(plan)
|
||||
manager.ensure_plan(plan, tap=True)
|
||||
output = wait_for("READY")
|
||||
dimensions = manager._run(
|
||||
"display-message",
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ class FakeControl:
|
|||
|
||||
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"])
|
||||
store.register_endpoint("tmux-amq-9-boot", 9, "tamq", ["repo-b"], "pane")
|
||||
message_id = store.add("repo-a", "repo-b", "continue")
|
||||
monkeypatch.setattr("tamq.service.ControlModeClient", FakeControl)
|
||||
Service(store=store)._deliver_once()
|
||||
|
|
@ -34,6 +34,18 @@ def test_service_delivery_injects_pending_messages(tmp_path, monkeypatch):
|
|||
assert store.list()[0]["state"] == "injected"
|
||||
|
||||
|
||||
def test_service_never_injects_manual_endpoint_messages(tmp_path, monkeypatch):
|
||||
store = Store(tmp_path / "queue.sqlite3")
|
||||
store.register_endpoint("tmux-amq-9-boot", 9, "tamq", ["repo-b"], "manual")
|
||||
store.add("repo-a", "repo-b", "continue")
|
||||
monkeypatch.setattr("tamq.service.ControlModeClient", FakeControl)
|
||||
|
||||
Service(store=store)._deliver_once()
|
||||
|
||||
assert FakeControl.instances[-1].injected == []
|
||||
assert store.list()[0]["state"] == "pending"
|
||||
|
||||
|
||||
def test_service_disconnects_disappeared_tmux_endpoint(tmp_path, monkeypatch):
|
||||
store = Store(tmp_path / "queue.sqlite3")
|
||||
store.register_endpoint("tmux-amq-9-boot", 9, "tamq", ["repo-b"])
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import json
|
||||
import sqlite3
|
||||
|
||||
from tamq.store import Store
|
||||
|
||||
|
|
@ -13,3 +14,32 @@ def test_message_history_and_jsonl(tmp_path):
|
|||
assert json.loads(output.read_text())["body"] == "hello"
|
||||
assert store.db.execute("PRAGMA journal_mode").fetchone()[0] == "wal"
|
||||
store.close()
|
||||
|
||||
|
||||
def test_store_migrates_legacy_endpoint_rows_to_manual_delivery(tmp_path):
|
||||
path = tmp_path / "legacy.sqlite3"
|
||||
db = sqlite3.connect(path)
|
||||
db.executescript(
|
||||
"""
|
||||
CREATE TABLE metadata (key TEXT PRIMARY KEY, value TEXT NOT NULL);
|
||||
INSERT INTO metadata VALUES('schema_version', '1');
|
||||
CREATE TABLE endpoints (
|
||||
endpoint_id TEXT PRIMARY KEY,
|
||||
pid INTEGER NOT NULL,
|
||||
session TEXT NOT NULL,
|
||||
repos TEXT NOT NULL,
|
||||
connected_at REAL NOT NULL,
|
||||
disconnected_at REAL
|
||||
);
|
||||
INSERT INTO endpoints VALUES('legacy', 1, 'tamq', '[\"repo\"]', 1, NULL);
|
||||
"""
|
||||
)
|
||||
db.commit()
|
||||
db.close()
|
||||
|
||||
store = Store(path)
|
||||
assert store.endpoints()[0]["delivery_mode"] == "manual"
|
||||
assert store.db.execute(
|
||||
"SELECT value FROM metadata WHERE key='schema_version'"
|
||||
).fetchone()[0] == "2"
|
||||
store.close()
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ def test_real_tmux_starts_two_repo_windows_and_reuses_them(tmp_path, monkeypatch
|
|||
)
|
||||
|
||||
try:
|
||||
endpoint = manager.ensure_plan(plan)
|
||||
endpoint = manager.ensure_plan(plan, tap=True)
|
||||
assert endpoint.created_session is True
|
||||
assert endpoint.created_windows == ("railiance-platform", "activity-core")
|
||||
|
||||
|
|
@ -65,7 +65,7 @@ def test_real_tmux_starts_two_repo_windows_and_reuses_them(tmp_path, monkeypatch
|
|||
time.sleep(0.05)
|
||||
assert all("alpha-ready" in capture for capture in captures)
|
||||
|
||||
reused = manager.ensure_plan(plan)
|
||||
reused = manager.ensure_plan(plan, tap=True)
|
||||
assert reused.created_session is False
|
||||
assert reused.created_windows == ()
|
||||
reused_rows = manager._run(
|
||||
|
|
|
|||
|
|
@ -21,7 +21,8 @@ def test_tmux_manager_builds_tap_windows(tmp_path, monkeypatch):
|
|||
return ""
|
||||
monkeypatch.setattr(manager, "_run", run)
|
||||
monkeypatch.setattr(tmux.subprocess, "run", lambda *args, **kwargs: type("R", (), {"returncode": 1})())
|
||||
endpoint = manager.ensure(["a", "b"], "codex --quiet")
|
||||
plan = manager.preflight(["a", "b"], "codex --quiet")
|
||||
endpoint = manager.ensure_plan(plan, tap=True)
|
||||
assert endpoint.endpoint_id == "tmux-amq-42"
|
||||
assert endpoint.instance_id.startswith("tmux-amq-42-")
|
||||
assert endpoint.repos == ["a", "b"]
|
||||
|
|
@ -118,6 +119,34 @@ def test_tmux_only_plan_starts_agent_without_tap(tmp_path, monkeypatch):
|
|||
assert "tamq tap" not in send
|
||||
|
||||
|
||||
def test_neutral_plan_starts_shell_without_sending_keystrokes(tmp_path, monkeypatch):
|
||||
manager = tmux.TmuxManager("tamq-test")
|
||||
calls = []
|
||||
plan = tmux.LaunchPlan(("a", "b"), {"a": str(tmp_path), "b": str(tmp_path)}, ())
|
||||
|
||||
def run(*args, check=True):
|
||||
calls.append(args)
|
||||
if args[:2] == ("display-message", "-p"):
|
||||
return "42"
|
||||
if args[:2] == ("list-windows", "-t"):
|
||||
return "__tamq_boot"
|
||||
return ""
|
||||
|
||||
monkeypatch.setattr(manager, "_run", run)
|
||||
monkeypatch.setattr(
|
||||
tmux.subprocess,
|
||||
"run",
|
||||
lambda *args, **kwargs: type("R", (), {"returncode": 1})(),
|
||||
)
|
||||
manager.ensure_plan(plan)
|
||||
|
||||
assert not any(call and call[0] == "send-keys" for call in calls)
|
||||
new_session = next(call for call in calls if call and call[0] == "new-session")
|
||||
new_window = next(call for call in calls if call and call[0] == "new-window")
|
||||
assert "TAMQ_REPO=a" in new_session
|
||||
assert "TAMQ_REPO=b" in new_window
|
||||
|
||||
|
||||
def test_preflight_rejects_missing_agent(tmp_path, monkeypatch):
|
||||
repo = tmp_path / "a"
|
||||
repo.mkdir()
|
||||
|
|
@ -125,7 +154,7 @@ def test_preflight_rejects_missing_agent(tmp_path, monkeypatch):
|
|||
monkeypatch.setattr(tmux, "validate_targets", lambda repos: None)
|
||||
monkeypatch.setattr(tmux, "repository_paths", lambda: {"a": str(repo)})
|
||||
monkeypatch.setattr(tmux.shutil, "which", lambda command: None if command == "missing-agent" else f"/bin/{command}")
|
||||
with pytest.raises(tmux.TmuxError, match="agent command not found"):
|
||||
with pytest.raises(tmux.TmuxError, match="initial command not found"):
|
||||
manager.preflight(["a"], "missing-agent")
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue