FU-8a: process→code guards (GAP-21/22) #16
48
mcp-server/tests/test_sync_verify_gate.py
Normal file
48
mcp-server/tests/test_sync_verify_gate.py
Normal file
@@ -0,0 +1,48 @@
|
||||
"""FU-8a / GAP-21: sync --verify drift-gate logic (offline)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
|
||||
_SCRIPT = Path(__file__).resolve().parents[2] / "scripts" / "sync_agents_across_companies.py"
|
||||
_spec = importlib.util.spec_from_file_location("sync_agents", _SCRIPT)
|
||||
sync = importlib.util.module_from_spec(_spec)
|
||||
_spec.loader.exec_module(sync)
|
||||
|
||||
|
||||
def _agent(name, adapter="claude_code", cfg=None):
|
||||
return {"id": f"id-{name}", "name": name, "adapter_type": adapter,
|
||||
"adapter_config": cfg or {"model": "x"}, "runtime_config": {}, "metadata": {},
|
||||
"budget_monthly_cents": 0, "icon": "", "title": "", "role": "", "agent_api_keys": []}
|
||||
|
||||
|
||||
def test_verify_exit_code_clean_is_zero():
|
||||
assert sync._verify_exit_code(plan=[], mismatches=[], missing=[]) == 0
|
||||
|
||||
|
||||
def test_verify_exit_code_drift_is_nonzero():
|
||||
assert sync._verify_exit_code(plan=[("m", "mi", {"x": 1})], mismatches=[], missing=[]) == 1
|
||||
|
||||
|
||||
def test_verify_exit_code_adapter_mismatch_is_nonzero():
|
||||
assert sync._verify_exit_code(plan=[], mismatches=["עוזר משפטי"], missing=[]) == 1
|
||||
|
||||
|
||||
def test_verify_exit_code_missing_is_nonzero():
|
||||
assert sync._verify_exit_code(plan=[], mismatches=[], missing=["סוכן"]) == 1
|
||||
|
||||
|
||||
def test_build_drift_report_flags_adapter_mismatch():
|
||||
master = [_agent("A", adapter="claude_code")]
|
||||
mirror_by_name = {"A": _agent("A", adapter="deepseek_local")}
|
||||
rep = sync.build_drift_report(master, mirror_by_name, mirror_skills=set(), only=None)
|
||||
assert "A" in rep["mismatches"]
|
||||
assert rep["plan"] == []
|
||||
|
||||
|
||||
def test_build_drift_report_flags_missing_and_plan():
|
||||
master = [_agent("A"), _agent("B")]
|
||||
mirror_by_name = {"B": _agent("B", cfg={"model": "different"})}
|
||||
rep = sync.build_drift_report(master, mirror_by_name, mirror_skills=set(), only=None)
|
||||
assert "A" in rep["missing"]
|
||||
assert any(p[0]["name"] == "B" for p in rep["plan"])
|
||||
@@ -345,6 +345,33 @@ async def check_instructions(agents: list[dict]) -> bool:
|
||||
return all_ok
|
||||
|
||||
|
||||
def build_drift_report(master_agents, mirror_by_name, mirror_skills, only=None) -> dict:
|
||||
"""Pure drift computation (no DB, no printing). Returns:
|
||||
{"plan": [(master, mirror, diff), ...], "mismatches": [name, ...], "missing": [name, ...]}.
|
||||
adapter_type mismatch and missing-in-mirror are recorded as drift, not skipped silently.
|
||||
"""
|
||||
plan, mismatches, missing = [], [], []
|
||||
for m in master_agents:
|
||||
if only and m["name"] != only:
|
||||
continue
|
||||
mirror = mirror_by_name.get(m["name"])
|
||||
if not mirror:
|
||||
missing.append(m["name"])
|
||||
continue
|
||||
if m["adapter_type"] != mirror["adapter_type"]:
|
||||
mismatches.append(m["name"])
|
||||
continue
|
||||
diff = compute_diff(m, mirror, mirror_skills)
|
||||
if diff:
|
||||
plan.append((m, mirror, diff))
|
||||
return {"plan": plan, "mismatches": mismatches, "missing": missing}
|
||||
|
||||
|
||||
def _verify_exit_code(plan, mismatches, missing) -> int:
|
||||
"""0 iff fully in sync; 1 if any drift (needs-sync / adapter mismatch / missing-in-mirror)."""
|
||||
return 1 if (plan or mismatches or missing) else 0
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
p = argparse.ArgumentParser()
|
||||
g = p.add_mutually_exclusive_group(required=True)
|
||||
@@ -376,26 +403,23 @@ async def main() -> None:
|
||||
print(f"=== Mirror has {len(mirror_skills)} local skills available ===\n")
|
||||
|
||||
print(f"=== Drift report ===")
|
||||
plan: list[tuple[dict, dict, dict]] = [] # (master, mirror, diff)
|
||||
for m in master_agents:
|
||||
if args.only and m["name"] != args.only:
|
||||
continue
|
||||
mirror = mirror_by_name.get(m["name"])
|
||||
if not mirror:
|
||||
print(f" ⚠ {m['name']:14s} — NOT FOUND in mirror (skipping; we never auto-create)")
|
||||
continue
|
||||
if m["adapter_type"] != mirror["adapter_type"]:
|
||||
print(f" ⚠ {m['name']:14s} — adapter_type mismatch ({m['adapter_type']} vs {mirror['adapter_type']}) — SKIPPING")
|
||||
continue
|
||||
diff = compute_diff(m, mirror, mirror_skills)
|
||||
print_diff(m["name"], diff, m["id"], mirror["id"])
|
||||
if diff:
|
||||
plan.append((m, mirror, diff))
|
||||
report = build_drift_report(master_agents, mirror_by_name, mirror_skills, only=args.only)
|
||||
plan = report["plan"]
|
||||
for name in report["missing"]:
|
||||
print(f" ⚠ {name:14s} — NOT FOUND in mirror (we never auto-create) — DRIFT")
|
||||
for name in report["mismatches"]:
|
||||
m = next(a for a in master_agents if a["name"] == name)
|
||||
mi = mirror_by_name[name]
|
||||
print(f" ❌ {name:14s} — adapter_type mismatch ({m['adapter_type']} vs {mi['adapter_type']}) "
|
||||
f"— DRIFT (apply skips it; fix manually in both companies)")
|
||||
for master, mirror, diff in plan:
|
||||
print_diff(master["name"], diff, master["id"], mirror["id"])
|
||||
|
||||
if args.verify:
|
||||
print(f"\n(verify mode — exiting without changes)")
|
||||
print(f"\nSummary: {len(plan)} agent(s) need sync, {len(master_agents) - len(plan)} in sync")
|
||||
return
|
||||
code = _verify_exit_code(plan, report["mismatches"], report["missing"])
|
||||
print(f"\nSummary: {len(plan)} need sync, {len(report['mismatches'])} adapter-mismatch, "
|
||||
f"{len(report['missing'])} missing-in-mirror → {'DRIFT' if code else 'IN SYNC'}")
|
||||
sys.exit(code)
|
||||
|
||||
if not plan:
|
||||
print(f"\n✓ All agents in sync — nothing to do.")
|
||||
|
||||
Reference in New Issue
Block a user