/*
RFC-0217: tests for the claim ledger — event-id identity (incl. value),
as-of query, lineage assembly, and the two projections.
- Not a contract source of truth — the module under test owns the authoritative behavior; fixtures are minimal scaffolding only.
- RFC-0217: initial ledger tests (added in self-review pass).
*/
import { test, expect } from "vitest";
import {
stableEventId,
parseNdjson,
serializeEvent,
buildLineage,
queryAsOf,
projectGraph,
projectTemporalSeo,
type ClaimEvent,
} from "../ledger.ts";
function evt(partial: Partial): ClaimEvent {
const subject = partial.subject ?? "business/de/location#residents";
const asOf = partial.asOf ?? "2026-01-01";
const event = partial.event ?? "genesis";
const actor = partial.actor ?? "agent:test";
return {
id: stableEventId(subject, asOf, event, actor, partial.value),
ts: partial.ts ?? `${asOf}T00:00:00Z`,
subject,
value: partial.value,
provenance: partial.provenance ?? "external",
asOf,
actor,
event,
...partial,
};
}
test("stableEventId: identical payload is idempotent", () => {
const a = stableEventId("s#f", "2026-01-01", "genesis", "agent:a", "39120");
const b = stableEventId("s#f", "2026-01-01", "genesis", "agent:a", "39120");
expect(a).toBe(b);
expect(a).toMatch(/^evt_[0-9a-f]{16}$/);
});
test("stableEventId: a different value on the same day/actor/event is a DISTINCT event", () => {
const first = stableEventId("s#f", "2026-01-01", "verify-update", "agent:a", "39120");
const corrected = stableEventId("s#f", "2026-01-01", "verify-update", "agent:a", "39200");
expect(first).not.toBe(corrected);
});
test("ndjson round-trip preserves a compact event", () => {
const e = evt({ value: "39120" });
const [parsed] = parseNdjson(`${serializeEvent(e)}\n`);
expect(parsed).toEqual(e);
});
test("parseNdjson ignores blank lines", () => {
const e = evt({ value: "1" });
expect(parseNdjson(`\n${serializeEvent(e)}\n\n`).length).toBe(1);
});
test("queryAsOf: returns the most recent event whose asOf precedes the query date", () => {
const events = [
evt({ asOf: "2025-01-15", value: "38500", event: "genesis" }),
evt({ asOf: "2026-06-18", value: "39120", event: "verify-update" }),
];
expect(queryAsOf(events, "business/de/location#residents", "2025-06-01")?.value).toBe("38500");
expect(queryAsOf(events, "business/de/location#residents", "2026-12-01")?.value).toBe("39120");
expect(queryAsOf(events, "business/de/location#residents", "2024-01-01")).toBe(null);
});
test("buildLineage: history is newest → oldest with the latest as current", () => {
const events = [
evt({ asOf: "2025-01-15", ts: "2025-01-15T00:00:00Z", value: "38500" }),
evt({ asOf: "2026-06-18", ts: "2026-06-18T00:00:00Z", value: "39120" }),
];
const lin = buildLineage(events, "business/de/location#residents");
expect(lin).toBeTruthy();
expect(lin!.current.value).toBe("39120");
expect(lin!.history[0].value).toBe("39120");
expect(lin!.history[1].value).toBe("38500");
});
test("buildLineage: null for an unknown subject", () => {
expect(buildLineage([evt({ value: "1" })], "business/de/nope#x")).toBe(null);
});
test("projectGraph: one node per subject, newest event wins, history counted", () => {
const events = [
evt({ asOf: "2025-01-15", ts: "2025-01-15T00:00:00Z", value: "38500" }),
evt({ asOf: "2026-06-18", ts: "2026-06-18T00:00:00Z", value: "39120" }),
evt({ subject: "business/de/company#foundingYear", value: "2026", ts: "2026-02-01T00:00:00Z" }),
];
const graph = projectGraph(events, "warpgogol-com");
expect(graph.nodes.length).toBe(2);
const residents = graph.nodes.find((n) => n.subject === "business/de/location#residents")!;
expect(residents.currentValue).toBe("39120");
expect(residents.historyCount).toBe(2);
});
test("projectTemporalSeo: datePublished is earliest genesis, dateModified is latest update", () => {
const subject = "business/de/location#residents";
const events = [
evt({ subject, asOf: "2025-01-15", event: "genesis", value: "38500" }),
evt({ subject, asOf: "2026-06-18", event: "verify-update", value: "39120" }),
];
const map = new Map([["de/location", [subject]]]);
const [seo] = projectTemporalSeo(events, map);
expect(seo.datePublished).toBe("2025-01-15");
expect(seo.dateModified).toBe("2026-06-18");
expect(seo.temporalCoverage).toBe("2025-01-15/2026-06-18");
});
test("projectTemporalSeo: no temporalCoverage when published equals modified", () => {
const subject = "business/de/company#foundingYear";
const events = [evt({ subject, asOf: "2026-02-01", event: "genesis", value: "2026" })];
const map = new Map([["de/company", [subject]]]);
const [seo] = projectTemporalSeo(events, map);
expect(seo.datePublished).toBe("2026-02-01");
expect(seo.dateModified).toBe("2026-02-01");
expect(seo.temporalCoverage).toBe(undefined);
});