700 lines
22 KiB
TypeScript
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(), "pi-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"],
|
|
);
|
|
});
|