opencode/packages/opencode/test/cli/run/subagent-data.test.ts
2026-04-20 20:56:10 +02:00

394 lines
9.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { describe, expect, test } from "bun:test"
import { entryBody } from "@/cli/cmd/run/entry.body"
import {
bootstrapSubagentData,
clearFinishedSubagents,
createSubagentData,
reduceSubagentData,
snapshotSelectedSubagentData,
snapshotSubagentData,
} from "@/cli/cmd/run/subagent-data"
function visible(commits: Array<Parameters<typeof entryBody>[0]>) {
return commits.flatMap((item) => {
const body = entryBody(item)
if (body.type === "none") {
return []
}
if (body.type === "structured") {
if (body.snapshot.kind === "code" || body.snapshot.kind === "task") {
return [body.snapshot.title]
}
if (body.snapshot.kind === "diff") {
return body.snapshot.items.map((item) => item.title)
}
if (body.snapshot.kind === "todo") {
return ["# Todos"]
}
return ["# Questions"]
}
return [body.content]
})
}
function taskMessage(sessionID: string, status: "running" | "completed" | "error" = "completed") {
return {
info: {
id: `msg-${sessionID}`,
role: "assistant",
},
parts: [
{
id: `part-${sessionID}`,
sessionID: "parent-1",
messageID: `msg-${sessionID}`,
type: "tool",
callID: `call-${sessionID}`,
tool: "task",
state: {
status,
input: {
description: "Scan reducer paths",
subagent_type: "explore",
},
title: "Reducer touchpoints",
metadata: {
sessionId: sessionID,
toolcalls: 4,
},
time: status === "running" ? { start: 1 } : { start: 1, end: 2 },
},
},
],
} as const
}
function question(id: string, sessionID: string) {
return {
id,
sessionID,
questions: [
{
question: "Mode?",
header: "Mode",
options: [{ label: "Fast", description: "Quick pass" }],
},
],
}
}
describe("run subagent data", () => {
test("bootstraps tabs and child blockers from parent task parts", () => {
const data = createSubagentData()
expect(
bootstrapSubagentData({
data,
messages: [taskMessage("child-1") as never],
children: [{ id: "child-1" }, { id: "child-2" }],
permissions: [
{
id: "perm-1",
sessionID: "child-1",
permission: "read",
patterns: ["src/**/*.ts"],
metadata: {},
always: [],
},
{
id: "perm-2",
sessionID: "other",
permission: "read",
patterns: ["src/**/*.ts"],
metadata: {},
always: [],
},
],
questions: [question("question-1", "child-1"), question("question-2", "other")],
}),
).toBe(true)
expect(snapshotSubagentData(data)).toEqual({
tabs: [
expect.objectContaining({
sessionID: "child-1",
label: "Explore",
description: "Scan reducer paths",
title: "Reducer touchpoints",
status: "completed",
toolCalls: 4,
}),
],
details: {
"child-1": {
sessionID: "child-1",
commits: [],
},
},
permissions: [expect.objectContaining({ id: "perm-1", sessionID: "child-1" })],
questions: [expect.objectContaining({ id: "question-1", sessionID: "child-1" })],
})
})
test("reduces child text tool and blocker events into footer detail state", () => {
const data = createSubagentData()
bootstrapSubagentData({
data,
messages: [taskMessage("child-1", "running") as never],
children: [{ id: "child-1" }],
permissions: [],
questions: [],
})
reduceSubagentData({
data,
sessionID: "parent-1",
thinking: true,
limits: {},
event: {
type: "message.part.updated",
properties: {
part: {
id: "txt-1",
messageID: "msg-user-1",
sessionID: "child-1",
type: "text",
text: "Inspect footer tabs",
},
},
} as never,
})
reduceSubagentData({
data,
sessionID: "parent-1",
thinking: true,
limits: {},
event: {
type: "message.updated",
properties: {
sessionID: "child-1",
info: {
id: "msg-user-1",
role: "user",
},
},
} as never,
})
reduceSubagentData({
data,
sessionID: "parent-1",
thinking: true,
limits: {},
event: {
type: "message.updated",
properties: {
sessionID: "child-1",
info: {
id: "msg-assistant-1",
role: "assistant",
},
},
} as never,
})
reduceSubagentData({
data,
sessionID: "parent-1",
thinking: true,
limits: {},
event: {
type: "message.part.updated",
properties: {
part: {
id: "reason-1",
messageID: "msg-assistant-1",
sessionID: "child-1",
type: "reasoning",
text: "planning next steps",
time: { start: 1 },
},
},
} as never,
})
reduceSubagentData({
data,
sessionID: "parent-1",
thinking: true,
limits: {},
event: {
type: "message.part.updated",
properties: {
part: {
id: "tool-1",
messageID: "msg-assistant-1",
sessionID: "child-1",
type: "tool",
callID: "call-1",
tool: "bash",
state: {
status: "running",
input: {
command: "git status --short",
},
time: { start: 1 },
},
},
},
} as never,
})
reduceSubagentData({
data,
sessionID: "parent-1",
thinking: true,
limits: {},
event: {
type: "permission.asked",
properties: {
id: "perm-1",
sessionID: "child-1",
permission: "bash",
patterns: ["git status --short"],
metadata: {},
always: [],
tool: {
messageID: "msg-assistant-1",
callID: "call-1",
},
},
} as never,
})
const snapshot = snapshotSubagentData(data)
expect(snapshot.tabs).toEqual([expect.objectContaining({ sessionID: "child-1", status: "running" })])
expect(snapshot.details["child-1"]).toEqual({
sessionID: "child-1",
commits: expect.any(Array),
})
expect(visible(snapshot.details["child-1"]?.commits ?? [])).toEqual([
" Inspect footer tabs",
"_Thinking:_ planning next steps",
"# Shell\n$ git status --short",
])
expect(snapshot.permissions).toEqual([
expect.objectContaining({
id: "perm-1",
metadata: {
input: {
command: "git status --short",
},
},
}),
])
expect(snapshot.questions).toEqual([])
})
test("continues live child text streams", () => {
const data = createSubagentData()
bootstrapSubagentData({
data,
messages: [taskMessage("child-1", "running") as never],
children: [{ id: "child-1" }],
permissions: [],
questions: [],
})
reduceSubagentData({
data,
sessionID: "parent-1",
thinking: true,
limits: {},
event: {
type: "message.updated",
properties: {
sessionID: "child-1",
info: {
id: "msg-assistant-1",
role: "assistant",
},
},
} as never,
})
reduceSubagentData({
data,
sessionID: "parent-1",
thinking: true,
limits: {},
event: {
type: "message.part.updated",
properties: {
part: {
id: "txt-1",
messageID: "msg-assistant-1",
sessionID: "child-1",
type: "text",
text: "hello",
},
},
} as never,
})
reduceSubagentData({
data,
sessionID: "parent-1",
thinking: true,
limits: {},
event: {
type: "message.part.delta",
properties: {
sessionID: "child-1",
messageID: "msg-assistant-1",
partID: "txt-1",
field: "text",
delta: " world",
},
} as never,
})
reduceSubagentData({
data,
sessionID: "parent-1",
thinking: true,
limits: {},
event: {
type: "message.part.updated",
properties: {
part: {
id: "txt-1",
messageID: "msg-assistant-1",
sessionID: "child-1",
type: "text",
text: "hello world",
time: { start: 1, end: 2 },
},
},
} as never,
})
expect(visible(snapshotSelectedSubagentData(data, "child-1").details["child-1"]?.commits ?? [])).toEqual([
"hello world",
])
})
test("clears finished tabs on the next parent prompt", () => {
const data = createSubagentData()
bootstrapSubagentData({
data,
messages: [taskMessage("child-1", "completed") as never, taskMessage("child-2", "running") as never],
children: [{ id: "child-1" }, { id: "child-2" }],
permissions: [],
questions: [],
})
expect(clearFinishedSubagents(data)).toBe(true)
expect(snapshotSubagentData(data).tabs).toEqual([
expect.objectContaining({ sessionID: "child-2", status: "running" }),
])
})
})