feat(team-mode): add team query tools with tests
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
/// <reference types="bun-types" />
|
||||
|
||||
import { describe, expect, mock, test } from "bun:test"
|
||||
|
||||
import type { ToolContext } from "@opencode-ai/plugin/tool"
|
||||
import { TeamModeConfigSchema } from "../../../config/schema/team-mode"
|
||||
import type { OpencodeClient } from "../../../tools/delegate-task/types"
|
||||
|
||||
const mockClient = {} as OpencodeClient
|
||||
|
||||
let aggregateStatusImplementation: typeof import("../team-runtime/status").aggregateStatus = async () => {
|
||||
throw new Error("aggregateStatusImplementation not set")
|
||||
}
|
||||
|
||||
let discoverTeamSpecsImplementation: typeof import("../team-registry/paths").discoverTeamSpecs = async () => {
|
||||
throw new Error("discoverTeamSpecsImplementation not set")
|
||||
}
|
||||
|
||||
let loadTeamSpecImplementation: typeof import("../team-registry/loader").loadTeamSpec = async () => {
|
||||
throw new Error("loadTeamSpecImplementation not set")
|
||||
}
|
||||
|
||||
let listActiveTeamsImplementation: typeof import("../team-state-store/store").listActiveTeams = async () => {
|
||||
throw new Error("listActiveTeamsImplementation not set")
|
||||
}
|
||||
|
||||
mock.module("../team-runtime/status", () => ({
|
||||
aggregateStatus: (...args: Parameters<typeof aggregateStatusImplementation>) => aggregateStatusImplementation(...args),
|
||||
}))
|
||||
|
||||
mock.module("../team-registry/paths", () => ({
|
||||
discoverTeamSpecs: (...args: Parameters<typeof discoverTeamSpecsImplementation>) => discoverTeamSpecsImplementation(...args),
|
||||
}))
|
||||
|
||||
mock.module("../team-registry/loader", () => ({
|
||||
loadTeamSpec: (...args: Parameters<typeof loadTeamSpecImplementation>) => loadTeamSpecImplementation(...args),
|
||||
}))
|
||||
|
||||
mock.module("../team-state-store/store", () => ({
|
||||
listActiveTeams: (...args: Parameters<typeof listActiveTeamsImplementation>) => listActiveTeamsImplementation(...args),
|
||||
}))
|
||||
|
||||
import { createTeamListTool, createTeamStatusTool } from "./query"
|
||||
|
||||
function createMockContext(): ToolContext {
|
||||
return {
|
||||
sessionID: "session",
|
||||
messageID: "message",
|
||||
agent: "agent",
|
||||
directory: "/tmp/team-mode",
|
||||
worktree: "/tmp/team-mode",
|
||||
abort: new AbortController().signal,
|
||||
metadata: mock(() => {}),
|
||||
ask: async () => undefined,
|
||||
} satisfies ToolContext
|
||||
}
|
||||
|
||||
describe("query tools", () => {
|
||||
test("team_status returns aggregated team status", async () => {
|
||||
// given
|
||||
const config = TeamModeConfigSchema.parse({ base_dir: "/tmp/team-mode" })
|
||||
const expectedStatus = {
|
||||
teamRunId: "team-run-1",
|
||||
teamName: "team-alpha",
|
||||
status: "active",
|
||||
createdAt: 1,
|
||||
members: [{ name: "worker", unreadMessages: 0 }],
|
||||
tasks: { pending: 0, claimed: 0, in_progress: 0, completed: 0, deleted: 0, total: 0 },
|
||||
shutdownRequests: [],
|
||||
concurrency: { runningOnSameModel: 0, queuedOnSameModel: 0 },
|
||||
bounds: { maxMembers: 8, maxParallelMembers: 4, maxMessagesPerRun: 10000, maxWallClockMinutes: 120, maxMemberTurns: 500 },
|
||||
staleLocks: [],
|
||||
} satisfies Awaited<ReturnType<typeof aggregateStatusImplementation>>
|
||||
aggregateStatusImplementation = async (teamRunId, passedConfig) => {
|
||||
expect(teamRunId).toBe("team-run-1")
|
||||
expect(passedConfig).toBe(config)
|
||||
return expectedStatus
|
||||
}
|
||||
const tool = createTeamStatusTool(config, mockClient)
|
||||
|
||||
// when
|
||||
const result = JSON.parse(await tool.execute({ teamRunId: "team-run-1" }, createMockContext()))
|
||||
|
||||
// then
|
||||
expect(result).toEqual(expectedStatus)
|
||||
})
|
||||
|
||||
test("team_list includes declared-only teams", async () => {
|
||||
// given
|
||||
const config = TeamModeConfigSchema.parse({ base_dir: "/tmp/team-mode" })
|
||||
discoverTeamSpecsImplementation = async () => [
|
||||
{ name: "foo", scope: "project", path: "/tmp/project/foo/config.json" },
|
||||
]
|
||||
loadTeamSpecImplementation = async (teamName) => {
|
||||
expect(teamName).toBe("foo")
|
||||
return {
|
||||
version: 1,
|
||||
name: "foo",
|
||||
createdAt: 1,
|
||||
leadAgentId: "lead",
|
||||
members: [{ kind: "category", name: "member-a", category: "agent", prompt: "do", backendType: "in-process", isActive: true }],
|
||||
}
|
||||
}
|
||||
listActiveTeamsImplementation = async () => [
|
||||
{ teamRunId: "run-1", teamName: "bar", status: "active", memberCount: 3, scope: "user" },
|
||||
]
|
||||
const tool = createTeamListTool(config, mockClient)
|
||||
|
||||
// when
|
||||
const result = JSON.parse(await tool.execute({}, createMockContext()))
|
||||
|
||||
// then
|
||||
expect(result).toEqual([
|
||||
{ name: "foo", scope: "project", status: "not-started", teamRunId: undefined, memberCount: 1 },
|
||||
{ name: "bar", scope: "user", status: "active", teamRunId: "run-1", memberCount: 3 },
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,96 @@
|
||||
import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool"
|
||||
|
||||
import type { TeamModeConfig } from "../../../config/schema/team-mode"
|
||||
import type { OpencodeClient } from "../../../tools/delegate-task/types"
|
||||
import { loadTeamSpec } from "../team-registry/loader"
|
||||
import { aggregateStatus } from "../team-runtime/status"
|
||||
import { discoverTeamSpecs } from "../team-registry/paths"
|
||||
import { listActiveTeams } from "../team-state-store/store"
|
||||
|
||||
type TeamListScope = "user" | "project" | "all"
|
||||
|
||||
type TeamListEntry = {
|
||||
name: string
|
||||
scope: "user" | "project"
|
||||
status: string
|
||||
teamRunId?: string
|
||||
memberCount: number
|
||||
}
|
||||
|
||||
export function createTeamStatusTool(
|
||||
config: TeamModeConfig,
|
||||
client: OpencodeClient,
|
||||
backgroundManager?: Parameters<typeof aggregateStatus>[2],
|
||||
): ToolDefinition {
|
||||
void client
|
||||
|
||||
return tool({
|
||||
description: "Return full status for a team run.",
|
||||
args: {
|
||||
teamRunId: tool.schema.string().describe("Team run ID"),
|
||||
},
|
||||
execute: async (args: { teamRunId: string }) => JSON.stringify(await aggregateStatus(args.teamRunId, config, backgroundManager)),
|
||||
})
|
||||
}
|
||||
|
||||
export function createTeamListTool(config: TeamModeConfig, client: OpencodeClient): ToolDefinition {
|
||||
void client
|
||||
|
||||
return tool({
|
||||
description: "List declared and active teams.",
|
||||
args: {
|
||||
scope: tool.schema.union([
|
||||
tool.schema.literal("user"),
|
||||
tool.schema.literal("project"),
|
||||
tool.schema.literal("all"),
|
||||
]).optional().describe("Team scope filter"),
|
||||
},
|
||||
execute: async (args: { scope?: TeamListScope }) => {
|
||||
const scope = args.scope ?? "all"
|
||||
const projectRoot = process.cwd()
|
||||
const declaredTeamSpecs = await discoverTeamSpecs(config, projectRoot)
|
||||
const activeTeams = await listActiveTeams(config)
|
||||
|
||||
const filteredDeclaredTeamSpecs = scope === "all"
|
||||
? declaredTeamSpecs
|
||||
: declaredTeamSpecs.filter((teamSpec) => teamSpec.scope === scope)
|
||||
|
||||
const declaredTeamSpecsByName = new Map(
|
||||
await Promise.all(filteredDeclaredTeamSpecs.map(async (teamSpec) => {
|
||||
const loadedTeamSpec = await loadTeamSpec(teamSpec.name, config, projectRoot)
|
||||
return [teamSpec.name, loadedTeamSpec.members.length] as const
|
||||
})),
|
||||
)
|
||||
|
||||
const activeTeamsByName = new Map(activeTeams.map((team) => [team.teamName, team]))
|
||||
|
||||
const teamEntries: TeamListEntry[] = []
|
||||
|
||||
for (const declaredTeamSpec of filteredDeclaredTeamSpecs) {
|
||||
const activeTeam = activeTeamsByName.get(declaredTeamSpec.name)
|
||||
const declaredTeamSpecMemberCount = declaredTeamSpecsByName.get(declaredTeamSpec.name)
|
||||
teamEntries.push({
|
||||
name: declaredTeamSpec.name,
|
||||
scope: declaredTeamSpec.scope,
|
||||
status: activeTeam?.status ?? "not-started",
|
||||
teamRunId: activeTeam?.teamRunId,
|
||||
memberCount: activeTeam?.memberCount ?? declaredTeamSpecMemberCount ?? 0,
|
||||
})
|
||||
}
|
||||
|
||||
for (const activeTeam of activeTeams) {
|
||||
if (declaredTeamSpecsByName.has(activeTeam.teamName)) continue
|
||||
|
||||
teamEntries.push({
|
||||
name: activeTeam.teamName,
|
||||
scope: activeTeam.scope,
|
||||
status: activeTeam.status,
|
||||
teamRunId: activeTeam.teamRunId,
|
||||
memberCount: activeTeam.memberCount,
|
||||
})
|
||||
}
|
||||
|
||||
return JSON.stringify(teamEntries)
|
||||
},
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user