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 { createAsk, answerAsk, escalateAsk, listAsks, getAsk } from '../src/core/services/askService.js'; import { askCreate, askAnswer, askEscalate } from '../src/cli/commands/ask.js'; import { addMemory, searchMemory } from '../src/core/services/memoryService.js'; describe('askService (TSK-0118)', () => { let cwd: string; beforeEach(async () => { cwd = mkdtempSync(join(tmpdir(), 'ah-ask-')); await init(cwd, { yes: true, projectName: 'test' }); }); afterEach(() => { rmSync(cwd, { recursive: true, force: true }); }); it('routes to the architect (never the CEO) by default', () => { const ask = createAsk(cwd, { from: 'kimi', question: 'Which DB driver?' }); expect(ask.id).toMatch(/^ASK-\d{4}$/); expect(ask.to).toBe('claude'); // config.roles.architect.preferredAgent expect(ask.status).toBe('pending'); }); it('refuses to route to the CEO — reroutes to the architect', () => { const ask = createAsk(cwd, { from: 'kimi', to: 'ceo', question: 'ship it?' }); expect(ask.to).toBe('claude'); }); it('answerAsk closes it as answered with the answer + author', () => { const ask = createAsk(cwd, { from: 'kimi', question: 'Which DB driver?' }); const answered = answerAsk(cwd, ask.id, 'node:sqlite', 'claude'); expect(answered.status).toBe('answered'); expect(answered.answer).toBe('node:sqlite'); expect(answered.answeredBy).toBe('claude'); expect(getAsk(cwd, ask.id).ask.status).toBe('answered'); }); it('escalateAsk flips to escalated → ceo (single channel, no second ask)', () => { const ask = createAsk(cwd, { from: 'kimi', question: 'Publish to OSS?' }); const escalated = escalateAsk(cwd, ask.id, 'OSS decision — CEO call'); expect(escalated.status).toBe('escalated'); expect(escalated.escalatedTo).toBe('ceo'); }); it('listAsks filters by status and recipient', () => { const a1 = createAsk(cwd, { from: 'kimi', question: 'q1' }); createAsk(cwd, { from: 'codex', question: 'q2' }); answerAsk(cwd, a1.id, 'yes'); expect(listAsks(cwd)).toHaveLength(2); expect(listAsks(cwd, { status: 'pending' })).toHaveLength(1); expect(listAsks(cwd, { to: 'claude' })).toHaveLength(2); expect(listAsks(cwd, { to: 'nobody' })).toHaveLength(0); }); it('asks are NOT indexed in FTS5 (memory search never returns them)', () => { createAsk(cwd, { from: 'kimi', question: 'znamqvist widget architecture' }); addMemory(cwd, { title: 'note', content: 'znamqvist widget architecture' }); const hits = searchMemory(cwd, 'znamqvist'); expect(hits.some((r) => r.type === 'ask')).toBe(false); // ask excluded from FTS expect(hits.some((r) => r.type === 'memory')).toBe(true); // search still works }); it('ask-cmd helpers create/answer/escalate through the service', () => { const ask = askCreate(cwd, { from: 'kimi', question: 'via cmd?' }); expect(ask.status).toBe('pending'); askAnswer(cwd, ask.id, 'ok', 'claude'); expect(getAsk(cwd, ask.id).ask.status).toBe('answered'); const ask2 = askCreate(cwd, { from: 'kimi', question: 'escalate me' }); askEscalate(cwd, ask2.id, 'needs ceo'); expect(getAsk(cwd, ask2.id).ask.status).toBe('escalated'); }); });