| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547 |
- import { describe, expect, test } from "bun:test"
- import type { Event } from "@kirincode-ai/sdk/v2"
- import { entryBody } from "@/cli/cmd/run/entry.body"
- import {
- bootstrapSubagentCalls,
- bootstrapSubagentData,
- createSubagentData,
- reduceSubagentData,
- snapshotSubagentData,
- } from "@/cli/cmd/run/subagent-data"
- type SessionMessage = Parameters<typeof bootstrapSubagentData>[0]["messages"][number]
- type ChildMessage = Parameters<typeof bootstrapSubagentCalls>[0]["messages"][number]
- 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 reduce(data: ReturnType<typeof createSubagentData>, event: unknown) {
- return reduceSubagentData({
- data,
- event: event as Event,
- sessionID: "parent-1",
- thinking: true,
- limits: {},
- })
- }
- function taskMessage(sessionID: string, status: "running" | "completed" | "interrupted" = "completed"): SessionMessage {
- if (status === "running") {
- return {
- parts: [
- {
- id: `part-${sessionID}`,
- sessionID: "parent-1",
- messageID: `msg-${sessionID}`,
- type: "tool",
- callID: `call-${sessionID}`,
- tool: "task",
- state: {
- status: "running",
- input: {
- description: "Scan reducer paths",
- subagent_type: "explore",
- },
- title: "Reducer touchpoints",
- metadata: {
- sessionId: sessionID,
- toolcalls: 4,
- },
- time: { start: 1 },
- },
- },
- ],
- }
- }
- if (status === "interrupted") {
- return {
- parts: [
- {
- id: `part-${sessionID}`,
- sessionID: "parent-1",
- messageID: `msg-${sessionID}`,
- type: "tool",
- callID: `call-${sessionID}`,
- tool: "task",
- state: {
- status: "error",
- input: {
- description: "Scan reducer paths",
- subagent_type: "explore",
- },
- error: "Tool execution aborted",
- metadata: {
- sessionId: sessionID,
- toolcalls: 4,
- interrupted: true,
- },
- time: { start: 1, end: 2 },
- },
- },
- ],
- }
- }
- return {
- parts: [
- {
- id: `part-${sessionID}`,
- sessionID: "parent-1",
- messageID: `msg-${sessionID}`,
- type: "tool",
- callID: `call-${sessionID}`,
- tool: "task",
- state: {
- status: "completed",
- input: {
- description: "Scan reducer paths",
- subagent_type: "explore",
- },
- output: "",
- title: "Reducer touchpoints",
- metadata: {
- sessionId: sessionID,
- toolcalls: 4,
- },
- time: { start: 1, end: 2 },
- },
- },
- ],
- }
- }
- function question(id: string, sessionID: string) {
- return {
- id,
- sessionID,
- questions: [
- {
- question: "Mode?",
- header: "Mode",
- options: [{ label: "Fast", description: "Quick pass" }],
- multiple: false,
- },
- ],
- }
- }
- function childMessage(input: {
- messageID: string
- sessionID: string
- role: "user" | "assistant"
- parts: ChildMessage["parts"]
- }) {
- if (input.role === "user") {
- return {
- info: {
- id: input.messageID,
- sessionID: input.sessionID,
- role: "user",
- time: {
- created: 1,
- },
- agent: "test",
- model: {
- providerID: "openai",
- modelID: "gpt-5",
- },
- },
- parts: input.parts,
- } satisfies ChildMessage
- }
- return {
- info: {
- id: input.messageID,
- sessionID: input.sessionID,
- role: "assistant",
- time: {
- created: 2,
- completed: 3,
- },
- parentID: "msg-user-1",
- providerID: "openai",
- modelID: "gpt-5",
- mode: "default",
- agent: "explore",
- path: {
- cwd: "/tmp",
- root: "/tmp",
- },
- cost: 0,
- tokens: {
- input: 1,
- output: 1,
- reasoning: 0,
- cache: {
- read: 0,
- write: 0,
- },
- },
- finish: "stop",
- },
- parts: input.parts,
- } satisfies ChildMessage
- }
- describe("run subagent data", () => {
- test("bootstraps tabs and child blockers from parent task parts", () => {
- const data = createSubagentData()
- expect(
- bootstrapSubagentData({
- data,
- messages: [taskMessage("child-1")],
- 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)
- const snapshot = snapshotSubagentData(data)
- expect(snapshot.tabs).toEqual([
- expect.objectContaining({
- sessionID: "child-1",
- label: "Explore",
- description: "Scan reducer paths",
- title: "Reducer touchpoints",
- status: "completed",
- toolCalls: 4,
- }),
- ])
- expect(snapshot.details).toEqual({
- "child-1": {
- sessionID: "child-1",
- commits: [],
- },
- })
- expect(snapshot.permissions.map((item) => item.id)).toEqual(["perm-1"])
- expect(snapshot.questions.map((item) => item.id)).toEqual(["question-1"])
- })
- test("marks interrupted task tabs as cancelled during bootstrap", () => {
- const data = createSubagentData()
- bootstrapSubagentData({
- data,
- messages: [taskMessage("child-1", "interrupted")],
- children: [{ id: "child-1" }],
- permissions: [],
- questions: [],
- })
- expect(snapshotSubagentData(data).tabs).toEqual([
- expect.objectContaining({
- sessionID: "child-1",
- status: "cancelled",
- }),
- ])
- })
- test("captures child activity and blocker metadata in the footer detail state", () => {
- const data = createSubagentData()
- bootstrapSubagentData({
- data,
- messages: [taskMessage("child-1", "running")],
- children: [{ id: "child-1" }],
- permissions: [],
- questions: [],
- })
- reduce(data, {
- type: "message.part.updated",
- properties: {
- part: {
- id: "txt-user-1",
- messageID: "msg-user-1",
- sessionID: "child-1",
- type: "text",
- text: "Inspect footer tabs",
- },
- },
- })
- reduce(data, {
- type: "message.updated",
- properties: {
- sessionID: "child-1",
- info: {
- id: "msg-user-1",
- role: "user",
- },
- },
- })
- reduce(data, {
- type: "message.updated",
- properties: {
- sessionID: "child-1",
- info: {
- id: "msg-assistant-1",
- role: "assistant",
- },
- },
- })
- reduce(data, {
- 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 },
- },
- },
- })
- reduce(data, {
- 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 },
- },
- },
- },
- })
- reduce(data, {
- 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",
- },
- },
- })
- reduce(data, {
- type: "message.part.updated",
- properties: {
- part: {
- id: "txt-1",
- messageID: "msg-assistant-1",
- sessionID: "child-1",
- type: "text",
- text: "hello",
- },
- },
- })
- reduce(data, {
- type: "message.part.delta",
- properties: {
- sessionID: "child-1",
- messageID: "msg-assistant-1",
- partID: "txt-1",
- field: "text",
- delta: " world",
- },
- })
- const snapshot = snapshotSubagentData(data)
- expect(snapshot.tabs).toEqual([expect.objectContaining({ sessionID: "child-1", status: "running" })])
- expect(visible(snapshot.details["child-1"]?.commits ?? [])).toEqual([
- "› Inspect footer tabs",
- "_Thinking:_ planning next steps",
- "$ git status --short",
- "hello world",
- ])
- expect(snapshot.permissions).toEqual([
- expect.objectContaining({
- id: "perm-1",
- metadata: {
- input: {
- command: "git status --short",
- },
- },
- }),
- ])
- expect(snapshot.questions).toEqual([])
- })
- test("replays bootstrapped child session messages into inspector commits", () => {
- const data = createSubagentData()
- bootstrapSubagentData({
- data,
- messages: [taskMessage("child-1", "completed")],
- children: [{ id: "child-1" }],
- permissions: [],
- questions: [],
- })
- expect(
- bootstrapSubagentCalls({
- data,
- sessionID: "child-1",
- messages: [
- childMessage({
- messageID: "msg-user-1",
- sessionID: "child-1",
- role: "user",
- parts: [
- {
- id: "txt-user-1",
- messageID: "msg-user-1",
- sessionID: "child-1",
- type: "text",
- text: "Inspect footer tabs",
- time: { start: 1, end: 1 },
- },
- ],
- }),
- childMessage({
- messageID: "msg-assistant-1",
- sessionID: "child-1",
- role: "assistant",
- parts: [
- {
- id: "reason-1",
- messageID: "msg-assistant-1",
- sessionID: "child-1",
- type: "reasoning",
- text: "planning next steps",
- time: { start: 2, end: 2 },
- },
- {
- id: "txt-1",
- messageID: "msg-assistant-1",
- sessionID: "child-1",
- type: "text",
- text: "hello world",
- time: { start: 2, end: 3 },
- },
- ],
- }),
- ],
- thinking: true,
- limits: {},
- }),
- ).toBe(true)
- expect(visible(snapshotSubagentData(data).details["child-1"]?.commits ?? [])).toEqual([
- "› Inspect footer tabs",
- "_Thinking:_ planning next steps",
- "hello world",
- ])
- })
- test("marks a running tab cancelled when the child session aborts", () => {
- const data = createSubagentData()
- bootstrapSubagentData({
- data,
- messages: [taskMessage("child-1", "running")],
- children: [{ id: "child-1" }],
- permissions: [],
- questions: [],
- })
- reduce(data, {
- type: "message.updated",
- properties: {
- sessionID: "child-1",
- info: {
- id: "msg-assistant-1",
- sessionID: "child-1",
- role: "assistant",
- time: {
- created: 1,
- completed: 2,
- },
- error: {
- name: "MessageAbortedError",
- data: {
- message: "Aborted",
- },
- },
- parentID: "msg-user-1",
- providerID: "openai",
- modelID: "gpt-5",
- mode: "default",
- agent: "explore",
- path: {
- cwd: "/tmp",
- root: "/tmp",
- },
- cost: 0,
- tokens: {
- input: 1,
- output: 1,
- reasoning: 0,
- cache: {
- read: 0,
- write: 0,
- },
- },
- finish: "error",
- },
- },
- })
- expect(snapshotSubagentData(data).tabs).toEqual([
- expect.objectContaining({
- sessionID: "child-1",
- status: "cancelled",
- }),
- ])
- })
- })
|