Files
harness-tools/extensions/workflow/profiles.test.ts
T

700 lines
22 KiB
TypeScript

import assert from "node:assert/strict";
import { mkdir, mkdtemp, rm, symlink, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import * as path from "node:path";
import test from "node:test";
import type { AgentProfile, WorkflowFacts } from "./domain.ts";
import {
eligibleProfiles,
loadAgentProfiles,
selectInitialDiscoveryProfile,
} from "./profiles.ts";
async function withTempDir(run: (directory: string) => Promise<void>) {
const directory = await mkdtemp(path.join(tmpdir(), "workflow-profiles-"));
try {
await run(directory);
} finally {
await rm(directory, { recursive: true, force: true });
}
}
function profileDocument(options: {
name: string;
description?: string;
roles?: string;
selection?: string;
extra?: string;
body?: string;
}) {
return `---\nname: ${options.name}\ndescription: ${options.description ?? options.name}\nroles: ${options.roles ?? "[discover]"}\n${options.extra ?? ""}selection:\n${options.selection ?? " stages: [discover]\n anyTags: []\n allTags: []\n pathGlobs: []\n keywords: []\n"}---\n\n${options.body ?? "Follow the assignment."}\n`;
}
function standardAgentDocument(options: {
name: string;
description?: string;
model?: string;
tools?: string;
thinking?: string;
body?: string;
}) {
return `---\nname: ${options.name}\ndescription: ${options.description ?? options.name}\n${options.model ? `model: ${options.model}\n` : ""}${options.tools ? `tools: ${options.tools}\n` : ""}${options.thinking ? `thinking: ${options.thinking}\n` : ""}subagent_agents: scout, researcher\nsystem-prompt: append\nauto-exit: true\n---\n\n${options.body ?? "Follow the standard assignment."}\n`;
}
async function writeProfile(
root: string,
file: string,
options: Parameters<typeof profileDocument>[0],
) {
await mkdir(root, { recursive: true });
await writeFile(path.join(root, file), profileDocument(options));
}
async function writeStandardAgent(
root: string,
file: string,
options: Parameters<typeof standardAgentDocument>[0],
) {
await mkdir(root, { recursive: true });
await writeFile(path.join(root, file), standardAgentDocument(options));
}
test("bundled profiles form a complete deterministic catalog", async () => {
await withTempDir(async (directory) => {
const catalog = await loadAgentProfiles({
cwd: directory,
agentDir: path.join(directory, "agent"),
projectTrusted: false,
});
assert.deepEqual(
catalog.profiles.map((profile) => profile.name),
[
"feature-designer",
"feature-implementer",
"feature-verifier",
"general-discovery",
"independent-reviewer",
"ui-prototyper",
],
);
assert.equal(catalog.diagnostics.length, 0);
assert.ok(
catalog.profiles.every((profile) => /^[a-f0-9]{64}$/.test(profile.digest)),
);
});
});
test("standard package, global, and trusted project agents use role overlays and precedence", async () => {
await withTempDir(async (directory) => {
const bundled = path.join(directory, "bundled");
const packageRoot = path.join(directory, "package-agents");
const agentDir = path.join(directory, "agent");
const globalRoot = path.join(agentDir, "agents");
const projectRoot = path.join(directory, ".pi", "agents");
await writeProfile(bundled, "general.md", {
name: "general-discovery",
});
await writeStandardAgent(packageRoot, "scout.md", {
name: "scout",
model: "openai-codex/gpt-test",
tools: "read, grep, find, ls",
thinking: "high",
body: "Package scout.",
});
await writeStandardAgent(packageRoot, "worker.md", {
name: "worker",
tools: "read, write, edit, bash",
body: "Package worker.",
});
await writeStandardAgent(globalRoot, "scout.md", {
name: "scout",
body: "Global scout.",
});
await writeStandardAgent(globalRoot, "unmapped.md", {
name: "unmapped",
});
await writeStandardAgent(projectRoot, "worker.md", {
name: "worker",
body: "Project worker.",
});
const catalog = await loadAgentProfiles({
cwd: directory,
agentDir,
bundledRoot: bundled,
standardPackageRoot: packageRoot,
projectRoot,
projectTrusted: true,
standardAgents: {
enabled: true,
roles: { scout: ["discover"], worker: ["implement"] },
},
});
const scout = catalog.profiles.find((profile) => profile.name === "scout");
assert.equal(scout?.source, "standard-global");
assert.equal(scout?.instructions, "Global scout.");
assert.deepEqual(scout?.roles, ["discover"]);
assert.deepEqual(scout?.selection.stages, ["discover"]);
const worker = catalog.profiles.find((profile) => profile.name === "worker");
assert.equal(worker?.source, "standard-project");
assert.equal(worker?.instructions, "Project worker.");
assert.deepEqual(worker?.roles, ["implement"]);
assert.equal(catalog.profiles.some((profile) => profile.name === "unmapped"), false);
});
});
test("package and global standard definitions cannot self-declare workflow roles", async () => {
await withTempDir(async (directory) => {
const bundled = path.join(directory, "bundled");
const packageRoot = path.join(directory, "package-agents");
await writeProfile(bundled, "general.md", { name: "general-discovery" });
await writeProfile(packageRoot, "worker.md", {
name: "worker",
roles: "[review]",
selection:
" stages: [review]\n anyTags: []\n allTags: []\n pathGlobs: []\n keywords: []\n",
body: "Attempted self-declared reviewer.",
});
await writeProfile(packageRoot, "unmapped.md", {
name: "unmapped",
roles: "[review]",
selection:
" stages: [review]\n anyTags: []\n allTags: []\n pathGlobs: []\n keywords: []\n",
});
const catalog = await loadAgentProfiles({
cwd: directory,
agentDir: path.join(directory, "agent"),
bundledRoot: bundled,
standardPackageRoot: packageRoot,
projectTrusted: false,
standardAgents: {
enabled: true,
roles: { worker: ["implement"] },
},
});
const worker = catalog.profiles.find((profile) => profile.name === "worker");
assert.equal(worker?.source, "standard-package");
assert.deepEqual(worker?.roles, ["implement"]);
assert.deepEqual(worker?.selection.stages, ["implement"]);
assert.equal(catalog.profiles.some((profile) => profile.name === "unmapped"), false);
});
});
test("a trusted workflow profile keeps its declared roles when its name also has a standard overlay", async () => {
await withTempDir(async (directory) => {
const bundled = path.join(directory, "bundled");
const packageRoot = path.join(directory, "package-agents");
const projectRoot = path.join(directory, ".pi", "agents");
await writeProfile(bundled, "general.md", { name: "general-discovery" });
await writeStandardAgent(packageRoot, "scout.md", { name: "scout" });
await writeProfile(projectRoot, "scout.md", {
name: "scout",
roles: "[verify]",
selection:
" stages: [verify]\n anyTags: []\n allTags: []\n pathGlobs: []\n keywords: []\n",
body: "Project verifier.",
});
const catalog = await loadAgentProfiles({
cwd: directory,
agentDir: path.join(directory, "agent"),
bundledRoot: bundled,
standardPackageRoot: packageRoot,
projectRoot,
projectTrusted: true,
standardAgents: {
enabled: true,
roles: { scout: ["discover"] },
},
});
const scout = catalog.profiles.find((profile) => profile.name === "scout");
assert.equal(scout?.source, "project");
assert.deepEqual(scout?.roles, ["verify"]);
assert.deepEqual(scout?.selection.stages, ["verify"]);
assert.equal(scout?.instructions, "Project verifier.");
});
});
test("standard agents are disabled explicitly and ignored for untrusted projects", async () => {
await withTempDir(async (directory) => {
const bundled = path.join(directory, "bundled");
const packageRoot = path.join(directory, "package-agents");
const projectRoot = path.join(directory, ".pi", "agents");
await writeProfile(bundled, "general.md", { name: "general-discovery" });
await writeStandardAgent(packageRoot, "scout.md", { name: "scout" });
await writeStandardAgent(projectRoot, "worker.md", { name: "worker" });
const disabled = await loadAgentProfiles({
cwd: directory,
agentDir: path.join(directory, "agent"),
bundledRoot: bundled,
standardPackageRoot: packageRoot,
projectRoot,
projectTrusted: true,
standardAgents: {
enabled: false,
roles: { scout: ["discover"], worker: ["implement"] },
},
});
assert.deepEqual(disabled.profiles.map((profile) => profile.name), ["general-discovery"]);
});
});
test("trusted project profiles override bundled profiles while first duplicate wins", async () => {
await withTempDir(async (directory) => {
const bundled = path.join(directory, "bundled");
const project = path.join(directory, "project");
await writeProfile(bundled, "10-shared.md", {
name: "shared",
description: "bundled first",
});
await writeProfile(bundled, "20-shared.md", {
name: "shared",
description: "bundled duplicate",
});
await writeProfile(project, "10-shared.md", {
name: "shared",
description: "project override",
});
await writeFile(path.join(project, "20-broken.md"), "not frontmatter");
const untrusted = await loadAgentProfiles({
cwd: directory,
bundledRoot: bundled,
projectRoot: project,
projectTrusted: false,
});
assert.equal(untrusted.profiles[0]?.description, "bundled first");
assert.equal(untrusted.diagnostics.length, 1);
assert.doesNotMatch(untrusted.diagnostics[0]?.sourcePath ?? "", /project/);
const trusted = await loadAgentProfiles({
cwd: directory,
bundledRoot: bundled,
projectRoot: project,
projectTrusted: true,
});
assert.equal(trusted.profiles[0]?.description, "project override");
assert.equal(trusted.profiles[0]?.source, "project");
assert.ok(
trusted.diagnostics.some((item) =>
/duplicate profile/.test(item.message),
),
);
assert.ok(
trusted.diagnostics.some((item) =>
/missing YAML frontmatter/.test(item.message),
),
);
});
});
test("malformed profiles are isolated and unknown fields produce warnings", async () => {
await withTempDir(async (directory) => {
const bundled = path.join(directory, "bundled");
await writeProfile(bundled, "good.md", {
name: "good-profile",
description: "Coordinates https://example.test: path",
roles: "\n - discover",
extra:
"unknownField: ignored\nmetadata:\n owner: team\nlabels:\n - internal\n",
selection:
" stages:\n - discover\n anyTags: []\n allTags: []\n pathGlobs: []\n keywords: []\n unknownSelection:\n owner: team\n",
});
await writeFile(
path.join(bundled, "quoted.md"),
String.raw`---
name: quoted-profile
description: "Path C:\\" # comment
roles: [discover]
selection:
stages: [discover]
anyTags: []
allTags: []
pathGlobs: []
keywords: ["path\\", dashboard] # comment
---
Quoted values remain valid.
`,
);
await writeFile(
path.join(bundled, "single.md"),
String.raw`---
name: single-profile
description: 'Path C:\' # comment
roles: [discover]
selection:
stages: [discover]
anyTags: []
allTags: []
pathGlobs: []
keywords: ['path\', dashboard] # comment
---
Single-quoted values keep literal backslashes.
`,
);
await writeProfile(bundled, "bad-provider.md", {
name: "bad-provider",
extra: "model: provider/model\nprovider: provider\n",
});
await writeProfile(bundled, "bad-name.md", { name: "Bad Name" });
const catalog = await loadAgentProfiles({
cwd: directory,
bundledRoot: bundled,
projectTrusted: false,
});
assert.deepEqual(
catalog.profiles.map((profile) => profile.name),
["good-profile", "quoted-profile", "single-profile"],
);
assert.equal(
catalog.profiles[0]?.description,
"Coordinates https://example.test: path",
);
assert.deepEqual(catalog.profiles[1]?.selection.keywords, [
"path\\",
"dashboard",
]);
assert.deepEqual(catalog.profiles[2]?.selection.keywords, [
"path\\",
"dashboard",
]);
assert.ok(
catalog.diagnostics.some(
(item) =>
item.severity === "warning" && /unknown profile/.test(item.message),
),
);
assert.ok(
catalog.diagnostics.some((item) =>
/unknown selection/.test(item.message),
),
);
assert.ok(
catalog.diagnostics.some((item) =>
/provider requires/.test(item.message),
),
);
assert.ok(
catalog.diagnostics.some((item) => /lowercase kebab/.test(item.message)),
);
});
});
test("a symlinked profile root is rejected", async () => {
await withTempDir(async (directory) => {
const actual = path.join(directory, "actual");
const linked = path.join(directory, "linked");
await writeProfile(actual, "agent.md", { name: "external-root" });
await symlink(actual, linked);
await assert.rejects(
loadAgentProfiles({
cwd: directory,
bundledRoot: linked,
projectTrusted: false,
}),
/profile root must not be a symlink/,
);
});
});
test("a symlinked project profile parent is rejected", async () => {
await withTempDir(async (directory) => {
const repository = path.join(directory, "repository");
const bundled = path.join(directory, "bundled");
const externalPi = path.join(directory, "external-pi");
await mkdir(repository, { recursive: true });
await writeProfile(bundled, "agent.md", { name: "bundled-agent" });
await writeProfile(path.join(externalPi, "agents"), "agent.md", {
name: "external-project",
});
await symlink(externalPi, path.join(repository, ".pi"));
await assert.rejects(
loadAgentProfiles({
cwd: repository,
bundledRoot: bundled,
projectTrusted: true,
}),
/project profile ancestry must not be a symlink/,
);
});
});
test("profile files and symlinks must resolve inside their source root", async () => {
await withTempDir(async (directory) => {
const bundled = path.join(directory, "bundled");
const outside = path.join(directory, "outside.md");
await mkdir(bundled, { recursive: true });
await writeFile(
path.join(bundled, "internal.profile"),
profileDocument({ name: "internal-link" }),
);
await writeFile(outside, profileDocument({ name: "outside-link" }));
await symlink("internal.profile", path.join(bundled, "internal.md"));
await symlink(outside, path.join(bundled, "outside.md"));
await symlink("missing.profile", path.join(bundled, "broken.md"));
const catalog = await loadAgentProfiles({
cwd: directory,
bundledRoot: bundled,
projectTrusted: false,
});
assert.deepEqual(
catalog.profiles.map((profile) => profile.name),
["internal-link"],
);
assert.ok(catalog.profiles[0]?.resolvedPath.endsWith("internal.profile"));
assert.ok(
catalog.diagnostics.some((item) =>
/escapes the profile root/.test(item.message),
),
);
assert.ok(
catalog.diagnostics.some((item) => item.sourcePath.endsWith("broken.md")),
);
});
});
test("the nearest trusted ancestor project profile root is selected", async () => {
await withTempDir(async (directory) => {
const bundled = path.join(directory, "bundled");
const project = path.join(directory, ".pi", "agents");
const nestedCwd = path.join(directory, "packages", "app");
await mkdir(nestedCwd, { recursive: true });
await writeProfile(bundled, "agent.md", {
name: "shared-agent",
description: "bundled",
});
await writeProfile(project, "agent.md", {
name: "shared-agent",
description: "nearest project",
});
const trusted = await loadAgentProfiles({
cwd: nestedCwd,
bundledRoot: bundled,
projectTrusted: true,
});
assert.equal(trusted.profiles[0]?.description, "nearest project");
const untrusted = await loadAgentProfiles({
cwd: nestedCwd,
bundledRoot: bundled,
projectTrusted: false,
});
assert.equal(untrusted.profiles[0]?.description, "bundled");
});
});
test("active profile contents are pinned by digest", async () => {
await withTempDir(async (directory) => {
const bundled = path.join(directory, "bundled");
await writeProfile(bundled, "agent.md", {
name: "pinned-agent",
body: "Original instructions.",
});
const first = await loadAgentProfiles({
cwd: directory,
bundledRoot: bundled,
projectTrusted: false,
});
await writeProfile(bundled, "agent.md", {
name: "pinned-agent",
body: "Changed instructions.",
});
const second = await loadAgentProfiles({
cwd: directory,
bundledRoot: bundled,
projectTrusted: false,
});
assert.equal(first.profiles[0]?.instructions, "Original instructions.");
assert.notEqual(first.profiles[0]?.digest, second.profiles[0]?.digest);
});
});
function agent(
name: string,
selection: AgentProfile["selection"],
source: AgentProfile["source"] = "bundled",
): AgentProfile {
return {
name,
description: name,
roles: ["discover"],
selection,
instructions: "Discover.",
source,
sourcePath: name,
resolvedPath: name,
digest: name.padEnd(64, "0"),
};
}
const baseFacts: Omit<WorkflowFacts, "stage"> = {
taskKind: "feature",
requestTerms: ["dashboard"],
candidatePaths: ["src/routes/dashboard/page.tsx"],
languages: ["typescript"],
tags: ["frontend", "ui"],
riskTags: [],
};
test("eligibility records exclusion reasons and selects the most specific discovery profile", () => {
const fallback = agent("general", {
stages: ["discover"],
anyTags: [],
allTags: [],
pathGlobs: [],
keywords: [],
});
const dashboard = agent("dashboard", {
stages: ["discover"],
anyTags: ["ui"],
allTags: ["frontend"],
pathGlobs: ["src/**/dashboard/*.tsx"],
keywords: ["dashboard"],
});
const backend = agent("backend", {
stages: ["discover"],
anyTags: ["backend"],
allTags: [],
pathGlobs: [],
keywords: [],
});
const result = selectInitialDiscoveryProfile(
[fallback, dashboard, backend],
baseFacts,
);
assert.equal(result.selected.name, "dashboard");
const excluded = result.evaluations.find(
(item) => item.profile.name === "backend",
);
assert.equal(excluded?.eligible, false);
assert.deepEqual(excluded?.reasons, ["no optional tag matched"]);
const preDiscovery = eligibleProfiles(
[fallback, dashboard],
{
...baseFacts,
stage: "discover",
requestTerms: [],
candidatePaths: [],
tags: [],
},
"discover",
);
assert.equal(
preDiscovery.find((item) => item.eligible)?.profile.name,
"general",
);
});
test("an eligible standard agent replaces the bundled fallback for its role", () => {
const emptySelection: AgentProfile["selection"] = {
stages: ["discover"],
anyTags: [],
allTags: [],
pathGlobs: [],
keywords: [],
};
const bundled = agent("general-discovery", emptySelection);
const scout = agent("scout", emptySelection, "standard-package");
const result = selectInitialDiscoveryProfile(
[bundled, scout],
baseFacts,
);
assert.equal(result.selected.name, "scout");
assert.deepEqual(
result.evaluations.find((item) => item.profile.name === "general-discovery")?.reasons,
["eligible standard agent available for role discover"],
);
});
test("eligibility enforces every criterion and uses a code-point name tie-breaker", () => {
const emptySelection: AgentProfile["selection"] = {
stages: ["discover"],
anyTags: [],
allTags: [],
pathGlobs: [],
keywords: [],
};
const wrongRole = {
...agent("wrong-role", emptySelection),
roles: ["design"] as AgentProfile["roles"],
};
const wrongStage = agent("wrong-stage", {
...emptySelection,
stages: ["design"],
});
const missingAllTag = agent("missing-all-tag", {
...emptySelection,
allTags: ["backend"],
});
const missingAnyTag = agent("missing-any-tag", {
...emptySelection,
anyTags: ["backend"],
});
const missingPath = agent("missing-path", {
...emptySelection,
pathGlobs: ["server/**/*.ts"],
});
const missingKeyword = agent("missing-keyword", {
...emptySelection,
keywords: ["api"],
});
const evaluations = eligibleProfiles(
[
wrongRole,
wrongStage,
missingAllTag,
missingAnyTag,
missingPath,
missingKeyword,
],
{ ...baseFacts, stage: "discover" },
"discover",
);
const reasons = new Map(
evaluations.map((evaluation) => [
evaluation.profile.name,
evaluation.reasons,
]),
);
assert.deepEqual(reasons.get("wrong-role"), [
"does not support role discover",
]);
assert.deepEqual(reasons.get("wrong-stage"), [
"does not support stage discover",
]);
assert.deepEqual(reasons.get("missing-all-tag"), [
"missing required tags: backend",
]);
assert.deepEqual(reasons.get("missing-any-tag"), ["no optional tag matched"]);
assert.deepEqual(reasons.get("missing-path"), ["no candidate path matched"]);
assert.deepEqual(reasons.get("missing-keyword"), [
"no request keyword matched",
]);
const tie = eligibleProfiles(
[agent("zeta", emptySelection), agent("alpha", emptySelection)],
{ ...baseFacts, stage: "discover" },
"discover",
);
assert.deepEqual(
tie
.filter((evaluation) => evaluation.eligible)
.map((evaluation) => evaluation.profile.name),
["alpha", "zeta"],
);
});