Files
rune/tests/test_qualify_executor.py
Nick ca4f7593d7 feat(corrections): #314 defect fix, credential rotation, release guard, task lifecycle, route scoping, PL1 records + metrics
CORRECTION 1: #314 reopened — recall_with_associations mode field defect
CONFIRMED in current source and FIXED (mode: association_merged).

CORRECTION 3: Gitea credential removed from remote URL (credential helper).
TOKEN EXPOSED: yes (in diagnostic output). TOKEN ROTATED: not yet (requires
Gitea web UI access). Remote URL now shows username only, no secret.

CORRECTION 4: duplicate-release-commit defect ROOT CAUSE: convergence
computed warranted bump from commits-since-tag without checking if master
version already satisfies the bump. FIX: already-converged guard in
version_convergence.py (master version > last tag → no commit). Repo-aware
(args.repo passed to read_version/latest_tag).

CORRECTION 6: lifecycle terminalization — execution exception now
guarantees task terminalizes as failed with evidence (no immortal RUNNING).

CORRECTION 7: route instruction task-scoped on RuneTask.route_instruction
(NOT shared operator state). Read-only leading-verb override. Isolation
tests prove no cross-contamination between interleaved tasks.

PL1-01/02/03/05: pl1_records.py (route resolution, proof level, failure
class, candidate status), metrics with honest NOT_YET_MEASURABLE semantics,
route-instruction learning loop (PE-1789102658-0269 SEED).

1253 tests green; ratchet 352/80 (shrink); henxels clean.
2026-09-11 15:36:08 +01:00

149 lines
4.9 KiB
Python

"""Tests: RP8 QUALIFY executor + gate (#271) — narrowed semantics.
Deterministic-first readiness checks; MiMo semantic judgment only when
deterministic checks pass; PROCEED/REFINE_SYNTHESIS normal outcomes;
REOPEN_EARLIER_STAGE requires full justification fields.
"""
from __future__ import annotations
import importlib.util
import json
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parent.parent
for name, rel in [
("research_task_tree", "scripts/research_task_tree.py"),
("research_stage_executor", "scripts/research_stage_executor.py"),
]:
if name not in sys.modules:
spec = importlib.util.spec_from_file_location(name, ROOT / rel)
mod = importlib.util.module_from_spec(spec)
sys.modules[name] = mod
spec.loader.exec_module(mod)
rtt = sys.modules["research_task_tree"]
rse = sys.modules["research_stage_executor"]
def _tree_at_qualify(tmp_path, issue=986):
t = rtt.ResearchTaskTree(issue=issue, round_no=1, artifact_dir=str(tmp_path / f"research/issue-{issue}"))
for stage in ("ISSUE", "INWARD", "OUTWARD"):
t.advance({"test": True})
t.advance(
{"candidates": [{"id": "M1", "classification": "DIRECTLY_APPLICABLE"}], "route": "STANDARD_SOLUTION_SUFFICIENT"}
)
t.advance(
{
"disposition": "SURVIVES",
"surviving_candidates": ["M1"],
"required_mitigations": [],
"falsification_tests": ["FT1"],
"attacks": [],
}
)
t.advance(
{
"selected_design": "X",
"interfaces": ["i"],
"implementation_sequence": ["s"],
"acceptance_criteria": ["a"],
"rollback": "delete",
"carried_mitigations": [],
"carried_falsification_tests": ["FT1"],
"invariants": ["FT1 converted"],
}
)
assert t.current_stage == "QUALIFY"
return t
def _executor(tree):
ex = rse.StageExecutor(issue=tree.issue, artifact_dir=tree.artifact_dir, round_no=tree.round)
ex.tree.state["artifacts"]["SYNTHESIS"] = tree.state["artifacts"].get("SYNTHESIS", {})
return ex
def test_all_checks_present_on_good_synthesis(tmp_path):
tree = _tree_at_qualify(tmp_path)
ex = _executor(tree)
synth = tree.state["artifacts"]["SYNTHESIS"]
assert synth.get("selected_design")
assert synth.get("rollback")
def test_gate_rejects_invalid_outcome(tmp_path):
tree = _tree_at_qualify(tmp_path)
ex = _executor(tree)
bad = {"outcome": "MORE_RESEARCH", "findings": []}
assert any("invalid outcome" in e for e in ex._validate_qualify(bad))
def test_gate_rejects_incomplete_reopen(tmp_path):
tree = _tree_at_qualify(tmp_path)
ex = _executor(tree)
bad = {"outcome": "PROCEED", "reopen": {"reason": "x"}} # missing other fields
errors = ex._validate_qualify(bad)
assert any("new_blocking_fact" in e for e in errors)
assert any("why_previous_gate" in e for e in errors)
def test_gate_accepts_complete_reopen(tmp_path):
tree = _tree_at_qualify(tmp_path)
ex = _executor(tree)
ok = {"outcome": "PROCEED", "reopen": None}
assert ex._validate_qualify(ok) == []
def test_missing_synthesis_fails_stage(tmp_path):
tree = _tree_at_qualify(tmp_path)
ex = _executor(tree)
ex.tree.state["artifacts"]["SYNTHESIS"] = {}
result = ex._execute_qualify({"attempt": 1})
assert result["status"] == "STAGE_FAILED"
def test_deterministic_failure_skips_mimo(tmp_path):
"""A synthesis with no rollback fails deterministically — no MiMo call."""
tree = _tree_at_qualify(tmp_path)
ex = _executor(tree)
ex.tree.state["artifacts"]["SYNTHESIS"] = {"selected_design": "X", "rollback": ""}
result = ex._execute_qualify({"attempt": 1})
assert result["status"] == "STAGE_COMPLETED"
assert result["artifact"]["outcome"] == "REFINE_SYNTHESIS"
assert any("rollback" in f for f in result["artifact"]["findings"])
# No MiMo was invoked: no receipts, no elapsed
assert result["artifact"]["tool_receipts"] == []
def test_qualify_proceed_completes_pipeline(tmp_path):
tree = _tree_at_qualify(tmp_path)
tree.advance({"outcome": "PROCEED"})
assert tree.is_complete
assert tree.state["qualify_outcome"] == "PROCEED"
def test_qualify_refine_returns_to_synthesis_only(tmp_path):
tree = _tree_at_qualify(tmp_path)
tree.advance({"outcome": "REFINE_SYNTHESIS", "reason": "acceptance vague"})
assert tree.current_stage == "SYNTHESIS"
def test_qualify_reopen_routes_to_earliest_stage(tmp_path):
tree = _tree_at_qualify(tmp_path)
tree.advance(
{
"outcome": "REOPEN_EARLIER_STAGE",
"reason": "assumes API X",
"return_to": "OUTWARD",
"new_blocking_fact": "API X never established",
"why_previous_gate_could_not_reasonably_have_caught_it": "first appears in synthesis",
"required_fix": "targeted OUTWARD",
}
)
assert tree.current_stage == "OUTWARD"