|
6 | 6 | tierGateRequiresTools, |
7 | 7 | type AgentFleetDeps, |
8 | 8 | } from "./agent-fleet.js"; |
| 9 | +import { formatCapabilityUnavailable } from "./capability-preflight.js"; |
9 | 10 | import { unlimitedAdmissionQueue } from "./admission.js"; |
10 | 11 | import { createPermissionGate } from "../permission/gate.js"; |
11 | 12 | import { NOOP_TELEMETRY, type TelemetryEvent } from "../telemetry/index.js"; |
@@ -157,7 +158,7 @@ describe("spawn_agent requires_tools preflight", () => { |
157 | 158 | }; |
158 | 159 | const session = deps.sessions.get(body.agent_id); |
159 | 160 | expect(session?.requiresTools).toEqual(["read_file"]); |
160 | | - expect(typeof session?.snapshotRevision).toBe("number"); |
| 161 | + expect("snapshotRevision" in (session ?? {})).toBe(false); |
161 | 162 | expect(seenRequires).toEqual(["read_file"]); |
162 | 163 | expect(telemetry).toContain("subagent_start"); |
163 | 164 | }); |
@@ -196,7 +197,7 @@ describe("spawn_agent requires_tools preflight", () => { |
196 | 197 | const body = JSON.parse(result.content) as { agent_id: string }; |
197 | 198 | const session = deps.sessions.get(body.agent_id); |
198 | 199 | expect(session?.requiresTools).toBeUndefined(); |
199 | | - expect(session?.snapshotRevision).toBeUndefined(); |
| 200 | + expect("snapshotRevision" in (session ?? {})).toBe(false); |
200 | 201 | }); |
201 | 202 |
|
202 | 203 | test("manage_tasks survives dispatch under a narrow allowlist and stamps canonically", async () => { |
@@ -268,6 +269,62 @@ describe("spawn_agent requires_tools preflight", () => { |
268 | 269 | expect(deps.sessions.list()).toEqual([]); |
269 | 270 | expect(telemetry).toEqual([]); |
270 | 271 | }); |
| 272 | + |
| 273 | + test("leaf requires_tools=[submit_result] passes preflight under a narrow allowlist (leaf reporting channel)", async () => { |
| 274 | + const telemetry: TelemetryEvent[] = []; |
| 275 | + let runCalled = false; |
| 276 | + let seenRequires: readonly string[] | undefined; |
| 277 | + let seenTier: unknown; |
| 278 | + const deps = makeDeps(async (params) => { |
| 279 | + runCalled = true; |
| 280 | + seenRequires = params.requiresTools; |
| 281 | + seenTier = params.tier; |
| 282 | + return { report: "done" }; |
| 283 | + }, telemetry); |
| 284 | + const spawn = createSpawnAgentTool(deps); |
| 285 | + |
| 286 | + const result = await callSpawn(spawn, { |
| 287 | + description: "leaf report job", |
| 288 | + prompt: "read a file and report", |
| 289 | + agent: "read-only-worker", |
| 290 | + requires_tools: ["submit_result"], |
| 291 | + }); |
| 292 | + |
| 293 | + expect(result.isError).not.toBe(true); |
| 294 | + expect(runCalled).toBe(true); |
| 295 | + const body = JSON.parse(result.content) as { agent_id: string }; |
| 296 | + expect(deps.sessions.get(body.agent_id)?.requiresTools).toEqual([ |
| 297 | + "submit_result", |
| 298 | + ]); |
| 299 | + expect(seenRequires).toEqual(["submit_result"]); |
| 300 | + expect(seenTier).toBe("leaf"); |
| 301 | + }); |
| 302 | + |
| 303 | + test("whitespace-only requires_tools rejects fail-closed with no session, telemetry, or run", async () => { |
| 304 | + for (const requiresTools of [[" "], ["read_file", " "]]) { |
| 305 | + const telemetry: TelemetryEvent[] = []; |
| 306 | + let runCalled = false; |
| 307 | + const deps = makeDeps(async () => { |
| 308 | + runCalled = true; |
| 309 | + return { report: "done" }; |
| 310 | + }, telemetry); |
| 311 | + const spawn = createSpawnAgentTool(deps); |
| 312 | + |
| 313 | + const result = await callSpawn(spawn, { |
| 314 | + description: "blank job", |
| 315 | + prompt: "do it", |
| 316 | + agent: "read-only-worker", |
| 317 | + requires_tools: requiresTools, |
| 318 | + }); |
| 319 | + |
| 320 | + expect(result.isError).toBe(true); |
| 321 | + expect(result.content.startsWith("Error:")).toBe(true); |
| 322 | + expect(result.content).toContain("non-empty tool names"); |
| 323 | + expect(runCalled).toBe(false); |
| 324 | + expect(deps.sessions.list()).toEqual([]); |
| 325 | + expect(telemetry).toEqual([]); |
| 326 | + } |
| 327 | + }); |
271 | 328 | }); |
272 | 329 |
|
273 | 330 | describe("tierGateRequiresTools", () => { |
@@ -305,6 +362,24 @@ describe("tierGateRequiresTools", () => { |
305 | 362 | } |
306 | 363 | }); |
307 | 364 |
|
| 365 | + test("tier-gated leaf channel names Tier 3 leaves instead of claiming no director mounts it", () => { |
| 366 | + const gated = tierGateRequiresTools(["submit_result"], "orchestrator"); |
| 367 | + expect(gated?.alternatives ?? []).toEqual([ |
| 368 | + "bruckheimer", |
| 369 | + "builder", |
| 370 | + "counsel", |
| 371 | + ]); |
| 372 | + const message = formatCapabilityUnavailable( |
| 373 | + gated ?? { code: "missing_tool", tool: "submit_result" }, |
| 374 | + "test-orchestrator", |
| 375 | + ); |
| 376 | + expect(message).toContain("Tier 3 leaf workers only"); |
| 377 | + expect(message).toContain( |
| 378 | + "Re-dispatch to one of (bruckheimer, builder, counsel)", |
| 379 | + ); |
| 380 | + expect(message).not.toContain("No spawnable director mounts"); |
| 381 | + }); |
| 382 | + |
308 | 383 | test("leaf requiring the leaf reporting channel passes", () => { |
309 | 384 | expect( |
310 | 385 | tierGateRequiresTools(["submit_result", "ask_director"], "leaf"), |
|
0 commit comments