feat(messaging-ui): KernelClientAdapter — direct-IIOS transport for the SDK
Implements MessagingAdapter over @insignia/iios-kernel-client (browser → IIOS,
token-in): listConversations/openThread/history/send/subscribe/typing/markRead
/react, mapping kernel Message/ThreadSummary/annotations onto the SDK's neutral
types. currentActorId comes from the host's session (senderId space), never
inferred from history — the exact bug the conformance suite kills.
- lives in adapters/ (transport boundary intact — core stays transport-free)
- ships from the ./adapters/kernel-client subpath (kernel-client is an optional
peer dep; excluded from the main bundle)
- connectKernelAdapter({serviceUrl, token, currentUserId}) convenience
- passes the shared adapter conformance suite (12 tests) over the real
MessageSocket facade with only the socket.io layer faked
- media/attachments deferred (kernel-client has no presign yet)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
// 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 { 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<string, KernelMessage[]>([
|
||||
[
|
||||
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<string, Set<(...a: unknown[]) => 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 */
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
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'] });
|
||||
Reference in New Issue
Block a user