Two related bugs that surfaced together when a Discord response exceeded 2000 chars: 1. **Session id lost on mid-turn container exit.** `runPollLoop` was calling `setStoredSessionId` only after `processQuery` returned. If the container died between the SDK's `init` event (where session_id arrives) and the stream completing, the id was never persisted. The next wake called `getStoredSessionId()` → undefined and started a fresh Claude session, dropping all prior context. Fix: persist immediately in the `init` branch inside `processQuery`. The existing post-query store becomes a harmless no-op. 2. **Silent truncation past adapter limits.** `chat-sdk-bridge.deliver` handed full text straight to `adapter.postMessage`. Discord's adapter hard-truncates at 2000 chars; Telegram's at 4096. Responses longer than that were cut off without any signal to the user or host. Fix: add `maxTextLength` to `ChatSdkBridgeConfig` and a `splitForLimit` helper that breaks on paragraph → line → hard-char boundaries, then posts chunks sequentially. Files ride on the first chunk; the returned id is the first chunk's so edits and reactions still target the reply head. Channel adapter files (Discord, Telegram, …) live on the `channels` branch — a companion PR wires `maxTextLength: 1900` for Discord and `4000` for Telegram so the splitter actually engages in those installs. Without wiring, behavior is unchanged.
81 lines
3.0 KiB
TypeScript
81 lines
3.0 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import type { Adapter } from 'chat';
|
|
|
|
import { createChatSdkBridge, splitForLimit } from './chat-sdk-bridge.js';
|
|
|
|
function stubAdapter(partial: Partial<Adapter>): Adapter {
|
|
return { name: 'stub', ...partial } as unknown as Adapter;
|
|
}
|
|
|
|
describe('splitForLimit', () => {
|
|
it('returns a single chunk when text fits', () => {
|
|
expect(splitForLimit('short text', 100)).toEqual(['short text']);
|
|
});
|
|
|
|
it('splits on paragraph boundaries when available', () => {
|
|
const text = 'para one line one\npara one line two\n\npara two line one\npara two line two';
|
|
const chunks = splitForLimit(text, 40);
|
|
expect(chunks.length).toBeGreaterThan(1);
|
|
for (const c of chunks) expect(c.length).toBeLessThanOrEqual(40);
|
|
});
|
|
|
|
it('falls back to line boundaries when no paragraph fits', () => {
|
|
const text = 'alpha\nbravo\ncharlie\ndelta\necho\nfoxtrot';
|
|
const chunks = splitForLimit(text, 15);
|
|
expect(chunks.length).toBeGreaterThan(1);
|
|
for (const c of chunks) expect(c.length).toBeLessThanOrEqual(15);
|
|
});
|
|
|
|
it('hard-cuts when no whitespace is available', () => {
|
|
const text = 'a'.repeat(100);
|
|
const chunks = splitForLimit(text, 30);
|
|
expect(chunks.length).toBe(Math.ceil(100 / 30));
|
|
for (const c of chunks) expect(c.length).toBeLessThanOrEqual(30);
|
|
expect(chunks.join('')).toBe(text);
|
|
});
|
|
});
|
|
|
|
describe('createChatSdkBridge', () => {
|
|
// The bridge is now transport-only: forward inbound events, relay outbound
|
|
// ops. All per-wiring engage / accumulate / drop / subscribe decisions live
|
|
// in the router (src/router.ts routeInbound / evaluateEngage) and are
|
|
// exercised by host-core.test.ts end-to-end. These tests only cover the
|
|
// bridge's narrow, platform-adjacent surface.
|
|
|
|
it('omits openDM when the underlying Chat SDK adapter has none', () => {
|
|
const bridge = createChatSdkBridge({
|
|
adapter: stubAdapter({}),
|
|
supportsThreads: false,
|
|
});
|
|
expect(bridge.openDM).toBeUndefined();
|
|
});
|
|
|
|
it('exposes openDM when the underlying adapter has one, and delegates directly', async () => {
|
|
const openDMCalls: string[] = [];
|
|
const bridge = createChatSdkBridge({
|
|
adapter: stubAdapter({
|
|
openDM: async (userId: string) => {
|
|
openDMCalls.push(userId);
|
|
return `thread::${userId}`;
|
|
},
|
|
channelIdFromThreadId: (threadId: string) => `stub:${threadId.replace(/^thread::/, '')}`,
|
|
}),
|
|
supportsThreads: false,
|
|
});
|
|
expect(bridge.openDM).toBeDefined();
|
|
const platformId = await bridge.openDM!('user-42');
|
|
// Delegation: adapter.openDM → adapter.channelIdFromThreadId, no chat.openDM in between.
|
|
expect(openDMCalls).toEqual(['user-42']);
|
|
expect(platformId).toBe('stub:user-42');
|
|
});
|
|
|
|
it('exposes subscribe (lets the router initiate thread subscription on mention-sticky engage)', () => {
|
|
const bridge = createChatSdkBridge({
|
|
adapter: stubAdapter({}),
|
|
supportsThreads: true,
|
|
});
|
|
expect(typeof bridge.subscribe).toBe('function');
|
|
});
|
|
});
|