mirror of
https://github.com/obra/episodic-memory.git
synced 2026-09-14 13:43:14 +08:00
30916f8cad
Adds Cursor (live transcripts + opt-in state.vscdb backfill) and opencode (DB->JSONL export) as harnesses, following the existing Codex pattern. opencode summarization is fixed to route via transcript text instead of a doomed claude --resume, and hasConversationContent recognizes opencode_message lines. Harness union is 'claude'|'codex'|'cursor'|'opencode'. Also pins conversation timestamps to en-US/UTC in every renderer (#130) and restores valid YAML in the search agent frontmatter (#153). Co-authored-by: vicnaum <vicnaum@users.noreply.github.com> Co-authored-by: slandau3 <slandau3@users.noreply.github.com> Co-authored-by: arimu1 <arimu1@users.noreply.github.com> Claude-Session: https://claude.ai/code/session_0112vdwZphiWzfCYfaXMes4C
63 lines
2.4 KiB
TypeScript
63 lines
2.4 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
import type { ConversationExchange } from '../src/types.js';
|
|
|
|
// Mock the SDK so no test makes a live query() call.
|
|
vi.mock('@anthropic-ai/claude-agent-sdk', () => ({ query: vi.fn() }));
|
|
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
import { summarizeConversation } from '../src/summarizer.js';
|
|
|
|
function asyncIterableFor(messages: unknown[]): AsyncIterable<unknown> {
|
|
return {
|
|
[Symbol.asyncIterator]() {
|
|
let i = 0;
|
|
return {
|
|
next() {
|
|
return i < messages.length
|
|
? Promise.resolve({ value: messages[i++], done: false })
|
|
: Promise.resolve({ value: undefined, done: true });
|
|
},
|
|
};
|
|
},
|
|
};
|
|
}
|
|
|
|
function opencodeExchange(over: Partial<ConversationExchange> = {}): ConversationExchange {
|
|
return {
|
|
id: 'oc-1',
|
|
project: 'p',
|
|
timestamp: '2025-10-01T12:00:00Z',
|
|
userMessage: 'Add retry to the opencode exporter',
|
|
assistantMessage: 'Wrapped exportOpencodeSessions in a per-session try/catch.',
|
|
archivePath: '/tmp/archive/opencode-ses_abc.jsonl',
|
|
lineStart: 1,
|
|
lineEnd: 2,
|
|
harness: 'opencode',
|
|
sessionId: 'ses_abc',
|
|
cwd: '/tmp/nope',
|
|
...over,
|
|
};
|
|
}
|
|
|
|
describe('summarizeConversation — opencode routes to transcript text (never resume)', () => {
|
|
beforeEach(() => vi.mocked(query).mockReset());
|
|
|
|
it('summarizes an opencode conversation with resume disabled and transcript in the prompt', async () => {
|
|
vi.mocked(query).mockReturnValueOnce(asyncIterableFor([
|
|
{ type: 'result', is_error: false, result: '<summary>Added retry to opencode exporter.</summary>' },
|
|
]) as any);
|
|
|
|
// Even though a sessionId is passed, the harness gate must prevent a
|
|
// doomed `claude --resume` on an opencode id and go straight to transcript.
|
|
const result = await summarizeConversation([opencodeExchange()], 'ses_abc');
|
|
|
|
expect(result).toBe('Added retry to opencode exporter.');
|
|
// Exactly one call: a resume attempt + transcript fallback would be two.
|
|
expect(vi.mocked(query)).toHaveBeenCalledTimes(1);
|
|
const opts = vi.mocked(query).mock.calls[0][0].options as any;
|
|
expect(opts.resume).toBeUndefined();
|
|
const prompt = vi.mocked(query).mock.calls[0][0].prompt as string;
|
|
expect(prompt).toContain('Add retry to the opencode exporter');
|
|
expect(prompt).toContain('Wrapped exportOpencodeSessions');
|
|
});
|
|
});
|