/** * Happy Path Test: Observation Capture (PostToolUse) * * Tests that tool usage is captured and queued for SDK processing. * This is the core functionality of claude-mem - turning tool usage * into compressed observations. */ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { bashCommandScenario, readFileScenario, writeFileScenario, editFileScenario, grepScenario, sessionScenario } from '../helpers/scenarios.js'; import { getWorkerPort } from '../../src/shared/worker-utils.js'; describe('Observation Capture (PostToolUse)', () => { const WORKER_PORT = getWorkerPort(); beforeEach(() => { vi.clearAllMocks(); }); it('captures Bash command observation', async () => { // Setup: Mock worker response global.fetch = vi.fn().mockResolvedValue({ ok: true, status: 200, json: async () => ({ status: 'queued' }) }); // Execute: Send Bash tool observation const response = await fetch( `http://127.0.0.1:${WORKER_PORT}/api/sessions/observations`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ claudeSessionId: sessionScenario.claudeSessionId, tool_name: bashCommandScenario.tool_name, tool_input: bashCommandScenario.tool_input, tool_response: bashCommandScenario.tool_response, cwd: '/project/claude-mem' }) } ); // Verify: Observation queued successfully expect(response.ok).toBe(true); const result = await response.json(); expect(result.status).toBe('queued'); // Verify: Correct data sent to worker const fetchCall = (global.fetch as any).mock.calls[0]; const requestBody = JSON.parse(fetchCall[1].body); expect(requestBody.tool_name).toBe('Bash'); expect(requestBody.tool_input.command).toBe('git status'); }); it('captures Read file observation', async () => { // Setup: Mock worker response global.fetch = vi.fn().mockResolvedValue({ ok: true, status: 200, json: async () => ({ status: 'queued' }) }); // Execute: Send Read tool observation const response = await fetch( `http://127.0.0.1:${WORKER_PORT}/api/sessions/observations`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ claudeSessionId: sessionScenario.claudeSessionId, tool_name: readFileScenario.tool_name, tool_input: readFileScenario.tool_input, tool_response: readFileScenario.tool_response, cwd: '/project' }) } ); // Verify: Observation queued successfully expect(response.ok).toBe(true); const result = await response.json(); expect(result.status).toBe('queued'); // Verify: File path captured correctly const fetchCall = (global.fetch as any).mock.calls[0]; const requestBody = JSON.parse(fetchCall[1].body); expect(requestBody.tool_name).toBe('Read'); expect(requestBody.tool_input.file_path).toContain('index.ts'); }); it('captures Write file observation', async () => { // Setup: Mock worker response global.fetch = vi.fn().mockResolvedValue({ ok: true, status: 200, json: async () => ({ status: 'queued' }) }); // Execute: Send Write tool observation const response = await fetch( `http://127.0.0.1:${WORKER_PORT}/api/sessions/observations`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ claudeSessionId: sessionScenario.claudeSessionId, tool_name: writeFileScenario.tool_name, tool_input: writeFileScenario.tool_input, tool_response: writeFileScenario.tool_response, cwd: '/project' }) } ); // Verify: Observation queued successfully expect(response.ok).toBe(true); const result = await response.json(); expect(result.status).toBe('queued'); }); it('captures Edit file observation', async () => { // Setup: Mock worker response global.fetch = vi.fn().mockResolvedValue({ ok: true, status: 200, json: async () => ({ status: 'queued' }) }); // Execute: Send Edit tool observation const response = await fetch( `http://127.0.0.1:${WORKER_PORT}/api/sessions/observations`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ claudeSessionId: sessionScenario.claudeSessionId, tool_name: editFileScenario.tool_name, tool_input: editFileScenario.tool_input, tool_response: editFileScenario.tool_response, cwd: '/project' }) } ); // Verify: Observation queued successfully expect(response.ok).toBe(true); const result = await response.json(); expect(result.status).toBe('queued'); // Verify: Edit details captured const fetchCall = (global.fetch as any).mock.calls[0]; const requestBody = JSON.parse(fetchCall[1].body); expect(requestBody.tool_name).toBe('Edit'); expect(requestBody.tool_input.old_string).toBe('const PORT = 3000;'); expect(requestBody.tool_input.new_string).toBe('const PORT = 8080;'); }); it('captures Grep search observation', async () => { // Setup: Mock worker response global.fetch = vi.fn().mockResolvedValue({ ok: true, status: 200, json: async () => ({ status: 'queued' }) }); // Execute: Send Grep tool observation const response = await fetch( `http://127.0.0.1:${WORKER_PORT}/api/sessions/observations`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ claudeSessionId: sessionScenario.claudeSessionId, tool_name: grepScenario.tool_name, tool_input: grepScenario.tool_input, tool_response: grepScenario.tool_response, cwd: '/project' }) } ); // Verify: Observation queued successfully expect(response.ok).toBe(true); const result = await response.json(); expect(result.status).toBe('queued'); }); it('handles rapid succession of observations (burst mode)', async () => { // Setup: Mock worker to accept all observations let observationCount = 0; global.fetch = vi.fn().mockImplementation(async () => { const currentId = ++observationCount; return { ok: true, status: 200, json: async () => ({ status: 'queued', observationId: currentId }) }; }); // Execute: Send 5 observations rapidly (simulates active coding session) const observations = [ bashCommandScenario, readFileScenario, writeFileScenario, editFileScenario, grepScenario ]; const promises = observations.map(obs => fetch(`http://127.0.0.1:${WORKER_PORT}/api/sessions/observations`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ claudeSessionId: sessionScenario.claudeSessionId, tool_name: obs.tool_name, tool_input: obs.tool_input, tool_response: obs.tool_response, cwd: '/project' }) }) ); const responses = await Promise.all(promises); // Verify: All observations queued successfully expect(responses.every(r => r.ok)).toBe(true); expect(observationCount).toBe(5); // Verify: Each got unique ID const results = await Promise.all(responses.map(r => r.json())); const ids = results.map(r => r.observationId); expect(new Set(ids).size).toBe(5); // All IDs unique }); it('preserves tool metadata in observation', async () => { // Setup: Mock worker response global.fetch = vi.fn().mockResolvedValue({ ok: true, status: 200, json: async () => ({ status: 'queued' }) }); const complexTool = { tool_name: 'Task', tool_input: { subagent_type: 'Explore', prompt: 'Find authentication code', description: 'Search for auth' }, tool_response: { result: 'Found auth in /src/auth.ts', files_analyzed: ['/src/auth.ts', '/src/login.ts'] } }; // Execute: Send complex tool observation await fetch( `http://127.0.0.1:${WORKER_PORT}/api/sessions/observations`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ claudeSessionId: sessionScenario.claudeSessionId, ...complexTool, cwd: '/project' }) } ); // Verify: All metadata preserved in request const fetchCall = (global.fetch as any).mock.calls[0]; const requestBody = JSON.parse(fetchCall[1].body); expect(requestBody.tool_name).toBe('Task'); expect(requestBody.tool_input.subagent_type).toBe('Explore'); expect(requestBody.tool_response.files_analyzed).toHaveLength(2); }); });