Checkpoint: current Lumen state
Some checks failed
ci / check (push) Has been cancelled

This commit is contained in:
Lumen Stage1 2026-09-23 18:58:21 -05:00
commit fcc18ddcc8
96 changed files with 8074 additions and 214 deletions

View file

@ -0,0 +1,177 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/** Stage 8 — A/B staging, activation, rollback, and user-state isolation. */
import { beforeEach, describe, expect, it } from "vitest";
// @ts-expect-error fake-indexeddb types via exports fallback
import FDBFactory from "fake-indexeddb/lib/FDBFactory";
import { buildPackage } from "../../pipeline/package.js";
import { generateTestKeyPair } from "../../pipeline/sign.js";
import { makeValidInput } from "../../pipeline/fixtures.js";
import { canonicalJson } from "../../pipeline/canonical-json.js";
import { publicKeyFromDerBase64 } from "../../src/sync/verifier/ed25519.js";
import { verifyPackage } from "../../src/sync/verifier/package.js";
import type { VerifiedPackage } from "../../src/sync/verifier/types.js";
import {
activateStagedPackage,
restorePreviousSlot,
stageVerifiedPackage,
} from "../../src/sync/activation.js";
import { openSystemDB, readSystemMeta } from "../../src/data/system-meta/store.js";
import { openSlotDB, readStagingProgress } from "../../src/data/slot/store.js";
import { openUserDB, addFavorite, listFavorites } from "../../src/data/user/store.js";
import { DB, SLOT_FILES } from "../../src/platform/idb/names.js";
import { withTx } from "../../src/platform/idb/wrapper.js";
import { activateSlot } from "../../src/data/system-meta/store.js";
const g = globalThis as unknown as Record<string, unknown>;
function deleteDb(name: string): Promise<void> {
return new Promise((resolve, reject) => {
const request = (g.indexedDB as IDBFactory).deleteDatabase(name);
request.onsuccess = () => {
resolve();
};
request.onerror = () => {
reject(request.error ?? new Error("delete database failed"));
};
request.onblocked = () => {
resolve();
};
});
}
async function fixture(version = 1): Promise<VerifiedPackage> {
const keyPair = generateTestKeyPair();
const built = buildPackage(makeValidInput({ packageVersion: version }), { signWith: keyPair });
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);
for (const asset of built.pkg.assets) files.set(asset.file, asset.bytesContent);
const result = await verifyPackage(
{
manifestBytes,
signature: built.pkg.signature,
files: { getFile: (name) => Promise.resolve(files.get(name)) },
emergencyFloor: built.pkg.emergencyFloor,
},
{
trustedKeys: new Map([
[keyPair.fingerprint, publicKeyFromDerBase64(keyPair.publicKeyDerBase64)],
]),
appVersion: "1.0.0",
supportedSchemaRange: [1],
},
);
if (!result.ok) throw new Error(result.reason);
return result;
}
describe("Stage 8 A/B activation", () => {
beforeEach(async () => {
g.indexedDB = new FDBFactory() as unknown;
await Promise.all(Object.values(DB).map((name) => deleteDb(name)));
});
it("stages into A and activates atomically, leaving user favorites intact", async () => {
const user = await openUserDB();
await addFavorite(user, { eventId: "event-1", addedAt: 1 });
user.close();
const pkg = await fixture();
const staged = await stageVerifiedPackage(pkg);
expect(staged.slot).toBe("A");
expect(staged.journal.complete).toBe(true);
expect((await activateStagedPackage(pkg, staged, "1.0.0")).ok).toBe(true);
const system = await openSystemDB();
expect((await readSystemMeta(system)).activeSlot).toBe("A");
expect((await readSystemMeta(system)).readbackPending).toBe(false);
system.close();
const userAfter = await openUserDB();
expect(await listFavorites(userAfter)).toEqual([{ eventId: "event-1", addedAt: 1 }]);
userAfter.close();
});
it("resumes matching progress and discards stale progress before restaging", async () => {
const pkg = await fixture();
const first = await stageVerifiedPackage(pkg);
const slot = await openSlotDB(first.slot);
const journal = await readStagingProgress(slot);
expect(journal?.complete).toBe(true);
slot.close();
const resumed = await stageVerifiedPackage(pkg);
expect(resumed.journal.stagedFiles).toHaveLength(5);
expect(resumed.journal.startedAt).toBe(first.journal.startedAt);
});
it("restores the retained complete slot without changing user data", async () => {
const first = await fixture(1);
const stagedFirst = await stageVerifiedPackage(first);
expect((await activateStagedPackage(first, stagedFirst, "1.0.0")).ok).toBe(true);
const second = await fixture(2);
const stagedSecond = await stageVerifiedPackage(second);
expect((await activateStagedPackage(second, stagedSecond, "1.0.0")).ok).toBe(true);
expect(await restorePreviousSlot()).toBe(true);
const system = await openSystemDB();
const meta = await readSystemMeta(system);
expect(meta.activeSlot).toBe("A");
expect(meta.activePackageVersion).toBe(1);
expect(meta.verification?.manifestSha256).toBe(first.manifestSha256);
expect(meta.readbackPending).toBe(false);
system.close();
});
it("does not activate an incomplete target slot", async () => {
const pkg = await fixture();
const staged = await stageVerifiedPackage(pkg);
const slot = await openSlotDB(staged.slot);
await withTx(slot, SLOT_FILES, "readwrite", (tx) => {
tx.objectStore(SLOT_FILES).delete("schedule");
});
slot.close();
const result = await activateStagedPackage(pkg, staged, "1.0.0");
expect(result).toMatchObject({ ok: false, rolledBack: false });
const system = await openSystemDB();
expect((await readSystemMeta(system)).activeSlot).toBeNull();
system.close();
});
it("cannot persist a structurally forged package without a verifier witness", async () => {
const verified = await fixture();
const forged = { ...verified } as VerifiedPackage;
await expect(stageVerifiedPackage(forged)).rejects.toThrow(/Stage 7/);
});
it("retries activation once and rolls back corruption after the flip", async () => {
const pkg = await fixture();
const staged = await stageVerifiedPackage(pkg);
let attempts = 0;
const retried = await activateStagedPackage(pkg, staged, "1.0.0", Date.now, {
activate: async (db, next) => {
attempts++;
if (attempts === 1) throw new Error("injected activation failure");
return activateSlot(db, next);
},
});
expect(retried.ok).toBe(true);
expect(attempts).toBe(2);
const second = await fixture(2);
const secondStaged = await stageVerifiedPackage(second);
const failed = await activateStagedPackage(second, secondStaged, "1.0.0", Date.now, {
afterFlip: async (slotId) => {
const db = await openSlotDB(slotId);
await withTx(db, SLOT_FILES, "readwrite", (tx) => {
tx.objectStore(SLOT_FILES).delete("schedule");
});
db.close();
},
});
expect(failed).toMatchObject({ ok: false, rolledBack: true });
const system = await openSystemDB();
expect(await readSystemMeta(system)).toMatchObject({
activeSlot: "A",
activePackageVersion: 1,
readbackPending: false,
});
system.close();
});
});