Files
rune/tests/test_runner_evidence.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

146 lines
5.3 KiB
Python

"""Red-first tests for scripts/runner_evidence.py — RP2/#265 runner placement.
THE HEAVY RUNNER IS THE DESKTOP PC, NOT THE PI.
A desktop-heavy stage executed on the Pi must NOT count as valid acceptance.
Evidence must be captured at runtime, not inferred from configuration.
"""
from __future__ import annotations
import importlib.util
import json
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
SPEC = importlib.util.spec_from_file_location("runner_evidence", ROOT / "scripts" / "runner_evidence.py")
re_mod = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(re_mod)
def test_evidence_captures_hostname_and_identity(monkeypatch):
monkeypatch.delenv("RUNNER_NAME", raising=False)
monkeypatch.delenv("GITEA_RUNNER_NAME", raising=False)
ev = re_mod.capture_execution_evidence("OUTWARD", issue=300, process_id="research-300-r1")
assert ev["runner_hostname"]
assert ev["runner_name"].startswith("local:")
assert ev["stage"] == "OUTWARD"
assert ev["issue_id"] == 300
assert ev["research_process_id"] == "research-300-r1"
assert ev["timestamp"] # actual timestamp, not inferred
def test_evidence_reads_runner_env(monkeypatch):
monkeypatch.setenv("RUNNER_NAME", "rune-ci-desktop")
monkeypatch.setenv("RUNNER_LABELS", "rune-ci-desktop,desktop-heavy")
ev = re_mod.capture_execution_evidence("OUTWARD")
assert ev["runner_name"] == "rune-ci-desktop"
assert "rune-ci-desktop" in ev["runner_labels"]
def test_desktop_execution_accepted():
ev = {
"runner_hostname": "nick-desktop",
"runner_name": "rune-ci-desktop",
"runner_labels": ["rune-ci-desktop"],
"execution_class": "desktop-heavy",
"stage": "OUTWARD",
"timestamp": "2026-09-09T00:00:00+00:00",
}
ok, violations = re_mod.validate_placement(ev)
assert ok, violations
def test_desktop_heavy_on_pi_rejected():
"""THE negative acceptance: Pi execution of a desktop-heavy stage = REJECT."""
ev = {
"runner_hostname": "rune-pi",
"runner_name": "rune-infra-pi",
"runner_labels": ["rune-infra-pi:host"],
"execution_class": "desktop-heavy",
"stage": "OUTWARD",
"timestamp": "2026-09-09T00:00:00+00:00",
}
ok, violations = re_mod.validate_placement(ev)
assert not ok
assert any("PLACEMENT VIOLATION" in v for v in violations)
def test_pi_via_hostname_only_rejected():
"""Cannot hide the Pi behind a desktop runner name — hostname is checked too."""
ev = {
"runner_hostname": "raspberrypi",
"runner_name": "rune-ci-desktop",
"runner_labels": ["rune-ci-desktop"],
"execution_class": "desktop-heavy",
"stage": "SYNTHESIS",
"timestamp": "2026-09-09T00:00:00+00:00",
}
ok, violations = re_mod.validate_placement(ev)
assert not ok
def test_missing_evidence_fields_rejected():
"""Incomplete evidence is not acceptance — identity must be actually captured."""
ev = {"runner_name": "rune-ci-desktop", "stage": "OUTWARD"}
ok, violations = re_mod.validate_placement(ev)
assert not ok
assert any("missing field" in v for v in violations)
def test_unknown_runner_rejected_for_heavy_stage():
"""No silent fallback: an unverified runner cannot execute heavy stages."""
ev = {
"runner_hostname": "some-laptop",
"runner_name": "mystery-runner",
"runner_labels": ["mystery-runner"],
"execution_class": "desktop-heavy",
"stage": "OUTWARD",
"timestamp": "2026-09-09T00:00:00+00:00",
}
ok, violations = re_mod.validate_placement(ev)
assert not ok
assert any("not 'rune-ci-desktop'" in v for v in violations)
def test_all_canonical_stages_are_desktop_heavy():
"""Every cognitive stage requires the desktop. ISSUE gate is deterministic
but still validated for placement completeness."""
for stage in ("INWARD", "OUTWARD", "APPLICABILITY", "INNOVATION", "ADVERSARIAL", "SYNTHESIS", "QUALIFY"):
assert stage in re_mod.DESKTOP_HEAVY_STAGES
def test_lightweight_pi_operations_allowed():
"""Forge/control work (metadata, labels, eligibility) may run on the Pi."""
ev = {
"runner_hostname": "rune-pi",
"runner_name": "rune-infra-pi",
"runner_labels": ["rune-infra-pi:host"],
"execution_class": "pi-lightweight",
"stage": "label_change",
"timestamp": "2026-09-09T00:00:00+00:00",
}
ok, violations = re_mod.validate_placement(ev)
assert ok, violations
def test_evidence_serializes_to_artifact():
ev = re_mod.capture_execution_evidence("OUTWARD", issue=1, process_id="research-1-r1")
summarized = re_mod.summarize_for_artifact(ev)
assert "monotonic" not in summarized
json.dumps(summarized) # must be JSON-serializable for artifact embedding
def test_registered_runner_name_accepted():
"""The Gitea-registered runner NAME (rune-desktop) with the desktop label
is a valid desktop identity — this is the exact identity CI jobs carry."""
ev = {
"runner_hostname": "nick-desktop",
"runner_name": "rune-desktop",
"runner_labels": ["rune-ci-desktop"],
"execution_class": "desktop-heavy",
"stage": "OUTWARD",
"timestamp": "2026-09-09T10:00:00+00:00",
}
ok, violations = re_mod.validate_placement(ev)
assert ok, violations