240 lines
7.7 KiB
TypeScript
240 lines
7.7 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import * as path from "node:path";
|
|
import test from "node:test";
|
|
import { loadWorkflowConfig } from "./config.ts";
|
|
import { DEFAULT_WORKFLOW_CONFIG, MAX_CONCURRENCY } from "./domain.ts";
|
|
|
|
async function withTempDir(run: (directory: string) => Promise<void>) {
|
|
const directory = await mkdtemp(path.join(tmpdir(), "pi-workflow-config-"));
|
|
try {
|
|
await run(directory);
|
|
} finally {
|
|
await rm(directory, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
async function writeJson(file: string, value: unknown) {
|
|
await mkdir(path.dirname(file), { recursive: true });
|
|
await writeFile(file, `${JSON.stringify(value)}\n`);
|
|
}
|
|
|
|
test("configuration defaults to concurrency 8 with immutable ceiling 32", async () => {
|
|
await withTempDir(async (directory) => {
|
|
const config = await loadWorkflowConfig({
|
|
cwd: directory,
|
|
agentDir: path.join(directory, "agent"),
|
|
projectTrusted: false,
|
|
});
|
|
assert.deepEqual(config, DEFAULT_WORKFLOW_CONFIG);
|
|
assert.equal(config.concurrency.ceiling, MAX_CONCURRENCY);
|
|
assert.deepEqual(config.standardAgents, {
|
|
enabled: true,
|
|
roles: {
|
|
scout: ["discover"],
|
|
worker: ["implement"],
|
|
},
|
|
});
|
|
});
|
|
});
|
|
|
|
test("a lowered ceiling clamps an inherited default", async () => {
|
|
await withTempDir(async (directory) => {
|
|
const agentDir = path.join(directory, "agent");
|
|
await writeJson(path.join(agentDir, "pi-workflow.json"), {
|
|
concurrency: { ceiling: 4 },
|
|
});
|
|
const globalOnly = await loadWorkflowConfig({
|
|
cwd: directory,
|
|
agentDir,
|
|
projectTrusted: false,
|
|
});
|
|
assert.deepEqual(globalOnly.concurrency, { default: 4, ceiling: 4 });
|
|
|
|
await writeJson(path.join(agentDir, "pi-workflow.json"), {
|
|
concurrency: { default: 6, ceiling: 16 },
|
|
});
|
|
await writeJson(path.join(directory, ".pi", "pi-workflow.json"), {
|
|
concurrency: { ceiling: 3 },
|
|
});
|
|
const project = await loadWorkflowConfig({
|
|
cwd: directory,
|
|
agentDir,
|
|
projectTrusted: true,
|
|
});
|
|
assert.deepEqual(project.concurrency, { default: 3, ceiling: 3 });
|
|
});
|
|
});
|
|
|
|
test("trusted project configuration layers over global configuration and may lower limits", async () => {
|
|
await withTempDir(async (directory) => {
|
|
const agentDir = path.join(directory, "agent");
|
|
await writeJson(path.join(agentDir, "pi-workflow.json"), {
|
|
concurrency: { default: 6, ceiling: 16 },
|
|
maxAssignmentsPerRun: 64,
|
|
analysisTools: ["read", "grep", "find"],
|
|
worktrees: { root: ".worktrees/global" },
|
|
});
|
|
await writeJson(path.join(directory, ".pi", "pi-workflow.json"), {
|
|
concurrency: { default: 4, ceiling: 8 },
|
|
maxReviewCycles: 5,
|
|
analysisTools: ["read", "find", "ls"],
|
|
worktrees: { root: ".worktrees/project" },
|
|
});
|
|
|
|
const config = await loadWorkflowConfig({
|
|
cwd: directory,
|
|
agentDir,
|
|
projectTrusted: true,
|
|
invocationConcurrency: 7,
|
|
});
|
|
assert.deepEqual(config.concurrency, { default: 7, ceiling: 8 });
|
|
assert.equal(config.maxAssignmentsPerRun, 64);
|
|
assert.equal(config.maxReviewCycles, 5);
|
|
assert.deepEqual(config.analysisTools, ["read", "find"]);
|
|
assert.equal(config.worktrees.root, path.normalize(".worktrees/project"));
|
|
});
|
|
});
|
|
|
|
test("untrusted project configuration is never applied", async () => {
|
|
await withTempDir(async (directory) => {
|
|
const agentDir = path.join(directory, "agent");
|
|
await writeJson(path.join(directory, ".pi", "pi-workflow.json"), {
|
|
concurrency: { default: 1, ceiling: 1 },
|
|
unknownField: true,
|
|
});
|
|
const config = await loadWorkflowConfig({
|
|
cwd: directory,
|
|
agentDir,
|
|
projectTrusted: false,
|
|
});
|
|
assert.deepEqual(config.concurrency, { default: 8, ceiling: 32 });
|
|
});
|
|
});
|
|
|
|
test("trusted configuration can replace or disable standard-agent role overlays", async () => {
|
|
await withTempDir(async (directory) => {
|
|
const agentDir = path.join(directory, "agent");
|
|
await writeJson(path.join(agentDir, "pi-workflow.json"), {
|
|
standardAgents: {
|
|
roles: {
|
|
"codebase-analyzer": ["discover"],
|
|
worker: ["implement", "verify"],
|
|
},
|
|
},
|
|
});
|
|
let config = await loadWorkflowConfig({
|
|
cwd: directory,
|
|
agentDir,
|
|
projectTrusted: false,
|
|
});
|
|
assert.deepEqual(config.standardAgents.roles, {
|
|
"codebase-analyzer": ["discover"],
|
|
worker: ["implement", "verify"],
|
|
});
|
|
|
|
await writeJson(path.join(directory, ".pi", "pi-workflow.json"), {
|
|
standardAgents: { enabled: false },
|
|
});
|
|
config = await loadWorkflowConfig({
|
|
cwd: directory,
|
|
agentDir,
|
|
projectTrusted: true,
|
|
});
|
|
assert.equal(config.standardAgents.enabled, false);
|
|
});
|
|
});
|
|
|
|
test("invalid standard-agent names and roles fail before a run starts", async () => {
|
|
await withTempDir(async (directory) => {
|
|
const agentDir = path.join(directory, "agent");
|
|
await writeJson(path.join(agentDir, "pi-workflow.json"), {
|
|
standardAgents: { roles: { Worker: ["implement"] } },
|
|
});
|
|
await assert.rejects(
|
|
loadWorkflowConfig({ cwd: directory, agentDir, projectTrusted: false }),
|
|
/lowercase kebab case/,
|
|
);
|
|
|
|
await writeJson(path.join(agentDir, "pi-workflow.json"), {
|
|
standardAgents: { roles: { worker: ["remediate"] } },
|
|
});
|
|
await assert.rejects(
|
|
loadWorkflowConfig({ cwd: directory, agentDir, projectTrusted: false }),
|
|
/unsupported role remediate/,
|
|
);
|
|
});
|
|
});
|
|
|
|
test("configuration cannot raise ceilings or activate unaudited tools", async () => {
|
|
await withTempDir(async (directory) => {
|
|
const agentDir = path.join(directory, "agent");
|
|
await writeJson(path.join(agentDir, "pi-workflow.json"), {
|
|
concurrency: { ceiling: 33 },
|
|
});
|
|
await assert.rejects(
|
|
loadWorkflowConfig({ cwd: directory, agentDir, projectTrusted: false }),
|
|
/between 1 and 32/,
|
|
);
|
|
|
|
await writeJson(path.join(agentDir, "pi-workflow.json"), {
|
|
analysisTools: ["read", "bash"],
|
|
});
|
|
await assert.rejects(
|
|
loadWorkflowConfig({ cwd: directory, agentDir, projectTrusted: false }),
|
|
/unsupported tools: bash/,
|
|
);
|
|
});
|
|
});
|
|
|
|
test("project configuration cannot raise a lowered global ceiling", async () => {
|
|
await withTempDir(async (directory) => {
|
|
const agentDir = path.join(directory, "agent");
|
|
await writeJson(path.join(agentDir, "pi-workflow.json"), {
|
|
concurrency: { default: 4, ceiling: 8 },
|
|
});
|
|
await writeJson(path.join(directory, ".pi", "pi-workflow.json"), {
|
|
concurrency: { ceiling: 16 },
|
|
});
|
|
await assert.rejects(
|
|
loadWorkflowConfig({ cwd: directory, agentDir, projectTrusted: true }),
|
|
/cannot raise the current ceiling/,
|
|
);
|
|
});
|
|
});
|
|
|
|
test("invalid numeric and path values fail before a run starts", async () => {
|
|
await withTempDir(async (directory) => {
|
|
const agentDir = path.join(directory, "agent");
|
|
await writeJson(path.join(agentDir, "pi-workflow.json"), {
|
|
maxAssignmentsPerRun: 1.5,
|
|
});
|
|
await assert.rejects(
|
|
loadWorkflowConfig({ cwd: directory, agentDir, projectTrusted: false }),
|
|
/must be an integer/,
|
|
);
|
|
|
|
await writeJson(path.join(agentDir, "pi-workflow.json"), {
|
|
worktrees: { root: "../outside" },
|
|
});
|
|
await assert.rejects(
|
|
loadWorkflowConfig({ cwd: directory, agentDir, projectTrusted: false }),
|
|
/must stay inside the repository/,
|
|
);
|
|
|
|
await writeJson(path.join(agentDir, "pi-workflow.json"), {
|
|
concurrency: { default: 8, ceiling: 8 },
|
|
});
|
|
await assert.rejects(
|
|
loadWorkflowConfig({
|
|
cwd: directory,
|
|
agentDir,
|
|
projectTrusted: false,
|
|
invocationConcurrency: 9,
|
|
}),
|
|
/between 1 and 8/,
|
|
);
|
|
});
|
|
});
|