recursive-llm-ts 4.9.0 → 5.0.0

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,1407 @@
1
+ package rlm
2
+
3
+ import (
4
+ "encoding/json"
5
+ "fmt"
6
+ "os"
7
+ "path/filepath"
8
+ "strings"
9
+ "testing"
10
+ )
11
+
12
+ // ─── LCM Store Tests ────────────────────────────────────────────────────────
13
+
14
+ func TestLCMStore_NewStore(t *testing.T) {
15
+ store := NewLCMStore("test-session")
16
+ if store == nil {
17
+ t.Fatal("NewLCMStore returned nil")
18
+ }
19
+ if store.sessionID != "test-session" {
20
+ t.Errorf("sessionID = %s, want test-session", store.sessionID)
21
+ }
22
+ if store.MessageCount() != 0 {
23
+ t.Errorf("MessageCount = %d, want 0", store.MessageCount())
24
+ }
25
+ }
26
+
27
+ func TestLCMStore_PersistMessage(t *testing.T) {
28
+ store := NewLCMStore("test")
29
+
30
+ msg := store.PersistMessage(RoleUser, "Hello, world!", nil)
31
+ if msg == nil {
32
+ t.Fatal("PersistMessage returned nil")
33
+ }
34
+ if msg.Role != RoleUser {
35
+ t.Errorf("Role = %s, want user", msg.Role)
36
+ }
37
+ if msg.Content != "Hello, world!" {
38
+ t.Errorf("Content = %s, want 'Hello, world!'", msg.Content)
39
+ }
40
+ if msg.Tokens <= 0 {
41
+ t.Errorf("Tokens = %d, want > 0", msg.Tokens)
42
+ }
43
+ if store.MessageCount() != 1 {
44
+ t.Errorf("MessageCount = %d, want 1", store.MessageCount())
45
+ }
46
+ }
47
+
48
+ func TestLCMStore_PersistMessage_WithFileIDs(t *testing.T) {
49
+ store := NewLCMStore("test")
50
+ fileIDs := []string{"file_1", "file_2"}
51
+ msg := store.PersistMessage(RoleUser, "Check these files", fileIDs)
52
+ if len(msg.FileIDs) != 2 {
53
+ t.Errorf("FileIDs length = %d, want 2", len(msg.FileIDs))
54
+ }
55
+ }
56
+
57
+ func TestLCMStore_GetMessage(t *testing.T) {
58
+ store := NewLCMStore("test")
59
+ msg := store.PersistMessage(RoleUser, "test content", nil)
60
+
61
+ retrieved, ok := store.GetMessage(msg.ID)
62
+ if !ok {
63
+ t.Fatal("GetMessage returned false")
64
+ }
65
+ if retrieved.Content != "test content" {
66
+ t.Errorf("Content = %s, want 'test content'", retrieved.Content)
67
+ }
68
+
69
+ _, ok = store.GetMessage("nonexistent")
70
+ if ok {
71
+ t.Error("GetMessage returned true for nonexistent ID")
72
+ }
73
+ }
74
+
75
+ func TestLCMStore_GetAllMessages(t *testing.T) {
76
+ store := NewLCMStore("test")
77
+ store.PersistMessage(RoleUser, "first", nil)
78
+ store.PersistMessage(RoleAssistant, "second", nil)
79
+ store.PersistMessage(RoleUser, "third", nil)
80
+
81
+ msgs := store.GetAllMessages()
82
+ if len(msgs) != 3 {
83
+ t.Fatalf("GetAllMessages returned %d, want 3", len(msgs))
84
+ }
85
+ if msgs[0].Content != "first" {
86
+ t.Errorf("First message = %s, want 'first'", msgs[0].Content)
87
+ }
88
+ if msgs[2].Content != "third" {
89
+ t.Errorf("Third message = %s, want 'third'", msgs[2].Content)
90
+ }
91
+ }
92
+
93
+ func TestLCMStore_ImmutableStore(t *testing.T) {
94
+ // Verify that messages cannot be modified after persistence
95
+ store := NewLCMStore("test")
96
+ msg := store.PersistMessage(RoleUser, "original", nil)
97
+ originalID := msg.ID
98
+
99
+ // Persist more messages
100
+ store.PersistMessage(RoleAssistant, "reply", nil)
101
+
102
+ // Retrieve the original - should still be intact
103
+ retrieved, ok := store.GetMessage(originalID)
104
+ if !ok {
105
+ t.Fatal("Original message not found")
106
+ }
107
+ if retrieved.Content != "original" {
108
+ t.Errorf("Content changed from 'original' to '%s'", retrieved.Content)
109
+ }
110
+ }
111
+
112
+ // ─── Summary DAG Tests ──────────────────────────────────────────────────────
113
+
114
+ func TestLCMStore_CreateLeafSummary(t *testing.T) {
115
+ store := NewLCMStore("test")
116
+ msg1 := store.PersistMessage(RoleUser, "Hello", nil)
117
+ msg2 := store.PersistMessage(RoleAssistant, "Hi there", nil)
118
+
119
+ summary := store.CreateLeafSummary(
120
+ []string{msg1.ID, msg2.ID},
121
+ "User greeted, assistant replied",
122
+ 1,
123
+ )
124
+
125
+ if summary == nil {
126
+ t.Fatal("CreateLeafSummary returned nil")
127
+ }
128
+ if summary.Kind != SummaryLeaf {
129
+ t.Errorf("Kind = %s, want leaf", summary.Kind)
130
+ }
131
+ if summary.Level != 1 {
132
+ t.Errorf("Level = %d, want 1", summary.Level)
133
+ }
134
+ if len(summary.MessageIDs) != 2 {
135
+ t.Errorf("MessageIDs length = %d, want 2", len(summary.MessageIDs))
136
+ }
137
+ }
138
+
139
+ func TestLCMStore_CreateLeafSummary_FileIDPropagation(t *testing.T) {
140
+ store := NewLCMStore("test")
141
+ msg1 := store.PersistMessage(RoleUser, "File A", []string{"file_a"})
142
+ msg2 := store.PersistMessage(RoleUser, "File B", []string{"file_b"})
143
+
144
+ summary := store.CreateLeafSummary(
145
+ []string{msg1.ID, msg2.ID},
146
+ "Summary of files",
147
+ 1,
148
+ )
149
+
150
+ if len(summary.FileIDs) != 2 {
151
+ t.Errorf("FileIDs length = %d, want 2", len(summary.FileIDs))
152
+ }
153
+ }
154
+
155
+ func TestLCMStore_CreateCondensedSummary(t *testing.T) {
156
+ store := NewLCMStore("test")
157
+ msg1 := store.PersistMessage(RoleUser, "Hello", []string{"f1"})
158
+ msg2 := store.PersistMessage(RoleAssistant, "Hi", nil)
159
+ msg3 := store.PersistMessage(RoleUser, "More", []string{"f2"})
160
+
161
+ leaf1 := store.CreateLeafSummary([]string{msg1.ID, msg2.ID}, "Greeting exchange", 1)
162
+ leaf2 := store.CreateLeafSummary([]string{msg3.ID}, "Continuation", 1)
163
+
164
+ condensed := store.CreateCondensedSummary(
165
+ []string{leaf1.ID, leaf2.ID},
166
+ "Full conversation summary",
167
+ 1,
168
+ )
169
+
170
+ if condensed.Kind != SummaryCondensed {
171
+ t.Errorf("Kind = %s, want condensed", condensed.Kind)
172
+ }
173
+ if len(condensed.ChildIDs) != 2 {
174
+ t.Errorf("ChildIDs length = %d, want 2", len(condensed.ChildIDs))
175
+ }
176
+ // File IDs should propagate from children
177
+ if len(condensed.FileIDs) != 2 {
178
+ t.Errorf("FileIDs length = %d, want 2 (propagated from children)", len(condensed.FileIDs))
179
+ }
180
+ // Parent pointers should be set on children
181
+ updatedLeaf1, _ := store.GetSummary(leaf1.ID)
182
+ if updatedLeaf1.ParentID != condensed.ID {
183
+ t.Errorf("Leaf1 ParentID = %s, want %s", updatedLeaf1.ParentID, condensed.ID)
184
+ }
185
+ }
186
+
187
+ func TestLCMStore_ExpandSummary_Leaf(t *testing.T) {
188
+ store := NewLCMStore("test")
189
+ msg1 := store.PersistMessage(RoleUser, "Message 1", nil)
190
+ msg2 := store.PersistMessage(RoleAssistant, "Message 2", nil)
191
+
192
+ leaf := store.CreateLeafSummary([]string{msg1.ID, msg2.ID}, "Summary", 1)
193
+
194
+ expanded, err := store.ExpandSummary(leaf.ID)
195
+ if err != nil {
196
+ t.Fatalf("ExpandSummary error: %v", err)
197
+ }
198
+ if len(expanded) != 2 {
199
+ t.Fatalf("Expanded length = %d, want 2", len(expanded))
200
+ }
201
+ if expanded[0].Content != "Message 1" {
202
+ t.Errorf("First expanded = %s, want 'Message 1'", expanded[0].Content)
203
+ }
204
+ }
205
+
206
+ func TestLCMStore_ExpandSummary_Condensed(t *testing.T) {
207
+ store := NewLCMStore("test")
208
+ msg1 := store.PersistMessage(RoleUser, "A", nil)
209
+ msg2 := store.PersistMessage(RoleAssistant, "B", nil)
210
+ msg3 := store.PersistMessage(RoleUser, "C", nil)
211
+
212
+ leaf1 := store.CreateLeafSummary([]string{msg1.ID, msg2.ID}, "Summary AB", 1)
213
+ leaf2 := store.CreateLeafSummary([]string{msg3.ID}, "Summary C", 1)
214
+ condensed := store.CreateCondensedSummary([]string{leaf1.ID, leaf2.ID}, "All", 2)
215
+
216
+ expanded, err := store.ExpandSummary(condensed.ID)
217
+ if err != nil {
218
+ t.Fatalf("ExpandSummary error: %v", err)
219
+ }
220
+ if len(expanded) != 3 {
221
+ t.Fatalf("Expanded length = %d, want 3 (recursive)", len(expanded))
222
+ }
223
+ }
224
+
225
+ func TestLCMStore_ExpandSummary_NotFound(t *testing.T) {
226
+ store := NewLCMStore("test")
227
+ _, err := store.ExpandSummary("nonexistent")
228
+ if err == nil {
229
+ t.Error("Expected error for nonexistent summary")
230
+ }
231
+ }
232
+
233
+ // ─── Active Context Tests ───────────────────────────────────────────────────
234
+
235
+ func TestLCMStore_ActiveContext(t *testing.T) {
236
+ store := NewLCMStore("test")
237
+ store.PersistMessage(RoleSystem, "You are helpful", nil)
238
+ store.PersistMessage(RoleUser, "Hello", nil)
239
+
240
+ active := store.GetActiveContext()
241
+ if len(active) != 2 {
242
+ t.Fatalf("Active context length = %d, want 2", len(active))
243
+ }
244
+ if !active[0].IsMessage() {
245
+ t.Error("First item should be a message")
246
+ }
247
+ if active[0].Message.Role != RoleSystem {
248
+ t.Errorf("First item role = %s, want system", active[0].Message.Role)
249
+ }
250
+ }
251
+
252
+ func TestLCMStore_ActiveContextTokens(t *testing.T) {
253
+ store := NewLCMStore("test")
254
+ store.PersistMessage(RoleUser, "Hello world this is a test message", nil)
255
+ store.PersistMessage(RoleAssistant, "Yes it is indeed a test", nil)
256
+
257
+ tokens := store.ActiveContextTokens()
258
+ if tokens <= 0 {
259
+ t.Errorf("ActiveContextTokens = %d, want > 0", tokens)
260
+ }
261
+ }
262
+
263
+ func TestLCMStore_CompactOldestBlock(t *testing.T) {
264
+ store := NewLCMStore("test")
265
+ store.PersistMessage(RoleSystem, "System prompt", nil)
266
+ msg1 := store.PersistMessage(RoleUser, "First question", nil)
267
+ msg2 := store.PersistMessage(RoleAssistant, "First answer", nil)
268
+ store.PersistMessage(RoleUser, "Second question", nil)
269
+
270
+ // Create summary for msg1 and msg2
271
+ summary := store.CreateLeafSummary(
272
+ []string{msg1.ID, msg2.ID},
273
+ "Q&A about first topic",
274
+ 1,
275
+ )
276
+
277
+ removed := store.CompactOldestBlock(summary)
278
+ if len(removed) != 2 {
279
+ t.Errorf("Removed %d messages, want 2", len(removed))
280
+ }
281
+
282
+ active := store.GetActiveContext()
283
+ // Should now be: system prompt, summary, second question
284
+ if len(active) != 3 {
285
+ t.Fatalf("Active context length = %d, want 3", len(active))
286
+ }
287
+ if active[0].IsMessage() && active[0].Message.Role != RoleSystem {
288
+ t.Error("First item should be system prompt")
289
+ }
290
+ if active[1].IsMessage() {
291
+ t.Error("Second item should be summary, not message")
292
+ }
293
+ if active[1].Summary == nil || active[1].Summary.ID != summary.ID {
294
+ t.Error("Second item should be the compaction summary")
295
+ }
296
+ }
297
+
298
+ func TestLCMStore_BuildMessages(t *testing.T) {
299
+ store := NewLCMStore("test")
300
+ store.PersistMessage(RoleSystem, "System prompt", nil)
301
+ msg1 := store.PersistMessage(RoleUser, "Hello", nil)
302
+ msg2 := store.PersistMessage(RoleAssistant, "Hi", nil)
303
+
304
+ // Compact first two non-system messages
305
+ summary := store.CreateLeafSummary([]string{msg1.ID, msg2.ID}, "Greeting exchange", 1)
306
+ store.CompactOldestBlock(summary)
307
+
308
+ store.PersistMessage(RoleUser, "New question", nil)
309
+
310
+ msgs := store.BuildMessages()
311
+ if len(msgs) != 3 {
312
+ t.Fatalf("BuildMessages returned %d, want 3", len(msgs))
313
+ }
314
+ if msgs[0].Role != "system" {
315
+ t.Errorf("First role = %s, want system", msgs[0].Role)
316
+ }
317
+ // Second should be the summary
318
+ if !strings.Contains(msgs[1].Content, "Summary") {
319
+ t.Error("Second message should contain summary annotation")
320
+ }
321
+ if msgs[2].Content != "New question" {
322
+ t.Errorf("Third message = %s, want 'New question'", msgs[2].Content)
323
+ }
324
+ }
325
+
326
+ // ─── LCM Grep Tests ────────────────────────────────────────────────────────
327
+
328
+ func TestLCMStore_Grep(t *testing.T) {
329
+ store := NewLCMStore("test")
330
+ store.PersistMessage(RoleUser, "What is the weather today?", nil)
331
+ store.PersistMessage(RoleAssistant, "The weather is sunny with a high of 75°F", nil)
332
+ store.PersistMessage(RoleUser, "What about tomorrow?", nil)
333
+ store.PersistMessage(RoleAssistant, "Tomorrow will be rainy", nil)
334
+
335
+ results, err := store.Grep("weather", 10)
336
+ if err != nil {
337
+ t.Fatalf("Grep error: %v", err)
338
+ }
339
+ if len(results) != 2 {
340
+ t.Errorf("Grep returned %d results, want 2", len(results))
341
+ }
342
+ }
343
+
344
+ func TestLCMStore_Grep_WithSummary(t *testing.T) {
345
+ store := NewLCMStore("test")
346
+ msg1 := store.PersistMessage(RoleUser, "Error: disk full", nil)
347
+ store.PersistMessage(RoleAssistant, "Try cleaning up temp files", nil)
348
+
349
+ // Create summary covering msg1
350
+ store.CreateLeafSummary([]string{msg1.ID}, "Disk issue", 1)
351
+
352
+ results, err := store.Grep("Error", 10)
353
+ if err != nil {
354
+ t.Fatalf("Grep error: %v", err)
355
+ }
356
+ if len(results) != 1 {
357
+ t.Fatalf("Grep returned %d results, want 1", len(results))
358
+ }
359
+ if results[0].SummaryID == "" {
360
+ t.Error("Expected SummaryID to be set for message covered by summary")
361
+ }
362
+ }
363
+
364
+ func TestLCMStore_Grep_InvalidRegex(t *testing.T) {
365
+ store := NewLCMStore("test")
366
+ _, err := store.Grep("[invalid", 10)
367
+ if err == nil {
368
+ t.Error("Expected error for invalid regex")
369
+ }
370
+ }
371
+
372
+ func TestLCMStore_Grep_Pagination(t *testing.T) {
373
+ store := NewLCMStore("test")
374
+ for i := 0; i < 20; i++ {
375
+ store.PersistMessage(RoleUser, fmt.Sprintf("test message %d", i), nil)
376
+ }
377
+
378
+ results, err := store.Grep("test", 5)
379
+ if err != nil {
380
+ t.Fatalf("Grep error: %v", err)
381
+ }
382
+ if len(results) != 5 {
383
+ t.Errorf("Grep returned %d results, want 5 (paginated)", len(results))
384
+ }
385
+ }
386
+
387
+ // ─── LCM Describe Tests ────────────────────────────────────────────────────
388
+
389
+ func TestLCMStore_Describe_Message(t *testing.T) {
390
+ store := NewLCMStore("test")
391
+ msg := store.PersistMessage(RoleUser, "Hello", []string{"file_1"})
392
+
393
+ desc, err := store.Describe(msg.ID)
394
+ if err != nil {
395
+ t.Fatalf("Describe error: %v", err)
396
+ }
397
+ if desc.Type != "message" {
398
+ t.Errorf("Type = %s, want message", desc.Type)
399
+ }
400
+ if desc.Role != "user" {
401
+ t.Errorf("Role = %s, want user", desc.Role)
402
+ }
403
+ if len(desc.FileIDs) != 1 {
404
+ t.Errorf("FileIDs length = %d, want 1", len(desc.FileIDs))
405
+ }
406
+ }
407
+
408
+ func TestLCMStore_Describe_Summary(t *testing.T) {
409
+ store := NewLCMStore("test")
410
+ msg := store.PersistMessage(RoleUser, "Hello", nil)
411
+ summary := store.CreateLeafSummary([]string{msg.ID}, "Greeting", 2)
412
+
413
+ desc, err := store.Describe(summary.ID)
414
+ if err != nil {
415
+ t.Fatalf("Describe error: %v", err)
416
+ }
417
+ if desc.Type != "summary" {
418
+ t.Errorf("Type = %s, want summary", desc.Type)
419
+ }
420
+ if desc.Kind != "leaf" {
421
+ t.Errorf("Kind = %s, want leaf", desc.Kind)
422
+ }
423
+ if desc.Level != 2 {
424
+ t.Errorf("Level = %d, want 2", desc.Level)
425
+ }
426
+ if desc.Content != "Greeting" {
427
+ t.Errorf("Content = %s, want 'Greeting'", desc.Content)
428
+ }
429
+ }
430
+
431
+ func TestLCMStore_Describe_NotFound(t *testing.T) {
432
+ store := NewLCMStore("test")
433
+ _, err := store.Describe("nonexistent")
434
+ if err == nil {
435
+ t.Error("Expected error for nonexistent ID")
436
+ }
437
+ }
438
+
439
+ // ─── Store Stats Tests ──────────────────────────────────────────────────────
440
+
441
+ func TestLCMStore_Stats(t *testing.T) {
442
+ store := NewLCMStore("test")
443
+ store.PersistMessage(RoleUser, "Hello world", nil)
444
+ store.PersistMessage(RoleAssistant, "Hi there friend", nil)
445
+
446
+ stats := store.Stats()
447
+ if stats.TotalMessages != 2 {
448
+ t.Errorf("TotalMessages = %d, want 2", stats.TotalMessages)
449
+ }
450
+ if stats.TotalSummaries != 0 {
451
+ t.Errorf("TotalSummaries = %d, want 0", stats.TotalSummaries)
452
+ }
453
+ if stats.ActiveContextItems != 2 {
454
+ t.Errorf("ActiveContextItems = %d, want 2", stats.ActiveContextItems)
455
+ }
456
+ if stats.CompressionRatio == 0 {
457
+ t.Error("CompressionRatio should be > 0")
458
+ }
459
+ }
460
+
461
+ func TestLCMStore_Stats_WithCompaction(t *testing.T) {
462
+ store := NewLCMStore("test")
463
+ msg1 := store.PersistMessage(RoleUser, strings.Repeat("Hello world test message. ", 50), nil)
464
+ msg2 := store.PersistMessage(RoleAssistant, strings.Repeat("Reply content here. ", 50), nil)
465
+
466
+ summary := store.CreateLeafSummary([]string{msg1.ID, msg2.ID}, "Brief summary", 1)
467
+ store.CompactOldestBlock(summary)
468
+
469
+ stats := store.Stats()
470
+ if stats.TotalMessages != 2 {
471
+ t.Errorf("TotalMessages = %d, want 2 (immutable)", stats.TotalMessages)
472
+ }
473
+ if stats.TotalSummaries != 1 {
474
+ t.Errorf("TotalSummaries = %d, want 1", stats.TotalSummaries)
475
+ }
476
+ if stats.CompressionRatio >= 1.0 {
477
+ t.Errorf("CompressionRatio = %f, want < 1.0 (compression occurred)", stats.CompressionRatio)
478
+ }
479
+ }
480
+
481
+ // ─── Three-Level Summarization Tests ────────────────────────────────────────
482
+
483
+ func TestLCMSummarizer_DeterministicTruncate(t *testing.T) {
484
+ obs := NewNoopObserver()
485
+ summarizer := NewLCMSummarizer("test", "", "", 30, nil, obs)
486
+
487
+ // Test deterministic truncation directly
488
+ longInput := strings.Repeat("This is a test sentence. ", 100)
489
+ result := summarizer.deterministicTruncate(longInput, 50)
490
+
491
+ if result.Level != 5 {
492
+ t.Errorf("Level = %d, want 5", result.Level)
493
+ }
494
+ if result.Tokens > 200 {
495
+ t.Errorf("Tokens = %d, want <= 200 (truncated to ~50 target)", result.Tokens)
496
+ }
497
+ if !strings.Contains(result.Content, "truncated") {
498
+ t.Error("Truncated content should contain truncation marker")
499
+ }
500
+ }
501
+
502
+ func TestLCMSummarizer_ShortInput(t *testing.T) {
503
+ obs := NewNoopObserver()
504
+ summarizer := NewLCMSummarizer("test", "", "", 30, nil, obs)
505
+
506
+ result, err := summarizer.Summarize("Short text", 1000)
507
+ if err != nil {
508
+ t.Fatalf("Summarize error: %v", err)
509
+ }
510
+ if result.Level != 0 {
511
+ t.Errorf("Level = %d, want 0 (no summarization needed)", result.Level)
512
+ }
513
+ if result.Content != "Short text" {
514
+ t.Errorf("Content changed for short input")
515
+ }
516
+ }
517
+
518
+ func TestLCMSummarizer_SummarizeMessages(t *testing.T) {
519
+ obs := NewNoopObserver()
520
+ summarizer := NewLCMSummarizer("test", "", "", 30, nil, obs)
521
+
522
+ msgs := []*StoreMessage{
523
+ {Role: RoleUser, Content: "Hello"},
524
+ {Role: RoleAssistant, Content: "Hi there"},
525
+ }
526
+
527
+ // This will try LLM calls (which will fail without API key) and fall back to deterministic
528
+ result, err := summarizer.SummarizeMessages(msgs, 10)
529
+ if err != nil {
530
+ // Summarize should eventually succeed via Level 3 deterministic
531
+ t.Fatalf("SummarizeMessages error: %v", err)
532
+ }
533
+ if result.Content == "" {
534
+ t.Error("Result content should not be empty")
535
+ }
536
+ }
537
+
538
+ // ─── Context Control Loop Tests ─────────────────────────────────────────────
539
+
540
+ func TestLCMEngine_ZeroCostContinuity(t *testing.T) {
541
+ store := NewLCMStore("test")
542
+ obs := NewNoopObserver()
543
+ summarizer := NewLCMSummarizer("test", "", "", 30, nil, obs)
544
+
545
+ config := LCMConfig{
546
+ Enabled: true,
547
+ SoftThreshold: 10000,
548
+ HardThreshold: 20000,
549
+ CompactionBlockSize: 5,
550
+ SummaryTargetTokens: 200,
551
+ }
552
+
553
+ engine := NewLCMEngine(config, store, summarizer, obs, 128000)
554
+
555
+ // Add a small message — should be below soft threshold
556
+ store.PersistMessage(RoleUser, "Hello", nil)
557
+ err := engine.OnNewItem()
558
+ if err != nil {
559
+ t.Fatalf("OnNewItem error: %v", err)
560
+ }
561
+
562
+ // Active context should be unchanged (zero-cost continuity)
563
+ if store.ActiveContextTokens() > config.SoftThreshold {
564
+ t.Error("Small message should be below soft threshold")
565
+ }
566
+ }
567
+
568
+ func TestLCMEngine_Disabled(t *testing.T) {
569
+ store := NewLCMStore("test")
570
+ obs := NewNoopObserver()
571
+ summarizer := NewLCMSummarizer("test", "", "", 30, nil, obs)
572
+
573
+ config := LCMConfig{Enabled: false}
574
+ engine := NewLCMEngine(config, store, summarizer, obs, 128000)
575
+
576
+ store.PersistMessage(RoleUser, "Hello", nil)
577
+ err := engine.OnNewItem()
578
+ if err != nil {
579
+ t.Fatalf("OnNewItem error when disabled: %v", err)
580
+ }
581
+ }
582
+
583
+ func TestLCMEngine_DefaultThresholds(t *testing.T) {
584
+ store := NewLCMStore("test")
585
+ obs := NewNoopObserver()
586
+ summarizer := NewLCMSummarizer("test", "", "", 30, nil, obs)
587
+
588
+ config := LCMConfig{Enabled: true}
589
+ engine := NewLCMEngine(config, store, summarizer, obs, 100000)
590
+
591
+ if engine.config.SoftThreshold != 70000 {
592
+ t.Errorf("SoftThreshold = %d, want 70000 (70%% of 100000)", engine.config.SoftThreshold)
593
+ }
594
+ if engine.config.HardThreshold != 90000 {
595
+ t.Errorf("HardThreshold = %d, want 90000 (90%% of 100000)", engine.config.HardThreshold)
596
+ }
597
+ }
598
+
599
+ func TestLCMEngine_GetStore(t *testing.T) {
600
+ store := NewLCMStore("test")
601
+ obs := NewNoopObserver()
602
+ summarizer := NewLCMSummarizer("test", "", "", 30, nil, obs)
603
+ config := LCMConfig{Enabled: true}
604
+
605
+ engine := NewLCMEngine(config, store, summarizer, obs, 128000)
606
+ if engine.GetStore() != store {
607
+ t.Error("GetStore returned different store")
608
+ }
609
+ }
610
+
611
+ // ─── LLM-Map Tests ──────────────────────────────────────────────────────────
612
+
613
+ func TestLLMMap_ReadJSONL(t *testing.T) {
614
+ // Create temp JSONL file
615
+ tmpDir := t.TempDir()
616
+ inputPath := filepath.Join(tmpDir, "input.jsonl")
617
+
618
+ lines := []string{
619
+ `{"name": "Alice", "age": 30}`,
620
+ `{"name": "Bob", "age": 25}`,
621
+ `{"name": "Charlie", "age": 35}`,
622
+ }
623
+ err := os.WriteFile(inputPath, []byte(strings.Join(lines, "\n")), 0644)
624
+ if err != nil {
625
+ t.Fatal(err)
626
+ }
627
+
628
+ items, err := readJSONLFile(inputPath)
629
+ if err != nil {
630
+ t.Fatalf("readJSONLFile error: %v", err)
631
+ }
632
+ if len(items) != 3 {
633
+ t.Errorf("Read %d items, want 3", len(items))
634
+ }
635
+
636
+ var first map[string]interface{}
637
+ _ = json.Unmarshal(items[0], &first)
638
+ if first["name"] != "Alice" {
639
+ t.Errorf("First name = %v, want Alice", first["name"])
640
+ }
641
+ }
642
+
643
+ func TestLLMMap_WriteJSONL(t *testing.T) {
644
+ tmpDir := t.TempDir()
645
+ outputPath := filepath.Join(tmpDir, "output.jsonl")
646
+
647
+ results := []MapItemResult{
648
+ {Index: 0, Status: MapItemCompleted, Output: json.RawMessage(`{"result": "ok"}`)},
649
+ {Index: 1, Status: MapItemFailed, Error: "timeout"},
650
+ }
651
+
652
+ err := writeJSONLOutput(outputPath, results)
653
+ if err != nil {
654
+ t.Fatalf("writeJSONLOutput error: %v", err)
655
+ }
656
+
657
+ // Read back
658
+ data, err := os.ReadFile(outputPath)
659
+ if err != nil {
660
+ t.Fatal(err)
661
+ }
662
+ lines := strings.Split(strings.TrimSpace(string(data)), "\n")
663
+ if len(lines) != 2 {
664
+ t.Errorf("Output has %d lines, want 2", len(lines))
665
+ }
666
+ }
667
+
668
+ func TestLLMMap_ExtractJSON(t *testing.T) {
669
+ tests := []struct {
670
+ input string
671
+ wantNil bool
672
+ }{
673
+ {`{"key": "value"}`, false},
674
+ {`Some text {"key": "value"} more text`, false},
675
+ {"```json\n{\"key\": \"value\"}\n```", false},
676
+ {`[1, 2, 3]`, false},
677
+ {`no json here`, true},
678
+ {``, true},
679
+ }
680
+
681
+ for _, tt := range tests {
682
+ result := extractJSON(tt.input)
683
+ if tt.wantNil && result != nil {
684
+ t.Errorf("extractJSON(%q) = %s, want nil", tt.input, string(result))
685
+ }
686
+ if !tt.wantNil && result == nil {
687
+ t.Errorf("extractJSON(%q) = nil, want non-nil", tt.input)
688
+ }
689
+ }
690
+ }
691
+
692
+ func TestLLMMap_ValidateMapOutput(t *testing.T) {
693
+ schema := &JSONSchema{
694
+ Type: "object",
695
+ Properties: map[string]*JSONSchema{
696
+ "name": {Type: "string"},
697
+ "age": {Type: "number"},
698
+ },
699
+ Required: []string{"name"},
700
+ }
701
+
702
+ // Valid output
703
+ valid := json.RawMessage(`{"name": "Alice", "age": 30}`)
704
+ if err := validateMapOutput(valid, schema); err != "" {
705
+ t.Errorf("validateMapOutput valid = %s, want empty", err)
706
+ }
707
+
708
+ // Invalid JSON
709
+ invalid := json.RawMessage(`not json`)
710
+ if err := validateMapOutput(invalid, schema); err == "" {
711
+ t.Error("validateMapOutput invalid JSON should return error")
712
+ }
713
+ }
714
+
715
+ // ─── Large File Handling Tests ──────────────────────────────────────────────
716
+
717
+ func TestLCMFileHandler_SmallFile(t *testing.T) {
718
+ obs := NewNoopObserver()
719
+ handler := NewLCMFileHandler(DefaultLCMFileConfig(), "test", "", "", 30, nil, obs)
720
+
721
+ content := "Small file content"
722
+ result, ref, err := handler.ProcessFile("test.txt", content)
723
+ if err != nil {
724
+ t.Fatalf("ProcessFile error: %v", err)
725
+ }
726
+ if ref != nil {
727
+ t.Error("Small file should not create a file ref")
728
+ }
729
+ if result != content {
730
+ t.Errorf("Content should be unchanged for small files")
731
+ }
732
+ }
733
+
734
+ func TestLCMFileHandler_LargeFile(t *testing.T) {
735
+ obs := NewNoopObserver()
736
+ config := LCMFileConfig{TokenThreshold: 10} // Very low threshold for testing
737
+ handler := NewLCMFileHandler(config, "test", "", "", 30, nil, obs)
738
+
739
+ content := strings.Repeat("Line of content for testing. ", 100)
740
+ result, ref, err := handler.ProcessFile("test.txt", content)
741
+ if err != nil {
742
+ t.Fatalf("ProcessFile error: %v", err)
743
+ }
744
+ if ref == nil {
745
+ t.Fatal("Large file should create a file ref")
746
+ }
747
+ if ref.Path != "test.txt" {
748
+ t.Errorf("Path = %s, want test.txt", ref.Path)
749
+ }
750
+ if !strings.Contains(result, ref.ID) {
751
+ t.Error("Context reference should contain file ID")
752
+ }
753
+ }
754
+
755
+ func TestLCMFileHandler_GetFileRef(t *testing.T) {
756
+ obs := NewNoopObserver()
757
+ config := LCMFileConfig{TokenThreshold: 10}
758
+ handler := NewLCMFileHandler(config, "test", "", "", 30, nil, obs)
759
+
760
+ content := strings.Repeat("Content. ", 100)
761
+ _, ref, _ := handler.ProcessFile("test.txt", content)
762
+
763
+ retrieved, ok := handler.GetFileRef(ref.ID)
764
+ if !ok {
765
+ t.Fatal("GetFileRef returned false")
766
+ }
767
+ if retrieved.Path != "test.txt" {
768
+ t.Errorf("Path = %s, want test.txt", retrieved.Path)
769
+ }
770
+ }
771
+
772
+ func TestClassifyFileType(t *testing.T) {
773
+ tests := []struct {
774
+ ext string
775
+ want fileType
776
+ }{
777
+ {".json", fileTypeStructuredData},
778
+ {".csv", fileTypeStructuredData},
779
+ {".go", fileTypeCode},
780
+ {".py", fileTypeCode},
781
+ {".ts", fileTypeCode},
782
+ {".txt", fileTypeText},
783
+ {".md", fileTypeText},
784
+ {".unknown", fileTypeText},
785
+ }
786
+
787
+ for _, tt := range tests {
788
+ got := classifyFileType(tt.ext)
789
+ if got != tt.want {
790
+ t.Errorf("classifyFileType(%s) = %d, want %d", tt.ext, got, tt.want)
791
+ }
792
+ }
793
+ }
794
+
795
+ func TestDetectMIMEType(t *testing.T) {
796
+ tests := []struct {
797
+ path string
798
+ want string
799
+ }{
800
+ {"file.json", "application/json"},
801
+ {"file.go", "text/x-go"},
802
+ {"file.py", "text/x-python"},
803
+ {"file.unknown", "application/octet-stream"},
804
+ }
805
+
806
+ for _, tt := range tests {
807
+ got := detectMIMEType(tt.path)
808
+ if got != tt.want {
809
+ t.Errorf("detectMIMEType(%s) = %s, want %s", tt.path, got, tt.want)
810
+ }
811
+ }
812
+ }
813
+
814
+ func TestSummarizeJSON(t *testing.T) {
815
+ obs := NewNoopObserver()
816
+ handler := NewLCMFileHandler(DefaultLCMFileConfig(), "test", "", "", 30, nil, obs)
817
+
818
+ jsonContent := `{"users": [{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}], "count": 2}`
819
+ result := handler.summarizeJSON(jsonContent)
820
+ if !strings.Contains(result, "JSON file analysis") {
821
+ t.Error("JSON summary should contain analysis header")
822
+ }
823
+ }
824
+
825
+ func TestSummarizeCSV(t *testing.T) {
826
+ obs := NewNoopObserver()
827
+ handler := NewLCMFileHandler(DefaultLCMFileConfig(), "test", "", "", 30, nil, obs)
828
+
829
+ csvContent := "name,age,city\nAlice,30,NYC\nBob,25,LA"
830
+ result := handler.summarizeCSV(csvContent, ".csv")
831
+ if !strings.Contains(result, "3 rows") {
832
+ t.Error("CSV summary should contain row count")
833
+ }
834
+ if !strings.Contains(result, "Columns") {
835
+ t.Error("CSV summary should contain column names")
836
+ }
837
+ }
838
+
839
+ func TestExtractCodeStructure(t *testing.T) {
840
+ goCode := `package main
841
+
842
+ import "fmt"
843
+
844
+ func main() {
845
+ fmt.Println("Hello")
846
+ }
847
+
848
+ func helper(x int) int {
849
+ return x * 2
850
+ }
851
+
852
+ type Config struct {
853
+ Name string
854
+ }
855
+ `
856
+ result := extractCodeStructure(goCode, ".go")
857
+ if !strings.Contains(result, "func main") {
858
+ t.Error("Should extract func main definition")
859
+ }
860
+ if !strings.Contains(result, "func helper") {
861
+ t.Error("Should extract func helper definition")
862
+ }
863
+ if !strings.Contains(result, "type Config") {
864
+ t.Error("Should extract type Config definition")
865
+ }
866
+ }
867
+
868
+ func TestSummarizeJSONL(t *testing.T) {
869
+ obs := NewNoopObserver()
870
+ handler := NewLCMFileHandler(DefaultLCMFileConfig(), "test", "", "", 30, nil, obs)
871
+
872
+ jsonlContent := `{"id": 1, "name": "Alice"}
873
+ {"id": 2, "name": "Bob"}
874
+ {"id": 3, "name": "Charlie"}`
875
+ result := handler.summarizeJSONL(jsonlContent)
876
+ if !strings.Contains(result, "3 lines") {
877
+ t.Error("JSONL summary should contain line count")
878
+ }
879
+ }
880
+
881
+ // ─── Config Parsing Tests ───────────────────────────────────────────────────
882
+
883
+ func TestConfigFromMap_LCM(t *testing.T) {
884
+ configMap := map[string]interface{}{
885
+ "lcm": map[string]interface{}{
886
+ "enabled": true,
887
+ "soft_threshold": 50000.0,
888
+ "hard_threshold": 80000.0,
889
+ "compaction_block_size": 8.0,
890
+ "summary_target_tokens": 300.0,
891
+ },
892
+ }
893
+
894
+ config := ConfigFromMap(configMap)
895
+ if config.LCM == nil {
896
+ t.Fatal("LCM config should not be nil")
897
+ }
898
+ if !config.LCM.Enabled {
899
+ t.Error("LCM should be enabled")
900
+ }
901
+ if config.LCM.SoftThreshold != 50000 {
902
+ t.Errorf("SoftThreshold = %d, want 50000", config.LCM.SoftThreshold)
903
+ }
904
+ if config.LCM.HardThreshold != 80000 {
905
+ t.Errorf("HardThreshold = %d, want 80000", config.LCM.HardThreshold)
906
+ }
907
+ if config.LCM.CompactionBlockSize != 8 {
908
+ t.Errorf("CompactionBlockSize = %d, want 8", config.LCM.CompactionBlockSize)
909
+ }
910
+ if config.LCM.SummaryTargetTokens != 300 {
911
+ t.Errorf("SummaryTargetTokens = %d, want 300", config.LCM.SummaryTargetTokens)
912
+ }
913
+ }
914
+
915
+ func TestConfigFromMap_LCM_NotPresent(t *testing.T) {
916
+ configMap := map[string]interface{}{
917
+ "api_key": "test-key",
918
+ }
919
+
920
+ config := ConfigFromMap(configMap)
921
+ if config.LCM != nil {
922
+ t.Error("LCM config should be nil when not present")
923
+ }
924
+ }
925
+
926
+ // ─── Integration: ActiveContextItem Tests ───────────────────────────────────
927
+
928
+ func TestActiveContextItem_GetTokens(t *testing.T) {
929
+ msg := &StoreMessage{Content: "test", Tokens: 10}
930
+ item := &ActiveContextItem{Message: msg}
931
+ if item.GetTokens() != 10 {
932
+ t.Errorf("GetTokens = %d, want 10", item.GetTokens())
933
+ }
934
+
935
+ sum := &SummaryNode{Content: "summary", Tokens: 5}
936
+ sumItem := &ActiveContextItem{Summary: sum}
937
+ if sumItem.GetTokens() != 5 {
938
+ t.Errorf("GetTokens = %d, want 5", sumItem.GetTokens())
939
+ }
940
+
941
+ empty := &ActiveContextItem{}
942
+ if empty.GetTokens() != 0 {
943
+ t.Errorf("GetTokens = %d, want 0", empty.GetTokens())
944
+ }
945
+ }
946
+
947
+ func TestActiveContextItem_GetContent(t *testing.T) {
948
+ msg := &StoreMessage{Content: "hello"}
949
+ item := &ActiveContextItem{Message: msg}
950
+ if item.GetContent() != "hello" {
951
+ t.Errorf("GetContent = %s, want 'hello'", item.GetContent())
952
+ }
953
+
954
+ sum := &SummaryNode{Content: "summary text"}
955
+ sumItem := &ActiveContextItem{Summary: sum}
956
+ if sumItem.GetContent() != "summary text" {
957
+ t.Errorf("GetContent = %s, want 'summary text'", sumItem.GetContent())
958
+ }
959
+ }
960
+
961
+ func TestActiveContextItem_GetFileIDs(t *testing.T) {
962
+ msg := &StoreMessage{FileIDs: []string{"f1", "f2"}}
963
+ item := &ActiveContextItem{Message: msg}
964
+ if len(item.GetFileIDs()) != 2 {
965
+ t.Errorf("GetFileIDs length = %d, want 2", len(item.GetFileIDs()))
966
+ }
967
+
968
+ sum := &SummaryNode{FileIDs: []string{"f3"}}
969
+ sumItem := &ActiveContextItem{Summary: sum}
970
+ if len(sumItem.GetFileIDs()) != 1 {
971
+ t.Errorf("GetFileIDs length = %d, want 1", len(sumItem.GetFileIDs()))
972
+ }
973
+ }
974
+
975
+ func TestIsDefinitionLine(t *testing.T) {
976
+ tests := []struct {
977
+ line string
978
+ ext string
979
+ want bool
980
+ }{
981
+ {"func main() {", ".go", true},
982
+ {"type Config struct {", ".go", true},
983
+ {"def hello():", ".py", true},
984
+ {"class MyClass:", ".py", true},
985
+ {"export function foo() {", ".ts", true},
986
+ {"interface Props {", ".ts", true},
987
+ {"fn main() {", ".rs", true},
988
+ {"// just a comment", ".go", false},
989
+ {"x := 5", ".go", false},
990
+ }
991
+
992
+ for _, tt := range tests {
993
+ got := isDefinitionLine(tt.line, tt.ext)
994
+ if got != tt.want {
995
+ t.Errorf("isDefinitionLine(%q, %q) = %v, want %v", tt.line, tt.ext, got, tt.want)
996
+ }
997
+ }
998
+ }
999
+
1000
+ func TestIsImportLine(t *testing.T) {
1001
+ tests := []struct {
1002
+ line string
1003
+ ext string
1004
+ want bool
1005
+ }{
1006
+ {`import "fmt"`, ".go", true},
1007
+ {`import os`, ".py", true},
1008
+ {`from datetime import date`, ".py", true},
1009
+ {`import { useState } from 'react'`, ".ts", true},
1010
+ {`use std::io`, ".rs", true},
1011
+ {`// import comment`, ".go", false},
1012
+ }
1013
+
1014
+ for _, tt := range tests {
1015
+ got := isImportLine(tt.line, tt.ext)
1016
+ if got != tt.want {
1017
+ t.Errorf("isImportLine(%q, %q) = %v, want %v", tt.line, tt.ext, got, tt.want)
1018
+ }
1019
+ }
1020
+ }
1021
+
1022
+ // ─── Delegation Guard Tests ─────────────────────────────────────────────────
1023
+
1024
+ func TestDelegationGuard_RootAgentAlwaysAllowed(t *testing.T) {
1025
+ obs := NewNoopObserver()
1026
+ guard := NewDelegationGuard(obs)
1027
+
1028
+ err := guard.ValidateDelegation(0, DelegationRequest{
1029
+ Prompt: "Do all the work",
1030
+ // No scope or kept_work needed for root
1031
+ })
1032
+ if err != nil {
1033
+ t.Errorf("Root agent should always be allowed, got: %v", err)
1034
+ }
1035
+ }
1036
+
1037
+ func TestDelegationGuard_ReadOnlyExempt(t *testing.T) {
1038
+ obs := NewNoopObserver()
1039
+ guard := NewDelegationGuard(obs)
1040
+
1041
+ err := guard.ValidateDelegation(2, DelegationRequest{
1042
+ Prompt: "Read some files",
1043
+ ReadOnly: true,
1044
+ })
1045
+ if err != nil {
1046
+ t.Errorf("Read-only agents should be exempt, got: %v", err)
1047
+ }
1048
+ }
1049
+
1050
+ func TestDelegationGuard_ParallelExempt(t *testing.T) {
1051
+ obs := NewNoopObserver()
1052
+ guard := NewDelegationGuard(obs)
1053
+
1054
+ err := guard.ValidateDelegation(1, DelegationRequest{
1055
+ Prompt: "Process chunk A",
1056
+ Parallel: true,
1057
+ })
1058
+ if err != nil {
1059
+ t.Errorf("Parallel decomposition should be exempt, got: %v", err)
1060
+ }
1061
+ }
1062
+
1063
+ func TestDelegationGuard_MissingDelegatedScope(t *testing.T) {
1064
+ obs := NewNoopObserver()
1065
+ guard := NewDelegationGuard(obs)
1066
+
1067
+ err := guard.ValidateDelegation(1, DelegationRequest{
1068
+ Prompt: "Do something",
1069
+ KeptWork: "I'll aggregate",
1070
+ })
1071
+ if err == nil {
1072
+ t.Error("Should reject when delegated_scope is missing")
1073
+ }
1074
+ delegErr, ok := err.(*DelegationError)
1075
+ if !ok {
1076
+ t.Fatalf("Expected DelegationError, got %T", err)
1077
+ }
1078
+ if delegErr.Reason == "" {
1079
+ t.Error("DelegationError should have a reason")
1080
+ }
1081
+ }
1082
+
1083
+ func TestDelegationGuard_MissingKeptWork(t *testing.T) {
1084
+ obs := NewNoopObserver()
1085
+ guard := NewDelegationGuard(obs)
1086
+
1087
+ err := guard.ValidateDelegation(1, DelegationRequest{
1088
+ Prompt: "Do something",
1089
+ DelegatedScope: "Parse the files",
1090
+ })
1091
+ if err == nil {
1092
+ t.Error("Should reject when kept_work is missing")
1093
+ }
1094
+ }
1095
+
1096
+ func TestDelegationGuard_TotalDelegation(t *testing.T) {
1097
+ obs := NewNoopObserver()
1098
+ guard := NewDelegationGuard(obs)
1099
+
1100
+ err := guard.ValidateDelegation(1, DelegationRequest{
1101
+ Prompt: "Implement the entire feature including tests and documentation",
1102
+ DelegatedScope: "Implement the entire feature including tests and documentation",
1103
+ KeptWork: "none",
1104
+ })
1105
+ if err == nil {
1106
+ t.Error("Should reject total delegation (kept_work = 'none')")
1107
+ }
1108
+ }
1109
+
1110
+ func TestDelegationGuard_TotalDelegation_TrivialKeptWork(t *testing.T) {
1111
+ obs := NewNoopObserver()
1112
+ guard := NewDelegationGuard(obs)
1113
+
1114
+ trivialKeptWorks := []string{"nothing", "n/a", "waiting", "collect results", "return results"}
1115
+ for _, kw := range trivialKeptWorks {
1116
+ err := guard.ValidateDelegation(1, DelegationRequest{
1117
+ Prompt: "Do all the work",
1118
+ DelegatedScope: "Implement everything from scratch with full testing",
1119
+ KeptWork: kw,
1120
+ })
1121
+ if err == nil {
1122
+ t.Errorf("Should reject trivial kept_work %q", kw)
1123
+ }
1124
+ }
1125
+ }
1126
+
1127
+ func TestDelegationGuard_ValidDelegation(t *testing.T) {
1128
+ obs := NewNoopObserver()
1129
+ guard := NewDelegationGuard(obs)
1130
+
1131
+ err := guard.ValidateDelegation(1, DelegationRequest{
1132
+ Prompt: "Parse the configuration files and extract database settings",
1133
+ DelegatedScope: "Parse configuration files in the config/ directory",
1134
+ KeptWork: "Validate extracted settings against the schema and apply migrations",
1135
+ })
1136
+ if err != nil {
1137
+ t.Errorf("Valid delegation should be allowed, got: %v", err)
1138
+ }
1139
+ }
1140
+
1141
+ func TestDelegationGuard_TotalDelegation_ShortKeptWork(t *testing.T) {
1142
+ obs := NewNoopObserver()
1143
+ guard := NewDelegationGuard(obs)
1144
+
1145
+ err := guard.ValidateDelegation(1, DelegationRequest{
1146
+ Prompt: "Implement the full authentication system with OAuth2, JWT tokens, refresh tokens, and rate limiting",
1147
+ DelegatedScope: "Implement the full authentication system with OAuth2, JWT tokens, refresh tokens, and rate limiting",
1148
+ KeptWork: "ok",
1149
+ })
1150
+ if err == nil {
1151
+ t.Error("Should reject when kept_work is suspiciously short compared to scope")
1152
+ }
1153
+ }
1154
+
1155
+ func TestIsTotalDelegation(t *testing.T) {
1156
+ tests := []struct {
1157
+ scope string
1158
+ kept string
1159
+ want bool
1160
+ }{
1161
+ {"big task", "none", true},
1162
+ {"big task", "nothing", true},
1163
+ {"big task", "n/a", true},
1164
+ {"big task", "waiting", true},
1165
+ {"big task", "I will validate the output and merge the results", false},
1166
+ {"parse files", "aggregate and report", false},
1167
+ {"long scope description here with many details", "ok", true},
1168
+ }
1169
+
1170
+ for _, tt := range tests {
1171
+ got := isTotalDelegation(tt.scope, tt.kept)
1172
+ if got != tt.want {
1173
+ t.Errorf("isTotalDelegation(%q, %q) = %v, want %v", tt.scope, tt.kept, got, tt.want)
1174
+ }
1175
+ }
1176
+ }
1177
+
1178
+ // ─── ExpandSummary Restriction Tests ────────────────────────────────────────
1179
+
1180
+ func TestExpandSummaryRestricted_RootAgentBlocked(t *testing.T) {
1181
+ store := NewLCMStore("test")
1182
+ msg := store.PersistMessage(RoleUser, "Hello", nil)
1183
+ summary := store.CreateLeafSummary([]string{msg.ID}, "Greeting", 1)
1184
+
1185
+ _, err := store.ExpandSummaryRestricted(summary.ID, 0)
1186
+ if err == nil {
1187
+ t.Error("Root agent (depth 0) should be blocked from expanding summaries")
1188
+ }
1189
+ expandErr, ok := err.(*ExpandRestrictionError)
1190
+ if !ok {
1191
+ t.Fatalf("Expected ExpandRestrictionError, got %T", err)
1192
+ }
1193
+ if expandErr.SummaryID != summary.ID {
1194
+ t.Errorf("SummaryID = %s, want %s", expandErr.SummaryID, summary.ID)
1195
+ }
1196
+ }
1197
+
1198
+ func TestExpandSummaryRestricted_SubAgentAllowed(t *testing.T) {
1199
+ store := NewLCMStore("test")
1200
+ msg := store.PersistMessage(RoleUser, "Hello", nil)
1201
+ summary := store.CreateLeafSummary([]string{msg.ID}, "Greeting", 1)
1202
+
1203
+ msgs, err := store.ExpandSummaryRestricted(summary.ID, 1)
1204
+ if err != nil {
1205
+ t.Fatalf("Sub-agent (depth 1) should be allowed, got: %v", err)
1206
+ }
1207
+ if len(msgs) != 1 {
1208
+ t.Errorf("Expected 1 expanded message, got %d", len(msgs))
1209
+ }
1210
+ }
1211
+
1212
+ func TestExpandSummaryRestricted_DeepSubAgentAllowed(t *testing.T) {
1213
+ store := NewLCMStore("test")
1214
+ msg := store.PersistMessage(RoleUser, "Hello", nil)
1215
+ summary := store.CreateLeafSummary([]string{msg.ID}, "Greeting", 1)
1216
+
1217
+ _, err := store.ExpandSummaryRestricted(summary.ID, 5)
1218
+ if err != nil {
1219
+ t.Errorf("Deep sub-agent (depth 5) should be allowed, got: %v", err)
1220
+ }
1221
+ }
1222
+
1223
+ // ─── LCM Completion Integration Tests ───────────────────────────────────────
1224
+
1225
+ func TestLCMEngine_CompletionIntegration_StorePopulated(t *testing.T) {
1226
+ // Test that the LCM engine populates the store when used in completion
1227
+ config := Config{
1228
+ APIKey: "test-key",
1229
+ MaxDepth: 3,
1230
+ MaxIterations: 5,
1231
+ LCM: &LCMConfig{
1232
+ Enabled: true,
1233
+ SoftThreshold: 100000,
1234
+ HardThreshold: 200000,
1235
+ CompactionBlockSize: 10,
1236
+ SummaryTargetTokens: 500,
1237
+ },
1238
+ }
1239
+
1240
+ engine := New("test-model", config)
1241
+ defer engine.Shutdown()
1242
+
1243
+ // Verify LCM engine is set up
1244
+ if engine.GetLCMEngine() == nil {
1245
+ t.Fatal("LCM engine should be initialized when config.LCM.Enabled = true")
1246
+ }
1247
+ if !engine.GetLCMEngine().IsEnabled() {
1248
+ t.Error("LCM engine should be enabled")
1249
+ }
1250
+
1251
+ store := engine.GetLCMEngine().GetStore()
1252
+ if store.MessageCount() != 0 {
1253
+ t.Errorf("Store should be empty initially, got %d messages", store.MessageCount())
1254
+ }
1255
+ }
1256
+
1257
+ func TestLCMEngine_NotInitializedWhenDisabled(t *testing.T) {
1258
+ config := Config{
1259
+ APIKey: "test-key",
1260
+ MaxDepth: 3,
1261
+ MaxIterations: 5,
1262
+ // No LCM config
1263
+ }
1264
+
1265
+ engine := New("test-model", config)
1266
+ defer engine.Shutdown()
1267
+
1268
+ if engine.GetLCMEngine() != nil {
1269
+ t.Error("LCM engine should be nil when not configured")
1270
+ }
1271
+ }
1272
+
1273
+ func TestLCMEngine_NotInitializedWhenExplicitlyDisabled(t *testing.T) {
1274
+ config := Config{
1275
+ APIKey: "test-key",
1276
+ MaxDepth: 3,
1277
+ MaxIterations: 5,
1278
+ LCM: &LCMConfig{Enabled: false},
1279
+ }
1280
+
1281
+ engine := New("test-model", config)
1282
+ defer engine.Shutdown()
1283
+
1284
+ if engine.GetLCMEngine() != nil {
1285
+ t.Error("LCM engine should be nil when explicitly disabled")
1286
+ }
1287
+ }
1288
+
1289
+ // ─── Agentic-Map Tests ──────────────────────────────────────────────────────
1290
+
1291
+ func TestAgenticMapper_DefaultConfig(t *testing.T) {
1292
+ config := AgenticMapConfig{
1293
+ InputPath: "/tmp/input.jsonl",
1294
+ OutputPath: "/tmp/output.jsonl",
1295
+ Prompt: "Process {{item}}",
1296
+ }
1297
+
1298
+ // Verify defaults are applied
1299
+ if config.Concurrency <= 0 {
1300
+ config.Concurrency = 8
1301
+ }
1302
+ if config.MaxRetries <= 0 {
1303
+ config.MaxRetries = 2
1304
+ }
1305
+ if config.MaxDepth <= 0 {
1306
+ config.MaxDepth = 3
1307
+ }
1308
+ if config.MaxIter <= 0 {
1309
+ config.MaxIter = 15
1310
+ }
1311
+
1312
+ if config.Concurrency != 8 {
1313
+ t.Errorf("Default concurrency = %d, want 8", config.Concurrency)
1314
+ }
1315
+ if config.MaxRetries != 2 {
1316
+ t.Errorf("Default MaxRetries = %d, want 2", config.MaxRetries)
1317
+ }
1318
+ if config.MaxDepth != 3 {
1319
+ t.Errorf("Default MaxDepth = %d, want 3", config.MaxDepth)
1320
+ }
1321
+ }
1322
+
1323
+ func TestAgenticMapper_WriteOutput(t *testing.T) {
1324
+ tmpDir := t.TempDir()
1325
+ outputPath := filepath.Join(tmpDir, "agentic_output.jsonl")
1326
+
1327
+ results := []AgenticItemResult{
1328
+ {Index: 0, Status: MapItemCompleted, Output: json.RawMessage(`{"result": "ok"}`), LLMCalls: 3, Iterations: 2},
1329
+ {Index: 1, Status: MapItemFailed, Error: "sub-agent timed out", LLMCalls: 5, Iterations: 5},
1330
+ }
1331
+
1332
+ err := writeAgenticOutput(outputPath, results)
1333
+ if err != nil {
1334
+ t.Fatalf("writeAgenticOutput error: %v", err)
1335
+ }
1336
+
1337
+ data, err := os.ReadFile(outputPath)
1338
+ if err != nil {
1339
+ t.Fatal(err)
1340
+ }
1341
+ lines := strings.Split(strings.TrimSpace(string(data)), "\n")
1342
+ if len(lines) != 2 {
1343
+ t.Errorf("Output has %d lines, want 2", len(lines))
1344
+ }
1345
+
1346
+ // Verify error record includes metadata
1347
+ var errRecord map[string]interface{}
1348
+ _ = json.Unmarshal([]byte(lines[1]), &errRecord)
1349
+ if errRecord["_error"] != "sub-agent timed out" {
1350
+ t.Errorf("Error record missing error, got: %v", errRecord)
1351
+ }
1352
+ if errRecord["_llm_calls"] != float64(5) {
1353
+ t.Errorf("Error record missing llm_calls, got: %v", errRecord["_llm_calls"])
1354
+ }
1355
+ }
1356
+
1357
+ // ─── Balanced Brace Extraction Tests ────────────────────────────────────────
1358
+
1359
+ func TestExtractBalancedBraces(t *testing.T) {
1360
+ tests := []struct {
1361
+ input string
1362
+ startChar byte
1363
+ want string
1364
+ }{
1365
+ {`{"key": "value"}`, '{', `{"key": "value"}`},
1366
+ {`{"key": "value"} extra`, '{', `{"key": "value"}`},
1367
+ {`{"nested": {"inner": true}}`, '{', `{"nested": {"inner": true}}`},
1368
+ {`[1, 2, 3]`, '[', `[1, 2, 3]`},
1369
+ {`[1, [2, 3], 4] trailing`, '[', `[1, [2, 3], 4]`},
1370
+ {`{"str": "has \" escaped"}`, '{', `{"str": "has \" escaped"}`},
1371
+ {`{unclosed`, '{', ``},
1372
+ }
1373
+
1374
+ for _, tt := range tests {
1375
+ got := ExtractBalancedBraces(tt.input, tt.startChar)
1376
+ if got != tt.want {
1377
+ t.Errorf("ExtractBalancedBraces(%q, %q) = %q, want %q", tt.input, string(tt.startChar), got, tt.want)
1378
+ }
1379
+ }
1380
+ }
1381
+
1382
+ // ─── DelegationError Tests ──────────────────────────────────────────────────
1383
+
1384
+ func TestDelegationError_ErrorMessage(t *testing.T) {
1385
+ err := &DelegationError{
1386
+ Reason: "test reason",
1387
+ Suggestion: "try something else",
1388
+ }
1389
+ msg := err.Error()
1390
+ if !strings.Contains(msg, "test reason") {
1391
+ t.Error("Error message should contain reason")
1392
+ }
1393
+ if !strings.Contains(msg, "try something else") {
1394
+ t.Error("Error message should contain suggestion")
1395
+ }
1396
+ }
1397
+
1398
+ func TestExpandRestrictionError_ErrorMessage(t *testing.T) {
1399
+ err := &ExpandRestrictionError{SummaryID: "sum_123"}
1400
+ msg := err.Error()
1401
+ if !strings.Contains(msg, "sum_123") {
1402
+ t.Error("Error message should contain summary ID")
1403
+ }
1404
+ if !strings.Contains(msg, "sub-agent") {
1405
+ t.Error("Error message should mention sub-agents")
1406
+ }
1407
+ }