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
152 lines
8.0 KiB
Python
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)
|