import { describe, expect, it, vi } from 'vitest'; import type { MemoryAgentInput, MemoryAgentResult } from '../../../src/context/memory/types.js'; import type { MemoryAgentService } from '../../../src/context/memory/memory-agent.service.js'; import { MemoryIngestService, type MemoryRunStorePort } from '../../../src/context/memory/memory-runs.js'; class InMemoryRunStore implements MemoryRunStorePort { readonly rows = new Map< string, { id: string; status: 'running' | 'done' | 'error'; stage: string; inputHash: string; chatId: string | null; outputSummary: MemoryAgentResult | null; error: string | null; } >(); async createRunning(args: { inputHash: string; chatId?: string | null }): Promise<{ id: string }> { const id = `run-${this.rows.size + 1}`; this.rows.set(id, { id, status: 'running', stage: 'queued', inputHash: args.inputHash, chatId: args.chatId ?? null, outputSummary: null, error: null, }); return { id }; } async markRunning(id: string, stage: string): Promise { const row = this.rows.get(id); if (!row) { throw new Error(`unknown run ${id}`); } row.stage = stage; } async markDone(id: string, outputSummary: MemoryAgentResult): Promise { const row = this.rows.get(id); if (!row) { throw new Error(`unknown run ${id}`); } row.status = 'done'; row.stage = 'done'; row.outputSummary = outputSummary; } async markError(id: string, error: string): Promise { const row = this.rows.get(id); if (!row) { throw new Error(`unknown run ${id}`); } row.status = 'error'; row.stage = 'error'; row.error = error; } async findById(id: string) { return this.rows.get(id) ?? null; } } function deferred() { let resolve!: (value: T) => void; let reject!: (reason?: unknown) => void; const promise = new Promise((res, rej) => { resolve = res; reject = rej; }); return { promise, resolve, reject }; } function buildService(): { ingest: MemoryIngestService; store: InMemoryRunStore; memoryAgentIngest: ReturnType; run: ReturnType>; } { const store = new InMemoryRunStore(); const run = deferred(); const memoryAgentIngest = vi.fn().mockReturnValue(run.promise); const memoryAgent = { ingest: memoryAgentIngest }; return { ingest: new MemoryIngestService({ memoryAgent, runs: store }), store, memoryAgentIngest, run, }; } describe('MemoryIngestService', () => { it('creates a run, executes memory ingest, and stores a done summary', async () => { const result: MemoryAgentResult = { signalDetected: true, actions: [{ target: 'wiki', type: 'created', key: 'revenue', detail: 'captured revenue definition' }], skillsLoaded: ['wiki_capture'], commitHash: 'abc123', }; const { ingest, store, memoryAgentIngest, run } = buildService(); const input: MemoryAgentInput = { userId: 'user-1', chatId: 'chat-1', userMessage: 'Revenue means paid order value.', assistantMessage: 'Captured.', connectionId: '00000000-0000-0000-0000-000000000001', }; const started = await ingest.ingest(input); expect(started.runId).toBe('run-1'); expect(memoryAgentIngest).toHaveBeenCalledWith(input); await expect(ingest.status(started.runId)).resolves.toMatchObject({ runId: 'run-1', status: 'running', stage: 'ingesting', done: false, }); run.resolve(result); await ingest.waitForRun(started.runId); const status = await ingest.status(started.runId); expect(status).toEqual({ runId: 'run-1', stage: 'done', done: true, status: 'done', captured: { wiki: ['revenue'], sl: [], xrefs: [], }, error: null, commitHash: 'abc123', skillsLoaded: ['wiki_capture'], signalDetected: true, }); expect(store.rows.get('run-1')?.inputHash).toHaveLength(64); }); it('stores no-signal ingests as done with empty captured arrays', async () => { const { ingest, run } = buildService(); const started = await ingest.ingest({ userId: 'user-1', chatId: 'chat-2', userMessage: 'Thanks.', }); run.resolve({ signalDetected: false, actions: [], skillsLoaded: [], commitHash: null, }); await ingest.waitForRun(started.runId); await expect(ingest.status(started.runId)).resolves.toMatchObject({ done: true, status: 'done', captured: { wiki: [], sl: [], xrefs: [] }, signalDetected: false, }); }); it('stores thrown errors and projects them as failed statuses', async () => { const store = new InMemoryRunStore(); const memoryAgent = { ingest: vi.fn().mockRejectedValue(new Error('LLM provider missing')), }; const ingest = new MemoryIngestService({ memoryAgent, runs: store }); const started = await ingest.ingest({ userId: 'user-1', chatId: 'chat-3', userMessage: 'Remember this.', }); await ingest.waitForRun(started.runId); await expect(ingest.status(started.runId)).resolves.toMatchObject({ done: true, status: 'error', stage: 'error', captured: { wiki: [], sl: [], xrefs: [] }, error: 'LLM provider missing', }); }); it('returns null for an unknown run id', async () => { const { ingest } = buildService(); await expect(ingest.status('missing')).resolves.toBeNull(); }); });