agentic-ai-engineering/pipeline/test_integration.py

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)