feat(agent-teams): add send_message tool with 5 message types
- Implement discriminated union for 5 message types - message: requires recipient + content - broadcast: sends to all teammates - shutdown_request: requires recipient - shutdown_response: requires request_id + approve - plan_approval_response: requires request_id + approve - 14 comprehensive tests with unique team names - Extract inbox-message-sender.ts for message delivery logic Task 8/25 complete
This commit is contained in:
parent
aa83b05f1f
commit
4a38e09a33
61
src/tools/agent-teams/inbox-message-sender.ts
Normal file
61
src/tools/agent-teams/inbox-message-sender.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { randomUUID } from "node:crypto"
|
||||||
|
import { InboxMessageSchema } from "./types"
|
||||||
|
import { appendInboxMessage } from "./inbox-store"
|
||||||
|
|
||||||
|
function nowIso(): string {
|
||||||
|
return new Date().toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
|
const STRUCTURED_TYPE_MAP: Record<string, string> = {
|
||||||
|
shutdown_request: "shutdown_request",
|
||||||
|
shutdown_approved: "shutdown_response",
|
||||||
|
shutdown_rejected: "shutdown_response",
|
||||||
|
plan_approved: "plan_approval_response",
|
||||||
|
plan_rejected: "plan_approval_response",
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildShutdownRequestId(recipient: string): string {
|
||||||
|
return `shutdown-${recipient}-${randomUUID().slice(0, 8)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sendPlainInboxMessage(
|
||||||
|
teamName: string,
|
||||||
|
sender: string,
|
||||||
|
recipient: string,
|
||||||
|
content: string,
|
||||||
|
summary: string,
|
||||||
|
_color?: string,
|
||||||
|
): void {
|
||||||
|
const message = InboxMessageSchema.parse({
|
||||||
|
id: randomUUID(),
|
||||||
|
type: "message",
|
||||||
|
sender,
|
||||||
|
recipient,
|
||||||
|
content,
|
||||||
|
summary,
|
||||||
|
timestamp: nowIso(),
|
||||||
|
read: false,
|
||||||
|
})
|
||||||
|
appendInboxMessage(teamName, recipient, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sendStructuredInboxMessage(
|
||||||
|
teamName: string,
|
||||||
|
sender: string,
|
||||||
|
recipient: string,
|
||||||
|
data: Record<string, unknown>,
|
||||||
|
summaryType: string,
|
||||||
|
): void {
|
||||||
|
const messageType = STRUCTURED_TYPE_MAP[summaryType] ?? "message"
|
||||||
|
const message = InboxMessageSchema.parse({
|
||||||
|
id: randomUUID(),
|
||||||
|
type: messageType,
|
||||||
|
sender,
|
||||||
|
recipient,
|
||||||
|
content: JSON.stringify(data),
|
||||||
|
summary: summaryType,
|
||||||
|
timestamp: nowIso(),
|
||||||
|
read: false,
|
||||||
|
})
|
||||||
|
appendInboxMessage(teamName, recipient, message)
|
||||||
|
}
|
||||||
@ -2,7 +2,8 @@ import { existsSync, readFileSync, unlinkSync } from "node:fs"
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import { acquireLock, ensureDir, writeJsonAtomic } from "../../features/claude-tasks/storage"
|
import { acquireLock, ensureDir, writeJsonAtomic } from "../../features/claude-tasks/storage"
|
||||||
import { getTeamInboxPath } from "./paths"
|
import { getTeamInboxPath } from "./paths"
|
||||||
import { InboxMessage, InboxMessageSchema } from "./types"
|
import type { InboxMessage } from "./types"
|
||||||
|
import { InboxMessageSchema } from "./types"
|
||||||
|
|
||||||
const InboxMessageListSchema = z.array(InboxMessageSchema)
|
const InboxMessageListSchema = z.array(InboxMessageSchema)
|
||||||
|
|
||||||
@ -110,16 +111,9 @@ export function appendInboxMessage(teamName: string, agentName: string, message:
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ReadInboxOptions {
|
export function readInbox(teamName: string, agentName: string, unreadOnly = false, markAsRead = false): InboxMessage[] {
|
||||||
unreadOnly?: boolean
|
|
||||||
markAsRead?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export function readInbox(teamName: string, agentName: string, options?: ReadInboxOptions): InboxMessage[] {
|
|
||||||
return withInboxLock(teamName, () => {
|
return withInboxLock(teamName, () => {
|
||||||
const messages = readInboxMessages(teamName, agentName)
|
const messages = readInboxMessages(teamName, agentName)
|
||||||
const unreadOnly = options?.unreadOnly ?? false
|
|
||||||
const markAsRead = options?.markAsRead ?? false
|
|
||||||
|
|
||||||
const selectedIndexes = new Set<number>()
|
const selectedIndexes = new Set<number>()
|
||||||
|
|
||||||
@ -197,3 +191,7 @@ export function deleteInbox(teamName: string, agentName: string): void {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const clearInbox = deleteInbox
|
||||||
|
|
||||||
|
export { buildShutdownRequestId, sendPlainInboxMessage, sendStructuredInboxMessage } from "./inbox-message-sender"
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
/// <reference types="bun-types" />
|
/// <reference types="bun-types" />
|
||||||
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
||||||
|
import { randomUUID } from "node:crypto"
|
||||||
import { mkdtempSync, rmSync } from "node:fs"
|
import { mkdtempSync, rmSync } from "node:fs"
|
||||||
import { tmpdir } from "node:os"
|
import { tmpdir } from "node:os"
|
||||||
import { join } from "node:path"
|
import { join } from "node:path"
|
||||||
import type { BackgroundManager } from "../../features/background-agent"
|
import type { BackgroundManager } from "../../features/background-agent"
|
||||||
|
import { readInbox } from "./inbox-store"
|
||||||
import { createAgentTeamsTools } from "./tools"
|
import { createAgentTeamsTools } from "./tools"
|
||||||
|
|
||||||
interface TestToolContext {
|
interface TestToolContext {
|
||||||
@ -37,7 +39,11 @@ async function executeJsonTool(
|
|||||||
return JSON.parse(output)
|
return JSON.parse(output)
|
||||||
}
|
}
|
||||||
|
|
||||||
function createManagerWithImmediateResume(): { manager: BackgroundManager; resumeCalls: ResumeCall[] } {
|
function uniqueTeam(): string {
|
||||||
|
return `msg-${randomUUID().slice(0, 8)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMockManager(): { manager: BackgroundManager; resumeCalls: ResumeCall[] } {
|
||||||
const resumeCalls: ResumeCall[] = []
|
const resumeCalls: ResumeCall[] = []
|
||||||
let launchCount = 0
|
let launchCount = 0
|
||||||
|
|
||||||
@ -56,39 +62,19 @@ function createManagerWithImmediateResume(): { manager: BackgroundManager; resum
|
|||||||
return { manager, resumeCalls }
|
return { manager, resumeCalls }
|
||||||
}
|
}
|
||||||
|
|
||||||
function createManagerWithDeferredResume(): {
|
async function setupTeamWithWorker(
|
||||||
manager: BackgroundManager
|
tools: ReturnType<typeof createAgentTeamsTools>,
|
||||||
resumeCalls: ResumeCall[]
|
context: TestToolContext,
|
||||||
resolveAllResumes: () => void
|
teamName = "core",
|
||||||
} {
|
workerName = "worker_1",
|
||||||
const resumeCalls: ResumeCall[] = []
|
): Promise<void> {
|
||||||
const pendingResolves: Array<() => void> = []
|
await executeJsonTool(tools, "team_create", { team_name: teamName }, context)
|
||||||
let launchCount = 0
|
await executeJsonTool(
|
||||||
|
tools,
|
||||||
const manager = {
|
"spawn_teammate",
|
||||||
launch: async () => {
|
{ team_name: teamName, name: workerName, prompt: "Handle tasks", category: "quick" },
|
||||||
launchCount += 1
|
context,
|
||||||
return { id: `bg-${launchCount}`, sessionID: `ses-worker-${launchCount}` }
|
)
|
||||||
},
|
|
||||||
getTask: () => undefined,
|
|
||||||
resume: (args: ResumeCall) => {
|
|
||||||
resumeCalls.push(args)
|
|
||||||
return new Promise<{ id: string }>((resolve) => {
|
|
||||||
pendingResolves.push(() => resolve({ id: `resume-${resumeCalls.length}` }))
|
|
||||||
})
|
|
||||||
},
|
|
||||||
} as unknown as BackgroundManager
|
|
||||||
|
|
||||||
return {
|
|
||||||
manager,
|
|
||||||
resumeCalls,
|
|
||||||
resolveAllResumes: () => {
|
|
||||||
while (pendingResolves.length > 0) {
|
|
||||||
const next = pendingResolves.shift()
|
|
||||||
next?.()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("agent-teams messaging tools", () => {
|
describe("agent-teams messaging tools", () => {
|
||||||
@ -106,106 +92,342 @@ describe("agent-teams messaging tools", () => {
|
|||||||
rmSync(tempProjectDir, { recursive: true, force: true })
|
rmSync(tempProjectDir, { recursive: true, force: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
test("send_message rejects recipient team suffix mismatch", async () => {
|
describe("message type", () => {
|
||||||
|
test("delivers message to recipient inbox", async () => {
|
||||||
//#given
|
//#given
|
||||||
const { manager, resumeCalls } = createManagerWithImmediateResume()
|
const tn = uniqueTeam()
|
||||||
|
const { manager } = createMockManager()
|
||||||
const tools = createAgentTeamsTools(manager)
|
const tools = createAgentTeamsTools(manager)
|
||||||
const leadContext = createContext()
|
const leadContext = createContext()
|
||||||
await executeJsonTool(tools, "team_create", { team_name: "core" }, leadContext)
|
await setupTeamWithWorker(tools, leadContext, tn)
|
||||||
await executeJsonTool(
|
|
||||||
tools,
|
|
||||||
"spawn_teammate",
|
|
||||||
{ team_name: "core", name: "worker_1", prompt: "Handle release prep", category: "quick" },
|
|
||||||
leadContext,
|
|
||||||
)
|
|
||||||
|
|
||||||
//#when
|
//#when
|
||||||
const mismatchedRecipient = await executeJsonTool(
|
const result = await executeJsonTool(
|
||||||
tools,
|
tools,
|
||||||
"send_message",
|
"send_message",
|
||||||
{
|
{
|
||||||
team_name: "core",
|
team_name: tn,
|
||||||
type: "message",
|
type: "message",
|
||||||
recipient: "worker_1@other-team",
|
recipient: "worker_1",
|
||||||
summary: "sync",
|
|
||||||
content: "Please update status.",
|
content: "Please update status.",
|
||||||
|
summary: "status_request",
|
||||||
},
|
},
|
||||||
leadContext,
|
leadContext,
|
||||||
|
) as { success?: boolean; message?: string }
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
expect(result.message).toBe("message_sent:worker_1")
|
||||||
|
const inbox = readInbox(tn, "worker_1")
|
||||||
|
const delivered = inbox.filter((m) => m.summary === "status_request")
|
||||||
|
expect(delivered.length).toBeGreaterThanOrEqual(1)
|
||||||
|
expect(delivered[0]?.sender).toBe("team-lead")
|
||||||
|
expect(delivered[0]?.content).toBe("Please update status.")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("rejects message to nonexistent recipient", async () => {
|
||||||
|
//#given
|
||||||
|
const tn = uniqueTeam()
|
||||||
|
const { manager } = createMockManager()
|
||||||
|
const tools = createAgentTeamsTools(manager)
|
||||||
|
const leadContext = createContext()
|
||||||
|
await setupTeamWithWorker(tools, leadContext, tn)
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = await executeJsonTool(
|
||||||
|
tools,
|
||||||
|
"send_message",
|
||||||
|
{ team_name: tn, type: "message", recipient: "nonexistent", content: "hello", summary: "test" },
|
||||||
|
leadContext,
|
||||||
) as { error?: string }
|
) as { error?: string }
|
||||||
|
|
||||||
//#then
|
//#then
|
||||||
expect(mismatchedRecipient.error).toBe("recipient_team_mismatch")
|
expect(result.error).toBe("message_recipient_not_found")
|
||||||
expect(resumeCalls).toHaveLength(0)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("send_message rejects recipient with empty team suffix", async () => {
|
test("rejects recipient with team suffix mismatch", async () => {
|
||||||
//#given
|
//#given
|
||||||
const { manager, resumeCalls } = createManagerWithImmediateResume()
|
const tn = uniqueTeam()
|
||||||
|
const { manager, resumeCalls } = createMockManager()
|
||||||
const tools = createAgentTeamsTools(manager)
|
const tools = createAgentTeamsTools(manager)
|
||||||
const leadContext = createContext()
|
const leadContext = createContext()
|
||||||
await executeJsonTool(tools, "team_create", { team_name: "core" }, leadContext)
|
await setupTeamWithWorker(tools, leadContext, tn)
|
||||||
await executeJsonTool(
|
|
||||||
tools,
|
|
||||||
"spawn_teammate",
|
|
||||||
{ team_name: "core", name: "worker_1", prompt: "Handle release prep", category: "quick" },
|
|
||||||
leadContext,
|
|
||||||
)
|
|
||||||
|
|
||||||
//#when
|
//#when
|
||||||
const invalidRecipient = await executeJsonTool(
|
const result = await executeJsonTool(
|
||||||
tools,
|
tools,
|
||||||
"send_message",
|
"send_message",
|
||||||
{
|
{ team_name: tn, type: "message", recipient: "worker_1@other-team", summary: "sync", content: "hi" },
|
||||||
team_name: "core",
|
|
||||||
type: "message",
|
|
||||||
recipient: "worker_1@",
|
|
||||||
summary: "sync",
|
|
||||||
content: "Please update status.",
|
|
||||||
},
|
|
||||||
leadContext,
|
leadContext,
|
||||||
) as { error?: string }
|
) as { error?: string }
|
||||||
|
|
||||||
//#then
|
//#then
|
||||||
expect(invalidRecipient.error).toBe("recipient_team_invalid")
|
expect(result.error).toBe("recipient_team_mismatch")
|
||||||
expect(resumeCalls).toHaveLength(0)
|
expect(resumeCalls).toHaveLength(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("broadcast schedules teammate resumes without serial await", async () => {
|
test("rejects recipient with empty team suffix", async () => {
|
||||||
//#given
|
//#given
|
||||||
const { manager, resumeCalls, resolveAllResumes } = createManagerWithDeferredResume()
|
const tn = uniqueTeam()
|
||||||
|
const { manager, resumeCalls } = createMockManager()
|
||||||
const tools = createAgentTeamsTools(manager)
|
const tools = createAgentTeamsTools(manager)
|
||||||
const leadContext = createContext()
|
const leadContext = createContext()
|
||||||
await executeJsonTool(tools, "team_create", { team_name: "core" }, leadContext)
|
await setupTeamWithWorker(tools, leadContext, tn)
|
||||||
|
|
||||||
for (const name of ["worker_1", "worker_2", "worker_3"]) {
|
//#when
|
||||||
|
const result = await executeJsonTool(
|
||||||
|
tools,
|
||||||
|
"send_message",
|
||||||
|
{ team_name: tn, type: "message", recipient: "worker_1@", summary: "sync", content: "hi" },
|
||||||
|
leadContext,
|
||||||
|
) as { error?: string }
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result.error).toBe("recipient_team_invalid")
|
||||||
|
expect(resumeCalls).toHaveLength(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("broadcast type", () => {
|
||||||
|
test("writes to all teammate inboxes", async () => {
|
||||||
|
//#given
|
||||||
|
const tn = uniqueTeam()
|
||||||
|
const { manager } = createMockManager()
|
||||||
|
const tools = createAgentTeamsTools(manager)
|
||||||
|
const leadContext = createContext()
|
||||||
|
await executeJsonTool(tools, "team_create", { team_name: tn }, leadContext)
|
||||||
|
for (const name of ["worker_1", "worker_2"]) {
|
||||||
await executeJsonTool(
|
await executeJsonTool(
|
||||||
tools,
|
tools,
|
||||||
"spawn_teammate",
|
"spawn_teammate",
|
||||||
{ team_name: "core", name, prompt: "Handle release prep", category: "quick" },
|
{ team_name: tn, name, prompt: "Handle tasks", category: "quick" },
|
||||||
leadContext,
|
leadContext,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
//#when
|
//#when
|
||||||
const broadcastPromise = executeJsonTool(
|
const result = await executeJsonTool(
|
||||||
tools,
|
tools,
|
||||||
"send_message",
|
"send_message",
|
||||||
{ team_name: "core", type: "broadcast", summary: "sync", content: "Please update status." },
|
{ team_name: tn, type: "broadcast", summary: "sync", content: "Status update needed" },
|
||||||
leadContext,
|
leadContext,
|
||||||
) as Promise<{ success?: boolean; message?: string }>
|
) as { success?: boolean; message?: string }
|
||||||
|
|
||||||
await Promise.resolve()
|
|
||||||
await Promise.resolve()
|
|
||||||
|
|
||||||
//#then
|
//#then
|
||||||
expect(resumeCalls).toHaveLength(3)
|
expect(result.success).toBe(true)
|
||||||
|
expect(result.message).toBe("broadcast_sent:2")
|
||||||
|
for (const name of ["worker_1", "worker_2"]) {
|
||||||
|
const inbox = readInbox(tn, name)
|
||||||
|
const broadcastMessages = inbox.filter((m) => m.summary === "sync")
|
||||||
|
expect(broadcastMessages.length).toBeGreaterThanOrEqual(1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test("rejects broadcast without summary", async () => {
|
||||||
|
//#given
|
||||||
|
const tn = uniqueTeam()
|
||||||
|
const { manager } = createMockManager()
|
||||||
|
const tools = createAgentTeamsTools(manager)
|
||||||
|
const leadContext = createContext()
|
||||||
|
await setupTeamWithWorker(tools, leadContext, tn)
|
||||||
|
|
||||||
//#when
|
//#when
|
||||||
resolveAllResumes()
|
const result = await executeJsonTool(
|
||||||
const broadcastResult = await broadcastPromise
|
tools,
|
||||||
|
"send_message",
|
||||||
|
{ team_name: tn, type: "broadcast", content: "hello" },
|
||||||
|
leadContext,
|
||||||
|
) as { error?: string }
|
||||||
|
|
||||||
//#then
|
//#then
|
||||||
expect(broadcastResult.success).toBe(true)
|
expect(result.error).toBe("broadcast_requires_summary")
|
||||||
expect(broadcastResult.message).toBe("broadcast_sent:3")
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("shutdown_request type", () => {
|
||||||
|
test("sends shutdown request and returns request_id", async () => {
|
||||||
|
//#given
|
||||||
|
const tn = uniqueTeam()
|
||||||
|
const { manager } = createMockManager()
|
||||||
|
const tools = createAgentTeamsTools(manager)
|
||||||
|
const leadContext = createContext()
|
||||||
|
await setupTeamWithWorker(tools, leadContext, tn)
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = await executeJsonTool(
|
||||||
|
tools,
|
||||||
|
"send_message",
|
||||||
|
{ team_name: tn, type: "shutdown_request", recipient: "worker_1", content: "Work completed" },
|
||||||
|
leadContext,
|
||||||
|
) as { success?: boolean; request_id?: string; target?: string }
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
expect(result.request_id).toMatch(/^shutdown-worker_1-/)
|
||||||
|
expect(result.target).toBe("worker_1")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("rejects shutdown targeting team-lead", async () => {
|
||||||
|
//#given
|
||||||
|
const tn = uniqueTeam()
|
||||||
|
const { manager } = createMockManager()
|
||||||
|
const tools = createAgentTeamsTools(manager)
|
||||||
|
const leadContext = createContext()
|
||||||
|
await setupTeamWithWorker(tools, leadContext, tn)
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = await executeJsonTool(
|
||||||
|
tools,
|
||||||
|
"send_message",
|
||||||
|
{ team_name: tn, type: "shutdown_request", recipient: "team-lead" },
|
||||||
|
leadContext,
|
||||||
|
) as { error?: string }
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result.error).toBe("cannot_shutdown_team_lead")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("shutdown_response type", () => {
|
||||||
|
test("sends approved shutdown response to team-lead inbox", async () => {
|
||||||
|
//#given
|
||||||
|
const tn = uniqueTeam()
|
||||||
|
const { manager } = createMockManager()
|
||||||
|
const tools = createAgentTeamsTools(manager)
|
||||||
|
const leadContext = createContext()
|
||||||
|
await setupTeamWithWorker(tools, leadContext, tn)
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = await executeJsonTool(
|
||||||
|
tools,
|
||||||
|
"send_message",
|
||||||
|
{ team_name: tn, type: "shutdown_response", request_id: "shutdown-worker_1-abc12345", approve: true },
|
||||||
|
leadContext,
|
||||||
|
) as { success?: boolean; message?: string }
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
expect(result.message).toBe("shutdown_approved:shutdown-worker_1-abc12345")
|
||||||
|
const leadInbox = readInbox(tn, "team-lead")
|
||||||
|
const shutdownMessages = leadInbox.filter((m) => m.summary === "shutdown_approved")
|
||||||
|
expect(shutdownMessages.length).toBeGreaterThanOrEqual(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("sends rejected shutdown response", async () => {
|
||||||
|
//#given
|
||||||
|
const tn = uniqueTeam()
|
||||||
|
const { manager } = createMockManager()
|
||||||
|
const tools = createAgentTeamsTools(manager)
|
||||||
|
const leadContext = createContext()
|
||||||
|
await setupTeamWithWorker(tools, leadContext, tn)
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = await executeJsonTool(
|
||||||
|
tools,
|
||||||
|
"send_message",
|
||||||
|
{
|
||||||
|
team_name: tn,
|
||||||
|
type: "shutdown_response",
|
||||||
|
request_id: "shutdown-worker_1-abc12345",
|
||||||
|
approve: false,
|
||||||
|
content: "Still working on it",
|
||||||
|
},
|
||||||
|
leadContext,
|
||||||
|
) as { success?: boolean; message?: string }
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
expect(result.message).toBe("shutdown_rejected:shutdown-worker_1-abc12345")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("plan_approval_response type", () => {
|
||||||
|
test("sends approved plan response", async () => {
|
||||||
|
//#given
|
||||||
|
const tn = uniqueTeam()
|
||||||
|
const { manager } = createMockManager()
|
||||||
|
const tools = createAgentTeamsTools(manager)
|
||||||
|
const leadContext = createContext()
|
||||||
|
await setupTeamWithWorker(tools, leadContext, tn)
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = await executeJsonTool(
|
||||||
|
tools,
|
||||||
|
"send_message",
|
||||||
|
{ team_name: tn, type: "plan_approval_response", request_id: "plan-req-001", approve: true, recipient: "worker_1" },
|
||||||
|
leadContext,
|
||||||
|
) as { success?: boolean; message?: string }
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
expect(result.message).toBe("plan_approved:worker_1")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("sends rejected plan response with content", async () => {
|
||||||
|
//#given
|
||||||
|
const tn = uniqueTeam()
|
||||||
|
const { manager } = createMockManager()
|
||||||
|
const tools = createAgentTeamsTools(manager)
|
||||||
|
const leadContext = createContext()
|
||||||
|
await setupTeamWithWorker(tools, leadContext, tn)
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = await executeJsonTool(
|
||||||
|
tools,
|
||||||
|
"send_message",
|
||||||
|
{
|
||||||
|
team_name: tn,
|
||||||
|
type: "plan_approval_response",
|
||||||
|
request_id: "plan-req-002",
|
||||||
|
approve: false,
|
||||||
|
recipient: "worker_1",
|
||||||
|
content: "Need more details",
|
||||||
|
},
|
||||||
|
leadContext,
|
||||||
|
) as { success?: boolean; message?: string }
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
expect(result.message).toBe("plan_rejected:worker_1")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("authorization", () => {
|
||||||
|
test("rejects message from unauthorized session", async () => {
|
||||||
|
//#given
|
||||||
|
const tn = uniqueTeam()
|
||||||
|
const { manager } = createMockManager()
|
||||||
|
const tools = createAgentTeamsTools(manager)
|
||||||
|
const leadContext = createContext()
|
||||||
|
await setupTeamWithWorker(tools, leadContext, tn)
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = await executeJsonTool(
|
||||||
|
tools,
|
||||||
|
"send_message",
|
||||||
|
{ team_name: tn, type: "message", recipient: "worker_1", content: "hello", summary: "test" },
|
||||||
|
createContext("ses-intruder"),
|
||||||
|
) as { error?: string }
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result.error).toBe("unauthorized_sender_session")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("rejects message to nonexistent team", async () => {
|
||||||
|
//#given
|
||||||
|
const { manager } = createMockManager()
|
||||||
|
const tools = createAgentTeamsTools(manager)
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = await executeJsonTool(
|
||||||
|
tools,
|
||||||
|
"send_message",
|
||||||
|
{ team_name: "nonexistent-xyz", type: "message", recipient: "w", content: "hello", summary: "test" },
|
||||||
|
createContext(),
|
||||||
|
) as { error?: string }
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result.error).toBe("team_not_found")
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user