189 lines
6.9 KiB
TypeScript
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();
|
|
});
|
|
}); |