Files

244 lines
7.0 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
compareRealtimeSequences,
isCanonicalRealtimeSequence,
isRealtimeResumeCursor,
isStrictRealtimeTimestamp,
nextRealtimeSequence,
REALTIME_MAX_SEQUENCE,
} from "../../../src/contracts/realtime-events.ts";
import {
isValidatedRealtimeEventDto,
} from "../../../src/adapters/realtime/event-codec.ts";
import {
TEST_LIMITS,
createTestRealtimeCodec,
createTestRealtimeRegistry,
realtimeEventJson,
realtimeEventValue,
} from "./fixture.ts";
describe("REALTIME_EVENT_V1 codec", () => {
it("accepts an exact registered envelope and snapshots schema output", () => {
const codec = createTestRealtimeCodec();
const result = codec.decode(realtimeEventJson());
expect(result).toMatchObject({
ok: true,
value: {
envelope: {
protocol: "REALTIME_EVENT_V1",
sequence: "1",
recoveryMode: "CURSOR",
resumeCursor: "cursor-00000001",
payload: { value: "changed" },
},
},
});
if (!result.ok) return;
expect(isValidatedRealtimeEventDto(result.value)).toBe(true);
expect(Object.isFrozen(result.value)).toBe(true);
expect(Object.isFrozen(result.value.envelope)).toBe(true);
expect(Object.isFrozen(result.value.envelope.payload)).toBe(true);
expect(result.value.wireBytes).toBeGreaterThan(0);
expect(result.value.fingerprintBytes).toBeGreaterThan(0);
});
it("rejects extra keys, unknown registrations and schema failures", () => {
const codec = createTestRealtimeCodec();
expect(
codec.decode(
JSON.stringify({
...realtimeEventValue(),
arbitrary: "override",
}),
),
).toMatchObject({
ok: false,
error: { kind: "MALFORMED_EVENT", operation: "DECODE" },
});
expect(
codec.decode(
realtimeEventJson({ streamId: "UNKNOWN_STREAM" }),
),
).toMatchObject({
ok: false,
error: { kind: "MALFORMED_EVENT" },
});
expect(
codec.decode(
realtimeEventJson({ eventType: "UNKNOWN_EVENT" }),
),
).toMatchObject({
ok: false,
error: { kind: "MALFORMED_EVENT" },
});
expect(
codec.decode(realtimeEventJson({ payload: { value: 42 } })),
).toMatchObject({
ok: false,
error: { kind: "MALFORMED_EVENT" },
});
const duplicateEnvelopeMember =
`{"eventId":"shadowed",${JSON.stringify(
realtimeEventValue(),
).slice(1)}`;
expect(codec.decode(duplicateEnvelopeMember)).toMatchObject({
ok: false,
error: { kind: "MALFORMED_EVENT", operation: "DECODE" },
});
const duplicatePayloadMember = realtimeEventJson().replace(
'"payload":{"value":"changed"}',
'"payload":{"value":"first","\\u0076alue":"changed"}',
);
expect(codec.decode(duplicatePayloadMember)).toMatchObject({
ok: false,
error: { kind: "MALFORMED_EVENT", operation: "DECODE" },
});
});
it("fails closed on version, sequence, timestamp, scope and cursor syntax", () => {
const codec = createTestRealtimeCodec();
expect(
codec.decode(realtimeEventJson({ protocol: "REALTIME_EVENT_V2" })),
).toMatchObject({
ok: false,
error: { kind: "PROTOCOL_MISMATCH" },
});
for (const overrides of [
{ sequence: "01" },
{ sequence: "18446744073709551616" },
{ occurredAt: "2026-02-30T01:02:03Z" },
{ occurredAt: "2026-07-28 01:02:03Z" },
{ scopeBinding: "scope\r\ninjected" },
{ resumeCursor: "" },
{ resumeCursor: "cursor\ninjected" },
{ recoveryMode: "SNAPSHOT_ONLY", resumeCursor: null },
]) {
expect(codec.decode(realtimeEventJson(overrides))).toMatchObject({
ok: false,
error: { kind: "MALFORMED_EVENT" },
});
}
});
it("enforces global, per-stream and payload structure ceilings", () => {
const strictCodec = createTestRealtimeCodec(
createTestRealtimeRegistry({
limits: {
...TEST_LIMITS,
maxEventBytes: 512,
maxPayloadNodes: 1,
},
}),
);
expect(
strictCodec.decode(realtimeEventJson()),
).toMatchObject({
ok: false,
error: { kind: "MALFORMED_EVENT" },
});
const byteBoundCodec = createTestRealtimeCodec(
createTestRealtimeRegistry({
limits: {
...TEST_LIMITS,
maxEventBytes: 512,
},
}),
);
expect(
byteBoundCodec.decode(
realtimeEventJson({ payload: { value: "x".repeat(600) } }),
),
).toMatchObject({
ok: false,
error: { kind: "EVENT_TOO_LARGE" },
});
expect(
createTestRealtimeCodec().decode(
"x".repeat(64 * 1024 + 1),
),
).toMatchObject({
ok: false,
error: { kind: "EVENT_TOO_LARGE" },
});
});
it("supports the exact null-cursor SESSION_REBUILD discriminant", () => {
const registry = createTestRealtimeRegistry({
recovery: {
mode: "SESSION_REBUILD",
rebuildInputId: "referenceRealtimeRebuild",
},
delivery: "EPHEMERAL",
stateBearing: false,
});
const codec = createTestRealtimeCodec(registry);
expect(
codec.decode(
realtimeEventJson({
recoveryMode: "SESSION_REBUILD",
resumeCursor: null,
}),
),
).toMatchObject({
ok: true,
value: {
envelope: {
recoveryMode: "SESSION_REBUILD",
resumeCursor: null,
},
},
});
expect(
codec.decode(
realtimeEventJson({
recoveryMode: "SESSION_REBUILD",
resumeCursor: "synthetic",
}),
),
).toMatchObject({
ok: false,
error: { kind: "MALFORMED_EVENT" },
});
});
it("normalizes semantic identity independently of JSON key order", () => {
const codec = createTestRealtimeCodec();
const value = realtimeEventValue();
const reversed = Object.fromEntries(
Object.entries(value).reverse(),
);
const first = codec.decode(JSON.stringify(value));
const second = codec.decode(JSON.stringify(reversed));
expect(first.ok).toBe(true);
expect(second.ok).toBe(true);
if (!first.ok || !second.ok) return;
expect(first.value.semanticFingerprint).toBe(
second.value.semanticFingerprint,
);
});
it("uses bounded uint64 sequence and header-safe cursor helpers", () => {
expect(isCanonicalRealtimeSequence("0")).toBe(true);
expect(isCanonicalRealtimeSequence(REALTIME_MAX_SEQUENCE)).toBe(true);
expect(isCanonicalRealtimeSequence("00")).toBe(false);
expect(isCanonicalRealtimeSequence("18446744073709551616")).toBe(false);
expect(compareRealtimeSequences("9", "10")).toBe(-1);
expect(nextRealtimeSequence("9")).toBe("10");
expect(nextRealtimeSequence(REALTIME_MAX_SEQUENCE)).toBeNull();
expect(isRealtimeResumeCursor("cursor:/+=._~-")).toBe(true);
expect(isRealtimeResumeCursor("cursor\nunsafe")).toBe(false);
expect(isStrictRealtimeTimestamp("2024-02-29T23:59:59Z")).toBe(true);
expect(isStrictRealtimeTimestamp("2023-02-29T23:59:59Z")).toBe(false);
});
});