@@ -0,0 +1,253 @@
|
||||
// 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 });
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user