feat: add conversational replies and stable output
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 23:03:30 +02:00
parent 704eeea5c1
commit a4ba6e32e5
17 changed files with 363 additions and 59 deletions

View file

@ -18,12 +18,12 @@ def test_pane_tty_is_resolved_through_tmux(monkeypatch):
def run(command, **kwargs):
calls.append(command)
return type("Result", (), {"returncode": 0, "stdout": "/dev/pts/7\n", "stderr": ""})()
return type("Result", (), {"returncode": 0, "stdout": "/dev/pts/7|12|8|24|0\n", "stderr": ""})()
monkeypatch.setattr(control.subprocess, "run", run)
client = ControlModeClient("tamq", tmux_command=("tmux", "-L", "test"))
assert client.pane_tty("tamq:audit-core") == "/dev/pts/7"
assert calls == [[
"tmux", "-L", "test", "display-message", "-p", "-t",
"tamq:audit-core", "#{pane_tty}",
"tamq:audit-core", "#{pane_tty}|#{cursor_x}|#{cursor_y}|#{pane_height}|#{alternate_on}",
]]

View file

@ -32,6 +32,43 @@ def test_manual_send_inbox_and_ack_use_window_repository_identity(tmp_path, monk
assert capsys.readouterr().out == ""
def test_bare_reply_targets_latest_inbound_sender(tmp_path, monkeypatch, capsys):
monkeypatch.setenv("TAMQ_STATE_DIR", str(tmp_path / "state"))
monkeypatch.setenv("TAMQ_REPO", "audit-core")
monkeypatch.setattr("tamq.cli.ping", service_is_down)
monkeypatch.setattr("tamq.cli.validate_targets", lambda repos: None)
store = Store(tmp_path / "state" / "tamq.sqlite3")
store.add("flex-auth", "audit-core", "older")
latest = store.add("railiance-platform", "audit-core", "latest")
store.acknowledge(latest)
store.add("audit-core", "audit-core", "self note")
store.close()
assert main(["reply", "--", "--looks-like-an-option", "thanks"]) == 0
message_id = capsys.readouterr().out.strip()
store = Store(tmp_path / "state" / "tamq.sqlite3")
row = next(row for row in store.list() if row["message_id"] == message_id)
assert row["sender_repo"] == "audit-core"
assert row["target_repo"] == "railiance-platform"
assert row["body"] == "--looks-like-an-option thanks"
store.close()
def test_bare_reply_requires_managed_identity_and_prior_sender(tmp_path, monkeypatch, capsys):
monkeypatch.setenv("TAMQ_STATE_DIR", str(tmp_path / "state"))
monkeypatch.setattr("tamq.cli.ping", service_is_down)
monkeypatch.setattr("tamq.cli.validate_targets", lambda repos: None)
monkeypatch.delenv("TAMQ_REPO", raising=False)
assert main(["reply", "hello"]) == 2
assert "managed window" in capsys.readouterr().err
monkeypatch.setenv("TAMQ_REPO", "audit-core")
assert main(["reply", "hello"]) == 2
assert "no counterparty" in capsys.readouterr().err
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)

View file

@ -1,3 +1,4 @@
from tamq.control import PaneDisplay
from tamq.service import Service
from tamq.store import Store
@ -19,8 +20,14 @@ class FakeControl:
def inject(self, window, text):
self.injected.append((window, text))
def pane_tty(self, window):
return f"/dev/pts/{window.rsplit(':', 1)[-1]}"
def pane_display(self, window):
return PaneDisplay(
tty_path=f"/dev/pts/{window.rsplit(':', 1)[-1]}",
cursor_x=0,
cursor_y=0,
pane_height=24,
alternate_on=False,
)
def close(self):
return None

View file

@ -59,3 +59,15 @@ def test_mark_displayed_releases_lease_without_acknowledging(tmp_path):
assert row["displayed_at"] is not None
assert store.db.execute("SELECT COUNT(*) FROM leases").fetchone()[0] == 0
store.close()
def test_latest_counterparty_uses_latest_inbound_message_regardless_of_state(tmp_path):
store = Store(tmp_path / "queue.sqlite3")
store.add("audit-core", "audit-core", "self note")
older = store.add("flex-auth", "audit-core", "first")
store.acknowledge(older)
store.add("railiance-platform", "audit-core", "latest")
assert store.latest_counterparty("audit-core") == "railiance-platform"
assert store.latest_counterparty("unknown") is None
store.close()

View file

@ -16,6 +16,24 @@ def test_comment_format_escapes_controls_and_prefixes_every_line():
assert format_comment("repo-a", "first\nsecond\x1b[31m", "m-1") == (
"#repo-a: first\n# second\\x1b[31m [m-1]"
)
def test_terminal_frame_scrolls_only_rows_above_the_cursor():
assert terminal_frame(
"repo-a", "first\nsecond", "m-1", cursor_y=8, pane_height=24
) == (
"\x1b7\x1b[1;8r\x1b[8;1H"
"\n\r#repo-a: first\n\r# second [m-1]"
"\x1b[r\x1b8"
)
def test_terminal_frame_falls_back_when_stable_region_is_not_safe():
expected = "\r\n#repo-a: hello [m-1]\r\n"
assert terminal_frame("repo-a", "hello", "m-1", cursor_y=0, pane_height=24) == expected
assert terminal_frame(
"repo-a", "hello", "m-1", cursor_y=8, pane_height=24, alternate_on=True
) == expected
assert terminal_frame("repo-a", "hello", "m-1") == (
"\r\n#repo-a: hello [m-1]\r\n"
)

View file

@ -10,6 +10,7 @@ import pytest
from tamq.broker import BrokerIdentity, InputBroker
from tamq.control import ControlModeClient
from tamq.store import Store
from tamq.terminal import terminal_frame, write_terminal_output
from tamq.tmux import LaunchPlan, TmuxManager
@ -112,3 +113,88 @@ def test_real_tmux_starts_two_repo_windows_and_reuses_them(tmp_path, monkeypatch
store.close()
finally:
manager._run("kill-server", check=False)
@pytest.mark.skipif(shutil.which("tmux") is None, reason="tmux is not installed")
def test_real_tmux_output_preserves_partial_input_line_and_cursor(tmp_path):
repo = tmp_path / "audit-core"
repo.mkdir()
socket_name = f"tamq-output-{os.getpid()}-{uuid4().hex[:8]}"
session = f"tamq-output-{uuid4().hex[:8]}"
manager = TmuxManager(
session,
tmux_command=("tmux", "-L", socket_name),
tamq_command=(sys.executable, "-m", "tamq.cli"),
command_dir=tmp_path / "commands",
)
target = f"{session}:audit-core"
plan = LaunchPlan(("audit-core",), {"audit-core": str(repo)}, ())
try:
manager.ensure_plan(plan)
deadline = time.monotonic() + 5
while time.monotonic() < deadline:
current_command = manager._run(
"display-message", "-p", "-t", target, "#{pane_current_command}"
)
if current_command in {"bash", "sh", "zsh", "fish"}:
break
time.sleep(0.05)
assert current_command in {"bash", "sh", "zsh", "fish"}
manager._run(
"send-keys", "-t", target, "-l", "--", "printf 'line-one\\nline-two\\n'"
)
manager._run("send-keys", "-t", target, "Enter")
deadline = time.monotonic() + 5
while time.monotonic() < deadline:
initial_capture = manager._run("capture-pane", "-p", "-t", target)
initial_lines = initial_capture.splitlines()
if "line-one" in initial_lines and "line-two" in initial_lines:
break
time.sleep(0.05)
assert "line-one" in initial_lines and "line-two" in initial_lines
manager._run("send-keys", "-t", target, "-l", "--", "PARTIAL-INPUT")
deadline = time.monotonic() + 5
while time.monotonic() < deadline:
before_capture = manager._run("capture-pane", "-p", "-t", target)
if "PARTIAL-INPUT" in before_capture:
break
time.sleep(0.05)
assert "PARTIAL-INPUT" in before_capture
control = ControlModeClient(
session,
tmux_command=("tmux", "-L", socket_name),
)
before = control.pane_display(target)
write_terminal_output(
before.tty_path,
terminal_frame(
"flex-auth",
"stable-message",
"m-stable",
cursor_y=before.cursor_y,
pane_height=before.pane_height,
alternate_on=before.alternate_on,
),
)
deadline = time.monotonic() + 5
while time.monotonic() < deadline:
after_capture = manager._run("capture-pane", "-p", "-t", target)
if "#flex-auth: stable-message [m-stable]" in after_capture:
break
time.sleep(0.05)
after = control.pane_display(target)
assert "#flex-auth: stable-message [m-stable]" in after_capture
before_input = next(line for line in before_capture.splitlines() if "PARTIAL-INPUT" in line)
after_lines = after_capture.splitlines()
assert any("PARTIAL-INPUT" in line for line in after_lines), repr(after_capture)
after_input_index = next(
index for index, line in enumerate(after_lines) if "PARTIAL-INPUT" in line
)
assert after_lines[after_input_index] == before_input
assert after_lines[after_input_index - 1] == "#flex-auth: stable-message [m-stable]"
assert (after.cursor_x, after.cursor_y) == (before.cursor_x, before.cursor_y)
finally:
manager._run("kill-server", check=False)

View file

@ -155,6 +155,7 @@ def test_neutral_plan_starts_shell_without_sending_keystrokes(tmp_path, monkeypa
assert (command_dir / "@a:").is_file()
assert (command_dir / "@b").is_file()
assert (command_dir / "@b:").is_file()
assert (command_dir / "@").is_file()
def test_address_commands_preserve_message_arguments(tmp_path):
@ -173,6 +174,14 @@ def test_address_commands_preserve_message_arguments(tmp_path):
)
assert result.stdout == "send -- @audit-core: Some message! $value ; literal --from\n"
result = subprocess.run(
[str(command_dir / "@"), "Some reply!", "$value", "--literal"],
text=True,
capture_output=True,
check=True,
)
assert result.stdout == "reply -- Some reply! $value --literal\n"
def test_address_commands_reject_unsafe_repository_names(tmp_path):
manager = tmux.TmuxManager("tamq-test", command_dir=tmp_path / "commands")
@ -192,6 +201,18 @@ def test_address_commands_do_not_replace_unowned_commands(tmp_path):
assert existing.read_text(encoding="utf-8") == "#!/bin/sh\necho mine\n"
def test_address_commands_do_not_replace_unowned_bare_reply(tmp_path):
command_dir = tmp_path / "commands"
command_dir.mkdir()
existing = command_dir / "@"
existing.write_text("#!/bin/sh\necho mine\n", encoding="utf-8")
manager = tmux.TmuxManager("tamq-test", command_dir=command_dir)
with pytest.raises(tmux.TmuxError, match="refusing to replace"):
manager._install_address_commands(["audit-core"])
assert existing.read_text(encoding="utf-8") == "#!/bin/sh\necho mine\n"
def test_preflight_rejects_missing_agent(tmp_path, monkeypatch):
repo = tmp_path / "a"
repo.mkdir()