import { describe, it, expect } from 'vitest'; import type { MessagingAdapter } from './adapter'; import type { MessageEvent } from './types'; export interface ConformanceOptions { /** Build a fresh, isolated adapter per test. */ makeAdapter: () => Promise | MessagingAdapter; /** A thread id that exists in the fixture. */ seededThreadId: string; /** Participant ids openThread can legally be called with. */ openWith: string[]; } /** * The definition of a correct MessagingAdapter. Every adapter runs this. * * Imports vitest, so it ships from the './conformance' subpath ONLY and is never * reachable from the main barrel. Consumers supply their own vitest. * * Usage from another package: * import { runAdapterConformance } from '@insignia/iios-messaging-ui/conformance'; * runAdapterConformance({ makeAdapter: () => new MockAdapter(), seededThreadId: 'th_1', openWith: ['pp_a'] }); */ export function runAdapterConformance(opts: ConformanceOptions): void { const make = async () => await opts.makeAdapter(); describe('MessagingAdapter conformance', () => { it('lists conversations', async () => { const a = await make(); const list = await a.listConversations(); expect(Array.isArray(list)).toBe(true); }); it('returns history for a seeded thread', async () => { const a = await make(); const msgs = await a.history(opts.seededThreadId); expect(Array.isArray(msgs)).toBe(true); }); it('send resolves with a message carrying the sent text and a stable id', async () => { const a = await make(); const m = await a.send(opts.seededThreadId, 'conformance hello'); expect(m.text).toBe('conformance hello'); expect(typeof m.id).toBe('string'); expect(m.id.length).toBeGreaterThan(0); }); it('a sent message is attributed to the current actor', async () => { const a = await make(); const m = await a.send(opts.seededThreadId, 'whose is this'); expect(m.actorId).toBe(a.currentActorId()); }); it('currentActorId is known BEFORE any message is sent', async () => { // The bug this SDK exists to kill: identity must come from auth, never be // inferred from history. A fresh adapter already knows who you are. const a = await make(); expect(a.currentActorId()).not.toBeNull(); }); it('a sent message appears in history', async () => { const a = await make(); await a.send(opts.seededThreadId, 'persist me'); const msgs = await a.history(opts.seededThreadId); expect(msgs.some((m) => m.text === 'persist me')).toBe(true); }); it('subscribe delivers a message event on send', async () => { const a = await make(); const seen: MessageEvent[] = []; const off = a.subscribe(opts.seededThreadId, (e) => seen.push(e)); await a.send(opts.seededThreadId, 'live one'); off(); const msgs = seen.filter((e) => e.kind === 'message'); expect(msgs.length).toBeGreaterThan(0); }); it('unsubscribe stops delivery', async () => { const a = await make(); const seen: MessageEvent[] = []; const off = a.subscribe(opts.seededThreadId, (e) => seen.push(e)); off(); await a.send(opts.seededThreadId, 'should not be heard'); expect(seen).toHaveLength(0); }); it('unsubscribe is idempotent', async () => { const a = await make(); const off = a.subscribe(opts.seededThreadId, () => {}); off(); expect(() => off()).not.toThrow(); }); it('openThread returns a thread id', async () => { const a = await make(); const { threadId } = await a.openThread({ participantIds: opts.openWith }); expect(typeof threadId).toBe('string'); expect(threadId.length).toBeGreaterThan(0); }); it('markRead resolves', async () => { const a = await make(); const m = await a.send(opts.seededThreadId, 'read me'); await expect(a.markRead(opts.seededThreadId, m.id)).resolves.toBeUndefined(); }); it('sendTyping does not throw', async () => { const a = await make(); expect(() => a.sendTyping(opts.seededThreadId)).not.toThrow(); }); }); }