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); }); });