import { describe, it, expect } from 'vitest'; import Fastify, { type FastifyInstance } from 'fastify'; import { registerAgentSessionRoutes } from '../agent-sessions.js'; import type { Sql } from '../../db.js'; // Mock the porsager surface this route uses: a tagged-template `sql` dispatched by // query substring. Two queries: the session-existence check and the agent_sessions // JOIN. We return post-coercion shapes (booleans/strings) exactly as porsager would // hand them to the route — `has_session` already a JS boolean, `last_active_at` a // string|null — so the asserted JSON matches the API contract end-to-end. interface MockState { sessionExists: boolean; rows: Array<{ agent: string; status: string; has_session: boolean; last_active_at: string | null }>; } function mockSql(state: MockState): Sql { return ((strings: TemplateStringsArray) => { const q = strings.join(''); if (q.includes('SELECT id FROM sessions')) { return Promise.resolve(state.sessionExists ? [{ id: 'session-1' }] : []); } if (q.includes('FROM agent_sessions')) { return Promise.resolve(state.rows); } return Promise.resolve([]); }) as unknown as Sql; } function buildApp(state: MockState): FastifyInstance { const app = Fastify(); registerAgentSessionRoutes(app, mockSql(state)); return app; } describe('GET /api/sessions/:id/agent-sessions', () => { it('returns the per-(chat,agent) rows in the contracted shape', async () => { const app = buildApp({ sessionExists: true, rows: [ { agent: 'opencode', status: 'active', has_session: true, last_active_at: '2026-05-31T12:00:00.000Z' }, { agent: 'goose', status: 'idle', has_session: false, last_active_at: null }, ], }); const res = await app.inject({ method: 'GET', url: '/api/sessions/session-1/agent-sessions' }); expect(res.statusCode).toBe(200); const body = res.json(); expect(Array.isArray(body)).toBe(true); expect(body).toEqual([ { agent: 'opencode', status: 'active', has_session: true, last_active_at: '2026-05-31T12:00:00.000Z' }, { agent: 'goose', status: 'idle', has_session: false, last_active_at: null }, ]); // Contract field types. expect(typeof body[0].agent).toBe('string'); expect(typeof body[0].status).toBe('string'); expect(typeof body[0].has_session).toBe('boolean'); expect(body[1].last_active_at).toBeNull(); await app.close(); }); it('returns an empty array when the session has no agent_sessions rows', async () => { const app = buildApp({ sessionExists: true, rows: [] }); const res = await app.inject({ method: 'GET', url: '/api/sessions/session-1/agent-sessions' }); expect(res.statusCode).toBe(200); expect(res.json()).toEqual([]); await app.close(); }); it('404s when the session does not exist', async () => { const app = buildApp({ sessionExists: false, rows: [] }); const res = await app.inject({ method: 'GET', url: '/api/sessions/nope/agent-sessions' }); expect(res.statusCode).toBe(404); expect(res.json()).toEqual({ error: 'session not found' }); await app.close(); }); });