169 lines
4.0 KiB
TypeScript
169 lines
4.0 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { parseAgentManifest } from "../src/agents/manifest.js";
|
|
|
|
function validManifest(): unknown {
|
|
return {
|
|
schemaVersion: "1",
|
|
topologies: ["hierarchical", "retry-unrolled", "sequential"],
|
|
personas: [
|
|
{
|
|
id: "product",
|
|
displayName: "Product",
|
|
systemPromptTemplate: "Product for {{repo}}",
|
|
toolClearance: {
|
|
allowlist: ["read_file"],
|
|
banlist: ["delete_file"],
|
|
},
|
|
},
|
|
{
|
|
id: "coder",
|
|
displayName: "Coder",
|
|
systemPromptTemplate: "Coder for {{repo}}",
|
|
toolClearance: {
|
|
allowlist: ["read_file", "write_file"],
|
|
banlist: [],
|
|
},
|
|
},
|
|
],
|
|
relationships: [
|
|
{
|
|
parentPersonaId: "product",
|
|
childPersonaId: "coder",
|
|
constraints: {
|
|
maxDepth: 2,
|
|
maxChildren: 3,
|
|
},
|
|
},
|
|
],
|
|
topologyConstraints: {
|
|
maxDepth: 5,
|
|
maxRetries: 2,
|
|
},
|
|
pipeline: {
|
|
entryNodeId: "product-node",
|
|
nodes: [
|
|
{
|
|
id: "product-node",
|
|
actorId: "product_actor",
|
|
personaId: "product",
|
|
},
|
|
{
|
|
id: "coder-node",
|
|
actorId: "coder_actor",
|
|
personaId: "coder",
|
|
constraints: {
|
|
maxRetries: 1,
|
|
},
|
|
},
|
|
],
|
|
edges: [
|
|
{
|
|
from: "product-node",
|
|
to: "coder-node",
|
|
on: "success",
|
|
when: [{ type: "always" }],
|
|
},
|
|
],
|
|
},
|
|
};
|
|
}
|
|
|
|
test("parses a valid AgentManifest", () => {
|
|
const manifest = parseAgentManifest(validManifest());
|
|
assert.equal(manifest.schemaVersion, "1");
|
|
assert.equal(manifest.pipeline.nodes.length, 2);
|
|
assert.equal(manifest.relationships.length, 1);
|
|
});
|
|
|
|
test("parses optional persona modelConstraint", () => {
|
|
const manifest = validManifest() as {
|
|
personas: Array<Record<string, unknown>>;
|
|
};
|
|
manifest.personas[1] = {
|
|
...manifest.personas[1],
|
|
modelConstraint: "claude-3-haiku",
|
|
};
|
|
|
|
const parsed = parseAgentManifest(manifest);
|
|
const coder = parsed.personas.find((persona) => persona.id === "coder");
|
|
assert.ok(coder);
|
|
assert.equal(coder.modelConstraint, "claude-3-haiku");
|
|
});
|
|
|
|
test("rejects pipeline cycles", () => {
|
|
const manifest = validManifest() as {
|
|
pipeline: {
|
|
edges: Array<{ from: string; to: string; on: string }>;
|
|
};
|
|
};
|
|
|
|
manifest.pipeline.edges.push({
|
|
from: "coder-node",
|
|
to: "product-node",
|
|
on: "success",
|
|
});
|
|
|
|
assert.throws(() => parseAgentManifest(manifest), /strict DAG/);
|
|
});
|
|
|
|
test("rejects relationship with unknown persona", () => {
|
|
const manifest = validManifest() as {
|
|
relationships: Array<{ parentPersonaId: string; childPersonaId: string }>;
|
|
};
|
|
|
|
manifest.relationships.push({
|
|
parentPersonaId: "product",
|
|
childPersonaId: "unknown",
|
|
});
|
|
|
|
assert.throws(() => parseAgentManifest(manifest), /unknown child persona/);
|
|
});
|
|
|
|
test("rejects relationship cycles", () => {
|
|
const manifest = validManifest() as {
|
|
relationships: Array<{ parentPersonaId: string; childPersonaId: string }>;
|
|
};
|
|
|
|
manifest.relationships.push({
|
|
parentPersonaId: "coder",
|
|
childPersonaId: "product",
|
|
});
|
|
|
|
assert.throws(() => parseAgentManifest(manifest), /Relationship graph must be acyclic/);
|
|
});
|
|
|
|
test("rejects legacy edge trigger aliases", () => {
|
|
const manifest = validManifest() as {
|
|
pipeline: {
|
|
edges: Array<{ from: string; to: string; on: string }>;
|
|
};
|
|
};
|
|
|
|
manifest.pipeline.edges[0] = {
|
|
from: "product-node",
|
|
to: "coder-node",
|
|
on: "onValidationFail",
|
|
};
|
|
|
|
assert.throws(
|
|
() => parseAgentManifest(manifest),
|
|
/Invalid option/,
|
|
);
|
|
});
|
|
|
|
test("rejects empty persona modelConstraint", () => {
|
|
const manifest = validManifest() as {
|
|
personas: Array<Record<string, unknown>>;
|
|
};
|
|
manifest.personas[0] = {
|
|
...manifest.personas[0],
|
|
modelConstraint: " ",
|
|
};
|
|
|
|
assert.throws(
|
|
() => parseAgentManifest(manifest),
|
|
/modelConstraint/,
|
|
);
|
|
});
|