import * as fs from "node:fs"; import * as path from "node:path"; import * as os from "node:os"; import { ClarifyPhase, saveSpecification, loadSpecification } from "../core/clarify.js"; import { DEFAULT_CI_CONFIG } from "../types/config.js"; import { Specification, parseSpecification } from "../types/specification.js"; describe("ClarifyPhase", () => { let tempDir: string; beforeEach(() => { tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ci-clarify-test-")); fs.mkdirSync(path.join(tempDir, ".ci"), { recursive: true }); }); afterEach(() => { fs.rmSync(tempDir, { recursive: true, force: true }); }); const specWithRequirements: Specification = { title: "Test Project", objective: "Build a REST API", requirements: ["User authentication", "CRUD operations", "Deploy to AWS"], constraints: ["Must use Node.js"], out_of_scope: ["Admin dashboard"], raw_content: "# Test Project\n## Objective\nBuild a REST API", source: "inline", created_at: new Date().toISOString(), }; const specWithoutRequirements: Specification = { title: "Empty Project", objective: "Build something", requirements: [], constraints: [], out_of_scope: [], raw_content: "# Empty Project", source: "inline", created_at: new Date().toISOString(), }; describe("generateQuestions", () => { it("generates questions for missing requirements", () => { const clarify = new ClarifyPhase(DEFAULT_CI_CONFIG, tempDir); const questions = clarify.generateQuestions(specWithoutRequirements); expect(questions.length).toBeGreaterThan(0); const reqQuestion = questions.find((q) => q.category === "requirements"); expect(reqQuestion).toBeDefined(); expect(reqQuestion!.impact).toBe("critical"); }); it("generates questions for missing constraints", () => { const clarify = new ClarifyPhase(DEFAULT_CI_CONFIG, tempDir); const questions = clarify.generateQuestions(specWithoutRequirements); const constraintQuestion = questions.find((q) => q.category === "constraints"); expect(constraintQuestion).toBeDefined(); expect(constraintQuestion!.impact).toBe("high"); }); it("generates deployment question when deploy is mentioned without deploy constraint", () => { const clarify = new ClarifyPhase(DEFAULT_CI_CONFIG, tempDir); const questions = clarify.generateQuestions(specWithRequirements); const deployQuestion = questions.find((q) => q.category === "deployment"); expect(deployQuestion).toBeDefined(); }); it("respects clarify_budget", () => { const limitedConfig = { ...DEFAULT_CI_CONFIG, autonomy: { ...DEFAULT_CI_CONFIG.autonomy, clarify_budget: 1 }, }; const clarify = new ClarifyPhase(limitedConfig, tempDir); const questions = clarify.generateQuestions(specWithoutRequirements); expect(questions.length).toBeLessThanOrEqual(1); }); it("assigns sequential question IDs", () => { const clarify = new ClarifyPhase(DEFAULT_CI_CONFIG, tempDir); const questions = clarify.generateQuestions(specWithoutRequirements); for (let i = 0; i < questions.length; i++) { expect(questions[i].id).toBe(`Q-${String(i + 1).padStart(3, "0")}`); } }); it("sorts questions by impact priority", () => { const clarify = new ClarifyPhase(DEFAULT_CI_CONFIG, tempDir); const questions = clarify.generateQuestions(specWithoutRequirements); const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 }; for (let i = 1; i < questions.length; i++) { expect(priorityOrder[questions[i].impact]).toBeGreaterThanOrEqual( priorityOrder[questions[i - 1].impact] ); } }); }); describe("answerQuestion", () => { it("records an answer to a question", () => { const clarify = new ClarifyPhase(DEFAULT_CI_CONFIG, tempDir); const questions = clarify.generateQuestions(specWithoutRequirements); expect(questions.length).toBeGreaterThan(0); const answered = clarify.answerQuestion(questions[0].id, "My custom answer"); expect(answered).not.toBeNull(); expect(answered!.answered).toBe(true); expect(answered!.answer).toBe("My custom answer"); }); it("returns null for unknown question ID", () => { const clarify = new ClarifyPhase(DEFAULT_CI_CONFIG, tempDir); const result = clarify.answerQuestion("Q-999", "answer"); expect(result).toBeNull(); }); }); describe("acceptDefaults", () => { it("accepts defaults for all unanswered questions", () => { const clarify = new ClarifyPhase(DEFAULT_CI_CONFIG, tempDir); clarify.generateQuestions(specWithoutRequirements); const result = clarify.acceptDefaults(); expect(result.unanswered_defaults_accepted).toBeGreaterThan(0); expect(result.total_questions).toBeGreaterThan(0); expect(result.answered_questions).toBe(result.total_questions); }); it("preserves manually answered questions", () => { const clarify = new ClarifyPhase(DEFAULT_CI_CONFIG, tempDir); const questions = clarify.generateQuestions(specWithoutRequirements); if (questions.length > 0) { clarify.answerQuestion(questions[0].id, "My answer"); } const result = clarify.acceptDefaults(); const manuallyAnswered = result.questions.find( (q) => q.answer === "My answer" ); expect(manuallyAnswered).toBeDefined(); }); it("saves clarify responses file", () => { const clarify = new ClarifyPhase(DEFAULT_CI_CONFIG, tempDir); clarify.generateQuestions(specWithoutRequirements); clarify.acceptDefaults(); const responsesPath = path.join(tempDir, ".ci", "clarify-responses.md"); expect(fs.existsSync(responsesPath)).toBe(true); const content = fs.readFileSync(responsesPath, "utf-8"); expect(content).toContain("Clarify Phase Responses"); }); }); }); describe("saveSpecification / loadSpecification", () => { let tempDir: string; beforeEach(() => { tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ci-spec-test-")); fs.mkdirSync(path.join(tempDir, ".ci"), { recursive: true }); }); afterEach(() => { fs.rmSync(tempDir, { recursive: true, force: true }); }); it("saves and loads a specification", () => { const spec: Specification = { title: "Test", objective: "Build it", requirements: ["Feature A"], constraints: ["Node.js"], out_of_scope: [], raw_content: "# Test\n## Objective\nBuild it\n## Requirements\n- Feature A\n## Constraints\n- Node.js", source: "file", created_at: new Date().toISOString(), }; saveSpecification(tempDir, spec); const loaded = loadSpecification(tempDir); expect(loaded).not.toBeNull(); expect(loaded!.title).toBe("Test"); expect(loaded!.requirements).toContain("Feature A"); }); it("returns null when no specification exists", () => { const loaded = loadSpecification(tempDir); expect(loaded).toBeNull(); }); });