Files
veridian/tests/topic-sync.test.ts

132 lines
6.1 KiB
TypeScript

import { describe, expect, test } from 'bun:test';
import { createHash } from 'node:crypto';
import { topicEvents } from '../server/utils/events';
import { createTopicSnapshotStream } from '../server/utils/topic-snapshot-stream';
import { appendTextDelta, serializeTopicSnapshot } from '../utils/topic-sync';
const snapshot = () => ({
id: 'topic',
name: 'Example',
messages: [{
id: 'message',
activeChildId: null as string | null,
updatedAt: new Date('2026-09-27T00:00:00Z'),
generation: { status: 'pending', tokens: null as unknown },
parts: [{ id: 'part', type: 'text', content: 'hello', finished: false }],
}],
});
const fingerprint = (value: unknown) => createHash('sha256').update(serializeTopicSnapshot(value)).digest('hex');
const readEvent = async (reader: ReadableStreamDefaultReader<string>) => {
const { value } = await reader.read();
return JSON.parse(value!.slice(6));
};
describe('topic snapshot synchronization', () => {
test('matches database Dates and hydrated JSON regardless of object key order', () => {
const topic = snapshot();
const hydrated = JSON.parse(JSON.stringify(topic));
expect(fingerprint({ messages: hydrated.messages, name: hydrated.name, id: hydrated.id })).toBe(fingerprint(topic));
});
test('detects older-message, part, generation, variant, and topic changes without relying on count', () => {
const original = fingerprint(snapshot());
for (const change of [
(topic: ReturnType<typeof snapshot>) => topic.name = 'Renamed',
(topic: ReturnType<typeof snapshot>) => topic.messages[0]!.activeChildId = 'variant',
(topic: ReturnType<typeof snapshot>) => topic.messages[0]!.parts[0]!.content = 'edited',
(topic: ReturnType<typeof snapshot>) => topic.messages[0]!.parts[0]!.finished = true,
(topic: ReturnType<typeof snapshot>) => topic.messages[0]!.generation.tokens = { output: 123 },
]) {
const topic = snapshot();
change(topic);
expect(fingerprint(topic)).not.toBe(original);
}
});
test('matching state receives a small ready event, then live changes', async () => {
const topic = snapshot();
const subscription = createTopicSnapshotStream('match', fingerprint(topic));
const result = await subscription.open(async () => topic);
expect(result.ok).toBe(true);
if (!result.ok) return;
const reader = result.data.getReader();
try {
expect(await readEvent(reader)).toEqual({ type: 'synced', payload: { id: 'match' } });
await topicEvents.emit('match', { type: 'topic_updated', payload: { name: 'New' } });
expect((await readEvent(reader)).payload).toEqual({ name: 'New' });
} finally {
await reader.cancel();
}
});
test('stale or missing fingerprints receive a full replacement snapshot', async () => {
for (const offered of [undefined, fingerprint(snapshot())]) {
const topic = snapshot();
topic.messages[0]!.parts[0]!.content = 'changed';
const subscription = createTopicSnapshotStream('stale', offered);
const result = await subscription.open(async () => topic);
expect(result.ok).toBe(true);
if (!result.ok) continue;
const reader = result.data.getReader();
try {
const event = await readEvent(reader);
expect(event.type).toBe('initial_state');
expect(event.payload.messages[0].parts[0].content).toBe('changed');
} finally {
await reader.cancel();
}
}
});
test('captures events during the DB query and includes text not yet persisted', async () => {
const id = 'query-race';
const initial = snapshot();
await topicEvents.emit(id, {
type: 'text-start', payload: { messageId: 'message', part: initial.messages[0]!.parts[0] },
});
// This delta predates subscription and has not reached the database yet.
await topicEvents.emit(id, {
type: 'text-delta', payload: { messageId: 'message', partId: 'part', content: ' world', offset: 5 },
});
const pending = Promise.withResolvers<ReturnType<typeof snapshot>>();
const subscription = createTopicSnapshotStream(id, fingerprint(initial));
const opening = subscription.open(() => pending.promise);
await topicEvents.emit(id, {
type: 'text-delta', payload: { messageId: 'message', partId: 'part', content: '!', offset: 11 },
});
pending.resolve(snapshot());
const result = await opening;
expect(result.ok).toBe(true);
if (!result.ok) return;
const reader = result.data.getReader();
try {
const state = await readEvent(reader);
expect(state.type).toBe('initial_state');
const text = state.payload.messages[0].parts[0].content;
expect(text).toBe('hello world!');
const replay = await readEvent(reader);
expect(replay.replay).toBe(true);
expect(appendTextDelta(text, replay.payload.content, replay.payload.offset)).toBe(text);
} finally {
await reader.cancel();
topicEvents.clearLiveText(id, 'message');
}
});
test('cancellation during snapshot loading does not establish a stream', async () => {
const pending = Promise.withResolvers<ReturnType<typeof snapshot>>();
const subscription = createTopicSnapshotStream('cancelled', undefined);
const opening = subscription.open(() => pending.promise);
subscription.close();
pending.resolve(snapshot());
expect((await opening).ok).toBe(false);
});
test('text offsets handle partial overlaps, duplicates, and gaps', () => {
expect(appendTextDelta('hello w', ' world', 5)).toBe('hello world');
expect(appendTextDelta('hello world', ' world', 5)).toBe('hello world');
expect(appendTextDelta('hello', '!', 5)).toBe('hello!');
expect(appendTextDelta('hello', '!', 10)).toBe('hello');
});
});