1package core
2
3import (
4 "encoding/json"
5 "os"
6 "path/filepath"
7 "strings"
8 "testing"
9
10 "github.com/bnema/zut/packages/provider"
11)
12
13// TestSessionExportImportRoundTrip writes a few messages to a live
14// session, exports it, imports the export under a different cwd,
15// and verifies OpenSession on the imported file yields the same
16// message payloads.
17func TestSessionExportImportRoundTrip(t *testing.T) {
18 root := t.TempDir()
19 originalCWD := "/path/to/project"
20 sess, err := NewSession(root, originalCWD, "anthropic", "claude-opus-4-7", "0.0.0-test")
21 if err != nil {
22 t.Fatal(err)
23 }
24 _ = sess.AppendMessage(provider.Message{
25 Role: provider.RoleUser,
26 Content: []provider.Content{provider.TextBlock{Text: "hello from the exporter"}},
27 })
28 _ = sess.AppendMessage(provider.Message{
29 Role: provider.RoleAssistant,
30 Content: []provider.Content{provider.TextBlock{Text: "hi — reply from the assistant"}},
31 })
32 _ = sess.Close()
33
34 // Export to a directory — helper should build a name inside it.
35 exportDir := t.TempDir()
36 exportPath, err := ExportSession(sess.Path, exportDir)
37 if err != nil {
38 t.Fatalf("ExportSession: %v", err)
39 }
40 if !strings.HasSuffix(exportPath, PortableExt) {
41 t.Errorf("exported path should end in %s, got %q", PortableExt, exportPath)
42 }
43 if _, err := os.Stat(exportPath); err != nil {
44 t.Fatalf("exported file doesn't exist: %v", err)
45 }
46
47 // Import into a different root + cwd.
48 root2 := t.TempDir()
49 cwd2 := "/some/other/project"
50 importedPath, err := ImportSession(exportPath, root2, cwd2, "0.0.0-test")
51 if err != nil {
52 t.Fatalf("ImportSession: %v", err)
53 }
54 if filepath.Dir(importedPath) != SessionsDir(root2, cwd2) {
55 t.Errorf("imported file should land in SessionsDir, got %q", importedPath)
56 }
57
58 // Reopen and verify message round-trip.
59 imported, msgs, err := OpenSession(importedPath)
60 if err != nil {
61 t.Fatalf("OpenSession: %v", err)
62 }
63 defer imported.Close()
64 if imported.Meta.CWD != cwd2 {
65 t.Errorf("meta cwd: want %q, got %q", cwd2, imported.Meta.CWD)
66 }
67 if imported.Meta.ID == sess.ID {
68 t.Errorf("imported session kept the original id %q; must be rotated", sess.ID)
69 }
70 if imported.Meta.Model != "claude-opus-4-7" {
71 t.Errorf("model not preserved: %q", imported.Meta.Model)
72 }
73 if len(msgs) != 2 {
74 t.Fatalf("want 2 messages, got %d", len(msgs))
75 }
76 // Text should round-trip.
77 if extractText(msgs[0]) != "hello from the exporter" {
78 t.Errorf("msg 0 mismatch: %q", extractText(msgs[0]))
79 }
80 if extractText(msgs[1]) != "hi — reply from the assistant" {
81 t.Errorf("msg 1 mismatch: %q", extractText(msgs[1]))
82 }
83}
84
85func TestExportImportUsesLatestProviderModelMetadata(t *testing.T) {
86 root := t.TempDir()
87 session, err := NewSession(root, "/source", "old-provider", "old-model", "test")
88 if err != nil {
89 t.Fatal(err)
90 }
91 _ = session.AppendMessage(provider.Message{
92 Role: provider.RoleUser,
93 Content: []provider.Content{provider.TextBlock{Text: "metadata prompt"}},
94 })
95 if err := session.UpdateModel("new-provider", "new-model"); err != nil {
96 t.Fatal(err)
97 }
98 path := session.Path
99 if err := session.Close(); err != nil {
100 t.Fatal(err)
101 }
102
103 exportPath, err := ExportSession(path, filepath.Join(t.TempDir(), "latest"+PortableExt))
104 if err != nil {
105 t.Fatalf("ExportSession: %v", err)
106 }
107 exported, err := ReadSessionSnapshot(exportPath)
108 if err != nil {
109 t.Fatalf("Read exported snapshot: %v", err)
110 }
111 if exported.Meta.Provider != "new-provider" || exported.Meta.Model != "new-model" {
112 t.Fatalf("export metadata = provider %q model %q, want latest values", exported.Meta.Provider, exported.Meta.Model)
113 }
114
115 importedPath, err := ImportSession(exportPath, t.TempDir(), "/destination", "test")
116 if err != nil {
117 t.Fatalf("ImportSession: %v", err)
118 }
119 imported, _, err := OpenSession(importedPath)
120 if err != nil {
121 t.Fatalf("Open imported session: %v", err)
122 }
123 defer imported.Close()
124 if imported.Meta.Provider != "new-provider" || imported.Meta.Model != "new-model" {
125 t.Fatalf("import metadata = provider %q model %q, want latest values", imported.Meta.Provider, imported.Meta.Model)
126 }
127}
128
129// TestScanSessionMetaReturnsLatestRowWithoutHydratingMessages verifies that
130// metadata lookup follows append-only model updates without reconstructing the
131// transcript.
132func TestScanSessionMetaReturnsLatestRowWithoutHydratingMessages(t *testing.T) {
133 root := t.TempDir()
134 session, err := NewSession(root, "/workspace", "old-provider", "old-model", "test")
135 if err != nil {
136 t.Fatal(err)
137 }
138 if err := session.AppendMessage(provider.Message{
139 Role: provider.RoleUser,
140 Content: []provider.Content{provider.TextBlock{Text: strings.Repeat("large transcript ", 1000)}},
141 }); err != nil {
142 t.Fatal(err)
143 }
144 if err := session.UpdateModel("new-provider", "new-model"); err != nil {
145 t.Fatal(err)
146 }
147 if err := session.AppendExtensionState("tasked-phases", json.RawMessage(`{"version":1}`)); err != nil {
148 t.Fatal(err)
149 }
150 path := session.Path
151 if err := session.Close(); err != nil {
152 t.Fatal(err)
153 }
154
155 meta, err := scanSessionMeta(path)
156 if err != nil {
157 t.Fatal(err)
158 }
159 if meta.Provider != "new-provider" || meta.Model != "new-model" {
160 t.Fatalf("scanned metadata = %q/%q, want latest values", meta.Provider, meta.Model)
161 }
162}
163
164// TestExportToFilePath writes to an explicit file path (no directory
165// guessing) and checks the .zutsession extension is appended when missing.
166func TestExportToFilePath(t *testing.T) {
167 root := t.TempDir()
168 sess, err := NewSession(root, "/cwd", "anthropic", "claude-opus-4-7", "0.0.0-test")
169 if err != nil {
170 t.Fatal(err)
171 }
172 _ = sess.AppendMessage(provider.Message{
173 Role: provider.RoleUser,
174 Content: []provider.Content{provider.TextBlock{Text: "x"}},
175 })
176 _ = sess.Close()
177
178 // No extension — should add .zutsession.
179 dst := filepath.Join(t.TempDir(), "mysession")
180 out, err := ExportSession(sess.Path, dst)
181 if err != nil {
182 t.Fatal(err)
183 }
184 if !strings.HasSuffix(out, PortableExt) {
185 t.Errorf("want .zutsession suffix on %q", out)
186 }
187}
188
189// TestExportStripsCWDFromMeta verifies the exported meta no longer
190// carries the source user's cwd (not useful to the recipient).
191func TestExportSessionHandlesHugeJSONLRows(t *testing.T) {
192 root := t.TempDir()
193 sess, err := NewSession(root, "/cwd", "anthropic", "claude-opus-4-7", "0.0.0-test")
194 if err != nil {
195 t.Fatal(err)
196 }
197 _ = sess.AppendMessage(provider.Message{
198 Role: provider.RoleUser,
199 Content: []provider.Content{provider.TextBlock{Text: "first prompt names export"}},
200 })
201 _ = sess.AppendMessage(provider.Message{
202 Role: provider.RoleAssistant,
203 Content: []provider.Content{provider.TextBlock{Text: strings.Repeat("x", 22*1024*1024)}},
204 })
205 _ = sess.Close()
206
207 exportDir := t.TempDir()
208 out, err := ExportSession(sess.Path, exportDir)
209 if err != nil {
210 t.Fatalf("ExportSession with huge row: %v", err)
211 }
212 if _, err := os.Stat(out); err != nil {
213 t.Fatalf("exported file missing: %v", err)
214 }
215 opened, msgs, err := OpenSession(sess.Path)
216 if err != nil {
217 t.Fatalf("OpenSession with huge row: %v", err)
218 }
219 _ = opened.Close()
220 if len(msgs) != 2 {
221 t.Fatalf("OpenSession messages=%d, want 2", len(msgs))
222 }
223 // Ensure the exported file is still readable JSONL and contains the
224 // huge assistant message row, not just the meta/header.
225 b, err := os.ReadFile(out)
226 if err != nil {
227 t.Fatal(err)
228 }
229 if !strings.Contains(string(b), strings.Repeat("x", 1024)) {
230 t.Fatalf("export does not appear to contain huge assistant row")
231 }
232 for n, line := range strings.Split(strings.TrimSpace(string(b)), "\n") {
233 var raw map[string]any
234 if err := json.Unmarshal([]byte(line), &raw); err != nil {
235 t.Fatalf("exported line %d is invalid json: %v", n+1, err)
236 }
237 }
238}
239
240func TestExportStripsCWDFromMeta(t *testing.T) {
241 root := t.TempDir()
242 sess, err := NewSession(root, "/original/cwd", "anthropic", "claude-opus-4-7", "0.0.0-test")
243 if err != nil {
244 t.Fatal(err)
245 }
246 _ = sess.AppendMessage(provider.Message{
247 Role: provider.RoleUser,
248 Content: []provider.Content{provider.TextBlock{Text: "x"}},
249 })
250 _ = sess.Close()
251
252 out, err := ExportSession(sess.Path, filepath.Join(t.TempDir(), "x"+PortableExt))
253 if err != nil {
254 t.Fatal(err)
255 }
256 b, _ := os.ReadFile(out)
257 if strings.Contains(string(b), "/original/cwd") {
258 t.Errorf("exported file leaks the source cwd: %s", string(b))
259 }
260}
261
262// TestBranchSessionCopiesPrefix writes several messages to a
263// session, branches at message index 2 (first user + first
264// assistant), and verifies the new session has exactly those two
265// messages with parent + fork_point meta set.
266func TestBranchSessionCopiesPrefix(t *testing.T) {
267 root := t.TempDir()
268 cwd := "/project"
269 parent, err := NewSession(root, cwd, "anthropic", "claude-opus-4-7", "0.0.0-test")
270 if err != nil {
271 t.Fatal(err)
272 }
273 _ = parent.AppendMessage(provider.Message{
274 Role: provider.RoleUser,
275 Content: []provider.Content{provider.TextBlock{Text: "first"}},
276 })
277 _ = parent.AppendMessage(provider.Message{
278 Role: provider.RoleAssistant,
279 Content: []provider.Content{provider.TextBlock{Text: "first reply"}},
280 })
281 _ = parent.AppendMessage(provider.Message{
282 Role: provider.RoleUser,
283 Content: []provider.Content{provider.TextBlock{Text: "second"}},
284 })
285 _ = parent.Close()
286
287 // Branch at the first user+assistant pair (upToMessageIdx=2).
288 branchPath, err := BranchSession(parent.Path, root, cwd, "0.0.0-test", 2)
289 if err != nil {
290 t.Fatalf("BranchSession: %v", err)
291 }
292 branch, msgs, err := OpenSession(branchPath)
293 if err != nil {
294 t.Fatal(err)
295 }
296 defer branch.Close()
297
298 if len(msgs) != 2 {
299 t.Errorf("want 2 copied messages, got %d", len(msgs))
300 }
301 if branch.Meta.Parent != parent.Meta.ID {
302 t.Errorf("parent id: want %q, got %q", parent.Meta.ID, branch.Meta.Parent)
303 }
304 if branch.Meta.ForkPoint != 2 {
305 t.Errorf("fork_point: want 2, got %d", branch.Meta.ForkPoint)
306 }
307 if branch.Meta.ID == parent.Meta.ID {
308 t.Errorf("branch kept parent id; must rotate")
309 }
310 if branch.Meta.Title != "" {
311 t.Errorf("meta title should remain append-only, got %q", branch.Meta.Title)
312 }
313 if summary := describeSession(branchPath); summary.Title != "first" {
314 t.Errorf("branch title: want %q, got %q", "first", summary.Title)
315 }
316}
317
318func TestBranchSessionTitleUsesSelectedUserPrompt(t *testing.T) {
319 root := t.TempDir()
320 cwd := "/project"
321 parent, err := NewSession(root, cwd, "anthropic", "claude-opus-4-7", "0.0.0-test")
322 if err != nil {
323 t.Fatal(err)
324 }
325 for _, msg := range []provider.Message{
326 {Role: provider.RoleUser, Content: []provider.Content{provider.TextBlock{Text: "first prompt"}}},
327 {Role: provider.RoleAssistant, Content: []provider.Content{provider.TextBlock{Text: "first reply"}}},
328 {Role: provider.RoleUser, Content: []provider.Content{provider.TextBlock{Text: "second prompt"}}},
329 } {
330 _ = parent.AppendMessage(msg)
331 }
332 _ = parent.Close()
333
334 branchPath, err := BranchSession(parent.Path, root, cwd, "0.0.0-test", 3)
335 if err != nil {
336 t.Fatalf("BranchSession: %v", err)
337 }
338 if summary := describeSession(branchPath); summary.Title != "second prompt" {
339 t.Errorf("branch title: want %q, got %q", "second prompt", summary.Title)
340 }
341}
342
343func TestBranchSessionTitleUsesFirstPromptAfterForkWhenBranchDiverges(t *testing.T) {
344 root := t.TempDir()
345 cwd := "/project"
346 parent, err := NewSession(root, cwd, "anthropic", "claude-opus-4-7", "0.0.0-test")
347 if err != nil {
348 t.Fatal(err)
349 }
350 _ = parent.AppendMessage(provider.Message{Role: provider.RoleUser, Content: []provider.Content{provider.TextBlock{Text: "hello"}}})
351 _ = parent.Close()
352
353 branchPath, err := BranchSession(parent.Path, root, cwd, "0.0.0-test", 1)
354 if err != nil {
355 t.Fatalf("BranchSession: %v", err)
356 }
357 branch, _, err := OpenSession(branchPath)
358 if err != nil {
359 t.Fatal(err)
360 }
361 _ = branch.AppendMessage(provider.Message{Role: provider.RoleUser, Content: []provider.Content{provider.TextBlock{Text: "how are you?"}}})
362 _ = branch.AppendMessage(provider.Message{Role: provider.RoleAssistant, Content: []provider.Content{provider.TextBlock{Text: "fine"}}})
363 _ = branch.Close()
364
365 if summary := describeSession(branchPath); summary.Title != "how are you?" {
366 t.Errorf("branch title: want %q, got %q", "how are you?", summary.Title)
367 }
368}
369
370func TestGeneratedBranchTitleWinsWhenWrittenBeforeBranchPrompt(t *testing.T) {
371 root := t.TempDir()
372 cwd := "/project"
373 parent, err := NewSession(root, cwd, "anthropic", "claude-opus-4-7", "0.0.0-test")
374 if err != nil {
375 t.Fatal(err)
376 }
377 _ = parent.AppendMessage(provider.Message{Role: provider.RoleUser, Content: []provider.Content{provider.TextBlock{Text: "hello"}}})
378 _ = parent.Close()
379
380 branchPath, err := BranchSession(parent.Path, root, cwd, "0.0.0-test", 1)
381 if err != nil {
382 t.Fatalf("BranchSession: %v", err)
383 }
384 branch, _, err := OpenSession(branchPath)
385 if err != nil {
386 t.Fatal(err)
387 }
388 if err := branch.UpdateTitle("AI branch title"); err != nil {
389 t.Fatal(err)
390 }
391 if err := branch.AppendMessage(provider.Message{Role: provider.RoleUser, Content: []provider.Content{provider.TextBlock{Text: "how are you?"}}}); err != nil {
392 t.Fatal(err)
393 }
394 if err := branch.Close(); err != nil {
395 t.Fatal(err)
396 }
397
398 if summary := describeSession(branchPath); summary.Title != "AI branch title" {
399 t.Errorf("branch title: want %q, got %q", "AI branch title", summary.Title)
400 }
401}
402
403func TestRenamedBranchKeepsExplicitTitle(t *testing.T) {
404 root := t.TempDir()
405 cwd := "/project"
406 parent, err := NewSession(root, cwd, "anthropic", "claude-opus-4-7", "0.0.0-test")
407 if err != nil {
408 t.Fatal(err)
409 }
410 _ = parent.AppendMessage(provider.Message{Role: provider.RoleUser, Content: []provider.Content{provider.TextBlock{Text: "hello"}}})
411 _ = parent.Close()
412
413 branchPath, err := BranchSession(parent.Path, root, cwd, "0.0.0-test", 1)
414 if err != nil {
415 t.Fatalf("BranchSession: %v", err)
416 }
417 branch, _, err := OpenSession(branchPath)
418 if err != nil {
419 t.Fatal(err)
420 }
421 _ = branch.AppendMessage(provider.Message{Role: provider.RoleUser, Content: []provider.Content{provider.TextBlock{Text: "how are you?"}}})
422 _ = branch.Close()
423 if err := RenameSession(branchPath, "friendly branch name"); err != nil {
424 t.Fatal(err)
425 }
426
427 if summary := describeSession(branchPath); summary.Title != "friendly branch name" {
428 t.Errorf("branch title: want %q, got %q", "friendly branch name", summary.Title)
429 }
430}
431
432func TestBranchSessionUsesEffectiveTranscriptAfterCompaction(t *testing.T) {
433 root := t.TempDir()
434 cwd := "/project"
435 parent, err := NewSession(root, cwd, "anthropic", "claude-opus-4-7", "0.0.0-test")
436 if err != nil {
437 t.Fatal(err)
438 }
439 for _, text := range []string{"raw-a", "raw-b", "raw-c", "raw-d"} {
440 _ = parent.AppendMessage(provider.Message{
441 Role: provider.RoleUser,
442 Content: []provider.Content{provider.TextBlock{Text: text}},
443 })
444 }
445 _ = parent.AppendCompaction([]provider.Message{
446 {Role: provider.RoleAssistant, Content: []provider.Content{provider.TextBlock{Text: "summary"}}},
447 {Role: provider.RoleUser, Content: []provider.Content{provider.TextBlock{Text: "tail-c"}}},
448 {Role: provider.RoleAssistant, Content: []provider.Content{provider.TextBlock{Text: "tail-d"}}},
449 })
450 _ = parent.AppendMessage(provider.Message{
451 Role: provider.RoleUser,
452 Content: []provider.Content{provider.TextBlock{Text: "after-compact"}},
453 })
454 _ = parent.Close()
455
456 opened, msgs, err := OpenSession(parent.Path)
457 if err != nil {
458 t.Fatalf("OpenSession parent: %v", err)
459 }
460 _ = opened.Close()
461 assertMessageTexts(t, msgs, []string{"summary", "tail-c", "tail-d", "after-compact"})
462
463 branchPath, err := BranchSession(parent.Path, root, cwd, "0.0.0-test", 4)
464 if err != nil {
465 t.Fatalf("BranchSession: %v", err)
466 }
467 branch, branchMsgs, err := OpenSession(branchPath)
468 if err != nil {
469 t.Fatalf("OpenSession branch: %v", err)
470 }
471 defer branch.Close()
472
473 assertMessageTexts(t, branchMsgs, []string{"summary", "tail-c", "tail-d", "after-compact"})
474 if branch.Meta.Parent != parent.Meta.ID {
475 t.Errorf("parent id: want %q, got %q", parent.Meta.ID, branch.Meta.Parent)
476 }
477 if branch.Meta.ForkPoint != 4 {
478 t.Errorf("fork_point: want 4, got %d", branch.Meta.ForkPoint)
479 }
480
481 shortBranchPath, err := BranchSession(parent.Path, root, cwd, "0.0.0-test", 2)
482 if err != nil {
483 t.Fatalf("BranchSession short fork: %v", err)
484 }
485 shortBranch, shortBranchMsgs, err := OpenSession(shortBranchPath)
486 if err != nil {
487 t.Fatalf("OpenSession short branch: %v", err)
488 }
489 defer shortBranch.Close()
490
491 assertMessageTexts(t, shortBranchMsgs, []string{"summary", "tail-c"})
492 if shortBranch.Meta.ForkPoint != 2 {
493 t.Errorf("short branch fork_point: want 2, got %d", shortBranch.Meta.ForkPoint)
494 }
495}
496
497func TestBranchSessionCopiesExtensionStateAtFork(t *testing.T) {
498 root := t.TempDir()
499 cwd := "/project"
500 parent, err := NewSession(root, cwd, "anthropic", "claude", "test")
501 if err != nil {
502 t.Fatal(err)
503 }
504 if err := parent.AppendMessage(provider.Message{Role: provider.RoleUser, Content: []provider.Content{provider.TextBlock{Text: "first"}}}); err != nil {
505 t.Fatal(err)
506 }
507 if err := parent.AppendExtensionState("tasked-phases", json.RawMessage(`{"version":1}`)); err != nil {
508 t.Fatal(err)
509 }
510 if err := parent.AppendMessage(provider.Message{Role: provider.RoleUser, Content: []provider.Content{provider.TextBlock{Text: "second"}}}); err != nil {
511 t.Fatal(err)
512 }
513 if err := parent.AppendExtensionState("tasked-phases", json.RawMessage(`{"version":2}`)); err != nil {
514 t.Fatal(err)
515 }
516 if err := parent.Close(); err != nil {
517 t.Fatal(err)
518 }
519
520 branchPath, err := BranchSession(parent.Path, root, cwd, "test", 1)
521 if err != nil {
522 t.Fatal(err)
523 }
524 branch, _, err := OpenSession(branchPath)
525 if err != nil {
526 t.Fatal(err)
527 }
528 t.Cleanup(func() { _ = branch.Close() })
529 if got := string(branch.ExtensionState["tasked-phases"]); got != `{"version":1}` {
530 t.Fatalf("branch extension state = %q, want version 1", got)
531 }
532}
533
534func TestBranchSessionCarriesStateAcrossCompactionBoundary(t *testing.T) {
535 root := t.TempDir()
536 cwd := "/project"
537 parent, err := NewSession(root, cwd, "anthropic", "claude", "test")
538 if err != nil {
539 t.Fatal(err)
540 }
541 for _, text := range []string{"before-a", "before-b"} {
542 if err := parent.AppendMessage(provider.Message{Role: provider.RoleUser, Content: []provider.Content{provider.TextBlock{Text: text}}}); err != nil {
543 t.Fatal(err)
544 }
545 }
546 if err := parent.AppendExtensionState("tasked-phases", json.RawMessage(`{"version":1}`)); err != nil {
547 t.Fatal(err)
548 }
549 if err := parent.AppendCompaction([]provider.Message{
550 {Role: provider.RoleAssistant, Content: []provider.Content{provider.TextBlock{Text: "summary"}}},
551 {Role: provider.RoleUser, Content: []provider.Content{provider.TextBlock{Text: "tail"}}},
552 }); err != nil {
553 t.Fatal(err)
554 }
555 if err := parent.Close(); err != nil {
556 t.Fatal(err)
557 }
558
559 branchPath, err := BranchSession(parent.Path, root, cwd, "test", 1)
560 if err != nil {
561 t.Fatal(err)
562 }
563 branch, _, err := OpenSession(branchPath)
564 if err != nil {
565 t.Fatal(err)
566 }
567 t.Cleanup(func() { _ = branch.Close() })
568 if got := string(branch.ExtensionState["tasked-phases"]); got != `{"version":1}` {
569 t.Fatalf("compacted branch extension state = %q, want version 1", got)
570 }
571}
572
573// TestBuildSessionTree verifies parent/child edges are rebuilt
574// from meta + sibling-scan.
575func TestBuildSessionTree(t *testing.T) {
576 root := t.TempDir()
577 cwd := "/project"
578 parent, _ := NewSession(root, cwd, "anthropic", "claude-opus-4-7", "0.0.0-test")
579 _ = parent.AppendMessage(provider.Message{
580 Role: provider.RoleUser,
581 Content: []provider.Content{provider.TextBlock{Text: "x"}},
582 })
583 _ = parent.Close()
584
585 childA, err := BranchSession(parent.Path, root, cwd, "0.0.0-test", 1)
586 if err != nil {
587 t.Fatal(err)
588 }
589 childB, err := BranchSession(parent.Path, root, cwd, "0.0.0-test", 1)
590 if err != nil {
591 t.Fatal(err)
592 }
593 _ = childA
594 _ = childB
595
596 tree := BuildSessionTree(root, cwd)
597 if len(tree) != 1 {
598 t.Fatalf("want 1 root, got %d", len(tree))
599 }
600 rootNode := tree[0]
601 if rootNode.Meta.ID != parent.Meta.ID {
602 t.Errorf("root should be parent %q, got %q", parent.Meta.ID, rootNode.Meta.ID)
603 }
604 if len(rootNode.Children) != 2 {
605 t.Errorf("want 2 children, got %d", len(rootNode.Children))
606 }
607}
608
609func assertMessageTexts(t *testing.T, msgs []provider.Message, want []string) {
610 t.Helper()
611 if len(msgs) != len(want) {
612 t.Fatalf("message count: want %d, got %d", len(want), len(msgs))
613 }
614 for i, msg := range msgs {
615 if got := extractText(msg); got != want[i] {
616 t.Errorf("message %d: want %q, got %q", i, want[i], got)
617 }
618 }
619}