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 = (
45 id: string,
46 status: "queued" | "running" | "completed" | "cancelled" | "rolled_back",
47 overrides: { readonly ordinal?: number; readonly started?: boolean } = {},
48) =>
49 ({
50 id: RunId.make(id),
51 status,
52 ordinal: overrides.ordinal ?? Number(id.slice("run-".length)),
53 startedAt: overrides.started === false ? null : at("2026-06-20T00:00:00.000Z"),
54 }) as never;
55const activeRun = run("run-1", "running");
56const finishedRun = run("run-1", "completed");
57
58describe("deriveThreadTurnSubagents", () => {
59 it.each(["running", "completed"] as const)(
60 "keeps finished workflows after a later %s turn",
61 (status) => {
62 const workflow = subagent({
63 id: "workflow",
64 status: "completed",
65 workflow: { phases: [], agents: [] },
66 });
67 const turn = deriveThreadTurnSubagents({
68 runs: [finishedRun, run("run-2", status)],
69 subagents: [
70 workflow,
71 subagent({ id: "old", status: "completed" }),
72 subagent({
73 id: "recent",
74 runId: RunId.make("run-2"),
75 status,
76 updatedAt: at("2026-06-20T00:00:09.000Z"),
77 }),
78 ],
79 });
80
81 expect(turn?.subagents.map((entry) => entry.id)).toEqual(["recent", "workflow"]);
82 expect(resolveSubagentPillSegment(turn)).not.toBeNull();
83 },
84 );
85
86 it("scopes the roster to the active run and orders it by start time", () => {
87 const turn = deriveThreadTurnSubagents({
88 runs: [run("run-0", "completed"), activeRun],
89 subagents: [
90 subagent({ id: "b", startedAt: at("2026-06-20T00:00:05.000Z") }),
91 subagent({ id: "old", runId: RunId.make("run-0"), status: "completed" }),
92 subagent({ id: "a", startedAt: at("2026-06-20T00:00:02.000Z") }),
93 ],
94 });
95
96 expect(turn?.runId).toBe("run-1");
97 expect(turn?.subagents.map((entry) => entry.id)).toEqual(["a", "b"]);
98 expect(turn?.turnActive).toBe(true);
99 });
100
101 it("falls back to the most recently updated agent's run once the turn settles", () => {
102 const turn = deriveThreadTurnSubagents({
103 runs: [finishedRun, run("run-2", "completed")],
104 subagents: [
105 subagent({ id: "old", status: "completed", updatedAt: at("2026-06-20T00:00:01.000Z") }),
106 subagent({
107 id: "recent",
108 runId: RunId.make("run-2"),
109 status: "completed",
110 updatedAt: at("2026-06-20T00:00:09.000Z"),
111 }),
112 ],
113 });
114
115 expect(turn?.runId).toBe("run-2");
116 expect(turn?.turnActive).toBe(false);
117 expect(turn?.subagents.map((entry) => entry.id)).toEqual(["recent"]);
118 });
119
120 it("lists every live agent from earlier turns once no turn is running", () => {
121 const turn = deriveThreadTurnSubagents({
122 runs: ["run-103", "run-105", "run-107", "run-109"].map((id) => run(id, "completed")),
123 subagents: [
124 subagent({
125 id: "settled-103",
126 runId: RunId.make("run-103"),
127 status: "completed",
128 startedAt: at("2026-06-20T00:00:00.000Z"),
129 updatedAt: at("2026-06-20T00:00:04.000Z"),
130 }),
131 subagent({
132 id: "bg-103",
133 runId: RunId.make("run-103"),
134 startedAt: at("2026-06-20T00:00:01.000Z"),
135 updatedAt: at("2026-06-20T00:00:05.000Z"),
136 }),
137 subagent({
138 id: "bg-105",
139 runId: RunId.make("run-105"),
140 startedAt: at("2026-06-20T00:00:02.000Z"),
141 updatedAt: at("2026-06-20T00:00:06.000Z"),
142 }),
143 subagent({
144 id: "bg-107",
145 runId: RunId.make("run-107"),
146 startedAt: at("2026-06-20T00:00:03.000Z"),
147 updatedAt: at("2026-06-20T00:00:07.000Z"),
148 }),
149 ],
150 });
151
152 expect(turn?.subagents.map((entry) => entry.id)).toEqual(["bg-103", "bg-105", "bg-107"]);
153 expect(resolveSubagentPillSegment(turn)?.label).toBe("3/3");
154 });
155
156 it("keeps the active turn's roster and adds a live agent from an earlier turn", () => {
157 const turn = deriveThreadTurnSubagents({
158 runs: [run("run-0", "completed"), activeRun],
159 subagents: [
160 subagent({ id: "background", runId: RunId.make("run-0") }),
161 subagent({ id: "earlier-done", runId: RunId.make("run-0"), status: "completed" }),
162 subagent({ id: "working", startedAt: at("2026-06-20T00:00:05.000Z") }),
163 subagent({
164 id: "done",
165 status: "completed",
166 startedAt: at("2026-06-20T00:00:06.000Z"),
167 }),
168 ],
169 });
170
171 expect(turn?.runId).toBe("run-1");
172 expect(turn?.turnActive).toBe(true);
173 expect(turn?.subagents.map((entry) => entry.id)).toEqual(["background", "working", "done"]);
174 expect(resolveSubagentPillSegment(turn)?.label).toBe("2/3");
175 });
176
177 it("leaves out a live agent whose turn was rolled back", () => {
178 const turn = deriveThreadTurnSubagents({
179 runs: [run("run-0", "rolled_back"), finishedRun],
180 subagents: [
181 subagent({ id: "rolled-back", runId: RunId.make("run-0") }),
182 subagent({ id: "live", updatedAt: at("2026-06-20T00:00:09.000Z") }),
183 ],
184 });
185
186 expect(turn?.subagents.map((entry) => entry.id)).toEqual(["live"]);
187 });
188
189 it("lists a workflow coordinator's members, which carry no run", () => {
190 const turn = deriveThreadTurnSubagents({
191 runs: [],
192 subagents: [
193 subagent({ id: "member-0", runId: null, startedAt: at("2026-06-20T00:00:01.000Z") }),
194 subagent({
195 id: "member-1",
196 runId: null,
197 status: "completed",
198 startedAt: at("2026-06-20T00:00:02.000Z"),
199 }),
200 ],
201 });
202
203 expect(turn?.subagents.map((entry) => entry.id)).toEqual(["member-0", "member-1"]);
204 expect(resolveSubagentPillSegment(turn)?.label).toBe("1/2");
205 });
206
207 it("keeps the finished agents reachable while their turn is the latest", () => {
208 const turn = deriveThreadTurnSubagents({
209 runs: [finishedRun],
210 subagents: [
211 subagent({ id: "a", status: "completed" }),
212 subagent({ id: "b", status: "failed" }),
213 ],
214 });
215
216 expect(resolveSubagentPillSegment(turn)?.label).toBe("2 done");
217 });
218
219 it("drops finished agents from the pill once a later turn ran without any", () => {
220 const turn = deriveThreadTurnSubagents({
221 runs: [finishedRun, run("run-2", "completed")],
222 subagents: [subagent({ id: "a", status: "completed" })],
223 });
224
225 expect(resolveSubagentPillSegment(turn)).toBeNull();
226 });
227
228 it("shows finished agents again when the later turn was rolled back", () => {
229 const turn = deriveThreadTurnSubagents({
230 runs: [finishedRun, run("run-2", "rolled_back")],
231 subagents: [subagent({ id: "a", status: "completed" })],
232 });
233
234 expect(resolveSubagentPillSegment(turn)?.label).toBe("1 done");
235 });
236
237 it("keeps finished agents while a later turn is only queued or was cancelled before starting", () => {
238 for (const later of [
239 run("run-2", "queued", { started: false }),
240 run("run-2", "cancelled", { started: false }),
241 ]) {
242 const turn = deriveThreadTurnSubagents({
243 runs: [finishedRun, later],
244 subagents: [subagent({ id: "a", status: "completed" })],
245 });
246
247 expect(resolveSubagentPillSegment(turn)?.label).toBe("1 done");
248 }
249 });
250
251 it("finds the latest turn by ordinal when a reordered queue left runs out of order", () => {
252 const turn = deriveThreadTurnSubagents({
253 runs: [run("run-1", "completed", { ordinal: 3 }), run("run-2", "completed", { ordinal: 2 })],
254 subagents: [subagent({ id: "a", status: "completed" })],
255 });
256
257 expect(resolveSubagentPillSegment(turn)?.label).toBe("1 done");
258 });
259
260 it("keeps the latest turn's agents when an earlier turn's background agent finishes later", () => {
261 const turn = deriveThreadTurnSubagents({
262 runs: [finishedRun, run("run-2", "completed")],
263 subagents: [
264 subagent({ id: "late", status: "completed", updatedAt: at("2026-06-20T00:00:09.000Z") }),
265 subagent({
266 id: "latest",
267 runId: RunId.make("run-2"),
268 status: "completed",
269 updatedAt: at("2026-06-20T00:00:05.000Z"),
270 }),
271 ],
272 });
273
274 expect(turn?.subagents.map((entry) => entry.id)).toEqual(["latest"]);
275 expect(resolveSubagentPillSegment(turn)?.label).toBe("1 done");
276 });
277
278 it("counts only terminal agents as done while a held agent remains", () => {
279 const turn = deriveThreadTurnSubagents({
280 runs: [finishedRun],
281 subagents: [
282 subagent({ id: "a", status: "completed" }),
283 subagent({ id: "b", status: "idle" }),
284 ],
285 });
286
287 expect(resolveSubagentPillSegment(turn)?.label).toBe("1 done");
288 });
289
290 it("counts idle as neither working nor done", () => {
291 const turn = deriveThreadTurnSubagents({
292 runs: [activeRun],
293 subagents: [
294 subagent({ id: "a", status: "completed" }),
295 subagent({ id: "b", status: "idle" }),
296 subagent({ id: "c", status: "failed" }),
297 ],
298 });
299
300 expect(turn?.liveCount).toBe(0);
301 expect(turn?.settledCount).toBe(2);
302 expect(turn?.subagents).toHaveLength(3);
303 expect(resolveSubagentPillSegment(turn)?.label).toBe("2 done");
304 });
305
306 it("keeps settled workflows reachable without counting idle agents as done", () => {
307 const turn = deriveThreadTurnSubagents({
308 runs: [finishedRun, run("run-2", "completed")],
309 subagents: [
310 subagent({ id: "workflow", status: "completed", workflow: { phases: [], agents: [] } }),
311 subagent({
312 id: "idle",
313 runId: RunId.make("run-2"),
314 status: "idle",
315 updatedAt: at("2026-06-20T00:00:09.000Z"),
316 }),
317 ],
318 });
319
320 expect(turn?.subagents.map((entry) => entry.id)).toEqual(["idle", "workflow"]);
321 expect(resolveSubagentPillSegment(turn)).toEqual({
322 label: "1 done",
323 accessibilityLabel: "1 agent done",
324 });
325 });
326});