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 { createTask, claimTask, reviewTask } from '../src/core/services/taskService.js'; import { searchMemory } from '../src/core/services/memoryService.js'; describe('core correctness (TSK-0007)', () => { let cwd: string; beforeEach(() => { cwd = mkdtempSync(join(tmpdir(), 'ah-core-')); init(cwd, { projectName: 'core-test', yes: true }); }); afterEach(() => { rmSync(cwd, { recursive: true, force: true }); }); // ── FTS5 duplicate-on-update ────────────────────────────────────────────── it('search returns no duplicates after an entity is updated repeatedly', () => { const t = createTask(cwd, { title: 'Zephyr indexing widget', role: 'implementer' }); // Each mutation re-upserts into the FTS index. claimTask(cwd, t.id, 'kimi'); reviewTask(cwd, t.id); const hits = searchMemory(cwd, 'Zephyr'); const forTask = hits.filter((r) => r.id === t.id); expect(forTask).toHaveLength(1); // exactly one, not one-per-update }); // ── claimTask race-guard ────────────────────────────────────────────────── it('only one of two concurrent claims on the same open task wins', () => { const t = createTask(cwd, { title: 'contended', role: 'implementer' }); const winners: string[] = []; let refused = 0; for (const agent of ['kimi', 'codex']) { try { const claimed = claimTask(cwd, t.id, agent); winners.push(claimed.assignedTo ?? ''); } catch { refused += 1; } } expect(winners).toHaveLength(1); expect(refused).toBe(1); expect(winners[0]).toBe('kimi'); // the first to act wins }); it('a re-claim by the same agent is an idempotent no-op', () => { const t = createTask(cwd, { title: 'mine', role: 'implementer' }); claimTask(cwd, t.id, 'kimi'); const again = claimTask(cwd, t.id, 'kimi'); // no throw expect(again.status).toBe('in_progress'); expect(again.claimedBy).toBe('kimi'); }); it('refuses to claim a task that is not open', () => { const t = createTask(cwd, { title: 'x', role: 'implementer' }); claimTask(cwd, t.id, 'kimi'); reviewTask(cwd, t.id); // now in review expect(() => claimTask(cwd, t.id, 'codex')).toThrow(/cannot be claimed/i); }); });