Lumen/tests/unit/data-package.test.ts
Lumen Stage1 e83946c207
Some checks failed
ci / check (push) Has been cancelled
Checkpoint: current working state
2026-09-29 21:33:33 -05:00

377 lines
11 KiB
TypeScript

/**
* Stage 4 — data contracts tests.
* Covers: valid package, missing/malformed, versions, timestamps, ranges,
* stable IDs, optional sections, forward compat, incompatible, emergency, separation, metadata, readiness inputs.
* Trace: SPIKE-04, IMPLEMENTATION-CONTRACT.md §15, §20, §24, §40
*/
import { describe, it, expect } from "vitest";
import {
validateManifest,
validateSignature,
validateScheduleEvent,
validateMapPoi,
validatePackage,
isCompatible,
validateEdition,
validatePackageVersion,
validateEmergencyFloor,
} from "../../src/data/festival-package/validation.js";
import type { FestivalManifest } from "../../src/data/festival-package/types.js";
import { isStableId } from "../../src/data/festival-package/validation.js";
function validManifest(): FestivalManifest {
return {
format: "lumen.package/1",
edition: "lumen-2026",
packageVersion: 7,
schemaVersion: 1,
generatedAt: "2026-08-28T14:02:11Z",
festival: {
name: "Lumen Festival 2026",
timezone: "America/Chicago",
startUtc: Date.UTC(2026, 8, 10),
endUtc: Date.UTC(2026, 8, 13),
},
appCompatibility: { minAppVersion: "1.0.0", maxAppVersion: null },
sections: {
emergency: { file: "emergency.json", sha256: "a".repeat(64), bytes: 41233, required: true },
schedule: { file: "schedule.json", sha256: "b".repeat(64), bytes: 412201, required: true },
map: { file: "map.json", sha256: "c".repeat(64), bytes: 38122, required: true },
info: { file: "info.json", sha256: "d".repeat(64), bytes: 96710, required: true },
assets: { file: "assets.json", sha256: "e".repeat(64), bytes: 7111, required: true },
},
counts: { events: 312, pois: 87, assets: 14 },
limits: { totalBytes: 31240012 },
};
}
function validSignature() {
return {
algorithm: "ed25519" as const,
over: "sha256(manifest.json exact bytes)",
manifestSha256: "f".repeat(64),
publicKeyFingerprint: "sha256:abc",
signature: "base64==",
};
}
describe("Festival Data Package — valid package", () => {
it("valid manifest passes", () => {
expect(validateManifest(validManifest()).ok).toBe(true);
});
it("valid signature passes", () => {
expect(validateSignature(validSignature()).ok).toBe(true);
});
it("valid package (manifest+signature+emergency) passes", () => {
expect(
validatePackage({
manifest: validManifest(),
signature: validSignature(),
emergency: {
section: "emergency",
emergencySchemaVersion: 1,
contentVersion: 3,
updatedAt: "2026-08-27T09:00:00Z",
services: {
emergencyNumber: "911",
security: { phone: "+1-555" },
firstAid: { location: "A" },
},
locations: { musterPoints: [], exits: [], aeds: [] },
address: { lines: ["x"], coordinates: { lat: 0, lon: 0 } },
procedures: [],
},
}).ok,
).toBe(true);
});
});
describe("missing required data", () => {
it("missing sections fails", () => {
const m = { ...validManifest(), sections: undefined as unknown as Record<string, unknown> };
expect(validateManifest(m).ok).toBe(false);
});
it("missing festival timezone fails", () => {
const m = validManifest();
const bad = { ...m, festival: { ...m.festival, timezone: "" } };
expect(validateManifest(bad as unknown as FestivalManifest).ok).toBe(false);
});
it("missing signature field fails", () => {
const sig = { ...validSignature(), signature: "" };
expect(validateSignature(sig).ok).toBe(false);
});
});
describe("malformed IDs", () => {
it("event id with spaces fails", () => {
expect(
validateScheduleEvent({
id: "bad id",
title: "x",
stageId: "s",
artistIds: [],
startUtc: 1000,
endUtc: 2000,
dayKey: "2026-09-11",
tags: [],
status: "scheduled",
}).ok,
).toBe(false);
});
it("valid event id passes", () => {
expect(
validateScheduleEvent({
id: "evt-0113",
title: "x",
stageId: "s",
artistIds: [],
startUtc: 1000,
endUtc: 2000,
dayKey: "2026-09-11",
tags: [],
status: "scheduled",
}).ok,
).toBe(true);
});
});
describe("malformed versions", () => {
it("packageVersion 0 fails, 1 passes", () => {
expect(validatePackageVersion(0).ok).toBe(false);
expect(validatePackageVersion(1).ok).toBe(true);
});
it("edition empty fails", () => {
expect(validateEdition("").ok).toBe(false);
});
it("schemaVersion float fails", () => {
expect(
validatePackage({
manifest: { ...validManifest(), schemaVersion: 1.5 },
signature: validSignature(),
emergency: { section: "emergency", emergencySchemaVersion: 1 },
}).ok,
).toBe(false);
});
});
describe("invalid timestamps / ranges", () => {
it("end before start fails", () => {
expect(
validateScheduleEvent({
id: "evt-1",
title: "x",
stageId: "s",
artistIds: [],
startUtc: 2000,
endUtc: 1000,
dayKey: "2026-09-11",
tags: [],
status: "scheduled",
}).ok,
).toBe(false);
});
it("zero-length fails", () => {
expect(
validateScheduleEvent({
id: "evt-1",
title: "x",
stageId: "s",
artistIds: [],
startUtc: 1000,
endUtc: 1000,
dayKey: "2026-09-11",
tags: [],
status: "scheduled",
}).ok,
).toBe(false);
});
it("festival window end before start fails", () => {
const bad = {
...validManifest(),
festival: { ...validManifest().festival, startUtc: 2000, endUtc: 1000 },
};
expect(validateManifest(bad as unknown as FestivalManifest).ok).toBe(false);
});
it("event longer than 24h fails", () => {
expect(
validateScheduleEvent({
id: "evt-1",
title: "x",
stageId: "s",
artistIds: [],
startUtc: 0,
endUtc: 25 * 3600 * 1000,
dayKey: "2026-09-11",
tags: [],
status: "scheduled",
}).ok,
).toBe(false);
});
it("dayKey malformed fails", () => {
expect(
validateScheduleEvent({
id: "evt-1",
title: "x",
stageId: "s",
artistIds: [],
startUtc: 1000,
endUtc: 2000,
dayKey: "11-09-2026",
tags: [],
status: "scheduled",
}).ok,
).toBe(false);
});
});
describe("map POI validation", () => {
it("x/y 0..1 passes, out of range fails", () => {
expect(validateMapPoi({ id: "p", name: "n", category: "aed", x: 0.5, y: 0.5 }).ok).toBe(true);
expect(validateMapPoi({ id: "p", name: "n", category: "aed", x: 1.5, y: 0.5 }).ok).toBe(false);
});
it("lat/lng null allowed (GPS optional per I-7)", () => {
expect(
validateMapPoi({ id: "p", name: "n", category: "stage", x: 0, y: 0, lat: null, lng: null })
.ok,
).toBe(true);
});
});
describe("stable ID relationships (favorites survive updates)", () => {
it("stable id check", () => {
const prev = new Set(["evt-0113", "evt-0114"]);
expect(isStableId("evt-0113", prev)).toBe(true);
expect(isStableId("evt-9999", prev)).toBe(false);
});
});
describe("optional sections + forward compat", () => {
it("optional section may be missing without failing manifest (required flag)", () => {
// In Stage 4 we require all 5 for now; but forward compat means unknown sections allowed
const m = validManifest() as unknown as Record<string, unknown>;
// Add unknown optional section — should not break manifest validation (allowed forward compat)
(m.sections as Record<string, unknown>).announcements = {
file: "announcements.json",
sha256: "a".repeat(64),
bytes: 100,
required: false,
};
expect(validateManifest(m).ok).toBe(true);
});
it("unknown fields on event are allowed forward-compat", () => {
expect(
validateScheduleEvent({
id: "evt-1",
title: "x",
stageId: "s",
artistIds: [],
startUtc: 1000,
endUtc: 2000,
dayKey: "2026-09-11",
tags: [],
status: "scheduled",
// unknown future field
futureField: "ok",
}).ok,
).toBe(true);
});
});
describe("incompatible package versions", () => {
it("schemaVersion outside shell range is incompatible", () => {
const m = validManifest();
expect(isCompatible({ ...m, schemaVersion: 99 }, [1, 2], "1.0.0")).toBe(false);
});
it("app version older than min is incompatible", () => {
const m = validManifest();
expect(isCompatible(m, [1], "0.9.0")).toBe(false);
});
it("compatible within range passes", () => {
const m = validManifest();
expect(isCompatible(m, [1], "1.0.0")).toBe(true);
});
});
describe("emergency data representation (SPIKE-06)", () => {
it("floor shape passes", () => {
expect(
validateEmergencyFloor({
floor: true,
floorVersion: "1.0.0+2026.08.20",
emergencySchemaVersion: 1,
generatedAt: "2026-08-20T12:00:00Z",
sourceContentVersion: 5,
services: {},
address: { lines: ["x"], coordinates: { lat: 0, lon: 0 } },
musterPoints: [],
exits: [],
aedSummary: "x",
procedures: [],
}).ok,
).toBe(true);
});
it("floor missing version fails", () => {
expect(validateEmergencyFloor({ floor: true }).ok).toBe(false);
});
});
describe("separation of user data from festival data (IMP contract §CRITICAL)", () => {
it("festival package does not contain favorites", () => {
const pkg = validManifest() as unknown as Record<string, unknown>;
expect(pkg.favorites).toBeUndefined();
expect(pkg.favorites).not.toBeDefined();
});
it("user store is separate type", async () => {
const mod = await import("../../src/data/user/types.js");
expect(mod).toBeDefined();
});
});
describe("dataset metadata", () => {
it("counts and limits present", () => {
const m = validManifest();
expect(m.counts.events).toBe(312);
expect(m.limits.totalBytes).toBe(31240012);
});
it("totalBytes >50MB hard ceiling fails", () => {
const bad = { ...validManifest(), limits: { totalBytes: 60 * 1024 * 1024 } };
expect(validateManifest(bad as unknown as FestivalManifest).ok).toBe(false);
});
});
describe("readiness inputs (time-independent per SPIKE-05)", () => {
it("readiness is data-driven, not clock", async () => {
const { evaluateReadiness } = await import("../../src/domain/readiness/logic.js");
const allOk = {
shellValid: true,
datasetPresent: true,
authenticity: "verified" as const,
integrity: "ok" as const,
schemaCompatible: true,
requiredSectionsOk: true,
requiredAssetsOk: true,
emergencyFloorOk: true,
missing: [] as readonly string[],
hadVerifiedDataset: true,
manifestOk: true,
incompatible: false,
failure: null,
presentSections: ["emergency", "schedule", "info", "map", "assets"] as readonly string[],
stagedPending: null,
};
expect(evaluateReadiness(allOk).state).toBe("READY");
expect(
evaluateReadiness({
...allOk,
datasetPresent: false,
authenticity: "record-missing",
integrity: "files-missing",
schemaCompatible: false,
requiredSectionsOk: false,
requiredAssetsOk: false,
hadVerifiedDataset: false,
presentSections: [],
}).state,
).toBe("NOT_READY");
});
});