Files
rune/tests/test_adversarial_gated_refinement.py
T
Nick ca4f7593d7
CI Pipeline / best-practices (push) Successful in 8s
Auto-version and Release / release (push) Failing after 14s
CI Pipeline / documentation (push) Successful in 5s
CI Pipeline / lint (push) Failing after 34s
CI Pipeline / canary (push) Successful in 37s
CI Pipeline / type-check (push) Successful in 31s
CI Pipeline / gitea-offline (push) Successful in 16s
CI Pipeline / unit-tests (push) Successful in 1m4s
CI Pipeline / required-checks (push) Successful in 1s
deploy / deploy (push) Successful in 48s
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

218 lines
7.5 KiB
Python

"""Tests: adversarial-gated research refinement + narrowed QUALIFY (correction 2026-09-09).
Acceptance matrix from §18 of the amendment, proven as deterministic behavior.
"""
from __future__ import annotations
import importlib.util
import json
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parent.parent
if "research_task_tree" not in sys.modules:
spec = importlib.util.spec_from_file_location("research_task_tree", ROOT / "scripts" / "research_task_tree.py")
mod = importlib.util.module_from_spec(spec)
sys.modules["research_task_tree"] = mod
spec.loader.exec_module(mod)
rtt = sys.modules["research_task_tree"]
def _tree_at_adversarial(tmp_path, issue=984):
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"}
)
assert t.current_stage == "ADVERSARIAL"
return t
BASE_SURVIVOR = {
"disposition": "SURVIVES_WITH_MITIGATIONS",
"surviving_candidates": ["M1"],
"required_mitigations": ["mode labeling", "cache controls"],
"falsification_tests": ["FT1", "FT2"],
"attacks": [{"id": "A1", "target_candidate": "M1", "dimension": "failure modes", "finding": "x"}],
}
def test_disposition_enum_declared():
assert rtt.ADVERSARIAL_DISPOSITIONS == {
"SURVIVES",
"SURVIVES_WITH_MITIGATIONS",
"EVIDENCE_DEFICIENT",
"APPLICABILITY_DEFICIENT",
"INNOVATION_WARRANTED",
"NO_VIABLE_CANDIDATE",
}
assert rtt.SYNTHESIS_UNLOCKING_DISPOSITIONS == {"SURVIVES", "SURVIVES_WITH_MITIGATIONS"}
def test_survives_unlocks_synthesis(tmp_path):
t = _tree_at_adversarial(tmp_path)
t.advance({**BASE_SURVIVOR, "disposition": "SURVIVES", "required_mitigations": []})
assert t.current_stage == "SYNTHESIS"
assert t.state["adversarial_disposition"] == "SURVIVES"
def test_survives_with_mitigations_unlocks_synthesis(tmp_path):
t = _tree_at_adversarial(tmp_path)
t.advance(BASE_SURVIVOR)
assert t.current_stage == "SYNTHESIS"
def test_evidence_deficient_routes_to_outward(tmp_path):
t = _tree_at_adversarial(tmp_path)
t.advance(
{
"disposition": "EVIDENCE_DEFICIENT",
"surviving_candidates": [],
"reason": "anchor weak",
"challenged_claims": ["E5"],
}
)
assert t.current_stage == "OUTWARD"
assert t.current_stage != "SYNTHESIS"
def test_applicability_deficient_routes_to_applicability(tmp_path):
t = _tree_at_adversarial(tmp_path)
t.advance(
{"disposition": "APPLICABILITY_DEFICIENT", "surviving_candidates": [], "reason": "integration point wrong"}
)
assert t.current_stage == "APPLICABILITY"
def test_innovation_warranted_routes_to_innovation(tmp_path):
t = _tree_at_adversarial(tmp_path)
t.advance(
{
"disposition": "INNOVATION_WARRANTED",
"surviving_candidates": ["M1"],
"reason": "leading option killed; compositional opportunity",
}
)
assert t.current_stage == "INNOVATION"
def test_no_viable_candidate_routes_to_outward(tmp_path):
t = _tree_at_adversarial(tmp_path)
t.advance({"disposition": "NO_VIABLE_CANDIDATE", "surviving_candidates": [], "reason": "all killed"})
assert t.current_stage == "OUTWARD"
def test_missing_disposition_raises(tmp_path):
t = _tree_at_adversarial(tmp_path)
with pytest.raises(ValueError, match="disposition"):
t.advance({"surviving_candidates": ["M1"], "attacks": []})
def test_readiness_invariant_no_survivor(tmp_path):
t = _tree_at_adversarial(tmp_path)
with pytest.raises(ValueError, match="readiness invariant"):
t.advance(
{
"disposition": "SURVIVES",
"surviving_candidates": [],
"required_mitigations": [],
"falsification_tests": [],
}
)
def test_readiness_invariant_mitigations_required(tmp_path):
t = _tree_at_adversarial(tmp_path)
with pytest.raises(ValueError, match="mitigations"):
t.advance(
{
"disposition": "SURVIVES_WITH_MITIGATIONS",
"surviving_candidates": ["M1"],
"required_mitigations": [],
"falsification_tests": [],
}
)
def test_readiness_invariant_blocking_unknowns_block(tmp_path):
t = _tree_at_adversarial(tmp_path)
t.state["blocking_unknowns"] = [{"id": "U1", "question": "?"}]
ok, problems = t.synthesis_readiness(BASE_SURVIVOR)
assert not ok
assert any("blocking" in p for p in problems)
def test_falsification_and_mitigations_survive_into_synthesis_inputs(tmp_path):
"""§5: kill-tests and mitigations are first-class synthesis inputs."""
t = _tree_at_adversarial(tmp_path)
t.advance(BASE_SURVIVOR)
assert t.current_stage == "SYNTHESIS"
adv = t.state["artifacts"]["ADVERSARIAL"]
assert adv["falsification_tests"] == ["FT1", "FT2"]
assert adv["required_mitigations"] == ["mode labeling", "cache controls"]
# capsule hands them to SYNTHESIS
cap = t.capsule()
assert cap["accepted_artifacts"]["ADVERSARIAL"]["falsification_tests"] == ["FT1", "FT2"]
def test_qualify_narrowed_outcomes(tmp_path):
t = _tree_at_adversarial(tmp_path)
t.advance(BASE_SURVIVOR)
for stage in ("SYNTHESIS", "QUALIFY"):
t.advance({"test": True} if stage == "SYNTHESIS" else {"outcome": "PROCEED"})
assert t.is_complete
def test_qualify_refine_synthesis_returns_to_synthesis_only(tmp_path):
t = _tree_at_adversarial(tmp_path)
t.advance(BASE_SURVIVOR)
t.advance({"build_instructions": ["x"]}) # SYNTHESIS
t.advance({"outcome": "REFINE_SYNTHESIS", "reason": "rollback missing"})
assert t.current_stage == "SYNTHESIS"
# NOT OUTWARD, NOT a research repeat
assert "OUTWARD" not in t.state["targeted_branch_stages"] if "targeted_branch_stages" in t.state else True
def test_qualify_reopen_earlier_stage_requires_full_fields(tmp_path):
t = _tree_at_adversarial(tmp_path)
t.advance(BASE_SURVIVOR)
t.advance({"build_instructions": ["x"]})
with pytest.raises(ValueError, match="REOPEN_EARLIER_STAGE requires"):
t.advance({"outcome": "REOPEN_EARLIER_STAGE", "reason": "assumption unverified"})
def test_qualify_reopen_earlier_stage_routes(tmp_path):
t = _tree_at_adversarial(tmp_path)
t.advance(BASE_SURVIVOR)
t.advance({"build_instructions": ["x"]})
t.advance(
{
"outcome": "REOPEN_EARLIER_STAGE",
"reason": "synthesis assumes API X exists",
"return_to": "OUTWARD",
"new_blocking_fact": "API X was never established in any accepted artifact",
"why_previous_gate_could_not_reasonably_have_caught_it": "assumption first appears in synthesis text",
"required_fix": "targeted OUTWARD for API X",
}
)
assert t.current_stage == "OUTWARD"
def test_qualify_invalid_outcome_rejected(tmp_path):
t = _tree_at_adversarial(tmp_path)
t.advance(BASE_SURVIVOR)
t.advance({"build_instructions": ["x"]})
with pytest.raises(ValueError, match="outcome"):
t.advance({"outcome": "MORE_RESEARCH"})
def test_no_generic_more_research_route():
"""§11: generic end-of-pipeline MORE_RESEARCH is not a valid outcome."""
assert "MORE_RESEARCH" not in rtt.QUALIFY_OUTCOMES
assert "MORE_RESEARCH" not in rtt.ADVERSARIAL_DISPOSITIONS