import { describe, expect, test } from "bun:test" import type { SessionInfo, SessionMessageAssistantTool, ShellInfo } from "@opencode/client/promise" import { createRoot } from "solid-js" import { createStore } from "solid-js/store" import { createSessionBackground } from "@/session/requests/background" const tool = ( id: string, name: string, metadata: Record, input: Record = {}, status: "completed" | "running" = "completed", time: SessionMessageAssistantTool["time"] = { created: 0 }, ): SessionMessageAssistantTool => ({ id, name, type: "tool", state: status === "running" ? { status, input, metadata } : { status, input, metadata, content: [{ type: "text", text: "backgrounded" }] }, time, }) const assistant = (id: string, content: SessionMessageAssistantTool[], completed?: number) => ({ id, type: "assistant" as const, agent: "build", model: { id: "model", providerID: "provider" }, content, time: { created: 0, completed }, }) const session = (id: string, parentID = "root", created = 0): SessionInfo => ({ id, title: id, parentID, projectID: "project", location: { directory: "/project" }, cost: 0, tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, time: { created, updated: 0 }, }) const shell = (id: string, command: string): ShellInfo => ({ id, command, status: "running", cwd: "/project", shell: "sh", file: "output", metadata: { sessionID: "root" }, time: { started: 0 }, }) const notification = (id: string, metadata: Record) => ({ id, type: "synthetic" as const, text: "complete", metadata, time: { created: 0 }, }) describe("createSessionBackground", () => { test("counts a subagent from its session status, whatever its call or completion notices say", () => { createRoot((dispose) => { const [store, setStore] = createStore<{ status: Record }>({ status: { stuck: "idle", noticed: "running" }, }) const background = createSessionBackground({ sessionID: () => "root", messages: () => [ // A background call stays "running" when no completion notice ever arrives for it. assistant("stuck-launch", [ tool("stuck-part", "subagent", { status: "running", sessionID: "stuck" }, { description: "Stuck" }), ]), // An old notice for a child says nothing about its later run. notification("noticed-done", { source: "subagent", childID: "noticed" }), assistant("noticed-launch", [ tool("noticed-part", "subagent", { status: "running", sessionID: "noticed" }, { description: "Again" }), ]), // History alone no longer counts a shell either. assistant("shell-launch", [tool("shell-part", "shell", { status: "running", shellID: "gone" })]), ], sessions: () => [session("stuck"), session("noticed")], status: (id) => store.status[id] ?? "idle", shells: () => [], }) expect(background.tasks().map((task) => [task.id, task.label])).toEqual([["noticed", "Again"]]) setStore("status", "stuck", "running") expect(background.tasks().map((task) => task.id)).toEqual(["stuck", "noticed"]) setStore("status", { stuck: "idle", noticed: "idle" }) expect(background.tasks()).toEqual([]) expect(background.subagents()).toEqual([]) dispose() }) }) test("counts every running descendant, as the TUI's family does, and nothing outside it", () => { createRoot((dispose) => { const background = createSessionBackground({ sessionID: () => "root", messages: () => [], sessions: () => [ session("child"), session("grandchild", "child"), session("other-child", "other"), ], status: () => "running", shells: () => [], }) expect(background.subagents().map((task) => task.id)).toEqual(["child", "grandchild"]) dispose() }) }) test("shows a blocking call until its child session is known, then follows the child's status", () => { createRoot((dispose) => { const [store, setStore] = createStore<{ messages: ReturnType[] sessions: SessionInfo[] status: Record }>({ messages: [ assistant("current", [ tool("call", "subagent", {}, { description: "Inspect", agent: "explore" }, "running", { created: 5, ran: 6, }), ]), ], sessions: [], status: {}, }) const background = createSessionBackground({ sessionID: () => "root", messages: () => store.messages, sessions: () => store.sessions, status: (id) => store.status[id] ?? "idle", shells: () => [], }) const pending = { id: "call", type: "subagent", sessionID: undefined, partID: "call", label: "Inspect", agent: "explore", started: 6, } expect(background.subagents()).toEqual([pending]) setStore("messages", 0, "content", 0, "state", "metadata", { sessionID: "child" }) expect(background.subagents()).toEqual([{ ...pending, id: "child", sessionID: "child" }]) setStore("sessions", [session("child")]) expect(background.subagents()).toEqual([]) setStore("status", "child", "running") expect(background.subagents()).toEqual([{ ...pending, id: "child", sessionID: "child" }]) // Blocking work is not background work. expect(background.tasks()).toEqual([]) setStore("messages", 0, "time", "completed", 7) expect(background.blocking()).toEqual([]) expect(background.tasks()).toEqual([{ ...pending, id: "child", sessionID: "child", partID: undefined }]) dispose() }) }) test("joins live tasks while idle without rescanning history, then switches sessions", () => { createRoot((dispose) => { const [store, setStore] = createStore<{ id: string | undefined messages: ReturnType[] sessions: SessionInfo[] status: Record shells: ShellInfo[] }>({ id: "root", messages: [assistant("assistant", [tool("child-part", "subagent", { status: "running", sessionID: "child" })])], sessions: [session("live-child", "root", 40), session("child", "root", 10)], status: { root: "idle", child: "idle", "live-child": "idle" }, shells: [{ ...shell("shell", "command"), status: "exited" }], }) let scans = 0 const background = createSessionBackground({ sessionID: () => store.id, messages: (id) => { scans += 1 return id === "root" ? store.messages : [] }, sessions: () => store.sessions, status: (id) => store.status[id], shells: () => store.shells, }) const blocking = background.blocking() expect(background.tasks()).toEqual([]) setStore("status", { child: "running", "live-child": "running" }) setStore("shells", 0, "status", "running") setStore("sessions", 1, "title", "renamed") setStore("shells", 0, "command", "updated command") const live = background.tasks() expect(live).toEqual([ { id: "live-child", type: "subagent", sessionID: "live-child", partID: undefined, label: "live-child", started: 40, }, { id: "child", type: "subagent", sessionID: "child", partID: undefined, label: "renamed", started: 0 }, { id: "shell", type: "shell", label: "updated command", started: 0 }, ]) expect(background.blocking()).toBe(blocking) expect(scans).toBe(1) setStore("id", "other") expect(background.tasks()).toEqual([]) setStore("id", "root") expect(background.tasks()).toEqual(live) setStore("status", { child: "idle", "live-child": "idle" }) setStore("shells", 0, "status", "exited") expect(background.tasks()).toEqual([]) expect(scans).toBe(3) setStore("id", undefined) expect(background.tasks()).toEqual([]) dispose() }) }) test("keeps blocking shells out of background tasks", () => { createRoot((dispose) => { const background = createSessionBackground({ sessionID: () => "root", messages: () => [ assistant("current", [ tool("shell-part", "shell", {}, { command: "build" }, "running", { created: 30, ran: 31 }), ]), ], sessions: () => [], status: () => "idle", shells: () => [shell("build", "build"), { ...shell("serve", "serve"), time: { started: 50 } }], }) expect(background.blocking()).toEqual([ { type: "shell", partID: "shell-part", id: undefined, label: "build", agent: undefined, started: 31 }, ]) expect(background.tasks()).toEqual([{ id: "serve", type: "shell", label: "serve", started: 50 }]) dispose() }) }) })