Some checks failed
✅ Check / 🔬 ESLint (push) Failing after 21s
✅ Check / 🏷️ Typecheck (push) Failing after 11s
✅ Check / 🔎 Test (push) Failing after 11s
✅ Check / e2e-test (push) Failing after 11s
✅ Check / 🔨 Build (push) Failing after 11s
🚀 Deploy / 🚀 Deploy (doc) (push) Failing after 12s
🚀 Deploy / 🚀 Deploy (demo) (push) Failing after 12s
🚀 Deploy / 🚀 Deploy (supabase) (push) Failing after 18s
91 lines
3.3 KiB
JavaScript
91 lines
3.3 KiB
JavaScript
// Tests for block-nested-orchestrator.mjs — the PreToolUse(Agent) gate that
|
|
// enforces the harness dispatch topology. Two rules:
|
|
// 1. the orchestrator may dispatch ONLY the typed harness agents
|
|
// (planner, developer, quality-reviewer, merger, documentator).
|
|
// 2. a subagent (has agent_id) must NOT dispatch an orchestrator.
|
|
// A block is emitted via decisionBlock: exit 0 with {"decision":"block"} on
|
|
// stdout. An allowed dispatch exits 0 with no decision JSON.
|
|
|
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { dirname, join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { spawnSync } from "node:child_process";
|
|
import { afterAll, describe, test, expect } from "vitest";
|
|
|
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
const HOOK = join(HERE, "..", "block-nested-orchestrator.mjs");
|
|
const TMP = mkdtempSync(join(tmpdir(), "block-nested-orch-test-"));
|
|
const env = { ...process.env, APP_DIR: TMP, CRM_TMP_ROOT: TMP };
|
|
delete env.CLAUDE_AGENT_NAME;
|
|
|
|
afterAll(() => rmSync(TMP, { recursive: true, force: true }));
|
|
|
|
// callerType = the dispatching agent's agent_type; agentId distinguishes the
|
|
// main thread ("") from a subagent (non-empty).
|
|
const run = (callerType, target, agentId = "") => {
|
|
const r = spawnSync("node", [HOOK], {
|
|
input: JSON.stringify({
|
|
session_id: "td-nested-1",
|
|
agent_type: callerType,
|
|
agent_id: agentId,
|
|
tool_input: { subagent_type: target },
|
|
}),
|
|
env,
|
|
encoding: "utf8",
|
|
});
|
|
return {
|
|
blocked: /"decision":"block"/.test(r.stdout || ""),
|
|
status: r.status,
|
|
};
|
|
};
|
|
|
|
describe("block-nested-orchestrator — Rule 1 (orchestrator allowlist)", () => {
|
|
for (const t of [
|
|
"planner",
|
|
"developer",
|
|
"quality-reviewer",
|
|
"merger",
|
|
"documentator",
|
|
]) {
|
|
test(`orchestrator dispatching ${t} → allowed`, () => {
|
|
expect(run("orchestrator", t, "orch-1").blocked).toBe(false);
|
|
});
|
|
}
|
|
|
|
test("orchestrator dispatching general-purpose → blocked", () => {
|
|
expect(run("orchestrator", "general-purpose", "orch-1").blocked).toBe(true);
|
|
});
|
|
|
|
test("orchestrator dispatching another orchestrator → blocked", () => {
|
|
expect(run("orchestrator", "orchestrator", "orch-1").blocked).toBe(true);
|
|
});
|
|
|
|
test("a suffixed orchestrator-name caller is still gated by the allowlist", () => {
|
|
expect(run("orchestrator-abc12", "Explore", "orch-1").blocked).toBe(true);
|
|
expect(run("orchestrator-abc12", "planner", "orch-1").blocked).toBe(false);
|
|
});
|
|
|
|
test("chat-orchestrator dispatching planner → allowed", () => {
|
|
expect(run("chat-orchestrator", "planner", "chat-1").blocked).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("block-nested-orchestrator — Rule 2 (no nested orchestrator)", () => {
|
|
test("a subagent dispatching orchestrator → blocked", () => {
|
|
expect(run("general-purpose", "orchestrator", "gp-1").blocked).toBe(true);
|
|
});
|
|
|
|
test("a subagent dispatching chat-orchestrator → blocked", () => {
|
|
expect(run("developer", "chat-orchestrator", "dev-1").blocked).toBe(true);
|
|
});
|
|
|
|
test("the main thread (no agent_id) dispatching orchestrator → allowed", () => {
|
|
expect(run("", "orchestrator", "").blocked).toBe(false);
|
|
});
|
|
|
|
test("a subagent dispatching a non-orchestrator agent → allowed", () => {
|
|
expect(run("developer", "general-purpose", "dev-1").blocked).toBe(false);
|
|
});
|
|
});
|