recursive-llm-ts 5.0.1 → 5.0.2

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.
@@ -1,895 +0,0 @@
1
- package rlm
2
-
3
- import (
4
- "encoding/json"
5
- "fmt"
6
- "strings"
7
- "testing"
8
- )
9
-
10
- // ─── extractBalancedJSON Tests ───────────────────────────────────────────────
11
-
12
- func TestExtractBalancedJSON_SimpleObject(t *testing.T) {
13
- input := `{"name": "Alice", "age": 30}`
14
- results := extractBalancedJSON(input)
15
- if len(results) != 1 {
16
- t.Fatalf("expected 1 result, got %d", len(results))
17
- }
18
- if results[0] != input {
19
- t.Errorf("expected %q, got %q", input, results[0])
20
- }
21
- }
22
-
23
- func TestExtractBalancedJSON_NestedObject(t *testing.T) {
24
- input := `{"user": {"name": "Alice", "address": {"city": "NYC", "zip": "10001"}}}`
25
- results := extractBalancedJSON(input)
26
- if len(results) != 1 {
27
- t.Fatalf("expected 1 result, got %d", len(results))
28
- }
29
-
30
- var parsed map[string]interface{}
31
- if err := json.Unmarshal([]byte(results[0]), &parsed); err != nil {
32
- t.Fatalf("failed to parse extracted JSON: %v", err)
33
- }
34
-
35
- user, ok := parsed["user"].(map[string]interface{})
36
- if !ok {
37
- t.Fatal("missing or invalid 'user' field")
38
- }
39
- address, ok := user["address"].(map[string]interface{})
40
- if !ok {
41
- t.Fatal("missing or invalid 'address' field")
42
- }
43
- if address["city"] != "NYC" {
44
- t.Errorf("expected city 'NYC', got %v", address["city"])
45
- }
46
- }
47
-
48
- func TestExtractBalancedJSON_DeeplyNested(t *testing.T) {
49
- // 4 levels of nesting - the old regex would fail on this
50
- input := `{"a": {"b": {"c": {"d": "deep"}}}}`
51
- results := extractBalancedJSON(input)
52
- if len(results) != 1 {
53
- t.Fatalf("expected 1 result, got %d", len(results))
54
- }
55
-
56
- var parsed map[string]interface{}
57
- if err := json.Unmarshal([]byte(results[0]), &parsed); err != nil {
58
- t.Fatalf("failed to parse: %v", err)
59
- }
60
- // Navigate to the deepest level
61
- a := parsed["a"].(map[string]interface{})
62
- b := a["b"].(map[string]interface{})
63
- c := b["c"].(map[string]interface{})
64
- if c["d"] != "deep" {
65
- t.Errorf("expected 'deep', got %v", c["d"])
66
- }
67
- }
68
-
69
- func TestExtractBalancedJSON_WithSurroundingText(t *testing.T) {
70
- input := `Here is the JSON result: {"key": "value"} and some trailing text.`
71
- results := extractBalancedJSON(input)
72
- if len(results) != 1 {
73
- t.Fatalf("expected 1 result, got %d", len(results))
74
- }
75
- if results[0] != `{"key": "value"}` {
76
- t.Errorf("expected clean JSON, got %q", results[0])
77
- }
78
- }
79
-
80
- func TestExtractBalancedJSON_MultipleObjects(t *testing.T) {
81
- input := `First: {"a": 1} Second: {"b": 2}`
82
- results := extractBalancedJSON(input)
83
- if len(results) != 2 {
84
- t.Fatalf("expected 2 results, got %d", len(results))
85
- }
86
- }
87
-
88
- func TestExtractBalancedJSON_BracesInStrings(t *testing.T) {
89
- input := `{"text": "This has { braces } inside", "value": 42}`
90
- results := extractBalancedJSON(input)
91
- if len(results) != 1 {
92
- t.Fatalf("expected 1 result, got %d", len(results))
93
- }
94
-
95
- var parsed map[string]interface{}
96
- if err := json.Unmarshal([]byte(results[0]), &parsed); err != nil {
97
- t.Fatalf("failed to parse: %v", err)
98
- }
99
- if parsed["text"] != "This has { braces } inside" {
100
- t.Errorf("string content not preserved: %v", parsed["text"])
101
- }
102
- }
103
-
104
- func TestExtractBalancedJSON_EscapedQuotes(t *testing.T) {
105
- input := `{"text": "He said \"hello\"", "count": 1}`
106
- results := extractBalancedJSON(input)
107
- if len(results) != 1 {
108
- t.Fatalf("expected 1 result, got %d", len(results))
109
- }
110
-
111
- var parsed map[string]interface{}
112
- if err := json.Unmarshal([]byte(results[0]), &parsed); err != nil {
113
- t.Fatalf("failed to parse: %v", err)
114
- }
115
- }
116
-
117
- func TestExtractBalancedJSON_NoObjects(t *testing.T) {
118
- input := `Just some plain text with no JSON`
119
- results := extractBalancedJSON(input)
120
- if len(results) != 0 {
121
- t.Errorf("expected 0 results, got %d", len(results))
122
- }
123
- }
124
-
125
- func TestExtractBalancedJSON_ComplexNestedWithArrays(t *testing.T) {
126
- // This mimics the sentiment analysis schema structure
127
- input := `{"sentimentValue": 3, "sentimentExplanation": "Positive", "phrases": [{"sentimentValue": 4, "phrase": "Great work"}, {"sentimentValue": 2, "phrase": "Could improve"}], "keyMoments": [{"phrase": "Budget discussion", "type": "budget_concern"}]}`
128
- results := extractBalancedJSON(input)
129
- if len(results) != 1 {
130
- t.Fatalf("expected 1 result, got %d", len(results))
131
- }
132
-
133
- var parsed map[string]interface{}
134
- if err := json.Unmarshal([]byte(results[0]), &parsed); err != nil {
135
- t.Fatalf("failed to parse complex nested JSON: %v", err)
136
- }
137
-
138
- phrases, ok := parsed["phrases"].([]interface{})
139
- if !ok {
140
- t.Fatal("expected 'phrases' to be an array")
141
- }
142
- if len(phrases) != 2 {
143
- t.Errorf("expected 2 phrases, got %d", len(phrases))
144
- }
145
- }
146
-
147
- // ─── wrapFieldSchema Tests ──────────────────────────────────────────────────
148
-
149
- func TestWrapFieldSchema_Number(t *testing.T) {
150
- min := 1.0
151
- max := 5.0
152
- fieldSchema := &JSONSchema{
153
- Type: "number",
154
- Minimum: &min,
155
- Maximum: &max,
156
- }
157
-
158
- wrapped := wrapFieldSchema("sentimentValue", fieldSchema)
159
-
160
- if wrapped.Type != "object" {
161
- t.Errorf("expected object type, got %s", wrapped.Type)
162
- }
163
- if len(wrapped.Required) != 1 || wrapped.Required[0] != "sentimentValue" {
164
- t.Errorf("expected required [sentimentValue], got %v", wrapped.Required)
165
- }
166
- if wrapped.Properties["sentimentValue"] != fieldSchema {
167
- t.Error("inner schema should reference the original")
168
- }
169
- }
170
-
171
- func TestWrapFieldSchema_Array(t *testing.T) {
172
- fieldSchema := &JSONSchema{
173
- Type: "array",
174
- Items: &JSONSchema{
175
- Type: "object",
176
- Properties: map[string]*JSONSchema{
177
- "phrase": {Type: "string"},
178
- "score": {Type: "number"},
179
- },
180
- Required: []string{"phrase", "score"},
181
- },
182
- }
183
-
184
- wrapped := wrapFieldSchema("phrases", fieldSchema)
185
-
186
- if wrapped.Type != "object" {
187
- t.Errorf("expected object type, got %s", wrapped.Type)
188
- }
189
- innerSchema := wrapped.Properties["phrases"]
190
- if innerSchema.Type != "array" {
191
- t.Errorf("expected inner type array, got %s", innerSchema.Type)
192
- }
193
- }
194
-
195
- func TestWrapFieldSchema_String(t *testing.T) {
196
- fieldSchema := &JSONSchema{
197
- Type: "string",
198
- }
199
-
200
- wrapped := wrapFieldSchema("explanation", fieldSchema)
201
-
202
- if wrapped.Type != "object" {
203
- t.Errorf("expected object type, got %s", wrapped.Type)
204
- }
205
- if wrapped.Properties["explanation"].Type != "string" {
206
- t.Error("inner schema should be string type")
207
- }
208
- }
209
-
210
- // ─── parseAndValidateJSON Tests ─────────────────────────────────────────────
211
-
212
- func TestParseAndValidateJSON_SimpleObject(t *testing.T) {
213
- schema := &JSONSchema{
214
- Type: "object",
215
- Properties: map[string]*JSONSchema{
216
- "name": {Type: "string"},
217
- "age": {Type: "number"},
218
- },
219
- Required: []string{"name", "age"},
220
- }
221
-
222
- result, err := parseAndValidateJSON(`{"name": "Alice", "age": 30}`, schema)
223
- if err != nil {
224
- t.Fatalf("unexpected error: %v", err)
225
- }
226
- if result["name"] != "Alice" {
227
- t.Errorf("expected name 'Alice', got %v", result["name"])
228
- }
229
- }
230
-
231
- func TestParseAndValidateJSON_WithMarkdownCodeBlock(t *testing.T) {
232
- schema := &JSONSchema{
233
- Type: "object",
234
- Properties: map[string]*JSONSchema{
235
- "key": {Type: "string"},
236
- },
237
- Required: []string{"key"},
238
- }
239
-
240
- input := "```json\n{\"key\": \"value\"}\n```"
241
- result, err := parseAndValidateJSON(input, schema)
242
- if err != nil {
243
- t.Fatalf("unexpected error: %v", err)
244
- }
245
- if result["key"] != "value" {
246
- t.Errorf("expected 'value', got %v", result["key"])
247
- }
248
- }
249
-
250
- func TestParseAndValidateJSON_WithSurroundingText(t *testing.T) {
251
- schema := &JSONSchema{
252
- Type: "object",
253
- Properties: map[string]*JSONSchema{
254
- "status": {Type: "string"},
255
- },
256
- Required: []string{"status"},
257
- }
258
-
259
- input := `Here is the result: {"status": "ok"} hope that helps!`
260
- result, err := parseAndValidateJSON(input, schema)
261
- if err != nil {
262
- t.Fatalf("unexpected error: %v", err)
263
- }
264
- if result["status"] != "ok" {
265
- t.Errorf("expected 'ok', got %v", result["status"])
266
- }
267
- }
268
-
269
- func TestParseAndValidateJSON_DeeplyNestedObject(t *testing.T) {
270
- schema := &JSONSchema{
271
- Type: "object",
272
- Properties: map[string]*JSONSchema{
273
- "data": {
274
- Type: "object",
275
- Properties: map[string]*JSONSchema{
276
- "inner": {
277
- Type: "object",
278
- Properties: map[string]*JSONSchema{
279
- "value": {Type: "string"},
280
- },
281
- Required: []string{"value"},
282
- },
283
- },
284
- Required: []string{"inner"},
285
- },
286
- },
287
- Required: []string{"data"},
288
- }
289
-
290
- input := `{"data": {"inner": {"value": "deep"}}}`
291
- result, err := parseAndValidateJSON(input, schema)
292
- if err != nil {
293
- t.Fatalf("unexpected error: %v", err)
294
- }
295
- data := result["data"].(map[string]interface{})
296
- inner := data["inner"].(map[string]interface{})
297
- if inner["value"] != "deep" {
298
- t.Errorf("expected 'deep', got %v", inner["value"])
299
- }
300
- }
301
-
302
- func TestParseAndValidateJSON_NonObjectSchema_Array(t *testing.T) {
303
- schema := &JSONSchema{
304
- Type: "array",
305
- Items: &JSONSchema{
306
- Type: "string",
307
- },
308
- }
309
-
310
- input := `["a", "b", "c"]`
311
- result, err := parseAndValidateJSON(input, schema)
312
- if err != nil {
313
- t.Fatalf("unexpected error: %v", err)
314
- }
315
- arr, ok := result["__value__"].([]interface{})
316
- if !ok {
317
- t.Fatalf("expected array in __value__, got %T", result["__value__"])
318
- }
319
- if len(arr) != 3 {
320
- t.Errorf("expected 3 items, got %d", len(arr))
321
- }
322
- }
323
-
324
- func TestParseAndValidateJSON_NonObjectSchema_WrappedArray(t *testing.T) {
325
- schema := &JSONSchema{
326
- Type: "array",
327
- Items: &JSONSchema{
328
- Type: "string",
329
- },
330
- }
331
-
332
- // LLM sometimes wraps array in an object
333
- input := `{"items": ["a", "b", "c"]}`
334
- result, err := parseAndValidateJSON(input, schema)
335
- if err != nil {
336
- t.Fatalf("unexpected error: %v", err)
337
- }
338
- arr, ok := result["__value__"].([]interface{})
339
- if !ok {
340
- t.Fatalf("expected array in __value__, got %T", result["__value__"])
341
- }
342
- if len(arr) != 3 {
343
- t.Errorf("expected 3 items, got %d", len(arr))
344
- }
345
- }
346
-
347
- func TestParseAndValidateJSON_NonObjectSchema_Number(t *testing.T) {
348
- schema := &JSONSchema{
349
- Type: "number",
350
- }
351
-
352
- input := `42`
353
- result, err := parseAndValidateJSON(input, schema)
354
- if err != nil {
355
- t.Fatalf("unexpected error: %v", err)
356
- }
357
- val, ok := result["__value__"].(float64)
358
- if !ok {
359
- t.Fatalf("expected float64 in __value__, got %T", result["__value__"])
360
- }
361
- if val != 42 {
362
- t.Errorf("expected 42, got %v", val)
363
- }
364
- }
365
-
366
- func TestParseAndValidateJSON_NonObjectSchema_WrappedNumber(t *testing.T) {
367
- schema := &JSONSchema{
368
- Type: "number",
369
- }
370
-
371
- // LLM wraps number in an object
372
- input := `{"sentimentValue": 3.5}`
373
- result, err := parseAndValidateJSON(input, schema)
374
- if err != nil {
375
- t.Fatalf("unexpected error: %v", err)
376
- }
377
- val, ok := result["__value__"].(float64)
378
- if !ok {
379
- t.Fatalf("expected float64 in __value__, got %T", result["__value__"])
380
- }
381
- if val != 3.5 {
382
- t.Errorf("expected 3.5, got %v", val)
383
- }
384
- }
385
-
386
- func TestParseAndValidateJSON_MissingRequiredField(t *testing.T) {
387
- schema := &JSONSchema{
388
- Type: "object",
389
- Properties: map[string]*JSONSchema{
390
- "name": {Type: "string"},
391
- "age": {Type: "number"},
392
- },
393
- Required: []string{"name", "age"},
394
- }
395
-
396
- input := `{"name": "Alice"}`
397
- _, err := parseAndValidateJSON(input, schema)
398
- if err == nil {
399
- t.Fatal("expected validation error")
400
- }
401
- if !strings.Contains(err.Error(), "missing required field: age") {
402
- t.Errorf("expected missing field error, got: %v", err)
403
- }
404
- }
405
-
406
- func TestParseAndValidateJSON_TypeMismatch(t *testing.T) {
407
- schema := &JSONSchema{
408
- Type: "object",
409
- Properties: map[string]*JSONSchema{
410
- "count": {Type: "number"},
411
- },
412
- Required: []string{"count"},
413
- }
414
-
415
- input := `{"count": "not a number"}`
416
- _, err := parseAndValidateJSON(input, schema)
417
- if err == nil {
418
- t.Fatal("expected type validation error")
419
- }
420
- }
421
-
422
- // ─── validateValue Tests ────────────────────────────────────────────────────
423
-
424
- func TestValidateValue_IntegerAlsoAcceptsFloat64(t *testing.T) {
425
- schema := &JSONSchema{Type: "integer"}
426
-
427
- // JSON numbers always decode as float64, but integer type should accept them
428
- err := validateValue(float64(42), schema)
429
- if err != nil {
430
- t.Errorf("integer schema should accept float64: %v", err)
431
- }
432
- }
433
-
434
- func TestValidateValue_NumberType(t *testing.T) {
435
- schema := &JSONSchema{Type: "number"}
436
-
437
- if err := validateValue(float64(3.14), schema); err != nil {
438
- t.Errorf("should accept float: %v", err)
439
- }
440
- if err := validateValue("not a number", schema); err == nil {
441
- t.Error("should reject string")
442
- }
443
- }
444
-
445
- func TestValidateValue_NullableField(t *testing.T) {
446
- schema := &JSONSchema{Type: "string", Nullable: true}
447
-
448
- if err := validateValue(nil, schema); err != nil {
449
- t.Errorf("nullable field should accept nil: %v", err)
450
- }
451
- }
452
-
453
- func TestValidateValue_ArrayWithItems(t *testing.T) {
454
- schema := &JSONSchema{
455
- Type: "array",
456
- Items: &JSONSchema{
457
- Type: "object",
458
- Properties: map[string]*JSONSchema{
459
- "phrase": {Type: "string"},
460
- "score": {Type: "number"},
461
- },
462
- Required: []string{"phrase", "score"},
463
- },
464
- }
465
-
466
- valid := []interface{}{
467
- map[string]interface{}{"phrase": "good", "score": float64(4)},
468
- map[string]interface{}{"phrase": "bad", "score": float64(1)},
469
- }
470
-
471
- if err := validateValue(valid, schema); err != nil {
472
- t.Errorf("valid array should pass: %v", err)
473
- }
474
-
475
- invalid := []interface{}{
476
- map[string]interface{}{"phrase": "missing score"},
477
- }
478
-
479
- if err := validateValue(invalid, schema); err == nil {
480
- t.Error("invalid array item should fail")
481
- }
482
- }
483
-
484
- // ─── decomposeSchema Tests ──────────────────────────────────────────────────
485
-
486
- func TestDecomposeSchema_SentimentAnalysis(t *testing.T) {
487
- min := 1.0
488
- max := 5.0
489
-
490
- schema := &JSONSchema{
491
- Type: "object",
492
- Properties: map[string]*JSONSchema{
493
- "sentimentValue": {
494
- Type: "number",
495
- Minimum: &min,
496
- Maximum: &max,
497
- },
498
- "sentimentExplanation": {
499
- Type: "string",
500
- },
501
- "phrases": {
502
- Type: "array",
503
- Items: &JSONSchema{
504
- Type: "object",
505
- Properties: map[string]*JSONSchema{
506
- "sentimentValue": {Type: "number", Minimum: &min, Maximum: &max},
507
- "phrase": {Type: "string"},
508
- },
509
- Required: []string{"sentimentValue", "phrase"},
510
- },
511
- },
512
- "keyMoments": {
513
- Type: "array",
514
- Items: &JSONSchema{
515
- Type: "object",
516
- Properties: map[string]*JSONSchema{
517
- "phrase": {Type: "string"},
518
- "type": {Type: "string", Enum: []string{"churn_mention", "personnel_change"}},
519
- },
520
- Required: []string{"phrase", "type"},
521
- },
522
- },
523
- },
524
- Required: []string{"sentimentValue", "sentimentExplanation", "phrases", "keyMoments"},
525
- }
526
-
527
- subTasks := decomposeSchema(schema)
528
-
529
- if len(subTasks) != 4 {
530
- t.Fatalf("expected 4 subtasks, got %d", len(subTasks))
531
- }
532
-
533
- // Check that each field has a corresponding subtask
534
- fieldNames := make(map[string]bool)
535
- for _, task := range subTasks {
536
- fieldName := strings.TrimPrefix(task.ID, "field_")
537
- fieldNames[fieldName] = true
538
- }
539
-
540
- for _, expected := range []string{"sentimentValue", "sentimentExplanation", "phrases", "keyMoments"} {
541
- if !fieldNames[expected] {
542
- t.Errorf("missing subtask for field: %s", expected)
543
- }
544
- }
545
- }
546
-
547
- func TestDecomposeSchema_NonObject(t *testing.T) {
548
- schema := &JSONSchema{Type: "array", Items: &JSONSchema{Type: "string"}}
549
- subTasks := decomposeSchema(schema)
550
- if len(subTasks) != 0 {
551
- t.Errorf("non-object schema should produce 0 subtasks, got %d", len(subTasks))
552
- }
553
- }
554
-
555
- // ─── generateFieldQuery Tests ───────────────────────────────────────────────
556
-
557
- func TestGenerateFieldQuery_Number(t *testing.T) {
558
- schema := &JSONSchema{Type: "number"}
559
- query := generateFieldQuery("sentimentValue", schema)
560
-
561
- if !strings.Contains(query, "sentimentValue") {
562
- t.Error("query should reference the field name")
563
- }
564
- if !strings.Contains(query, `{"sentimentValue": <number>}`) {
565
- t.Errorf("query should include JSON object format example, got: %s", query)
566
- }
567
- }
568
-
569
- func TestGenerateFieldQuery_StringWithEnum(t *testing.T) {
570
- schema := &JSONSchema{
571
- Type: "string",
572
- Enum: []string{"positive", "negative", "neutral"},
573
- }
574
- query := generateFieldQuery("sentiment", schema)
575
-
576
- if !strings.Contains(query, "positive") {
577
- t.Error("query should list enum values")
578
- }
579
- if !strings.Contains(query, "EXACTLY one of") {
580
- t.Error("query should mention exact match")
581
- }
582
- }
583
-
584
- func TestGenerateFieldQuery_ArrayWithObjectItems(t *testing.T) {
585
- schema := &JSONSchema{
586
- Type: "array",
587
- Items: &JSONSchema{
588
- Type: "object",
589
- Properties: map[string]*JSONSchema{
590
- "name": {Type: "string"},
591
- "score": {Type: "number"},
592
- },
593
- Required: []string{"name", "score"},
594
- },
595
- }
596
- query := generateFieldQuery("results", schema)
597
-
598
- if !strings.Contains(query, "results") {
599
- t.Error("query should reference field name")
600
- }
601
- if !strings.Contains(query, "REQUIRED") {
602
- t.Error("query should mention required fields")
603
- }
604
- }
605
-
606
- // ─── generateSchemaConstraints Tests ────────────────────────────────────────
607
-
608
- func TestGenerateSchemaConstraints_WithNumberRange(t *testing.T) {
609
- min := 1.0
610
- max := 5.0
611
- schema := &JSONSchema{
612
- Type: "object",
613
- Properties: map[string]*JSONSchema{
614
- "score": {Type: "number", Minimum: &min, Maximum: &max},
615
- },
616
- }
617
-
618
- constraints := generateSchemaConstraints(schema)
619
- if !strings.Contains(constraints, ">= 1") {
620
- t.Error("should include minimum constraint")
621
- }
622
- if !strings.Contains(constraints, "<= 5") {
623
- t.Error("should include maximum constraint")
624
- }
625
- }
626
-
627
- func TestGenerateSchemaConstraints_WithEnum(t *testing.T) {
628
- schema := &JSONSchema{
629
- Type: "object",
630
- Properties: map[string]*JSONSchema{
631
- "status": {Type: "string", Enum: []string{"active", "inactive"}},
632
- },
633
- }
634
-
635
- constraints := generateSchemaConstraints(schema)
636
- if !strings.Contains(constraints, "EXACTLY") {
637
- t.Error("should emphasize exact match")
638
- }
639
- if !strings.Contains(constraints, "active") {
640
- t.Error("should list enum values")
641
- }
642
- }
643
-
644
- // ─── buildValidationFeedback Tests ──────────────────────────────────────────
645
-
646
- func TestBuildValidationFeedback_MissingField(t *testing.T) {
647
- schema := &JSONSchema{
648
- Type: "object",
649
- Properties: map[string]*JSONSchema{
650
- "name": {Type: "string"},
651
- "email": {Type: "string"},
652
- },
653
- Required: []string{"name", "email"},
654
- }
655
-
656
- err := fmt.Errorf("missing required field: email")
657
- feedback := buildValidationFeedback(err, schema, `{"name": "Alice"}`)
658
-
659
- if !strings.Contains(feedback, "email") {
660
- t.Error("feedback should mention the missing field")
661
- }
662
- if !strings.Contains(feedback, "REQUIRED") {
663
- t.Error("feedback should indicate field is required")
664
- }
665
- if !strings.Contains(feedback, "EXPECTED SCHEMA") {
666
- t.Error("feedback should include the expected schema")
667
- }
668
- }
669
-
670
- func TestBuildValidationFeedback_TypeMismatch(t *testing.T) {
671
- schema := &JSONSchema{
672
- Type: "object",
673
- Properties: map[string]*JSONSchema{
674
- "count": {Type: "number"},
675
- },
676
- Required: []string{"count"},
677
- }
678
-
679
- err := fmt.Errorf("field count: expected number, got string")
680
- feedback := buildValidationFeedback(err, schema, `{"count": "five"}`)
681
-
682
- if !strings.Contains(feedback, "Type mismatch") {
683
- t.Error("feedback should mention type mismatch")
684
- }
685
- }
686
-
687
- // ─── buildExampleJSON Tests ─────────────────────────────────────────────────
688
-
689
- func TestBuildExampleJSON_SimpleObject(t *testing.T) {
690
- schema := &JSONSchema{
691
- Type: "object",
692
- Properties: map[string]*JSONSchema{
693
- "name": {Type: "string"},
694
- "score": {Type: "number"},
695
- },
696
- Required: []string{"name", "score"},
697
- }
698
-
699
- example := buildExampleJSON(schema)
700
- if example == "" {
701
- t.Fatal("expected non-empty example")
702
- }
703
-
704
- var parsed map[string]interface{}
705
- if err := json.Unmarshal([]byte(example), &parsed); err != nil {
706
- t.Fatalf("example should be valid JSON: %v", err)
707
- }
708
- if _, ok := parsed["name"].(string); !ok {
709
- t.Error("example should have string 'name'")
710
- }
711
- }
712
-
713
- func TestBuildExampleJSON_WithEnum(t *testing.T) {
714
- schema := &JSONSchema{
715
- Type: "object",
716
- Properties: map[string]*JSONSchema{
717
- "status": {Type: "string", Enum: []string{"active", "inactive"}},
718
- },
719
- Required: []string{"status"},
720
- }
721
-
722
- example := buildExampleJSON(schema)
723
- if !strings.Contains(example, "active") {
724
- t.Error("example should use first enum value")
725
- }
726
- }
727
-
728
- func TestBuildExampleJSON_NoRequiredFields(t *testing.T) {
729
- schema := &JSONSchema{
730
- Type: "object",
731
- Properties: map[string]*JSONSchema{
732
- "optional": {Type: "string"},
733
- },
734
- }
735
-
736
- example := buildExampleJSON(schema)
737
- if example != "" {
738
- t.Error("schema with no required fields should produce empty example")
739
- }
740
- }
741
-
742
- // ─── Integration-style tests for the full parallel flow ─────────────────────
743
-
744
- func TestParallelResultMerging_SimulatedWorkflow(t *testing.T) {
745
- // Simulate what happens when parallel results are merged
746
- min := 1.0
747
- max := 5.0
748
-
749
- fullSchema := &JSONSchema{
750
- Type: "object",
751
- Properties: map[string]*JSONSchema{
752
- "sentimentValue": {Type: "number", Minimum: &min, Maximum: &max},
753
- "explanation": {Type: "string"},
754
- "tags": {Type: "array", Items: &JSONSchema{Type: "string"}},
755
- },
756
- Required: []string{"sentimentValue", "explanation", "tags"},
757
- }
758
-
759
- // Simulate parallel results after unwrapping from wrapped schemas
760
- results := map[string]interface{}{
761
- "sentimentValue": float64(4),
762
- "explanation": "Very positive conversation",
763
- "tags": []interface{}{"positive", "engaged"},
764
- }
765
-
766
- // Validate merged result against full schema
767
- err := validateAgainstSchema(results, fullSchema)
768
- if err != nil {
769
- t.Errorf("merged results should validate: %v", err)
770
- }
771
- }
772
-
773
- func TestParallelResultMerging_NestedObjectField(t *testing.T) {
774
- // Test that object-type fields survive the wrap/unwrap cycle
775
- fullSchema := &JSONSchema{
776
- Type: "object",
777
- Properties: map[string]*JSONSchema{
778
- "metadata": {
779
- Type: "object",
780
- Properties: map[string]*JSONSchema{
781
- "author": {Type: "string"},
782
- "date": {Type: "string"},
783
- },
784
- Required: []string{"author", "date"},
785
- },
786
- "summary": {Type: "string"},
787
- },
788
- Required: []string{"metadata", "summary"},
789
- }
790
-
791
- // Simulate what we'd get from the wrapped sub-task for "metadata"
792
- wrappedResponse := `{"metadata": {"author": "Alice", "date": "2024-01-01"}}`
793
- wrappedSchema := wrapFieldSchema("metadata", fullSchema.Properties["metadata"])
794
-
795
- result, err := parseAndValidateJSON(wrappedResponse, wrappedSchema)
796
- if err != nil {
797
- t.Fatalf("wrapped response should parse: %v", err)
798
- }
799
-
800
- // Extract field value (simulating what structuredCompletionParallel does)
801
- metadataValue, ok := result["metadata"]
802
- if !ok {
803
- t.Fatal("expected 'metadata' key in result")
804
- }
805
-
806
- metadataObj, ok := metadataValue.(map[string]interface{})
807
- if !ok {
808
- t.Fatalf("expected metadata to be an object, got %T", metadataValue)
809
- }
810
-
811
- if metadataObj["author"] != "Alice" {
812
- t.Errorf("expected author 'Alice', got %v", metadataObj["author"])
813
- }
814
- }
815
-
816
- func TestParallelResultMerging_ArrayField(t *testing.T) {
817
- // Test that array-type fields survive the wrap/unwrap cycle
818
- arraySchema := &JSONSchema{
819
- Type: "array",
820
- Items: &JSONSchema{
821
- Type: "object",
822
- Properties: map[string]*JSONSchema{
823
- "name": {Type: "string"},
824
- "score": {Type: "number"},
825
- },
826
- Required: []string{"name", "score"},
827
- },
828
- }
829
-
830
- wrappedSchema := wrapFieldSchema("phrases", arraySchema)
831
- wrappedResponse := `{"phrases": [{"name": "hello", "score": 4.5}, {"name": "goodbye", "score": 2.0}]}`
832
-
833
- result, err := parseAndValidateJSON(wrappedResponse, wrappedSchema)
834
- if err != nil {
835
- t.Fatalf("wrapped array response should parse: %v", err)
836
- }
837
-
838
- phrasesValue, ok := result["phrases"]
839
- if !ok {
840
- t.Fatal("expected 'phrases' key in result")
841
- }
842
-
843
- phrases, ok := phrasesValue.([]interface{})
844
- if !ok {
845
- t.Fatalf("expected phrases to be an array, got %T", phrasesValue)
846
- }
847
-
848
- if len(phrases) != 2 {
849
- t.Errorf("expected 2 phrases, got %d", len(phrases))
850
- }
851
- }
852
-
853
- func TestParallelResultMerging_NumberField(t *testing.T) {
854
- // Test that number-type fields survive the wrap/unwrap cycle
855
- min := 1.0
856
- max := 5.0
857
- numberSchema := &JSONSchema{Type: "number", Minimum: &min, Maximum: &max}
858
- wrappedSchema := wrapFieldSchema("sentimentValue", numberSchema)
859
-
860
- wrappedResponse := `{"sentimentValue": 3.5}`
861
- result, err := parseAndValidateJSON(wrappedResponse, wrappedSchema)
862
- if err != nil {
863
- t.Fatalf("wrapped number response should parse: %v", err)
864
- }
865
-
866
- val, ok := result["sentimentValue"]
867
- if !ok {
868
- t.Fatal("expected 'sentimentValue' key")
869
- }
870
- if val != 3.5 {
871
- t.Errorf("expected 3.5, got %v", val)
872
- }
873
- }
874
-
875
- func TestParallelResultMerging_StringEnumField(t *testing.T) {
876
- enumSchema := &JSONSchema{
877
- Type: "string",
878
- Enum: []string{"positive", "negative", "neutral"},
879
- }
880
- wrappedSchema := wrapFieldSchema("sentiment", enumSchema)
881
-
882
- wrappedResponse := `{"sentiment": "positive"}`
883
- result, err := parseAndValidateJSON(wrappedResponse, wrappedSchema)
884
- if err != nil {
885
- t.Fatalf("wrapped enum response should parse: %v", err)
886
- }
887
-
888
- val, ok := result["sentiment"]
889
- if !ok {
890
- t.Fatal("expected 'sentiment' key")
891
- }
892
- if val != "positive" {
893
- t.Errorf("expected 'positive', got %v", val)
894
- }
895
- }