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.
146 lines
5.3 KiB
Python
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
|