@codexa/cli 9.0.1 → 9.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,936 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import {
3
+ extractJsonFromText,
4
+ parseSubagentReturn,
5
+ formatValidationErrors,
6
+ type ParseResult,
7
+ type SubagentReturn,
8
+ } from "./subagent-protocol";
9
+
10
+ describe("extractJsonFromText", () => {
11
+ describe("JSON in code blocks", () => {
12
+ it("should extract JSON from json code block", () => {
13
+ const input = '```json\n{"status":"completed","summary":"ok"}\n```';
14
+ const result = extractJsonFromText(input);
15
+ expect(result).toBe('{"status":"completed","summary":"ok"}');
16
+ });
17
+
18
+ it("should extract JSON from plain code block", () => {
19
+ const input = '```\n{"status":"completed","summary":"ok"}\n```';
20
+ const result = extractJsonFromText(input);
21
+ expect(result).toBe('{"status":"completed","summary":"ok"}');
22
+ });
23
+
24
+ it("should handle code block with extra whitespace", () => {
25
+ const input = '```json\n\n {"status":"completed"} \n\n```';
26
+ const result = extractJsonFromText(input);
27
+ expect(result).toBe('{"status":"completed"}');
28
+ });
29
+ });
30
+
31
+ describe("Direct JSON objects", () => {
32
+ it("should extract direct JSON object", () => {
33
+ const input = '{"status":"completed","summary":"Task done"}';
34
+ const result = extractJsonFromText(input);
35
+ expect(result).toBe('{"status":"completed","summary":"Task done"}');
36
+ });
37
+
38
+ it("should extract JSON object with whitespace", () => {
39
+ const input = ' \n{"status":"completed"}\n ';
40
+ const result = extractJsonFromText(input);
41
+ expect(result).toBe('{"status":"completed"}');
42
+ });
43
+ });
44
+
45
+ describe("JSON with surrounding text", () => {
46
+ it("should extract JSON object from mixed text", () => {
47
+ const input = 'Here is my result: {"status":"completed","summary":"Done"} All finished.';
48
+ const result = extractJsonFromText(input);
49
+ expect(result).toBe('{"status":"completed","summary":"Done"}');
50
+ });
51
+
52
+ it("should extract first valid JSON object containing status", () => {
53
+ const input = 'First: {"other":"data"} Second: {"status":"completed","summary":"ok"}';
54
+ const result = extractJsonFromText(input);
55
+ expect(result).toBe('{"status":"completed","summary":"ok"}');
56
+ });
57
+
58
+ it("should handle nested braces in JSON", () => {
59
+ const input = 'Result: {"status":"completed","data":{"nested":"value"},"summary":"ok"}';
60
+ const result = extractJsonFromText(input);
61
+ expect(result).toBe('{"status":"completed","data":{"nested":"value"},"summary":"ok"}');
62
+ });
63
+
64
+ it("should handle escaped quotes in strings", () => {
65
+ const input = '{"status":"completed","summary":"He said \\"hello\\""}';
66
+ const result = extractJsonFromText(input);
67
+ expect(result).toBe('{"status":"completed","summary":"He said \\"hello\\""}');
68
+ });
69
+
70
+ it("should handle braces inside strings", () => {
71
+ const input = '{"status":"completed","summary":"Use {braces} like this"}';
72
+ const result = extractJsonFromText(input);
73
+ expect(result).toBe('{"status":"completed","summary":"Use {braces} like this"}');
74
+ });
75
+ });
76
+
77
+ describe("Invalid or missing JSON", () => {
78
+ it("should return null for plain text", () => {
79
+ const input = "Just plain text without JSON";
80
+ const result = extractJsonFromText(input);
81
+ expect(result).toBeNull();
82
+ });
83
+
84
+ it("should return trimmed text for brace-wrapped text without status", () => {
85
+ const input = "{not json at all}";
86
+ const result = extractJsonFromText(input);
87
+ // Falls back to trimmed text since it starts with { and ends with }
88
+ expect(result).toBe("{not json at all}");
89
+ });
90
+
91
+ it("should return null for JSON object without status key", () => {
92
+ const input = '{"other":"field","data":"value"}';
93
+ const result = extractJsonFromText(input);
94
+ // Should return trimmed since it starts with { and ends with }
95
+ expect(result).toBe('{"other":"field","data":"value"}');
96
+ });
97
+
98
+ it("should return null for empty string", () => {
99
+ const input = "";
100
+ const result = extractJsonFromText(input);
101
+ expect(result).toBeNull();
102
+ });
103
+
104
+ it("should return null for unclosed braces", () => {
105
+ const input = '{"status":"completed"';
106
+ const result = extractJsonFromText(input);
107
+ expect(result).toBeNull();
108
+ });
109
+ });
110
+ });
111
+
112
+ describe("parseSubagentReturn", () => {
113
+ describe("Valid returns", () => {
114
+ it("should parse valid minimal completed return", () => {
115
+ const input = JSON.stringify({
116
+ status: "completed",
117
+ summary: "Task completed successfully",
118
+ files_created: [],
119
+ files_modified: ["a.ts"],
120
+ reasoning: {
121
+ approach: "I analyzed the requirements and implemented the solution step by step",
122
+ },
123
+ });
124
+
125
+ const result = parseSubagentReturn(input);
126
+ expect(result.success).toBe(true);
127
+ expect(result.errors).toHaveLength(0);
128
+ expect(result.data).toBeDefined();
129
+ expect(result.data!.status).toBe("completed");
130
+ expect(result.data!.summary).toBe("Task completed successfully");
131
+ expect(result.data!.files_modified).toEqual(["a.ts"]);
132
+ });
133
+
134
+ it("should parse valid blocked return", () => {
135
+ const input = JSON.stringify({
136
+ status: "blocked",
137
+ summary: "Cannot proceed with implementation",
138
+ files_created: [],
139
+ files_modified: [],
140
+ blockers: ["Missing API key configuration"],
141
+ });
142
+
143
+ const result = parseSubagentReturn(input);
144
+ expect(result.success).toBe(true);
145
+ expect(result.data!.status).toBe("blocked");
146
+ expect(result.data!.blockers).toEqual(["Missing API key configuration"]);
147
+ });
148
+
149
+ it("should parse valid needs_decision return", () => {
150
+ const input = JSON.stringify({
151
+ status: "needs_decision",
152
+ summary: "Need guidance on approach",
153
+ files_created: [],
154
+ files_modified: [],
155
+ blockers: ["Which database migration strategy should we use?"],
156
+ });
157
+
158
+ const result = parseSubagentReturn(input);
159
+ expect(result.success).toBe(true);
160
+ expect(result.data!.status).toBe("needs_decision");
161
+ expect(result.data!.blockers).toEqual(["Which database migration strategy should we use?"]);
162
+ });
163
+
164
+ it("should parse return with all optional fields", () => {
165
+ const input = JSON.stringify({
166
+ status: "completed",
167
+ summary: "Implemented feature with patterns",
168
+ files_created: ["new.ts"],
169
+ files_modified: ["existing.ts"],
170
+ reasoning: {
171
+ approach: "Used factory pattern for extensibility and single responsibility",
172
+ challenges: ["Had to refactor legacy code"],
173
+ alternatives: ["Considered singleton pattern"],
174
+ recommendations: "Add more unit tests",
175
+ },
176
+ patterns_discovered: ["Factory pattern in auth"],
177
+ decisions_made: [
178
+ {
179
+ title: "Use TypeScript",
180
+ decision: "All new code in TS",
181
+ rationale: "Type safety",
182
+ },
183
+ ],
184
+ knowledge_to_broadcast: [
185
+ {
186
+ category: "pattern",
187
+ content: "Factory pattern used here",
188
+ severity: "info",
189
+ },
190
+ ],
191
+ utilities_created: [
192
+ {
193
+ name: "formatDate",
194
+ file: "utils/date.ts",
195
+ type: "function",
196
+ signature: "(date: Date) => string",
197
+ description: "Formats dates consistently",
198
+ },
199
+ ],
200
+ });
201
+
202
+ const result = parseSubagentReturn(input);
203
+ expect(result.success).toBe(true);
204
+ expect(result.data!.patterns_discovered).toEqual(["Factory pattern in auth"]);
205
+ expect(result.data!.decisions_made).toHaveLength(1);
206
+ expect(result.data!.knowledge_to_broadcast).toHaveLength(1);
207
+ expect(result.data!.utilities_created).toHaveLength(1);
208
+ expect(result.data!.reasoning!.challenges).toEqual(["Had to refactor legacy code"]);
209
+ });
210
+
211
+ it("should parse JSON in markdown code block", () => {
212
+ const input = `Here is the result:
213
+ \`\`\`json
214
+ {
215
+ "status": "completed",
216
+ "summary": "Feature implemented successfully",
217
+ "files_created": [],
218
+ "files_modified": ["app.ts"],
219
+ "reasoning": {
220
+ "approach": "Implemented the feature using existing patterns and added proper error handling"
221
+ }
222
+ }
223
+ \`\`\`
224
+ Done!`;
225
+
226
+ const result = parseSubagentReturn(input);
227
+ expect(result.success).toBe(true);
228
+ expect(result.data!.status).toBe("completed");
229
+ });
230
+ });
231
+
232
+ describe("Default values", () => {
233
+ it("should default files_created to empty array if omitted", () => {
234
+ const input = JSON.stringify({
235
+ status: "blocked",
236
+ summary: "Cannot proceed",
237
+ blockers: ["Missing info"],
238
+ });
239
+
240
+ const result = parseSubagentReturn(input);
241
+ expect(result.success).toBe(true);
242
+ expect(result.data!.files_created).toEqual([]);
243
+ });
244
+
245
+ it("should default files_modified to empty array if omitted", () => {
246
+ const input = JSON.stringify({
247
+ status: "blocked",
248
+ summary: "Cannot proceed",
249
+ blockers: ["Missing info"],
250
+ });
251
+
252
+ const result = parseSubagentReturn(input);
253
+ expect(result.success).toBe(true);
254
+ expect(result.data!.files_modified).toEqual([]);
255
+ });
256
+ });
257
+
258
+ describe("Invalid status", () => {
259
+ it("should reject invalid status value", () => {
260
+ const input = JSON.stringify({
261
+ status: "invalid_status",
262
+ summary: "This should fail",
263
+ files_created: [],
264
+ files_modified: [],
265
+ });
266
+
267
+ const result = parseSubagentReturn(input);
268
+ expect(result.success).toBe(false);
269
+ expect(result.errors.some((e) => e.includes("status"))).toBe(true);
270
+ });
271
+
272
+ it("should reject missing status", () => {
273
+ const input = JSON.stringify({
274
+ summary: "This should fail",
275
+ files_created: [],
276
+ files_modified: [],
277
+ });
278
+
279
+ const result = parseSubagentReturn(input);
280
+ expect(result.success).toBe(false);
281
+ expect(result.errors.some((e) => e.includes("status"))).toBe(true);
282
+ });
283
+ });
284
+
285
+ describe("Summary validation", () => {
286
+ it("should reject missing summary", () => {
287
+ const input = JSON.stringify({
288
+ status: "completed",
289
+ files_created: [],
290
+ files_modified: [],
291
+ reasoning: { approach: "Did something good here" },
292
+ });
293
+
294
+ const result = parseSubagentReturn(input);
295
+ expect(result.success).toBe(false);
296
+ expect(result.errors.some((e) => e.includes("summary"))).toBe(true);
297
+ });
298
+
299
+ it("should reject summary shorter than 10 characters", () => {
300
+ const input = JSON.stringify({
301
+ status: "completed",
302
+ summary: "Too short",
303
+ files_created: [],
304
+ files_modified: [],
305
+ reasoning: { approach: "Did something good here" },
306
+ });
307
+
308
+ const result = parseSubagentReturn(input);
309
+ expect(result.success).toBe(false);
310
+ expect(result.errors.some((e) => e.includes("summary") && e.includes("10"))).toBe(true);
311
+ });
312
+
313
+ it("should reject summary longer than 500 characters", () => {
314
+ const longSummary = "x".repeat(501);
315
+ const input = JSON.stringify({
316
+ status: "completed",
317
+ summary: longSummary,
318
+ files_created: [],
319
+ files_modified: [],
320
+ reasoning: { approach: "Did something good here" },
321
+ });
322
+
323
+ const result = parseSubagentReturn(input);
324
+ expect(result.success).toBe(false);
325
+ expect(result.errors.some((e) => e.includes("summary") && e.includes("500"))).toBe(true);
326
+ });
327
+
328
+ it("should accept summary exactly 10 characters", () => {
329
+ const input = JSON.stringify({
330
+ status: "blocked",
331
+ summary: "1234567890",
332
+ files_created: [],
333
+ files_modified: [],
334
+ blockers: ["test"],
335
+ });
336
+
337
+ const result = parseSubagentReturn(input);
338
+ expect(result.success).toBe(true);
339
+ });
340
+ });
341
+
342
+ describe("File arrays validation", () => {
343
+ it("should reject files_created as non-array", () => {
344
+ const input = JSON.stringify({
345
+ status: "completed",
346
+ summary: "This should fail",
347
+ files_created: "not-an-array.ts",
348
+ files_modified: [],
349
+ reasoning: { approach: "Did something good here" },
350
+ });
351
+
352
+ const result = parseSubagentReturn(input);
353
+ expect(result.success).toBe(false);
354
+ expect(result.errors.some((e) => e.includes("files_created") && e.includes("array"))).toBe(true);
355
+ });
356
+
357
+ it("should reject files_modified as non-array", () => {
358
+ const input = JSON.stringify({
359
+ status: "completed",
360
+ summary: "This should fail",
361
+ files_created: [],
362
+ files_modified: "not-an-array.ts",
363
+ reasoning: { approach: "Did something good here" },
364
+ });
365
+
366
+ const result = parseSubagentReturn(input);
367
+ expect(result.success).toBe(false);
368
+ expect(result.errors.some((e) => e.includes("files_modified") && e.includes("array"))).toBe(true);
369
+ });
370
+
371
+ it("should reject non-string elements in files_created", () => {
372
+ const input = JSON.stringify({
373
+ status: "completed",
374
+ summary: "This should fail",
375
+ files_created: ["valid.ts", 123, "another.ts"],
376
+ files_modified: [],
377
+ reasoning: { approach: "Did something good here" },
378
+ });
379
+
380
+ const result = parseSubagentReturn(input);
381
+ expect(result.success).toBe(false);
382
+ expect(result.errors.some((e) => e.includes("files_created[1]"))).toBe(true);
383
+ });
384
+ });
385
+
386
+ describe("Decisions validation", () => {
387
+ it("should reject decision missing title", () => {
388
+ const input = JSON.stringify({
389
+ status: "completed",
390
+ summary: "This should fail",
391
+ files_created: [],
392
+ files_modified: [],
393
+ reasoning: { approach: "Did something good here" },
394
+ decisions_made: [
395
+ {
396
+ decision: "Use React",
397
+ rationale: "Popular",
398
+ },
399
+ ],
400
+ });
401
+
402
+ const result = parseSubagentReturn(input);
403
+ expect(result.success).toBe(false);
404
+ expect(result.errors.some((e) => e.includes("decisions_made[0].title"))).toBe(true);
405
+ });
406
+
407
+ it("should reject decision missing decision field", () => {
408
+ const input = JSON.stringify({
409
+ status: "completed",
410
+ summary: "This should fail",
411
+ files_created: [],
412
+ files_modified: [],
413
+ reasoning: { approach: "Did something good here" },
414
+ decisions_made: [
415
+ {
416
+ title: "Framework choice",
417
+ rationale: "Popular",
418
+ },
419
+ ],
420
+ });
421
+
422
+ const result = parseSubagentReturn(input);
423
+ expect(result.success).toBe(false);
424
+ expect(result.errors.some((e) => e.includes("decisions_made[0].decision"))).toBe(true);
425
+ });
426
+
427
+ it("should accept valid decision with optional rationale", () => {
428
+ const input = JSON.stringify({
429
+ status: "completed",
430
+ summary: "Made a decision",
431
+ files_created: [],
432
+ files_modified: [],
433
+ reasoning: { approach: "Did something good here" },
434
+ decisions_made: [
435
+ {
436
+ title: "Framework",
437
+ decision: "Use React",
438
+ rationale: "Team expertise",
439
+ },
440
+ ],
441
+ });
442
+
443
+ const result = parseSubagentReturn(input);
444
+ expect(result.success).toBe(true);
445
+ });
446
+ });
447
+
448
+ describe("Knowledge validation", () => {
449
+ it("should reject invalid knowledge category", () => {
450
+ const input = JSON.stringify({
451
+ status: "completed",
452
+ summary: "This should fail",
453
+ files_created: [],
454
+ files_modified: [],
455
+ reasoning: { approach: "Did something good here" },
456
+ knowledge_to_broadcast: [
457
+ {
458
+ category: "invalid_category",
459
+ content: "Some content",
460
+ severity: "info",
461
+ },
462
+ ],
463
+ });
464
+
465
+ const result = parseSubagentReturn(input);
466
+ expect(result.success).toBe(false);
467
+ expect(result.errors.some((e) => e.includes("knowledge_to_broadcast[0].category"))).toBe(true);
468
+ });
469
+
470
+ it("should reject invalid knowledge severity", () => {
471
+ const input = JSON.stringify({
472
+ status: "completed",
473
+ summary: "This should fail",
474
+ files_created: [],
475
+ files_modified: [],
476
+ reasoning: { approach: "Did something good here" },
477
+ knowledge_to_broadcast: [
478
+ {
479
+ category: "pattern",
480
+ content: "Some content",
481
+ severity: "invalid_severity",
482
+ },
483
+ ],
484
+ });
485
+
486
+ const result = parseSubagentReturn(input);
487
+ expect(result.success).toBe(false);
488
+ expect(result.errors.some((e) => e.includes("knowledge_to_broadcast[0].severity"))).toBe(true);
489
+ });
490
+
491
+ it("should reject knowledge with empty content", () => {
492
+ const input = JSON.stringify({
493
+ status: "completed",
494
+ summary: "This should fail",
495
+ files_created: [],
496
+ files_modified: [],
497
+ reasoning: { approach: "Did something good here" },
498
+ knowledge_to_broadcast: [
499
+ {
500
+ category: "pattern",
501
+ content: "",
502
+ severity: "info",
503
+ },
504
+ ],
505
+ });
506
+
507
+ const result = parseSubagentReturn(input);
508
+ expect(result.success).toBe(false);
509
+ expect(result.errors.some((e) => e.includes("knowledge_to_broadcast[0].content"))).toBe(true);
510
+ });
511
+
512
+ it("should accept all valid knowledge categories", () => {
513
+ const categories = ["discovery", "decision", "blocker", "pattern", "constraint"];
514
+
515
+ for (const category of categories) {
516
+ const input = JSON.stringify({
517
+ status: "completed",
518
+ summary: "Testing category",
519
+ files_created: [],
520
+ files_modified: [],
521
+ reasoning: { approach: "Did something good here" },
522
+ knowledge_to_broadcast: [
523
+ {
524
+ category,
525
+ content: "Test content",
526
+ severity: "info",
527
+ },
528
+ ],
529
+ });
530
+
531
+ const result = parseSubagentReturn(input);
532
+ expect(result.success).toBe(true);
533
+ }
534
+ });
535
+
536
+ it("should accept all valid severities", () => {
537
+ const severities = ["info", "warning", "critical"];
538
+
539
+ for (const severity of severities) {
540
+ const input = JSON.stringify({
541
+ status: "completed",
542
+ summary: "Testing severity",
543
+ files_created: [],
544
+ files_modified: [],
545
+ reasoning: { approach: "Did something good here" },
546
+ knowledge_to_broadcast: [
547
+ {
548
+ category: "pattern",
549
+ content: "Test content",
550
+ severity,
551
+ },
552
+ ],
553
+ });
554
+
555
+ const result = parseSubagentReturn(input);
556
+ expect(result.success).toBe(true);
557
+ }
558
+ });
559
+ });
560
+
561
+ describe("Reasoning validation", () => {
562
+ it("should reject completed status without reasoning", () => {
563
+ const input = JSON.stringify({
564
+ status: "completed",
565
+ summary: "This should fail",
566
+ files_created: [],
567
+ files_modified: [],
568
+ });
569
+
570
+ const result = parseSubagentReturn(input);
571
+ expect(result.success).toBe(false);
572
+ expect(result.errors.some((e) => e.includes("reasoning"))).toBe(true);
573
+ });
574
+
575
+ it("should reject completed status with reasoning but no approach", () => {
576
+ const input = JSON.stringify({
577
+ status: "completed",
578
+ summary: "This should fail",
579
+ files_created: [],
580
+ files_modified: [],
581
+ reasoning: {
582
+ challenges: ["Some challenge"],
583
+ },
584
+ });
585
+
586
+ const result = parseSubagentReturn(input);
587
+ expect(result.success).toBe(false);
588
+ expect(result.errors.some((e) => e.includes("reasoning.approach"))).toBe(true);
589
+ });
590
+
591
+ it("should reject completed status with approach shorter than 20 characters", () => {
592
+ const input = JSON.stringify({
593
+ status: "completed",
594
+ summary: "This should fail",
595
+ files_created: [],
596
+ files_modified: [],
597
+ reasoning: {
598
+ approach: "Too short",
599
+ },
600
+ });
601
+
602
+ const result = parseSubagentReturn(input);
603
+ expect(result.success).toBe(false);
604
+ expect(result.errors.some((e) => e.includes("reasoning.approach") && e.includes("20"))).toBe(true);
605
+ });
606
+
607
+ it("should accept completed status with valid reasoning", () => {
608
+ const input = JSON.stringify({
609
+ status: "completed",
610
+ summary: "Task completed well",
611
+ files_created: [],
612
+ files_modified: [],
613
+ reasoning: {
614
+ approach: "I analyzed the codebase and implemented a solution using existing patterns",
615
+ },
616
+ });
617
+
618
+ const result = parseSubagentReturn(input);
619
+ expect(result.success).toBe(true);
620
+ });
621
+
622
+ it("should accept reasoning with all optional fields", () => {
623
+ const input = JSON.stringify({
624
+ status: "completed",
625
+ summary: "Task completed well",
626
+ files_created: [],
627
+ files_modified: [],
628
+ reasoning: {
629
+ approach: "Used factory pattern for better extensibility and maintainability",
630
+ challenges: ["Refactoring legacy code", "Maintaining backwards compatibility"],
631
+ alternatives: ["Could have used singleton", "Considered dependency injection"],
632
+ recommendations: "Add integration tests for the new factory",
633
+ },
634
+ });
635
+
636
+ const result = parseSubagentReturn(input);
637
+ expect(result.success).toBe(true);
638
+ expect(result.data!.reasoning!.challenges).toHaveLength(2);
639
+ expect(result.data!.reasoning!.alternatives).toHaveLength(2);
640
+ });
641
+
642
+ it("should not require reasoning for blocked status", () => {
643
+ const input = JSON.stringify({
644
+ status: "blocked",
645
+ summary: "Cannot proceed",
646
+ files_created: [],
647
+ files_modified: [],
648
+ blockers: ["Missing configuration"],
649
+ });
650
+
651
+ const result = parseSubagentReturn(input);
652
+ expect(result.success).toBe(true);
653
+ });
654
+
655
+ it("should not require reasoning for needs_decision status", () => {
656
+ const input = JSON.stringify({
657
+ status: "needs_decision",
658
+ summary: "Need guidance",
659
+ files_created: [],
660
+ files_modified: [],
661
+ blockers: ["Which approach to use?"],
662
+ });
663
+
664
+ const result = parseSubagentReturn(input);
665
+ expect(result.success).toBe(true);
666
+ });
667
+ });
668
+
669
+ describe("Utilities validation", () => {
670
+ it("should accept valid utilities_created", () => {
671
+ const input = JSON.stringify({
672
+ status: "completed",
673
+ summary: "Created utilities",
674
+ files_created: ["utils.ts"],
675
+ files_modified: [],
676
+ reasoning: { approach: "Created reusable utility functions for date handling" },
677
+ utilities_created: [
678
+ {
679
+ name: "formatDate",
680
+ file: "utils/date.ts",
681
+ type: "function",
682
+ signature: "(date: Date) => string",
683
+ description: "Formats dates",
684
+ },
685
+ ],
686
+ });
687
+
688
+ const result = parseSubagentReturn(input);
689
+ expect(result.success).toBe(true);
690
+ expect(result.data!.utilities_created).toHaveLength(1);
691
+ });
692
+
693
+ it("should reject utilities_created with missing name", () => {
694
+ const input = JSON.stringify({
695
+ status: "completed",
696
+ summary: "This should fail",
697
+ files_created: [],
698
+ files_modified: [],
699
+ reasoning: { approach: "Did something good here" },
700
+ utilities_created: [
701
+ {
702
+ file: "utils.ts",
703
+ type: "function",
704
+ },
705
+ ],
706
+ });
707
+
708
+ const result = parseSubagentReturn(input);
709
+ expect(result.success).toBe(false);
710
+ expect(result.errors.some((e) => e.includes("utilities_created[0].name"))).toBe(true);
711
+ });
712
+
713
+ it("should reject utilities_created with missing file", () => {
714
+ const input = JSON.stringify({
715
+ status: "completed",
716
+ summary: "This should fail",
717
+ files_created: [],
718
+ files_modified: [],
719
+ reasoning: { approach: "Did something good here" },
720
+ utilities_created: [
721
+ {
722
+ name: "formatDate",
723
+ type: "function",
724
+ },
725
+ ],
726
+ });
727
+
728
+ const result = parseSubagentReturn(input);
729
+ expect(result.success).toBe(false);
730
+ expect(result.errors.some((e) => e.includes("utilities_created[0].file"))).toBe(true);
731
+ });
732
+
733
+ it("should accept utilities_created with only required fields", () => {
734
+ const input = JSON.stringify({
735
+ status: "completed",
736
+ summary: "Created minimal utility",
737
+ files_created: [],
738
+ files_modified: [],
739
+ reasoning: { approach: "Did something good here" },
740
+ utilities_created: [
741
+ {
742
+ name: "helper",
743
+ file: "utils.ts",
744
+ },
745
+ ],
746
+ });
747
+
748
+ const result = parseSubagentReturn(input);
749
+ expect(result.success).toBe(true);
750
+ });
751
+
752
+ it("should reject utilities_created as non-array", () => {
753
+ const input = JSON.stringify({
754
+ status: "completed",
755
+ summary: "This should fail",
756
+ files_created: [],
757
+ files_modified: [],
758
+ reasoning: { approach: "Did something good here" },
759
+ utilities_created: "not-an-array",
760
+ });
761
+
762
+ const result = parseSubagentReturn(input);
763
+ expect(result.success).toBe(false);
764
+ expect(result.errors.some((e) => e.includes("utilities_created") && e.includes("array"))).toBe(true);
765
+ });
766
+ });
767
+
768
+ describe("Semantic validations", () => {
769
+ it("should require blockers for blocked status", () => {
770
+ const input = JSON.stringify({
771
+ status: "blocked",
772
+ summary: "This should fail",
773
+ files_created: [],
774
+ files_modified: [],
775
+ });
776
+
777
+ const result = parseSubagentReturn(input);
778
+ expect(result.success).toBe(false);
779
+ expect(result.errors.some((e) => e.includes("blocked") && e.includes("blockers"))).toBe(true);
780
+ });
781
+
782
+ it("should require non-empty blockers for blocked status", () => {
783
+ const input = JSON.stringify({
784
+ status: "blocked",
785
+ summary: "This should fail",
786
+ files_created: [],
787
+ files_modified: [],
788
+ blockers: [],
789
+ });
790
+
791
+ const result = parseSubagentReturn(input);
792
+ expect(result.success).toBe(false);
793
+ expect(result.errors.some((e) => e.includes("blocked") && e.includes("blockers"))).toBe(true);
794
+ });
795
+
796
+ it("should require blockers for needs_decision status", () => {
797
+ const input = JSON.stringify({
798
+ status: "needs_decision",
799
+ summary: "This should fail",
800
+ files_created: [],
801
+ files_modified: [],
802
+ });
803
+
804
+ const result = parseSubagentReturn(input);
805
+ expect(result.success).toBe(false);
806
+ expect(result.errors.some((e) => e.includes("needs_decision") && e.includes("blockers"))).toBe(true);
807
+ });
808
+
809
+ it("should require non-empty blockers for needs_decision status", () => {
810
+ const input = JSON.stringify({
811
+ status: "needs_decision",
812
+ summary: "This should fail",
813
+ files_created: [],
814
+ files_modified: [],
815
+ blockers: [],
816
+ });
817
+
818
+ const result = parseSubagentReturn(input);
819
+ expect(result.success).toBe(false);
820
+ expect(result.errors.some((e) => e.includes("needs_decision") && e.includes("blockers"))).toBe(true);
821
+ });
822
+ });
823
+
824
+ describe("No JSON found", () => {
825
+ it("should reject input with no JSON", () => {
826
+ const input = "Just plain text without any JSON structure";
827
+
828
+ const result = parseSubagentReturn(input);
829
+ expect(result.success).toBe(false);
830
+ expect(result.errors.some((e) => e.includes("Nenhum JSON encontrado"))).toBe(true);
831
+ });
832
+ });
833
+
834
+ describe("Invalid JSON", () => {
835
+ it("should reject malformed JSON", () => {
836
+ const input = '{"status": "completed", invalid json}';
837
+
838
+ const result = parseSubagentReturn(input);
839
+ expect(result.success).toBe(false);
840
+ expect(result.errors.some((e) => e.includes("JSON invalido"))).toBe(true);
841
+ });
842
+
843
+ it("should reject JSON with trailing comma", () => {
844
+ const input = '{"status": "completed", "summary": "test",}';
845
+
846
+ const result = parseSubagentReturn(input);
847
+ expect(result.success).toBe(false);
848
+ expect(result.errors.some((e) => e.includes("JSON invalido"))).toBe(true);
849
+ });
850
+ });
851
+ });
852
+
853
+ describe("formatValidationErrors", () => {
854
+ it("should return empty string for successful result", () => {
855
+ const result: ParseResult = {
856
+ success: true,
857
+ data: {
858
+ status: "completed",
859
+ summary: "Task completed",
860
+ files_created: [],
861
+ files_modified: [],
862
+ reasoning: { approach: "Did something good" },
863
+ },
864
+ errors: [],
865
+ rawInput: "{}",
866
+ };
867
+
868
+ const formatted = formatValidationErrors(result);
869
+ expect(formatted).toBe("");
870
+ });
871
+
872
+ it("should format single error", () => {
873
+ const result: ParseResult = {
874
+ success: false,
875
+ errors: ["Campo 'status' invalido"],
876
+ rawInput: "{}",
877
+ };
878
+
879
+ const formatted = formatValidationErrors(result);
880
+ expect(formatted).toContain("ERRO: Retorno do subagent INVALIDO");
881
+ expect(formatted).toContain("Campo 'status' invalido");
882
+ });
883
+
884
+ it("should format multiple errors", () => {
885
+ const result: ParseResult = {
886
+ success: false,
887
+ errors: [
888
+ "Campo 'status' invalido",
889
+ "Campo 'summary' deve ter pelo menos 10 caracteres",
890
+ "Status 'blocked' requer campo 'blockers' nao vazio",
891
+ ],
892
+ rawInput: "{}",
893
+ };
894
+
895
+ const formatted = formatValidationErrors(result);
896
+ expect(formatted).toContain("Campo 'status' invalido");
897
+ expect(formatted).toContain("Campo 'summary' deve ter pelo menos 10 caracteres");
898
+ expect(formatted).toContain("Status 'blocked' requer campo 'blockers' nao vazio");
899
+ });
900
+
901
+ it("should include protocol example in output", () => {
902
+ const result: ParseResult = {
903
+ success: false,
904
+ errors: ["Some error"],
905
+ rawInput: "{}",
906
+ };
907
+
908
+ const formatted = formatValidationErrors(result);
909
+ expect(formatted).toContain("O retorno deve seguir o formato:");
910
+ expect(formatted).toContain('"status": "completed | blocked | needs_decision"');
911
+ expect(formatted).toContain('"reasoning"');
912
+ });
913
+
914
+ it("should include separator lines", () => {
915
+ const result: ParseResult = {
916
+ success: false,
917
+ errors: ["Error 1", "Error 2"],
918
+ rawInput: "{}",
919
+ };
920
+
921
+ const formatted = formatValidationErrors(result);
922
+ expect(formatted).toContain("─".repeat(50));
923
+ });
924
+
925
+ it("should format each error with bullet point", () => {
926
+ const result: ParseResult = {
927
+ success: false,
928
+ errors: ["First error", "Second error"],
929
+ rawInput: "{}",
930
+ };
931
+
932
+ const formatted = formatValidationErrors(result);
933
+ expect(formatted).toContain(" - First error");
934
+ expect(formatted).toContain(" - Second error");
935
+ });
936
+ });