mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-06 06:52:20 +02:00
135 lines
6.4 KiB
Python
135 lines
6.4 KiB
Python
"""Native Bash does not resurrect tmux; legacy cleanup requires identity."""
|
|
import asyncio
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
from src import containment, process_ownership, process_reaper, tool_execution
|
|
from src.agent_tools import subprocess_tools
|
|
from src.constants import DATA_DIR
|
|
from tests.containment_helpers import capture_owned_spawn
|
|
|
|
|
|
async def test_a_chat_session_always_uses_the_owned_runner(monkeypatch, tmp_path):
|
|
captured = capture_owned_spawn(monkeypatch, tmp_path)
|
|
monkeypatch.setattr(tool_execution, "agent_cwd", lambda: str(tmp_path))
|
|
original = subprocess_tools.shutil.which
|
|
monkeypatch.setattr(subprocess_tools.shutil, "which", lambda name: "/fake/tmux" if name == "tmux" else original(name))
|
|
async def forbidden(*args, **kwargs):
|
|
pytest.fail("native Bash resurrected a persistent tmux shell")
|
|
monkeypatch.setattr(subprocess_tools.asyncio, "create_subprocess_shell", forbidden)
|
|
result = await subprocess_tools.BashTool().execute("printf ok", {"session_id": "same-chat"})
|
|
assert result["output"] == "ok"
|
|
assert result["teardown"]["dead"] is True
|
|
assert "tmux_session" not in result
|
|
assert captured["kwargs"].get("start_new_session") is True
|
|
|
|
|
|
@pytest.fixture
|
|
def legacy(monkeypatch, tmp_path):
|
|
import shlex
|
|
import subprocess
|
|
import shutil
|
|
monkeypatch.setattr(containment, "_store_path", lambda: tmp_path / "grants.json")
|
|
monkeypatch.setattr(shutil, "which", lambda name: "/usr/bin/tmux" if name == "tmux" else None)
|
|
launcher = f"env HOME={shlex.quote(DATA_DIR)} TERM=xterm-256color /bin/bash --noprofile --norc"
|
|
row = f"$8\tody-agent-chat\t1234\t4200\t%9\t4100\t{launcher}\n"
|
|
state = {"rows": row, "calls": [], "released": []}
|
|
def run(argv, **kwargs):
|
|
state["calls"].append(argv)
|
|
if argv[1] == "list-panes":
|
|
return SimpleNamespace(returncode=0, stdout=state["rows"], stderr="")
|
|
if argv[1] == "kill-session":
|
|
state["rows"] = ""
|
|
return SimpleNamespace(returncode=0, stdout="", stderr="")
|
|
monkeypatch.setattr(subprocess, "run", run)
|
|
monkeypatch.setattr(process_ownership, "process_table", lambda: {
|
|
4200: process_ownership.ProcessInfo(4200, 4100, "/bin/bash --noprofile --norc"),
|
|
4201: process_ownership.ProcessInfo(4201, 4200, "sleep 60"),
|
|
})
|
|
monkeypatch.setattr(process_ownership, "start_token", lambda pid: f"token:{pid}")
|
|
monkeypatch.setattr(process_ownership, "verify", lambda pid, token: process_ownership.OWNED)
|
|
monkeypatch.setattr(containment, "_pgid_of", lambda pid: pid)
|
|
def release(grant, **kwargs):
|
|
state["released"].append((grant.pid, kwargs))
|
|
return containment.ReleaseOutcome(dead=True, escalated=False)
|
|
monkeypatch.setattr(containment, "release", release)
|
|
return state
|
|
|
|
|
|
def test_legacy_cleanup_checks_home_and_identity_and_kills_children_first(legacy):
|
|
report = process_reaper.reap_legacy_agent_tmux()
|
|
assert report["torn_down"] == 1
|
|
assert [pid for pid, _ in legacy["released"]] == [4201, 4200]
|
|
assert all(options["require_identity"] for _, options in legacy["released"])
|
|
assert legacy["calls"][-2][1:] == ["kill-session", "-t", "$8"]
|
|
|
|
|
|
def test_a_name_prefix_alone_never_authorizes_cleanup(legacy):
|
|
legacy["rows"] = "$8\tody-agent-chat\t1234\t4200\t%9\t4100\t/bin/bash\n"
|
|
report = process_reaper.reap_legacy_agent_tmux()
|
|
assert report["unverifiable"] == 1
|
|
assert legacy["released"] == []
|
|
assert all(call[1] != "kill-session" for call in legacy["calls"])
|
|
|
|
|
|
def test_a_recycled_pane_pid_is_never_signalled(legacy, monkeypatch):
|
|
monkeypatch.setattr(process_ownership, "verify", lambda pid, token: process_ownership.FOREIGN if pid == 4200 else process_ownership.OWNED)
|
|
report = process_reaper.reap_legacy_agent_tmux()
|
|
assert report["unverifiable"] == 1
|
|
assert legacy["released"] == []
|
|
|
|
|
|
def test_a_stale_pane_pid_pointing_at_another_parent_is_not_signalled(legacy, monkeypatch):
|
|
monkeypatch.setattr(process_ownership, "process_table", lambda: {
|
|
4200: process_ownership.ProcessInfo(4200, 9999, "/bin/bash --noprofile --norc"),
|
|
})
|
|
assert process_reaper.reap_legacy_agent_tmux()["unverifiable"] == 1
|
|
assert legacy["released"] == []
|
|
|
|
|
|
def test_a_session_changed_during_discovery_is_never_killed(legacy, monkeypatch):
|
|
original = process_ownership.process_table
|
|
def table():
|
|
legacy["rows"] = legacy["rows"].replace("1234", "5678")
|
|
return original()
|
|
monkeypatch.setattr(process_ownership, "process_table", table)
|
|
report = process_reaper.reap_legacy_agent_tmux()
|
|
assert report["unverifiable"] == 1
|
|
assert legacy["released"] == []
|
|
|
|
|
|
def test_startup_reaper_does_not_kill_a_current_runtime_grant(tmp_path, monkeypatch):
|
|
monkeypatch.setattr(containment, "_store_path", lambda: tmp_path / "grants.json")
|
|
monkeypatch.setattr(containment, "CONTAINMENT_MODE", containment.MODE_REPORT_ONLY)
|
|
grant = containment.acquire(containment.agent_spec(str(tmp_path), {}, 1), owner="active-chat")
|
|
monkeypatch.setattr(containment, "reap_record", lambda record: pytest.fail("startup killed current execution"))
|
|
assert process_reaper.reap_containment_grants()["manager_kept"] == 1
|
|
containment.release(grant)
|
|
|
|
|
|
def test_legacy_cleanup_against_a_private_real_tmux_server(tmp_path, monkeypatch):
|
|
import os
|
|
import shlex
|
|
import shutil
|
|
import subprocess
|
|
real_tmux = shutil.which("tmux")
|
|
if os.name == "nt" or not real_tmux:
|
|
pytest.skip("requires POSIX tmux")
|
|
socket = str(tmp_path / "tmux.sock")
|
|
wrapper = tmp_path / "tmux"
|
|
wrapper.write_text(f"#!/bin/sh\nexec {shlex.quote(real_tmux)} -S {shlex.quote(socket)} \"$@\"\n")
|
|
wrapper.chmod(0o700)
|
|
subprocess.run([real_tmux, "-S", socket, "-f", "/dev/null", "new-session", "-d", "-s", "ody-agent-real",
|
|
"env", f"HOME={DATA_DIR}", "TERM=xterm-256color", "/bin/bash", "--noprofile", "--norc"], check=True)
|
|
original = shutil.which
|
|
monkeypatch.setattr(shutil, "which", lambda name: str(wrapper) if name == "tmux" else original(name))
|
|
monkeypatch.setattr(containment, "_store_path", lambda: tmp_path / "grants.json")
|
|
try:
|
|
report = process_reaper.reap_legacy_agent_tmux()
|
|
assert report["torn_down"] == 1, report
|
|
assert subprocess.run([real_tmux, "-S", socket, "has-session", "-t", "ody-agent-real"], capture_output=True).returncode != 0
|
|
assert containment.active_grants() == []
|
|
finally:
|
|
subprocess.run([real_tmux, "-S", socket, "kill-server"], capture_output=True)
|