Some checks failed
✅ Check / 🔬 ESLint (push) Failing after 21s
✅ Check / 🏷️ Typecheck (push) Failing after 11s
✅ Check / 🔎 Test (push) Failing after 11s
✅ Check / e2e-test (push) Failing after 11s
✅ Check / 🔨 Build (push) Failing after 11s
🚀 Deploy / 🚀 Deploy (doc) (push) Failing after 12s
🚀 Deploy / 🚀 Deploy (demo) (push) Failing after 12s
🚀 Deploy / 🚀 Deploy (supabase) (push) Failing after 18s
158 lines
4.5 KiB
JavaScript
158 lines
4.5 KiB
JavaScript
// Tests for lib/config.mjs: the harness.config.json loader. It merges the file
|
|
// over the built-in defaults, validates the shape (fail-closed on malformed),
|
|
// and degrades to defaults when the file is missing.
|
|
|
|
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { dirname, join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { afterEach, describe, test, expect } from "vitest";
|
|
import {
|
|
loadConfig,
|
|
clearConfigCache,
|
|
CONFIG_FILENAME,
|
|
isDeployEnabled,
|
|
deployGlobs,
|
|
pipelineRoles,
|
|
debounceRoles,
|
|
roleModel,
|
|
worktreeProvision,
|
|
prePrSteps,
|
|
} from "../lib/config.mjs";
|
|
|
|
const REPO_ROOT = join(
|
|
dirname(fileURLToPath(import.meta.url)),
|
|
"..",
|
|
"..",
|
|
"..",
|
|
);
|
|
|
|
const tmpRepos = [];
|
|
const makeRepo = (config) => {
|
|
const dir = mkdtempSync(join(tmpdir(), "harness-config-"));
|
|
tmpRepos.push(dir);
|
|
if (config !== undefined) {
|
|
writeFileSync(
|
|
join(dir, CONFIG_FILENAME),
|
|
typeof config === "string" ? config : JSON.stringify(config),
|
|
);
|
|
}
|
|
clearConfigCache();
|
|
return dir;
|
|
};
|
|
|
|
const VALID = {
|
|
name: "sample",
|
|
validation: {
|
|
steps: [
|
|
{ id: "typecheck", kind: "typecheck", command: "npm run typecheck" },
|
|
{
|
|
id: "unit",
|
|
kind: "unit",
|
|
runner: "vitest",
|
|
config: "vitest.config.ts",
|
|
projects: ["app"],
|
|
},
|
|
],
|
|
},
|
|
roles: {
|
|
developer: { model: "sonnet", pipeline: true, debounce: true },
|
|
planner: { model: "opus", pipeline: true, debounce: false },
|
|
},
|
|
deploy: { adapter: "supabase", relevantGlobs: ["supabase/**"] },
|
|
};
|
|
|
|
afterEach(() => {
|
|
while (tmpRepos.length)
|
|
rmSync(tmpRepos.pop(), { recursive: true, force: true });
|
|
clearConfigCache();
|
|
});
|
|
|
|
describe("config loader", () => {
|
|
test("loads a valid config and applies defaults for absent keys", () => {
|
|
const cfg = loadConfig(makeRepo(VALID));
|
|
expect(cfg.name).toBe("sample");
|
|
// default filled in when absent
|
|
expect(worktreeProvision(cfg)).toBe("npm-link");
|
|
expect(cfg.launcher.sessionDirEnv).toBe("CHAT_SESSION_DIR");
|
|
});
|
|
|
|
test("missing file degrades to defaults (no deploy capability)", () => {
|
|
const cfg = loadConfig(makeRepo(undefined));
|
|
expect(isDeployEnabled(cfg)).toBe(false);
|
|
expect(cfg.validation.steps).toEqual([]);
|
|
});
|
|
|
|
test("typed readers reflect the config", () => {
|
|
const cfg = loadConfig(makeRepo(VALID));
|
|
expect(isDeployEnabled(cfg)).toBe(true);
|
|
expect(deployGlobs(cfg)).toEqual(["supabase/**"]);
|
|
expect(pipelineRoles(cfg).sort()).toEqual(["developer", "planner"]);
|
|
expect(debounceRoles(cfg)).toEqual(["developer"]);
|
|
expect(roleModel(cfg, "planner")).toBe("opus");
|
|
});
|
|
|
|
test("malformed JSON throws a clear, prefixed error", () => {
|
|
expect(() => loadConfig(makeRepo("{ not json"))).toThrow(
|
|
new RegExp(`${CONFIG_FILENAME}: invalid JSON`),
|
|
);
|
|
});
|
|
|
|
test("validation.steps not an array is rejected", () => {
|
|
expect(() =>
|
|
loadConfig(makeRepo({ validation: { steps: {} }, roles: {} })),
|
|
).toThrow(/validation\.steps` must be an array/);
|
|
});
|
|
|
|
test("a role missing a model is rejected", () => {
|
|
expect(() =>
|
|
loadConfig(
|
|
makeRepo({ validation: { steps: [] }, roles: { developer: {} } }),
|
|
),
|
|
).toThrow(/roles\.developer needs/);
|
|
});
|
|
|
|
test("deploy present but without relevantGlobs is rejected", () => {
|
|
expect(() =>
|
|
loadConfig(
|
|
makeRepo({ validation: { steps: [] }, roles: {}, deploy: {} }),
|
|
),
|
|
).toThrow(/deploy\.relevantGlobs/);
|
|
});
|
|
|
|
test("prePrSteps takes typecheck/lint but excludes changedScoped steps", () => {
|
|
const cfg = loadConfig(
|
|
makeRepo({
|
|
validation: {
|
|
steps: [
|
|
{
|
|
id: "typecheck",
|
|
kind: "typecheck",
|
|
command: "npm run typecheck",
|
|
},
|
|
{
|
|
id: "lint",
|
|
kind: "lint",
|
|
command: "npx eslint",
|
|
changedScoped: true,
|
|
},
|
|
],
|
|
},
|
|
roles: {},
|
|
}),
|
|
);
|
|
// typecheck (whole-repo-safe) is on the human push path; the diff-scoped lint
|
|
// step is not — it needs a per-worktree base the human path lacks.
|
|
expect(prePrSteps(cfg).map((s) => s.id)).toEqual(["typecheck"]);
|
|
});
|
|
|
|
test("the committed harness.config.json is valid", () => {
|
|
clearConfigCache();
|
|
const cfg = loadConfig(REPO_ROOT);
|
|
expect(cfg.name).toBe("atomic-crm");
|
|
expect(isDeployEnabled(cfg)).toBe(true);
|
|
expect(roleModel(cfg, "quality-reviewer")).toBe("opus");
|
|
expect(pipelineRoles(cfg)).toContain("test-writer");
|
|
});
|
|
});
|