Files
Agent-System-Dev/tests/test_agent_exec_entry.py

195 lines
7.6 KiB
Python

"""scripts/agent-exec 薄入口与 pre-commit 委派测试:临时真实 Git 仓库 + 假 dm-server,不访问网络。"""
from __future__ import annotations
import os
import shutil
import subprocess
import tempfile
import unittest
from pathlib import Path
SOURCE = Path(__file__).resolve().parents[1]
HAS_GO = shutil.which("go") is not None
GO_MAIN = r'''package main
import (
"fmt"
"os"
"strings"
)
func main() {
fmt.Printf("fake-dm-server %s\n", strings.Join(os.Args[1:], " "))
}
'''
# 预置在缓存中的桩二进制:用于不依赖 go 的 pre-commit 委派测试(入口命中缓存即直接 exec)。
STUB = "#!/bin/sh\nprintf 'fake-dm-server %s\\n' \"$*\"\n"
def _clean_env(**extra: str) -> dict[str, str]:
env = {k: v for k, v in os.environ.items() if not k.startswith("GIT_")}
env.update(
GIT_TERMINAL_PROMPT="0",
GIT_AUTHOR_NAME="Agent Exec",
GIT_AUTHOR_EMAIL="agent-exec@example.com",
GIT_COMMITTER_NAME="Agent Exec",
GIT_COMMITTER_EMAIL="agent-exec@example.com",
)
env.update(extra)
return env
def _git(cwd: Path, *args: str) -> str:
proc = subprocess.run(
["git", "-C", str(cwd), *args],
capture_output=True,
text=True,
env=_clean_env(),
check=True,
)
return proc.stdout.strip()
class AgentExecEntryTests(unittest.TestCase):
def setUp(self) -> None:
self.tmp = tempfile.TemporaryDirectory(prefix="agent-exec-entry-")
self.addCleanup(self.tmp.cleanup)
self.base = Path(os.path.realpath(self.tmp.name))
self.cache = self.base / "cache"
self.refused_cache = self.base / "refused-cache"
self.dm_src = self._make_fake_dm()
self.pin = _git(self.dm_src, "rev-parse", "HEAD")
self.repo = self._make_super_repo()
self.dm = self.repo / "deps" / "development-manager"
def _make_fake_dm(self) -> Path:
src = self.base / "fake-dm"
(src / "cmd" / "dm-server").mkdir(parents=True)
(src / "go.mod").write_text("module example.com/fakedm\n\ngo 1.22\n", encoding="utf-8")
(src / "cmd" / "dm-server" / "main.go").write_text(GO_MAIN, encoding="utf-8")
(src / "README.md").write_text("fake dm\n", encoding="utf-8")
_git(src, "init", "-q", "-b", "main")
_git(src, "add", "-A")
_git(src, "commit", "-q", "-m", "fake dm")
return src
def _make_super_repo(self) -> Path:
repo = self.base / "super"
(repo / "scripts" / "hooks").mkdir(parents=True)
for src, rel in (
(SOURCE / "scripts" / "agent-exec", "scripts/agent-exec"),
(SOURCE / "scripts" / "hooks" / "pre-commit", "scripts/hooks/pre-commit"),
):
dest = repo / rel
shutil.copy(src, dest)
dest.chmod(0o755)
(repo / "README.md").write_text("super\n", encoding="utf-8")
_git(repo, "init", "-q", "-b", "main")
_git(repo, "update-index", "--add", "--cacheinfo",
f"160000,{self.pin},deps/development-manager")
dm = repo / "deps" / "development-manager"
dm.parent.mkdir()
_git(repo, "clone", "-q", "--no-checkout", str(self.dm_src), str(dm))
_git(dm, "checkout", "-q", "--detach", self.pin)
_git(repo, "add", "-A")
_git(repo, "commit", "-q", "-m", "init")
return repo
def _entry(self, *args: str, cache: Path | None = None) -> subprocess.CompletedProcess[str]:
return subprocess.run(
[str(self.repo / "scripts" / "agent-exec"), *args],
cwd=str(self.repo),
capture_output=True,
text=True,
env=_clean_env(AGENT_EXEC_CACHE=str(cache or self.cache)),
)
def _binary(self, cache: Path | None = None) -> Path:
return (cache or self.cache) / f"dm-server-{self.pin}"
def _assert_no_binary(self, cache: Path) -> None:
leftovers = list(cache.glob("dm-server-*")) if cache.exists() else []
self.assertEqual(leftovers, [])
def _assert_refused(self, proc: subprocess.CompletedProcess[str], needle: str) -> None:
combined = proc.stdout + proc.stderr
self.assertEqual(proc.returncode, 2, combined)
self.assertIn(needle, proc.stderr)
self.assertNotIn("fake-dm-server", combined)
self._assert_no_binary(self.refused_cache)
@unittest.skipUnless(HAS_GO, "go is not on PATH")
def test_positive_run_builds_and_execs_pinned_binary(self) -> None:
proc = self._entry("validate", "--repo", ".")
self.assertEqual(proc.returncode, 0, proc.stderr)
self.assertIn("fake-dm-server agent-exec validate --repo .", proc.stdout)
self.assertTrue(self._binary().is_file())
@unittest.skipUnless(HAS_GO, "go is not on PATH")
def test_second_run_reuses_cached_binary(self) -> None:
first = self._entry("version")
self.assertEqual(first.returncode, 0, first.stderr)
before = self._binary().stat().st_mtime_ns
second = self._entry("version")
self.assertEqual(second.returncode, 0, second.stderr)
self.assertIn("fake-dm-server agent-exec version", second.stdout)
self.assertEqual(self._binary().stat().st_mtime_ns, before)
self.assertEqual(list(self.cache.glob(".dm-server-*")), [])
def test_refuses_when_dm_head_differs_from_pin(self) -> None:
_git(self.dm, "commit", "-q", "--allow-empty", "-m", "move head")
proc = self._entry("validate", "--repo", ".", cache=self.refused_cache)
self._assert_refused(proc, "与 index 钉住的")
self.assertIn("git submodule update --init deps/development-manager", proc.stderr)
def test_refuses_tracked_modification_in_dm_checkout(self) -> None:
(self.dm / "README.md").write_text("changed\n", encoding="utf-8")
proc = self._entry("validate", "--repo", ".", cache=self.refused_cache)
self._assert_refused(proc, "存在未提交的修改")
def test_refuses_when_gitlink_absent(self) -> None:
_git(self.repo, "rm", "--cached", "-q", "deps/development-manager")
proc = self._entry("validate", "--repo", ".", cache=self.refused_cache)
self._assert_refused(proc, "gitlink")
def test_precommit_delegates_admit_to_pinned_binary(self) -> None:
_git(self.repo, "config", "core.hooksPath", "scripts/hooks")
(self.repo / "src").mkdir()
(self.repo / "src" / "b.txt").write_text("staged\n", encoding="utf-8")
_git(self.repo, "add", "src/b.txt")
main_cache = self.base / "main-cache"
rejected = self._commit(main_cache, "on main")
combined = rejected.stdout + rejected.stderr
self.assertNotEqual(rejected.returncode, 0, combined)
self.assertIn("集成分支", combined)
self.assertNotIn("fake-dm-server", combined)
self._assert_no_binary(main_cache)
cache = self.base / "hook-cache"
cache.mkdir()
stub = self._binary(cache)
stub.write_text(STUB, encoding="utf-8")
stub.chmod(0o755)
_git(self.repo, "checkout", "-q", "-b", "feature/x")
accepted = self._commit(cache, "feat: x")
combined = accepted.stdout + accepted.stderr
self.assertEqual(accepted.returncode, 0, combined)
self.assertIn(f"fake-dm-server agent-exec admit --repo {self.repo}", combined)
def _commit(self, cache: Path, message: str) -> subprocess.CompletedProcess[str]:
return subprocess.run(
["git", "-C", str(self.repo), "commit", "-q", "-m", message],
capture_output=True,
text=True,
env=_clean_env(AGENT_EXEC_CACHE=str(cache)),
)
if __name__ == "__main__":
unittest.main()