import { describe, it, expect, beforeAll } from 'vitest'; import request from 'supertest'; import { pool } from '../../lib/db/pool.js'; import { createApp } from '../../server.js'; import { resetDb } from '../helpers/db.js'; import { migrateUp } from '../../lib/db/migrate.js'; let app, spaceId; beforeAll(async () => { await resetDb(); await migrateUp(); process.env.OWNER_TOKEN = 'test-token'; ({ rows: [{ id: spaceId }] } = await pool.query( `INSERT INTO spaces(slug,name) VALUES('s','S') RETURNING id`)); app = createApp(); let step = 0; app.locals.callModel = async ({ onTextDelta }) => { if (step++ === 0) return { text: '', toolUses: [{ id: 't1', name: 'propose_change', input: { entity_type: 'task', action: 'create', payload: { space_id: spaceId, title: 'Validate CSV' } } }], stopReason: 'tool_use', usage: {} }; for (const ch of 'Drafted a task.') onTextDelta?.(ch); return { text: 'Drafted a task.', toolUses: [], stopReason: 'end_turn', usage: { output_tokens: 3 } }; }; }); const auth = (r) => r.set('Authorization', 'Bearer test-token'); describe('companion API', () => { it('GET creates the conversation and returns empty history', async () => { const res = await auth(request(app).get(`/api/spaces/${spaceId}/companion`)); expect(res.status).toBe(200); expect(res.body.conversation_id).toBeTruthy(); expect(res.body.messages).toEqual([]); }); it('POST /turn streams SSE events and persists messages + draft', async () => { const res = await auth(request(app).post(`/api/spaces/${spaceId}/companion/turn`)) .send({ text: 'make a task to validate the CSV' }); expect(res.status).toBe(200); expect(res.headers['content-type']).toMatch(/text\/event-stream/); expect(res.text).toMatch(/event: tool/); expect(res.text).toMatch(/event: draft/); expect(res.text).toMatch(/event: delta/); expect(res.text).toMatch(/event: done/); const { rows: msgs } = await pool.query( `SELECT role, body, metadata FROM messages ORDER BY created_at`); expect(msgs.map(m => m.role)).toEqual(['user', 'assistant']); expect(msgs[1].body).toBe('Drafted a task.'); expect(msgs[1].metadata.draft_ids).toHaveLength(1); const { rows: pc } = await pool.query(`SELECT * FROM pending_changes`); expect(pc).toHaveLength(1); expect(pc[0].status).toBe('pending'); const { rows: tasks } = await pool.query(`SELECT * FROM tasks WHERE title='Validate CSV'`); expect(tasks).toHaveLength(0); // draft only, not applied }); });