Lands the lean-SDK direction (boocode_code_review_v2 §1 #9) behind a flag. Adds @anthropic-ai/claude-agent-sdk@0.3.159 (Commercial Terms, runtime dep). - PostgresSessionStore: clean-room impl of the SDK's real SessionStore type over a new claude_session_entries table. Typechecks against the SDK type; 8 DB-integration tests. - ClaudeSdkBackend (implements AgentBackend): one warm query() per (chat,claude) in streaming-input mode via a pushable async-iterable pump, sessionStore + resume continuity, pure mapSdkMessage->AgentEvent, session_id from init, usage/cost onto agent_sessions (backend CHECK gains 'claude_sdk'). - Routing env-gated by CLAUDE_SDK_BACKEND (default off) -> PTY path UNCHANGED. - Built against real SDK 0.3.159 types (install paid off: partial=stream_event needing includePartialMessages, MessageParam, result error arm). - Fix latent test-infra deadlock: serialize DB suites (fileParallelism:false). Coder 269 passing default / 290 with DB; tsc clean vs SDK types; builds clean. LIVE pump + resume + actual claude turn need a host smoke (CLAUDE_SDK_BACKEND=1 + claude binary + auth). zod peer-dep wants ^4 (workspace 3.25). Builds on v2.7.4. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
97 lines
3.4 KiB
TypeScript
97 lines
3.4 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { createPushable } from '../pushable-iterable.js';
|
|
|
|
/**
|
|
* The pushable async-iterable that feeds the Claude SDK's streaming-input query()
|
|
* one message per turn while staying open across turns. Tests cover the ordering
|
|
* contract (push/close/async-iterate) without any SDK shape.
|
|
*/
|
|
describe('createPushable — push/iterate ordering', () => {
|
|
it('yields buffered values in FIFO order then parks', async () => {
|
|
const p = createPushable<number>();
|
|
const it = p.iterable[Symbol.asyncIterator]();
|
|
|
|
p.push(1);
|
|
p.push(2);
|
|
expect(await it.next()).toEqual({ value: 1, done: false });
|
|
expect(await it.next()).toEqual({ value: 2, done: false });
|
|
|
|
// No more buffered → next() parks; resolve it by pushing.
|
|
const parked = it.next();
|
|
p.push(3);
|
|
expect(await parked).toEqual({ value: 3, done: false });
|
|
});
|
|
|
|
it('hands a value directly to a parked consumer (push after await)', async () => {
|
|
const p = createPushable<string>();
|
|
const it = p.iterable[Symbol.asyncIterator]();
|
|
const pending = it.next(); // parks immediately (empty buffer)
|
|
p.push('hello');
|
|
expect(await pending).toEqual({ value: 'hello', done: false });
|
|
});
|
|
|
|
it('close() resolves a parked consumer as done and reports done thereafter', async () => {
|
|
const p = createPushable<number>();
|
|
const it = p.iterable[Symbol.asyncIterator]();
|
|
const pending = it.next();
|
|
p.close();
|
|
expect(await pending).toEqual({ value: undefined, done: true });
|
|
expect(await it.next()).toEqual({ value: undefined, done: true });
|
|
expect(p.closed).toBe(true);
|
|
});
|
|
|
|
it('still drains values buffered BEFORE close', async () => {
|
|
const p = createPushable<number>();
|
|
const it = p.iterable[Symbol.asyncIterator]();
|
|
p.push(10);
|
|
p.push(20);
|
|
p.close();
|
|
expect(await it.next()).toEqual({ value: 10, done: false });
|
|
expect(await it.next()).toEqual({ value: 20, done: false });
|
|
expect(await it.next()).toEqual({ value: undefined, done: true });
|
|
});
|
|
|
|
it('drops values pushed after close', async () => {
|
|
const p = createPushable<number>();
|
|
const it = p.iterable[Symbol.asyncIterator]();
|
|
p.close();
|
|
p.push(99); // no-op
|
|
expect(await it.next()).toEqual({ value: undefined, done: true });
|
|
});
|
|
|
|
it('close() is idempotent', () => {
|
|
const p = createPushable<number>();
|
|
p.close();
|
|
expect(() => p.close()).not.toThrow();
|
|
expect(p.closed).toBe(true);
|
|
});
|
|
|
|
it('works with a for-await loop driven by interleaved pushes', async () => {
|
|
const p = createPushable<number>();
|
|
const seen: number[] = [];
|
|
const consumer = (async () => {
|
|
for await (const v of p.iterable) seen.push(v);
|
|
})();
|
|
|
|
p.push(1);
|
|
await Promise.resolve();
|
|
p.push(2);
|
|
await Promise.resolve();
|
|
p.close();
|
|
await consumer;
|
|
expect(seen).toEqual([1, 2]);
|
|
});
|
|
|
|
it('return() on the iterator closes the queue (for-await break)', async () => {
|
|
const p = createPushable<number>();
|
|
const it = p.iterable[Symbol.asyncIterator]();
|
|
p.push(1);
|
|
expect(await it.next()).toEqual({ value: 1, done: false });
|
|
// Simulate a `break` in for-await: the runtime calls return().
|
|
expect(await it.return!()).toEqual({ value: undefined, done: true });
|
|
expect(p.closed).toBe(true);
|
|
p.push(2); // dropped — queue is closed
|
|
expect(await it.next()).toEqual({ value: undefined, done: true });
|
|
});
|
|
});
|