#!/usr/bin/env bash # test-owed-withdrawal.sh — rule tests for the owed-set derivation in # extensions/pi/mempalace.ts: isStrictlyAfter, isAnswered, isWithdrawn. # # WHY THIS EXISTS # isWithdrawn changes who is allowed to clear an obligation, which is the one # thing in this extension that can go wrong SILENTLY. A wrong rule here does # not throw and does not show up in a log — it makes a real unanswered ask # vanish from a mailbox forever. So the rules get pinned down before they ship. # # WHY IT EXTRACTS THE PREDICATES FROM THE SOURCE INSTEAD OF RESTATING THEM # These are closures inside createExtension(), so they cannot be imported. The # tempting shortcut is to paste a copy of the logic into the test — which tests # the copy. This repo has already paid for a divergent second copy (the # pi-extensions skill mirror, 9579 B behind for weeks; the shell-lint logic # extracted to one file for the same reason). So the harness cuts the actual # declaration text out of mempalace.ts by brace matching and runs THAT. Change # the source and this test follows; paraphrase the source and it cannot. # # FIXTURES ARE VERBATIM REAL EVENTS from the fleet log (project/pi-devbox), not # invented shapes — including the exact incident that motivated isWithdrawn: # mbp-m1-2020 withdrew a v1.8.13 rollout ask to tor-ms22 at seq 119 and the # withdrawal had no effect, so tor-ms22 was still being told it owed a reply 41h # later for a release it never installed. # # Usage: scripts/test-owed-withdrawal.sh [source.ts] (exit 0 = all rules behave) # # The optional argument exists so the suite can be pointed at a deliberately # MUTATED copy of the source to prove it is sensitive — a suite that has never # been observed to fail is not evidence. See the mutation check in the CHANGELOG # entry that introduced isWithdrawn. set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" SRC="${1:-$REPO_ROOT/extensions/pi/mempalace.ts}" WORK="$(mktemp -d)" trap 'rm -rf "$WORK"' EXIT [ -r "$SRC" ] || { echo "FAIL: cannot read $SRC" >&2; exit 2; } # A gate that cannot run must not pass — the standing rule in this repo. node --experimental-strip-types --check "$SRC" \ || { echo "FAIL: $SRC does not parse" >&2; exit 2; } # ---------------------------------------------------------------- extractor --- cat >"$WORK/extract.mjs" <<'EXTRACT' import { readFileSync, writeFileSync } from "node:fs"; const src = readFileSync(process.argv[2], "utf8"); /** * Cut `const = ...;` out of the source by matching delimiters, so the * test runs the shipped text. Returns the declaration verbatim. */ function decl(name) { const start = src.indexOf(`const ${name} =`); if (start < 0) throw new Error(`declaration not found in source: ${name}`); let depth = 0; let inStr = null; for (let i = start; i < src.length; i++) { const c = src[i]; const prev = src[i - 1]; if (inStr) { if (c === inStr && prev !== "\\") inStr = null; continue; } if (c === '"' || c === "'" || c === "`") { inStr = c; continue; } if (c === "/" && src[i + 1] === "/") { i = src.indexOf("\n", i); if (i < 0) break; continue; } if (c === "{" || c === "(" || c === "[") depth++; else if (c === "}" || c === ")" || c === "]") depth--; else if (c === ";" && depth === 0) return src.slice(start, i + 1); } throw new Error(`unterminated declaration: ${name}`); } const parts = ["TERMINAL_STATUS", "isStrictlyAfter", "isAnswered", "isWithdrawn"].map(decl); // Sanity: the extractor must have found real bodies, not empty matches. A // silently-empty extraction would make every assertion below pass vacuously. for (const [i, p] of parts.entries()) { if (p.length < 40) throw new Error(`extracted declaration ${i} is implausibly short: ${p}`); } if (!parts[3].includes("withdraws")) throw new Error("isWithdrawn does not mention its marker key"); writeFileSync(process.argv[3], parts.join("\n\n")); EXTRACT node "$WORK/extract.mjs" "$SRC" "$WORK/extracted.ts" echo "[extract] pulled 4 declarations from $(basename "$SRC") ($(wc -c <"$WORK/extracted.ts") bytes)" # ------------------------------------------------------------------ harness --- { cat <<'HEAD' type LogEvent = { id?: string; seq?: number; hlc?: string; type?: string; status?: string; from_agent?: string; to_agent?: string; correlation_id?: string | null; created_at?: string; body?: string; metadata?: Record | null; }; const mailboxAddress = "pi@tor-ms22"; HEAD cat "$WORK/extracted.ts" cat <<'TAIL' // ---- VERBATIM fixtures from the real fleet log, stream project/pi-devbox ---- const REP = "rep_d344e349ba276d6fc11997cd552f6937"; /** seq 112 — mbp's v1.8.13 rollout ask to tor-ms22. The obligation. */ const ask112: LogEvent = { id: "evt_20260907T125110_2bef6f9b07a8", seq: 112, hlc: `1788785470242-000000-${REP}`, type: "task.request", status: "open", from_agent: "pi@mbp-m1-2020", to_agent: "pi@tor-ms22", correlation_id: "v1813-client-rollout-tor-ms22", }; /** seq 119 — mbp's withdrawal of its OWN ask. Terminal, directed, marker present. */ const withdraw119: LogEvent = { id: "evt_20260908T223949_8f5b9bec7c92", seq: 119, hlc: `1788907189286-000000-${REP}`, type: "task.reply", status: "superseded", from_agent: "pi@mbp-m1-2020", to_agent: "pi@tor-ms22", correlation_id: "v1813-client-rollout-tor-ms22", metadata: { closes: "v1813-client-rollout-tor-ms22", nothing_owed: "no reply required to this event", replacement: "v1814-client-rollout-tor-ms22", }, }; /** seq 120 — the LIVE v1.8.14 ask. Must survive the v1813 withdrawal. */ const ask120: LogEvent = { id: "evt_20260908T224118_808de43d6982", seq: 120, hlc: `1788907278075-000000-${REP}`, type: "task.request", status: "open", from_agent: "pi@mbp-m1-2020", to_agent: "pi@tor-ms22", correlation_id: "v1814-client-rollout-tor-ms22", }; /** seq 122 — tor-ms22's OWN terminal reply, which is what actually cleared 112. */ const myReply122: LogEvent = { id: "evt_20260909T061851_cf9bd18800db", seq: 122, hlc: `1788934731146-000000-${REP}`, type: "task.reply", status: "superseded", from_agent: "pi@tor-ms22", to_agent: "pi@mbp-m1-2020", correlation_id: "v1813-client-rollout-tor-ms22", metadata: { closes: "v1813-client-rollout-tor-ms22 — from the RECIPIENT side, which is the only side that can" }, }; const clone = (e: LogEvent, over: Partial): LogEvent => ({ ...e, ...over }); let failed = 0; const check = (name: string, expected: boolean, actual: boolean): void => { const ok = expected === actual; if (!ok) failed++; console.log(`${ok ? " ok " : " FAIL"} ${name} (expected ${expected}, got ${actual})`); }; console.log("\nisWithdrawn — the requester retracting its own ask"); // THE INCIDENT. Before this rule existed the answer was false and tor-ms22 was // told it owed a reply for a release that no longer existed. check("real seq 119 withdraws real seq 112", true, isWithdrawn(ask112, [withdraw119])); check("withdrawal does NOT touch the live v1814 ask", false, isWithdrawn(ask120, [withdraw119])); console.log("\nisWithdrawn — the ways a mailbox must NOT be silently emptied"); check( "no marker: a bare terminal event from the requester is not a withdrawal", false, isWithdrawn(ask112, [clone(withdraw119, { metadata: { nothing_owed: "no reply required" } })]), ); check( "marker naming a DIFFERENT thread does not clear this one", false, isWithdrawn(ask112, [clone(withdraw119, { metadata: { closes: "v1814-client-rollout-tor-ms22" } })]), ); check( "marker carrying PROSE does not count (tor-ms22's own seq 122 shape)", false, isWithdrawn(ask112, [ clone(withdraw119, { metadata: { closes: "v1813-client-rollout-tor-ms22 — from the RECIPIENT side, which is the only side that can" }, }), ]), ); check( "a THIRD PARTY cannot retract someone else's ask", false, isWithdrawn(ask112, [clone(withdraw119, { from_agent: "pi@emb-7kj4vr4g" })]), ); check( "a BROADCAST cannot empty every machine's mailbox at once", false, isWithdrawn(ask112, [clone(withdraw119, { to_agent: "*" })]), ); check( "a NON-TERMINAL status is not a withdrawal", false, isWithdrawn(ask112, [clone(withdraw119, { status: "open" })]), ); check( "an event addressed to a DIFFERENT device does not clear my ask", false, isWithdrawn(ask112, [clone(withdraw119, { to_agent: "pi@emb-7kj4vr4g" })]), ); check( "ORDERING: a withdrawal cannot retire an ask the requester sent LATER", false, isWithdrawn(clone(ask112, { seq: 121, hlc: `1788907300000-000000-${REP}` }), [withdraw119]), ); console.log("\nisWithdrawn — accepted marker spellings"); check( "canonical `withdraws` naming the correlation", true, isWithdrawn(ask112, [clone(withdraw119, { metadata: { withdraws: "v1813-client-rollout-tor-ms22" } })]), ); check( "marker naming the ask's EVENT ID instead of its correlation", true, isWithdrawn(ask112, [clone(withdraw119, { metadata: { withdraws: ask112.id } })]), ); console.log("\nisAnswered — regression guard, unchanged behaviour"); check("my own terminal reply still closes my own ask", true, isAnswered(ask112, [myReply122])); check("my reply on one thread does not close another", false, isAnswered(ask120, [myReply122])); check( "ORDERING still load-bearing: my reply cannot pre-close a LATER ask", false, isAnswered(clone(ask112, { seq: 130, hlc: `1788999999999-000000-${REP}` }), [myReply122]), ); console.log("\nEND TO END — the derived owed set for tor-ms22 on 2026-09-09"); // The behaviour change, stated as the derivation states it. `mine` deliberately // EXCLUDES seq 122 so this measures the new rule rather than the reply that // happened to be written first. const candidates = [ask112, ask120]; const mine: LogEvent[] = []; const inbound = [withdraw119]; const owed = candidates.filter((c) => !isAnswered(c, mine) && !isWithdrawn(c, inbound)); const owedIds = owed.map((e) => e.correlation_id).join(", "); check("exactly one ask remains owed", true, owed.length === 1); check("and it is the v1814 one, not the withdrawn v1813", true, owedIds === "v1814-client-rollout-tor-ms22"); console.log(` derived owed set: [${owedIds}]`); console.log(failed === 0 ? "\n=== PASSED ===" : `\n=== FAILED: ${failed} ===`); process.exit(failed === 0 ? 0 : 1); TAIL } >"$WORK/harness.ts" node --experimental-strip-types "$WORK/harness.ts"