import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { mkdtempSync, rmSync } from 'fs'; import { tmpdir } from 'os'; import { join } from 'path'; import { init } from '../src/cli/commands/init.js'; import { buildApp } from '../src/server/index.js'; import { startServer } from '../src/server/index.js'; import { waitForAsk } from '../src/cli/commands/ask.js'; import type { Ask } from '../src/core/schema.js'; describe('ask routes (TSK-0118)', () => { let cwd: string; let app: ReturnType; beforeEach(() => { cwd = mkdtempSync(join(tmpdir(), 'ah-ask-routes-')); init(cwd, { projectName: 'ask-routes', yes: true }); app = buildApp(cwd); }); afterEach(() => { rmSync(cwd, { recursive: true, force: true }); }); it('POST/GET/answer/escalate roundtrip', async () => { const created = JSON.parse((await app.inject({ method: 'POST', url: '/asks', payload: { from: 'kimi', question: 'q?' } })).payload) as Ask; expect(created.id).toBe('ASK-0001'); expect(created.to).toBe('claude'); expect(created.status).toBe('pending'); // GET /asks — the data source agenthub_work's architect branch surfaces. const pending = JSON.parse((await app.inject({ method: 'GET', url: '/asks?status=pending' })).payload) as Ask[]; expect(pending.map((a) => a.id)).toContain('ASK-0001'); const answered = JSON.parse((await app.inject({ method: 'POST', url: '/asks/ASK-0001/answer', payload: { text: 'yes', by: 'claude' } })).payload) as Ask; expect(answered.status).toBe('answered'); expect(answered.answer).toBe('yes'); const created2 = JSON.parse((await app.inject({ method: 'POST', url: '/asks', payload: { from: 'kimi', question: 'ship?' } })).payload) as Ask; const escalated = JSON.parse((await app.inject({ method: 'POST', url: `/asks/${created2.id}/escalate`, payload: { note: 'ceo call' } })).payload) as Ask; expect(escalated.status).toBe('escalated'); expect(escalated.escalatedTo).toBe('ceo'); }); }); describe('ask --wait (server SSE)', () => { let cwd: string; let server: Awaited>; beforeEach(async () => { cwd = mkdtempSync(join(tmpdir(), 'ah-ask-wait-')); init(cwd, { projectName: 'ask-wait', yes: true }); server = await startServer(cwd, { host: '127.0.0.1', port: 0 }); }); afterEach(async () => { await server.app.close().catch(() => undefined); rmSync(cwd, { recursive: true, force: true }); }); it('resolves with the answer when the architect answers', async () => { const created = (await fetch(`${server.url}/asks`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ from: 'kimi', question: 'which driver?' }), }).then((r) => r.json())) as Ask; const waitP = waitForAsk(server.url, created.id, 4); await new Promise((r) => setTimeout(r, 200)); await fetch(`${server.url}/asks/${created.id}/answer`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: 'node:sqlite', by: 'claude' }), }); const settled = await waitP; expect(settled).not.toBeNull(); expect(settled?.status).toBe('answered'); expect(settled?.answer).toBe('node:sqlite'); }, 7000); it('times out cleanly when there is no answer', async () => { const created = (await fetch(`${server.url}/asks`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ from: 'kimi', question: 'no answer coming' }), }).then((r) => r.json())) as Ask; const settled = await waitForAsk(server.url, created.id, 1); expect(settled).toBeNull(); }, 4000); });