feat: make sessions terminal neutral
Some checks failed
tamq-ci / test (push) Failing after 6s

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a03397-4d51-7fd1-8ff2-946eb22ea2bc
This commit is contained in:
tegwick 2026-08-24 20:10:08 +02:00
parent 408d32df88
commit 74b2f27997
20 changed files with 608 additions and 182 deletions

View file

@ -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

View file

@ -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]

View file

@ -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(

View 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

View file

@ -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",

View file

@ -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"])

View file

@ -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()

View file

@ -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(

View file

@ -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")