254 lines
11 KiB
TypeScript
254 lines
11 KiB
TypeScript
// Hermetic NG-7 tests: goal-state incarnation fencing (Mercer F1 class).
|
|
// Run: node --test test/fencing.test.ts
|
|
//
|
|
// SAFETY (NG7-SAFETY-V1W2): every arm uses an ISOLATED temp directory —
|
|
// no live seat state, no Mercer/Marcie/marcie-2 files, no running sessions.
|
|
// The extension source edit is confined to store resolution (store.ts) and
|
|
// the one-line index.ts wiring.
|
|
|
|
import assert from "node:assert/strict";
|
|
import { test } from "node:test";
|
|
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync, readdirSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
import {
|
|
resolveStatePath,
|
|
fencedStateFilePath,
|
|
loadState,
|
|
saveState,
|
|
stateFilePath,
|
|
} from "../lib/store.ts";
|
|
import { setGoal, pauseGoal, initialState } from "../lib/state.ts";
|
|
|
|
function fixtureDir(): string {
|
|
return mkdtempSync(join(tmpdir(), "goal-fence-"));
|
|
}
|
|
|
|
function writeLegacy(dir: string, text: string): void {
|
|
writeFileSync(stateFilePath(dir), JSON.stringify(setGoal(initialState(), text), null, 2) + "\n");
|
|
}
|
|
|
|
// ---- (a) hostile two-incarnation collision: the measured class ---------------
|
|
|
|
test("(a) two incarnations of one seat never share or clobber state", () => {
|
|
const dir = fixtureDir();
|
|
try {
|
|
const a = resolveStatePath(dir, { incarnationId: "inc-marcie" });
|
|
const b = resolveStatePath(dir, { incarnationId: "inc-marcie-2" });
|
|
assert.notEqual(a, b, "distinct incarnations must resolve distinct files");
|
|
|
|
// incarnation A sets an active goal
|
|
const stateA = setGoal(initialState(), "ship the canary");
|
|
saveState(stateA, a);
|
|
// incarnation B (a second process of the SAME seat) loads ITS state
|
|
const stateB = loadState(b);
|
|
assert.equal(stateB.status, "none", "B must not inherit A's active focus");
|
|
assert.equal(stateB.text, "", "B sees no goal text of A's");
|
|
// B sets its own goal; A's file is untouched
|
|
saveState(setGoal(initialState(), "B's own goal"), b);
|
|
assert.equal(loadState(a).text, "ship the canary", "A's focus survives B's activity");
|
|
assert.equal(loadState(b).text, "B's own goal");
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("(a) B cannot pause or settle A's focus through any store operation", () => {
|
|
const dir = fixtureDir();
|
|
try {
|
|
const a = resolveStatePath(dir, { incarnationId: "inc-a" });
|
|
const b = resolveStatePath(dir, { incarnationId: "inc-b" });
|
|
saveState(setGoal(initialState(), "A active"), a);
|
|
|
|
// Every mutation B performs addresses b's path only; A's bytes cannot
|
|
// change through B's store API — proven by writing from B and re-reading A.
|
|
saveState(pauseGoal(loadState(b), "blocked:sibling", "cap"), b);
|
|
const aNow = loadState(a);
|
|
assert.equal(aNow.status, "active", "A's focus is still active after B's pause");
|
|
assert.equal(aNow.pausedReason, undefined);
|
|
|
|
// And the directory contains exactly the two fenced files.
|
|
const names = readdirSync(dir).filter((n) => n.startsWith("goal-state"));
|
|
assert.equal(names.length, 2);
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("(a) session_start-style reload stays incarnation-scoped", () => {
|
|
const dir = fixtureDir();
|
|
try {
|
|
const a = resolveStatePath(dir, { incarnationId: "inc-a" });
|
|
saveState(setGoal(initialState(), "A goal v1"), a);
|
|
// simulate: A's session reloads (new/resume/reload all re-load from ITS path)
|
|
const reloaded = loadState(resolveStatePath(dir, { incarnationId: "inc-a" }));
|
|
assert.equal(reloaded.text, "A goal v1");
|
|
// a sibling's state never leaks into A's reload
|
|
saveState(setGoal(initialState(), "sibling"), resolveStatePath(dir, { incarnationId: "inc-sib" }));
|
|
assert.equal(loadState(resolveStatePath(dir, { incarnationId: "inc-a" })).text, "A goal v1");
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
// ---- (b) legacy migration fails safe -------------------------------------------
|
|
|
|
test("(b) legacy unfenced state is QUARANTINED, never claimed (NG-7 F4)", () => {
|
|
const dir = fixtureDir();
|
|
try {
|
|
writeLegacy(dir, "legacy active goal");
|
|
const a = resolveStatePath(dir, { incarnationId: "inc-first" });
|
|
// FAIL SAFE: the first incarnation must NOT inherit the unknown owner's
|
|
// ACTIVE focus — it starts fresh.
|
|
const state = loadState(a);
|
|
assert.equal(state.status, "none", "no active focus inherited from legacy");
|
|
assert.equal(state.text, "");
|
|
// EVIDENCE PRESERVED: the legacy bytes live on in the quarantine file.
|
|
const quarantined = JSON.parse(readFileSync(join(dir, "goal-state.legacy.json"), "utf8")) as { text: string };
|
|
assert.equal(quarantined.text, "legacy active goal");
|
|
const names = readdirSync(dir);
|
|
assert.equal(names.includes("goal-state.json"), false, "legacy name vacated");
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("(b/F4) ACTIVE legacy + fresh incarnation: no active focus inherited, evidence preserved", () => {
|
|
const dir = fixtureDir();
|
|
try {
|
|
// the live canary shape: legacy state carrying an ACTIVE focus of a
|
|
// previous (unknown-owner) incarnation
|
|
writeLegacy(dir, "mercer's previous focus");
|
|
for (const inc of ["inc-canary-1", "inc-canary-2"]) {
|
|
const p = resolveStatePath(dir, { incarnationId: inc });
|
|
assert.equal(loadState(p).status, "none", `${inc} inherits no active focus`);
|
|
}
|
|
// second quarantine slot when another legacy reappears (counter path)
|
|
writeLegacy(dir, "second legacy");
|
|
resolveStatePath(dir, { incarnationId: "inc-third" });
|
|
assert.equal(
|
|
readFileSync(join(dir, "goal-state.legacy.1.json"), "utf8").includes("second legacy"),
|
|
true,
|
|
"counter slot preserves the second legacy",
|
|
);
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("(b/F3) migration race: rename attempted with legacy present, failure -> fresh", () => {
|
|
const dir = fixtureDir();
|
|
try {
|
|
writeLegacy(dir, "legacy goal");
|
|
let renameCalls = 0;
|
|
const path = resolveStatePath(dir, {
|
|
incarnationId: "inc-raced",
|
|
io: {
|
|
// sequenced: fenced absent (first probe), legacy present (second)
|
|
existsSync: (p: string) => p === stateFilePath(dir),
|
|
renameSync: () => {
|
|
renameCalls += 1;
|
|
throw new Error("EEXIST: sibling quarantined it first");
|
|
},
|
|
},
|
|
});
|
|
assert.equal(path, fencedStateFilePath(dir, "inc-raced"));
|
|
assert.equal(renameCalls, 1, "the rename branch EXECUTED (not the fenced early-return)");
|
|
assert.equal(loadState(path).status, "none", "race loses to fresh");
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("(b/F2) traversal-shaped launcher claim is rejected at the identity source", async () => {
|
|
const { incarnationIdentity, resetIncarnationGlobal } = await import("../../mosaic-core/lib/incarnation.ts");
|
|
const prev = process.env.MOSAIC_LAUNCH_INCARNATION;
|
|
resetIncarnationGlobal();
|
|
try {
|
|
for (const hostile of ["../../etc/pwn", "..", "/abs", "a/b", ".hidden", "x".repeat(200)]) {
|
|
process.env.MOSAIC_LAUNCH_INCARNATION = hostile;
|
|
const id = incarnationIdentity(() => `minted-for-${hostile.slice(0, 3)}`);
|
|
assert.notEqual(id, hostile, `claim ${JSON.stringify(hostile)} must not become the identity`);
|
|
assert.equal(id.startsWith("minted-for-"), true, `falls through to mint for ${JSON.stringify(hostile)}`);
|
|
resetIncarnationGlobal();
|
|
}
|
|
} finally {
|
|
if (prev === undefined) delete process.env.MOSAIC_LAUNCH_INCARNATION;
|
|
else process.env.MOSAIC_LAUNCH_INCARNATION = prev;
|
|
resetIncarnationGlobal();
|
|
}
|
|
});
|
|
|
|
test("(b/F2) hostile claim keeps BOTH consumer paths inside the state root", async () => {
|
|
const { createJournal } = await import("../../mosaic-core/lib/journal.ts");
|
|
const { incarnationIdentity, resetIncarnationGlobal } = await import("../../mosaic-core/lib/incarnation.ts");
|
|
const prev = process.env.MOSAIC_LAUNCH_INCARNATION;
|
|
resetIncarnationGlobal();
|
|
try {
|
|
process.env.MOSAIC_LAUNCH_INCARNATION = "../../outside";
|
|
const id = incarnationIdentity(); // no-arg production form (F1)
|
|
assert.notEqual(id, "../../outside");
|
|
const j = createJournal({ incarnationId: id, stateHome: "/xdg/state", io: { mkdirSync() {}, appendFileSync() {} } });
|
|
assert.equal(j.path.startsWith("/xdg/state/mosaic-core/"), true, "journal contained");
|
|
assert.equal(j.path.includes(".."), false);
|
|
const dir = fixtureDir();
|
|
const gp = resolveStatePath(dir, { incarnationId: id });
|
|
assert.equal(gp.startsWith(dir), true, "goal path contained");
|
|
assert.equal(gp.includes(".."), false);
|
|
rmSync(dir, { recursive: true, force: true });
|
|
} finally {
|
|
if (prev === undefined) delete process.env.MOSAIC_LAUNCH_INCARNATION;
|
|
else process.env.MOSAIC_LAUNCH_INCARNATION = prev;
|
|
resetIncarnationGlobal();
|
|
}
|
|
});
|
|
|
|
test("(b) corrupt legacy never blocks fencing", () => {
|
|
const dir = fixtureDir();
|
|
try {
|
|
writeFileSync(stateFilePath(dir), "{corrupt");
|
|
const a = resolveStatePath(dir, { incarnationId: "inc-x" });
|
|
assert.equal(loadState(a).status, "none", "corrupt legacy -> fresh initial state");
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
// ---- (c) PI_SESSION_ID is never the incarnation --------------------------------
|
|
|
|
test("(c) fenced paths key on the process-launch identity, not PI_SESSION_ID", async () => {
|
|
const dir = fixtureDir();
|
|
try {
|
|
const prev = process.env.PI_SESSION_ID;
|
|
process.env.PI_SESSION_ID = "session-should-not-appear";
|
|
const { incarnationIdentity } = await import("../../mosaic-core/lib/incarnation.ts");
|
|
const id = incarnationIdentity(() => "minted-check");
|
|
process.env.PI_SESSION_ID = prev === undefined ? "" : prev;
|
|
if (prev === undefined) delete process.env.PI_SESSION_ID;
|
|
assert.notEqual(id, "session-should-not-appear");
|
|
const p = resolveStatePath(dir, { incarnationId: id });
|
|
assert.doesNotMatch(p, /session-should-not-appear/);
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
// ---- compatibility ---------------------------------------------------------------
|
|
|
|
test("goal_report compatibility: the state machine surface is unchanged", async () => {
|
|
// The pure state machine (setGoal/pauseGoal/resume/recordReport/validateReport)
|
|
// is untouched by fencing; the existing goal suite covers it. Here we pin
|
|
// the compatibility contract fencing must not break: a state saved and
|
|
// loaded through the fenced path round-trips exactly as the legacy path did.
|
|
const dir = fixtureDir();
|
|
try {
|
|
const p = resolveStatePath(dir, { incarnationId: "inc-compat" });
|
|
const s = setGoal(initialState(), "round trip");
|
|
saveState(s, p);
|
|
assert.deepEqual(loadState(p), s);
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|