// The §11 secure-reset contract: the durable generation record, the six states, // create-on-miss refusal, archival by generation, crash recovery at every step, // and the sync gate. Runs entirely against an in-memory io — no Chrome, no // vault, no crypto. import { describe, expect, it } from "vitest"; import { GENERATION_UNKNOWN, LEGACY_ACTIVE, REPAIRING, RESET_CONFIRMED, RESET_REQUIRED, V2_ACTIVE, V2_ANCHOR, assertSyncAllowed, confirmSecureReset, decodeGeneration, encodeGeneration, enterResetRequired, isPostReset, mintDecision, onDirectV2PairingEstablished, resolveGeneration, type ResetResult, type SyncGeneration, type SyncGenerationBlob, type SyncGenerationIo, } from "./sync-generation"; class CrashAt extends Error {} class FakeGenerationIo implements SyncGenerationIo { blob: unknown = null; pairedDevices = false; revocations = false; pendingHolders = false; legacyRouteMaterial = false; /** Storage that refuses the write, to model a generation that cannot land. */ failWrites = false; failArchive = false; failClear = false; failIdentity = false; /** Simulated process death: the checkpoint with this name throws. */ crashAt: string | null = null; archiveRef: string | null = null; archiveCount = 0; cleared = false; identityMints = 0; deviceId = "anchor-device"; /** The order the reset actually touched storage. */ readonly calls: string[] = []; get record(): SyncGeneration | null { return decodeGeneration(this.blob)?.active ?? null; } async readBlob(): Promise { return this.blob; } async writeBlob(blob: SyncGenerationBlob): Promise { if (this.failWrites) return false; this.blob = JSON.parse(encodeGeneration(blob)) as unknown; return true; } async hasPairedDevices(): Promise { return this.pairedDevices; } async hasRevocations(): Promise { return this.revocations; } async hasPendingHolders(): Promise { return this.pendingHolders; } async hasLegacyRouteMaterial(): Promise { return this.legacyRouteMaterial; } async archiveLegacyState(generation: number): Promise { if (this.failArchive) return null; this.archiveCount += 1; this.archiveRef = `peach_sync_archive_g${generation}_v1`; this.calls.push(`archive:${generation}`); return this.archiveRef; } async clearLegacyTrustState(): Promise { if (this.failClear) return false; this.cleared = true; this.pairedDevices = false; this.revocations = false; this.pendingHolders = false; this.legacyRouteMaterial = false; this.calls.push("clear"); return true; } async ensureV2Identity(): Promise { if (this.failIdentity) return false; this.identityMints += 1; this.calls.push("identity"); return true; } async getDeviceId(): Promise { return this.deviceId; } reset(nowMs = 1_000): Promise { return confirmSecureReset(this, { nowMs, checkpoint: (name) => { if (name === this.crashAt) throw new CrashAt(name); }, }); } } const CHECKPOINTS = [ "before-confirmed", "before-archive", "after-archive", "before-clear", "after-clear", "before-identity", "after-identity", "before-generation-write", "after-generation-write", ]; describe("sync generation — derived state", () => { it("derives V2_ACTIVE at generation 2 for a fresh install, and persists it", async () => { const io = new FakeGenerationIo(); const record = await resolveGeneration(io, 1_000); expect(record.state).toBe(V2_ACTIVE); expect(record.generation).toBe(2); expect(io.record).toEqual(record); }); it("derives LEGACY_ACTIVE at generation 1 when any trust state is present", async () => { const io = new FakeGenerationIo(); io.pairedDevices = true; const record = await resolveGeneration(io, 1_000); expect(record.state).toBe(LEGACY_ACTIVE); expect(record.generation).toBe(1); }); it("resolves an unreadable record to UNKNOWN rather than to a generation", async () => { const io = new FakeGenerationIo(); io.blob = "{ not json"; expect((await resolveGeneration(io, 1_000)).state).toBe(GENERATION_UNKNOWN); expect(await assertSyncAllowed(io, 1_000)).toBe("sync_generation_unknown"); }); it("refuses a record of another schema instead of misreading it", async () => { // The rollback case: a build that does not know the generation must not // treat v2 state as legacy trust. const io = new FakeGenerationIo(); io.blob = { schemaVersion: 99, active: { schemaVersion: 99, generation: 7, state: V2_ACTIVE, createdAt: 1 }, }; expect(await assertSyncAllowed(io, 1_000)).toBe("sync_generation_unknown"); }); it("refuses an unrecognised state instead of guessing", async () => { const io = new FakeGenerationIo(); io.blob = { schemaVersion: 1, active: { schemaVersion: 1, generation: 1, state: "WOBBLING", createdAt: 1 }, }; expect(await assertSyncAllowed(io, 1_000)).toBe("sync_generation_unknown"); }); it("does not trust a derived record that could not be persisted", async () => { const io = new FakeGenerationIo(); io.pairedDevices = true; io.failWrites = true; expect((await resolveGeneration(io, 1_000)).state).toBe(GENERATION_UNKNOWN); expect(await assertSyncAllowed(io, 1_000)).toBe("sync_generation_unknown"); }); }); describe("sync generation — create-on-miss refusal (§11.5)", () => { it("refuses to mint over every class of trust state", async () => { const classes: Array<[string, (io: FakeGenerationIo) => void]> = [ ["paired device", (io) => (io.pairedDevices = true)], ["revocation", (io) => (io.revocations = true)], ["pending holder", (io) => (io.pendingHolders = true)], ["legacy route material", (io) => (io.legacyRouteMaterial = true)], ]; for (const [label, configure] of classes) { const io = new FakeGenerationIo(); configure(io); expect(await mintDecision(io, 1_000), label).toBe("REFUSE_RESET_REQUIRED"); expect(io.record?.state, label).toBe(RESET_REQUIRED); expect(io.identityMints, label).toBe(0); expect(await assertSyncAllowed(io, 1_000), label).toBe("reset_required"); } }); it("mints only on a genuinely fresh installation", async () => { const io = new FakeGenerationIo(); expect(await mintDecision(io, 1_000)).toBe("MINT"); expect(io.record).toBeNull(); }); }); describe("sync generation — the reset", () => { it("archives, clears, mints, then flips the pointer — in that order", async () => { const io = new FakeGenerationIo(); io.pairedDevices = true; io.revocations = true; expect(await io.reset()).toBe("V2_ANCHOR"); expect(io.calls).toEqual(["archive:1", "clear", "identity"]); expect(io.record).toMatchObject({ state: V2_ANCHOR, generation: 2, anchorDeviceId: "anchor-device", legacyArchiveRef: "peach_sync_archive_g1_v1", }); expect(io.cleared).toBe(true); }); it("does not retire the v2 generation or mint twice when repeated", async () => { const io = new FakeGenerationIo(); io.pairedDevices = true; await io.reset(); const firstArchive = io.record?.legacyArchiveRef; expect(await io.reset()).toBe("reset_not_completed"); expect(io.record?.legacyArchiveRef).toBe(firstArchive); expect(io.identityMints).toBe(1); }); it("stays in RESET_CONFIRMED and reports the failure when a step fails", async () => { for (const configure of [ (io: FakeGenerationIo) => (io.failArchive = true), (io: FakeGenerationIo) => (io.failClear = true), (io: FakeGenerationIo) => (io.failIdentity = true), (io: FakeGenerationIo) => (io.failWrites = true), ]) { const io = new FakeGenerationIo(); io.pairedDevices = true; configure(io); expect(await io.reset()).toBe("reset_not_completed"); expect(io.record?.state === RESET_CONFIRMED || io.record === null).toBe(true); } }); }); describe("sync generation — crash injection (§11.8)", () => { it("leaves the archive durable and the retry reaches V2_ANCHOR from any crash point", async () => { for (const name of CHECKPOINTS) { const io = new FakeGenerationIo(); io.pairedDevices = true; io.crashAt = name; await expect(io.reset()).rejects.toThrow(CrashAt); // Never a wrong outcome: nothing retired yet, mid-flight, or committed. const state = io.record?.state; expect([LEGACY_ACTIVE, RESET_CONFIRMED, V2_ANCHOR], name).toContain(state); // Trust is never cleared without an archive first. expect(io.cleared && io.archiveCount === 0, `${name} cleared without an archive`).toBe(false); io.crashAt = null; await io.reset(); expect(io.record?.state, `retry after ${name}`).toBe(V2_ANCHOR); expect(io.record?.generation, `retry after ${name}`).toBe(2); expect(io.archiveRef).not.toBeNull(); } }); it("blocks sync before the pointer flip and allows it after", async () => { const before = new FakeGenerationIo(); before.pairedDevices = true; before.crashAt = "before-generation-write"; await expect(before.reset()).rejects.toThrow(CrashAt); expect(before.record?.state).toBe(RESET_CONFIRMED); expect(await assertSyncAllowed(before, 1_000)).toBe("reset_required"); const after = new FakeGenerationIo(); after.pairedDevices = true; after.crashAt = "after-generation-write"; await expect(after.reset()).rejects.toThrow(CrashAt); expect(after.record?.state).toBe(V2_ANCHOR); expect(await assertSyncAllowed(after, 1_000)).toBe("allowed"); expect(after.archiveRef).not.toBeNull(); expect(after.identityMints).toBe(1); }); it("loses nothing when the crash lands after the archive but before the clear", async () => { const io = new FakeGenerationIo(); io.pairedDevices = true; io.revocations = true; io.crashAt = "before-clear"; await expect(io.reset()).rejects.toThrow(CrashAt); expect(io.archiveCount).toBe(1); expect(io.cleared).toBe(false); expect(io.pairedDevices).toBe(true); }); }); describe("sync generation — the sync gate (§7)", () => { it("allows a legacy device to sync before it resets", async () => { const io = new FakeGenerationIo(); io.pairedDevices = true; expect(await assertSyncAllowed(io, 1_000)).toBe("allowed"); expect(await isPostReset(io, 1_000)).toBe(false); }); it("allows a device that has already reset to sync and pair again", async () => { const io = new FakeGenerationIo(); io.pairedDevices = true; await io.reset(); expect(await assertSyncAllowed(io, 1_000)).toBe("allowed"); expect(await isPostReset(io, 1_000)).toBe(true); }); it("blocks sync from RESET_REQUIRED and never walks a reset device backwards", async () => { const legacy = new FakeGenerationIo(); legacy.pairedDevices = true; await enterResetRequired(legacy, 1_000); expect(await assertSyncAllowed(legacy, 1_000)).toBe("reset_required"); const reset = new FakeGenerationIo(); reset.pairedDevices = true; await reset.reset(); await enterResetRequired(reset, 2_000); expect(reset.record?.state).toBe(V2_ANCHOR); }); }); describe("sync generation — repair progression (§11.3)", () => { it("is REPAIRING until a direct peer exists, then V2_ACTIVE", async () => { const io = new FakeGenerationIo(); io.pairedDevices = true; await io.reset(); await onDirectV2PairingEstablished(io, false, 2_000); expect(io.record?.state).toBe(REPAIRING); expect(await isPostReset(io, 2_000)).toBe(true); await onDirectV2PairingEstablished(io, true, 3_000); expect(io.record?.state).toBe(V2_ACTIVE); }); it("never disturbs a device that never reset", async () => { const io = new FakeGenerationIo(); io.pairedDevices = true; await onDirectV2PairingEstablished(io, true, 2_000); expect(io.record?.state).toBe(LEGACY_ACTIVE); }); }); describe("sync generation — §15.4: the reset retires a generation, it does not rotate the root", () => { it("reaches storage only through archive, clear and identity, in that order", async () => { const io = new FakeGenerationIo(); io.pairedDevices = true; io.revocations = true; io.legacyRouteMaterial = true; expect(await io.reset()).toBe("V2_ANCHOR"); // The io is the whole surface the reset can reach. It carries no root key, // no recovery material, and no vault-id parameter, so no rotation is // possible by construction; what is worth pinning is the order that makes // the archive safe. expect(io.calls).toEqual(["archive:1", "clear", "identity"]); expect(io.record?.legacyArchiveRef).toBe("peach_sync_archive_g1_v1"); expect(io.record?.generation).toBe(2); }); });