Files
ci/src/core/git-context.test.ts
T
Jon Chery 4a58aa1657 refactor(rebrand): rename & rebrand CI → CIAgent across all source and test files
- 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---
2026-05-29 18:01:13 +00:00

323 lines
8.8 KiB
TypeScript

import { execSync } from "node:child_process";
import * as os from "node:os";
import * as path from "node:path";
import * as fs from "node:fs";
import { GitContext } from "../core/git-context.js";
function createTempRepo(): string {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-test-"));
execSync("git init", { cwd: dir, stdio: "pipe" });
execSync('git config user.email "test@test.com"', { cwd: dir, stdio: "pipe" });
execSync('git config user.name "Test"', { cwd: dir, stdio: "pipe" });
return dir;
}
function commit(repoDir: string, message: string): void {
const filePath = path.join(repoDir, `file-${Date.now()}.txt`);
fs.writeFileSync(filePath, "test", "utf-8");
execSync(`git add "${filePath}"`, { cwd: repoDir, stdio: "pipe" });
execSync(`git commit -m "${message.replace(/"/g, '\\"')}"`, { cwd: repoDir, stdio: "pipe" });
}
function cleanup(dir: string): void {
fs.rmSync(dir, { recursive: true, force: true });
}
describe("GitContext", () => {
let repoDir: string;
beforeEach(() => {
repoDir = createTempRepo();
});
afterEach(() => {
cleanup(repoDir);
});
describe("isGitRepo", () => {
it("returns true for a git repo", () => {
const ctx = new GitContext(repoDir);
expect(ctx.isGitRepo()).toBe(true);
});
it("returns false for non-git directory", () => {
const nonGit = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-nongit-"));
const ctx = new GitContext(nonGit);
expect(ctx.isGitRepo()).toBe(false);
cleanup(nonGit);
});
});
describe("getCurrentBranch", () => {
it("returns current branch name", () => {
const ctx = new GitContext(repoDir);
commit(repoDir, "initial");
expect(ctx.getCurrentBranch()).toBeTruthy();
});
});
describe("projectSlug", () => {
it("defaults to undefined", () => {
const ctx = new GitContext(repoDir);
expect(ctx.getProjectSlug()).toBeUndefined();
});
it("accepts project slug in constructor", () => {
const ctx = new GitContext(repoDir, "task-api");
expect(ctx.getProjectSlug()).toBe("task-api");
});
it("setProjectSlug updates slug", () => {
const ctx = new GitContext(repoDir);
ctx.setProjectSlug("auth-svc");
expect(ctx.getProjectSlug()).toBe("auth-svc");
});
});
describe("getRecentCommits", () => {
it("returns parsed commits with ci blocks", () => {
commit(repoDir, `docs(init): initialize project
---ci---
phase: 0
milestone: v1.0
status: specify
---/ci---
Initial commit`);
const ctx = new GitContext(repoDir);
const commits = ctx.getRecentCommits(5);
expect(commits.length).toBeGreaterThanOrEqual(1);
const ciCommit = commits.find((c) => c.ci !== null);
expect(ciCommit).toBeDefined();
expect(ciCommit!.ci!.phase).toBe(0);
expect(ciCommit!.ci!.milestone).toBe("v1.0");
});
it("returns commits without ci blocks as null ci", () => {
commit(repoDir, "feat: some feature");
const ctx = new GitContext(repoDir);
const commits = ctx.getRecentCommits(5);
expect(commits.length).toBeGreaterThanOrEqual(1);
expect(commits[0].ci).toBeNull();
});
});
describe("reconstructState", () => {
it("reconstructs state from latest ci commit", () => {
commit(repoDir, `docs(init): initialize project
---ci---
phase: 1
milestone: v1.0
status: execute
---/ci---`);
const ctx = new GitContext(repoDir);
const state = ctx.reconstructState();
expect(state.currentPhase).toBe(1);
expect(state.currentMilestone).toBe("v1.0");
expect(state.currentStage).toBe("execute");
});
it("returns default state without ci commits", () => {
commit(repoDir, "feat: some regular feature");
const ctx = new GitContext(repoDir);
const state = ctx.reconstructState();
expect(state.currentPhase).toBe(0);
expect(state.currentStage).toBe("specify");
});
});
describe("getDecisionsFromCommits", () => {
it("extracts decisions from parsed commits", () => {
commit(repoDir, `feat(P01): add auth
---ci---
phase: 1
milestone: v1.0
status: execute
decisions:
- id: D-001
decision: Use bcrypt
rationale: Industry standard
confidence: 0.90
alternatives: [argon2]
---/ci---`);
const ctx = new GitContext(repoDir);
const commits = ctx.getRecentCommits(5);
const decisions = ctx.getDecisionsFromCommits(commits);
expect(decisions).toHaveLength(1);
expect(decisions[0].id).toBe("D-001");
expect(decisions[0].decision).toBe("Use bcrypt");
});
});
describe("getLessons", () => {
it("extracts lessons from commits", () => {
commit(repoDir, `docs(P01): complete phase
---ci---
phase: 1
milestone: v1.0
status: complete
lessons:
- Never use sync bcrypt
- Always check JWT expiry first
---/ci---`);
const ctx = new GitContext(repoDir);
const commits = ctx.getRecentCommits(10);
const lessons: string[] = [];
for (const commit of commits) {
if (commit.ci?.lessons) lessons.push(...commit.ci.lessons);
}
expect(lessons).toContain("Never use sync bcrypt");
expect(lessons).toContain("Always check JWT expiry first");
});
});
describe("getBranches", () => {
it("lists branches with type info", () => {
commit(repoDir, "initial");
execSync("git checkout -b phase/01-auth", { cwd: repoDir, stdio: "pipe" });
commit(repoDir, "feat: auth work");
execSync("git checkout -b milestone/v1.0-mvp", { cwd: repoDir, stdio: "pipe" });
commit(repoDir, "feat: milestone work");
const ctx = new GitContext(repoDir);
const branches = ctx.getBranches();
const phaseBranches = branches.filter((b) => b.type === "phase");
const milestoneBranches = branches.filter((b) => b.type === "milestone");
expect(phaseBranches.length).toBeGreaterThanOrEqual(1);
expect(milestoneBranches.length).toBeGreaterThanOrEqual(1);
expect(phaseBranches[0].phaseNumber).toBe(1);
});
it("strips project prefix when projectSlug is set", () => {
commit(repoDir, "initial");
execSync("git checkout -b task-api/phase/01-auth", { cwd: repoDir, stdio: "pipe" });
commit(repoDir, "feat: auth work");
const ctx = new GitContext(repoDir, "task-api");
const branches = ctx.getBranches();
const phaseBranches = branches.filter((b) => b.type === "phase");
expect(phaseBranches.length).toBeGreaterThanOrEqual(1);
expect(phaseBranches[0].phaseNumber).toBe(1);
expect(phaseBranches[0].name).toBe("task-api/phase/01-auth");
});
});
describe("detectProjectFromCommit", () => {
it("detects project from ci block project field", () => {
commit(repoDir, `feat(P01): task work
---ci---
phase: 1
milestone: v1.0
project: task-api
status: execute
---/ci---`);
const ctx = new GitContext(repoDir);
expect(ctx.detectProjectFromCommit()).toBe("task-api");
});
it("detects project from branch prefix", () => {
commit(repoDir, "initial");
execSync("git checkout -b auth-svc/phase/01-auth", { cwd: repoDir, stdio: "pipe" });
commit(repoDir, "feat: auth work");
const ctx = new GitContext(repoDir);
expect(ctx.detectProjectFromCommit()).toBe("auth-svc");
});
it("returns null when no project detected", () => {
commit(repoDir, "feat: some work");
const ctx = new GitContext(repoDir);
expect(ctx.detectProjectFromCommit()).toBeNull();
});
});
describe("isNfrMilestone", () => {
it("returns true when no feat commits exist", () => {
commit(repoDir, `chore(P01): cleanup
---ci---
phase: 1
milestone: v0.1.1
status: execute
---/ci---`);
const ctx = new GitContext(repoDir);
expect(ctx.isNfrMilestone()).toBe(true);
});
it("returns false when feat commits exist", () => {
commit(repoDir, `feat(P01): add feature
---ci---
phase: 1
milestone: v1.0
status: execute
---/ci---`);
const ctx = new GitContext(repoDir);
expect(ctx.isNfrMilestone()).toBe(false);
});
});
describe("getMilestoneType", () => {
it("returns nfr when only NFR commits exist", () => {
commit(repoDir, `chore(P01): cleanup
---ci---
phase: 1
milestone: v0.1.1
status: execute
---/ci---`);
const ctx = new GitContext(repoDir);
expect(ctx.getMilestoneType()).toBe("nfr");
});
it("returns feature when feat commits exist", () => {
commit(repoDir, `feat(P01): add feature
---ci---
phase: 1
milestone: v1.0
status: execute
---/ci---`);
const ctx = new GitContext(repoDir);
expect(ctx.getMilestoneType()).toBe("feature");
});
it("returns schema-breaking when refactor commits exist", () => {
commit(repoDir, `refactor(P01): rewrite core
---ci---
phase: 1
milestone: v0.5
status: execute
---/ci---`);
const ctx = new GitContext(repoDir);
expect(ctx.getMilestoneType()).toBe("schema-breaking");
});
});
});