| 1 | import { EMPTY_V2_ITEM_SUPPORT } from "@t3tools/client-runtime/state/item-support"; |
| 2 | import { |
| 3 | CheckpointId, |
| 4 | CheckpointScopeId, |
| 5 | NodeId, |
| 6 | ProviderDriverKind, |
| 7 | ProviderInstanceId, |
| 8 | RunId, |
| 9 | ThreadId, |
| 10 | TurnItemId, |
| 11 | type OrchestrationV2ProjectedTurnItem, |
| 12 | type OrchestrationV2TurnItem, |
| 13 | } from "@t3tools/contracts"; |
| 14 | import * as DateTime from "effect/DateTime"; |
| 15 | import { describe, expect, it } from "vite-plus/test"; |
| 16 | |
| 17 | import type { ThreadFeedActivity } from "./threadActivity"; |
| 18 | import { buildThreadActivityInspector } from "./threadActivityInspector"; |
| 19 | |
| 20 | const threadId = ThreadId.make("thread-1"); |
| 21 | const sourceThreadId = ThreadId.make("thread-source"); |
| 22 | const runId = RunId.make("run-1"); |
| 23 | const nodeId = NodeId.make("node-1"); |
| 24 | const startedAt = DateTime.makeUnsafe("2026-06-20T00:00:00.000Z"); |
| 25 | const completedAt = DateTime.makeUnsafe("2026-06-20T00:00:02.000Z"); |
| 26 | |
| 27 | function itemBase(id: string) { |
| 28 | return { |
| 29 | id: TurnItemId.make(id), |
| 30 | threadId, |
| 31 | runId, |
| 32 | nodeId, |
| 33 | providerThreadId: null, |
| 34 | providerTurnId: null, |
| 35 | nativeItemRef: null, |
| 36 | parentItemId: null, |
| 37 | ordinal: 0, |
| 38 | status: "completed" as const, |
| 39 | title: null, |
| 40 | startedAt, |
| 41 | completedAt, |
| 42 | updatedAt: completedAt, |
| 43 | }; |
| 44 | } |
| 45 | |
| 46 | function activityFor(item: OrchestrationV2TurnItem): Pick<ThreadFeedActivity, "projectedItem"> { |
| 47 | const row: OrchestrationV2ProjectedTurnItem = { |
| 48 | position: 0, |
| 49 | visibility: "inherited", |
| 50 | sourceThreadId, |
| 51 | sourceItemId: item.id, |
| 52 | item, |
| 53 | }; |
| 54 | return { projectedItem: row }; |
| 55 | } |
| 56 | |
| 57 | describe("buildThreadActivityInspector", () => { |
| 58 | it("presents command lifecycle and exit state without cached raw output", () => { |
| 59 | const item: OrchestrationV2TurnItem = { |
| 60 | ...itemBase("command"), |
| 61 | type: "command_execution", |
| 62 | input: "vp check", |
| 63 | output: "all checks passed", |
| 64 | exitCode: 0, |
| 65 | }; |
| 66 | const support = { |
| 67 | ...EMPTY_V2_ITEM_SUPPORT, |
| 68 | item, |
| 69 | run: { status: "completed" } as never, |
| 70 | attempts: [ |
| 71 | { |
| 72 | id: "attempt-1", |
| 73 | attemptOrdinal: 1, |
| 74 | status: "superseded", |
| 75 | reason: "initial", |
| 76 | }, |
| 77 | { |
| 78 | id: "attempt-2", |
| 79 | attemptOrdinal: 2, |
| 80 | status: "completed", |
| 81 | reason: "steering_restart", |
| 82 | }, |
| 83 | ] as never, |
| 84 | node: { kind: "tool_call", status: "completed" } as never, |
| 85 | providerSession: { |
| 86 | status: "ready", |
| 87 | model: "gpt-5.4", |
| 88 | cwd: "/workspace/project", |
| 89 | } as never, |
| 90 | }; |
| 91 | |
| 92 | const model = buildThreadActivityInspector(activityFor(item), support, sourceThreadId); |
| 93 | expect(model.fields).toEqual( |
| 94 | expect.arrayContaining([ |
| 95 | { label: "Duration", value: "2.0s" }, |
| 96 | { label: "Run", value: "completed" }, |
| 97 | { label: "Working directory", value: "/workspace/project" }, |
| 98 | { label: "Visibility", value: "inherited" }, |
| 99 | ]), |
| 100 | ); |
| 101 | expect(model.blocks).toEqual( |
| 102 | expect.arrayContaining([ |
| 103 | expect.objectContaining({ label: "Command", value: "vp check" }), |
| 104 | expect.objectContaining({ label: "Exit", value: "Process exited with code 0" }), |
| 105 | expect.objectContaining({ label: "Attempt history" }), |
| 106 | ]), |
| 107 | ); |
| 108 | expect(model.blocks.some((block) => block.label === "Output")).toBe(false); |
| 109 | expect(model.structuredDetails).not.toContain("all checks passed"); |
| 110 | }); |
| 111 | |
| 112 | it("exposes file and web result provenance plus dynamic structured data", () => { |
| 113 | const fileSearch: OrchestrationV2TurnItem = { |
| 114 | ...itemBase("file-search"), |
| 115 | type: "file_search", |
| 116 | pattern: "resolveSession", |
| 117 | results: [{ fileName: "src/session.ts", line: 42, preview: "function resolveSession()" }], |
| 118 | }; |
| 119 | const fileModel = buildThreadActivityInspector( |
| 120 | activityFor(fileSearch), |
| 121 | EMPTY_V2_ITEM_SUPPORT, |
| 122 | sourceThreadId, |
| 123 | ); |
| 124 | expect(fileModel.fileLinks).toEqual([ |
| 125 | { |
| 126 | label: "src/session.ts — function resolveSession()", |
| 127 | path: "src/session.ts", |
| 128 | line: 42, |
| 129 | }, |
| 130 | ]); |
| 131 | |
| 132 | const webSearch: OrchestrationV2TurnItem = { |
| 133 | ...itemBase("web-search"), |
| 134 | type: "web_search", |
| 135 | patterns: ["Effect Schema docs"], |
| 136 | results: [ |
| 137 | { title: "Schema", url: "https://effect.website/docs/schema", snippet: "Typed schemas" }, |
| 138 | ], |
| 139 | }; |
| 140 | const webModel = buildThreadActivityInspector( |
| 141 | activityFor(webSearch), |
| 142 | EMPTY_V2_ITEM_SUPPORT, |
| 143 | sourceThreadId, |
| 144 | ); |
| 145 | expect(webModel.webLinks).toEqual([ |
| 146 | { label: "Schema", url: "https://effect.website/docs/schema" }, |
| 147 | ]); |
| 148 | expect(webModel.blocks).toEqual( |
| 149 | expect.arrayContaining([ |
| 150 | expect.objectContaining({ label: "Schema", value: "Typed schemas" }), |
| 151 | ]), |
| 152 | ); |
| 153 | |
| 154 | const dynamicTool: OrchestrationV2TurnItem = { |
| 155 | ...itemBase("dynamic"), |
| 156 | type: "dynamic_tool", |
| 157 | toolName: "custom", |
| 158 | input: { nested: { value: 1 } }, |
| 159 | output: { ok: true }, |
| 160 | }; |
| 161 | const dynamicModel = buildThreadActivityInspector( |
| 162 | activityFor(dynamicTool), |
| 163 | EMPTY_V2_ITEM_SUPPORT, |
| 164 | sourceThreadId, |
| 165 | ); |
| 166 | expect(dynamicModel.blocks).toEqual( |
| 167 | expect.arrayContaining([ |
| 168 | expect.objectContaining({ label: "Input", value: expect.stringContaining('"nested"') }), |
| 169 | ]), |
| 170 | ); |
| 171 | expect(dynamicModel.structuredDetails).toContain('"type": "dynamic_tool"'); |
| 172 | expect(dynamicModel.blocks.some((block) => block.label === "Output")).toBe(false); |
| 173 | expect(dynamicModel.structuredDetails).not.toContain('"output"'); |
| 174 | }); |
| 175 | |
| 176 | it("keeps file links and change counts without cached patch bodies", () => { |
| 177 | const item: OrchestrationV2TurnItem = { |
| 178 | ...itemBase("file"), |
| 179 | type: "file_change", |
| 180 | fileName: "src/example.ts", |
| 181 | additions: 2, |
| 182 | deletions: 1, |
| 183 | diffStr: "RAW_PATCH", |
| 184 | oldStr: "RAW_BEFORE", |
| 185 | newStr: "RAW_AFTER", |
| 186 | }; |
| 187 | const model = buildThreadActivityInspector(activityFor(item), EMPTY_V2_ITEM_SUPPORT, threadId); |
| 188 | expect(model.fileLinks).toEqual([{ label: `modify ${item.fileName}`, path: item.fileName }]); |
| 189 | expect(model.fields).toContainEqual({ label: "Changes", value: "+2 −1" }); |
| 190 | expect(model.blocks).toEqual([]); |
| 191 | expect(model.structuredDetails).not.toContain("RAW_"); |
| 192 | expect(model.structuredDetails).toContain(item.fileName); |
| 193 | }); |
| 194 | |
| 195 | it("enables rollback only for the matching ready checkpoint", () => { |
| 196 | const checkpointId = CheckpointId.make("checkpoint-1"); |
| 197 | const scopeId = CheckpointScopeId.make("scope-1"); |
| 198 | const item: OrchestrationV2TurnItem = { |
| 199 | ...itemBase("checkpoint"), |
| 200 | type: "checkpoint", |
| 201 | checkpointId, |
| 202 | scopeId, |
| 203 | files: [{ path: "src/main.ts", kind: "modified", additions: 2, deletions: 1 }], |
| 204 | }; |
| 205 | const checkpoint = { id: checkpointId, scopeId, status: "ready" } as never; |
| 206 | const model = buildThreadActivityInspector( |
| 207 | activityFor(item), |
| 208 | { |
| 209 | ...EMPTY_V2_ITEM_SUPPORT, |
| 210 | item, |
| 211 | checkpoint, |
| 212 | }, |
| 213 | sourceThreadId, |
| 214 | ); |
| 215 | |
| 216 | expect(model.canRollback).toBe(true); |
| 217 | expect(model.rollbackTarget).toEqual({ |
| 218 | threadId: sourceThreadId, |
| 219 | checkpointId, |
| 220 | scopeId, |
| 221 | }); |
| 222 | |
| 223 | const inheritedModel = buildThreadActivityInspector( |
| 224 | activityFor(item), |
| 225 | { |
| 226 | ...EMPTY_V2_ITEM_SUPPORT, |
| 227 | item, |
| 228 | checkpoint, |
| 229 | }, |
| 230 | ThreadId.make("child-thread"), |
| 231 | ); |
| 232 | expect(inheritedModel.canRollback).toBe(false); |
| 233 | expect(inheritedModel.rollbackTarget).toBeNull(); |
| 234 | }); |
| 235 | |
| 236 | it("prefers live subagent progress from supporting state", () => { |
| 237 | const item: OrchestrationV2TurnItem = { |
| 238 | ...itemBase("subagent"), |
| 239 | type: "subagent", |
| 240 | subagentId: nodeId, |
| 241 | origin: "provider_native", |
| 242 | driver: ProviderDriverKind.make("claude"), |
| 243 | providerInstanceId: ProviderInstanceId.make("claude"), |
| 244 | childThreadId: null, |
| 245 | prompt: "Audit the reducer", |
| 246 | progress: "Starting", |
| 247 | result: null, |
| 248 | }; |
| 249 | const model = buildThreadActivityInspector( |
| 250 | activityFor(item), |
| 251 | { |
| 252 | ...EMPTY_V2_ITEM_SUPPORT, |
| 253 | item, |
| 254 | subagent: { |
| 255 | origin: "provider_native", |
| 256 | status: "running", |
| 257 | progress: "Reading projection tests", |
| 258 | result: null, |
| 259 | } as never, |
| 260 | }, |
| 261 | sourceThreadId, |
| 262 | ); |
| 263 | |
| 264 | expect(model.fields).toContainEqual({ |
| 265 | label: "Delegated task", |
| 266 | value: "provider native · running", |
| 267 | }); |
| 268 | expect(model.blocks).toContainEqual({ |
| 269 | label: "Progress", |
| 270 | value: "Reading projection tests", |
| 271 | monospaced: false, |
| 272 | }); |
| 273 | }); |
| 274 | }); |