tmux-amq/tests/test_tmux_integration.py
tegwick 17b6bd7ae7
Some checks failed
tamq-ci / test (push) Failing after 6s
feat: add reliable trigger and worker message blocks
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a03397-4d51-7fd1-8ff2-946eb22ea2bc
2026-08-25 23:22:39 +02:00

461 lines
17 KiB
Python

import os
import shutil
import sys
import time
from pathlib import Path
from uuid import uuid4
import pytest
from tamq.broker import BrokerIdentity, InputBroker
from tamq.control import ControlModeClient
from tamq.service import Service
from tamq.store import Store
from tamq.terminal import terminal_frame, write_terminal_output
from tamq.tmux import LaunchPlan, TmuxManager
@pytest.mark.skipif(shutil.which("tmux") is None, reason="tmux is not installed")
def test_real_tmux_starts_two_repo_windows_and_reuses_them(tmp_path, monkeypatch):
repo_a = tmp_path / "railiance-platform"
repo_b = tmp_path / "activity-core"
repo_a.mkdir()
repo_b.mkdir()
project_src = str(Path(__file__).resolve().parents[1] / "src")
existing_pythonpath = os.environ.get("PYTHONPATH")
monkeypatch.setenv("PYTHONPATH", project_src if not existing_pythonpath else f"{project_src}:{existing_pythonpath}")
monkeypatch.setenv("TAMQ_STATE_DIR", str(tmp_path / "state"))
socket_name = f"tamq-pytest-{os.getpid()}-{uuid4().hex[:8]}"
session = f"tamq-test-{uuid4().hex[:8]}"
manager = TmuxManager(
session,
tmux_command=("tmux", "-L", socket_name),
tamq_command=(sys.executable, "-m", "tamq.cli"),
command_dir=tmp_path / "commands",
)
plan = LaunchPlan(
("railiance-platform", "activity-core"),
{"railiance-platform": str(repo_a), "activity-core": str(repo_b)},
("sh", "-c", "printf 'alpha-ready\\n'; exec sleep 30"),
)
try:
endpoint = manager.ensure_plan(plan, tap=True)
assert endpoint.created_session is True
assert endpoint.created_windows == ("railiance-platform", "activity-core")
rows = manager._run(
"list-windows",
"-t",
session,
"-F",
"#{window_name}|#{pane_current_path}|#{pane_pid}",
).splitlines()
parsed = {name: (path, pid) for name, path, pid in (row.split("|", 2) for row in rows)}
assert set(parsed) == {"railiance-platform", "activity-core"}
assert parsed["railiance-platform"][0] == str(repo_a)
assert parsed["activity-core"][0] == str(repo_b)
deadline = time.monotonic() + 5
while time.monotonic() < deadline:
captures = [
manager._run("capture-pane", "-p", "-t", f"{session}:{repo}")
for repo in plan.repos
]
if all("alpha-ready" in capture for capture in captures):
break
time.sleep(0.05)
assert all("alpha-ready" in capture for capture in captures)
reused = manager.ensure_plan(plan, tap=True)
assert reused.created_session is False
assert reused.created_windows == ()
reused_rows = manager._run(
"list-windows",
"-t",
session,
"-F",
"#{window_name}|#{pane_current_path}|#{pane_pid}",
).splitlines()
reused_parsed = {name: (path, pid) for name, path, pid in (row.split("|", 2) for row in reused_rows)}
assert reused_parsed == parsed
assert reused.instance_id == endpoint.instance_id
store = Store(tmp_path / "state" / "tamq.sqlite3")
control = ControlModeClient(
session,
tmux_command=("tmux", "-L", socket_name),
)
try:
message_id = store.add("local", "activity-core", "integration-message")
control.start()
delivered = InputBroker(
store,
BrokerIdentity(endpoint.instance_id, "railiance-platform"),
).deliver_pending(
control,
window_for_repo=lambda repo: f"{session}:{repo}",
)
assert delivered == 1
assert store.list(state="injected")[0]["message_id"] == message_id
deadline = time.monotonic() + 5
while time.monotonic() < deadline:
capture = manager._run(
"capture-pane", "-p", "-t", f"{session}:activity-core"
)
if "From:local: integration-message" in capture:
break
time.sleep(0.05)
assert "From:local: integration-message" in capture
finally:
control.close()
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",
"worker_output",
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 "From:flex-auth: stable-message" in after_capture:
break
time.sleep(0.05)
after = control.pane_display(target)
assert "From:flex-auth: stable-message" 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] == "From:flex-auth: stable-message"
assert (after.cursor_x, after.cursor_y) == (before.cursor_x, before.cursor_y)
finally:
manager._run("kill-server", check=False)
@pytest.mark.skipif(shutil.which("tmux") is None, reason="tmux is not installed")
def test_real_tmux_pushy_mode_places_one_readable_input_without_enter(tmp_path, monkeypatch):
repo = tmp_path / "audit-core"
repo.mkdir()
socket_name = f"tamq-pushy-{os.getpid()}-{uuid4().hex[:8]}"
session = f"tamq-pushy-{uuid4().hex[:8]}"
monkeypatch.setenv("TAMQ_TMUX_SOCKET", socket_name)
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"
store = Store(tmp_path / "queue.sqlite3")
try:
endpoint = manager.ensure_plan(
LaunchPlan(("audit-core",), {"audit-core": str(repo)}, ())
)
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"}
store.register_endpoint(
endpoint.instance_key,
endpoint.pid,
endpoint.session,
endpoint.repos,
"pushy",
)
store.add("flex-auth", "audit-core", "What's next?\nsecond", provenance="operator_input")
service = Service(store=store, input_grace=0)
service._deliver_once()
expected_lines = ["From:flex-auth/o: What's next?", "From:flex-auth/o: second"]
deadline = time.monotonic() + 5
while time.monotonic() < deadline:
capture = manager._run("capture-pane", "-p", "-J", "-t", target)
if all(expected in capture for expected in expected_lines):
break
time.sleep(0.05)
assert all(expected in capture for expected in expected_lines)
assert store.list()[0]["state"] == "injected"
assert store.db.execute("SELECT COUNT(*) FROM leases").fetchone()[0] == 0
service._deliver_once()
repeated_capture = manager._run("capture-pane", "-p", "-J", "-t", target)
assert all(repeated_capture.count(expected) == 1 for expected in expected_lines)
finally:
store.close()
manager._run("kill-server", check=False)
@pytest.mark.skipif(shutil.which("tmux") is None, reason="tmux is not installed")
def test_real_tmux_trigger_clears_terminal_paste_window_before_enter(tmp_path):
repo = tmp_path / "audit-core"
repo.mkdir()
fixture = tmp_path / "paste_sensitive_fixture.py"
fixture.write_text(
"import os, time, tty\n"
"tty.setraw(0)\n"
"os.write(1, b'READY\\r\\n')\n"
"body = bytearray()\n"
"last_text = 0.0\n"
"while True:\n"
" data = os.read(0, 4096)\n"
" now = time.monotonic()\n"
" for byte in data:\n"
" if byte == 13:\n"
" label = b'SUBMITTED:' if now - last_text >= 0.1 else b'NEWLINE:'\n"
" os.write(1, label + bytes(body) + b'\\r\\n')\n"
" raise SystemExit(0)\n"
" body.append(byte)\n"
" last_text = now\n",
encoding="utf-8",
)
socket_name = f"tamq-trigger-paste-{uuid4().hex[:8]}"
session = f"tamq-trigger-paste-{uuid4().hex[:8]}"
tmux_command = ("tmux", "-L", socket_name)
manager = TmuxManager(
session,
tmux_command=tmux_command,
tamq_command=(sys.executable, "-m", "tamq.cli"),
command_dir=tmp_path / "commands",
)
try:
manager.ensure_plan(
LaunchPlan(
("audit-core",),
{"audit-core": str(repo)},
(sys.executable, str(fixture)),
)
)
target = f"{session}:audit-core"
deadline = time.monotonic() + 5
while time.monotonic() < deadline:
capture = manager._run("capture-pane", "-p", "-t", target)
if "READY" in capture:
break
time.sleep(0.05)
assert "READY" in capture
ControlModeClient(
session, tmux_command=tmux_command, submit_delay=0.15
).submit(target, "From:flex-auth: first delivery")
deadline = time.monotonic() + 5
while time.monotonic() < deadline:
capture = manager._run("capture-pane", "-p", "-t", target)
if "SUBMITTED:" in capture:
break
time.sleep(0.05)
assert "SUBMITTED:From:flex-auth: first delivery" in capture
assert "NEWLINE:" not in capture
finally:
manager._run("kill-server", check=False)
@pytest.mark.skipif(shutil.which("tmux") is None, reason="tmux is not installed")
def test_real_tmux_to_route_triggers_once_without_feedback(tmp_path, monkeypatch):
repo_a = tmp_path / "railiance-platform"
repo_b = tmp_path / "activity-core"
bin_dir = tmp_path / "bin"
for path in (repo_a, repo_b, bin_dir):
path.mkdir()
gita = bin_dir / "gita"
gita.write_text(
"#!/bin/sh\n"
"if [ \"$1\" = ls ]; then printf 'railiance-platform activity-core\\n'; exit 0; fi\n"
"exit 2\n",
encoding="utf-8",
)
gita.chmod(0o755)
fixture = tmp_path / "agent_fixture.py"
fixture.write_text(
"import sys\n"
"print('AGENT-READY', flush=True)\n"
"for line in sys.stdin:\n"
" print('AGENT:' + line.rstrip('\\r\\n'), flush=True)\n",
encoding="utf-8",
)
project_src = str(Path(__file__).resolve().parents[1] / "src")
existing_pythonpath = os.environ.get("PYTHONPATH")
monkeypatch.setenv(
"PYTHONPATH",
project_src if not existing_pythonpath else f"{project_src}:{existing_pythonpath}",
)
monkeypatch.setenv("PATH", f"{bin_dir}:{os.environ['PATH']}")
monkeypatch.setenv("TAMQ_STATE_DIR", str(tmp_path / "state"))
socket_name = f"tamq-hash-{os.getpid()}-{uuid4().hex[:8]}"
session = f"tamq-hash-{uuid4().hex[:8]}"
monkeypatch.setenv("TAMQ_TMUX_SOCKET", socket_name)
manager = TmuxManager(
session,
tmux_command=("tmux", "-L", socket_name),
tamq_command=(sys.executable, "-m", "tamq.cli"),
command_dir=tmp_path / "commands",
)
plan = LaunchPlan(
("railiance-platform", "activity-core"),
{
"railiance-platform": str(repo_a),
"activity-core": str(repo_b),
},
(sys.executable, str(fixture)),
)
store = None
try:
endpoint = manager.ensure_plan(plan, tap=True)
deadline = time.monotonic() + 5
while time.monotonic() < deadline:
captures = {
repo: manager._run("capture-pane", "-p", "-t", f"{session}:{repo}")
for repo in plan.repos
}
if all("AGENT-READY" in output for output in captures.values()):
break
time.sleep(0.05)
assert all("AGENT-READY" in output for output in captures.values())
store = Store(tmp_path / "state" / "tamq.sqlite3")
store.register_endpoint(
endpoint.instance_key,
endpoint.pid,
endpoint.session,
endpoint.repos,
"trigger",
)
manager._run(
"send-keys",
"-t",
f"{session}:railiance-platform",
"-l",
"--",
"To:activity-core: Hello!",
)
manager._run("send-keys", "-t", f"{session}:railiance-platform", "Enter")
deadline = time.monotonic() + 5
while time.monotonic() < deadline:
rows = store.list()
if rows:
break
time.sleep(0.05)
assert len(rows) == 1
row = rows[0]
assert row["sender_repo"] == "railiance-platform"
assert row["target_repo"] == "activity-core"
assert row["body"] == "Hello!"
assert row["endpoint_id"] == endpoint.instance_key
service = Service(store=store, input_grace=0)
service._deliver_once()
expected = "From:railiance-platform/o: Hello!"
deadline = time.monotonic() + 5
while time.monotonic() < deadline:
target_capture = manager._run(
"capture-pane", "-p", "-J", "-t", f"{session}:activity-core"
)
if f"AGENT:{expected}" in target_capture:
break
time.sleep(0.05)
assert f"AGENT:{expected}" in target_capture
assert store.list()[0]["state"] == "injected"
legacy_envelope = "#railiance-platform: Hello!"
manager._run(
"send-keys",
"-t",
f"{session}:activity-core",
"-l",
"--",
legacy_envelope,
)
manager._run(
"send-keys", "-t", f"{session}:activity-core", "Enter"
)
time.sleep(0.1)
assert len(store.list()) == 1
service._deliver_once()
time.sleep(0.1)
assert len(store.list()) == 1
assert manager._run(
"capture-pane", "-p", "-J", "-t", f"{session}:activity-core"
).count(f"AGENT:{expected}") == 1
finally:
if store is not None:
store.close()
manager._run("kill-server", check=False)