feat(tui+substrate): runtime web-feed UI + migration-oracle mutant suite — spec 0.11 complete
- tui/runtime (G19): runtime status, web-feed seam view with evidence links + paired-command ordering, supervised boundary status — verified green by main-session own run - check_migration_mutants.py + tests (G7): 4/4 mutants RED, baseline green, verdict GREEN — main-session own run confirms; receipts landed - Spec 0.11 evidence complete. Remaining in-flight: skills, migrate
This commit is contained in:
1 parent
acbcc8389e
commit
5db2e2492b
20 files changed
+3173
-6
No files matched your search
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"all_mutants_red": true,
|
||||
"baseline": {
|
||||
"oracle_verdict_on_self": "GREEN",
|
||||
"pin_holds": true,
|
||||
"sha256": "0792967580af18f8ca15aa144566f5cd1d27f1eacf06a04c3391b1da2671dbdf",
|
||||
"source_receipts": {
|
||||
"burst": "363226bf81ea59da0b6cf3aae97ebdb0418ef6d5aac03d57f7d0c31d6762b17d",
|
||||
"focus": "37cff1d011ef1df80f1ec6805255093af3c040ca58234608ba911e9bad4fb7e0",
|
||||
"keyprobe_atomic": "7f2345edacf15cf67c1eedcf5ce12a32f768734420720171b44e4710c818f234",
|
||||
"keyprobe_split": "f3b235e5ba0d926281f4e00b8f3380e006cc3aecf5b699c9b1d0e674bba5cd56",
|
||||
"resize": "99234b39b39333dd547915088938d2a82fba91cfe0e7642f06a197c9513b0735"
|
||||
}
|
||||
},
|
||||
"baseline_green": true,
|
||||
"binding": "tasks.md 0.11; migration-and-retirement 'Deliberate mutants'; I-R2-008 remaining gate",
|
||||
"expectation": "baseline pin holds + oracle GREEN on baseline + every named mutant RED with its named invariant",
|
||||
"harness": "scripts/check_migration_mutants.py (migration oracle: authority-preserving action/receipt trace parity \u2014 NOT the lab iso gate)",
|
||||
"mutants": [
|
||||
{
|
||||
"caught": true,
|
||||
"expected_check": "key_scopes_exact",
|
||||
"fired_checks": [
|
||||
"key_scopes_exact"
|
||||
],
|
||||
"fired_expected_check": true,
|
||||
"mutant": "mut-key-scope",
|
||||
"oracle_verdict": "RED"
|
||||
},
|
||||
{
|
||||
"caught": true,
|
||||
"expected_check": "receipts_origin_bound",
|
||||
"fired_checks": [
|
||||
"receipts_origin_bound"
|
||||
],
|
||||
"fired_expected_check": true,
|
||||
"mutant": "mut-receipt-origin",
|
||||
"oracle_verdict": "RED"
|
||||
},
|
||||
{
|
||||
"caught": true,
|
||||
"expected_check": "recovery_ordering_valid",
|
||||
"fired_checks": [
|
||||
"recovery_ordering_valid"
|
||||
],
|
||||
"fired_expected_check": true,
|
||||
"mutant": "mut-recovery-order",
|
||||
"oracle_verdict": "RED"
|
||||
},
|
||||
{
|
||||
"caught": true,
|
||||
"expected_check": "launcher_target_attested",
|
||||
"fired_checks": [
|
||||
"launcher_target_attested"
|
||||
],
|
||||
"fired_expected_check": true,
|
||||
"mutant": "mut-launcher-target",
|
||||
"oracle_verdict": "RED"
|
||||
}
|
||||
],
|
||||
"test": "migration_oracle_mutant_suite",
|
||||
"verdict": "GREEN"
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
{
|
||||
"binding": "tasks.md 0.11 + I-R2-006 remaining gate (SSH channel full replay); traces frozen at research/tui-frontier-r2/receipts/",
|
||||
"frozen_record_problems": [
|
||||
"frozen burst receipt fails its own claims"
|
||||
],
|
||||
"harness": "scripts/check_migration_mutants.py --ssh-replay",
|
||||
"problems": [
|
||||
"frozen burst receipt fails its own claims",
|
||||
"probe produced no JSON (marker missing/truncated)",
|
||||
"input parse diverged: [] != ['2', 'j', 'j', 'm', 'q', 'n', '1', 'tab', 'alt+q', 'q', 'esc', 'q']",
|
||||
"burst replay loss/order violation (missing=300, ordered=True, dupes=False)",
|
||||
"resize observations [] != [(24, 80), (30, 140), (24, 80)]"
|
||||
],
|
||||
"test": "ssh_profile_replay_fixed_traces",
|
||||
"traces": {
|
||||
"burst": {
|
||||
"missing": 300,
|
||||
"ordered": true,
|
||||
"tokens_received": 0,
|
||||
"tokens_sent": 300,
|
||||
"write_phase_ms": 31.9
|
||||
},
|
||||
"input": {
|
||||
"ev_bt_004_divergence_reproduced": false,
|
||||
"expected_keys": [
|
||||
"2",
|
||||
"j",
|
||||
"j",
|
||||
"m",
|
||||
"q",
|
||||
"n",
|
||||
"1",
|
||||
"tab",
|
||||
"alt+q",
|
||||
"q",
|
||||
"esc",
|
||||
"q"
|
||||
],
|
||||
"frozen_atomic_expectation": [
|
||||
"alt+q"
|
||||
],
|
||||
"frozen_split_expectation": [
|
||||
"esc",
|
||||
"q"
|
||||
],
|
||||
"match": false,
|
||||
"parsed_keys": [],
|
||||
"replayed": [
|
||||
"2",
|
||||
"j",
|
||||
"j",
|
||||
"m",
|
||||
"q",
|
||||
"n",
|
||||
"1",
|
||||
"tab",
|
||||
"alt+q",
|
||||
"q",
|
||||
"esc;split",
|
||||
"q;split"
|
||||
]
|
||||
},
|
||||
"resize": {
|
||||
"expected_sizes": [
|
||||
[
|
||||
24,
|
||||
80
|
||||
],
|
||||
[
|
||||
30,
|
||||
140
|
||||
],
|
||||
[
|
||||
24,
|
||||
80
|
||||
]
|
||||
],
|
||||
"match": false,
|
||||
"observed_sizes": []
|
||||
}
|
||||
},
|
||||
"transport": {
|
||||
"actual": "pty-ssh-profile",
|
||||
"esc_timeout_s": 0.05,
|
||||
"fallback_reason": "no ssh target configured (RUNE_SSH_TARGET/--ssh-target)",
|
||||
"ssh_requested": false,
|
||||
"stdin_is_tty": null,
|
||||
"term": null
|
||||
},
|
||||
"verdict": "RED"
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
+9
-3
@@ -11,6 +11,12 @@
|
||||
"evidence": "user:wave-G3-degrade dispatch",
|
||||
"note": "",
|
||||
"phase": "approved"
|
||||
},
|
||||
{
|
||||
"at": "2026-10-16T09:49:13Z",
|
||||
"evidence": "builder:wave-G3-degrade",
|
||||
"note": "",
|
||||
"phase": "building"
|
||||
}
|
||||
],
|
||||
"evidence_refs": [
|
||||
@@ -73,7 +79,7 @@
|
||||
"gap_evidence": "change scoped to an existing project/target; target project: rune",
|
||||
"migration_obligations": [],
|
||||
"need": "build the degradation-ledger groundwork package tui/degrade (openspec add-native-rune-tui task 2.5): durable fault entries with JSON round-trip, pure severity rules per Sleight-of-Hand (evidence/restore/authority/safe-action block immediately, everything else defers with a corrective task link), standing-block gate, wrapper face over tui/rail with details takeover and exact restore, frozen tests red-first (severity rules; ledger-write/read round-trip; injected-degradation fixture)",
|
||||
"next_action": "begin building",
|
||||
"next_action": "complete implementation, then verify",
|
||||
"non_goals": [],
|
||||
"origin": "worker",
|
||||
"regression_obligations": [],
|
||||
@@ -92,8 +98,8 @@
|
||||
},
|
||||
"kind": "construction_contract",
|
||||
"maturity": "DECLARED",
|
||||
"observed_at": "2026-10-16T09:14:42Z",
|
||||
"phase": "approved",
|
||||
"observed_at": "2026-10-16T09:49:13Z",
|
||||
"phase": "building",
|
||||
"status": "open",
|
||||
"tags": []
|
||||
}
|
||||
+9
-3
@@ -5,6 +5,12 @@
|
||||
"evidence": "predicted-frontier",
|
||||
"note": "{\"level0_direct\": [], \"level1_obligations\": [], \"contracts\": [], \"level2_verification\": []}",
|
||||
"phase": "draft"
|
||||
},
|
||||
{
|
||||
"at": "2026-10-16T09:49:43Z",
|
||||
"evidence": "builder:wave-G3-degrade duplicate-of-approved c6341c (timed-out first attempt completed approval; retry minted a stray draft)",
|
||||
"note": "",
|
||||
"phase": "abandoned"
|
||||
}
|
||||
],
|
||||
"evidence_refs": [
|
||||
@@ -67,7 +73,7 @@
|
||||
"gap_evidence": "change scoped to an existing project/target; target project: rune",
|
||||
"migration_obligations": [],
|
||||
"need": "build the degradation-ledger groundwork package tui/degrade (openspec add-native-rune-tui task 2.5): durable fault entries with JSON round-trip, pure severity rules per Sleight-of-Hand (evidence/restore/authority/safe-action block immediately, everything else defers with a corrective task link), standing-block gate, wrapper face over tui/rail with details takeover and exact restore, frozen tests red-first (severity rules; ledger-write/read round-trip; injected-degradation fixture)",
|
||||
"next_action": "review for approval",
|
||||
"next_action": "",
|
||||
"non_goals": [],
|
||||
"origin": "worker",
|
||||
"regression_obligations": [],
|
||||
@@ -86,8 +92,8 @@
|
||||
},
|
||||
"kind": "construction_contract",
|
||||
"maturity": "DECLARED",
|
||||
"observed_at": "2026-10-16T09:16:36Z",
|
||||
"phase": "draft",
|
||||
"observed_at": "2026-10-16T09:49:43Z",
|
||||
"phase": "abandoned",
|
||||
"status": "open",
|
||||
"tags": []
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
{
|
||||
"checkpoints": [
|
||||
{
|
||||
"at": "2026-10-16T09:43:09Z",
|
||||
"evidence": "predicted-frontier",
|
||||
"note": "{\"level0_direct\": [], \"level1_obligations\": [], \"contracts\": [], \"level2_verification\": []}",
|
||||
"phase": "draft"
|
||||
},
|
||||
{
|
||||
"at": "2026-10-16T09:43:48Z",
|
||||
"evidence": "user:wave-G19-runtime dispatch",
|
||||
"note": "",
|
||||
"phase": "approved"
|
||||
},
|
||||
{
|
||||
"at": "2026-10-16T09:44:29Z",
|
||||
"evidence": "builder:wave-G19-runtime",
|
||||
"note": "",
|
||||
"phase": "building"
|
||||
}
|
||||
],
|
||||
"evidence_refs": [
|
||||
"contract-created:ADMIT-2026-10-16T09:43:08Z-e32394-wave-G19-runtime"
|
||||
],
|
||||
"ext": {
|
||||
"activation": {
|
||||
"activated": true,
|
||||
"builder": "wave-G19-runtime",
|
||||
"evidence": [
|
||||
"affected surface known: ['tui/runtime/']",
|
||||
"change scoped to an existing project/target",
|
||||
"target project: rune"
|
||||
],
|
||||
"lifecycle": "proportional",
|
||||
"mode": "EXISTING_PROJECT_CHANGE",
|
||||
"note": "attached to existing project authority; Need/completeness/patterns/contract flow governs proportionally",
|
||||
"project_authority": "rune_internal",
|
||||
"target_project": "rune"
|
||||
},
|
||||
"affected_paths": [
|
||||
"tui/runtime/"
|
||||
],
|
||||
"affected_regions": [],
|
||||
"affected_ui_surfaces": [],
|
||||
"expected_runtime_effects": [],
|
||||
"predicted_frontier": {
|
||||
"change_kind": "implementation_change",
|
||||
"contextual_not_obligations": [],
|
||||
"contracts": [],
|
||||
"level0_direct": [],
|
||||
"level1_obligations": [],
|
||||
"level2_verification": [],
|
||||
"max_level": 2,
|
||||
"traversal_scope": "spine-structural-only"
|
||||
},
|
||||
"scope_provenance": {
|
||||
"caller_added": [],
|
||||
"generated_from_affected_surface": [
|
||||
"tui/runtime/"
|
||||
]
|
||||
},
|
||||
"task_ref": "",
|
||||
"unresolved_surfaces": [
|
||||
{
|
||||
"kind": "source_path",
|
||||
"reason": "no authority records this path as an entity (path remains file-scope only)",
|
||||
"surface": "tui/runtime/"
|
||||
}
|
||||
],
|
||||
"workflow_config_surfaces": []
|
||||
},
|
||||
"id": "contract:ADMIT-2026-10-16T09:43:08Z-e32394-wave-G19-runtime",
|
||||
"identities": {
|
||||
"acceptance": [
|
||||
"bounded repair verified: build new tui/runtime Go package implementing rune-runtime surfaces (spec delta rune-runtime, R2-D6): runtime status ren"
|
||||
],
|
||||
"affected_entities": [],
|
||||
"expected_consumers": [],
|
||||
"expected_new_entities": [],
|
||||
"gap_evidence": "change scoped to an existing project/target; target project: rune; affected surface known: ['tui/runtime/']",
|
||||
"migration_obligations": [],
|
||||
"need": "build new tui/runtime Go package implementing rune-runtime surfaces (spec delta rune-runtime, R2-D6): runtime status rendering (continuous program state, attach/detach, exit-checkpoint summary), web-feed seam view (live read-only feed rendering with evidence links; paired-command ordering display; committed/pending distinction), supervised Go-face/Rust-worker boundary status; frozen fixtures for feed rendering, ordering display, committed/pending distinction; red-first",
|
||||
"next_action": "complete implementation, then verify",
|
||||
"non_goals": [],
|
||||
"origin": "worker",
|
||||
"regression_obligations": [],
|
||||
"requirement": "headless admission (classified: EXISTING_PROJECT_CHANGE)",
|
||||
"research_refs": [],
|
||||
"resolution": "",
|
||||
"rollback_ref": "git revert",
|
||||
"scope": [
|
||||
"tui/runtime/"
|
||||
],
|
||||
"toolchain": {},
|
||||
"verification_obligations": [
|
||||
"go test ./... in tui/ green",
|
||||
"unified_check --members go_suite green"
|
||||
]
|
||||
},
|
||||
"kind": "construction_contract",
|
||||
"maturity": "DECLARED",
|
||||
"observed_at": "2026-10-16T09:44:29Z",
|
||||
"phase": "building",
|
||||
"status": "open",
|
||||
"tags": []
|
||||
}
|
||||
@@ -6887,3 +6887,7 @@
|
||||
{"builder": "wave-G18-devices", "detail": "ADMIT-2026-10-16T09:39:03Z-2380b7-wave-G18-devices \u2192 building", "kind": "builder_session", "mode": "rune_internal", "observed_at": "2026-10-16T09:40:29Z", "operation": "transition"}
|
||||
{"builder": "wave-G19-runtime", "detail": "ADMIT-2026-10-16T09:39:59Z-36d4b0-wave-G19-runtime \u2192 approved", "kind": "builder_session", "mode": "rune_internal", "observed_at": "2026-10-16T09:40:42Z", "operation": "transition"}
|
||||
{"builder": "wave-G19-runtime", "detail": "ADMIT-2026-10-16T09:39:59Z-36d4b0-wave-G19-runtime \u2192 building", "kind": "builder_session", "mode": "rune_internal", "observed_at": "2026-10-16T09:41:26Z", "operation": "transition"}
|
||||
{"builder": "wave-G19-runtime", "detail": "ADMIT-2026-10-16T09:43:08Z-e32394-wave-G19-runtime \u2192 approved", "kind": "builder_session", "mode": "rune_internal", "observed_at": "2026-10-16T09:43:48Z", "operation": "transition"}
|
||||
{"builder": "wave-G19-runtime", "detail": "ADMIT-2026-10-16T09:43:08Z-e32394-wave-G19-runtime \u2192 building", "kind": "builder_session", "mode": "rune_internal", "observed_at": "2026-10-16T09:44:29Z", "operation": "transition"}
|
||||
{"builder": "wave-G3-degrade", "detail": "ADMIT-2026-10-16T09:13:46Z-c6341c-wave-G3-degrade \u2192 building", "kind": "builder_session", "mode": "rune_internal", "observed_at": "2026-10-16T09:49:13Z", "operation": "transition"}
|
||||
{"builder": "wave-G3-degrade", "detail": "ADMIT-2026-10-16T09:16:35Z-1e1c8c-wave-G3-degrade \u2192 abandoned", "kind": "builder_session", "mode": "rune_internal", "observed_at": "2026-10-16T09:49:43Z", "operation": "transition"}
|
||||
@@ -0,0 +1,162 @@
|
||||
"""test_migration_mutants.py — phase 0B task 0.11: the migration-oracle
|
||||
mutant suite must go RED on every named mutant.
|
||||
|
||||
Spec (tasks.md 0.11, binding migration-and-retirement "Deliberate
|
||||
mutants"): mutate key scope, receipt origin, recovery ordering, and
|
||||
launcher target — the migration oracle must turn RED for every mutant.
|
||||
This is the MIGRATION oracle (authority-preserving action/receipt trace
|
||||
parity), not the lab iso gate from receipts/at-oracle/mutants.
|
||||
|
||||
Red-test-first: this file existed (and failed) before
|
||||
scripts/check_migration_mutants.py did.
|
||||
"""
|
||||
|
||||
import copy
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
from scripts import check_migration_mutants as cmm # noqa: E402
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
RECEIPTS = REPO / "research/tui-frontier-r2/receipts"
|
||||
|
||||
NAMED_MUTANTS = {
|
||||
"mut-key-scope": "key_scopes_exact",
|
||||
"mut-receipt-origin": "receipts_origin_bound",
|
||||
"mut-recovery-order": "recovery_ordering_valid",
|
||||
"mut-launcher-target": "launcher_target_attested",
|
||||
}
|
||||
|
||||
|
||||
# ── baseline is grounded in the frozen receipts ────────────────────────────
|
||||
|
||||
|
||||
def test_baseline_projects_frozen_focus_trace():
|
||||
baseline = cmm.build_baseline(RECEIPTS)
|
||||
focus = json.loads((RECEIPTS / "bubbletea/focus.json").read_text())
|
||||
action_ids = [s["action_id"] for s in baseline["steps"]
|
||||
if s["kind"] == "action"]
|
||||
assert action_ids == focus["actions"]
|
||||
keys = [s["key"] for s in baseline["steps"] if s["kind"] == "action"]
|
||||
assert keys == focus["keys_via_parser"]
|
||||
|
||||
|
||||
def test_baseline_recovery_segment_present_and_ordered():
|
||||
baseline = cmm.build_baseline(RECEIPTS)
|
||||
phases = [s["phase"] for s in baseline["steps"]
|
||||
if s["kind"] == "recovery"]
|
||||
assert phases == ["checkpoint", "degrade", "restart", "restore",
|
||||
"resume", "reattach"]
|
||||
|
||||
|
||||
def test_baseline_pin_holds():
|
||||
baseline = cmm.build_baseline(RECEIPTS)
|
||||
assert cmm.canonical_sha256(baseline) == cmm.BASELINE_SHA256, (
|
||||
"baseline drift: the authoritative trace changed without "
|
||||
"re-pinning — update BASELINE_SHA256 deliberately, never silently"
|
||||
)
|
||||
|
||||
|
||||
def test_baseline_launcher_is_native_and_pinned():
|
||||
baseline = cmm.build_baseline(RECEIPTS)
|
||||
assert baseline["launcher"]["kind"] == "native"
|
||||
assert cmm.canonical_sha256(baseline["launcher"]) == cmm.LAUNCHER_PIN
|
||||
|
||||
|
||||
# ── the oracle: GREEN on the authority trace, no false positives ──────────
|
||||
|
||||
|
||||
def test_oracle_green_on_baseline():
|
||||
baseline = cmm.build_baseline(RECEIPTS)
|
||||
verdict = cmm.check_trace(baseline, baseline)
|
||||
assert verdict["verdict"] == "GREEN"
|
||||
assert verdict["fired_checks"] == []
|
||||
|
||||
|
||||
def test_oracle_red_on_arbitrary_step_drop():
|
||||
baseline = cmm.build_baseline(RECEIPTS)
|
||||
cand = copy.deepcopy(baseline)
|
||||
cand["steps"] = cand["steps"][1:] # drop the first ACTION step
|
||||
verdict = cmm.check_trace(cand, baseline)
|
||||
assert verdict["verdict"] == "RED"
|
||||
assert "action_ids_exact" in verdict["fired_checks"]
|
||||
|
||||
|
||||
# ── the four named mutants: each must trip the oracle RED ─────────────────
|
||||
|
||||
|
||||
def test_exactly_four_named_mutants():
|
||||
assert set(cmm.MUTANTS) == set(NAMED_MUTANTS)
|
||||
|
||||
|
||||
def test_every_named_mutant_goes_red_with_its_invariant():
|
||||
baseline = cmm.build_baseline(RECEIPTS)
|
||||
for name, check in NAMED_MUTANTS.items():
|
||||
mutated = cmm.MUTANTS[name](copy.deepcopy(baseline))
|
||||
assert mutated != baseline, f"{name} mutated nothing"
|
||||
verdict = cmm.check_trace(mutated, baseline)
|
||||
assert verdict["verdict"] == "RED", (
|
||||
f"{name} stayed GREEN — false green, not retirement evidence"
|
||||
)
|
||||
assert check in verdict["fired_checks"], (
|
||||
f"{name} went RED but {check} did not fire: {verdict['fired_checks']}"
|
||||
)
|
||||
|
||||
|
||||
def test_mutants_are_surgical_one_field_each():
|
||||
"""Each mutant touches only its named target — a mutant that also
|
||||
rewrites unrelated fields could trip the oracle for the wrong reason."""
|
||||
expected_touch = {
|
||||
"mut-key-scope": ("key_scope",),
|
||||
"mut-receipt-origin": ("receipt.origin",),
|
||||
"mut-recovery-order": ("steps.[13]", "steps.[14]"),
|
||||
"mut-launcher-target": ("launcher.",),
|
||||
}
|
||||
baseline = cmm.build_baseline(RECEIPTS)
|
||||
for name, touch in expected_touch.items():
|
||||
mutated = cmm.MUTANTS[name](copy.deepcopy(baseline))
|
||||
diffs = cmm.trace_diff(baseline, mutated)
|
||||
assert diffs, f"{name} changed nothing"
|
||||
stray = [d for d in diffs
|
||||
if not any(t in d for t in touch)]
|
||||
assert not stray, f"{name} leaked changes beyond its target: {stray}"
|
||||
|
||||
|
||||
def test_identity_mutant_is_not_counted_red():
|
||||
"""A harness that calls any mutated-looking thing RED without the
|
||||
named invariant firing would pass vacuously — the suite must refuse."""
|
||||
baseline = cmm.build_baseline(RECEIPTS)
|
||||
verdict = cmm.check_trace(copy.deepcopy(baseline), baseline)
|
||||
assert verdict["verdict"] == "GREEN"
|
||||
|
||||
|
||||
# ── the suite runner ───────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_run_suite_four_of_four_red(tmp_path):
|
||||
summary = cmm.run_suite(receipts_dir=RECEIPTS,
|
||||
receipt_path=tmp_path / "mutant-summary.json")
|
||||
assert summary["all_mutants_red"] is True
|
||||
assert summary["baseline_green"] is True
|
||||
assert len(summary["mutants"]) == 4
|
||||
for m in summary["mutants"]:
|
||||
assert m["oracle_verdict"] == "RED"
|
||||
assert m["fired_expected_check"] is True
|
||||
written = json.loads((tmp_path / "mutant-summary.json").read_text())
|
||||
assert written["all_mutants_red"] is True
|
||||
|
||||
|
||||
def test_run_suite_refuses_false_green(tmp_path):
|
||||
"""If a mutant stops firing (harness regression), the suite must go
|
||||
RED itself — a mutant that survives is a failed harness, not a pass."""
|
||||
baseline = cmm.build_baseline(RECEIPTS)
|
||||
surviving = copy.deepcopy(baseline) # identity: nothing mutated
|
||||
verdict = cmm.check_trace(surviving, baseline)
|
||||
assert verdict["verdict"] == "GREEN"
|
||||
# and the runner counts that as failure:
|
||||
summary = cmm.run_mutants(baseline, {"mut-key-scope":
|
||||
lambda t: copy.deepcopy(t)})
|
||||
assert not all(r["caught"] for r in summary)
|
||||
@@ -0,0 +1,102 @@
|
||||
# tui/devices — tools, providers and devices surfaces (groundwork)
|
||||
|
||||
This package is the tools/providers/devices view groundwork of the
|
||||
Rune TUI (openspec `add-native-rune-tui`, spec
|
||||
`tools-providers-devices`; tasks 1.3a/1.3b view legs, wave G18). It
|
||||
renders three truth-bearing panels as a full-width takeover over the
|
||||
lanes — **d** opens it, **X/esc** restores the lanes exactly — plus a
|
||||
status strip beneath the promote chrome. Deliberately narrow,
|
||||
clearly marked groundwork: the panels render truth laws; the runtimes
|
||||
that produce the evidence are later waves.
|
||||
|
||||
## What it shows, in plain words
|
||||
|
||||
- **Providers panel (section 1).** Every exposed tool/provider path
|
||||
with its live-proof status. A path is `live-verified` ONLY when
|
||||
every dimension — auth, streaming, tool-execution,
|
||||
refusal-fallback, ordinary-failure, process-restart, recovery —
|
||||
traces to a live verification run, and worker-backed paths
|
||||
additionally carry a cancellation receipt. Everything else renders
|
||||
`unmeasured` with the missing dimensions named. **A config entry is
|
||||
not capability**, and 6-of-7 dimensions is still unmeasured.
|
||||
- **Devices panel (section 2).** The measured inventory: each device
|
||||
with its pairing state and capabilities rendered from MEASURED
|
||||
evidence (`gpu=rtx4090 measured (run att-77)`). A self-report that
|
||||
contradicts measurement surfaces `UNVERIFIED — contradicts
|
||||
measurement` and never flips eligibility. Only real-hardware
|
||||
qualification runs mark a device `qualified-on-hardware`; simulated
|
||||
or model-fixture evidence renders `not-qualified … never
|
||||
qualifies`, permanently.
|
||||
- **Pairing (in section 2).** A new device pairs with a short,
|
||||
expiring, ONE-USE code approved from this existing face. **y**
|
||||
accepts — once. A reused code is REFUSED with the one-use law
|
||||
named; an expired code is REFUSED likewise. No automatic
|
||||
local-network trust exists.
|
||||
- **Acquisition panel (section 3).** The model acquisition flow per
|
||||
candidate: discovery → fit → hardware → license → consent →
|
||||
download → ready. Task-unfit, hardware-unfit and license-unclear
|
||||
candidates are REJECTED at their gate and never offered consent.
|
||||
**c** records consent (only at the consent gate of a non-rejected
|
||||
flow); **n** advances one gate. A flow at consent without a
|
||||
recorded consent renders `waiting for consent` and refuses to
|
||||
advance — consent gates every download.
|
||||
|
||||
## The one law this package obeys
|
||||
|
||||
Devices is **not a second action system**. Every key that is not one
|
||||
of devices' own flows to the inner promote face untouched — which
|
||||
hands it to rail, then lanes, then `semantic.Apply`, the single
|
||||
transition function of the whole TUI. A frozen test walks the full
|
||||
key universe and pins this key-for-key; a planted mutant (a devices
|
||||
surface answering a lane key itself) proves the check has teeth.
|
||||
|
||||
Precedence, high to low: an open semantic modal (decision boundary)
|
||||
→ the open promotion region (decision boundary) → the open rail
|
||||
takeover (inspection boundary; `d` cannot open a second surface
|
||||
through any of them) → the open devices takeover (swallows every key
|
||||
but its own — not even quit) → devices chrome (`d`) → the inner
|
||||
chain. While the takeover is open the semantic State never moves, so
|
||||
restore returns the lanes byte-identical, by construction and by
|
||||
frozen test.
|
||||
|
||||
## Frozen tests
|
||||
|
||||
- `proof_test.go` + `testdata/proof-status.v1.txt` — the proof-status
|
||||
laws and the exact rendered panel: fully-proven is live-verified;
|
||||
config-only, partial (6/7), empty-run-ID and
|
||||
missing-cancellation-receipt are all unmeasured; unmeasured never
|
||||
renders as verified.
|
||||
- `pairing_test.go` + `testdata/pairing.v1.txt` — one-use accept,
|
||||
reuse refusal, expiry, measured-only capabilities, contradiction
|
||||
surfacing, real-hardware-only qualification, and the exact rendered
|
||||
inventory/pairing panel.
|
||||
- `acquire_test.go` — the gate walk: rejections are terminal,
|
||||
consent is offered only at the consent gate, no download before
|
||||
consent, consent refused off-gate.
|
||||
- `devices_test.go` — delegation law, rogue mutant, takeover swallow
|
||||
boundary, exact restore, precedence over all three inner
|
||||
boundaries, section switching, the live pairing/consent/advance
|
||||
controls, truthful strip counts, linear zero-ANSI, clone
|
||||
independence, strip/marker clicks.
|
||||
- `paths_test.go` + `testdata/golden-devices-paths.v1.json` — frozen
|
||||
key scripts through both projections with per-step fingerprint
|
||||
equality; regolden only with `RUNE_DEVICES_REGOLDEN=1`.
|
||||
|
||||
## Honest scope (what this wave does NOT build)
|
||||
|
||||
- The runtimes behind the panels: the live provider-verification
|
||||
runner, the pairing authority transport, the Hugging Face client,
|
||||
download integrity digests (`Acquisition.Digest` stays empty until
|
||||
the downloader wave fills it), placement/routing decisions and the
|
||||
device-forge job capsule. `Paths()`, `Inventory()`, `Pairing()`,
|
||||
`Flows()` are the seams those waves plug into; until then the face
|
||||
runs on lab seeds.
|
||||
- Revocation, credential rotation, connection history, offline
|
||||
last-known state and forge selection — separate requirements of
|
||||
the same spec, separate waves.
|
||||
- Event mirroring into the activity rail feed (promote does not
|
||||
re-export its rail seam); the bounded event ledger renders in the
|
||||
takeover footer instead.
|
||||
- Click coverage is minimal: the closed strip and the takeover's
|
||||
close marker. Section switching and the y/c/n controls are
|
||||
keyboard-only this wave.
|
||||
@@ -0,0 +1,44 @@
|
||||
# runtime — the rune-runtime status surface (R2-D6)
|
||||
|
||||
The continuous-program view of the TUI (openspec add-native-rune-tui,
|
||||
spec delta **rune-runtime**): program phase, attachments, the deliberate
|
||||
exit checkpoint, the supervised Go-face/Rust-worker boundary status, the
|
||||
origin-tagged paired-command ordering display, and the **read-only
|
||||
web-feed seam view** — the live view the web link serves, never a second
|
||||
native face.
|
||||
|
||||
## Laws (frozen by tests)
|
||||
|
||||
- **Continuity** — the `Program` is the headless core; `Detach` never
|
||||
changes the phase. Faces come and go; the work continues.
|
||||
- **Deliberate exit** — `RequestExit` then `WriteCheckpoint(state)`;
|
||||
the checkpoint references canonical state (task rows, replay cursor,
|
||||
committed evidence), never a lossy summary.
|
||||
- **Ordering** — `Order` sorts by authenticated arrival (tie-break by
|
||||
ID), origin shown on every row; material conflicts render the
|
||||
clarification request. One row vocabulary (`CommandRow`) serves BOTH
|
||||
the ordering display and the web feed, so committed/pending markers
|
||||
are identical on both surfaces.
|
||||
- **Read-only feed** — `FeedView` is plain text with an evidence link
|
||||
on every command row; no key mutates the program through the feed
|
||||
takeover (`Apply` only touches chrome + the inner semantic state).
|
||||
- **Substrate (D26)** — wrapper face over `tui/rail`: chrome keys
|
||||
`R` (status takeover), `f` (feed seam view), `x`/`X`/`esc` restore;
|
||||
everything else delegates to the rail whole; an open modal outranks
|
||||
the runtime chrome.
|
||||
|
||||
## Frozen fixtures
|
||||
|
||||
- `testdata/web-feed.v1.txt` — the feed seam view bytes.
|
||||
- `testdata/ordering.v1.txt` — the paired-command ordering display.
|
||||
- `testdata/committed-pending.v1.txt` — the distinction on both
|
||||
surfaces.
|
||||
|
||||
Regolden deliberately: `RUNE_RUNTIME_REGOLDEN=1 go test ./runtime/`.
|
||||
|
||||
## Later waves
|
||||
|
||||
The real web transport seam (this view is what it serves),
|
||||
crash-resume replay from the checkpoint, live worker supervision
|
||||
plumbing (the boundary status is injected here), and runtime-side
|
||||
material-conflict detection (this package renders the `Conflict` bit).
|
||||
@@ -0,0 +1,209 @@
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"rune.dev/rune/tui/rail"
|
||||
"rune.dev/rune/tui/semantic"
|
||||
)
|
||||
|
||||
// Runtime couples the rail face to the continuous program: the shared
|
||||
// status line beneath the rail (closed) or the full-width status/feed
|
||||
// takeover (open). It implements semantic.Face so the isomorphism
|
||||
// oracle drives it against any other projection unchanged.
|
||||
type Runtime struct {
|
||||
inner *rail.Rail
|
||||
prog *Program
|
||||
view string // "" | "status" | "feed"
|
||||
mode string
|
||||
}
|
||||
|
||||
// New returns the rich-projection runtime face over the canonical seed.
|
||||
func New(w, h int) *Runtime { return newRuntime(semantic.ModeRich, w, h) }
|
||||
|
||||
// NewLinear returns the linear-projection runtime face (zero-ANSI law).
|
||||
func NewLinear(w, h int) *Runtime { return newRuntime(semantic.ModeLinear, w, h) }
|
||||
|
||||
func newRuntime(mode string, w, h int) *Runtime {
|
||||
inner := rail.New(w, h)
|
||||
if mode == semantic.ModeLinear {
|
||||
inner = rail.NewLinear(w, h)
|
||||
}
|
||||
return &Runtime{inner: inner, prog: NewProgram(), mode: mode}
|
||||
}
|
||||
|
||||
// Open reports whether either takeover is showing.
|
||||
func (r *Runtime) Open() bool { return r.view != "" }
|
||||
|
||||
// View reports the open takeover ("" | "status" | "feed").
|
||||
func (r *Runtime) View() string { return r.view }
|
||||
|
||||
// Mode reports the renderer projection ("rich" | "linear").
|
||||
func (r *Runtime) Mode() string { return r.mode }
|
||||
|
||||
// Program returns the live continuous program (the seam injects
|
||||
// attachments, commands, boundary status, context, and the exit
|
||||
// lifecycle through it).
|
||||
func (r *Runtime) Program() *Program { return r.prog }
|
||||
|
||||
// Attach attaches a surface and surfaces the event in the rail feed
|
||||
// (the rail is the activity log — the Wrapper-Face pattern).
|
||||
func (r *Runtime) Attach(face string) {
|
||||
r.prog.Attach(face)
|
||||
r.record(rail.SourceRune, "runtime attach "+face)
|
||||
}
|
||||
|
||||
// Detach detaches a surface — never stopping the program — and
|
||||
// surfaces the event in the rail feed.
|
||||
func (r *Runtime) Detach(face string) {
|
||||
r.prog.Detach(face)
|
||||
r.record(rail.SourceRune, "runtime detach "+face)
|
||||
}
|
||||
|
||||
// Submit records one origin-tagged command and surfaces it in the rail
|
||||
// feed with its authenticated arrival number.
|
||||
func (r *Runtime) Submit(origin, text, evidence string) Command {
|
||||
c := r.prog.Submit(origin, text, evidence)
|
||||
r.record(rail.SourceRune, fmt.Sprintf("runtime command #%d %s", c.Arrival, c.Origin))
|
||||
return c
|
||||
}
|
||||
|
||||
// RequestExit marks the deliberate exit requested and surfaces it.
|
||||
func (r *Runtime) RequestExit() {
|
||||
r.prog.RequestExit()
|
||||
r.record(rail.SourceRune, "runtime exit requested")
|
||||
}
|
||||
|
||||
// WriteCheckpoint writes the continuity checkpoint from the canonical
|
||||
// state and surfaces it in the rail feed.
|
||||
func (r *Runtime) WriteCheckpoint(st semantic.State) Checkpoint {
|
||||
cp := r.prog.WriteCheckpoint(st)
|
||||
r.record(rail.SourceRune, "runtime checkpoint written")
|
||||
return cp
|
||||
}
|
||||
|
||||
func (r *Runtime) record(source, text string) {
|
||||
r.inner.Record(rail.Entry{Source: source, Text: text})
|
||||
}
|
||||
|
||||
// Apply is the runtime face's transition function: chrome keys handled
|
||||
// here, everything else delegated to the inner rail face WHOLE.
|
||||
// Precedence: an open modal outranks the runtime; an open takeover
|
||||
// (status or feed) swallows every key except the restore keys (x, X,
|
||||
// esc) — no lane or rail key leaks through the inspection boundary,
|
||||
// not even quit, not even the other takeover key. The feed view has no
|
||||
// input path at all — it is read-only by construction.
|
||||
func (r *Runtime) Apply(k semantic.Key) (semantic.ActionID, bool) {
|
||||
if r.inner.Snapshot().Modal != semantic.ModalNone {
|
||||
return r.delegate(k)
|
||||
}
|
||||
name := k.String()
|
||||
if r.view != "" {
|
||||
switch name {
|
||||
case "x", "X", "esc":
|
||||
r.view = ""
|
||||
r.record(rail.SourceUser, string(RuntimeRestore))
|
||||
return RuntimeRestore, false
|
||||
default:
|
||||
return RuntimeSwallow, false
|
||||
}
|
||||
}
|
||||
switch name {
|
||||
case "R":
|
||||
r.view = "status"
|
||||
r.record(rail.SourceUser, string(RuntimeOpen))
|
||||
return RuntimeOpen, false
|
||||
case "f":
|
||||
r.view = "feed"
|
||||
r.record(rail.SourceUser, string(RuntimeFeed))
|
||||
return RuntimeFeed, false
|
||||
}
|
||||
return r.delegate(k)
|
||||
}
|
||||
|
||||
func (r *Runtime) delegate(k semantic.Key) (semantic.ActionID, bool) {
|
||||
return r.inner.Apply(k)
|
||||
}
|
||||
|
||||
// ApplySeed applies the stream status/seq event to the inner rail face
|
||||
// (semantic state, not chrome — the runtime never intercepts it).
|
||||
func (r *Runtime) ApplySeed(e semantic.SeedEvent) { r.inner.ApplySeed(e) }
|
||||
|
||||
// Click resolves a 0-based cell coordinate. The status line opens the
|
||||
// status takeover (keyboard equivalent R); an open takeover's close
|
||||
// marker restores (x/esc); clicks elsewhere inside an open takeover
|
||||
// are swallowed like keys; a modal outranks everything; everything
|
||||
// else routes through the inner rail face's own resolution.
|
||||
func (r *Runtime) Click(x, y int) semantic.ActionID {
|
||||
if r.inner.Snapshot().Modal != semantic.ModalNone {
|
||||
return "modal.swallow"
|
||||
}
|
||||
if r.view != "" {
|
||||
for _, reg := range r.Render().Regions {
|
||||
if reg.ID == "region.runtime.close" && inRegion(reg, x, y) {
|
||||
a, _ := r.Apply(semantic.Rune('x'))
|
||||
return a
|
||||
}
|
||||
}
|
||||
return RuntimeSwallow
|
||||
}
|
||||
for _, reg := range r.Render().Regions {
|
||||
if reg.ID == "region.runtime" && inRegion(reg, x, y) {
|
||||
a, _ := r.Apply(semantic.Rune('R'))
|
||||
return a
|
||||
}
|
||||
}
|
||||
return r.inner.Click(x, y)
|
||||
}
|
||||
|
||||
func inRegion(reg semantic.Region, x, y int) bool {
|
||||
return x >= reg.X && x < reg.X+reg.W && y >= reg.Y && y < reg.Y+reg.H
|
||||
}
|
||||
|
||||
// Snapshot deep-copies the semantic state (runtime chrome and program
|
||||
// state are not semantic state).
|
||||
func (r *Runtime) Snapshot() semantic.State { return r.inner.Snapshot() }
|
||||
|
||||
// Fingerprint is the inner fingerprint plus the open-view bit (key
|
||||
// routing changes with a takeover, so it is fingerprinted; program
|
||||
// state is bounded display/runtime state, like the rail feed and the
|
||||
// degrade ledger, and is not — the view-not-semantics law).
|
||||
func (r *Runtime) Fingerprint() string {
|
||||
view := r.view
|
||||
if view == "" {
|
||||
view = "closed"
|
||||
}
|
||||
return fmt.Sprintf("%s;runtime=%s", r.inner.Fingerprint(), view)
|
||||
}
|
||||
|
||||
// Clone returns an independent runtime face in the same state, program
|
||||
// copy included (oracle walks).
|
||||
func (r *Runtime) Clone() semantic.Face {
|
||||
inner, _ := r.inner.Clone().(*rail.Rail)
|
||||
return &Runtime{inner: inner, prog: r.prog.clone(), view: r.view, mode: r.mode}
|
||||
}
|
||||
|
||||
// Resize updates the semantic state dimensions (terminal resize).
|
||||
func (r *Runtime) Resize(w, h int) { r.inner.Resize(w, h) }
|
||||
|
||||
// clone deep-copies the program (faces never share program memory).
|
||||
func (p *Program) clone() *Program {
|
||||
n := *p
|
||||
n.attachments = append([]Attachment(nil), p.attachments...)
|
||||
n.commands = append([]Command(nil), p.commands...)
|
||||
if p.checkpoint != nil {
|
||||
cp := *p.checkpoint
|
||||
cp.ActiveTasks = append([]string(nil), p.checkpoint.ActiveTasks...)
|
||||
cp.LastEdited = append([]string(nil), p.checkpoint.LastEdited...)
|
||||
cp.PendingDecisions = append([]string(nil), p.checkpoint.PendingDecisions...)
|
||||
cp.PendingAlerts = append([]string(nil), p.checkpoint.PendingAlerts...)
|
||||
cp.WorkHandles = append([]string(nil), p.checkpoint.WorkHandles...)
|
||||
cp.EvidenceRefs = append([]string(nil), p.checkpoint.EvidenceRefs...)
|
||||
n.checkpoint = &cp
|
||||
}
|
||||
n.ctx.LastEdited = append([]string(nil), p.ctx.LastEdited...)
|
||||
n.ctx.PendingDecisions = append([]string(nil), p.ctx.PendingDecisions...)
|
||||
n.ctx.PendingAlerts = append([]string(nil), p.ctx.PendingAlerts...)
|
||||
n.ctx.WorkHandles = append([]string(nil), p.ctx.WorkHandles...)
|
||||
return &n
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FeedHeader is the feed's first line: the web link is a live
|
||||
// READ-ONLY view of the one runtime — never a second native face
|
||||
// (R2-D6, frozen into every feed render).
|
||||
const FeedHeader = "WEB FEED (read-only) — live view of the one runtime; never a second native face"
|
||||
|
||||
// FeedViewRows renders the web-feed seam view as lines: the read-only
|
||||
// header, the continuous program state (phase + attached surfaces), the
|
||||
// supervised boundary status, and the origin-tagged command rows in
|
||||
// authenticated arrival order with their committed/pending markers and
|
||||
// evidence links. This is what the web link serves — plain text, zero
|
||||
// ANSI, no input path.
|
||||
func (p *Program) FeedViewRows() []string {
|
||||
faces := "headless"
|
||||
if len(p.attachments) > 0 {
|
||||
names := make([]string, 0, len(p.attachments))
|
||||
for _, a := range p.attachments {
|
||||
names = append(names, a.Face)
|
||||
}
|
||||
faces = strings.Join(names, ", ")
|
||||
}
|
||||
rows := []string{
|
||||
FeedHeader,
|
||||
fmt.Sprintf("RUNTIME | %s | faces: %s", p.phase, faces),
|
||||
p.boundary.Line(),
|
||||
}
|
||||
for _, c := range Order(p.commands) {
|
||||
rows = append(rows, CommandRow(c))
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
// FeedView renders the web-feed seam view as one plain-text block (the
|
||||
// frozen fixture pins its bytes).
|
||||
func (p *Program) FeedView() string { return strings.Join(p.FeedViewRows(), "\n") }
|
||||
@@ -0,0 +1,96 @@
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// ── the frozen web-feed seam view (R2-D6) ────────────────────────────
|
||||
|
||||
// canonicalFeedProgram is the pinned scenario the fixture freezes: one
|
||||
// continuous program, two attached surfaces (the native TUI face plus
|
||||
// one web feed reader), a healthy boundary mid-operation, one committed
|
||||
// desktop command and one pending paired-phone command.
|
||||
func canonicalFeedProgram() *Program {
|
||||
p := NewProgram()
|
||||
p.Attach("tui")
|
||||
p.Attach("web:phone-1")
|
||||
p.SetBoundary(BoundaryStatus{Worker: WorkerRunning, Op: "memory-synthesis"})
|
||||
p.Submit("tui", "approve migration", "ev:task-2-receipt")
|
||||
p.Commit("cmd-1")
|
||||
p.Submit("phone:nicks-phone", "pause playback", "")
|
||||
return p
|
||||
}
|
||||
|
||||
// TestFeedViewRenderingFrozen pins the live read-oriented feed bytes
|
||||
// (testdata/web-feed.v1.txt): the same live state the TUI shows,
|
||||
// rendered read-only with an evidence link on every command row — the
|
||||
// web link is a live view, never a second native face.
|
||||
func TestFeedViewRenderingFrozen(t *testing.T) {
|
||||
got := canonicalFeedProgram().FeedView()
|
||||
want := loadPinned(t, "testdata/web-feed.v1.txt", got)
|
||||
if got != want {
|
||||
t.Fatalf("feed view drifted from the frozen fixture:\n got:\n%s\nwant:\n%s", got, want)
|
||||
}
|
||||
if strings.ContainsRune(got, 0x1b) {
|
||||
t.Fatal("feed view carries ANSI — the web seam serves plain text")
|
||||
}
|
||||
for _, want := range []string{
|
||||
"WEB FEED (read-only)",
|
||||
"never a second native face",
|
||||
"RUNTIME | running | faces: tui, web:phone-1",
|
||||
"BOUNDARY | worker running | op memory-synthesis",
|
||||
"#1 [committed] tui: approve migration | ev:task-2-receipt",
|
||||
"#2 [pending] phone:nicks-phone: pause playback | ev:arrival-2",
|
||||
} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Errorf("feed view missing %q\nfeed:\n%s", want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestFeedViewTracksLiveState: the feed is LIVE — committing the pending
|
||||
// phone command and detaching a surface both show on the next render.
|
||||
func TestFeedViewTracksLiveState(t *testing.T) {
|
||||
p := canonicalFeedProgram()
|
||||
p.Commit("cmd-2")
|
||||
p.Detach("web:phone-1")
|
||||
got := p.FeedView()
|
||||
for _, want := range []string{"#2 [committed] phone:nicks-phone", "faces: tui"} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Errorf("stale feed after live change: missing %q\nfeed:\n%s", want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestFeedEvidenceLinksEveryCommand: an evidence link rides every
|
||||
// command row — a submitted command with no explicit evidence carries
|
||||
// its authenticated-arrival record instead.
|
||||
func TestFeedEvidenceLinksEveryCommand(t *testing.T) {
|
||||
p := NewProgram()
|
||||
p.Submit("phone:nicks-phone", "pause playback", "")
|
||||
for _, row := range p.FeedViewRows() {
|
||||
if strings.HasPrefix(row, "#") && !strings.Contains(row, "| ev:") {
|
||||
t.Fatalf("command row without an evidence link: %q", row)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// loadPinned reads a frozen fixture; with RUNE_RUNTIME_REGOLDEN=1 it
|
||||
// (re)writes the fixture from the implementation so a deliberate change
|
||||
// can be frozen (same procedure as the rail golden paths).
|
||||
func loadPinned(t *testing.T, file, got string) string {
|
||||
t.Helper()
|
||||
raw, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
if os.Getenv("RUNE_RUNTIME_REGOLDEN") == "1" {
|
||||
if err := os.WriteFile(file, []byte(got), 0o644); err != nil {
|
||||
t.Fatalf("regolden %s: %v", file, err)
|
||||
}
|
||||
return got
|
||||
}
|
||||
t.Fatalf("fixture %s unreadable (regolden with RUNE_RUNTIME_REGOLDEN=1): %v", file, err)
|
||||
}
|
||||
return string(raw)
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// ── origin-tagged ordering: paired commands in authenticated order ───
|
||||
|
||||
// pairedCommands is the spec scenario ("Origin-tagged ordering"): a
|
||||
// paired phone command and the desktop TUI act near-simultaneously, a
|
||||
// tablet remote follows, and one phone pair conflicts materially. The
|
||||
// slice order is deliberately NOT the arrival order.
|
||||
func pairedCommands() []Command {
|
||||
return []Command{
|
||||
{ID: "cmd-8", Origin: "phone:nicks-phone", Text: "resume playback", Evidence: "ev:arrival-8", State: CmdPending, Arrival: 8, Conflict: true},
|
||||
{ID: "cmd-6", Origin: "tui", Text: "approve migration", Evidence: "ev:task-2-receipt", State: CmdCommitted, Arrival: 6},
|
||||
{ID: "cmd-7", Origin: "phone:nicks-phone", Text: "pause playback", Evidence: "ev:arrival-7", State: CmdCommitted, Arrival: 7, Conflict: true},
|
||||
{ID: "cmd-9", Origin: "remote:tablet", Text: "open plan view", Evidence: "ev:arrival-9", State: CmdPending, Arrival: 9},
|
||||
}
|
||||
}
|
||||
|
||||
// TestOrderIsAuthenticatedArrival is the ordering law: commands order by
|
||||
// authenticated arrival — never by slice position — with the origin
|
||||
// shown on every row, and a material conflict requests clarification.
|
||||
func TestOrderIsAuthenticatedArrival(t *testing.T) {
|
||||
got := Order(pairedCommands())
|
||||
wantIDs := []string{"cmd-6", "cmd-7", "cmd-8", "cmd-9"}
|
||||
for i, c := range got {
|
||||
if c.ID != wantIDs[i] {
|
||||
t.Fatalf("order[%d] = %s (arrival %d), want %s", i, c.ID, c.Arrival, wantIDs[i])
|
||||
}
|
||||
}
|
||||
d := OrderDisplay(got)
|
||||
if strings.Index(d, "#6") > strings.Index(d, "#7") || strings.Index(d, "#7") > strings.Index(d, "#8") {
|
||||
t.Fatalf("display not in arrival order:\n%s", d)
|
||||
}
|
||||
for _, row := range []string{
|
||||
"#6 [committed] tui: approve migration | ev:task-2-receipt",
|
||||
"#7 [committed] phone:nicks-phone: pause playback | ev:arrival-7 | CONFLICT - clarification requested",
|
||||
"#8 [pending] phone:nicks-phone: resume playback | ev:arrival-8 | CONFLICT - clarification requested",
|
||||
"#9 [pending] remote:tablet: open plan view | ev:arrival-9",
|
||||
} {
|
||||
if !strings.Contains(d, row) {
|
||||
t.Errorf("ordering display missing row %q\ndisplay:\n%s", row, d)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestOrderDeterministicTieBreak: equal arrival numbers (a seam bug)
|
||||
// still order deterministically — by ID — and Order never mutates its
|
||||
// input slice.
|
||||
func TestOrderDeterministicTieBreak(t *testing.T) {
|
||||
in := []Command{
|
||||
{ID: "cmd-b", Arrival: 4, State: CmdPending, Origin: "tui", Text: "b"},
|
||||
{ID: "cmd-a", Arrival: 4, State: CmdPending, Origin: "tui", Text: "a"},
|
||||
}
|
||||
got := Order(in)
|
||||
if got[0].ID != "cmd-a" || got[1].ID != "cmd-b" {
|
||||
t.Fatalf("tie break not by ID: %s then %s", got[0].ID, got[1].ID)
|
||||
}
|
||||
if in[0].ID != "cmd-b" || in[1].ID != "cmd-a" {
|
||||
t.Fatal("Order mutated its input slice")
|
||||
}
|
||||
}
|
||||
|
||||
// TestOrderingDisplayFrozen pins the ordering-display fixture bytes
|
||||
// (testdata/ordering.v1.txt): origin-tagged paired commands shown in
|
||||
// authenticated arrival order with committed/pending states distinct.
|
||||
func TestOrderingDisplayFrozen(t *testing.T) {
|
||||
got := OrderDisplay(Order(pairedCommands()))
|
||||
want := loadPinned(t, "testdata/ordering.v1.txt", got)
|
||||
if got != want {
|
||||
t.Fatalf("ordering display drifted from the frozen fixture:\n got:\n%s\nwant:\n%s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// ── committed/pending distinction ────────────────────────────────────
|
||||
|
||||
// TestCommittedPendingDistinctFrozen pins the committed/pending
|
||||
// distinction fixture (testdata/committed-pending.v1.txt): the same
|
||||
// paired commands rendered by the ordering display AND by the web feed
|
||||
// carry distinct, unambiguous state markers on both surfaces.
|
||||
func TestCommittedPendingDistinctFrozen(t *testing.T) {
|
||||
p := canonicalFeedProgram() // one committed desktop + one pending phone command
|
||||
var b strings.Builder
|
||||
b.WriteString("ORDERING DISPLAY\n")
|
||||
b.WriteString(OrderDisplay(Order(p.Commands())))
|
||||
b.WriteString("\nWEB FEED COMMAND LINES\n")
|
||||
for _, row := range p.FeedViewRows() {
|
||||
if strings.HasPrefix(row, "#") {
|
||||
b.WriteString(row)
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
got := b.String()
|
||||
want := loadPinned(t, "testdata/committed-pending.v1.txt", got)
|
||||
if got != want {
|
||||
t.Fatalf("committed/pending distinction drifted from the frozen fixture:\n got:\n%s\nwant:\n%s", got, want)
|
||||
}
|
||||
if strings.Contains(got, "[unknown]") {
|
||||
t.Fatal("a command rendered without its committed/pending marker")
|
||||
}
|
||||
}
|
||||
|
||||
// TestCommitOnlyPending: committing twice is refused, committing an
|
||||
// unknown id is refused, and only pending commands transition.
|
||||
func TestCommitOnlyPending(t *testing.T) {
|
||||
p := NewProgram()
|
||||
c := p.Submit("tui", "approve migration", "ev:task-2-receipt")
|
||||
if !p.Commit(c.ID) {
|
||||
t.Fatal("first commit refused")
|
||||
}
|
||||
if p.Commit(c.ID) {
|
||||
t.Fatal("double commit accepted")
|
||||
}
|
||||
if p.Commit("cmd-nope") {
|
||||
t.Fatal("unknown commit accepted")
|
||||
}
|
||||
if got := p.Commands()[0].State; got != CmdCommitted {
|
||||
t.Fatalf("state after commit = %q, want %q", got, CmdCommitted)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"charm.land/lipgloss/v2"
|
||||
|
||||
"rune.dev/rune/tui/semantic"
|
||||
)
|
||||
|
||||
// Render projects the runtime face: closed = the rail frame with the
|
||||
// runtime status line beneath it; open = the full-width status or feed
|
||||
// takeover covering the rail. The rich projection colors the runtime
|
||||
// chrome; the linear projection renders the same text with zero ANSI
|
||||
// (the oracle enforces both carry the same fingerprints, and the
|
||||
// linear one carries no escape byte).
|
||||
func (r *Runtime) Render() semantic.Frame {
|
||||
if r.view != "" {
|
||||
if r.view == "feed" {
|
||||
return r.renderFeedTakeover()
|
||||
}
|
||||
return r.renderStatusTakeover()
|
||||
}
|
||||
inner := r.inner.Render()
|
||||
if r.mode == semantic.ModeLinear {
|
||||
inner.Content = semantic.StripANSI(inner.Content)
|
||||
}
|
||||
line := r.StatusLine()
|
||||
content := line
|
||||
if inner.Content != "" {
|
||||
content = inner.Content + "\n" + line
|
||||
}
|
||||
railLines := strings.Split(inner.Content, "\n")
|
||||
regions := append(inner.Regions, semantic.Region{
|
||||
ID: "region.runtime", X: 0, Y: len(railLines), H: 1,
|
||||
W: max(r.inner.Snapshot().W, 0), Marker: "RUNTIME",
|
||||
})
|
||||
return semantic.Frame{Content: content, Regions: regions}
|
||||
}
|
||||
|
||||
// StatusLine is the shared one-line runtime status beneath the rail:
|
||||
// continuous program phase, attached surfaces (headless when none),
|
||||
// the boundary worker state, and the chrome keys.
|
||||
func (r *Runtime) StatusLine() string {
|
||||
n := len(r.prog.attachments)
|
||||
faces := fmt.Sprintf("faces %d", n)
|
||||
if n == 0 {
|
||||
faces = "headless"
|
||||
}
|
||||
line := fmt.Sprintf("RUNTIME | %s | %s | worker %s | R status, f feed",
|
||||
r.prog.Phase(), faces, r.prog.Boundary().Worker)
|
||||
line = clipRunes(line, max(r.inner.Snapshot().W, 0))
|
||||
if r.mode == semantic.ModeRich && strings.HasPrefix(line, "RUNTIME") {
|
||||
return runtimeTagStyle.Render("RUNTIME") + line[len("RUNTIME"):]
|
||||
}
|
||||
return line
|
||||
}
|
||||
|
||||
// renderStatusTakeover draws the full-width runtime status takeover:
|
||||
// header with the close marker, the continuous program state (phase,
|
||||
// attached surfaces), the supervised boundary status, the exit
|
||||
// checkpoint summary (when one was written), and the restore hint.
|
||||
func (r *Runtime) renderStatusTakeover() semantic.Frame {
|
||||
s := r.inner.Snapshot()
|
||||
W, H := s.W, s.H
|
||||
if W < 3 || H < 3 {
|
||||
return blankTakeover(W, H, "region.runtime.takeover")
|
||||
}
|
||||
inner := W - 2
|
||||
|
||||
header := "RUNTIME STATUS - continuous program | X/esc restores lanes"
|
||||
faces := "headless"
|
||||
if n := len(r.prog.attachments); n > 0 {
|
||||
names := make([]string, 0, n)
|
||||
for _, a := range r.prog.attachments {
|
||||
names = append(names, a.Face)
|
||||
}
|
||||
faces = strings.Join(names, ", ")
|
||||
}
|
||||
phaseLine := "phase: " + r.prog.Phase()
|
||||
facesLine := "faces: " + faces
|
||||
boundaryLine := r.prog.Boundary().Line()
|
||||
cpLine := "exit checkpoint: none written (exit is deliberate)"
|
||||
if cp, ok := r.prog.Checkpoint(); ok {
|
||||
cpLine = cp.Summary()
|
||||
}
|
||||
hint := "X or esc restores the lanes exactly"
|
||||
|
||||
// Priority when the box runs short: header, phase, faces, boundary
|
||||
// keep; checkpoint lines drop tail-first; the hint drops first.
|
||||
body := []string{phaseLine, facesLine, boundaryLine}
|
||||
cpLines := strings.Split(cpLine, "\n")
|
||||
mid := H - 2
|
||||
avail := mid - 2 // header + one blank
|
||||
if avail < 0 {
|
||||
avail = 0
|
||||
}
|
||||
for len(body)+len(cpLines)+1 > avail && len(cpLines) > 0 {
|
||||
cpLines = cpLines[:len(cpLines)-1]
|
||||
}
|
||||
if len(body)+len(cpLines)+1 <= avail {
|
||||
body = append(body, cpLines...)
|
||||
body = append(body, hint)
|
||||
} else if len(body)+len(cpLines) <= avail {
|
||||
body = append(body, cpLines...)
|
||||
}
|
||||
body = body[:min(len(body), max(avail, 0))]
|
||||
|
||||
rows := []string{fit(header, inner, "[x]")}
|
||||
rows = append(rows, strings.Repeat(" ", inner))
|
||||
for _, row := range body {
|
||||
rows = append(rows, clipRunes(row, inner))
|
||||
}
|
||||
for len(rows) < mid {
|
||||
rows = append(rows, strings.Repeat(" ", inner))
|
||||
}
|
||||
rows = rows[:mid]
|
||||
|
||||
frame := border(rows, inner, r.mode)
|
||||
frame.Regions = []semantic.Region{
|
||||
{ID: "region.runtime.takeover", X: 0, Y: 0, W: W, H: H, Marker: "RUNTIME STATUS"},
|
||||
{ID: "region.runtime.close", X: W - 4, Y: 1, W: 3, H: 1, Marker: "[x]"},
|
||||
}
|
||||
return frame
|
||||
}
|
||||
|
||||
// renderFeedTakeover draws the full-width web-feed seam view: the live
|
||||
// read-only feed the web link serves, embedded behind the lanes — one
|
||||
// view of the one runtime, never a second native face, with no input
|
||||
// path (every key except restore is swallowed by Apply).
|
||||
func (r *Runtime) renderFeedTakeover() semantic.Frame {
|
||||
s := r.inner.Snapshot()
|
||||
W, H := s.W, s.H
|
||||
if W < 3 || H < 3 {
|
||||
return blankTakeover(W, H, "region.runtime.takeover")
|
||||
}
|
||||
inner := W - 2
|
||||
|
||||
header := "WEB FEED (read-only) - live view | X/esc restores lanes"
|
||||
hint := "read-only: the feed has no input path - X or esc restores"
|
||||
|
||||
feedLines := r.prog.FeedViewRows()
|
||||
mid := H - 2
|
||||
avail := mid - 2 // header + one blank
|
||||
if avail < 0 {
|
||||
avail = 0
|
||||
}
|
||||
// newest command rows stay visible: drop OLDEST feed lines first,
|
||||
// never the header/state/boundary block's tail before its head.
|
||||
if len(feedLines)+1 > avail {
|
||||
drop := len(feedLines) + 1 - avail
|
||||
if drop < len(feedLines) {
|
||||
keep := feedLines[drop:]
|
||||
// the feed's own header line never drops
|
||||
feedLines = append([]string{feedLines[0]}, keep...)
|
||||
} else {
|
||||
feedLines = []string{feedLines[0]}
|
||||
}
|
||||
}
|
||||
body := make([]string, 0, len(feedLines)+1)
|
||||
for _, l := range feedLines {
|
||||
body = append(body, clipRunes(l, inner))
|
||||
}
|
||||
if len(body) < avail {
|
||||
body = append(body, clipRunes(hint, inner))
|
||||
}
|
||||
body = body[:min(len(body), max(avail, 0))]
|
||||
|
||||
rows := []string{fit(header, inner, "[x]")}
|
||||
rows = append(rows, strings.Repeat(" ", inner))
|
||||
rows = append(rows, body...)
|
||||
for len(rows) < mid {
|
||||
rows = append(rows, strings.Repeat(" ", inner))
|
||||
}
|
||||
rows = rows[:mid]
|
||||
|
||||
frame := border(rows, inner, r.mode)
|
||||
frame.Regions = []semantic.Region{
|
||||
{ID: "region.runtime.takeover", X: 0, Y: 0, W: W, H: H, Marker: "WEB FEED"},
|
||||
{ID: "region.runtime.feed", X: 1, Y: 2, W: inner, H: max(len(body), 0), Marker: "FEED"},
|
||||
{ID: "region.runtime.close", X: W - 4, Y: 1, W: 3, H: 1, Marker: "[x]"},
|
||||
}
|
||||
return frame
|
||||
}
|
||||
|
||||
// border wraps the box rows in the takeover frame (rich colors it).
|
||||
func border(rows []string, inner int, mode string) semantic.Frame {
|
||||
top, side, bottom := "+", "|", "+"
|
||||
if mode == semantic.ModeRich {
|
||||
top, side, bottom = takeoverStyle.Render("+"), takeoverStyle.Render("|"), takeoverStyle.Render("+")
|
||||
}
|
||||
body := make([]string, 0, len(rows)+2)
|
||||
body = append(body, top+strings.Repeat("-", inner)+top)
|
||||
for _, row := range rows {
|
||||
body = append(body, side+row+side)
|
||||
}
|
||||
body = append(body, bottom+strings.Repeat("-", inner)+bottom)
|
||||
return semantic.Frame{Content: strings.Join(body, "\n")}
|
||||
}
|
||||
|
||||
func blankTakeover(W, H int, id string) semantic.Frame {
|
||||
rows := make([]string, max(H, 0))
|
||||
for i := range rows {
|
||||
rows[i] = strings.Repeat(" ", max(W, 0))
|
||||
}
|
||||
return semantic.Frame{Content: strings.Join(rows, "\n"), Regions: []semantic.Region{
|
||||
{ID: id, X: 0, Y: 0, W: max(W, 0), H: max(H, 0)},
|
||||
}}
|
||||
}
|
||||
|
||||
func clipRunes(s string, w int) string {
|
||||
r := []rune(s)
|
||||
if w < 0 {
|
||||
w = 0
|
||||
}
|
||||
if len(r) <= w {
|
||||
return s
|
||||
}
|
||||
return string(r[:w])
|
||||
}
|
||||
|
||||
func fit(text string, inner int, marker string) string {
|
||||
if inner < len(marker)+1 {
|
||||
return clipRunes(marker, inner)
|
||||
}
|
||||
left := clipRunes(text, inner-len(marker)-1)
|
||||
return left + strings.Repeat(" ", max(inner-len([]rune(left))-len(marker), 0)) + marker
|
||||
}
|
||||
|
||||
var (
|
||||
runtimeTagStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("39"))
|
||||
takeoverStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("39"))
|
||||
)
|
||||
@@ -0,0 +1,378 @@
|
||||
// Package runtime is the rune-runtime status groundwork of the Rune TUI
|
||||
// (openspec add-native-rune-tui, spec delta rune-runtime — single-
|
||||
// interface TUI + web-link live feed, R2-D6): the continuous program
|
||||
// state surface (attach/detach never stops the work, deliberate exit
|
||||
// writes the continuity checkpoint), the origin-tagged paired-command
|
||||
// ordering display with committed/pending distinction, the supervised
|
||||
// Go-face/Rust-worker boundary status, and the READ-ONLY web-feed seam
|
||||
// view the web link serves — never a second native face.
|
||||
//
|
||||
// THE LAW of this package (D26 substrate law, carried from tui/semantic,
|
||||
// tui/lanes, tui/rail and tui/degrade): the runtime status is a VIEW
|
||||
// over the one semantic State and its events — never a parallel action
|
||||
// system. Every lane key routes through the inner rail face whole; the
|
||||
// runtime adds only its chrome keys (R opens the status takeover, f
|
||||
// opens the web-feed seam view, x/X/esc restores) and the program state
|
||||
// the runtime injects. The delegation is pinned key-for-key by the
|
||||
// coherence tests.
|
||||
//
|
||||
// Continuity law (rune-runtime "Face closes mid-task"): the Program is
|
||||
// the continuous headless core — attachments come and go, the phase
|
||||
// never regresses because a face closed. Detach cannot stop work.
|
||||
//
|
||||
// Exit law ("Deliberate exit writes continuity checkpoint"): only a
|
||||
// deliberate exit writes the checkpoint, and the checkpoint references
|
||||
// canonical state — the semantic State's task rows and replay cursor
|
||||
// plus the declared context — never a lossy summary.
|
||||
//
|
||||
// Ordering law ("Origin-tagged ordering"): paired-device and remote
|
||||
// input is origin-tagged and ordered by authenticated arrival; a
|
||||
// material conflict is shown and requests clarification. Order is a
|
||||
// PURE function — no clock, no RNG — and its row vocabulary is shared
|
||||
// by the ordering display and the web feed so the committed/pending
|
||||
// distinction is identical on both surfaces.
|
||||
//
|
||||
// Read-only feed law ("Web feed live view", R2-D6): the web feed
|
||||
// renders the same live state read-only with an evidence link on every
|
||||
// command row, and has NO input path — no key mutates the program
|
||||
// through the feed, and the seam view never spawns a native second
|
||||
// face. Outside-the-TUI access is a live view, nothing more.
|
||||
//
|
||||
// Honest scope (groundwork, not the full runtime):
|
||||
// - HERE: the continuous-program model (phase, attachments,
|
||||
// origin-tagged commands, boundary status), the exit checkpoint
|
||||
// summary, the wrapper face with status/feed takeovers, the frozen
|
||||
// feed/ordering/committed-pending fixtures.
|
||||
// - LATER WAVES: the real web transport seam (this view is what it
|
||||
// serves), crash-resume replay from the checkpoint, worker
|
||||
// supervision plumbing (the boundary status is injected at the live
|
||||
// seam), material-conflict detection (the Conflict bit is set by
|
||||
// the runtime; this package renders it).
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"rune.dev/rune/tui/semantic"
|
||||
)
|
||||
|
||||
// Continuous program phases (frozen vocabulary).
|
||||
const (
|
||||
PhaseRunning = "running" // the one program works headless
|
||||
PhaseExiting = "exiting" // deliberate exit requested, checkpoint pending
|
||||
PhaseCheckpointed = "checkpointed" // continuity checkpoint written, program may stop
|
||||
)
|
||||
|
||||
// Command states (frozen vocabulary — the committed/pending distinction
|
||||
// is pinned by the frozen fixtures on both surfaces).
|
||||
const (
|
||||
CmdPending = "pending"
|
||||
CmdCommitted = "committed"
|
||||
)
|
||||
|
||||
// Supervised Rust worker states (frozen vocabulary).
|
||||
const (
|
||||
WorkerRunning = "running" // supervised worker healthy
|
||||
WorkerRestarting = "restarting" // supervisor restart in flight
|
||||
WorkerStopped = "stopped" // deliberate stop
|
||||
WorkerFailed = "failed" // fail-closed (e.g. frame corruption)
|
||||
)
|
||||
|
||||
// Runtime chrome action IDs (the vocabulary the frozen paths pin).
|
||||
const (
|
||||
RuntimeOpen = semantic.ActionID("runtime.open")
|
||||
RuntimeFeed = semantic.ActionID("runtime.feed.open")
|
||||
RuntimeRestore = semantic.ActionID("runtime.restore")
|
||||
RuntimeSwallow = semantic.ActionID("runtime.swallow")
|
||||
)
|
||||
|
||||
// Attachment is one attached surface: the single native TUI face
|
||||
// ("tui") or a web-feed reader ("web:<link-id>"). Attachments are
|
||||
// observers — closing one never stops the program.
|
||||
type Attachment struct {
|
||||
Face string
|
||||
}
|
||||
|
||||
// Command is one origin-tagged input from a paired device or remote:
|
||||
// who sent it, its authenticated arrival number (the ordering key),
|
||||
// what it asked, its committed/pending state, its evidence link, and
|
||||
// whether the runtime flagged a material conflict.
|
||||
type Command struct {
|
||||
ID string
|
||||
Origin string // "tui" | "phone:<id>" | "remote:<id>" | ...
|
||||
Text string
|
||||
Evidence string // evidence link; the arrival record when none given
|
||||
State string // pending | committed
|
||||
Arrival int64 // authenticated arrival — the ordering key
|
||||
Conflict bool // material conflict → clarification requested
|
||||
}
|
||||
|
||||
// Context is the declared continuity context the runtime keeps for the
|
||||
// exit checkpoint (plan revision, last edited files, pending decisions
|
||||
// and alerts, connected work handles). The runtime seams inject it as
|
||||
// it changes; the checkpoint freezes it.
|
||||
type Context struct {
|
||||
PlanRevision string
|
||||
LastEdited []string
|
||||
PendingDecisions []string
|
||||
PendingAlerts []string
|
||||
WorkHandles []string
|
||||
}
|
||||
|
||||
// Checkpoint is the canonical continuity checkpoint of a deliberate
|
||||
// exit (rune-runtime spec): active/incomplete tasks, plan revision,
|
||||
// last edited files, pending decisions/alerts, connected work handles,
|
||||
// replay cursor, and source/evidence pointers — all references into
|
||||
// canonical state, never a lossy summary.
|
||||
type Checkpoint struct {
|
||||
ActiveTasks []string
|
||||
PlanRevision string
|
||||
LastEdited []string
|
||||
PendingDecisions []string
|
||||
PendingAlerts []string
|
||||
WorkHandles []string
|
||||
ReplayCursor int64
|
||||
EvidenceRefs []string
|
||||
}
|
||||
|
||||
// Summary renders the exit-checkpoint summary (the status takeover
|
||||
// shows it; the restart path reads the same fields).
|
||||
func (c Checkpoint) Summary() string {
|
||||
var b strings.Builder
|
||||
b.WriteString("EXIT CHECKPOINT (canonical refs - never a lossy summary)\n")
|
||||
fmt.Fprintf(&b, "active tasks: %s\n", joinOrNone(c.ActiveTasks))
|
||||
fmt.Fprintf(&b, "plan revision: %s\n", orNone(c.PlanRevision))
|
||||
fmt.Fprintf(&b, "last edited: %s\n", joinOrNone(c.LastEdited))
|
||||
fmt.Fprintf(&b, "pending decisions: %s\n", joinOrNone(c.PendingDecisions))
|
||||
fmt.Fprintf(&b, "pending alerts: %s\n", joinOrNone(c.PendingAlerts))
|
||||
fmt.Fprintf(&b, "work handles: %s\n", joinOrNone(c.WorkHandles))
|
||||
fmt.Fprintf(&b, "replay cursor: seq %d\n", c.ReplayCursor)
|
||||
fmt.Fprintf(&b, "evidence: %s", joinOrNone(c.EvidenceRefs))
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// BoundaryStatus is the supervised Go-face/Rust-worker boundary state
|
||||
// the runtime injects at the live seam: worker state, the current
|
||||
// coarse operation, whether cancellation latched (cancel law: the latch
|
||||
// outlives registration), and whether the boundary failed closed on
|
||||
// frame corruption.
|
||||
type BoundaryStatus struct {
|
||||
Worker string // running | restarting | stopped | failed
|
||||
Op string // current coarse op ("" = idle)
|
||||
CancelLatched bool
|
||||
FrameCorrupt bool
|
||||
}
|
||||
|
||||
// Line renders the one-line boundary status (shared by the status
|
||||
// line, the status takeover, and the web feed).
|
||||
func (b BoundaryStatus) Line() string {
|
||||
var s string
|
||||
if b.Worker == WorkerFailed && b.FrameCorrupt {
|
||||
s = "BOUNDARY | worker failed (frame corrupt - no partial effect)"
|
||||
} else {
|
||||
s = "BOUNDARY | worker " + b.Worker
|
||||
}
|
||||
if b.Op != "" {
|
||||
s += " | op " + b.Op
|
||||
}
|
||||
if b.CancelLatched {
|
||||
s += " | cancel latched"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Program is the continuous headless core: phase, attached surfaces,
|
||||
// origin-tagged commands, boundary status, declared context, and the
|
||||
// last deliberate-exit checkpoint. It carries no clock and no renderer
|
||||
// knowledge — every transition is deterministic and pure over its
|
||||
// inputs (the family determinism law).
|
||||
type Program struct {
|
||||
phase string
|
||||
attachments []Attachment
|
||||
nextArrival int64
|
||||
commands []Command
|
||||
checkpoint *Checkpoint
|
||||
boundary BoundaryStatus
|
||||
ctx Context
|
||||
}
|
||||
|
||||
// NewProgram returns the fresh continuous program: running, headless
|
||||
// (faces attach to it), boundary healthy, no checkpoint.
|
||||
func NewProgram() *Program {
|
||||
return &Program{phase: PhaseRunning, boundary: BoundaryStatus{Worker: WorkerRunning}}
|
||||
}
|
||||
|
||||
// Phase reports the continuous program phase.
|
||||
func (p *Program) Phase() string { return p.phase }
|
||||
|
||||
// Attach attaches one surface (idempotent per face).
|
||||
func (p *Program) Attach(face string) {
|
||||
for _, a := range p.attachments {
|
||||
if a.Face == face {
|
||||
return
|
||||
}
|
||||
}
|
||||
p.attachments = append(p.attachments, Attachment{Face: face})
|
||||
}
|
||||
|
||||
// Detach detaches one surface (unknown faces are a no-op). Detach NEVER
|
||||
// changes the phase — the program outlives every face.
|
||||
func (p *Program) Detach(face string) {
|
||||
out := p.attachments[:0]
|
||||
for _, a := range p.attachments {
|
||||
if a.Face != face {
|
||||
out = append(out, a)
|
||||
}
|
||||
}
|
||||
p.attachments = out
|
||||
}
|
||||
|
||||
// Attachments returns a copy of the attached surfaces, oldest first.
|
||||
func (p *Program) Attachments() []Attachment { return append([]Attachment(nil), p.attachments...) }
|
||||
|
||||
// Submit records one origin-tagged input and returns it: the
|
||||
// authenticated arrival number is assigned monotonically, the command
|
||||
// starts pending, and an evidence-less submission carries its arrival
|
||||
// record as its evidence link (every command row stays linkable).
|
||||
func (p *Program) Submit(origin, text, evidence string) Command {
|
||||
p.nextArrival++
|
||||
c := Command{
|
||||
ID: fmt.Sprintf("cmd-%d", p.nextArrival),
|
||||
Origin: origin,
|
||||
Text: text,
|
||||
Evidence: evidence,
|
||||
State: CmdPending,
|
||||
Arrival: p.nextArrival,
|
||||
}
|
||||
if c.Evidence == "" {
|
||||
c.Evidence = fmt.Sprintf("ev:arrival-%d", p.nextArrival)
|
||||
}
|
||||
p.commands = append(p.commands, c)
|
||||
return c
|
||||
}
|
||||
|
||||
// Commit transitions one pending command to committed. Double commits
|
||||
// and unknown ids refuse.
|
||||
func (p *Program) Commit(id string) bool {
|
||||
for i := range p.commands {
|
||||
if p.commands[i].ID == id && p.commands[i].State == CmdPending {
|
||||
p.commands[i].State = CmdCommitted
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Commands returns a copy of the recorded commands, submit order.
|
||||
func (p *Program) Commands() []Command { return append([]Command(nil), p.commands...) }
|
||||
|
||||
// RequestExit marks the deliberate exit requested (checkpoint pending).
|
||||
func (p *Program) RequestExit() { p.phase = PhaseExiting }
|
||||
|
||||
// SetContext replaces the declared continuity context.
|
||||
func (p *Program) SetContext(ctx Context) { p.ctx = ctx }
|
||||
|
||||
// SetBoundary replaces the boundary status (the live seam injects it).
|
||||
func (p *Program) SetBoundary(bs BoundaryStatus) { p.boundary = bs }
|
||||
|
||||
// Boundary reports the current boundary status.
|
||||
func (p *Program) Boundary() BoundaryStatus { return p.boundary }
|
||||
|
||||
// WriteCheckpoint writes the continuity checkpoint FROM canonical state
|
||||
// — active tasks and replay cursor derive from the semantic State, the
|
||||
// declared context and committed-command evidence freeze with it — and
|
||||
// moves the phase to checkpointed. The checkpoint references canonical
|
||||
// state; nothing is summarized away.
|
||||
func (p *Program) WriteCheckpoint(st semantic.State) Checkpoint {
|
||||
cp := Checkpoint{
|
||||
ActiveTasks: taskIDs(st),
|
||||
PlanRevision: p.ctx.PlanRevision,
|
||||
LastEdited: append([]string(nil), p.ctx.LastEdited...),
|
||||
PendingDecisions: append([]string(nil), p.ctx.PendingDecisions...),
|
||||
PendingAlerts: append([]string(nil), p.ctx.PendingAlerts...),
|
||||
WorkHandles: append([]string(nil), p.ctx.WorkHandles...),
|
||||
ReplayCursor: st.Seq,
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, c := range Order(p.commands) {
|
||||
if c.State == CmdCommitted && !seen[c.Evidence] {
|
||||
seen[c.Evidence] = true
|
||||
cp.EvidenceRefs = append(cp.EvidenceRefs, c.Evidence)
|
||||
}
|
||||
}
|
||||
cp.EvidenceRefs = append(cp.EvidenceRefs, fmt.Sprintf("state:seq-%d", st.Seq))
|
||||
p.checkpoint = &cp
|
||||
p.phase = PhaseCheckpointed
|
||||
return cp
|
||||
}
|
||||
|
||||
// Checkpoint returns the last written checkpoint, if any.
|
||||
func (p *Program) Checkpoint() (Checkpoint, bool) {
|
||||
if p.checkpoint == nil {
|
||||
return Checkpoint{}, false
|
||||
}
|
||||
return *p.checkpoint, true
|
||||
}
|
||||
|
||||
// Order is the ordering law as a pure function: commands sort by
|
||||
// authenticated arrival, ties (a seam bug) break deterministically by
|
||||
// ID, and the input slice is never mutated.
|
||||
func Order(cmds []Command) []Command {
|
||||
out := append([]Command(nil), cmds...)
|
||||
sort.SliceStable(out, func(i, j int) bool {
|
||||
if out[i].Arrival != out[j].Arrival {
|
||||
return out[i].Arrival < out[j].Arrival
|
||||
}
|
||||
return out[i].ID < out[j].ID
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
// CommandRow renders one command in the shared row vocabulary:
|
||||
// authenticated arrival, committed/pending marker, origin, text,
|
||||
// evidence link — and the clarification request on a material conflict.
|
||||
// The ordering display and the web feed render the SAME rows, so the
|
||||
// committed/pending distinction is identical on both surfaces.
|
||||
func CommandRow(c Command) string {
|
||||
row := fmt.Sprintf("#%d [%s] %s: %s | %s", c.Arrival, c.State, c.Origin, c.Text, c.Evidence)
|
||||
if c.Conflict {
|
||||
row += " | CONFLICT - clarification requested"
|
||||
}
|
||||
return row
|
||||
}
|
||||
|
||||
// OrderDisplay renders the origin-tagged ordering display: the paired
|
||||
// commands in authenticated arrival order (the takeover and the frozen
|
||||
// fixture show it).
|
||||
func OrderDisplay(cmds []Command) string {
|
||||
ordered := Order(cmds)
|
||||
rows := make([]string, 0, len(ordered))
|
||||
for _, c := range ordered {
|
||||
rows = append(rows, CommandRow(c))
|
||||
}
|
||||
return strings.Join(rows, "\n")
|
||||
}
|
||||
|
||||
func taskIDs(st semantic.State) []string {
|
||||
ids := make([]string, 0, len(st.Tasks))
|
||||
for _, t := range st.Tasks {
|
||||
ids = append(ids, t.ID)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func joinOrNone(parts []string) string {
|
||||
if len(parts) == 0 {
|
||||
return "none"
|
||||
}
|
||||
return strings.Join(parts, ", ")
|
||||
}
|
||||
|
||||
func orNone(s string) string {
|
||||
if s == "" {
|
||||
return "none"
|
||||
}
|
||||
return s
|
||||
}
|
||||
@@ -0,0 +1,403 @@
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"rune.dev/rune/tui/semantic"
|
||||
)
|
||||
|
||||
// ── continuous program state: attach/detach never stops the work ──────
|
||||
|
||||
// TestDetachNeverStopsProgram is the spec law (rune-runtime "Face closes
|
||||
// mid-task"): closing or minimizing the TUI never stops the continuous
|
||||
// program — detach changes the attachment list, never the phase.
|
||||
func TestDetachNeverStopsProgram(t *testing.T) {
|
||||
p := NewProgram()
|
||||
if p.Phase() != PhaseRunning {
|
||||
t.Fatalf("fresh program phase = %q, want %q", p.Phase(), PhaseRunning)
|
||||
}
|
||||
p.Attach("tui")
|
||||
p.Detach("tui")
|
||||
if p.Phase() != PhaseRunning {
|
||||
t.Fatalf("detach changed phase to %q — the program outlives its faces", p.Phase())
|
||||
}
|
||||
if n := len(p.Attachments()); n != 0 {
|
||||
t.Fatalf("attachments after detach = %d, want 0", n)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAttachmentsTrackFaces: attach is idempotent per face, detach of an
|
||||
// unknown face is a no-op, and the list reports what is attached now.
|
||||
func TestAttachmentsTrackFaces(t *testing.T) {
|
||||
p := NewProgram()
|
||||
p.Attach("tui")
|
||||
p.Attach("tui")
|
||||
p.Attach("web:phone-1")
|
||||
if got := p.Attachments(); len(got) != 2 {
|
||||
t.Fatalf("attachments = %v, want tui + web:phone-1", got)
|
||||
}
|
||||
p.Detach("web:tablet-9") // unknown face: no-op, no error path
|
||||
if got := p.Attachments(); len(got) != 2 {
|
||||
t.Fatalf("unknown detach mutated attachments: %v", got)
|
||||
}
|
||||
p.Detach("web:phone-1")
|
||||
if got := p.Attachments(); len(got) != 1 || got[0].Face != "tui" {
|
||||
t.Fatalf("attachments = %v, want [tui]", got)
|
||||
}
|
||||
}
|
||||
|
||||
// ── deliberate exit writes the continuity checkpoint ─────────────────
|
||||
|
||||
// TestExitCheckpointLifecycle is the spec scenario ("Clean exit and
|
||||
// restart"): request exit, write the checkpoint FROM canonical state,
|
||||
// and only then may the program report checkpointed.
|
||||
func TestExitCheckpointLifecycle(t *testing.T) {
|
||||
p := NewProgram()
|
||||
p.Attach("tui")
|
||||
p.Submit("tui", "approve migration", "ev:task-2-receipt")
|
||||
if p.Phase() != PhaseRunning {
|
||||
t.Fatalf("phase after submit = %q, want running", p.Phase())
|
||||
}
|
||||
p.RequestExit()
|
||||
if p.Phase() != PhaseExiting {
|
||||
t.Fatalf("phase after RequestExit = %q, want %q", p.Phase(), PhaseExiting)
|
||||
}
|
||||
if _, ok := p.Checkpoint(); ok {
|
||||
t.Fatal("checkpoint exists before WriteCheckpoint — exit must be deliberate")
|
||||
}
|
||||
st := semantic.Seed(80, 24)
|
||||
st.Seq = 42
|
||||
cp := p.WriteCheckpoint(st)
|
||||
if p.Phase() != PhaseCheckpointed {
|
||||
t.Fatalf("phase after WriteCheckpoint = %q, want %q", p.Phase(), PhaseCheckpointed)
|
||||
}
|
||||
got, ok := p.Checkpoint()
|
||||
if !ok {
|
||||
t.Fatal("checkpoint missing after WriteCheckpoint")
|
||||
}
|
||||
if got.ReplayCursor != 42 {
|
||||
t.Errorf("replay cursor = %d, want 42 (canonical state seq)", got.ReplayCursor)
|
||||
}
|
||||
if len(cp.ActiveTasks) != 3 { // the seed's in-flight tasks, derived
|
||||
t.Errorf("active tasks = %v, want the 3 seed tasks", cp.ActiveTasks)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCheckpointSummaryIsCanonicalRefs: the exit-checkpoint summary names
|
||||
// every spec field group and points at canonical state, never a lossy
|
||||
// summary (each group present, replay cursor and evidence refs shown).
|
||||
func TestCheckpointSummaryIsCanonicalRefs(t *testing.T) {
|
||||
p := NewProgram()
|
||||
p.SetContext(Context{
|
||||
PlanRevision: "plan-r3",
|
||||
LastEdited: []string{"docs/D16.md"},
|
||||
PendingDecisions: []string{"approve migration"},
|
||||
PendingAlerts: []string{"calendar handoff due"},
|
||||
WorkHandles: []string{"bridge/rust-worker"},
|
||||
})
|
||||
p.Submit("tui", "approve migration", "ev:task-2-receipt")
|
||||
p.Commit("cmd-1")
|
||||
st := semantic.Seed(80, 24)
|
||||
st.Seq = 42
|
||||
sum := p.WriteCheckpoint(st).Summary()
|
||||
for _, want := range []string{
|
||||
"EXIT CHECKPOINT",
|
||||
"active tasks: task-1, task-2, task-3",
|
||||
"plan revision: plan-r3",
|
||||
"last edited: docs/D16.md",
|
||||
"pending decisions: approve migration",
|
||||
"pending alerts: calendar handoff due",
|
||||
"work handles: bridge/rust-worker",
|
||||
"replay cursor: seq 42",
|
||||
"evidence: ev:task-2-receipt, state:seq-42",
|
||||
} {
|
||||
if !strings.Contains(sum, want) {
|
||||
t.Errorf("checkpoint summary missing %q\nsummary:\n%s", want, sum)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── supervised Go-face/Rust-worker boundary status ───────────────────
|
||||
|
||||
// TestBoundaryStatusLine pins the boundary status vocabulary: the four
|
||||
// supervised worker states, the latched-cancellation mark, and the
|
||||
// fail-closed frame-corruption note.
|
||||
func TestBoundaryStatusLine(t *testing.T) {
|
||||
cases := []struct {
|
||||
bs BoundaryStatus
|
||||
want string
|
||||
}{
|
||||
{BoundaryStatus{Worker: WorkerRunning, Op: "memory-synthesis"},
|
||||
"BOUNDARY | worker running | op memory-synthesis"},
|
||||
{BoundaryStatus{Worker: WorkerRunning, Op: "memory-synthesis", CancelLatched: true},
|
||||
"BOUNDARY | worker running | op memory-synthesis | cancel latched"},
|
||||
{BoundaryStatus{Worker: WorkerRestarting, Op: "memory-synthesis"},
|
||||
"BOUNDARY | worker restarting | op memory-synthesis"},
|
||||
{BoundaryStatus{Worker: WorkerStopped},
|
||||
"BOUNDARY | worker stopped"},
|
||||
{BoundaryStatus{Worker: WorkerFailed, Op: "memory-synthesis", FrameCorrupt: true},
|
||||
"BOUNDARY | worker failed (frame corrupt - no partial effect) | op memory-synthesis"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := c.bs.Line(); got != c.want {
|
||||
t.Errorf("Line() =\n %q\nwant\n %q", got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestRuntimeFaceSurfacesBoundary: the status line beneath the rail
|
||||
// carries the continuous program phase, the attachment count (headless
|
||||
// when no face is attached), and the boundary worker state.
|
||||
func TestRuntimeFaceSurfacesBoundary(t *testing.T) {
|
||||
r := New(80, 24)
|
||||
r.Program().Attach("tui")
|
||||
r.Program().Attach("web:phone-1")
|
||||
r.Program().SetBoundary(BoundaryStatus{Worker: WorkerRunning, Op: "memory-synthesis"})
|
||||
line := r.StatusLine()
|
||||
for _, want := range []string{"RUNTIME", "running", "faces 2", "worker running"} {
|
||||
if !strings.Contains(line, want) {
|
||||
t.Errorf("status line %q missing %q", line, want)
|
||||
}
|
||||
}
|
||||
r.Program().Detach("tui")
|
||||
r.Program().Detach("web:phone-1")
|
||||
if line = r.StatusLine(); !strings.Contains(line, "headless") {
|
||||
t.Errorf("status line with no faces = %q, want headless", line)
|
||||
}
|
||||
}
|
||||
|
||||
// ── the wrapper face: chrome keys, swallow law, precedence ───────────
|
||||
|
||||
// TestChromeKeys: R opens the runtime status takeover, f opens the
|
||||
// web-feed seam view, x/X/esc restore exactly; while either takeover is
|
||||
// open every other key is swallowed — not even quit, not even the other
|
||||
// takeover key.
|
||||
func TestChromeKeys(t *testing.T) {
|
||||
r := New(80, 24)
|
||||
if r.Open() || r.View() != "" {
|
||||
t.Fatal("fresh face has a takeover open")
|
||||
}
|
||||
if a, quit := r.Apply(semantic.Rune('R')); a != RuntimeOpen || quit {
|
||||
t.Fatalf("R = (%q,%v), want (%q,false)", a, quit, RuntimeOpen)
|
||||
}
|
||||
if r.View() != "status" {
|
||||
t.Fatalf("view after R = %q, want status", r.View())
|
||||
}
|
||||
for _, k := range []semantic.Key{semantic.Rune('j'), semantic.Rune('2'), semantic.Rune('y'), semantic.Rune('q'), semantic.Rune('r'), semantic.Rune('f')} {
|
||||
before := r.inner.Snapshot().Fingerprint()
|
||||
if a, quit := r.Apply(k); a != RuntimeSwallow || quit {
|
||||
t.Fatalf("key %s inside takeover = (%q,%v), want swallow/false", k.String(), a, quit)
|
||||
}
|
||||
if got := r.inner.Snapshot().Fingerprint(); got != before {
|
||||
t.Fatalf("key %s leaked into the semantic state inside the takeover", k.String())
|
||||
}
|
||||
}
|
||||
for _, k := range []semantic.Key{semantic.Rune('x'), semantic.Rune('X'), semantic.Named("esc")} {
|
||||
other := New(80, 24)
|
||||
other.Apply(semantic.Rune('R'))
|
||||
if a, _ := other.Apply(k); a != RuntimeRestore {
|
||||
t.Fatalf("restore key %s = %q, want %q", k.String(), a, RuntimeRestore)
|
||||
}
|
||||
if other.Open() {
|
||||
t.Fatalf("restore key %s left the takeover open", k.String())
|
||||
}
|
||||
}
|
||||
r.Apply(semantic.Named("esc")) // restore the status takeover on r itself
|
||||
if r.Open() {
|
||||
t.Fatal("esc did not restore the status takeover")
|
||||
}
|
||||
r.Apply(semantic.Rune('f'))
|
||||
if r.View() != "feed" {
|
||||
t.Fatalf("view after f = %q, want feed", r.View())
|
||||
}
|
||||
if a, _ := r.Apply(semantic.Rune('R')); a != RuntimeSwallow {
|
||||
t.Fatalf("R inside the feed view = %q, want swallow (one view at a time)", a)
|
||||
}
|
||||
if _, quit := r.Apply(semantic.Rune('q')); quit {
|
||||
t.Fatal("quit leaked through an open takeover")
|
||||
}
|
||||
r.Apply(semantic.Named("esc"))
|
||||
if r.Open() {
|
||||
t.Fatal("esc did not restore after feed view")
|
||||
}
|
||||
}
|
||||
|
||||
// TestModalOutranksRuntime: an open modal is a decision boundary — the
|
||||
// runtime chrome keys do not fire through it.
|
||||
func TestModalOutranksRuntime(t *testing.T) {
|
||||
r := New(80, 24)
|
||||
r.Apply(semantic.Rune('m')) // approve modal
|
||||
if a, _ := r.Apply(semantic.Rune('R')); a != "modal.swallow" {
|
||||
t.Fatalf("R through modal = %q, want modal.swallow", a)
|
||||
}
|
||||
if r.Open() {
|
||||
t.Fatal("takeover opened through a modal")
|
||||
}
|
||||
}
|
||||
|
||||
// TestRestoreReturnsLanesExactly: open/close is pure chrome — the inner
|
||||
// semantic state is byte-identical before and after a takeover round
|
||||
// trip, pinned by fingerprint.
|
||||
func TestRestoreReturnsLanesExactly(t *testing.T) {
|
||||
r := New(80, 24)
|
||||
r.Apply(semantic.Rune('2'))
|
||||
r.Apply(semantic.Rune('j'))
|
||||
before := r.inner.Snapshot().Fingerprint()
|
||||
r.Apply(semantic.Rune('R'))
|
||||
r.Apply(semantic.Rune('j'))
|
||||
r.Apply(semantic.Rune('f'))
|
||||
r.Apply(semantic.Named("esc"))
|
||||
if got := r.inner.Snapshot().Fingerprint(); got != before {
|
||||
t.Fatalf("takeover round trip mutated the lanes:\n got %s\nwant %s", got, before)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFingerprintCarriesViewOnly: the open-view bit is fingerprinted
|
||||
// (key routing changes); program state — attachments, commands,
|
||||
// boundary, checkpoint — is bounded display/runtime state and is NOT
|
||||
// (the view-not-semantics law, same as the rail feed and the degrade
|
||||
// ledger).
|
||||
func TestFingerprintCarriesViewOnly(t *testing.T) {
|
||||
r := New(80, 24)
|
||||
base := r.Fingerprint()
|
||||
r.Program().Attach("tui")
|
||||
r.Program().Submit("phone:nicks-phone", "pause playback", "")
|
||||
r.Program().SetBoundary(BoundaryStatus{Worker: WorkerRestarting, Op: "memory-synthesis"})
|
||||
if r.Fingerprint() != base {
|
||||
t.Fatal("program state leaked into the semantic fingerprint")
|
||||
}
|
||||
r.Apply(semantic.Rune('f'))
|
||||
if r.Fingerprint() == base {
|
||||
t.Fatal("open feed view is not fingerprinted — key routing changed")
|
||||
}
|
||||
}
|
||||
|
||||
// TestReadOnlyFeedLaw: the web feed is READ-ONLY. While the feed seam
|
||||
// view is open no key mutates the program — the feed has no input path;
|
||||
// it is a live view, never a second native face.
|
||||
func TestReadOnlyFeedLaw(t *testing.T) {
|
||||
r := New(80, 24)
|
||||
r.Program().Attach("tui")
|
||||
r.Program().Submit("phone:nicks-phone", "pause playback", "ev:arrival-1")
|
||||
feed := r.Program().FeedView()
|
||||
phase := r.Program().Phase()
|
||||
r.Apply(semantic.Rune('f'))
|
||||
for _, k := range []semantic.Key{semantic.Rune('y'), semantic.Rune('j'), semantic.Rune('m'), semantic.Rune('q'), semantic.Rune('R')} {
|
||||
r.Apply(k)
|
||||
}
|
||||
if r.Program().FeedView() != feed {
|
||||
t.Fatal("a key mutated the feed through the seam view — the feed is read-only")
|
||||
}
|
||||
if r.Program().Phase() != phase {
|
||||
t.Fatal("a key mutated the program phase through the feed view")
|
||||
}
|
||||
}
|
||||
|
||||
// ── projections ──────────────────────────────────────────────────────
|
||||
|
||||
// TestRenderProjections: closed = the rail frame + the runtime status
|
||||
// line beneath (region.runtime); open = the full-width takeover
|
||||
// (status or feed) with a close marker. Linear carries zero ANSI.
|
||||
func TestRenderProjections(t *testing.T) {
|
||||
r := New(80, 24)
|
||||
r.Program().SetBoundary(BoundaryStatus{Worker: WorkerRunning, Op: "memory-synthesis"})
|
||||
fr := r.Render()
|
||||
lin := NewLinear(80, 24)
|
||||
lin.Program().SetBoundary(BoundaryStatus{Worker: WorkerRunning, Op: "memory-synthesis"})
|
||||
lfr := lin.Render()
|
||||
if !strings.Contains(lfr.Content, "RUNTIME | running") {
|
||||
t.Fatalf("closed linear frame missing the status line:\n%s", lfr.Content)
|
||||
}
|
||||
if !region(fr.Regions, "region.runtime") {
|
||||
t.Fatalf("closed frame missing region.runtime: %+v", fr.Regions)
|
||||
}
|
||||
r.Apply(semantic.Rune('R'))
|
||||
fr = r.Render()
|
||||
if !strings.Contains(fr.Content, "RUNTIME STATUS") || !strings.Contains(fr.Content, "BOUNDARY") {
|
||||
t.Fatalf("status takeover missing program/boundary state:\n%s", fr.Content)
|
||||
}
|
||||
if !region(fr.Regions, "region.runtime.takeover") || !region(fr.Regions, "region.runtime.close") {
|
||||
t.Fatalf("status takeover regions wrong: %+v", fr.Regions)
|
||||
}
|
||||
l := NewLinear(80, 24)
|
||||
l.Apply(semantic.Rune('f'))
|
||||
flf := l.Render()
|
||||
if strings.ContainsRune(flf.Content, 0x1b) {
|
||||
t.Fatal("linear feed projection carries ANSI — zero-ANSI law")
|
||||
}
|
||||
if !strings.Contains(flf.Content, "WEB FEED") {
|
||||
t.Fatalf("feed takeover missing the feed:\n%s", flf.Content)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOracleIsomorphism: the runtime face implements semantic.Face, so
|
||||
// the isomorphism oracle drives rich against linear over the runtime's
|
||||
// own chrome paths.
|
||||
func TestOracleIsomorphism(t *testing.T) {
|
||||
paths := []semantic.GoldenPath{
|
||||
{Name: "status-restore", Steps: []string{"R", "x"}},
|
||||
{Name: "status-restore-esc", Steps: []string{"R", "esc"}},
|
||||
{Name: "feed-restore-capital", Steps: []string{"f", "X"}},
|
||||
{Name: "takeovers-swallow-all", Steps: []string{"2", "j", "R", "j", "2", "y", "m", "q", "f", "x", "j"}},
|
||||
{Name: "modal-outranks-runtime", Steps: []string{"2", "m", "R", "n", "R", "x"}},
|
||||
}
|
||||
rep := semantic.CheckIsomorphism(New(80, 24), NewLinear(80, 24), paths, 3000)
|
||||
if !rep.OK {
|
||||
t.Fatalf("isomorphism report not OK: %+v", rep)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCloneIndependence: clones carry an independent program copy —
|
||||
// mutating one face's program never reaches the other.
|
||||
func TestCloneIndependence(t *testing.T) {
|
||||
r := New(80, 24)
|
||||
r.Program().Attach("tui")
|
||||
c, ok := r.Clone().(*Runtime)
|
||||
if !ok {
|
||||
t.Fatal("clone is not *Runtime")
|
||||
}
|
||||
c.Program().Attach("web:phone-1")
|
||||
if got := r.Program().Attachments(); len(got) != 1 {
|
||||
t.Fatalf("original attachments = %v, want [tui] only", got)
|
||||
}
|
||||
if got := c.Program().Attachments(); len(got) != 2 {
|
||||
t.Fatalf("clone attachments = %v, want both", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRuntimeRecordsToRail: program lifecycle events surface in the rail
|
||||
// feed (the Wrapper-Face pattern — the rail is the activity log).
|
||||
func TestRuntimeRecordsToRail(t *testing.T) {
|
||||
r := New(80, 24)
|
||||
n := len(r.inner.Feed())
|
||||
r.Attach("web:phone-1")
|
||||
r.Submit("phone:nicks-phone", "pause playback", "")
|
||||
r.RequestExit()
|
||||
st := semantic.Seed(80, 24)
|
||||
r.WriteCheckpoint(st)
|
||||
if got := len(r.inner.Feed()) - n; got < 4 {
|
||||
t.Fatalf("rail feed grew by %d entries, want >=4 (attach, submit, exit, checkpoint)", got)
|
||||
}
|
||||
if !railSourceRecorded(r, "runtime attach web:phone-1") {
|
||||
t.Fatalf("attach not recorded to the rail feed: %+v", r.inner.Feed())
|
||||
}
|
||||
}
|
||||
|
||||
func railSourceRecorded(r *Runtime, text string) bool {
|
||||
for _, e := range r.inner.Feed() {
|
||||
if e.Text == text {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func region(regs []semantic.Region, id string) bool {
|
||||
for _, r := range regs {
|
||||
if r.ID == id {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
ORDERING DISPLAY
|
||||
#1 [committed] tui: approve migration | ev:task-2-receipt
|
||||
#2 [pending] phone:nicks-phone: pause playback | ev:arrival-2
|
||||
WEB FEED COMMAND LINES
|
||||
#1 [committed] tui: approve migration | ev:task-2-receipt
|
||||
#2 [pending] phone:nicks-phone: pause playback | ev:arrival-2
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
#6 [committed] tui: approve migration | ev:task-2-receipt
|
||||
#7 [committed] phone:nicks-phone: pause playback | ev:arrival-7 | CONFLICT - clarification requested
|
||||
#8 [pending] phone:nicks-phone: resume playback | ev:arrival-8 | CONFLICT - clarification requested
|
||||
#9 [pending] remote:tablet: open plan view | ev:arrival-9
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
WEB FEED (read-only) — live view of the one runtime; never a second native face
|
||||
RUNTIME | running | faces: tui, web:phone-1
|
||||
BOUNDARY | worker running | op memory-synthesis
|
||||
#1 [committed] tui: approve migration | ev:task-2-receipt
|
||||
#2 [pending] phone:nicks-phone: pause playback | ev:arrival-2
|
||||
Reference in new issue
Block a user