import { CiMetadata, CommitDecision, CommitEscalation, CommitRequirements, CommitCompoundMeta, parseCommitScope, formatCommitScope, CommitScope, } from "../types/commit-meta.js"; import { extractCiBlock, parseCiBlock, parseCommitMessage, } from "./commit-parser.js"; const SAMPLE_INIT_COMMIT = `docs(init): initialize task-api (4 phases) ---ci--- phase: 0 milestone: v1.0 status: specify decisions: - id: D-001 decision: Node.js with Express for REST API rationale: Spec requires Node.js; Express is minimal and well-supported confidence: 0.95 alternatives: [Fastify, Hono] - id: D-002 decision: PostgreSQL for persistence rationale: ACID compliance required by spec confidence: 0.90 alternatives: [MongoDB, SQLite] ---/ci--- Specification: Build a REST API for task management with JWT auth, CRUD operations, real-time notifications via WebSocket, PostgreSQL database. Requirements: AUTH-01 through AUTH-04, TASK-01 through TASK-05, NOTIF-01 Constraints: Node.js, production-ready, no Docker Out of scope: Admin dashboard, payment integration, mobile apps`; const SAMPLE_TASK_COMMIT = `feat(P01-01-02): create user registration endpoint ---ci--- phase: 1 milestone: v1.0 plan: 01-01 task: 01-01-02 status: execute decisions: - id: D-003 decision: Use bcrypt with 12 rounds for password hashing rationale: Industry standard; argon2 not available in target env confidence: 0.88 alternatives: [argon2, scrypt] requirements: covered: [AUTH-01] ---/ci--- - POST /auth/register validates email and password - Checks for duplicate users - Returns JWT token on success`; const SAMPLE_PHASE_COMPLETE_COMMIT = `docs(P01): complete authentication phase ---ci--- phase: 1 milestone: v1.0 status: complete decisions: - id: D-005 decision: Session JWTs with 1hr expiry + opaque refresh token rationale: Balances security and UX; refresh rotation prevents replay confidence: 0.92 alternatives: [Stateless JWT only, session cookies] requirements: covered: [AUTH-01, AUTH-02, AUTH-03, AUTH-04] lessons: - bcrypt async is 10x faster than sync in Node; always use bcrypt.compare() - JWT expiry must be checked before signature verification to prevent edge cases ---/ci--- Tasks completed: 4/4`; const SAMPLE_COMPOUND_COMMIT = `compound(auth): JWT refresh token rotation pattern ---ci--- phase: 1 milestone: v1.0 status: complete compound: category: auth problem: Refresh tokens can be replayed if stolen; naive implementation allows token reuse solution: Implement refresh token rotation — each use invalidates old token and issues new one. Store token family ID to detect replay attempts. On replay detection, revoke entire family. lessons: - Refresh token rotation is not optional for production auth - Token family detection prevents silent takeover ---/ci--- Discovered during AUTH-04 implementation.`; const SAMPLE_ESCALATION_COMMIT = `escalation(P03): deploy to staging requires approval ---ci--- phase: 3 milestone: v1.0 status: execute escalations: - id: E-001 type: irreversible_action description: Phase 3 requires deployment to staging environment resolution: pending ---/ci--- All tests pass. Awaiting deploy approval.`; const SAMPLE_PROJECT_COMMIT = `feat(task-api/P01-01-02): create registration endpoint ---ci--- phase: 1 milestone: v1.0 project: task-api plan: 01-01 task: 01-01-02 status: execute ---/ci--- Registration endpoint for task-api project.`; describe("extractCiBlock", () => { it("extracts ---ci--- block from commit message", () => { const block = extractCiBlock(SAMPLE_INIT_COMMIT); expect(block).toBeTruthy(); expect(block).toContain("phase: 0"); expect(block).toContain("milestone: v1.0"); }); it("returns null when no ---ci--- block exists", () => { const block = extractCiBlock("docs: some regular commit\n\nNo CI block here"); expect(block).toBeNull(); }); it("returns null for unclosed ---ci--- block", () => { const block = extractCiBlock("docs: bad\n---ci---\nphase: 1\nno end marker"); expect(block).toBeNull(); }); }); describe("parseCiBlock", () => { it("parses init commit ci block", () => { const block = extractCiBlock(SAMPLE_INIT_COMMIT)!; const meta = parseCiBlock(block)!; expect(meta.phase).toBe(0); expect(meta.milestone).toBe("v1.0"); expect(meta.status).toBe("specify"); expect(meta.decisions).toHaveLength(2); expect(meta.decisions![0].id).toBe("D-001"); expect(meta.decisions![0].decision).toBe("Node.js with Express for REST API"); expect(meta.decisions![0].confidence).toBe(0.95); expect(meta.decisions![0].alternatives).toEqual(["Fastify", "Hono"]); }); it("parses task commit ci block", () => { const block = extractCiBlock(SAMPLE_TASK_COMMIT)!; const meta = parseCiBlock(block)!; expect(meta.phase).toBe(1); expect(meta.plan).toBe("01-01"); expect(meta.task).toBe("01-01-02"); expect(meta.status).toBe("execute"); expect(meta.decisions).toHaveLength(1); expect(meta.decisions![0].id).toBe("D-003"); expect(meta.requirements).toBeDefined(); expect(meta.requirements!.covered).toEqual(["AUTH-01"]); }); it("parses phase completion with lessons", () => { const block = extractCiBlock(SAMPLE_PHASE_COMPLETE_COMMIT)!; const meta = parseCiBlock(block)!; expect(meta.phase).toBe(1); expect(meta.status).toBe("complete"); expect(meta.lessons).toHaveLength(2); expect(meta.lessons![0]).toContain("bcrypt async"); expect(meta.requirements!.covered).toEqual(["AUTH-01", "AUTH-02", "AUTH-03", "AUTH-04"]); }); it("parses compound commit", () => { const block = extractCiBlock(SAMPLE_COMPOUND_COMMIT)!; const meta = parseCiBlock(block)!; expect(meta.compound).toBeDefined(); expect(meta.compound!.category).toBe("auth"); expect(meta.compound!.problem).toContain("Refresh tokens can be replayed"); expect(meta.compound!.solution).toContain("refresh token rotation"); expect(meta.lessons).toHaveLength(2); }); it("parses escalation commit", () => { const block = extractCiBlock(SAMPLE_ESCALATION_COMMIT)!; const meta = parseCiBlock(block)!; expect(meta.escalations).toHaveLength(1); expect(meta.escalations![0].id).toBe("E-001"); expect(meta.escalations![0].type).toBe("irreversible_action"); expect(meta.escalations![0].resolution).toBe("pending"); }); it("parses project field", () => { const block = extractCiBlock(SAMPLE_PROJECT_COMMIT)!; const meta = parseCiBlock(block)!; expect(meta.project).toBe("task-api"); expect(meta.phase).toBe(1); expect(meta.plan).toBe("01-01"); }); it("returns null for empty block", () => { const meta = parseCiBlock(""); expect(meta).toBeNull(); }); it("returns null for block missing required fields", () => { const meta = parseCiBlock("something: true\nother: false"); expect(meta).toBeNull(); }); }); describe("parseCommitMessage", () => { it("parses init commit subject line", () => { const parsed = parseCommitMessage("abc123", SAMPLE_INIT_COMMIT); expect(parsed.hash).toBe("abc123"); expect(parsed.type).toBe("docs"); expect(parsed.scope).toBe("init"); expect(parsed.subject).toBe("initialize task-api (4 phases)"); expect(parsed.ci).not.toBeNull(); expect(parsed.ci!.phase).toBe(0); }); it("parses task commit with scope", () => { const parsed = parseCommitMessage("def456", SAMPLE_TASK_COMMIT); expect(parsed.type).toBe("feat"); expect(parsed.scope).toBe("P01-01-02"); expect(parsed.ci!.plan).toBe("01-01"); expect(parsed.ci!.task).toBe("01-01-02"); }); it("parses compound commit type", () => { const parsed = parseCommitMessage("ghi789", SAMPLE_COMPOUND_COMMIT); expect(parsed.type).toBe("compound"); expect(parsed.ci!.compound!.category).toBe("auth"); }); it("parses escalation commit type", () => { const parsed = parseCommitMessage("jkl012", SAMPLE_ESCALATION_COMMIT); expect(parsed.type).toBe("escalation"); expect(parsed.ci!.escalations![0].id).toBe("E-001"); }); it("handles commit without ci block", () => { const msg = "feat: some regular feature\n\nJust a normal commit."; const parsed = parseCommitMessage("mno345", msg); expect(parsed.type).toBe("feat"); expect(parsed.ci).toBeNull(); expect(parsed.body).toContain("Just a normal commit"); }); it("extracts body text outside ci block", () => { const parsed = parseCommitMessage("pqr678", SAMPLE_TASK_COMMIT); expect(parsed.body).toContain("POST /auth/register validates email and password"); }); it("parses commit with project-prefixed scope", () => { const parsed = parseCommitMessage("stu901", SAMPLE_PROJECT_COMMIT); expect(parsed.type).toBe("feat"); expect(parsed.scope).toBe("task-api/P01-01-02"); expect(parsed.ci!.project).toBe("task-api"); }); }); describe("parseCommitScope", () => { it("parses init scope", () => { const scope = parseCommitScope("init"); expect(scope.isInit).toBe(true); expect(scope.phase).toBe(0); }); it("parses milestone scope", () => { const scope = parseCommitScope("milestone"); expect(scope.isMilestone).toBe(true); expect(scope.phase).toBe(0); }); it("parses simple phase scope", () => { const scope = parseCommitScope("P01"); expect(scope.phase).toBe(1); expect(scope.isInit).toBe(false); expect(scope.isMilestone).toBe(false); }); it("parses task scope with plan and task", () => { const scope = parseCommitScope("P01-01-02"); expect(scope.phase).toBe(1); expect(scope.plan).toBe("01-01"); expect(scope.task).toBe("01-01-02"); }); it("parses project-prefixed scope", () => { const scope = parseCommitScope("task-api/P01-01-02"); expect(scope.project).toBe("task-api"); expect(scope.phase).toBe(1); expect(scope.plan).toBe("01-01"); expect(scope.task).toBe("01-01-02"); }); it("does not treat P-prefixed scope as project-prefixed", () => { const scope = parseCommitScope("P01-auth"); expect(scope.project).toBeUndefined(); expect(scope.phase).toBe(1); }); }); describe("formatCommitScope", () => { it("formats init scope", () => { const scope: CommitScope = { phase: 0, isInit: true, isMilestone: false }; expect(formatCommitScope(scope)).toBe("init"); }); it("formats milestone scope", () => { const scope: CommitScope = { phase: 0, isInit: false, isMilestone: true }; expect(formatCommitScope(scope)).toBe("milestone"); }); it("formats simple phase scope", () => { const scope: CommitScope = { phase: 1, isInit: false, isMilestone: false }; expect(formatCommitScope(scope)).toBe("P01"); }); it("formats task scope", () => { const scope: CommitScope = { phase: 1, plan: "01-01", task: "01-01-02", isInit: false, isMilestone: false }; expect(formatCommitScope(scope)).toBe("P01-01-02"); }); it("formats project-prefixed scope", () => { const scope: CommitScope = { phase: 1, project: "task-api", plan: "01-01", task: "01-01-02", isInit: false, isMilestone: false }; expect(formatCommitScope(scope)).toBe("task-api/P01-01-02"); }); it("formats project-prefixed phase scope without plan/task", () => { const scope: CommitScope = { phase: 2, project: "auth-svc", isInit: false, isMilestone: false }; expect(formatCommitScope(scope)).toBe("auth-svc/P02"); }); });