31850c4a81
One tick polls in-flight runs, posts results, classifies the first line and moves state/assignee; failures get a structured note with transient retry and backoff, timeouts are killed and handed over, resume failures and long threads rebuild with a handoff summary. Then the lowest Todo items are dispatched within terminal concurrency and serialize groups, and a heartbeat is written. CLI: tick, new, show, comment, state, assign, smoke, check-config.
82 lines
3.1 KiB
Python
82 lines
3.1 KiB
Python
"""Real LocalTransport + runner executors against throwaway scripts (local processes only, no network)."""
|
|
import os
|
|
import tempfile
|
|
import time
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
from agentd.config import Config
|
|
from agentd.executors import Run, make_executor
|
|
from agentd.transports.local import LocalTransport
|
|
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-42"}' "${#prompt}"
|
|
"""
|
|
|
|
|
|
def wait(ex, handle, seconds=10):
|
|
end = time.time() + seconds
|
|
while time.time() < end:
|
|
if ex.poll(handle) == "finished":
|
|
return True
|
|
time.sleep(0.1)
|
|
return False
|
|
|
|
|
|
class LocalRunnerTest(unittest.TestCase):
|
|
def setUp(self):
|
|
self.tmp = tempfile.TemporaryDirectory()
|
|
root = Path(self.tmp.name)
|
|
fake = root / "fake-claude"
|
|
fake.write_text(FAKE_CLAUDE)
|
|
fake.chmod(0o755)
|
|
data = config_data()
|
|
data["agentd"]["state_dir"] = str(root / "state")
|
|
data["terminals"]["oregon"]["bins"] = {"claude": str(fake)}
|
|
data["terminals"]["oregon"]["repo"] = str(root)
|
|
self.cfg = Config(data)
|
|
self.t = LocalTransport(self.cfg, "oregon")
|
|
self.root = root
|
|
|
|
def tearDown(self):
|
|
self.tmp.cleanup()
|
|
|
|
def test_shell_executor_round(self):
|
|
ex = make_executor(self.cfg, "oregon", "shell", self.t)
|
|
self.assertFalse(ex.resume)
|
|
run = Run("GW-1-1", str(self.root), "hello prompt", script="read line; echo \"[review] echo: $line\"")
|
|
h = ex.start(run)
|
|
self.assertTrue(wait(ex, h))
|
|
res = ex.collect(h)
|
|
self.assertEqual((res.last_message, res.exit_code, res.thread_id), ("[review] echo: hello prompt", 0, None))
|
|
|
|
def test_claude_executor_round_and_resume(self):
|
|
ex = make_executor(self.cfg, "oregon", "claude-code", self.t)
|
|
h = ex.start(Run("GW-2-1", str(self.root), "abc", model="sonnet"))
|
|
self.assertTrue(wait(ex, h))
|
|
res = ex.collect(h)
|
|
self.assertEqual((res.last_message, res.thread_id, res.exit_code), ("[review] got 3", "sess-42", 0))
|
|
h2 = ex.start(Run("GW-2-2", str(self.root), "abcd", model="sonnet", thread_id="sess-42"))
|
|
self.assertTrue(wait(ex, h2))
|
|
self.assertIn("--resume sess-42", self.t.read_text(os.path.join(h2["run_dir"], "stderr.txt")))
|
|
|
|
def test_kill_and_exit_code(self):
|
|
ex = make_executor(self.cfg, "oregon", "shell", self.t)
|
|
h = ex.start(Run("GW-3-1", str(self.root), "", script="sleep 30"))
|
|
time.sleep(0.5)
|
|
self.assertEqual(ex.poll(h), "running")
|
|
ex.kill(h)
|
|
time.sleep(0.3)
|
|
self.assertEqual(ex.poll(h), "running") # killed runner never writes exit.txt; core handles it as timeout
|
|
h = ex.start(Run("GW-3-2", str(self.root), "", script="echo partial; exit 3"))
|
|
self.assertTrue(wait(ex, h))
|
|
res = ex.collect(h)
|
|
self.assertEqual((res.exit_code, res.last_message), (3, "partial"))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|