Files
plane-agentd/tests/test_posix_remote.py

222 lines
11 KiB
Python

"""Remote POSIX transport (warpgate-ssh, os linux/darwin): the generated bash is executed by a local bash in
place of the relay, so quoting, chunked uploads, detached launch and process-group kill are exercised for real."""
import base64
import os
import shutil
import subprocess
import tempfile
import time
import unittest
from pathlib import Path
from unittest import mock
from agentd import ROOT, config
from agentd.config import Config
from agentd.executors import Run, make_executor
from agentd.transports import make_transport
from agentd.transports.warpgate_ssh import PosixWarpgateTransport, relay_argv
from tests.fakes import config_data
FAKE_CLAUDE = """#!/usr/bin/env bash
prompt="$(cat)"
echo "args: $*" >&2
printf '{"type":"result","subtype":"success","is_error":false,"result":"[review] got %s","session_id":"sess-7"}' "${#prompt}"
"""
def remote_data(root):
d = config_data()
d["vault"] = {"client": "/opt/vault/agent-access.py", "config": "https://vault.example/identity.json"}
d["terminals"]["mac"] = {"transport": "warpgate-ssh", "target": "mac-target", "os": "linux", "launch": "nohup",
"repo": str(root / "my repo's"), "run_root": str(root / "run root"), "plane_user": "oregon",
"token_key": "TOK_MAC", "concurrency": 1, "executors": ["claude-code", "shell"],
"bins": {"claude": str(root / "bin dir" / "claude")}}
return d
class LocalBash(PosixWarpgateTransport):
"""The relay replaced by `bash -c` on this machine; records every script (one entry = one relay call)."""
def __init__(self, cfg, terminal):
super().__init__(cfg, terminal)
self.calls = []
def sh(self, script, timeout=150, marker=None):
self.calls.append(script)
out = subprocess.run(["bash", "-c", script], capture_output=True, text=True, timeout=timeout,
env=self.cfg.scrubbed_env())
if marker and marker not in out.stdout:
raise AssertionError("marker %s missing: %s %s" % (marker, out.stdout, out.stderr))
return out.stdout
def wait(ex, h, seconds=10):
end = time.time() + seconds
while time.time() < end:
if ex.poll(h) == "finished":
return True
time.sleep(0.1)
return False
class PosixRemoteTest(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.root = Path(self.tmp.name)
(self.root / "my repo's").mkdir()
(self.root / "bin dir").mkdir()
fake = self.root / "bin dir" / "claude"
fake.write_text(FAKE_CLAUDE)
fake.chmod(0o755)
self.cfg = Config(remote_data(self.root))
self.t = LocalBash(self.cfg, "mac")
def tearDown(self):
self.tmp.cleanup()
def test_factory_picks_posix_for_linux_and_config_is_valid(self):
self.assertEqual(config.validate(remote_data(self.root)), [])
self.assertIsInstance(make_transport(self.cfg, "mac"), PosixWarpgateTransport)
self.assertEqual(make_transport(self.cfg, "win10").os, "windows")
def test_live_config_runs_oregon_remotely(self):
cfg = config.load(ROOT / "config" / "agentd.yaml")
t = make_transport(cfg, "oregon") # PAD-12: direct-ssh over Tailscale, Warpgate relay as fallback
self.assertIsInstance(t.primary, PosixWarpgateTransport)
self.assertIsInstance(t.fallback, PosixWarpgateTransport)
self.assertEqual((t.primary.channel.stdin, t.fallback.channel.stdin), (True, False))
self.assertEqual((t.fallback.target, t.os), ("oregon", "linux"))
self.assertEqual(cfg.executors["codex"]["tier_defaults"], {"gpt-6.1-sol": "fast"})
def test_relay_argv_and_scrubbed_env(self):
argv = relay_argv(self.cfg, "mac-target", ["bash", "-c", "echo hi"], 30)
self.assertEqual(argv[1:], ["-I", "/opt/vault/agent-access.py", "--config", "https://vault.example/identity.json",
"run", "--target", "mac-target", "--timeout", "30", "--", "bash", "-c", "echo hi"])
seen = {}
def fake_run(args, **kw):
seen.update(args=args, env=kw["env"])
return mock.Mock(returncode=0, stdout=b"MKDIR=ok\n", stderr=b"")
with mock.patch.dict(os.environ, {"TOK_MAC": "s", "TOK_W10": "s"}), \
mock.patch("agentd.transports.warpgate_ssh.subprocess.run", fake_run):
PosixWarpgateTransport(self.cfg, "mac").mkdir("/x")
self.assertNotIn("TOK_MAC", seen["env"])
self.assertNotIn("TOK_W10", seen["env"])
self.assertEqual(seen["args"][-3:-1], ["bash", "-c"])
def test_claude_round_is_one_call_and_resume_uses_session_id(self):
ex = make_executor(self.cfg, "mac", "claude-code", self.t)
prompt = "it's \"quoted\" $HOME `x` \\ 中文\n"
h = ex.start(Run("GW-4-1", self.cfg.terminals["mac"]["repo"], prompt, model="sonnet"))
self.assertEqual(len(self.t.calls), 1) # mkdir + prompt + run.sh + launch in one relay call
self.assertIn("setsid nohup bash 'run.sh' </dev/null >/dev/null 2>&1 &", self.t.calls[0])
self.assertTrue(wait(ex, h))
rd = Path(h["run_dir"])
self.assertEqual((rd / "prompt.md").read_text(encoding="utf-8"), prompt)
self.assertEqual((rd / "pid.txt").read_text().strip(), (rd / "launch.pid").read_text().strip())
res = ex.collect(h)
self.assertEqual((res.last_message, res.exit_code, res.thread_id),
("[review] got %d" % len(prompt.rstrip("\n")), 0, "sess-7"))
cmd = ex.command(Run("GW-4-2", "r", "p", model="sonnet", thread_id="sess-7"), h["run_dir"])
self.assertEqual(cmd, "'%s' -p --resume sess-7" % (self.root / "bin dir" / "claude"))
def test_runner_quotes_paths_and_cd_failure_writes_exit(self):
_, script = self.t.runner("/a b/O'Neil/run", "/no such/repo's", "cmd", "out.json")
self.assertIn("cd '/no such/repo'\\''s' ||", script)
self.assertIn("< '/a b/O'\\''Neil/run'/prompt.md > '/a b/O'\\''Neil/run'/out.json", script)
ex = make_executor(self.cfg, "mac", "shell", self.t)
h = ex.start(Run("GW-4-3", str(self.root / "missing"), "", script="true"))
self.assertTrue(wait(ex, h))
self.assertEqual(ex.exit_code(h), 97)
def test_chunked_upload_and_reads(self):
self.t.CHUNK = 64
path = str(self.root / "run root" / "x y" / "f'.txt")
text = "line1 'q' \"d\" $(nope)\n" * 20 + "last ünïcode"
self.t.mkdir(os.path.dirname(path))
n = len(self.t.calls)
self.t.write_text(path, text)
self.assertGreater(len(self.t.calls) - n, 2)
self.assertEqual(Path(path).read_text(encoding="utf-8"), text)
self.assertFalse(os.path.exists(path + ".b64"))
self.assertEqual(self.t.read_text(path), text)
self.assertIsNone(self.t.read_text(path + ".missing"))
self.assertEqual(self.t.grep_first(path, "$(nope)"), "line1 'q' \"d\" $(nope)")
self.assertIsNone(self.t.grep_first(path, "absent"))
self.assertEqual(self.t.tail(path, 2), "line1 'q' \"d\" $(nope)\nlast ünïcode")
self.assertEqual(self.t.tail(path + ".missing", 2), "")
def test_big_prompt_goes_up_before_the_launch_call(self):
self.t.CHUNK = 100
ex = make_executor(self.cfg, "mac", "shell", self.t)
h = ex.start(Run("GW-4-4", self.cfg.terminals["mac"]["repo"], "z" * 500, script="wc -c"))
self.assertGreater(len(self.t.calls), 2)
self.assertTrue(wait(ex, h))
self.assertEqual(ex.collect(h).last_message, "500")
@unittest.skipUnless(shutil.which("setsid") and shutil.which("pgrep"), "needs setsid/pgrep")
def test_kill_ends_the_whole_process_group(self):
ex = make_executor(self.cfg, "mac", "shell", self.t)
marker = "agentd-test-%d" % os.getpid()
h = ex.start(Run("GW-4-5", self.cfg.terminals["mac"]["repo"], "",
script="bash -c 'sleep 300; : %s' & sleep 300; : %s" % (marker, marker)))
end = time.time() + 5
while time.time() < end and not subprocess.run(["pgrep", "-f", marker], capture_output=True).stdout:
time.sleep(0.1)
self.assertTrue(subprocess.run(["pgrep", "-f", marker], capture_output=True).stdout)
ex.kill(h)
self.assertIn("KILLED=ok left=0", subprocess.run(["bash", "-c", self.t.kill_script(h["run_dir"])],
capture_output=True, text=True).stdout)
self.assertFalse(subprocess.run(["pgrep", "-f", marker], capture_output=True).stdout)
self.assertEqual(ex.poll(h), "running") # killed runners write no exit.txt; core treats it as cancelled
def test_kill_after_finish_does_not_signal_a_reused_pid(self):
ex = make_executor(self.cfg, "mac", "shell", self.t)
h = ex.start(Run("GW-4-6", self.cfg.terminals["mac"]["repo"], "", script="true"))
self.assertTrue(wait(ex, h))
(Path(h["run_dir"]) / "pid.txt").write_text("1\n")
self.assertIn("KILLED=finished", subprocess.run(["bash", "-c", self.t.kill_script(h["run_dir"])],
capture_output=True, text=True).stdout)
def test_darwin_detaches_without_setsid_binary(self):
d = remote_data(self.root)
d["terminals"]["mac"]["os"] = "darwin"
t = PosixWarpgateTransport(Config(d), "mac")
s = t.launch_script("/r", "run.sh")
self.assertIn("nohup perl -MPOSIX -e 'POSIX::setsid() or die", s)
self.assertNotIn("setsid nohup", s)
self.assertIn("echo $! > launch.pid", s)
def test_semantic_checks(self):
d = remote_data(self.root)
d["terminals"]["mac"]["launch"] = "schtasks"
d["terminals"]["mac"]["run_root"] = "~/runs"
del d["vault"]["config"]
errs = config.validate(d)
for frag in ("needs launch=nohup", "mac.run_root: must be an absolute path", "need vault.config"):
self.assertTrue(any(frag in e for e in errs), (frag, errs))
d = remote_data(self.root)
d["terminals"]["win10"]["target"] = "win11"
self.assertTrue(any("only reach target 'win10'" in e for e in config.validate(d)))
def test_upload_payload_is_plain_base64(self):
s = self.t.put_script("/r/p.md", base64.b64encode("x'y".encode()).decode())
self.assertIn("printf %s 'eCd5' | base64 -d > '/r/p.md.part' && mv -f '/r/p.md.part' '/r/p.md'", s)
if __name__ == "__main__":
unittest.main()
def test_probe_dir_name_and_pid_guard(self):
ex = make_executor(self.cfg, "mac", "shell", self.t)
run = Run("GW-5-1", self.cfg.terminals["mac"]["repo"], "", script="sleep 0.2", dir_name="GW-5-1-r2")
self.assertEqual(ex.probe(ex.handle(run)), [])
h = ex.start(run)
self.assertEqual((os.path.basename(h["run_dir"]), h["task"]), ("GW-5-1-r2", "agentd-GW-5-1-r2"))
self.assertTrue(wait(ex, h))
self.assertEqual(ex.probe(h), ["exit.txt", "launch.pid", "pid.txt"])
n = len(self.t.calls)
with self.assertRaises(AssertionError): # LocalBash: marker LAUNCHED= missing (HASPID)
ex.start(run)
self.assertIn("HASPID", self.t.calls[n])
self.assertTrue(os.path.exists(os.path.join(h["run_dir"], "exit.txt"))) # nothing wiped