Stage 7 close-out: verifier hint wiring (pointer identity into quarantine) + quarantine round-trip tests S06-S09. 10 new tests (store round-trip, reopen persistence, B-6 v1->v2 migration keeps favorites, slot-DB deletion isolation, no-loop skip with ZERO file fetches, latest.json advance clears skip, bad-sig lands in IDB via sink). Full CI green: 285 tests.
Some checks failed
ci / check (push) Has been cancelled

This commit is contained in:
Lumen Stage1 2026-10-01 10:22:13 -05:00
commit 853c19fbac
2 changed files with 363 additions and 0 deletions

View file

@ -0,0 +1,359 @@
/* eslint-disable @typescript-eslint/require-await, @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-unsafe-call, @typescript-eslint/prefer-promise-reject-errors */
/**
* Stage 7 close-out — persistent quarantine store + no-loop pull skip.
* Covers: quarantine round-trip (add/list/prune/clear), survives reopen,
* v1->v2 additive user-DB migration keeps favorites (B-6), pullCandidate
* issues ZERO manifest/file fetches for a quarantined pointer, and the
* verifier wiring that lands rejections into the IDB store (sole-decider:
* rejection quarantines, active dataset untouched).
* Trace: IMPLEMENTATION-CONTRACT.md §11 I-10, Stage 7 acceptance, SPIKE-02 F-5
*/
import { describe, it, expect, beforeEach, afterEach } from "vitest";
// @ts-expect-error fake-indexeddb types via exports fallback
import FDBFactory from "fake-indexeddb/lib/FDBFactory";
// @ts-expect-error fake-indexeddb types via exports fallback
import FDBKeyRange from "fake-indexeddb/lib/FDBKeyRange";
const g = globalThis as unknown as Record<string, unknown>;
g.indexedDB = new FDBFactory() as unknown;
g.IDBKeyRange = FDBKeyRange as unknown;
import { DB } from "../../src/platform/idb/names.js";
import { openDB, idbPut } from "../../src/platform/idb/wrapper.js";
import { openUserDB, listFavorites } from "../../src/data/user/store.js";
import {
addQuarantined,
isQuarantined,
listQuarantined,
clearQuarantined,
pruneQuarantinedUpTo,
quarantineSink,
quarantineKey,
} from "../../src/data/user/quarantine.js";
import { pullCandidate } from "../../src/sync/pull.js";
import type { Transport } from "../../src/sync/transport/types.js";
import { verifyPackage } from "../../src/sync/verifier/package.js";
import { publicKeyFromDerBase64 } from "../../src/sync/verifier/ed25519.js";
import { canonicalJson } from "../../pipeline/canonical-json.js";
import { buildPackage } from "../../pipeline/package.js";
import { generateTestKeyPair } from "../../pipeline/sign.js";
import { makeValidInput } from "../../pipeline/fixtures.js";
function deleteDB(name: string): Promise<void> {
return new Promise((resolve, reject) => {
const req = (globalThis as unknown as { indexedDB: IDBFactory }).indexedDB.deleteDatabase(name);
req.onsuccess = () => {
resolve();
};
req.onerror = () => {
reject(req.error);
};
req.onblocked = () => {
resolve();
};
});
}
async function cleanAll(): Promise<void> {
await deleteDB(DB.USER);
}
describe("quarantine store — persistent round-trip", () => {
beforeEach(cleanAll);
afterEach(cleanAll);
it("add / isQuarantined / list / clear round-trips", async () => {
const db = await openUserDB();
expect(await isQuarantined(db, "lumen-2026", 5)).toBe(false);
await addQuarantined(db, {
edition: "lumen-2026",
packageVersion: 5,
code: "signature_mismatch",
reason: "bad sig",
at: 1000,
});
expect(await isQuarantined(db, "lumen-2026", 5)).toBe(true);
expect(await isQuarantined(db, "lumen-2026", 6)).toBe(false);
expect(await isQuarantined(db, "other-edition", 5)).toBe(false);
const list = await listQuarantined(db, "lumen-2026");
expect(list).toHaveLength(1);
expect(list[0]!.code).toBe("signature_mismatch");
await clearQuarantined(db, "lumen-2026", 5);
expect(await isQuarantined(db, "lumen-2026", 5)).toBe(false);
db.close();
});
it("survives DB reopen (persistence, not session state)", async () => {
const db = await openUserDB();
await addQuarantined(db, {
edition: "lumen-2026",
packageVersion: 7,
code: "hash_mismatch",
reason: "corrupt",
at: 2000,
});
db.close();
const db2 = await openUserDB();
expect(await isQuarantined(db2, "lumen-2026", 7)).toBe(true);
db2.close();
});
it("unidentified rejections are not keyed (both coordinates required)", async () => {
const db = await openUserDB();
await addQuarantined(db, {
edition: null,
packageVersion: null,
code: "malformed_manifest",
reason: "no identity",
at: 3000,
});
const all = await listQuarantined(db, "lumen-2026");
expect(all).toHaveLength(0);
db.close();
});
it("list is ascending by packageVersion; prune clears through N inclusive", async () => {
const db = await openUserDB();
for (const v of [3, 1, 2]) {
await addQuarantined(db, {
edition: "lumen-2026",
packageVersion: v,
code: "replayed_version",
reason: `v${String(v)}`,
at: 4000,
});
}
const list = await listQuarantined(db, "lumen-2026");
expect(list.map((q) => q.packageVersion)).toEqual([1, 2, 3]);
const pruned = await pruneQuarantinedUpTo(db, "lumen-2026", 2);
expect(pruned).toBe(2);
expect(await isQuarantined(db, "lumen-2026", 1)).toBe(false);
expect(await isQuarantined(db, "lumen-2026", 2)).toBe(false);
expect(await isQuarantined(db, "lumen-2026", 3)).toBe(true);
db.close();
});
it("v1 -> v2 migration is additive: favorites intact, quarantine store usable (B-6)", async () => {
// Simulate an install that only ever had user v1 (no quarantine store).
await openDB(DB.USER, 1, (db) => {
if (!db.objectStoreNames.contains("favorites")) db.createObjectStore("favorites");
if (!db.objectStoreNames.contains("prefs")) db.createObjectStore("prefs");
if (!db.objectStoreNames.contains("diag")) db.createObjectStore("diag");
}).then((db) => {
void idbPut(db, "favorites", { eventId: "evt-old", addedAt: 1 }, "evt-old");
db.close();
});
// Reopen via production path (v2): migration must add quarantine, keep data.
const db = await openUserDB();
expect(db.objectStoreNames.contains("quarantine")).toBe(true);
const favs = await listFavorites(db);
expect(favs.map((f) => f.eventId)).toEqual(["evt-old"]);
await addQuarantined(db, {
edition: "lumen-2026",
packageVersion: 9,
code: "budget_exceeded",
reason: "too big",
at: 5000,
});
expect(await isQuarantined(db, "lumen-2026", 9)).toBe(true);
db.close();
});
it("deleting slot DBs never touches quarantine (B-6 isolation)", async () => {
const db = await openUserDB();
await addQuarantined(db, {
edition: "lumen-2026",
packageVersion: 4,
code: "corrupted",
reason: "x",
at: 6000,
});
db.close();
await deleteDB(DB.SLOT_A);
await deleteDB(DB.SLOT_B);
const db2 = await openUserDB();
expect(await isQuarantined(db2, "lumen-2026", 4)).toBe(true);
db2.close();
});
});
describe("quarantine no-loop — pullCandidate skips without fetching", () => {
beforeEach(cleanAll);
afterEach(cleanAll);
function fakeTransport(fetches: string[]): Transport {
return {
isAvailable: () => true,
fetchPointer: async () => {
fetches.push("latest.json");
return {
edition: "lumen-2026",
packageVersion: 1,
manifestUrl: "/editions/lumen-2026/packages/1/manifest.json",
generatedAt: "2026-08-30T12:00:00.000Z",
};
},
fetchBytes: async (url) => {
fetches.push(url);
throw new Error("must not be called for quarantined version");
},
};
}
it("quarantined pointer skips BEFORE manifest or file fetch", async () => {
const fetches: string[] = [];
const user = await openUserDB();
await addQuarantined(user, {
edition: "lumen-2026",
packageVersion: 1,
code: "signature_mismatch",
reason: "known bad",
at: 1,
});
const outcome = await pullCandidate(
fakeTransport(fetches),
"lumen-2026",
{
trustedKeys: new Map(),
appVersion: "1.0.0",
supportedSchemaRange: [1],
},
{
isQuarantined: async (v) => isQuarantined(user, "lumen-2026", v),
},
);
expect(outcome).toMatchObject({ skipped: "quarantined" });
expect(fetches).toEqual(["latest.json"]); // no manifest, no signature, no files
user.close();
});
it("non-quarantined pointer proceeds (one latest.json + manifest fetched)", async () => {
const keys = generateTestKeyPair();
const built = buildPackage(makeValidInput(), { signWith: keys });
if (!built.ok || !built.pkg.signature) throw new Error("fixture build failed");
const manifestBytes = new TextEncoder().encode(canonicalJson(built.pkg.manifest));
const sigBytes = new TextEncoder().encode(JSON.stringify(built.pkg.signature));
const pkgFiles = new Map<string, Uint8Array>();
for (const [name, file] of built.pkg.files) pkgFiles.set(name, file.canonicalBytes);
for (const asset of built.pkg.assets) pkgFiles.set(asset.file, asset.bytesContent);
const fetches: string[] = [];
const base = fakeTransport(fetches);
const transport: Transport = {
...base,
fetchBytes: async (url) => {
fetches.push(url);
if (url.endsWith("manifest.json")) return manifestBytes;
if (url.endsWith("signature.json")) return sigBytes;
const name = url.split("/").pop() ?? "";
const sub = url.includes("/assets/") ? `assets/${name}` : name;
const content = pkgFiles.get(sub);
if (content) return content;
throw new Error(`unexpected fetch ${url}`);
},
};
const user = await openUserDB();
const outcome = await pullCandidate(
transport,
"lumen-2026",
{
trustedKeys: new Map([[keys.fingerprint, publicKeyFromDerBase64(keys.publicKeyDerBase64)]]),
appVersion: "1.0.0",
supportedSchemaRange: [1],
},
{
isQuarantined: async (v) => isQuarantined(user, "lumen-2026", v),
},
);
if (!outcome || "skipped" in outcome) throw new Error("expected candidate");
expect(outcome.result.ok).toBe(true);
expect(fetches[0]).toBe("latest.json");
expect(fetches.some((u) => u.endsWith("manifest.json"))).toBe(true);
user.close();
});
it("advancing latest.json past the quarantined version clears the skip path", async () => {
// latest now points at v2 (not quarantined): pull must not skip.
const fetches: string[] = [];
const transport: Transport = {
isAvailable: () => true,
fetchPointer: async () => {
fetches.push("latest.json");
return {
edition: "lumen-2026",
packageVersion: 2,
manifestUrl: "/editions/lumen-2026/packages/2/manifest.json",
generatedAt: "2026-08-31T12:00:00.000Z",
};
},
fetchBytes: async (url) => {
fetches.push(url);
throw new Error("not needed for this assertion");
},
};
const user = await openUserDB();
await addQuarantined(user, {
edition: "lumen-2026",
packageVersion: 1,
code: "hash_mismatch",
reason: "old bad",
at: 1,
});
// v2 is not quarantined, so the pull must NOT skip: it advances to fetch
// the manifest, and our transport throws there — a rejection proves it
// passed the skip gate instead of returning {skipped}.
await expect(
pullCandidate(
transport,
"lumen-2026",
{
trustedKeys: new Map(),
appVersion: "1.0.0",
supportedSchemaRange: [1],
},
{
isQuarantined: async (v) => isQuarantined(user, "lumen-2026", v),
},
),
).rejects.toThrow(/not needed/);
expect(fetches.length).toBeGreaterThan(1);
expect(fetches.some((u) => u.endsWith("manifest.json"))).toBe(true);
user.close();
});
});
describe("verifier -> persistent quarantine integration (sole decider keeps active)", () => {
beforeEach(cleanAll);
afterEach(cleanAll);
it("a bad-signature package lands in the IDB quarantine store via the sink", async () => {
const keys = generateTestKeyPair();
const built = buildPackage(makeValidInput(), { signWith: keys });
if (!built.ok || !built.pkg.signature) throw new Error("fixture build failed");
const manifestBytes = new TextEncoder().encode(canonicalJson(built.pkg.manifest));
const files = new Map<string, Uint8Array>();
for (const [name, file] of built.pkg.files) files.set(name, file.canonicalBytes);
const user = await openUserDB();
const result = await verifyPackage(
{
manifestBytes,
signature: { ...built.pkg.signature, signature: "AAAA" }, // corrupted sig
files: { getFile: async (p) => files.get(p) },
hint: { edition: "lumen-2026", packageVersion: 1 },
},
{
trustedKeys: new Map([[keys.fingerprint, publicKeyFromDerBase64(keys.publicKeyDerBase64)]]),
appVersion: "1.0.0",
supportedSchemaRange: [1],
quarantine: quarantineSink(user),
},
);
expect(result).toMatchObject({ ok: false, code: "signature_mismatch" });
// rejection persisted in the real store keyed by edition/version
expect(await isQuarantined(user, "lumen-2026", 1)).toBe(true);
// and quarantineKey agrees with the stored coordinate
expect(quarantineKey("lumen-2026", 1)).toBe("lumen-2026/1");
// no activation happened: verifier result is the only decider (B-4)
user.close();
});
});