/** * Happy Path Test: Session Initialization * * Tests that when a user's first tool use occurs, the session is * created in the database and observations can be queued. */ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { bashCommandScenario, sessionScenario } from '../helpers/scenarios.js'; import { getWorkerPort } from '../../src/shared/worker-utils.js'; describe('Session Initialization (UserPromptSubmit)', () => { const WORKER_PORT = getWorkerPort(); beforeEach(() => { vi.clearAllMocks(); }); it('creates session when first observation is sent', async () => { // This tests the happy path: // User types first prompt → Tool runs → Hook sends observation → // Worker creates session → Observation queued for SDK processing // Setup: Mock successful response from worker global.fetch = vi.fn().mockResolvedValue({ ok: true, status: 200, json: async () => ({ status: 'queued', sessionId: 1 }) }); // Execute: Send first observation (what save-hook does) 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: Session created and observation queued expect(response.ok).toBe(true); const result = await response.json(); expect(result.status).toBe('queued'); expect(result.sessionId).toBeDefined(); // Verify: fetch was called with correct endpoint and data expect(global.fetch).toHaveBeenCalledWith( `http://127.0.0.1:${WORKER_PORT}/api/sessions/observations`, expect.objectContaining({ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: expect.stringContaining(sessionScenario.claudeSessionId) }) ); }); it('handles missing claudeSessionId gracefully', async () => { // Setup: Mock error response for missing session ID global.fetch = vi.fn().mockResolvedValue({ ok: false, status: 400, json: async () => ({ error: 'Missing claudeSessionId' }) }); // Execute: Send observation without session ID const response = await fetch( `http://127.0.0.1:${WORKER_PORT}/api/sessions/observations`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tool_name: 'Bash', tool_input: { command: 'ls' }, tool_response: { stdout: 'file.txt' } }) } ); // Verify: Returns 400 error expect(response.ok).toBe(false); expect(response.status).toBe(400); const error = await response.json(); expect(error.error).toContain('Missing claudeSessionId'); }); it('queues multiple observations for the same session', async () => { // Setup: Mock successful responses let callCount = 0; global.fetch = vi.fn().mockImplementation(async () => { const currentId = ++callCount; return { ok: true, status: 200, json: async () => ({ status: 'queued', observationId: currentId }) }; }); const sessionId = sessionScenario.claudeSessionId; // Execute: Send multiple observations for the same session const obs1 = await fetch( `http://127.0.0.1:${WORKER_PORT}/api/sessions/observations`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ claudeSessionId: sessionId, tool_name: 'Read', tool_input: { file_path: '/test.ts' }, tool_response: { content: 'code...' } }) } ); const obs2 = await fetch( `http://127.0.0.1:${WORKER_PORT}/api/sessions/observations`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ claudeSessionId: sessionId, tool_name: 'Edit', tool_input: { file_path: '/test.ts', old_string: 'old', new_string: 'new' }, tool_response: { success: true } }) } ); // Verify: Both observations were queued successfully expect(obs1.ok).toBe(true); expect(obs2.ok).toBe(true); const result1 = await obs1.json(); const result2 = await obs2.json(); expect(result1.status).toBe('queued'); expect(result2.status).toBe('queued'); expect(result1.observationId).toBe(1); expect(result2.observationId).toBe(2); }); it('includes project context from cwd', async () => { // Setup: Mock successful response global.fetch = vi.fn().mockResolvedValue({ ok: true, status: 200, json: async () => ({ status: 'queued' }) }); const projectPath = '/Users/alice/projects/my-app'; // Execute: Send observation with cwd 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: 'Bash', tool_input: { command: 'npm test' }, tool_response: { stdout: 'PASS', exit_code: 0 }, cwd: projectPath }) } ); // Verify: Request includes cwd expect(global.fetch).toHaveBeenCalledWith( expect.any(String), expect.objectContaining({ body: expect.stringContaining(projectPath) }) ); }); });