140 lines
6.0 KiB
Python
140 lines
6.0 KiB
Python
"""
|
|
Integration test: full pipeline flow.
|
|
|
|
Simulates a complete ADW-style pipeline run with:
|
|
1. State initialization + core_fields filter
|
|
2. Multiple phases, each producing a Verdict
|
|
3. Rejection envelope for blocked actions
|
|
4. State gate at the end
|
|
5. Expertise entry for what was learned
|
|
"""
|
|
import json
|
|
import sys
|
|
import os
|
|
|
|
if hasattr(sys.stdout, "reconfigure"):
|
|
sys.stdout.reconfigure(encoding="utf-8")
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
# Also add the parent of pipeline to ensure direct imports work
|
|
_parent = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
if _parent not in sys.path:
|
|
sys.path.insert(0, _parent)
|
|
|
|
from pipeline.confidence_ladder import Verdict, VerdictLevel, VerdictReport
|
|
from pipeline.rejection_envelope import RejectionEnvelope, Blocker
|
|
from pipeline.state_gate import StateGate, FieldSpec, PipelineState
|
|
from pipeline.expertise import append_entry, render_context, purge_domain
|
|
|
|
errors = 0
|
|
|
|
# ── Step 1: Pipeline state with core_fields ──
|
|
state = PipelineState(
|
|
{"plan_file": "plans/feat-123.md", "branch_name": "feat/instant-polls", "issue_number": 42},
|
|
core_fields=["plan_file", "branch_name", "issue_number", "backend_port", "frontend_port", "worktree_path"],
|
|
)
|
|
assert state.get("plan_file") == "plans/feat-123.md"
|
|
print("✓ Step 1: State init with core_fields")
|
|
|
|
# ── Step 2: Plan phase verdict ──
|
|
report = VerdictReport(pipeline_id="adw-abc12345")
|
|
report.add(Verdict(VerdictLevel.VERIFIED, "Plan covers all acceptance criteria", ["AC1 covered", "AC2 covered"], source="plan-review"))
|
|
print(f" Plan verdict: {report.aggregate_label}")
|
|
assert report.aggregate == VerdictLevel.VERIFIED
|
|
|
|
# ── Step 3: Build phase produces artifacts ──
|
|
state.update(backend_port=9105, frontend_port=9205, worktree_path="/tmp/worktrees/adw-abc12345")
|
|
report.add(Verdict(VerdictLevel.PERFECT, "Build completed, tests pass in worktree", ["exit code 0", "all 14 tests pass"], source="build"))
|
|
print(f" Build verdict: PERFECT")
|
|
|
|
# ── Step 4: A blocked action (leader tries to write a file) ──
|
|
no_write_blocker = Blocker(
|
|
name="no_direct_write",
|
|
match=lambda a: a.get("tool") in ("write_file", "patch", "execute_code"),
|
|
reject=lambda a: RejectionEnvelope.block(
|
|
received=a.get("tool", "unknown"),
|
|
reason="LEADER_MUST_NOT_WRITE_FILES",
|
|
allowed="Use delegate_task with a writer subagent",
|
|
hint="Leaders read, delegate, synthesize. Delegate file writes.",
|
|
),
|
|
)
|
|
blocked = no_write_blocker.evaluate({"tool": "write_file", "path": "src/main.py"})
|
|
assert blocked is not None, "write_file should be blocked"
|
|
print(f"✓ Step 4: Action blocked — {blocked.reason}")
|
|
print(f" Allowed: {blocked.allowed}")
|
|
|
|
# Verify a non-blocked action passes
|
|
allowed_action = no_write_blocker.evaluate({"tool": "read_file", "path": "state.json"})
|
|
assert allowed_action is None, "read_file should NOT be blocked"
|
|
print("✓ Step 4b: read_file passes through")
|
|
|
|
# ── Step 5: Security review finds gaps (PARTIAL) ──
|
|
report.add(Verdict(VerdictLevel.PARTIAL, "No vulnerabilities found, but no live penetration test was run", ["static analysis clean", "dependency scan clean", "NOTE: no live pen test"], source="security-review"))
|
|
print(f" Security verdict: PARTIAL (important: nothing failed, but gap is documented)")
|
|
|
|
# ── Step 6: Check aggregate after all phases ──
|
|
print(f" Aggregate: {report.aggregate_label}")
|
|
assert report.aggregate == VerdictLevel.PARTIAL, f"Aggregate should be PARTIAL (lowest of VERIFIED+PERFECT+PARTIAL), got {report.aggregate_label}"
|
|
assert report.passed == True, "PARTIAL should pass (green but documented gap)"
|
|
print("✓ Step 6: Aggregate correctly identifies PARTIAL gate")
|
|
|
|
# ── Step 7: State completeness gate before ship ──
|
|
ship_gate = StateGate(
|
|
FieldSpec("plan_file", required=True, predicate=lambda v: isinstance(v, str) and v.startswith("plans/")),
|
|
FieldSpec("branch_name", required=True),
|
|
FieldSpec("issue_number", required=True, type_hint=int),
|
|
FieldSpec("backend_port", required=True, type_hint=int),
|
|
FieldSpec("frontend_port", required=True, type_hint=int),
|
|
FieldSpec("worktree_path", required=True),
|
|
label="pre-ship",
|
|
)
|
|
gate_result = ship_gate.validate(state)
|
|
assert gate_result.passed, f"Complete state should pass ship gate: {gate_result.summary()}"
|
|
print(f"✓ Step 7: Ship gate passed ({gate_result.fields_checked} fields validated)")
|
|
|
|
# ── Step 8: State gate catches missing fields ──
|
|
incomplete = PipelineState(
|
|
{"plan_file": "plans/feat-123.md", "branch_name": "feat/instant-polls"},
|
|
core_fields=state._core_fields,
|
|
)
|
|
fail_result = ship_gate.validate(incomplete)
|
|
assert not fail_result.passed
|
|
assert "issue_number" in fail_result.missing
|
|
assert "backend_port" in fail_result.missing
|
|
print(f"✓ Step 8: Ship gate correctly catches missing: {fail_result.missing}")
|
|
|
|
# ── Step 9: Store expertise about what was learned ──
|
|
append_entry(
|
|
"integration-test",
|
|
domain="pipeline",
|
|
insight="PARTIAL verdict allows documented gaps to pass without blocking — critical for green-but-dead prevention",
|
|
source="cron:pipeline-integration-test",
|
|
verified=True,
|
|
)
|
|
append_entry(
|
|
"integration-test",
|
|
domain="state-gate",
|
|
insight="Required fields must include worktree_path and ports, not just plan_file and branch_name",
|
|
source="cron:pipeline-integration-test",
|
|
)
|
|
context = render_context("integration-test")
|
|
assert "PARTIAL" in context
|
|
assert "worktree_path" in context
|
|
print("✓ Step 9: Expertise entries stored and loadable")
|
|
|
|
# ── Step 10: JSON serialization of final report ──
|
|
serialized = report.to_json()
|
|
restored = VerdictReport.from_json(serialized)
|
|
assert restored.aggregate == report.aggregate
|
|
assert len(restored.verdicts) == len(report.verdicts)
|
|
print("✓ Step 10: Full VerdictReport JSON round-trip")
|
|
|
|
# Cleanup test expertise file
|
|
purge_domain("integration-test", "pipeline")
|
|
purge_domain("integration-test", "state-gate")
|
|
|
|
print(f"\n✅ Integration test passed ({10 - errors}/10)")
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(0 if errors == 0 else 1)
|