1import { describe, expect, it } from "vite-plus/test";
2import {
3 NodeId,
4 ProviderDriverKind,
5 ProviderInstanceId,
6 RunId,
7 ThreadId,
8 type OrchestrationV2Subagent,
9} from "@t3tools/contracts";
10import * as DateTime from "effect/DateTime";
11
12import { deriveThreadTurnSubagents, resolveSubagentPillSegment } from "./threadSubagents.ts";
13
14const at = (iso: string) => DateTime.makeUnsafe(iso);
15
16function subagent(
17 overrides: Omit<Partial<OrchestrationV2Subagent>, "id"> & { readonly id: string },
18): OrchestrationV2Subagent {
19 const { id, ...rest } = overrides;
20 return {
21 id: NodeId.make(id),
22 threadId: ThreadId.make("thread-1"),
23 runId: RunId.make("run-1"),
24 parentNodeId: NodeId.make("node-1"),
25 origin: "app_owned",
26 createdBy: "agent",
27 driver: ProviderDriverKind.make("codex"),
28 providerInstanceId: ProviderInstanceId.make("codex"),
29 providerThreadId: null,
30 childThreadId: ThreadId.make("thread-child"),
31 nativeTaskRef: null,
32 prompt: "Do the thing",
33 title: "Worker",
34 model: "gpt-5.4",
35 status: "running",
36 result: null,
37 startedAt: at("2026-06-20T00:00:00.000Z"),
38 completedAt: null,
39 updatedAt: at("2026-06-20T00:00:01.000Z"),
40 ...rest,
41 };
42}
43
44const run = (id: string, status: "running" | "completed") =>
45 ({ id: RunId.make(id), status }) as never;
46const activeRun = run("run-1", "running");
47const finishedRun = run("run-1", "completed");
48
49describe("deriveThreadTurnSubagents", () => {
50 it("returns null when the thread has never spawned an agent", () => {
51 expect(deriveThreadTurnSubagents({ runs: [activeRun], subagents: [] })).toBeNull();
52 });
53
54 it("scopes the roster to the active run and orders it by start time", () => {
55 const turn = deriveThreadTurnSubagents({
56 runs: [run("run-0", "completed"), activeRun],
57 subagents: [
58 subagent({ id: "b", startedAt: at("2026-06-20T00:00:05.000Z") }),
59 subagent({ id: "old", runId: RunId.make("run-0") }),
60 subagent({ id: "a", startedAt: at("2026-06-20T00:00:02.000Z") }),
61 ],
62 });
63
64 expect(turn?.runId).toBe("run-1");
65 expect(turn?.subagents.map((entry) => entry.id)).toEqual(["a", "b"]);
66 expect(turn?.turnActive).toBe(true);
67 });
68
69 it("falls back to the most recently updated agent's run once the turn settles", () => {
70 const turn = deriveThreadTurnSubagents({
71 runs: [finishedRun, run("run-2", "completed")],
72 subagents: [
73 subagent({ id: "old", status: "completed", updatedAt: at("2026-06-20T00:00:01.000Z") }),
74 subagent({
75 id: "recent",
76 runId: RunId.make("run-2"),
77 status: "completed",
78 updatedAt: at("2026-06-20T00:00:09.000Z"),
79 }),
80 ],
81 });
82
83 expect(turn?.runId).toBe("run-2");
84 expect(turn?.turnActive).toBe(false);
85 expect(turn?.subagents.map((entry) => entry.id)).toEqual(["recent"]);
86 });
87
88 it("counts idle as neither working nor done", () => {
89 const turn = deriveThreadTurnSubagents({
90 runs: [activeRun],
91 subagents: [
92 subagent({ id: "a", status: "running" }),
93 subagent({ id: "b", status: "idle" }),
94 subagent({ id: "c", status: "failed" }),
95 ],
96 });
97
98 expect(turn?.liveCount).toBe(1);
99 expect(turn?.settledCount).toBe(1);
100 expect(turn?.subagents).toHaveLength(3);
101 });
102});
103
104describe("resolveSubagentPillSegment", () => {
105 it("shows how many of the turn's agents are still working", () => {
106 const turn = deriveThreadTurnSubagents({
107 runs: [activeRun],
108 subagents: [
109 subagent({ id: "a", status: "running" }),
110 subagent({ id: "b", status: "waiting" }),
111 subagent({ id: "c", status: "completed" }),
112 ],
113 });
114
115 expect(resolveSubagentPillSegment(turn)).toEqual({
116 label: "2/3",
117 accessibilityLabel: "2 of 3 agents working",
118 });
119 });
120
121 it("reports the finished roster while the turn is still running", () => {
122 const turn = deriveThreadTurnSubagents({
123 runs: [activeRun],
124 subagents: [subagent({ id: "a", status: "completed" })],
125 });
126
127 expect(resolveSubagentPillSegment(turn)).toEqual({
128 label: "1 done",
129 accessibilityLabel: "1 agent done",
130 });
131 });
132
133 it("hides itself once the turn is over and nothing is working", () => {
134 const turn = deriveThreadTurnSubagents({
135 runs: [finishedRun],
136 subagents: [subagent({ id: "a", status: "completed" })],
137 });
138
139 expect(resolveSubagentPillSegment(turn)).toBeNull();
140 expect(resolveSubagentPillSegment(null)).toBeNull();
141 });
142
143 it("stays visible after the turn ends while an agent is still working", () => {
144 const turn = deriveThreadTurnSubagents({
145 runs: [finishedRun],
146 subagents: [subagent({ id: "a", status: "running" })],
147 });
148
149 expect(resolveSubagentPillSegment(turn)?.label).toBe("1/1");
150 });
151});