4a58aa1657
- Type renames: CIConfig → CIAgentConfig, DEFAULT_CI_CONFIG → DEFAULT_CIAGENT_CONFIG - Type renames: CiMetadata → CIAgentMetadata, ParsedCiCommit → ParsedCIAgentCommit - Function renames: initCI → initCIAgent, isCIInitialized → isCIAgentInitialized - Function renames: extractCiBlock → extractCIAgentBlock, parseCiBlock → parseCIAgentBlock - Class renames: CiFiles → CIAgentFiles - Import paths: ci-files.js → ciagent-files.js - Directory paths: .ci/ → .ciagent/ across all source and test files - Check names: ".ci directory exists" → ".ciagent directory exists" - Check names: "CI config valid" → "CIAgent config valid" - Temp dir names: ci-*-test- → ciagent-*-test- - CLI examples: "ci init" → "ciagent init" - Fix deepMerge infinite recursion bug in config.ts - ---ci---/---/ci--- block markers preserved unchanged - All 31 test suites, 370 tests passing ---ci--- phase: 1 milestone: v0.5 plan: 07 task: 07-01-01 status: execute ---/ci---
373 lines
12 KiB
TypeScript
373 lines
12 KiB
TypeScript
import { CommitBuilder } from "../core/commit-builder.js";
|
|
import { extractCIAgentBlock, parseCIAgentBlock } from "../core/commit-parser.js";
|
|
import { CIAgentMetadata } from "../types/commit-meta.js";
|
|
|
|
describe("CommitBuilder", () => {
|
|
describe("buildCiBlock", () => {
|
|
it("builds minimal ci block", () => {
|
|
const ci: CIAgentMetadata = { phase: 1, milestone: "v1.0", status: "execute" };
|
|
const block = CommitBuilder.buildCiBlock(ci);
|
|
|
|
expect(block).toContain("phase: 1");
|
|
expect(block).toContain("milestone: v1.0");
|
|
expect(block).toContain("status: execute");
|
|
});
|
|
|
|
it("builds ci block with project", () => {
|
|
const ci: CIAgentMetadata = { phase: 1, milestone: "v1.0", status: "execute", project: "task-api" };
|
|
const block = CommitBuilder.buildCiBlock(ci);
|
|
expect(block).toContain("project: task-api");
|
|
});
|
|
|
|
it("builds ci block without project when not set", () => {
|
|
const ci: CIAgentMetadata = { phase: 1, milestone: "v1.0", status: "execute" };
|
|
const block = CommitBuilder.buildCiBlock(ci);
|
|
expect(block).not.toContain("project:");
|
|
});
|
|
|
|
it("builds ci block with decisions", () => {
|
|
const ci: CIAgentMetadata = {
|
|
phase: 1,
|
|
milestone: "v1.0",
|
|
status: "execute",
|
|
decisions: [
|
|
{
|
|
id: "D-001",
|
|
decision: "Use PostgreSQL",
|
|
rationale: "ACID compliance",
|
|
confidence: 0.9,
|
|
alternatives: ["MongoDB", "SQLite"],
|
|
},
|
|
],
|
|
};
|
|
|
|
const block = CommitBuilder.buildCiBlock(ci);
|
|
expect(block).toContain("decisions:");
|
|
expect(block).toContain("id: D-001");
|
|
expect(block).toContain("decision: Use PostgreSQL");
|
|
expect(block).toContain("alternatives: [MongoDB, SQLite]");
|
|
});
|
|
|
|
it("builds ci block with lessons", () => {
|
|
const ci: CIAgentMetadata = {
|
|
phase: 1,
|
|
milestone: "v1.0",
|
|
status: "complete",
|
|
lessons: ["Always use async bcrypt", "Check JWT expiry first"],
|
|
};
|
|
|
|
const block = CommitBuilder.buildCiBlock(ci);
|
|
expect(block).toContain("lessons:");
|
|
expect(block).toContain(" - Always use async bcrypt");
|
|
expect(block).toContain(" - Check JWT expiry first");
|
|
});
|
|
|
|
it("builds ci block with compound", () => {
|
|
const ci: CIAgentMetadata = {
|
|
phase: 1,
|
|
milestone: "v1.0",
|
|
status: "complete",
|
|
compound: {
|
|
category: "auth",
|
|
problem: "Token replay",
|
|
solution: "Refresh rotation",
|
|
},
|
|
};
|
|
|
|
const block = CommitBuilder.buildCiBlock(ci);
|
|
expect(block).toContain("compound:");
|
|
expect(block).toContain("category: auth");
|
|
expect(block).toContain("problem: Token replay");
|
|
expect(block).toContain("solution: Refresh rotation");
|
|
});
|
|
|
|
it("builds ci block with escalations", () => {
|
|
const ci: CIAgentMetadata = {
|
|
phase: 3,
|
|
milestone: "v1.0",
|
|
status: "execute",
|
|
escalations: [
|
|
{
|
|
id: "E-001",
|
|
type: "irreversible_action",
|
|
description: "Deploy to staging",
|
|
resolution: "pending",
|
|
},
|
|
],
|
|
};
|
|
|
|
const block = CommitBuilder.buildCiBlock(ci);
|
|
expect(block).toContain("escalations:");
|
|
expect(block).toContain("id: E-001");
|
|
expect(block).toContain("type: irreversible_action");
|
|
});
|
|
|
|
it("builds ci block with requirements", () => {
|
|
const ci: CIAgentMetadata = {
|
|
phase: 1,
|
|
milestone: "v1.0",
|
|
status: "complete",
|
|
requirements: {
|
|
covered: ["AUTH-01", "AUTH-02"],
|
|
partial: ["AUTH-03"],
|
|
},
|
|
};
|
|
|
|
const block = CommitBuilder.buildCiBlock(ci);
|
|
expect(block).toContain("requirements:");
|
|
expect(block).toContain("covered: [AUTH-01, AUTH-02]");
|
|
expect(block).toContain("partial: [AUTH-03]");
|
|
});
|
|
});
|
|
|
|
describe("round-trip: build then parse", () => {
|
|
it("round-trips a simple ci block", () => {
|
|
const ci: CIAgentMetadata = { phase: 1, milestone: "v1.0", status: "execute" };
|
|
const block = CommitBuilder.buildCiBlock(ci);
|
|
|
|
const fullMessage = `feat(P01): test\n\n---ci---\n${block}\n---/ci---\n\nBody text`;
|
|
const extracted = extractCIAgentBlock(fullMessage)!;
|
|
const parsed = parseCIAgentBlock(extracted)!;
|
|
|
|
expect(parsed.phase).toBe(1);
|
|
expect(parsed.milestone).toBe("v1.0");
|
|
expect(parsed.status).toBe("execute");
|
|
});
|
|
|
|
it("round-trips decisions", () => {
|
|
const ci: CIAgentMetadata = {
|
|
phase: 1,
|
|
milestone: "v1.0",
|
|
status: "execute",
|
|
decisions: [
|
|
{
|
|
id: "D-001",
|
|
decision: "Use PostgreSQL",
|
|
rationale: "ACID compliance",
|
|
confidence: 0.9,
|
|
alternatives: ["MongoDB"],
|
|
},
|
|
],
|
|
};
|
|
|
|
const block = CommitBuilder.buildCiBlock(ci);
|
|
const fullMessage = `feat(P01): test\n\n---ci---\n${block}\n---/ci---`;
|
|
const extracted = extractCIAgentBlock(fullMessage)!;
|
|
const parsed = parseCIAgentBlock(extracted)!;
|
|
|
|
expect(parsed.decisions).toHaveLength(1);
|
|
expect(parsed.decisions![0].id).toBe("D-001");
|
|
expect(parsed.decisions![0].decision).toBe("Use PostgreSQL");
|
|
expect(parsed.decisions![0].confidence).toBe(0.9);
|
|
expect(parsed.decisions![0].alternatives).toEqual(["MongoDB"]);
|
|
});
|
|
|
|
it("round-trips compound with lessons", () => {
|
|
const ci: CIAgentMetadata = {
|
|
phase: 2,
|
|
milestone: "v1.0",
|
|
status: "complete",
|
|
compound: {
|
|
category: "auth",
|
|
problem: "Token replay attacks",
|
|
solution: "Refresh rotation with family IDs",
|
|
},
|
|
lessons: ["Token rotation is not optional"],
|
|
};
|
|
|
|
const block = CommitBuilder.buildCiBlock(ci);
|
|
const fullMessage = `compound(P02): test\n\n---ci---\n${block}\n---/ci---`;
|
|
const extracted = extractCIAgentBlock(fullMessage)!;
|
|
const parsed = parseCIAgentBlock(extracted)!;
|
|
|
|
expect(parsed.compound!.category).toBe("auth");
|
|
expect(parsed.compound!.problem).toBe("Token replay attacks");
|
|
expect(parsed.lessons).toHaveLength(1);
|
|
});
|
|
|
|
it("round-trips project field", () => {
|
|
const ci: CIAgentMetadata = { phase: 1, milestone: "v1.0", status: "execute", project: "task-api" };
|
|
const block = CommitBuilder.buildCiBlock(ci);
|
|
const fullMessage = `feat(task-api/P01): test\n\n---ci---\n${block}\n---/ci---`;
|
|
const extracted = extractCIAgentBlock(fullMessage)!;
|
|
const parsed = parseCIAgentBlock(extracted)!;
|
|
|
|
expect(parsed.project).toBe("task-api");
|
|
});
|
|
});
|
|
|
|
describe("buildInitCommit", () => {
|
|
it("builds an init commit message", () => {
|
|
const msg = CommitBuilder.buildInitCommit({
|
|
projectName: "task-api",
|
|
phaseCount: 4,
|
|
milestone: "v1.0",
|
|
specification: "Build a REST API for task management",
|
|
requirements: ["AUTH-01", "TASK-01"],
|
|
constraints: ["Node.js"],
|
|
outOfScope: ["Admin dashboard"],
|
|
});
|
|
|
|
expect(msg).toContain("docs(init):");
|
|
expect(msg).toContain("---ci---");
|
|
expect(msg).toContain("phase: 0");
|
|
expect(msg).toContain("milestone: v1.0");
|
|
expect(msg).toContain("Build a REST API for task management");
|
|
expect(msg).toContain("AUTH-01");
|
|
});
|
|
|
|
it("builds an init commit message with project", () => {
|
|
const msg = CommitBuilder.buildInitCommit({
|
|
projectName: "task-api",
|
|
phaseCount: 4,
|
|
milestone: "v1.0",
|
|
project: "task-api",
|
|
specification: "Build a REST API",
|
|
requirements: ["AUTH-01"],
|
|
});
|
|
|
|
expect(msg).toContain("project: task-api");
|
|
});
|
|
});
|
|
|
|
describe("buildTaskCommit", () => {
|
|
it("builds a task commit message", () => {
|
|
const msg = CommitBuilder.buildTaskCommit({
|
|
type: "feat",
|
|
phase: 1,
|
|
milestone: "v1.0",
|
|
plan: "01-01",
|
|
task: "01-01-02",
|
|
subject: "create user registration endpoint",
|
|
status: "execute",
|
|
decisions: [
|
|
{
|
|
id: "D-003",
|
|
decision: "Use bcrypt with 12 rounds",
|
|
rationale: "Industry standard",
|
|
confidence: 0.88,
|
|
alternatives: ["argon2"],
|
|
},
|
|
],
|
|
requirements: { covered: ["AUTH-01"], partial: [] },
|
|
});
|
|
|
|
expect(msg).toContain("feat(P01-01-02):");
|
|
expect(msg).toContain("plan: 01-01");
|
|
expect(msg).toContain("task: 01-01-02");
|
|
expect(msg).toContain("D-003");
|
|
expect(msg).toContain("AUTH-01");
|
|
});
|
|
|
|
it("builds a task commit with project prefix", () => {
|
|
const msg = CommitBuilder.buildTaskCommit({
|
|
type: "feat",
|
|
phase: 1,
|
|
milestone: "v1.0",
|
|
project: "task-api",
|
|
plan: "01-01",
|
|
task: "01-01-02",
|
|
subject: "registration endpoint",
|
|
status: "execute",
|
|
});
|
|
|
|
expect(msg).toContain("feat(task-api/P01-01-02):");
|
|
expect(msg).toContain("project: task-api");
|
|
});
|
|
});
|
|
|
|
describe("buildPhaseCompletionCommit", () => {
|
|
it("builds a phase completion commit", () => {
|
|
const msg = CommitBuilder.buildPhaseCompletionCommit({
|
|
phase: 1,
|
|
milestone: "v1.0",
|
|
phaseName: "authentication",
|
|
tasksCompleted: 4,
|
|
tasksTotal: 4,
|
|
taskNames: ["scaffold", "registration", "login", "reset"],
|
|
requirements: { covered: ["AUTH-01", "AUTH-02", "AUTH-03", "AUTH-04"], partial: [] },
|
|
lessons: ["Always use async bcrypt"],
|
|
});
|
|
|
|
expect(msg).toContain("docs(P01): complete authentication phase");
|
|
expect(msg).toContain("status: complete");
|
|
expect(msg).toContain("Tasks completed: 4/4");
|
|
expect(msg).toContain("Always use async bcrypt");
|
|
});
|
|
});
|
|
|
|
describe("buildCompoundCommit", () => {
|
|
it("builds a compound commit", () => {
|
|
const msg = CommitBuilder.buildCompoundCommit({
|
|
phase: 1,
|
|
milestone: "v1.0",
|
|
category: "auth",
|
|
problem: "Token replay allows persistent access",
|
|
solution: "Refresh token rotation with family IDs",
|
|
lessons: ["Rotation is not optional"],
|
|
});
|
|
|
|
expect(msg).toContain("compound(P01):");
|
|
expect(msg).toContain("category: auth");
|
|
expect(msg).toContain("problem: Token replay");
|
|
expect(msg).toContain("solution: Refresh token rotation");
|
|
});
|
|
});
|
|
|
|
describe("buildDecisionCommit", () => {
|
|
it("builds a decision-only commit", () => {
|
|
const msg = CommitBuilder.buildDecisionCommit({
|
|
phase: 1,
|
|
milestone: "v1.0",
|
|
subject: "use PostgreSQL over MongoDB",
|
|
decisions: [
|
|
{
|
|
id: "D-001",
|
|
decision: "PostgreSQL",
|
|
rationale: "ACID",
|
|
confidence: 0.92,
|
|
alternatives: ["MongoDB"],
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(msg).toContain("decision(P01): use PostgreSQL over MongoDB");
|
|
expect(msg).toContain("D-001");
|
|
});
|
|
});
|
|
|
|
describe("buildEscalationCommit", () => {
|
|
it("builds an escalation commit", () => {
|
|
const msg = CommitBuilder.buildEscalationCommit({
|
|
phase: 3,
|
|
milestone: "v1.0",
|
|
subject: "deploy to staging requires approval",
|
|
escalations: [
|
|
{
|
|
id: "E-001",
|
|
type: "irreversible_action",
|
|
description: "Deploy to staging",
|
|
resolution: "pending",
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(msg).toContain("escalation(P03): deploy to staging requires approval");
|
|
expect(msg).toContain("E-001");
|
|
});
|
|
});
|
|
|
|
describe("buildVerifyCommit", () => {
|
|
it("builds a verify commit", () => {
|
|
const msg = CommitBuilder.buildVerifyCommit({
|
|
phase: 1,
|
|
milestone: "v1.0",
|
|
subject: "all must-haves pass automated tests",
|
|
requirements: { covered: ["AUTH-01", "AUTH-02"], partial: [] },
|
|
});
|
|
|
|
expect(msg).toContain("verify(P01): all must-haves pass automated tests");
|
|
expect(msg).toContain("AUTH-01");
|
|
});
|
|
});
|
|
}); |