1import { assert, describe, it } from "@effect/vitest";
2import {
3 MessageId,
4 NodeId,
5 type OrchestrationV2Subagent,
6 type OrchestrationV2TurnItem,
7 RunId,
8 ThreadId,
9 TurnItemId,
10} from "@t3tools/contracts";
11import * as DateTime from "effect/DateTime";
12β
13import { backgroundWorkNotification, notificationTurnItem } from "./notification.ts";
14β
15const childThreadId = ThreadId.make("thread:child");
16β
17describe("backgroundWorkNotification", () => {
18 it("names one piece of work and the thread of a subagent", () => {
19 assert.deepEqual(
20 backgroundWorkNotification([
21 {
22 kind: "subagent",
23 label: "Review src/math.ts",
24 outcome: "completed",
25 childThreadId,
26 },
27 ]),
28 {
29 source: { kind: "subagent", childThreadId },
30 outcome: "completed",
31 summary: 'Subagent "Review src/math.ts" finished',
32 },
33 );
34 assert.equal(
35 backgroundWorkNotification([
36 { kind: "command", label: "npm test\nnpm run lint", outcome: "failed", exitCode: 1 },
37 ]).summary,
38 'Command "npm test" failed (exit 1)',
39 );
40 assert.equal(
41 backgroundWorkNotification([{ kind: "monitor", label: "Three ticks", outcome: "updated" }])
42 .summary,
43 'Monitor "Three ticks" reported new output',
44 );
45 assert.equal(
46 backgroundWorkNotification([
47 { kind: "command", label: `${"a".repeat(78)}π and more`, outcome: "completed" },
48 ]).summary,
49 `Command "${"a".repeat(78)}β¦" finished`,
50 );
51 });
52β
53 it("names every piece of work that ended together, without opening one of them", () => {
54 const notification = backgroundWorkNotification([
55 { kind: "subagent", label: "Agent B", outcome: "cancelled", childThreadId },
56 { kind: "command", label: "Sleep 60 seconds", outcome: "cancelled" },
57 ]);
58 assert.deepEqual(notification, {
59 source: { kind: "background_task" },
60 outcome: "cancelled",
61 summary: 'Subagent "Agent B" and command "Sleep 60 seconds" were stopped',
62 });
63 });
64β
65 it("counts work it cannot list", () => {
66 const reports = ["a", "b", "c", "d"].map((label) => ({
67 kind: "subagent" as const,
68 label,
69 outcome: "completed" as const,
70 }));
71 assert.deepEqual(backgroundWorkNotification(reports), {
72 source: { kind: "subagent" },
73 outcome: "completed",
74 summary: "4 subagents finished",
75 });
76 });
77});
78β
79describe("notificationTurnItem", () => {
80 const now = DateTime.makeUnsafe("2026-09-27T00:00:00.000Z");
81 const threadId = ThreadId.make("thread:parent");
82 const parentRunId = RunId.make("run:parent");
83 const userMessage: OrchestrationV2TurnItem = {
84 id: TurnItemId.make("item:delivery"),
85 threadId,
86 runId: RunId.make("run:delivery"),
87 nodeId: null,
88 providerThreadId: null,
89 providerTurnId: null,
90 nativeItemRef: null,
91 parentItemId: null,
92 ordinal: 1,
93 status: "completed",
94 title: null,
95 startedAt: now,
96 completedAt: now,
97 updatedAt: now,
98 type: "user_message",
99 messageId: MessageId.make("message:delivery"),
100 inputIntent: "turn_start",
101 text: "Delegated tasks reached terminal states.",
102 attachments: [],
103 createdBy: "agent",
104 creationSource: "server",
105 };
106 const task = (id: string, title: string, status: OrchestrationV2Subagent["status"]) =>
107 ({
108 id: NodeId.make(id),
109 runId: parentRunId,
110 origin: "app_owned",
111 title,
112 prompt: `${title} prompt`,
113 status,
114 childThreadId: ThreadId.make(`thread:${id}`),
115 }) as OrchestrationV2Subagent;
116 const delivery = (taskIds: ReadonlyArray<string>) => ({
117 delegatedCompletion: {
118 parentRunId,
119 generation: 1,
120 taskIds: taskIds.map((taskId) => NodeId.make(taskId)),
121 },
122 });
123 const tasks = [
124 task("a", "Review src/math.ts", "completed"),
125 task("b", "Write tests", "completed"),
126 task("c", "Update docs", "running"),
127 ];
128β
129 it("opens the child thread of a single delegated task", () => {
130 const item = notificationTurnItem(userMessage, delivery(["a"]), tasks);
131 assert.deepInclude(item, {
132 type: "notification",
133 summary: 'Delegated task "Review src/math.ts" finished',
134 source: {
135 kind: "delegated_task",
136 taskIds: [NodeId.make("a")],
137 childThreadId: ThreadId.make("thread:a"),
138 },
139 });
140 });
141});