chore: initialize from frontend template 4dc033c
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user