feat(agent-teams): add team, message, and task Zod schemas
- TeamConfigSchema with lead/teammate members - TeamMemberSchema and TeamTeammateMemberSchema - InboxMessageSchema with 5 message types - SendMessageInputSchema as discriminated union - Import TaskObjectSchema from tools/task/types.ts - 39 comprehensive tests covering all schemas Task 3/25 complete
This commit is contained in:
parent
3e2e4e29df
commit
d65912bc63
@ -1,10 +1,198 @@
|
|||||||
/// <reference types="bun-types" />
|
import { describe, it, expect } from "bun:test"
|
||||||
import { describe, expect, test } from "bun:test"
|
import { z } from "zod"
|
||||||
import { TeamTeammateMemberSchema } from "./types"
|
import {
|
||||||
|
TeamConfigSchema,
|
||||||
|
TeamMemberSchema,
|
||||||
|
TeamTeammateMemberSchema,
|
||||||
|
MessageTypeSchema,
|
||||||
|
InboxMessageSchema,
|
||||||
|
TeamTaskSchema,
|
||||||
|
TeamCreateInputSchema,
|
||||||
|
TeamDeleteInputSchema,
|
||||||
|
SendMessageInputSchema,
|
||||||
|
ReadInboxInputSchema,
|
||||||
|
ReadConfigInputSchema,
|
||||||
|
TeamSpawnInputSchema,
|
||||||
|
ForceKillTeammateInputSchema,
|
||||||
|
ProcessShutdownApprovedInputSchema,
|
||||||
|
} from "./types"
|
||||||
|
|
||||||
describe("agent-teams types", () => {
|
describe("TeamConfigSchema", () => {
|
||||||
test("rejects reserved agentType for teammate schema", () => {
|
it("validates a complete team config", () => {
|
||||||
//#given
|
// given
|
||||||
|
const validConfig = {
|
||||||
|
name: "my-team",
|
||||||
|
description: "A test team",
|
||||||
|
createdAt: "2026-02-11T10:00:00Z",
|
||||||
|
leadAgentId: "agent-123",
|
||||||
|
leadSessionId: "ses-456",
|
||||||
|
members: [
|
||||||
|
{
|
||||||
|
agentId: "agent-123",
|
||||||
|
name: "Lead Agent",
|
||||||
|
agentType: "lead",
|
||||||
|
color: "blue",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
agentId: "agent-789",
|
||||||
|
name: "Worker 1",
|
||||||
|
agentType: "teammate",
|
||||||
|
color: "green",
|
||||||
|
category: "quick",
|
||||||
|
model: "claude-sonnet-4-5",
|
||||||
|
prompt: "You are a helpful assistant",
|
||||||
|
planModeRequired: false,
|
||||||
|
joinedAt: "2026-02-11T10:05:00Z",
|
||||||
|
cwd: "/tmp",
|
||||||
|
subscriptions: ["task-updates"],
|
||||||
|
backendType: "native" as const,
|
||||||
|
isActive: true,
|
||||||
|
sessionID: "ses-789",
|
||||||
|
backgroundTaskID: "task-123",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = TeamConfigSchema.safeParse(validConfig)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("rejects invalid team config", () => {
|
||||||
|
// given
|
||||||
|
const invalidConfig = {
|
||||||
|
name: "",
|
||||||
|
description: "A test team",
|
||||||
|
createdAt: "invalid-date",
|
||||||
|
leadAgentId: "",
|
||||||
|
leadSessionId: "ses-456",
|
||||||
|
members: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = TeamConfigSchema.safeParse(invalidConfig)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.success).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("TeamMemberSchema", () => {
|
||||||
|
it("validates a lead member", () => {
|
||||||
|
// given
|
||||||
|
const leadMember = {
|
||||||
|
agentId: "agent-123",
|
||||||
|
name: "Lead Agent",
|
||||||
|
agentType: "lead",
|
||||||
|
color: "blue",
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = TeamMemberSchema.safeParse(leadMember)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("rejects invalid member", () => {
|
||||||
|
// given
|
||||||
|
const invalidMember = {
|
||||||
|
agentId: "",
|
||||||
|
name: "",
|
||||||
|
agentType: "invalid",
|
||||||
|
color: "invalid",
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = TeamMemberSchema.safeParse(invalidMember)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.success).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("TeamTeammateMemberSchema", () => {
|
||||||
|
it("validates a complete teammate member", () => {
|
||||||
|
// given
|
||||||
|
const teammateMember = {
|
||||||
|
agentId: "agent-789",
|
||||||
|
name: "Worker 1",
|
||||||
|
agentType: "teammate",
|
||||||
|
color: "green",
|
||||||
|
category: "quick",
|
||||||
|
model: "claude-sonnet-4-5",
|
||||||
|
prompt: "You are a helpful assistant",
|
||||||
|
planModeRequired: false,
|
||||||
|
joinedAt: "2026-02-11T10:05:00Z",
|
||||||
|
cwd: "/tmp",
|
||||||
|
subscriptions: ["task-updates"],
|
||||||
|
backendType: "native" as const,
|
||||||
|
isActive: true,
|
||||||
|
sessionID: "ses-789",
|
||||||
|
backgroundTaskID: "task-123",
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = TeamTeammateMemberSchema.safeParse(teammateMember)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("validates teammate member with optional fields missing", () => {
|
||||||
|
// given
|
||||||
|
const minimalTeammate = {
|
||||||
|
agentId: "agent-789",
|
||||||
|
name: "Worker 1",
|
||||||
|
agentType: "teammate",
|
||||||
|
color: "green",
|
||||||
|
category: "quick",
|
||||||
|
model: "claude-sonnet-4-5",
|
||||||
|
prompt: "You are a helpful assistant",
|
||||||
|
planModeRequired: false,
|
||||||
|
joinedAt: "2026-02-11T10:05:00Z",
|
||||||
|
cwd: "/tmp",
|
||||||
|
subscriptions: [],
|
||||||
|
backendType: "native" as const,
|
||||||
|
isActive: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = TeamTeammateMemberSchema.safeParse(minimalTeammate)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("rejects invalid teammate member", () => {
|
||||||
|
// given
|
||||||
|
const invalidTeammate = {
|
||||||
|
agentId: "",
|
||||||
|
name: "Worker 1",
|
||||||
|
agentType: "teammate",
|
||||||
|
color: "green",
|
||||||
|
category: "quick",
|
||||||
|
model: "claude-sonnet-4-5",
|
||||||
|
prompt: "You are a helpful assistant",
|
||||||
|
planModeRequired: false,
|
||||||
|
joinedAt: "invalid-date",
|
||||||
|
cwd: "/tmp",
|
||||||
|
subscriptions: [],
|
||||||
|
backendType: "invalid" as const,
|
||||||
|
isActive: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = TeamTeammateMemberSchema.safeParse(invalidTeammate)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.success).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("rejects reserved agentType for teammate schema", () => {
|
||||||
|
// given
|
||||||
const invalidTeammate = {
|
const invalidTeammate = {
|
||||||
agentId: "worker@team",
|
agentId: "worker@team",
|
||||||
name: "worker",
|
name: "worker",
|
||||||
@ -14,17 +202,520 @@ describe("agent-teams types", () => {
|
|||||||
prompt: "do work",
|
prompt: "do work",
|
||||||
color: "blue",
|
color: "blue",
|
||||||
planModeRequired: false,
|
planModeRequired: false,
|
||||||
joinedAt: Date.now(),
|
joinedAt: "2026-02-11T10:05:00Z",
|
||||||
cwd: "/tmp",
|
cwd: "/tmp",
|
||||||
subscriptions: [],
|
subscriptions: [],
|
||||||
backendType: "native",
|
backendType: "native",
|
||||||
isActive: false,
|
isActive: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
//#when
|
// when
|
||||||
const result = TeamTeammateMemberSchema.safeParse(invalidTeammate)
|
const result = TeamTeammateMemberSchema.safeParse(invalidTeammate)
|
||||||
|
|
||||||
//#then
|
// then
|
||||||
|
expect(result.success).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("MessageTypeSchema", () => {
|
||||||
|
it("validates all 5 message types", () => {
|
||||||
|
// given
|
||||||
|
const types = ["message", "broadcast", "shutdown_request", "shutdown_response", "plan_approval_response"]
|
||||||
|
|
||||||
|
// when & then
|
||||||
|
types.forEach(type => {
|
||||||
|
const result = MessageTypeSchema.safeParse(type)
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
expect(result.data).toBe(type)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("rejects invalid message type", () => {
|
||||||
|
// given
|
||||||
|
const invalidType = "invalid_type"
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = MessageTypeSchema.safeParse(invalidType)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.success).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("InboxMessageSchema", () => {
|
||||||
|
it("validates a complete inbox message", () => {
|
||||||
|
// given
|
||||||
|
const message = {
|
||||||
|
id: "msg-123",
|
||||||
|
type: "message" as const,
|
||||||
|
sender: "agent-123",
|
||||||
|
recipient: "agent-456",
|
||||||
|
content: "Hello world",
|
||||||
|
summary: "Greeting",
|
||||||
|
timestamp: "2026-02-11T10:00:00Z",
|
||||||
|
read: false,
|
||||||
|
requestId: "req-123",
|
||||||
|
approve: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = InboxMessageSchema.safeParse(message)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("validates message with optional fields missing", () => {
|
||||||
|
// given
|
||||||
|
const minimalMessage = {
|
||||||
|
id: "msg-123",
|
||||||
|
type: "broadcast" as const,
|
||||||
|
sender: "agent-123",
|
||||||
|
recipient: "agent-456",
|
||||||
|
content: "Hello world",
|
||||||
|
summary: "Greeting",
|
||||||
|
timestamp: "2026-02-11T10:00:00Z",
|
||||||
|
read: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = InboxMessageSchema.safeParse(minimalMessage)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("rejects invalid inbox message", () => {
|
||||||
|
// given
|
||||||
|
const invalidMessage = {
|
||||||
|
id: "",
|
||||||
|
type: "invalid" as const,
|
||||||
|
sender: "",
|
||||||
|
recipient: "",
|
||||||
|
content: "",
|
||||||
|
summary: "",
|
||||||
|
timestamp: "invalid-date",
|
||||||
|
read: "not-boolean",
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = InboxMessageSchema.safeParse(invalidMessage)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.success).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("TeamTaskSchema", () => {
|
||||||
|
it("validates a task object", () => {
|
||||||
|
// given
|
||||||
|
const task = {
|
||||||
|
id: "T-12345678-1234-1234-1234-123456789012",
|
||||||
|
subject: "Implement feature",
|
||||||
|
description: "Add new functionality",
|
||||||
|
status: "pending" as const,
|
||||||
|
activeForm: "Implementing feature",
|
||||||
|
blocks: [],
|
||||||
|
blockedBy: [],
|
||||||
|
owner: "agent-123",
|
||||||
|
metadata: { priority: "high" },
|
||||||
|
repoURL: "https://github.com/user/repo",
|
||||||
|
parentID: "T-parent",
|
||||||
|
threadID: "thread-123",
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = TeamTaskSchema.safeParse(task)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("rejects invalid task", () => {
|
||||||
|
// given
|
||||||
|
const invalidTask = {
|
||||||
|
id: "invalid-id",
|
||||||
|
subject: "",
|
||||||
|
description: "Add new functionality",
|
||||||
|
status: "invalid" as const,
|
||||||
|
activeForm: "Implementing feature",
|
||||||
|
blocks: [],
|
||||||
|
blockedBy: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = TeamTaskSchema.safeParse(invalidTask)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.success).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("TeamCreateInputSchema", () => {
|
||||||
|
it("validates create input with description", () => {
|
||||||
|
// given
|
||||||
|
const input = {
|
||||||
|
team_name: "my-team",
|
||||||
|
description: "A test team",
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = TeamCreateInputSchema.safeParse(input)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("validates create input without description", () => {
|
||||||
|
// given
|
||||||
|
const input = {
|
||||||
|
team_name: "my-team",
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = TeamCreateInputSchema.safeParse(input)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("rejects invalid create input", () => {
|
||||||
|
// given
|
||||||
|
const input = {
|
||||||
|
team_name: "invalid team name with spaces and special chars!",
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = TeamCreateInputSchema.safeParse(input)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.success).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("TeamDeleteInputSchema", () => {
|
||||||
|
it("validates delete input", () => {
|
||||||
|
// given
|
||||||
|
const input = {
|
||||||
|
team_name: "my-team",
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = TeamDeleteInputSchema.safeParse(input)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("rejects invalid delete input", () => {
|
||||||
|
// given
|
||||||
|
const input = {
|
||||||
|
team_name: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = TeamDeleteInputSchema.safeParse(input)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.success).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("SendMessageInputSchema", () => {
|
||||||
|
it("validates message type input", () => {
|
||||||
|
// given
|
||||||
|
const input = {
|
||||||
|
team_name: "my-team",
|
||||||
|
type: "message" as const,
|
||||||
|
recipient: "agent-456",
|
||||||
|
content: "Hello world",
|
||||||
|
summary: "Greeting",
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = SendMessageInputSchema.safeParse(input)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("validates broadcast type input", () => {
|
||||||
|
// given
|
||||||
|
const input = {
|
||||||
|
team_name: "my-team",
|
||||||
|
type: "broadcast" as const,
|
||||||
|
content: "Team announcement",
|
||||||
|
summary: "Announcement",
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = SendMessageInputSchema.safeParse(input)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("validates shutdown_request type input", () => {
|
||||||
|
// given
|
||||||
|
const input = {
|
||||||
|
team_name: "my-team",
|
||||||
|
type: "shutdown_request" as const,
|
||||||
|
recipient: "agent-456",
|
||||||
|
content: "Please shutdown",
|
||||||
|
summary: "Shutdown request",
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = SendMessageInputSchema.safeParse(input)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("validates shutdown_response type input", () => {
|
||||||
|
// given
|
||||||
|
const input = {
|
||||||
|
team_name: "my-team",
|
||||||
|
type: "shutdown_response" as const,
|
||||||
|
request_id: "req-123",
|
||||||
|
approve: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = SendMessageInputSchema.safeParse(input)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("validates plan_approval_response type input", () => {
|
||||||
|
// given
|
||||||
|
const input = {
|
||||||
|
team_name: "my-team",
|
||||||
|
type: "plan_approval_response" as const,
|
||||||
|
request_id: "req-456",
|
||||||
|
approve: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = SendMessageInputSchema.safeParse(input)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("rejects message type without recipient", () => {
|
||||||
|
// given
|
||||||
|
const input = {
|
||||||
|
team_name: "my-team",
|
||||||
|
type: "message" as const,
|
||||||
|
content: "Hello world",
|
||||||
|
summary: "Greeting",
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = SendMessageInputSchema.safeParse(input)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.success).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("rejects shutdown_response without request_id", () => {
|
||||||
|
// given
|
||||||
|
const input = {
|
||||||
|
team_name: "my-team",
|
||||||
|
type: "shutdown_response" as const,
|
||||||
|
approve: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = SendMessageInputSchema.safeParse(input)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.success).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("rejects invalid team_name", () => {
|
||||||
|
// given
|
||||||
|
const input = {
|
||||||
|
team_name: "invalid team name",
|
||||||
|
type: "broadcast" as const,
|
||||||
|
content: "Hello",
|
||||||
|
summary: "Greeting",
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = SendMessageInputSchema.safeParse(input)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.success).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("ReadInboxInputSchema", () => {
|
||||||
|
it("validates read inbox input", () => {
|
||||||
|
// given
|
||||||
|
const input = {
|
||||||
|
team_name: "my-team",
|
||||||
|
agent_name: "worker-1",
|
||||||
|
unread_only: true,
|
||||||
|
mark_as_read: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = ReadInboxInputSchema.safeParse(input)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("validates minimal read inbox input", () => {
|
||||||
|
// given
|
||||||
|
const input = {
|
||||||
|
team_name: "my-team",
|
||||||
|
agent_name: "worker-1",
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = ReadInboxInputSchema.safeParse(input)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("rejects invalid read inbox input", () => {
|
||||||
|
// given
|
||||||
|
const input = {
|
||||||
|
team_name: "",
|
||||||
|
agent_name: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = ReadInboxInputSchema.safeParse(input)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.success).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("ReadConfigInputSchema", () => {
|
||||||
|
it("validates read config input", () => {
|
||||||
|
// given
|
||||||
|
const input = {
|
||||||
|
team_name: "my-team",
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = ReadConfigInputSchema.safeParse(input)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("rejects invalid read config input", () => {
|
||||||
|
// given
|
||||||
|
const input = {
|
||||||
|
team_name: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = ReadConfigInputSchema.safeParse(input)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.success).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("TeamSpawnInputSchema", () => {
|
||||||
|
it("validates spawn input", () => {
|
||||||
|
// given
|
||||||
|
const input = {
|
||||||
|
team_name: "my-team",
|
||||||
|
name: "worker-1",
|
||||||
|
category: "quick",
|
||||||
|
prompt: "You are a helpful assistant",
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = TeamSpawnInputSchema.safeParse(input)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("rejects invalid spawn input", () => {
|
||||||
|
// given
|
||||||
|
const input = {
|
||||||
|
team_name: "invalid team",
|
||||||
|
name: "",
|
||||||
|
category: "quick",
|
||||||
|
prompt: "You are a helpful assistant",
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = TeamSpawnInputSchema.safeParse(input)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.success).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("ForceKillTeammateInputSchema", () => {
|
||||||
|
it("validates force kill input", () => {
|
||||||
|
// given
|
||||||
|
const input = {
|
||||||
|
team_name: "my-team",
|
||||||
|
teammate_name: "worker-1",
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = ForceKillTeammateInputSchema.safeParse(input)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("rejects invalid force kill input", () => {
|
||||||
|
// given
|
||||||
|
const input = {
|
||||||
|
team_name: "",
|
||||||
|
teammate_name: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = ForceKillTeammateInputSchema.safeParse(input)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.success).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("ProcessShutdownApprovedInputSchema", () => {
|
||||||
|
it("validates shutdown approved input", () => {
|
||||||
|
// given
|
||||||
|
const input = {
|
||||||
|
team_name: "my-team",
|
||||||
|
teammate_name: "worker-1",
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = ProcessShutdownApprovedInputSchema.safeParse(input)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("rejects invalid shutdown approved input", () => {
|
||||||
|
// given
|
||||||
|
const input = {
|
||||||
|
team_name: "",
|
||||||
|
teammate_name: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = ProcessShutdownApprovedInputSchema.safeParse(input)
|
||||||
|
|
||||||
|
// then
|
||||||
expect(result.success).toBe(false)
|
expect(result.success).toBe(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,82 +1,52 @@
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
import { TaskObjectSchema } from "../task/types"
|
||||||
|
|
||||||
export const TEAM_COLOR_PALETTE = [
|
// Team member schemas
|
||||||
"blue",
|
export const TeamMemberSchema = z.object({
|
||||||
"green",
|
agentId: z.string().min(1),
|
||||||
"yellow",
|
name: z.string().min(1),
|
||||||
"purple",
|
agentType: z.enum(["lead", "teammate"]),
|
||||||
"orange",
|
color: z.string().min(1),
|
||||||
"pink",
|
})
|
||||||
"cyan",
|
|
||||||
"red",
|
|
||||||
] as const
|
|
||||||
|
|
||||||
export const TeamTaskStatusSchema = z.enum(["pending", "in_progress", "completed", "deleted"])
|
export type TeamMember = z.infer<typeof TeamMemberSchema>
|
||||||
export type TeamTaskStatus = z.infer<typeof TeamTaskStatusSchema>
|
|
||||||
|
|
||||||
export const TeamLeadMemberSchema = z.object({
|
export const TeamTeammateMemberSchema = TeamMemberSchema.extend({
|
||||||
agentId: z.string(),
|
category: z.string().min(1),
|
||||||
name: z.literal("team-lead"),
|
model: z.string().min(1),
|
||||||
agentType: z.literal("team-lead"),
|
prompt: z.string().min(1),
|
||||||
model: z.string(),
|
planModeRequired: z.boolean(),
|
||||||
joinedAt: z.number(),
|
joinedAt: z.string().datetime(),
|
||||||
cwd: z.string(),
|
cwd: z.string().min(1),
|
||||||
subscriptions: z.array(z.unknown()).default([]),
|
subscriptions: z.array(z.string()),
|
||||||
}).strict()
|
backendType: z.literal("native"),
|
||||||
|
isActive: z.boolean(),
|
||||||
export const TeamTeammateMemberSchema = z.object({
|
|
||||||
agentId: z.string(),
|
|
||||||
name: z.string(),
|
|
||||||
agentType: z.string().refine((value) => value !== "team-lead", {
|
|
||||||
message: "agent_type_reserved",
|
|
||||||
}),
|
|
||||||
category: z.string(),
|
|
||||||
model: z.string(),
|
|
||||||
prompt: z.string(),
|
|
||||||
color: z.string(),
|
|
||||||
planModeRequired: z.boolean().default(false),
|
|
||||||
joinedAt: z.number(),
|
|
||||||
cwd: z.string(),
|
|
||||||
subscriptions: z.array(z.unknown()).default([]),
|
|
||||||
backendType: z.literal("native").default("native"),
|
|
||||||
isActive: z.boolean().default(false),
|
|
||||||
sessionID: z.string().optional(),
|
sessionID: z.string().optional(),
|
||||||
backgroundTaskID: z.string().optional(),
|
backgroundTaskID: z.string().optional(),
|
||||||
}).strict()
|
}).refine(
|
||||||
|
(data) => data.agentType === "teammate",
|
||||||
|
"TeamTeammateMemberSchema requires agentType to be 'teammate'"
|
||||||
|
).refine(
|
||||||
|
(data) => data.agentType !== "team-lead",
|
||||||
|
"agentType 'team-lead' is reserved and not allowed"
|
||||||
|
)
|
||||||
|
|
||||||
export const TeamMemberSchema = z.union([TeamLeadMemberSchema, TeamTeammateMemberSchema])
|
export type TeamTeammateMember = z.infer<typeof TeamTeammateMemberSchema>
|
||||||
|
|
||||||
|
// Team config schema
|
||||||
export const TeamConfigSchema = z.object({
|
export const TeamConfigSchema = z.object({
|
||||||
name: z.string(),
|
name: z.string().min(1),
|
||||||
description: z.string().default(""),
|
description: z.string().optional(),
|
||||||
createdAt: z.number(),
|
createdAt: z.string().datetime(),
|
||||||
leadAgentId: z.string(),
|
leadAgentId: z.string().min(1),
|
||||||
leadSessionId: z.string(),
|
leadSessionId: z.string().min(1),
|
||||||
members: z.array(TeamMemberSchema),
|
members: z.array(z.union([TeamMemberSchema, TeamTeammateMemberSchema])),
|
||||||
}).strict()
|
})
|
||||||
|
|
||||||
export const TeamInboxMessageSchema = z.object({
|
export type TeamConfig = z.infer<typeof TeamConfigSchema>
|
||||||
from: z.string(),
|
|
||||||
text: z.string(),
|
|
||||||
timestamp: z.string(),
|
|
||||||
read: z.boolean().default(false),
|
|
||||||
summary: z.string().optional(),
|
|
||||||
color: z.string().optional(),
|
|
||||||
}).strict()
|
|
||||||
|
|
||||||
export const TeamTaskSchema = z.object({
|
// Message schemas
|
||||||
id: z.string(),
|
export const MessageTypeSchema = z.enum([
|
||||||
subject: z.string(),
|
|
||||||
description: z.string(),
|
|
||||||
activeForm: z.string().optional(),
|
|
||||||
status: TeamTaskStatusSchema,
|
|
||||||
blocks: z.array(z.string()).default([]),
|
|
||||||
blockedBy: z.array(z.string()).default([]),
|
|
||||||
owner: z.string().optional(),
|
|
||||||
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
||||||
}).strict()
|
|
||||||
|
|
||||||
export const TeamSendMessageTypeSchema = z.enum([
|
|
||||||
"message",
|
"message",
|
||||||
"broadcast",
|
"broadcast",
|
||||||
"shutdown_request",
|
"shutdown_request",
|
||||||
@ -84,118 +54,113 @@ export const TeamSendMessageTypeSchema = z.enum([
|
|||||||
"plan_approval_response",
|
"plan_approval_response",
|
||||||
])
|
])
|
||||||
|
|
||||||
export type TeamLeadMember = z.infer<typeof TeamLeadMemberSchema>
|
export type MessageType = z.infer<typeof MessageTypeSchema>
|
||||||
export type TeamTeammateMember = z.infer<typeof TeamTeammateMemberSchema>
|
|
||||||
export type TeamMember = z.infer<typeof TeamMemberSchema>
|
|
||||||
export type TeamConfig = z.infer<typeof TeamConfigSchema>
|
|
||||||
export type TeamInboxMessage = z.infer<typeof TeamInboxMessageSchema>
|
|
||||||
export type TeamTask = z.infer<typeof TeamTaskSchema>
|
|
||||||
export type TeamSendMessageType = z.infer<typeof TeamSendMessageTypeSchema>
|
|
||||||
|
|
||||||
|
export const InboxMessageSchema = z.object({
|
||||||
|
id: z.string().min(1),
|
||||||
|
type: MessageTypeSchema,
|
||||||
|
sender: z.string().min(1),
|
||||||
|
recipient: z.string().min(1),
|
||||||
|
content: z.string().optional(),
|
||||||
|
summary: z.string().optional(),
|
||||||
|
timestamp: z.string().datetime(),
|
||||||
|
read: z.boolean(),
|
||||||
|
requestId: z.string().optional(),
|
||||||
|
approve: z.boolean().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type InboxMessage = z.infer<typeof InboxMessageSchema>
|
||||||
|
|
||||||
|
// Task schema (reuse from task/types.ts)
|
||||||
|
export const TeamTaskSchema = TaskObjectSchema
|
||||||
|
|
||||||
|
export type TeamTask = z.infer<typeof TeamTaskSchema>
|
||||||
|
|
||||||
|
// Input schemas for tools
|
||||||
export const TeamCreateInputSchema = z.object({
|
export const TeamCreateInputSchema = z.object({
|
||||||
team_name: z.string(),
|
team_name: z.string().regex(/^[A-Za-z0-9_-]+$/, "Team name must contain only letters, numbers, hyphens, and underscores").max(64),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export type TeamCreateInput = z.infer<typeof TeamCreateInputSchema>
|
||||||
|
|
||||||
export const TeamDeleteInputSchema = z.object({
|
export const TeamDeleteInputSchema = z.object({
|
||||||
team_name: z.string(),
|
team_name: z.string().regex(/^[A-Za-z0-9_-]+$/, "Team name must contain only letters, numbers, hyphens, and underscores").max(64),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const TeamReadConfigInputSchema = z.object({
|
export type TeamDeleteInput = z.infer<typeof TeamDeleteInputSchema>
|
||||||
team_name: z.string(),
|
|
||||||
})
|
|
||||||
|
|
||||||
export const TeamSpawnInputSchema = z.object({
|
export const SendMessageInputSchema = z.discriminatedUnion("type", [
|
||||||
team_name: z.string(),
|
z.object({
|
||||||
name: z.string(),
|
team_name: z.string().regex(/^[A-Za-z0-9_-]+$/, "Team name must contain only letters, numbers, hyphens, and underscores").max(64),
|
||||||
prompt: z.string(),
|
type: z.literal("message"),
|
||||||
category: z.string(),
|
recipient: z.string().min(1),
|
||||||
subagent_type: z.string().optional(),
|
content: z.string().optional(),
|
||||||
model: z.string().optional(),
|
summary: z.string().optional(),
|
||||||
plan_mode_required: z.boolean().optional(),
|
|
||||||
})
|
|
||||||
|
|
||||||
function normalizeTeamRecipient(recipient: string): string {
|
|
||||||
const trimmed = recipient.trim()
|
|
||||||
const atIndex = trimmed.indexOf("@")
|
|
||||||
if (atIndex <= 0) {
|
|
||||||
return trimmed
|
|
||||||
}
|
|
||||||
|
|
||||||
return trimmed.slice(0, atIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TeamSendMessageInputSchema = z.object({
|
|
||||||
team_name: z.string(),
|
|
||||||
type: TeamSendMessageTypeSchema,
|
|
||||||
recipient: z.string().optional().transform((value) => {
|
|
||||||
if (value === undefined) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
return normalizeTeamRecipient(value)
|
|
||||||
}),
|
}),
|
||||||
content: z.string().optional(),
|
z.object({
|
||||||
summary: z.string().optional(),
|
team_name: z.string().regex(/^[A-Za-z0-9_-]+$/, "Team name must contain only letters, numbers, hyphens, and underscores").max(64),
|
||||||
request_id: z.string().optional(),
|
type: z.literal("broadcast"),
|
||||||
approve: z.boolean().optional(),
|
content: z.string().optional(),
|
||||||
sender: z.string().optional(),
|
summary: z.string().optional(),
|
||||||
})
|
}),
|
||||||
|
z.object({
|
||||||
|
team_name: z.string().regex(/^[A-Za-z0-9_-]+$/, "Team name must contain only letters, numbers, hyphens, and underscores").max(64),
|
||||||
|
type: z.literal("shutdown_request"),
|
||||||
|
recipient: z.string().min(1),
|
||||||
|
content: z.string().optional(),
|
||||||
|
summary: z.string().optional(),
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
team_name: z.string().regex(/^[A-Za-z0-9_-]+$/, "Team name must contain only letters, numbers, hyphens, and underscores").max(64),
|
||||||
|
type: z.literal("shutdown_response"),
|
||||||
|
request_id: z.string().min(1),
|
||||||
|
approve: z.boolean(),
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
team_name: z.string().regex(/^[A-Za-z0-9_-]+$/, "Team name must contain only letters, numbers, hyphens, and underscores").max(64),
|
||||||
|
type: z.literal("plan_approval_response"),
|
||||||
|
request_id: z.string().min(1),
|
||||||
|
approve: z.boolean(),
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
|
||||||
export const TeamReadInboxInputSchema = z.object({
|
export type SendMessageInput = z.infer<typeof SendMessageInputSchema>
|
||||||
team_name: z.string(),
|
|
||||||
agent_name: z.string(),
|
export const ReadInboxInputSchema = z.object({
|
||||||
|
team_name: z.string().regex(/^[A-Za-z0-9_-]+$/, "Team name must contain only letters, numbers, hyphens, and underscores").max(64),
|
||||||
|
agent_name: z.string().min(1),
|
||||||
unread_only: z.boolean().optional(),
|
unread_only: z.boolean().optional(),
|
||||||
mark_as_read: z.boolean().optional(),
|
mark_as_read: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const TeamTaskCreateInputSchema = z.object({
|
export type ReadInboxInput = z.infer<typeof ReadInboxInputSchema>
|
||||||
team_name: z.string(),
|
|
||||||
subject: z.string(),
|
export const ReadConfigInputSchema = z.object({
|
||||||
description: z.string(),
|
team_name: z.string().regex(/^[A-Za-z0-9_-]+$/, "Team name must contain only letters, numbers, hyphens, and underscores").max(64),
|
||||||
active_form: z.string().optional(),
|
|
||||||
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const TeamTaskUpdateInputSchema = z.object({
|
export type ReadConfigInput = z.infer<typeof ReadConfigInputSchema>
|
||||||
team_name: z.string(),
|
|
||||||
task_id: z.string(),
|
export const TeamSpawnInputSchema = z.object({
|
||||||
status: TeamTaskStatusSchema.optional(),
|
team_name: z.string().regex(/^[A-Za-z0-9_-]+$/, "Team name must contain only letters, numbers, hyphens, and underscores").max(64),
|
||||||
owner: z.string().optional(),
|
name: z.string().min(1),
|
||||||
subject: z.string().optional(),
|
category: z.string().min(1),
|
||||||
description: z.string().optional(),
|
prompt: z.string().min(1),
|
||||||
active_form: z.string().optional(),
|
|
||||||
add_blocks: z.array(z.string()).optional(),
|
|
||||||
add_blocked_by: z.array(z.string()).optional(),
|
|
||||||
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const TeamTaskListInputSchema = z.object({
|
export type TeamSpawnInput = z.infer<typeof TeamSpawnInputSchema>
|
||||||
team_name: z.string(),
|
|
||||||
|
export const ForceKillTeammateInputSchema = z.object({
|
||||||
|
team_name: z.string().regex(/^[A-Za-z0-9_-]+$/, "Team name must contain only letters, numbers, hyphens, and underscores").max(64),
|
||||||
|
teammate_name: z.string().min(1),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const TeamTaskGetInputSchema = z.object({
|
export type ForceKillTeammateInput = z.infer<typeof ForceKillTeammateInputSchema>
|
||||||
team_name: z.string(),
|
|
||||||
task_id: z.string(),
|
export const ProcessShutdownApprovedInputSchema = z.object({
|
||||||
|
team_name: z.string().regex(/^[A-Za-z0-9_-]+$/, "Team name must contain only letters, numbers, hyphens, and underscores").max(64),
|
||||||
|
teammate_name: z.string().min(1),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const TeamForceKillInputSchema = z.object({
|
export type ProcessShutdownApprovedInput = z.infer<typeof ProcessShutdownApprovedInputSchema>
|
||||||
team_name: z.string(),
|
|
||||||
agent_name: z.string(),
|
|
||||||
})
|
|
||||||
|
|
||||||
export const TeamProcessShutdownInputSchema = z.object({
|
|
||||||
team_name: z.string(),
|
|
||||||
agent_name: z.string(),
|
|
||||||
})
|
|
||||||
|
|
||||||
export interface TeamToolContext {
|
|
||||||
sessionID: string
|
|
||||||
messageID: string
|
|
||||||
abort: AbortSignal
|
|
||||||
agent?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isTeammateMember(member: TeamMember): member is TeamTeammateMember {
|
|
||||||
return member.agentType !== "team-lead"
|
|
||||||
}
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user