// Runs the shared adapter conformance suite against KernelClientAdapter — proving it satisfies // the same contract as the mock, over the REAL MessageSocket facade. Only the lowest socket.io // layer is faked (via kernel-client's own SocketLike seam), so the facade's wire mapping is // exercised for real. No live IIOS required. import { describe, it, expect } from 'vitest'; import { MessageSocket } from '@insignia/iios-kernel-client'; import type { Message as KernelMessage, SocketLike } from '@insignia/iios-kernel-client'; import { runAdapterConformance } from '../conformance'; import { KernelClientAdapter, type RestPort } from './kernel-client'; const ME = 'me'; const SEEDED = 'th_seed'; /** An in-memory stand-in for the /message socket.io namespace: stores messages, echoes 'message'. */ function makeFakeSocket(): SocketLike { const threads = new Map([ [ SEEDED, [ { id: 'seed_1', threadId: SEEDED, senderActorId: 'actor_other', senderId: 'pp_other', senderName: 'Other', content: 'seeded message', createdAt: new Date(0).toISOString(), }, ], ], ]); const handlers = new Map void>>(); let seq = 1; const fire = (event: string, payload: unknown): void => handlers.get(event)?.forEach((h) => h(payload)); return { on(event, handler) { if (!handlers.has(event)) handlers.set(event, new Set()); handlers.get(event)!.add(handler); return undefined; }, off(event, handler) { handlers.get(event)?.delete(handler); return undefined; }, emit() { return undefined; // typing/focus — no echo needed for conformance }, async emitWithAck(event, payload) { const p = (payload ?? {}) as { threadId: string; content?: string; parentInteractionId?: string; interactionId?: string; type?: string; value?: string; }; if (event === 'open_thread') { if (!threads.has(p.threadId)) threads.set(p.threadId, []); return { threadId: p.threadId, status: 'OPEN', history: [...threads.get(p.threadId)!] }; } if (event === 'send_message') { const msg: KernelMessage = { id: `m_${seq++}`, threadId: p.threadId, senderActorId: `actor_${ME}`, senderId: ME, senderName: ME, content: p.content ?? '', createdAt: new Date().toISOString(), ...(p.parentInteractionId ? { parentInteractionId: p.parentInteractionId } : {}), }; if (!threads.has(p.threadId)) threads.set(p.threadId, []); threads.get(p.threadId)!.push(msg); fire('message', msg); return msg; } if (event === 'read') return { ok: true }; if (event === 'annotate') { fire('annotation', { threadId: p.threadId, interactionId: p.interactionId, type: p.type, value: p.value, op: 'add', users: [ME], userId: ME, }); return {}; } return {}; }, connect() { return undefined; }, disconnect() { return undefined; }, connected: true, }; } function makeFakeRest(): RestPort { let seq = 1; return { async listThreads() { return []; }, async createThread() { return { threadId: `th_new_${seq++}` }; }, async addParticipant() { /* governed server-side; a fake always allows */ }, async discoverThreads() { return [ { threadId: 'th_pub', subject: 'general', metadata: { membership: 'channel', visibility: 'public', topic: 'Company-wide' }, participantCount: 3, joined: false, }, ]; }, async leaveThread(threadId) { return { threadId, participantCount: 0 }; }, }; } function makeAdapter(): KernelClientAdapter { const socket = new MessageSocket({ serviceUrl: 'http://iios.test', token: 'tok', autoConnect: false }, makeFakeSocket()); return new KernelClientAdapter({ currentUserId: ME, socket, rest: makeFakeRest() }); } runAdapterConformance({ makeAdapter, seededThreadId: SEEDED, openWith: ['pp_a'] }); describe('KernelClientAdapter channels', () => { it('browse maps discovered public channels; create/join/leave delegate to the transport', async () => { const adapter = makeAdapter(); const list = await adapter.browseChannels!(); expect(list[0]).toMatchObject({ threadId: 'th_pub', name: 'general', visibility: 'public', joined: false, memberCount: 3, topic: 'Company-wide' }); const { threadId } = await adapter.createChannel!({ name: 'design', topic: 'UI', visibility: 'public' }); expect(typeof threadId).toBe('string'); await expect(adapter.joinChannel!('th_pub')).resolves.toBeUndefined(); await expect(adapter.leaveChannel!('th_pub')).resolves.toBeUndefined(); }); });