feat(sdk): P2.6 @insignia/iios-message-web (React hooks) + allowlist boundary

MessageProvider/useThread/useMessages over the kernel-client socket (live append,
typing, dedupe). Reconnect test (re-opens active thread). Rewrote import-boundary
as an allowlist so SDK<->service sibling imports are forbidden both ways. 28 tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-01 01:29:25 +05:30
parent 5a590c7fda
commit eff3c615d5
8 changed files with 262 additions and 19 deletions
@@ -0,0 +1,51 @@
import { describe, it, expect } from 'vitest';
import { MessageSocket } from './message-socket';
import type { SocketLike } from './types';
class FakeSocket implements SocketLike {
handlers = new Map<string, Array<(...a: unknown[]) => void>>();
acks: Array<{ event: string; payload: unknown }> = [];
ackReturn: unknown = { threadId: 't5', status: 'OPEN', history: [] };
on(event: string, handler: (...a: unknown[]) => void): unknown {
const list = this.handlers.get(event) ?? [];
list.push(handler);
this.handlers.set(event, list);
return this;
}
off(): unknown {
return this;
}
emit(): unknown {
return this;
}
async emitWithAck(event: string, ...args: unknown[]): Promise<unknown> {
this.acks.push({ event, payload: args[0] });
return this.ackReturn;
}
connect(): unknown {
return this;
}
disconnect(): unknown {
return this;
}
trigger(event: string, ...args: unknown[]): void {
(this.handlers.get(event) ?? []).forEach((h) => h(...args));
}
}
describe('MessageSocket reconnect resilience', () => {
it('re-opens the active thread when the socket reconnects (no lost subscription)', async () => {
const fake = new FakeSocket();
const ms = new MessageSocket({ serviceUrl: 'http://localhost:3200', token: 'tok' }, fake);
await ms.openThread('t5');
const before = fake.acks.filter((a) => a.event === 'open_thread').length;
fake.trigger('connect'); // simulate a reconnect
const opens = fake.acks.filter((a) => a.event === 'open_thread');
expect(opens.length).toBe(before + 1);
expect(opens.at(-1)?.payload).toMatchObject({ threadId: 't5' });
});
});