Files
Agent-System-Dev/scripts/component-check.py
T
weichen adb6cbcf48 Keep fixed checks and lifecycle fixtures isolated in CI (U5)
Why:
Go-to-Python tests leave ignored bytecode in fixed sources, lifecycle clones need a fixed main reference, and boundary gates require ripgrep.

Evidence:
SELF real byproduct and clone regressions failed before repair; host passed 11 component cases, 4 hook cases and 33 CI credential cases.

Impact:
Execute versioned Go checks in disposable clones while preserving editable overlays and strict dirty guards; install the boundary prerequisite and seed only isolated pinned lifecycle refs.

Refs:
R8-R10, R14-R15; dependency-workspace U5; Dev PR 537 run 3036
2026-10-14 02:29:36 +00:00

152 lines
8.0 KiB
Python

#!/usr/bin/env python3
"""Prepare selected fixed sources and run explicitly local component checks."""
import argparse
from contextlib import contextmanager
import os
from pathlib import Path
import subprocess
import sys
import tempfile
from workspace.cli import Workspace, process_environment
from workspace.common import WorkspaceError
from workspace.state import State
SINGLE = {"test-backend": "Agent-System-Backend", "test-backend-pg": "Agent-System-Backend",
"test-core": "Agent-System-Core", "test-agentcmd": "Agent-Command",
"test-frontend": "Agent-System-Frontend", "test-ui": "Agent-System-UI",
"test-multi-instance": "Agent-System-Backend", "eval-deliverables": "Agent-System-Backend",
"dev-backend": "Agent-System-Backend", "dev-frontend": "Agent-System-Frontend"}
GO_CHECKS = {"test-backend", "test-backend-pg", "test-core", "test-agentcmd",
"test-libs", "test-multi-instance", "eval-deliverables"}
def run(command, cwd, env):
print("[component-check] " + " ".join(map(str, command)), flush=True)
result = subprocess.run(command, cwd=cwd, env=env)
if result.returncode:
raise WorkspaceError(f"local component command failed ({result.returncode})")
def prepare_local_source(workspace, component):
"""Repeated local npm checks may reuse only recognized generated directories."""
record = workspace.state.data["components"].get(component["id"])
path = workspace.root / component["path"]
if record and record.get("mode") == "versioned" and (path / "package.json").is_file():
actual = workspace.git.safe(path, component["repo"], clean=False)
commit, source = workspace.baseline(component)
generated = ("node_modules/", "dist/")
if actual["head"] != commit or actual["branch"] or actual["changes"] or any(not name.startswith(generated) for name in actual["ignored"]):
raise WorkspaceError(f"local source or ignored unique files require owner coordination: {component['id']}")
record.update(baseline=commit, source=source)
workspace.state.save()
return
workspace.prepare_one(component)
@contextmanager
def check_sources(workspace, selected, target, overlay):
paths = {component["id"]: workspace.root / component["path"] for component in selected}
fixed = [component for component in selected
if workspace.state.data["components"][component["id"]].get("mode") == "versioned"]
if overlay or target not in GO_CHECKS or not fixed:
yield paths
return
# Go tests can create ignored outputs (including Python bytecode). Keep
# strict source guards and execute fixed commits in owned disposable clones.
# Editable sources and explicit overlays still exercise the owner's code.
with tempfile.TemporaryDirectory(prefix="component-check-") as temporary:
area = Path(temporary)
for component in fixed:
name = component["id"]
source = paths[name]
actual = workspace.git.safe(source, component["repo"])
commit = workspace.state.data["components"][name]["baseline"]
if actual["head"] != commit or actual["branch"]:
raise WorkspaceError(f"fixed source changed before local check: {name}")
destination = area / name
workspace.git.call(area, "clone", "--no-checkout", "--no-hardlinks", "--", str(source), str(destination))
workspace.git.call(destination, "checkout", "--detach", commit)
paths[name] = destination
print(f"[component-check] fixed check snapshot {name} commit={commit}", flush=True)
yield paths
def check(root, target, overlay):
root = root.resolve()
if target in ("test-backend-pg", "test-multi-instance") and not os.environ.get("AGENT_PG_TEST_DSN"):
raise WorkspaceError("AGENT_PG_TEST_DSN required; real PostgreSQL gate is not skipped")
if target == "test-backend-pg" and not os.environ.get("AGENT_REDIS_TEST_ADDR"):
raise WorkspaceError("AGENT_REDIS_TEST_ADDR required; real Redis gate is not skipped")
workspace = Workspace(root)
names = [SINGLE[target]] if target in SINGLE else (["Agent-Native", "Agent-Intent", "Agent-Squad"] if target == "test-libs" else ["Agent-System-Backend", "Agent-System-Frontend"])
selected = [workspace.manifest.select(name)[0] for name in names]
with workspace.state.operation():
workspace.state = State(root)
if any(component["role"] in ("go-library", "npm-library") for component in selected):
for component in workspace.manifest.select(fixed=True):
if component["role"] == "application":
prepare_local_source(workspace, component)
for component in selected:
prepare_local_source(workspace, component)
workspace.snapshot()
env = process_environment(root)
if overlay:
workspace.go.check_owned()
if not workspace.state.data.get("go_work") or not (root / "go.work").is_file():
raise WorkspaceError("LOCAL_OVERLAY=1 requires a registered go.work; use component-edit first")
env["GOWORK"] = str(root / "go.work")
else:
env["GOWORK"] = "off"
print(f"[component-check] local developer sources; GOWORK={env['GOWORK']}; registered edit branches and unsaved code retained", flush=True)
with check_sources(workspace, selected, target, overlay) as paths:
run_checks(root, target, selected, names, paths, env)
def run_checks(root, target, selected, names, paths, env):
if target == "dev":
print("Backend: make dev-backend [LOCAL_OVERLAY=1]\nFrontend: make dev-frontend\nPrivate module transport is scoped to those processes.")
return
path = paths[names[0]]
if target == "dev-backend":
run(["go", "run", "./cmd/agent-system", "serve", "--addr", "127.0.0.1:8787", "--data", str(root / "data"), "--plugins", str(root / "plugins")], path, env)
elif target == "dev-frontend":
run(["npm", "ci", "--ignore-scripts", "--no-fund", "--no-audit"], path, env)
run(["npm", "run", "dev"], path, env)
elif target == "test-multi-instance":
suite = root / "tests/integration/multi-instance"
binary = suite / ".build/agent-system"
binary.parent.mkdir(parents=True, exist_ok=True)
run(["go", "build", "-mod=readonly", "-o", str(binary), "./cmd/agent-system"], path, env | {"CGO_ENABLED": "0"})
run(["go", "test", "-mod=readonly", "-count=1", "-timeout=15m", "."], suite, env | {"AGENT_BIN": str(binary)})
elif target == "eval-deliverables":
run(["go", "test", "-mod=readonly", "./eval/", "./internal/quality/", "-count=1"], path, env)
elif target in ("test-frontend", "test-ui"):
run(["npm", "ci", "--ignore-scripts", "--no-fund", "--no-audit"], path, env)
if target == "test-ui":
run(["npm", "exec", "--offline", "--", "tsc", "--noEmit"], path, env)
run(["npm", "exec", "--offline", "--", "vitest", "run"], path, env)
run(["node", "scripts/check-knip.mjs"], path, env)
else:
for component in selected:
source = paths[component["id"]]
modules = component.get("modules", []) if target == "test-libs" else [{"directory": "."}]
for module in modules:
directory = source / module.get("directory", ".")
if target in ("test-libs", "test-agentcmd"):
run(["go", "vet", "-mod=readonly", "./..."], directory, env)
run(["go", "test", "-mod=readonly", "-count=1", "./..."], directory, env)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parents[1])
parser.add_argument("--target", choices=(*SINGLE, "test-libs", "dev"), required=True)
parser.add_argument("--overlay", action="store_true")
args = parser.parse_args()
try:
check(args.root, args.target, args.overlay)
except (WorkspaceError, OSError, ValueError) as error:
print(f"component-check: {error}", file=sys.stderr)
raise SystemExit(1)