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