Files
dev-puppeteer/packages/core/src/config.test.ts
2026-05-14 00:14:27 +09:00

211 lines
6.5 KiB
TypeScript

import { chmodSync, mkdirSync, mkdtempSync, realpathSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { loadConfigFromSources } from "./config.js";
import { DevflowError } from "./errors.js";
describe("config loader", () => {
it("loads .env, .env.local, then process env in descending precedence", () => {
const root = mkdtempSync(join(tmpdir(), "devflow-config-"));
const workspace = join(root, "workspace");
mkdirSync(workspace);
writeFileSync(
join(root, ".env"),
[
"DATABASE_URL=postgres://env:env@localhost:5432/env",
"WORKSPACE_ROOT=workspace",
"LOG_LEVEL=warn",
"TEMPORAL_ADDRESS=localhost:7233",
].join("\n"),
);
writeFileSync(join(root, ".env.local"), "LOG_LEVEL=debug\n");
const config = loadConfigFromSources({
cwd: root,
env: {
DATABASE_URL: "postgres://process:process@localhost:5432/process",
},
});
expect(config.DATABASE_URL).toBe("postgres://process:process@localhost:5432/process");
expect(config.LOG_LEVEL).toBe("debug");
expect(config.WORKSPACE_ROOT).toBe(realpathSync(workspace));
});
it("always exposes the fake backend as enabled", () => {
const root = mkdtempSync(join(tmpdir(), "devflow-config-"));
const workspace = join(root, "workspace");
mkdirSync(workspace);
const config = loadConfigFromSources({
cwd: root,
env: {
DATABASE_URL: "postgres://devflow:devflow@localhost:5432/devflow",
WORKSPACE_ROOT: workspace,
LOG_LEVEL: "info",
TEMPORAL_ADDRESS: "localhost:7233",
},
});
expect(config.backends).toContainEqual({ id: "fake", enabled: true });
expect(config.SESSION_MAX_HUNG_MS).toBe(20 * 60 * 1000);
});
it("loads configurable session hung timeout", () => {
const root = mkdtempSync(join(tmpdir(), "devflow-config-"));
const workspace = join(root, "workspace");
mkdirSync(workspace);
const config = loadConfigFromSources({
cwd: root,
env: {
DATABASE_URL: "postgres://devflow:devflow@localhost:5432/devflow",
WORKSPACE_ROOT: workspace,
LOG_LEVEL: "info",
TEMPORAL_ADDRESS: "localhost:7233",
SESSION_MAX_HUNG_MS: "2500",
},
});
expect(config.SESSION_MAX_HUNG_MS).toBe(2500);
});
it("resolves backend binaries from PATH during config load", () => {
const root = mkdtempSync(join(tmpdir(), "devflow-config-"));
const workspace = join(root, "workspace");
const binDir = join(root, "bin");
const codexBin = join(binDir, "codex");
mkdirSync(workspace);
mkdirSync(binDir);
writeFileSync(codexBin, "#!/bin/sh\nexit 0\n");
chmodSync(codexBin, 0o755);
const config = loadConfigFromSources({
cwd: root,
env: {
DATABASE_URL: "postgres://devflow:devflow@localhost:5432/devflow",
WORKSPACE_ROOT: workspace,
LOG_LEVEL: "info",
TEMPORAL_ADDRESS: "localhost:7233",
PATH: binDir,
DEVFLOW_BACKENDS_JSON: JSON.stringify([{ id: "codex", enabled: true }]),
},
});
expect(config.backends).toEqual([
{ id: "fake", enabled: true },
{ id: "codex", enabled: true, binaryPath: realpathSync(codexBin) },
]);
});
it("keeps enabled real backends unavailable when their binary cannot be resolved", () => {
const root = mkdtempSync(join(tmpdir(), "devflow-config-"));
const workspace = join(root, "workspace");
const emptyBin = join(root, "empty-bin");
mkdirSync(workspace);
mkdirSync(emptyBin);
const config = loadConfigFromSources({
cwd: root,
env: {
DATABASE_URL: "postgres://devflow:devflow@localhost:5432/devflow",
WORKSPACE_ROOT: workspace,
LOG_LEVEL: "info",
TEMPORAL_ADDRESS: "localhost:7233",
PATH: emptyBin,
DEVFLOW_BACKENDS_JSON: JSON.stringify([{ id: "codex", enabled: true }]),
},
});
expect(config.backends).toEqual([
{ id: "fake", enabled: true },
{ id: "codex", enabled: true },
]);
});
it("requires LOG_LEVEL and classifies invalid config as fatal", () => {
const root = mkdtempSync(join(tmpdir(), "devflow-config-"));
const workspace = join(root, "workspace");
mkdirSync(workspace);
let caught: unknown;
try {
loadConfigFromSources({
cwd: root,
env: {
DATABASE_URL: "postgres://devflow:devflow@localhost:5432/devflow",
WORKSPACE_ROOT: workspace,
},
});
} catch (error) {
caught = error;
}
expect(caught).toBeInstanceOf(DevflowError);
expect((caught as DevflowError).class).toBe("fatal");
expect((caught as DevflowError).code).toBe("config_invalid");
expect((caught as DevflowError).cause).toBeDefined();
});
it("requires TEMPORAL_ADDRESS at M5", () => {
const root = mkdtempSync(join(tmpdir(), "devflow-config-"));
const workspace = join(root, "workspace");
mkdirSync(workspace);
expect(() =>
loadConfigFromSources({
cwd: root,
env: {
DATABASE_URL: "postgres://devflow:devflow@localhost:5432/devflow",
WORKSPACE_ROOT: workspace,
LOG_LEVEL: "info",
},
}),
).toThrow(DevflowError);
});
it("classifies malformed backend JSON as invalid config", () => {
const root = mkdtempSync(join(tmpdir(), "devflow-config-"));
const workspace = join(root, "workspace");
mkdirSync(workspace);
expect(() =>
loadConfigFromSources({
cwd: root,
env: {
DATABASE_URL: "postgres://devflow:devflow@localhost:5432/devflow",
WORKSPACE_ROOT: workspace,
LOG_LEVEL: "info",
TEMPORAL_ADDRESS: "localhost:7233",
DEVFLOW_BACKENDS_JSON: "{",
},
}),
).toThrow(DevflowError);
});
it("freezes config and backend registrations", () => {
const root = mkdtempSync(join(tmpdir(), "devflow-config-"));
const workspace = join(root, "workspace");
mkdirSync(workspace);
const config = loadConfigFromSources({
cwd: root,
env: {
DATABASE_URL: "postgres://devflow:devflow@localhost:5432/devflow",
WORKSPACE_ROOT: workspace,
LOG_LEVEL: "info",
TEMPORAL_ADDRESS: "localhost:7233",
},
});
expect(Object.isFrozen(config)).toBe(true);
expect(Object.isFrozen(config.backends)).toBe(true);
expect(Object.isFrozen(config.backends[0])).toBe(true);
expect(() => {
(config.backends[0] as { enabled: boolean }).enabled = false;
}).toThrow(TypeError);
});
});