import * as fs from "node:fs"; import * as path from "node:path"; import * as os from "node:os"; import { initCI, loadConfig, saveConfig, isCIInitialized, ensureCIDir } from "../core/config.js"; import { DEFAULT_CI_CONFIG } from "../types/config.js"; describe("CI Config", () => { let tempDir: string; beforeEach(() => { tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ci-config-test-")); }); afterEach(() => { fs.rmSync(tempDir, { recursive: true, force: true }); }); describe("initCI", () => { it("initializes a new CI project with default config", () => { const config = initCI(tempDir); expect(config.autonomy.level).toBe("full"); expect(isCIInitialized(tempDir)).toBe(true); }); it("initializes with custom config merged on top of defaults", () => { const config = initCI(tempDir, { autonomy: { ...DEFAULT_CI_CONFIG.autonomy, level: "guided" }, }); expect(config.autonomy.level).toBe("guided"); expect(config.autonomy.clarify_budget).toBe(10); expect(config.model_profile).toBe("quality"); }); it("creates .ci/ directory structure", () => { initCI(tempDir); expect(fs.existsSync(path.join(tempDir, ".ci"))).toBe(true); expect(fs.existsSync(path.join(tempDir, ".ci", "config.json"))).toBe(true); }); it("deep merges nested config", () => { const config = initCI(tempDir, { autonomy: { ...DEFAULT_CI_CONFIG.autonomy, level: "supervised" }, }); expect(config.autonomy.level).toBe("supervised"); expect(config.autonomy.max_revision_iterations).toBe(3); expect(config.autonomy.escalation_hooks).toEqual(["deploy", "delete_data", "merge_to_main"]); }); it("initializes with project slug", () => { const config = initCI(tempDir, undefined, "task-api", "Task API"); expect(config.projects).toHaveLength(1); expect(config.projects[0].slug).toBe("task-api"); expect(config.projects[0].name).toBe("Task API"); expect(config.projects[0].default).toBe(true); expect(config.active_project).toBe("task-api"); }); it("does not re-add existing project slug", () => { initCI(tempDir, undefined, "task-api", "Task API"); const config = initCI(tempDir, undefined, "task-api", "Task API V2"); expect(config.projects).toHaveLength(1); }); it("defaults projects and active_project when no slug provided", () => { const config = initCI(tempDir); expect(config.projects).toEqual([]); expect(config.active_project).toBe(""); }); it("preserves existing projects when adding new one", () => { const config1 = initCI(tempDir, undefined, "task-api", "Task API"); const config2 = initCI(tempDir, { ...config1, projects: [...config1.projects, { slug: "auth-svc", name: "Auth Service" }], }, "auth-svc", "Auth Service"); expect(config2.projects).toHaveLength(2); expect(config2.active_project).toBe("auth-svc"); }); }); describe("loadConfig", () => { it("returns default config when no config file exists", () => { const config = loadConfig(tempDir); expect(config).toEqual(DEFAULT_CI_CONFIG); }); it("loads and deep merges config from file", () => { initCI(tempDir, { autonomy: { ...DEFAULT_CI_CONFIG.autonomy, decision_confidence_threshold: 0.85 } }); const config = loadConfig(tempDir); expect(config.autonomy.decision_confidence_threshold).toBe(0.85); expect(config.autonomy.level).toBe("full"); expect(config.autonomy.clarify_budget).toBe(10); }); it("preserves nested objects that are not overridden", () => { initCI(tempDir, { git: { ...DEFAULT_CI_CONFIG.git, auto_push: true } }); const config = loadConfig(tempDir); expect(config.git.auto_push).toBe(true); expect(config.git.auto_commit).toBe(true); expect(config.git.branching_strategy).toBe("phase"); }); it("loads projects array from config", () => { initCI(tempDir, undefined, "task-api", "Task API"); const config = loadConfig(tempDir); expect(config.projects).toHaveLength(1); expect(config.active_project).toBe("task-api"); }); }); describe("saveConfig", () => { it("saves and reloads config correctly", () => { ensureCIDir(tempDir); const customConfig = { ...DEFAULT_CI_CONFIG, autonomy: { ...DEFAULT_CI_CONFIG.autonomy, level: "guided" as const }, }; saveConfig(tempDir, customConfig); const loaded = loadConfig(tempDir); expect(loaded.autonomy.level).toBe("guided"); }); it("saves and reloads config with projects", () => { ensureCIDir(tempDir); const config = { ...DEFAULT_CI_CONFIG, projects: [{ slug: "my-app", name: "My App", default: true }], active_project: "my-app", }; saveConfig(tempDir, config); const loaded = loadConfig(tempDir); expect(loaded.projects).toHaveLength(1); expect(loaded.active_project).toBe("my-app"); }); }); describe("isCIInitialized", () => { it("returns false for uninitialized directory", () => { expect(isCIInitialized(tempDir)).toBe(false); }); it("returns true after initCI", () => { initCI(tempDir); expect(isCIInitialized(tempDir)).toBe(true); }); }); describe("ensureCIDir", () => { it("creates .ci directory", () => { ensureCIDir(tempDir); expect(fs.existsSync(path.join(tempDir, ".ci"))).toBe(true); }); it("is idempotent", () => { ensureCIDir(tempDir); ensureCIDir(tempDir); expect(fs.existsSync(path.join(tempDir, ".ci"))).toBe(true); }); }); });