|
1 | 1 | import { describe, expect, it } from "vitest"; |
2 | 2 |
|
3 | | -import { parseKiroPrompts, parseKiroSlashCommands } from "./KiroAdapter.ts"; |
| 3 | +import { RuntimeTaskId } from "@t3tools/contracts"; |
| 4 | + |
| 5 | +import { |
| 6 | + diffKiroSubagentRoster, |
| 7 | + formatSubagentToolLabel, |
| 8 | + parseKiroPrompts, |
| 9 | + parseKiroSlashCommands, |
| 10 | + parseKiroSubagentList, |
| 11 | +} from "./KiroAdapter.ts"; |
4 | 12 | import { parseKiroAgentListOutput } from "./KiroProvider.ts"; |
5 | 13 |
|
6 | 14 | describe("parseKiroSlashCommands", () => { |
@@ -164,3 +172,182 @@ describe("parseKiroAgentListOutput", () => { |
164 | 172 | expect(agents[0]!.description).toBeUndefined(); |
165 | 173 | }); |
166 | 174 | }); |
| 175 | + |
| 176 | +describe("parseKiroSubagentList", () => { |
| 177 | + it("extracts sessionId, names, and status", () => { |
| 178 | + const result = parseKiroSubagentList({ |
| 179 | + subagents: [ |
| 180 | + { |
| 181 | + sessionId: "s1", |
| 182 | + sessionName: "sdk-serialization", |
| 183 | + agentName: "codebase-explorer", |
| 184 | + status: { type: "working", message: "Running" }, |
| 185 | + group: "crew-1", |
| 186 | + role: "codebase-explorer", |
| 187 | + dependsOn: [], |
| 188 | + }, |
| 189 | + { |
| 190 | + sessionId: "s2", |
| 191 | + sessionName: "cli-serialization", |
| 192 | + agentName: "codebase-explorer", |
| 193 | + status: { type: "terminated" }, |
| 194 | + }, |
| 195 | + ], |
| 196 | + pendingStages: [], |
| 197 | + }); |
| 198 | + expect(result).toHaveLength(2); |
| 199 | + expect(result[0]).toEqual({ |
| 200 | + sessionId: "s1", |
| 201 | + sessionName: "sdk-serialization", |
| 202 | + agentName: "codebase-explorer", |
| 203 | + statusType: "working", |
| 204 | + }); |
| 205 | + expect(result[1]!.statusType).toBe("terminated"); |
| 206 | + }); |
| 207 | + |
| 208 | + it("falls back to sessionId when sessionName is missing", () => { |
| 209 | + const result = parseKiroSubagentList({ |
| 210 | + subagents: [{ sessionId: "abc", status: { type: "working" } }], |
| 211 | + }); |
| 212 | + expect(result[0]!.sessionName).toBe("abc"); |
| 213 | + expect(result[0]!.agentName).toBe("subagent"); |
| 214 | + }); |
| 215 | + |
| 216 | + it("treats unknown status shapes as 'unknown'", () => { |
| 217 | + const result = parseKiroSubagentList({ |
| 218 | + subagents: [{ sessionId: "x", status: { type: "mystery" } }], |
| 219 | + }); |
| 220 | + expect(result[0]!.statusType).toBe("unknown"); |
| 221 | + }); |
| 222 | + |
| 223 | + it("skips entries missing sessionId", () => { |
| 224 | + const result = parseKiroSubagentList({ |
| 225 | + subagents: [{ sessionName: "no-id", status: { type: "working" } }, null, "string"], |
| 226 | + }); |
| 227 | + expect(result).toHaveLength(0); |
| 228 | + }); |
| 229 | + |
| 230 | + it("returns [] for non-object input", () => { |
| 231 | + expect(parseKiroSubagentList(null)).toEqual([]); |
| 232 | + expect(parseKiroSubagentList({ subagents: "nope" })).toEqual([]); |
| 233 | + }); |
| 234 | +}); |
| 235 | + |
| 236 | +describe("diffKiroSubagentRoster", () => { |
| 237 | + const trackedWorking = () => |
| 238 | + new Map([ |
| 239 | + [ |
| 240 | + "s1", |
| 241 | + { |
| 242 | + taskId: RuntimeTaskId.make("s1"), |
| 243 | + sessionName: "sdk-serialization", |
| 244 | + agentName: "codebase-explorer", |
| 245 | + statusType: "working" as const, |
| 246 | + seenToolCallIds: new Set<string>(), |
| 247 | + }, |
| 248 | + ], |
| 249 | + ]); |
| 250 | + |
| 251 | + it("emits 'started' for a new working entry", () => { |
| 252 | + const changes = diffKiroSubagentRoster(new Map(), [ |
| 253 | + { |
| 254 | + sessionId: "s1", |
| 255 | + sessionName: "sdk", |
| 256 | + agentName: "codebase-explorer", |
| 257 | + statusType: "working", |
| 258 | + }, |
| 259 | + ]); |
| 260 | + expect(changes).toHaveLength(1); |
| 261 | + expect(changes[0]!.kind).toBe("started"); |
| 262 | + }); |
| 263 | + |
| 264 | + it("emits 'completed' when a tracked entry transitions to terminated", () => { |
| 265 | + const changes = diffKiroSubagentRoster(trackedWorking(), [ |
| 266 | + { sessionId: "s1", sessionName: "sdk", agentName: "x", statusType: "terminated" }, |
| 267 | + ]); |
| 268 | + expect(changes).toHaveLength(1); |
| 269 | + expect(changes[0]!.kind).toBe("completed"); |
| 270 | + }); |
| 271 | + |
| 272 | + it("emits 'completed' when a tracked entry disappears from the roster", () => { |
| 273 | + const changes = diffKiroSubagentRoster(trackedWorking(), []); |
| 274 | + expect(changes).toHaveLength(1); |
| 275 | + expect(changes[0]!.kind).toBe("completed"); |
| 276 | + }); |
| 277 | + |
| 278 | + it("does not re-emit 'started' for already-tracked entries", () => { |
| 279 | + const changes = diffKiroSubagentRoster(trackedWorking(), [ |
| 280 | + { sessionId: "s1", sessionName: "sdk", agentName: "x", statusType: "working" }, |
| 281 | + ]); |
| 282 | + expect(changes).toHaveLength(0); |
| 283 | + }); |
| 284 | + |
| 285 | + it("does not re-emit 'completed' for already-terminated entries", () => { |
| 286 | + const tracked = new Map([ |
| 287 | + [ |
| 288 | + "s1", |
| 289 | + { |
| 290 | + taskId: RuntimeTaskId.make("s1"), |
| 291 | + sessionName: "sdk", |
| 292 | + agentName: "x", |
| 293 | + statusType: "terminated" as const, |
| 294 | + seenToolCallIds: new Set<string>(), |
| 295 | + }, |
| 296 | + ], |
| 297 | + ]); |
| 298 | + const changes = diffKiroSubagentRoster(tracked, [ |
| 299 | + { sessionId: "s1", sessionName: "sdk", agentName: "x", statusType: "terminated" }, |
| 300 | + ]); |
| 301 | + expect(changes).toHaveLength(0); |
| 302 | + }); |
| 303 | +}); |
| 304 | + |
| 305 | +describe("formatSubagentToolLabel", () => { |
| 306 | + it("combines presentation title with its payload detail", () => { |
| 307 | + expect(formatSubagentToolLabel({ title: "Ran command", detail: "bun test" })).toBe( |
| 308 | + "Ran command: bun test", |
| 309 | + ); |
| 310 | + expect(formatSubagentToolLabel({ title: "Read file", detail: "src/foo.ts" })).toBe( |
| 311 | + "Read file: src/foo.ts", |
| 312 | + ); |
| 313 | + expect(formatSubagentToolLabel({ title: "Searched files", detail: "useState" })).toBe( |
| 314 | + "Searched files: useState", |
| 315 | + ); |
| 316 | + }); |
| 317 | + |
| 318 | + it("falls back to command when detail is missing", () => { |
| 319 | + expect(formatSubagentToolLabel({ title: "Ran command", command: "ls -la" })).toBe( |
| 320 | + "Ran command: ls -la", |
| 321 | + ); |
| 322 | + }); |
| 323 | + |
| 324 | + it("returns the title alone when no detail is available", () => { |
| 325 | + expect(formatSubagentToolLabel({ title: "Summarizing" })).toBe("Summarizing"); |
| 326 | + }); |
| 327 | + |
| 328 | + it("returns the detail alone when no title is available", () => { |
| 329 | + expect(formatSubagentToolLabel({ detail: "apps/server/foo.ts" })).toBe( |
| 330 | + "apps/server/foo.ts", |
| 331 | + ); |
| 332 | + }); |
| 333 | + |
| 334 | + it("does not duplicate when title and detail are identical", () => { |
| 335 | + expect(formatSubagentToolLabel({ title: "Summarizing", detail: "Summarizing" })).toBe( |
| 336 | + "Summarizing", |
| 337 | + ); |
| 338 | + }); |
| 339 | + |
| 340 | + it("falls through to kind when everything else is empty", () => { |
| 341 | + expect(formatSubagentToolLabel({ kind: "execute" })).toBe("execute"); |
| 342 | + }); |
| 343 | + |
| 344 | + it("returns 'Working' as a last-resort fallback", () => { |
| 345 | + expect(formatSubagentToolLabel({})).toBe("Working"); |
| 346 | + }); |
| 347 | + |
| 348 | + it("trims whitespace on all inputs", () => { |
| 349 | + expect(formatSubagentToolLabel({ title: " Ran command ", detail: " ls " })).toBe( |
| 350 | + "Ran command: ls", |
| 351 | + ); |
| 352 | + }); |
| 353 | +}); |
0 commit comments