Files
ci/src/core/clarify.test.ts
T

189 lines
6.9 KiB
TypeScript

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();
});
});