1import { describe, expect, it } from "vite-plus/test";
2import type { OrchestrationV2Subagent, OrchestrationV2SubagentWorkflow } from "@t3tools/contracts";
3import * as DateTime from "effect/DateTime";
4
5import { deriveWorkflowGroups, projectedSubagentsToRuntime } from "./subagentRuntime.ts";
6
7const at = (iso: string) => DateTime.makeUnsafe(iso);
8
9type ProjectedSubagent = Parameters<typeof projectedSubagentsToRuntime>[0][number];
10
11function subagent(overrides: Partial<ProjectedSubagent> = {}): ProjectedSubagent {
12 return {
13 id: "coordinator",
14 title: "probe",
15 prompt: "export const meta = …",
16 model: "claude-opus-5[1m]",
17 status: "running" satisfies OrchestrationV2Subagent["status"],
18 result: null,
19 startedAt: at("2026-08-01T10:00:00.000Z"),
20 completedAt: null,
21 updatedAt: at("2026-08-01T10:00:05.000Z"),
22 ...overrides,
23 };
24}
25
26const member = (
27 overrides: Partial<OrchestrationV2SubagentWorkflow["agents"][number]> = {},
28): OrchestrationV2SubagentWorkflow["agents"][number] => ({
29 index: 1,
30 label: "alpha:one",
31 state: "running",
32 phaseIndex: 1,
33 phaseTitle: "Alpha",
34 ...overrides,
35});
36
37const groupsOf = (subagents: ReadonlyArray<ProjectedSubagent>) =>
38 deriveWorkflowGroups(projectedSubagentsToRuntime(subagents));
39
40describe("projectedSubagentsToRuntime workflow expansion", () => {
41 it("leaves a plain subagent flat", () => {
42 const [agent, ...rest] = projectedSubagentsToRuntime([subagent()]);
43 expect(rest).toEqual([]);
44 expect(agent?.kind).toBe("subagent");
45 expect(agent?.phases).toEqual([]);
46 });
47
48 it("projects a coordinator plus one row per member", () => {
49 const rows = projectedSubagentsToRuntime([
50 subagent({
51 workflow: {
52 name: "probe-wf",
53 phases: [
54 { index: 1, title: "Alpha" },
55 { index: 2, title: "Beta" },
56 ],
57 agents: [member(), member({ index: 2, label: "alpha:two", state: "queued" })],
58 totalTokens: 1000,
59 },
60 }),
61 ]);
62 expect(rows.map((row) => row.kind)).toEqual(["workflow", "workflow_agent", "workflow_agent"]);
63 expect(rows[0]?.workflowName).toBe("probe-wf");
64 expect(rows[0]?.usage?.totalTokens).toBe(1000);
65 expect(rows[1]).toMatchObject({
66 id: "coordinator:agent:1",
67 title: "alpha:one",
68 parentAgentId: "coordinator",
69 phaseIndex: 1,
70 phaseTitle: "Alpha",
71 status: "running",
72 });
73 // A queued member is not started work: it must not report an elapsed clock.
74 expect(rows[2]?.status).toBe("pending");
75 expect(rows[2]?.startedAt).toBeNull();
76 });
77
78 it("derives a settled member's end from its reported duration", () => {
79 const [, settled] = projectedSubagentsToRuntime([
80 subagent({
81 workflow: {
82 phases: [],
83 agents: [
84 member({
85 state: "completed",
86 startedAt: Date.UTC(2026, 7, 1, 10, 0, 0),
87 durationMs: 1500,
88 totalTokens: 71_141,
89 toolCalls: 3,
90 prompt: "Reply with exactly: A1",
91 result: "A1",
92 }),
93 ],
94 },
95 }),
96 ]);
97 expect(settled?.status).toBe("completed");
98 expect(settled?.completedAt).toBe("2026-08-01T10:00:01.500Z");
99 expect(settled?.result).toBe("A1");
100 // The prompt stays on progress so the detail view can show both halves.
101 expect(settled?.progress).toBe("Reply with exactly: A1");
102 expect(settled?.usage).toEqual({ totalTokens: 71_141, toolUses: 3, durationMs: 1500 });
103 });
104
105 it.each(["completed", "cancelled", "failed"] as const)(
106 "settles unfinished members when the coordinator is %s",
107 (status) => {
108 const groups = groupsOf([
109 subagent({
110 status,
111 completedAt: at("2026-08-01T10:00:09.000Z"),
112 workflow: {
113 phases: [],
114 agents: [
115 member(),
116 member({ index: 2, state: "queued" }),
117 member({ index: 3, state: "completed" }),
118 ],
119 },
120 }),
121 ]);
122 // The settled run keeps its members reachable under the coordinator.
123 expect(groups[0]?.workflow.status).toBe(status);
124 const members = groups[0]?.phases.flatMap((phase) => phase.members);
125 expect(members?.map((agent) => agent.status)).toEqual([status, status, "completed"]);
126 expect(members?.[0]?.completedAt).toBe("2026-08-01T10:00:09.000Z");
127 },
128 );
129
130 it("surfaces a retried member through the row's activation count", () => {
131 const [, retried] = projectedSubagentsToRuntime([
132 subagent({ workflow: { phases: [], agents: [member({ attempt: 3 })] } }),
133 ]);
134 expect(retried?.activationCount).toBe(3);
135 expect(retried?.attempt).toBe(3);
136 });
137});
138
139describe("deriveWorkflowGroups over a dynamic workflow", () => {
140 it("groups members under their phases and counts the run", () => {
141 const groups = groupsOf([
142 subagent({
143 workflow: {
144 name: "probe-wf",
145 phases: [
146 { index: 1, title: "Alpha" },
147 { index: 2, title: "Beta" },
148 ],
149 agents: [
150 member({ state: "completed" }),
151 member({ index: 2, label: "alpha:two", state: "failed" }),
152 member({ index: 3, label: "beta:one", phaseIndex: 2, phaseTitle: "Beta" }),
153 member({
154 index: 4,
155 label: "beta:two",
156 phaseIndex: 2,
157 phaseTitle: "Beta",
158 state: "queued",
159 }),
160 ],
161 },
162 }),
163 ]);
164 const group = groups[0];
165 expect(group?.phases.map((phase) => [phase.title, phase.state])).toEqual([
166 ["Alpha", "done"],
167 ["Beta", "running"],
168 ]);
169 expect(group?.phases.map((phase) => phase.settledCount)).toEqual([2, 0]);
170 expect(group?.phases.flatMap((phase) => phase.members)).toHaveLength(4);
171 expect(group?.unphasedMembers).toEqual([]);
172 });
173
174 it("shows a phase the script only reached at runtime", () => {
175 const groups = groupsOf([
176 subagent({
177 workflow: {
178 // The declared plan has not caught up with the member's phase yet.
179 phases: [{ index: 1, title: "Alpha" }],
180 agents: [
181 member({ state: "completed" }),
182 member({ index: 2, label: "dynamic:late", phaseIndex: 3, phaseTitle: "Gamma" }),
183 ],
184 },
185 }),
186 ]);
187 const group = groups[0];
188 expect(group?.phases.map((phase) => phase.title)).toEqual(["Alpha", "Gamma"]);
189 expect(group?.unphasedMembers).toEqual([]);
190 });
191
192 it.each([false, true])("keeps a reported phase title, missing title first=%s", (missingFirst) => {
193 const agents = [member({ phaseTitle: "Review" }), member({ index: 2, phaseTitle: undefined })];
194 const groups = groupsOf([
195 subagent({ workflow: { phases: [], agents: missingFirst ? agents.toReversed() : agents } }),
196 ]);
197 expect(groups[0]?.phases[0]?.title).toBe("Review");
198 });
199
200 it("keeps unphased members with their workflow and excludes unrelated agents", () => {
201 const runtime = projectedSubagentsToRuntime([
202 subagent({ id: "ordinary" }),
203 subagent({
204 id: "__proto__",
205 workflow: {
206 phases: [],
207 agents: [member({ phaseIndex: undefined, phaseTitle: undefined })],
208 },
209 }),
210 subagent({ id: "constructor", workflow: { phases: [], agents: [] } }),
211 subagent({ id: "orphan", workflow: { phases: [], agents: [member()] } }),
212 ]);
213 const groups = deriveWorkflowGroups(runtime.filter((agent) => agent.id !== "orphan"));
214 expect(groups.map((group) => group.workflow.id)).toEqual(["__proto__", "constructor"]);
215 expect(groups[0]?.phases).toEqual([]);
216 expect(groups[0]?.unphasedMembers.map((agent) => agent.title)).toEqual(["alpha:one"]);
217 expect(groups[1]?.phases).toEqual([]);
218 expect(groups[1]?.unphasedMembers).toEqual([]);
219 });
220});