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
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.
218 lines
7.5 KiB
Python
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
|