CI Pipeline / best-practices (push) Successful in 8s
CI Pipeline / documentation (push) Successful in 4s
CI Pipeline / canary (push) Successful in 37s
CI Pipeline / lint (push) Successful in 44s
CI Pipeline / type-check (push) Successful in 31s
CI Pipeline / gitea-offline (push) Successful in 16s
CI Pipeline / unit-tests (push) Successful in 1m3s
CI Pipeline / required-checks (push) Successful in 1s
deploy / deploy (push) Successful in 1m41s
ADAPT-LIVE tranche (handoff 2026-09-10): 1. LOCAL_FIX vs STANDARD_CHANGE discriminator (epic #284 doctrine): defect language (fix/bug/broken/regression) routes LOCAL_FIX; clear change language (add/implement/extend/refactor) routes STANDARD_CHANGE. Recorded in sufficiency provenance (task_class_language). Legacy compat path keeps body-scan defect detection. 2. Verifiability NONE (no test surface named) no longer blocks LOCAL for defect-class narrow tasks — self-evident fixes verify by inspection; UNKNOWN still escalates (could not determine ≠ determined absent). 3. Route-specific execution context: LOCAL_FIX = focused verification, no research; STANDARD_CHANGE = integration verification + DESIGN-LITE (bounded build-contract: objective, owning surfaces from the task text, verification class, invariants, escalation policy — no model call, no dossier). 4. Probe-on-verification-failure (ADAPT-LIVE §4/§6): a real verification failure on the operator spine triggers ONE bounded Evidence Probe. GROUNDED → downgrade-eligible evidence; STILL_UNKNOWN/CONTRADICTED → recorded ESCALATION to RESEARCHED_CHANGE (route change, not task failure). Opt-in via MIMO_PROBE_ENABLED=1 (runtime policy, now in the TUI launcher); tests stay deterministic. 5. Probe-grounded downgrade (§7): after ALL probes ground and local authority is established, STANDARD_CHANGE downgrades to LOCAL_FIX with recorded trigger probe_grounded_local_authority. Canary evidence (live, deployed Rune): - LOCAL_FIX: 'Fix the failing test in tests/test_example.py' → LOCAL_FIX route event, task_language=defect, no research stages - STANDARD_CHANGE: 'Add config validation...' → STANDARD_CHANGE, task_language=change - Escalation: LOCAL_FIX + verification failure + probe STILL_UNKNOWN → ESCALATION event (verification_failure_probe_unresolved) → RESEARCHED_CHANGE persisted with evidence - Route latency: 0.2-16ms (vs fixed-depth 384s+ wasted on LOCAL tasks) 1227→1231 tests green; ratchet 353/80; henxels clean.
177 lines
7.6 KiB
Python
177 lines
7.6 KiB
Python
#!/usr/bin/env python3
|
|
"""tests/test_evidence_probe.py — Targeted Evidence Probe canaries (§20.D).
|
|
|
|
Canary 2: otherwise-local task → material external unknown → probe GROUNDED
|
|
→ returns to implementation path, does NOT launch full research.
|
|
Canary 3: probe STILL_UNKNOWN / CONTRADICTED → recorded escalation →
|
|
RESEARCHED_CHANGE (no probe loops).
|
|
Plus module gates: schema validation, budget (no recursion), file authority.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
PKG_ROOT = os.path.join(os.path.dirname(__file__), "..")
|
|
sys.path.insert(0, os.path.join(PKG_ROOT, "src"))
|
|
sys.path.insert(0, os.path.join(PKG_ROOT, "scripts"))
|
|
|
|
from rune import adaptive_router # noqa: E402
|
|
from rune import evidence_probe # noqa: E402
|
|
from research_stage_executor import StageExecutor, make_event_id # noqa: E402
|
|
|
|
|
|
def _issue_event(issue_no, stage="ISSUE", attempt=1):
|
|
return {
|
|
"event_type": "ResearchStageReady",
|
|
"event_id": make_event_id(issue_no, f"research-{issue_no}", 1, stage, attempt),
|
|
"issue": issue_no,
|
|
"round": 1,
|
|
"stage": stage,
|
|
"attempt": attempt,
|
|
}
|
|
|
|
|
|
# An otherwise-local task with ONE material external unknown: the issue
|
|
# references an external library's behaviour but is a known-pattern change.
|
|
AMBIGUOUS_ISSUE = {
|
|
"number": 9100,
|
|
"title": "Handle platformdirs config-dir resolution on new installs",
|
|
"body": (
|
|
"Wire the config dir through the platformdirs library package. "
|
|
"Reference: https://platformdirs.readthedocs.io. Depends on "
|
|
"current behaviour of the library's version. Existing test: "
|
|
"tests/test_config_paths.py covers the local wiring."
|
|
),
|
|
"labels": [],
|
|
}
|
|
|
|
GROUNDED_PROBE = {
|
|
"question": "probe",
|
|
"source": "https://platformdirs.readthedocs.io/changelog.html",
|
|
"answer": "Current version guarantees the assumed behaviour.",
|
|
"implication": "Local wiring is sufficient; no version guard needed.",
|
|
"status": "GROUNDED",
|
|
}
|
|
|
|
UNKNOWN_PROBE = {
|
|
"question": "probe",
|
|
"source": "",
|
|
"answer": "Could not find authoritative evidence.",
|
|
"implication": "Uncertainty remains.",
|
|
"status": "STILL_UNKNOWN",
|
|
}
|
|
|
|
|
|
def _runner_returning(payload):
|
|
def runner(prompt, artifact_path):
|
|
with open(artifact_path, "w") as f:
|
|
json.dump(dict(payload, question=payload.get("question", "probe")), f)
|
|
|
|
return runner
|
|
|
|
|
|
def _make_executor(tmpdir, issue_data, runner):
|
|
ex = StageExecutor(issue=issue_data["number"], artifact_dir=tmpdir, round_no=1, probe_runner=runner)
|
|
ex._fetch_issue = lambda: issue_data
|
|
return ex
|
|
|
|
|
|
class Canary2ProbeGroundedReturnsToLocal(unittest.TestCase):
|
|
def test_grounded_probe_resolves_unknown_without_deep_research(self):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
|
|
def runner(prompt, artifact_path):
|
|
# The prompt must name the EXACT artifact path (ADAPT-9)
|
|
self.assertIn(artifact_path, prompt)
|
|
with open(artifact_path, "w") as f:
|
|
json.dump(dict(GROUNDED_PROBE), f)
|
|
|
|
ex = _make_executor(tmp, AMBIGUOUS_ISSUE, runner)
|
|
result = ex.execute(_issue_event(9100))
|
|
|
|
# The route must NOT be deep research: the probe grounded the
|
|
# unknown and the task returns to the local implementation path.
|
|
self.assertEqual(
|
|
result["status"], "ROUTED_LOCAL_FIX", f"grounded probe should return to local lane: {result}"
|
|
)
|
|
# Exactly ONE probe ran — no loops.
|
|
self.assertEqual(len(result["probes"]), 1)
|
|
self.assertEqual(result["probes"][0]["status"], "GROUNDED")
|
|
# Probe persisted with citations
|
|
probe = json.loads((Path(tmp) / "probe.json").read_text())
|
|
self.assertEqual(probe["status"], "GROUNDED")
|
|
self.assertIn("https://", probe["source"])
|
|
# Route log: INITIAL_ROUTE (+ DOWNGRADE when the probe resolved local
|
|
# authority — §7: transitions persisted with reasons).
|
|
events = [json.loads(l) for l in (Path(tmp) / "route.jsonl").read_text().strip().splitlines()]
|
|
self.assertEqual(events[0]["kind"], "INITIAL_ROUTE")
|
|
self.assertEqual(events[-1]["route"], "LOCAL_FIX")
|
|
kinds = [e["kind"] for e in events]
|
|
if "DOWNGRADE" in kinds:
|
|
dg = next(e for e in events if e["kind"] == "DOWNGRADE")
|
|
self.assertEqual(dg["trigger"], "probe_grounded_local_authority")
|
|
self.assertEqual(dg["previous_route"], "STANDARD_CHANGE")
|
|
self.assertIn("evidence probe GROUNDED", events[0].get("provenance", {}).get("local_authority", ""))
|
|
|
|
|
|
class Canary3UnresolvedEscalates(unittest.TestCase):
|
|
def test_still_unknown_escalates_to_researched_change(self):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
ex = _make_executor(tmp, AMBIGUOUS_ISSUE, _runner_returning(dict(UNKNOWN_PROBE)))
|
|
result = ex.execute(_issue_event(9100))
|
|
|
|
# Escalation: enters the canonical chain (no terminal local route)
|
|
self.assertNotEqual(result.get("status"), "ROUTED_LOCAL_FIX")
|
|
events = [json.loads(l) for l in (Path(tmp) / "route.jsonl").read_text().strip().splitlines()]
|
|
kinds = [e["kind"] for e in events]
|
|
self.assertIn("ESCALATION", kinds)
|
|
esc = next(e for e in events if e["kind"] == "ESCALATION")
|
|
self.assertEqual(esc["new_route"], "RESEARCHED_CHANGE")
|
|
self.assertEqual(esc["trigger"], "evidence_probe_unresolved")
|
|
self.assertIn("STILL_UNKNOWN", esc["evidence"])
|
|
# Transition identity fields present
|
|
for field in ("previous_route", "trigger", "evidence"):
|
|
self.assertIn(field, esc)
|
|
# Probe recorded the failure honestly — in the PERSISTED state
|
|
# (the escalation path returns into the chain; the durable
|
|
# artifact is the tree, not the ephemeral return value).
|
|
self.assertEqual(ex.tree.state["probes"][0]["status"], "STILL_UNKNOWN")
|
|
probe = json.loads((Path(tmp) / "probe.json").read_text())
|
|
self.assertEqual(probe["status"], "STILL_UNKNOWN")
|
|
|
|
def test_no_probe_recursion_budget(self):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
for i in range(evidence_probe.MAX_PROBES_PER_TASK):
|
|
evidence_probe.run_probe(f"unknown {i}", tmp, runner=_runner_returning(dict(UNKNOWN_PROBE)))
|
|
with self.assertRaises(evidence_probe.ProbeBudgetExceeded):
|
|
evidence_probe.run_probe("one too many", tmp, runner=_runner_returning(dict(UNKNOWN_PROBE)))
|
|
|
|
|
|
class ProbeModuleGates(unittest.TestCase):
|
|
def test_schema_validation(self):
|
|
bad = {"question": "q", "source": "s", "answer": "a", "implication": "i", "status": "MAYBE"}
|
|
errors = evidence_probe.validate_probe(bad)
|
|
self.assertTrue(any("status" in e for e in errors))
|
|
grounded_no_source = dict(GROUNDED_PROBE, source="x")
|
|
errors = evidence_probe.validate_probe(grounded_no_source)
|
|
self.assertTrue(any("source" in e for e in errors))
|
|
self.assertEqual(evidence_probe.validate_probe(dict(GROUNDED_PROBE)), [])
|
|
|
|
def test_missing_artifact_fails_loud(self):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
|
|
def silent_runner(prompt, artifact_path):
|
|
pass # worker wrote nothing
|
|
|
|
with self.assertRaises(ValueError) as ctx:
|
|
evidence_probe.run_probe("q", tmp, runner=silent_runner)
|
|
self.assertIn("declared artifact", str(ctx.exception))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|