Files
Agent-System/scripts/tests/test_agentctl.py
T
WeiCHEN 0352c0996d merge: feature/governance-automation-2026-09-22
Keep macOS process platform; land W4 queue/automation and cleanup-report.
2026-09-22 22:20:37 +08:00

757 lines
42 KiB
Python

#!/usr/bin/env python3
"""Black-box agentctl regressions using isolated real Git repositories."""
import json
import fcntl
import hashlib
import os
import pathlib
import select
import shutil
import socket
import subprocess
import sys
import tempfile
import unittest
SOURCE = pathlib.Path(__file__).resolve().parents[1] / "agentctl.py"
class AgentctlTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory(prefix="agentctl-test-")
self.addCleanup(self.temp.cleanup)
self.root = pathlib.Path(self.temp.name) / "repo"
self.root.mkdir()
self.git("init", "-b", "dev")
self.git("config", "user.name", "Agentctl Test")
self.git("config", "user.email", "agentctl-test@example.invalid")
(self.root / "scripts").mkdir()
shutil.copy2(SOURCE, self.root / "scripts" / "agentctl.py")
shutil.copy2(SOURCE.parent / "agentctl_w4.py", self.root / "scripts" / "agentctl_w4.py")
(self.root / ".gitignore").write_text(".locks/\n.worktrees/\n", encoding="utf-8")
for directory in ("src/a", "src/b", ".doc/ops", ".doc/status"):
(self.root / directory).mkdir(parents=True, exist_ok=True)
self.current_rows = {
"alpha": "| alpha | Alice | feature/alpha | active |",
"beta": "| beta | Bob | feature/beta | active |",
}
(self.root / ".doc/ops/CURRENT.md").write_text(
"# Coordination\n\n| Task | Owner | Branch | State |\n| --- | --- | --- | --- |\n"
+ "\n".join(self.current_rows.values()) + "\n", encoding="utf-8")
(self.root / ".doc/status/FEATURE-MATRIX.md").write_text(
"# Features\n\n| Feature | State |\n| --- | --- |\n| core | planned |\n", encoding="utf-8")
self.git("add", ".")
self.git("commit", "-m", "test fixture")
def git(self, *args, cwd=None):
return subprocess.run(["git", *args], cwd=cwd or self.root,
text=True, capture_output=True, check=True).stdout.strip()
def cli(self, *args, cwd=None, check=True, env=None):
result = subprocess.run([sys.executable, str(self.root / "scripts/agentctl.py"), *args],
cwd=cwd or self.root, text=True, capture_output=True,
env=env, timeout=20)
if check:
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
return result
def initialized_task(self, task="alpha", scopes=("src/a", ".doc"), capabilities=()):
if not (self.root / ".locks").exists():
self.cli("init")
path = self.root / ".worktrees" / task
self.git("worktree", "add", "-b", "feature/" + task, str(path), "dev")
args = ["task", "register", "--id", task, "--owner", "Test Agent"]
for scope in scopes:
args.extend(("--scope", scope))
for capability in capabilities:
args.extend(("--capability", capability))
self.cli(*args, cwd=path)
return path
def run_lease(self, task, resources, code="pass", cwd=None, env=None):
args = ["with", "--task", task, "--wait", "0"]
if cwd is not None:
args.extend(("--cwd", str(cwd)))
for resource in resources:
args.extend(("--resource", resource))
return self.cli(*args, "--", sys.executable, "-c", code,
cwd=cwd, check=False, env=env)
def live_lease(self, task, resources, cwd):
"""Wait for the actual supervised child via a socket, never timing sleeps."""
listener = socket.socket(socket.AF_UNIX)
self.addCleanup(listener.close)
socket_path = str(pathlib.Path(self.temp.name) / ("control-" + str(len(self._cleanups))))
listener.bind(socket_path)
listener.listen(1)
listener.settimeout(15)
code = (
"import json,os,socket; s=socket.socket(socket.AF_UNIX); "
f"s.connect({socket_path!r}); "
"s.sendall((json.dumps({'run':os.environ.get('AGENTCTL_RUN_ID'),"
"'task':os.environ.get('AGENTCTL_TASK_ID'),'pid':os.getpid(),"
"'credentials':{k:v for k,v in os.environ.items() if k.startswith('AGENTCTL_')}})+'\\n').encode()); "
"s.recv(1)"
)
args = [sys.executable, str(self.root / "scripts/agentctl.py"), "with", "--task", task]
for resource in resources:
args.extend(("--resource", resource))
process = subprocess.Popen(args + ["--", sys.executable, "-c", code], cwd=cwd,
text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
def cleanup():
if process.poll() is None:
process.terminate()
try:
process.communicate(timeout=5)
except subprocess.TimeoutExpired:
process.kill()
process.communicate(timeout=5)
self.addCleanup(cleanup)
try:
connection, _ = listener.accept()
except TimeoutError:
if process.poll() is not None:
out, err = process.communicate()
self.fail("supervised child did not start: " + out + err)
raise
connection.settimeout(10)
self.addCleanup(connection.close)
payload = b""
while not payload.endswith(b"\n"):
payload += connection.recv(4096)
return process, connection, json.loads(payload)
def finish_lease(self, process, connection):
connection.sendall(b"x")
out, err = process.communicate(timeout=10)
self.assertEqual(process.returncode, 0, out + err)
def docs_request(self, changes, request_id="request-alpha"):
path = pathlib.Path(self.temp.name) / (request_id + ".json")
path.write_text(json.dumps({"request_id": request_id, "changes": changes}), encoding="utf-8")
return str(path)
def doc_change(self, file, record, old, replacement):
return {"file": file, "record_id": record,
"expected_sha256": hashlib.sha256(old.encode()).hexdigest() if old else None,
"replacement": replacement}
def test_uninitialized_status_and_doctor_do_not_create_state(self):
for command in ("status", "doctor"):
result = self.cli(command, check=False)
json.loads(result.stdout or result.stderr)
self.assertFalse((self.root / ".locks").exists())
self.assertFalse((self.root / ".git/agent-control").exists())
def test_init_preserves_existing_legacy_lock_inode(self):
legacy = self.root / ".git/agent-control/dev-integration.lock"
legacy.parent.mkdir()
legacy.write_text("")
original_inode = legacy.stat().st_ino
self.cli("init")
stable = self.root / ".locks/os/dev-integration.lock"
self.assertEqual(legacy.stat().st_ino, original_inode)
self.assertEqual(stable.stat().st_ino, original_inode)
self.cli("init")
self.assertEqual(stable.stat().st_ino, original_inode)
def test_init_cannot_steal_live_legacy_lock(self):
legacy = self.root / ".git/agent-control/dev-integration.lock"
legacy.parent.mkdir()
with legacy.open("w") as handle:
fcntl.flock(handle, fcntl.LOCK_EX | fcntl.LOCK_NB)
inode = legacy.stat().st_ino
result = self.cli("init", check=False)
self.assertNotEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertEqual(legacy.stat().st_ino, inode)
self.cli("init")
def test_linked_worktree_uses_only_main_coordination_root(self):
task = self.initialized_task()
json.loads(self.cli("status", cwd=task).stdout)
self.assertTrue((self.root / ".locks").is_dir())
self.assertFalse((task / ".locks").exists())
self.cli("init", cwd=task, check=False)
self.assertFalse((task / ".locks").exists())
def test_task_registration_rejects_protected_branch_and_invalid_scopes(self):
self.cli("init")
result = self.cli("task", "register", "--id", "main-task", "--owner", "Test",
"--scope", "src", check=False)
self.assertNotEqual(result.returncode, 0)
path = self.root / ".worktrees/invalid"
self.git("worktree", "add", "-b", "feature/invalid", str(path), "dev")
for index, scope in enumerate(("../outside", "/tmp/absolute", ".git", ".locks", "src/../../outside")):
with self.subTest(scope=scope):
result = self.cli("task", "register", "--id", "invalid-" + str(index),
"--owner", "Test", "--scope", scope, cwd=path, check=False)
self.assertNotEqual(result.returncode, 0, result.stdout + result.stderr)
def test_path_scope_and_capability_are_enforced(self):
task = self.initialized_task()
self.assertEqual(self.run_lease("alpha", ["path:src/a/item"], cwd=task).returncode, 0)
for resource in ("path:src/b", "path:src/ab", "path:../outside", "env:dev", "git:dev"):
with self.subTest(resource=resource):
result = self.run_lease("alpha", [resource], cwd=task)
self.assertNotEqual(result.returncode, 0, result.stdout + result.stderr)
def test_parent_child_path_conflict_and_disjoint_coexistence(self):
task = self.initialized_task(scopes=("src",))
process, connection, _ = self.live_lease("alpha", ["path:src/a"], task)
for resource in ("path:src", "path:src/a/file", "path:src/a"):
with self.subTest(resource=resource):
result = self.run_lease("alpha", [resource], cwd=task)
self.assertNotEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertEqual(self.run_lease("alpha", ["path:src/b"], cwd=task).returncode, 0)
self.finish_lease(process, connection)
self.assertEqual(self.run_lease("alpha", ["path:src/a"], cwd=task).returncode, 0)
def test_multi_resource_acquisition_is_atomic(self):
task = self.initialized_task(scopes=("src",))
process, connection, _ = self.live_lease("alpha", ["path:src/a"], task)
result = self.run_lease("alpha", ["path:src/b", "path:src/a"], cwd=task)
self.assertNotEqual(result.returncode, 0)
result = self.run_lease("alpha", ["path:src/b"], cwd=task)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.finish_lease(process, connection)
def test_task_index_lease_serializes_index_but_not_disjoint_work(self):
task = self.initialized_task(scopes=("src",))
other_task = self.initialized_task(task="beta", scopes=("src/b",))
process, connection, _ = self.live_lease("alpha", ["git:index", "path:src/a"], task)
result = self.run_lease("alpha", ["git:index", "path:src/b"], cwd=task)
self.assertNotEqual(result.returncode, 0, result.stdout + result.stderr)
result = self.run_lease("alpha", ["path:src/b"], cwd=task)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
result = self.run_lease("beta", ["git:index", "path:src/b"], cwd=other_task)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.finish_lease(process, connection)
result = self.run_lease("alpha", ["git:index", "path:src/a"], cwd=task)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
def test_failed_child_releases_lease_and_preserves_failure(self):
task = self.initialized_task()
result = self.run_lease("alpha", ["path:src/a"], "raise SystemExit(17)", cwd=task)
self.assertEqual(result.returncode, 17, result.stdout + result.stderr)
self.assertEqual(self.run_lease("alpha", ["path:src/a"], cwd=task).returncode, 0)
def test_live_and_stale_credentials_cannot_be_spoofed(self):
task = self.initialized_task()
process, connection, payload = self.live_lease("alpha", ["path:src/a"], task)
self.assertTrue(payload["run"])
environment = dict(os.environ, **payload["credentials"])
result = self.cli("guard", "resource", "path:src/a", cwd=task, env=environment, check=False)
self.assertNotEqual(result.returncode, 0)
self.finish_lease(process, connection)
result = self.cli("guard", "resource", "path:src/a", cwd=task, env=environment, check=False)
self.assertNotEqual(result.returncode, 0)
def test_live_run_cannot_be_recovered_or_task_closed(self):
task = self.initialized_task()
process, connection, payload = self.live_lease("alpha", ["path:src/a"], task)
result = self.cli("recover", "--run", payload["run"], "--reason", "test", check=False)
self.assertNotEqual(result.returncode, 0)
result = self.cli("task", "close", "--id", "alpha", "--status", "done", cwd=task, check=False)
self.assertNotEqual(result.returncode, 0)
self.finish_lease(process, connection)
def test_recovery_refuses_dead_supervisor_with_live_child(self):
if not hasattr(os, "pidfd_open"):
self.skipTest("pidfd_open is Linux-only")
task = self.initialized_task()
process, connection, payload = self.live_lease("alpha", ["path:src/a"], task)
child_fd = os.pidfd_open(payload["pid"])
self.addCleanup(os.close, child_fd)
process.kill()
process.wait(timeout=5)
self.assertFalse(select.select([child_fd], [], [], 0)[0], "fixture child must still be alive")
result = self.cli("recover", "--run", payload["run"], "--reason", "supervisor died", check=False)
self.assertNotEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertNotEqual(self.run_lease("alpha", ["path:src/a"], cwd=task).returncode, 0)
connection.sendall(b"x")
self.assertTrue(select.select([child_fd], [], [], 10)[0], "fixture child must finish")
process.communicate(timeout=5)
self.cli("recover", "--run", payload["run"], "--reason", "all processes stopped")
self.assertEqual(self.run_lease("alpha", ["path:src/a"], cwd=task).returncode, 0)
def test_descendant_resource_guard_accepts_only_held_resource(self):
task = self.initialized_task()
code = (
"import subprocess,sys; "
f"p=subprocess.run([sys.executable,{str(self.root / 'scripts/agentctl.py')!r},"
"'guard','resource','path:src/a/item']);sys.exit(p.returncode)"
)
result = self.run_lease("alpha", ["path:src/a"], code, cwd=task)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
result = self.run_lease("alpha", ["path:src/a"], code.replace("path:src/a/item", "path:src/b"), cwd=task)
self.assertNotEqual(result.returncode, 0)
def test_shared_lease_does_not_satisfy_write_guard(self):
task = self.initialized_task()
result = self.cli("with", "--task", "alpha", "--resource", "path:src/a", "--shared",
"--", sys.executable, str(self.root / "scripts/agentctl.py"),
"guard", "resource", "path:src/a", cwd=task, check=False)
self.assertNotEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertEqual(self.run_lease("alpha", ["path:src/a"], cwd=task).returncode, 0)
def test_env_guard_refuses_linked_worktree_even_with_capability(self):
task = self.initialized_task(capabilities=("env:dev",))
code = (
"import subprocess,sys; "
f"p=subprocess.run([sys.executable,{str(self.root / 'scripts/agentctl.py')!r},"
"'guard','env','--target','dev']);sys.exit(p.returncode)"
)
result = self.run_lease("alpha", ["env:dev"], code, cwd=task)
self.assertNotEqual(result.returncode, 0, result.stdout + result.stderr)
def test_env_guard_accepts_clean_main_and_requires_all_target_leases(self):
self.initialized_task(capabilities=("env:dev", "env:test", "env:main", "env:shared"))
code = (
"import subprocess,sys; "
f"p=subprocess.run([sys.executable,{str(self.root / 'scripts/agentctl.py')!r},"
"'guard','env','--target','dev']);sys.exit(p.returncode)"
)
result = self.run_lease("alpha", ["env:dev"], code, cwd=self.root)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
all_code = code.replace("'dev'", "'all'")
result = self.run_lease("alpha", ["env:dev", "env:test", "env:main", "env:shared"], all_code, cwd=self.root)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
result = self.run_lease("alpha", ["env:dev"], all_code, cwd=self.root)
self.assertNotEqual(result.returncode, 0)
def test_docs_row_cas_preserves_unrelated_rows_and_is_idempotent(self):
task = self.initialized_task(capabilities=("docs:publish",))
filename = ".doc/ops/CURRENT.md"
snapshot = json.loads(self.cli("docs", "snapshot", "--file", filename, "--record", "alpha", cwd=task).stdout)
self.assertEqual(snapshot["sha256"], hashlib.sha256(self.current_rows["alpha"].encode()).hexdigest())
document = task / filename
other = self.current_rows["beta"].replace("active", "reviewing")
document.write_text(document.read_text().replace(self.current_rows["beta"], other))
# A separate accepted update advanced the source baseline; row alpha is unchanged.
self.git("add", filename, cwd=task)
self.git("commit", "-m", "independent beta update", cwd=task)
replacement = self.current_rows["alpha"].replace("active", "done")
request = self.docs_request([self.doc_change(filename, "alpha", self.current_rows["alpha"], replacement)])
self.cli("docs", "apply", "--task", "alpha", "--request", request, cwd=task)
first = document.read_bytes()
self.assertIn(other, first.decode())
self.assertIn(replacement, first.decode())
self.cli("docs", "apply", "--task", "alpha", "--request", request, cwd=task)
self.assertEqual(document.read_bytes(), first)
self.assertIn(self.current_rows["alpha"], (self.root / filename).read_text())
def test_docs_foreign_current_row_is_rejected(self):
task = self.initialized_task(capabilities=("docs:publish",))
filename = ".doc/ops/CURRENT.md"
before = (task / filename).read_bytes()
request = self.docs_request([self.doc_change(filename, "beta", self.current_rows["beta"],
self.current_rows["beta"].replace("active", "done"))])
result = self.cli("docs", "apply", "--task", "alpha", "--request", request, cwd=task, check=False)
self.assertNotEqual(result.returncode, 0)
self.assertEqual((task / filename).read_bytes(), before)
def test_docs_multi_file_preflight_failure_has_no_partial_writes(self):
task = self.initialized_task(capabilities=("docs:publish",))
current = ".doc/ops/CURRENT.md"
features = ".doc/status/FEATURE-MATRIX.md"
before = {name: (task / name).read_bytes() for name in (current, features)}
request = self.docs_request([
self.doc_change(current, "alpha", self.current_rows["alpha"], self.current_rows["alpha"].replace("active", "done")),
self.doc_change(features, "core", "| core | stale |", "| core | done |"),
])
result = self.cli("docs", "apply", "--task", "alpha", "--request", request, cwd=task, check=False)
self.assertNotEqual(result.returncode, 0)
for filename, contents in before.items():
self.assertEqual((task / filename).read_bytes(), contents)
def test_docs_cannot_launder_unmanaged_other_task_changes(self):
task = self.initialized_task(capabilities=("docs:publish",))
filename = ".doc/ops/CURRENT.md"
document = task / filename
document.write_text(document.read_text().replace(self.current_rows["beta"], self.current_rows["beta"].replace("active", "done")))
before = document.read_bytes()
request = self.docs_request([self.doc_change(filename, "alpha", self.current_rows["alpha"], self.current_rows["alpha"].replace("active", "done"))])
result = self.cli("docs", "apply", "--task", "alpha", "--request", request, cwd=task, check=False)
self.assertNotEqual(result.returncode, 0)
self.assertIn("UNMANAGED_DOCUMENT_CHANGE", result.stderr)
self.assertEqual(document.read_bytes(), before)
def test_commit_lease_must_cover_staged_files_not_only_task_scope(self):
task = self.initialized_task(scopes=("src",))
self.cli("hooks", "install")
file = task / "src/b/other.py"
file.parent.mkdir(parents=True, exist_ok=True)
file.write_text("value = 2\n")
self.git("add", "src/b/other.py", cwd=task)
code = "import subprocess,sys;sys.exit(subprocess.run(['git','commit','-m','missing file lease']).returncode)"
result = self.run_lease("alpha", ["git:index", "path:src/a"], code, cwd=task)
self.assertNotEqual(result.returncode, 0)
self.assertIn("LEASE_REQUIRED", result.stderr)
def test_guard_build_requires_exact_clean_commit(self):
task = self.initialized_task()
head = self.git("rev-parse", "HEAD", cwd=task)
self.cli("guard", "build", "--commit", head, cwd=task)
self.assertNotEqual(self.cli("guard", "build", "--commit", "0" * 40, cwd=task, check=False).returncode, 0)
(task / "new-source.py").write_text("print('dirty')\n")
self.assertNotEqual(self.cli("guard", "build", "--commit", head, cwd=task, check=False).returncode, 0)
def test_installed_hooks_reject_direct_dev_commit(self):
self.cli("init")
self.cli("hooks", "install")
original = self.git("rev-parse", "HEAD")
(self.root / "source.txt").write_text("change\n")
self.git("add", "source.txt")
result = subprocess.run(["git", "commit", "-m", "unauthorized direct dev commit"],
cwd=self.root, text=True, capture_output=True, timeout=20)
self.assertNotEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertEqual(self.git("rev-parse", "HEAD"), original)
def test_hooks_refuse_overwriting_foreign_hook(self):
self.cli("init")
hook = self.root / ".git/hooks/pre-commit"
original = "#!/bin/sh\n# existing local protection\nexit 0\n"
hook.write_text(original)
hook.chmod(0o755)
result = self.cli("hooks", "install", check=False)
self.assertNotEqual(result.returncode, 0)
self.assertEqual(hook.read_text(), original)
def test_hook_checks_reject_out_of_scope_and_secret_paths(self):
task = self.initialized_task()
self.cli("hooks", "install")
for name in ("outside.txt", "src/a/.env"):
with self.subTest(name=name):
target = task / name
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text("test fixture, no actual secret\n")
self.git("add", "--", name, cwd=task)
code = (
"import subprocess,sys; "
f"p=subprocess.run([sys.executable,{str(self.root / 'scripts/agentctl.py')!r},"
"'hooks','check']);sys.exit(p.returncode)"
)
result = self.run_lease("alpha", ["git:index", "path:src/a"], code, cwd=task)
self.assertNotEqual(result.returncode, 0, result.stdout + result.stderr)
self.git("reset", "--", name, cwd=task)
target.unlink()
def test_hook_rejects_shared_document_without_publish_receipt(self):
task = self.initialized_task(capabilities=("docs:publish",))
self.cli("hooks", "install")
current = task / ".doc/ops/CURRENT.md"
current.write_text(current.read_text().replace(self.current_rows["alpha"],
self.current_rows["alpha"].replace("active", "done")))
self.git("add", ".doc/ops/CURRENT.md", cwd=task)
code = (
"import subprocess,sys; "
f"p=subprocess.run([sys.executable,{str(self.root / 'scripts/agentctl.py')!r},"
"'hooks','check']);sys.exit(p.returncode)"
)
result = self.run_lease("alpha", ["git:index", "path:.doc", "docs:publish"], code, cwd=task)
self.assertNotEqual(result.returncode, 0, result.stdout + result.stderr)
def test_hooks_allow_scoped_task_commit_under_live_lease(self):
task = self.initialized_task()
self.cli("hooks", "install")
source = task / "src/a/example.py"
source.parent.mkdir(parents=True, exist_ok=True)
source.write_text("value = 1\n")
self.git("add", "src/a/example.py", cwd=task)
code = "import subprocess,sys; sys.exit(subprocess.run(['git','commit','-m','scoped task change']).returncode)"
result = self.run_lease("alpha", ["git:index", "path:src/a"], code, cwd=task)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertEqual(self.git("status", "--porcelain", cwd=task), "")
def test_admin_full_document_requires_explicit_admin_capability(self):
task = self.initialized_task(capabilities=("docs:publish",))
filename = ".doc/ops/CURRENT.md"
before = (task/filename).read_text()
request = self.docs_request([self.doc_change(filename,"@document",before,before+"\n")], "no-admin")
result = self.cli("docs","apply","--task","alpha","--request",request,cwd=task,check=False)
self.assertNotEqual(result.returncode,0)
self.assertIn("CAPABILITY_REQUIRED",result.stderr)
self.assertEqual((task/filename).read_text(),before)
def test_registered_active_task_must_close_before_cleanup(self):
task = self.initialized_task()
result = self.cli("worktree-check","--path",str(task),check=False)
self.assertNotEqual(result.returncode,0)
self.assertIn("TASK_ACTIVE",result.stderr)
self.cli("task","close","--id","alpha")
self.cli("worktree-check","--path",str(task))
def test_failed_revalidation_revokes_old_success_receipt(self):
task = self.initialized_task()
self.cli("validate","--task","alpha","--",sys.executable,"-c","pass",cwd=task)
result = self.cli("validate","--task","alpha","--",sys.executable,"-c","raise SystemExit(3)",cwd=task,check=False)
self.assertEqual(result.returncode,3)
import sqlite3
with sqlite3.connect(self.root/'.locks/state.sqlite') as db:
self.assertEqual(db.execute('SELECT count(*) FROM validations').fetchone()[0],0)
result=self.cli('validate','--task','alpha','--',sys.executable,'-c',"from pathlib import Path;Path('dirty-generated').write_text('x')",cwd=task,check=False)
self.assertNotEqual(result.returncode,0)
with sqlite3.connect(self.root/'.locks/state.sqlite') as db:
self.assertEqual(db.execute('SELECT count(*) FROM validations').fetchone()[0],0)
def test_invalid_document_request_shape_has_no_write_side_effects(self):
task = self.initialized_task(capabilities=("docs:publish",))
original = (task/'.doc/ops/CURRENT.md').read_bytes()
for changes in [[None],[{'file':[]}]]:
request = self.docs_request(changes,'bad-shape-'+str(len(str(changes))))
result = self.cli('docs','apply','--task','alpha','--request',request,cwd=task,check=False)
self.assertNotEqual(result.returncode,0)
json.loads(result.stderr)
self.assertEqual((task/'.doc/ops/CURRENT.md').read_bytes(),original)
def test_admin_document_cannot_bypass_done_with_compact_cells(self):
task = self.initialized_task(capabilities=("docs:publish", "docs:admin"))
filename = ".doc/status/FEATURE-MATRIX.md"
snapshot = json.loads(self.cli("docs", "snapshot", "--file", filename, "--record", "@document", cwd=task).stdout)
original = (task / filename).read_text()
for indent in range(4):
with self.subTest(indent=indent):
request = self.docs_request([{"file":filename,"record_id":"@document","expected_sha256":snapshot["sha256"],
"replacement":original.replace("| core | planned |", " "*indent+"| core |done|")}], "compact-done-"+str(indent))
result = self.cli("docs", "apply", "--task", "alpha", "--request", request, cwd=task, check=False)
self.assertNotEqual(result.returncode, 0)
self.assertEqual((task / filename).read_text(), original)
def test_validated_linear_commit_cannot_bypass_candidate_entrypoint(self):
task = self.initialized_task(scopes=("src",), capabilities=("git:dev",))
self.cli("hooks", "install")
(task / "src/a/new.py").parent.mkdir(parents=True, exist_ok=True)
(task / "src/a/new.py").write_text("ok = 1\n")
self.git("add", "src/a/new.py", cwd=task)
result = self.run_lease("alpha", ["git:index", "path:src"], "import subprocess,sys;sys.exit(subprocess.run(['git','commit','-m','linear']).returncode)", cwd=task)
self.assertEqual(result.returncode, 0, result.stderr)
self.cli("validate", "--task", "alpha", "--", "git", "diff", "--check", cwd=task)
original = self.git("rev-parse", "dev")
result = self.cli("with", "--task", "alpha", "--resource", "git:dev", "--cwd", str(self.root),
"--", "git", "merge", "--ff-only", "feature/alpha", check=False)
self.assertNotEqual(result.returncode, 0)
self.assertEqual(self.git("rev-parse", "dev"), original)
def test_interrupted_validation_stops_child_before_releasing_scope(self):
if not hasattr(os, "pidfd_open"):
self.skipTest("pidfd_open is Linux-only")
task = self.initialized_task()
listener = socket.socket(socket.AF_UNIX)
endpoint = str(pathlib.Path(self.temp.name) / "validate.sock")
listener.bind(endpoint)
listener.listen(1)
listener.settimeout(10)
code = "import os,socket,time;s=socket.socket(socket.AF_UNIX);s.connect(%r);s.sendall(str(os.getpid()).encode());time.sleep(60)" % endpoint
process = subprocess.Popen([sys.executable,str(self.root/'scripts/agentctl.py'),'validate','--task','alpha','--',sys.executable,'-c',code],cwd=task,stdout=subprocess.PIPE,stderr=subprocess.PIPE,text=True)
child_pid = None
try:
connection,_ = listener.accept()
with connection:
child_pid = int(connection.recv(100))
pidfd = os.pidfd_open(child_pid)
try:
process.send_signal(__import__('signal').SIGINT)
process.wait(timeout=10)
self.assertNotEqual(process.returncode, 0)
self.assertTrue(select.select([pidfd],[],[],2)[0], 'validator released with live child')
finally:
os.close(pidfd)
finally:
listener.close()
if child_pid:
try: os.killpg(child_pid,__import__('signal').SIGKILL)
except ProcessLookupError: pass
if process.poll() is None:
process.kill()
process.communicate(timeout=5)
def test_validated_merge_candidate_is_exactly_promoted_to_dev(self):
task = self.initialized_task()
self.cli("hooks", "install")
(task / "src/a/feature.py").parent.mkdir(parents=True, exist_ok=True)
(task / "src/a/feature.py").write_text("feature = True\n")
self.git("add", "src/a/feature.py", cwd=task)
change = self.run_lease("alpha", ["git:index", "path:src/a"],
"import subprocess,sys;sys.exit(subprocess.run(['git','commit','-m','feature']).returncode)", cwd=task)
self.assertEqual(change.returncode, 0, change.stderr)
baseline = self.git("rev-parse", "dev")
candidate = self.initialized_task("candidate", scopes=(".",), capabilities=("git:dev",))
merged = self.run_lease("candidate", ["git:index", "git:dev", "path:."],
"import subprocess,sys;sys.exit(subprocess.run(['git','merge','--no-ff','-m','candidate','feature/alpha']).returncode)", cwd=candidate)
self.assertEqual(merged.returncode, 0, merged.stderr)
head = self.git("rev-parse", "HEAD", cwd=candidate)
denied = self.cli("integrate", "--task", "candidate", "--expected-dev", baseline, cwd=candidate, check=False)
self.assertNotEqual(denied.returncode, 0)
self.assertIn("VALIDATION_REQUIRED", denied.stderr)
self.cli("validate", "--task", "candidate", "--", "git", "diff", "--check", baseline, cwd=candidate)
# Interruption at the Git boundary must reap its child and retain recovery.
listener = socket.socket(socket.AF_UNIX)
endpoint = str(pathlib.Path(self.temp.name)/"integrate.sock")
listener.bind(endpoint); listener.listen(1); listener.settimeout(10)
proxy_dir = pathlib.Path(self.temp.name)/"proxy-bin"; proxy_dir.mkdir()
real_git = shutil.which("git")
proxy = proxy_dir/"git"
proxy.write_text("#!/usr/bin/env python3\nimport os,sys,socket\n"
+ "if 'merge' in sys.argv and '--ff-only' in sys.argv:\n"
+ f" s=socket.socket(socket.AF_UNIX);s.connect({endpoint!r});s.sendall(os.environ['AGENTCTL_RUN_ID'].encode());s.recv(1)\n"
+ f"os.execv({real_git!r},[{real_git!r},*sys.argv[1:]])\n")
proxy.chmod(0o755)
process = subprocess.Popen([sys.executable,str(self.root/'scripts/agentctl.py'),'integrate','--task','candidate','--expected-dev',baseline],cwd=candidate,
env={**os.environ,'PATH':str(proxy_dir)+os.pathsep+os.environ['PATH']},stdout=subprocess.PIPE,stderr=subprocess.PIPE,text=True)
try:
connection,_=listener.accept()
with connection:
run_id=connection.recv(100).decode()
process.send_signal(__import__('signal').SIGINT)
process.communicate(timeout=10)
self.assertNotEqual(process.returncode,0)
self.assertEqual(self.git('rev-parse','dev'),baseline)
blocked=self.run_lease('candidate',['git:index'],cwd=candidate)
self.assertNotEqual(blocked.returncode,0)
self.cli('recover','--run',run_id,'--reason','test confirmed Git proxy stopped before ref update')
finally:
listener.close()
if process.poll() is None:
process.terminate();process.communicate(timeout=10)
self.cli("integrate", "--task", "candidate", "--expected-dev", baseline, cwd=candidate)
self.assertEqual(self.git("rev-parse", "dev"), head)
self.assertEqual(self.git("status", "--porcelain"), "")
# Reusing the old expected base cannot replay a second ref update.
stale = self.cli("integrate", "--task", "candidate", "--expected-dev", baseline, cwd=candidate, check=False)
self.assertNotEqual(stale.returncode, 0)
self.assertEqual(self.git("rev-parse", "dev"), head)
def test_admin_document_cas_preserves_slot_and_needs_capability(self):
task = self.initialized_task(capabilities=("docs:publish", "docs:admin"))
filename = ".doc/ops/CURRENT.md"
snapshot = json.loads(self.cli("docs", "snapshot", "--file", filename, "--record", "@document", cwd=task).stdout)
original = (task / filename).read_text()
request = self.docs_request([{ "file":filename, "record_id":"@document", "expected_sha256":snapshot['sha256'],
"replacement":original.replace("# Coordination", "# Active coordination") }], "admin-layout")
self.cli("docs", "apply", "--task", "alpha", "--request", request, cwd=task)
self.assertIn("# Active coordination", (task / filename).read_text())
def test_replaced_compatibility_inode_is_not_a_new_free_lock(self):
self.cli("init")
bridge = self.root / ".locks/os/dev-integration.lock"
bridge.unlink()
bridge.write_text("")
self.assertNotEqual(self.cli("status", check=False).returncode, 0)
def test_hooks_allow_published_shared_document_receipt(self):
task = self.initialized_task(capabilities=("docs:publish",))
self.cli("hooks", "install")
filename = ".doc/ops/CURRENT.md"
request = self.docs_request([self.doc_change(filename, "alpha", self.current_rows["alpha"],
self.current_rows["alpha"].replace("active", "done"))])
self.cli("docs", "apply", "--task", "alpha", "--request", request, cwd=task)
self.git("add", filename, cwd=task)
code = "import subprocess,sys; sys.exit(subprocess.run(['git','commit','-m','published task state']).returncode)"
result = self.run_lease("alpha", ["git:index", "path:.doc", "docs:publish"], code, cwd=task)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertEqual(self.git("status", "--porcelain", cwd=task), "")
class ProcessPlatformTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
import importlib.util
spec = importlib.util.spec_from_file_location("agentctl_under_test", SOURCE)
cls.mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(cls.mod)
def test_boot_and_self_proc_are_observable(self):
boot = self.mod.boot_epoch()
self.assertTrue(boot)
lookup, info = self.mod.read_proc(os.getpid())
self.assertEqual(lookup, "ok")
self.assertIsNotNone(info)
self.assertNotEqual(info.state, "Z")
self.assertTrue(info.start)
self.assertEqual(self.mod.alive_status(os.getpid(), info.start, boot), "alive")
self.assertEqual(self.mod.alive_status(os.getpid(), info.start, "other-boot"), "dead")
self.assertEqual(self.mod.descent_status(os.getpid()), "yes")
def test_gone_pid_is_dead(self):
child = subprocess.Popen([sys.executable, "-c", "import time; time.sleep(30)"])
pid = child.pid
lookup, info = self.mod.read_proc(pid)
self.assertEqual(lookup, "ok")
self.assertIsNotNone(info)
child.kill()
child.wait(timeout=5)
self.assertEqual(self.mod.read_proc(pid)[0], "gone")
self.assertEqual(self.mod.alive_status(pid, info.start, self.mod.boot_epoch()), "dead")
def test_zombie_is_not_alive(self):
info = self.mod.ProcInfo(pid=9, ppid=1, pgid=9, state="Z", start="token")
from unittest import mock
with mock.patch.object(self.mod, "read_proc", return_value=("ok", info)), \
mock.patch.object(self.mod, "boot_epoch", return_value="boot"):
self.assertEqual(self.mod.alive_status(9, "token", "boot"), "dead")
def test_unreadable_proc_is_unknown_not_dead(self):
from unittest import mock
with mock.patch.object(self.mod, "read_proc", return_value=("unreadable", None)), \
mock.patch.object(self.mod, "boot_epoch", return_value="boot"):
self.assertEqual(self.mod.alive_status(9, "start", "boot"), "unknown")
self.assertTrue(self.mod.alive(9, "start", "boot"))
with mock.patch.object(self.mod, "_enumerate_group", return_value=(False, "unknown")):
self.assertEqual(self.mod.group_status(42), "unknown")
self.assertTrue(self.mod.group_alive(42))
with mock.patch.object(self.mod, "_enumerate_group", return_value=(False, "ok")):
self.assertEqual(self.mod.group_status(42), "dead")
with mock.patch.object(self.mod, "_enumerate_group", return_value=(True, "unknown")):
self.assertEqual(self.mod.group_status(42), "alive")
def test_descent_unreadable_is_unknown(self):
from unittest import mock
with mock.patch.object(self.mod, "read_proc", return_value=("unreadable", None)):
self.assertEqual(self.mod.descent_status(1), "unknown")
self.assertFalse(self.mod.descendants_of(1))
def test_lease_denies_when_supervisor_proc_unreadable(self):
from unittest import mock
temp = tempfile.TemporaryDirectory(prefix="agentctl-proc-")
self.addCleanup(temp.cleanup)
root = pathlib.Path(temp.name) / "repo"
root.mkdir()
subprocess.run(["git", "init", "-b", "dev"], cwd=root, check=True, capture_output=True)
subprocess.run(["git", "config", "user.name", "t"], cwd=root, check=True, capture_output=True)
subprocess.run(["git", "config", "user.email", "t@example.invalid"], cwd=root, check=True, capture_output=True)
(root / "scripts").mkdir()
shutil.copy2(SOURCE, root / "scripts" / "agentctl.py")
shutil.copy2(SOURCE.parent / "agentctl_w4.py", root / "scripts" / "agentctl_w4.py")
(root / ".gitignore").write_text(".locks/\n.worktrees/\n", encoding="utf-8")
(root / ".doc/ops").mkdir(parents=True)
(root / ".doc/ops/CURRENT.md").write_text("# c\n", encoding="utf-8")
(root / ".doc/status").mkdir(parents=True)
(root / ".doc/status/FEATURE-MATRIX.md").write_text("# f\n", encoding="utf-8")
(root / "src/a").mkdir(parents=True)
subprocess.run(["git", "add", "."], cwd=root, check=True, capture_output=True)
subprocess.run(["git", "commit", "-m", "fixture"], cwd=root, check=True, capture_output=True)
cli = [sys.executable, str(root / "scripts/agentctl.py")]
subprocess.run(cli + ["init"], cwd=root, check=True, capture_output=True)
task = root / ".worktrees" / "alpha"
subprocess.run(["git", "worktree", "add", "-b", "feature/alpha", str(task), "dev"],
cwd=root, check=True, capture_output=True)
subprocess.run(cli + ["task", "register", "--id", "alpha", "--owner", "t", "--scope", "src/a"],
cwd=task, check=True, capture_output=True)
import importlib.util
spec = importlib.util.spec_from_file_location("agentctl_lease", root / "scripts/agentctl.py")
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
repo = mod.Repo(task)
task_row = mod.task_row(repo, "alpha")
with mock.patch.object(mod, "read_proc", return_value=("unreadable", None)):
with self.assertRaises(mod.Denied) as raised:
mod.Lease(repo, task_row, ["path:src/a"], None, "fp", 0).__enter__()
self.assertEqual(raised.exception.code, "PROCESS_UNREADABLE")
if __name__ == "__main__":
unittest.main()