160 lines
5.6 KiB
TypeScript
160 lines
5.6 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
}); |