feat: add persona binding algorithm

This commit is contained in:
chungyeong
2026-05-10 00:31:18 +09:00
parent 4a7fc94f5c
commit 0d90cd97b6
6 changed files with 1066 additions and 48 deletions

View File

@@ -1,9 +1,10 @@
import { mkdirSync, mkdtempSync, realpathSync, writeFileSync } from "node:fs";
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", () => {
@@ -49,7 +50,100 @@ describe("config loader", () => {
expect(config.backends).toContainEqual({ id: "fake", enabled: true });
});
it("parses backend registration from DEVFLOW_BACKENDS_JSON", () => {
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",
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",
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("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",
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);
@@ -60,15 +154,14 @@ describe("config loader", () => {
DATABASE_URL: "postgres://devflow:devflow@localhost:5432/devflow",
WORKSPACE_ROOT: workspace,
LOG_LEVEL: "info",
DEVFLOW_BACKENDS_JSON: JSON.stringify([
{ id: "codex", enabled: true, binaryPath: "/usr/local/bin/codex" },
]),
},
});
expect(config.backends).toEqual([
{ id: "fake", enabled: true },
{ id: "codex", enabled: true, binaryPath: "/usr/local/bin/codex" },
]);
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);
});
});