1package core
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "os"
8 "path/filepath"
9 "strings"
10 "testing"
11 "time"
12)
13
14func TestSessionLogProjectionsRejectInvalidRowsConsistently(t *testing.T) {
15 meta := SessionMeta{ID: "session-1", CWD: "/workspace", Started: time.Now().UTC()}
16 metaLine, err := json.Marshal(sessionLine{Type: "meta", Meta: &meta})
17 if err != nil {
18 t.Fatal(err)
19 }
20
21 tests := []struct {
22 name string
23 contents string
24 want string
25 }{
26 {name: "missing meta", contents: `{"type":"message","message":{"role":"user","content":[{"text":"hello"}]}}` + "\n", want: "first row is not meta"},
27 {name: "invalid message", contents: string(metaLine) + "\n" + `{"type":"message","message":{"role":"user"}}` + "\n", want: "invalid message row"},
28 {name: "invalid usage", contents: string(metaLine) + "\n" + `{"type":"usage","cumulative":null}` + "\n", want: "usage row has no cumulative usage"},
29 {name: "invalid rename", contents: string(metaLine) + "\n" + `{"type":"rename"}` + "\n", want: "rename row has no title"},
30 {name: "unknown type", contents: string(metaLine) + "\n" + `{"type":"future-row"}` + "\n", want: `unknown row type "future-row"`},
31 }
32
33 for _, test := range tests {
34 t.Run(test.name, func(t *testing.T) {
35 path := filepath.Join(t.TempDir(), "session.jsonl")
36 if err := os.WriteFile(path, []byte(test.contents), 0o644); err != nil {
37 t.Fatal(err)
38 }
39
40 _, snapshotErr := ReadSessionSnapshot(path)
41 _, historyErr := ReadSessionHistory(path)
42 _, branchErr := readExtensionStateAtFork(context.Background(), path, 0)
43 for name, err := range map[string]error{
44 "snapshot": snapshotErr,
45 "history": historyErr,
46 "branch": branchErr,
47 } {
48 if err == nil || !strings.Contains(err.Error(), test.want) {
49 t.Errorf("%s error = %v, want substring %q", name, err, test.want)
50 }
51 }
52 })
53 }
54}
55
56func TestBranchSessionHonorsCancellationBeforeSnapshotRead(t *testing.T) {
57 root := t.TempDir()
58 ctx, cancel := context.WithCancel(context.Background())
59 cancel()
60
61 missingParent := filepath.Join(root, "missing.jsonl")
62 if _, err := branchSession(ctx, missingParent, root, "/workspace", "test", 0, false); !errors.Is(err, context.Canceled) {
63 t.Fatalf("branchSession error = %v, want context.Canceled", err)
64 }
65}
66
67func TestBranchSessionThreadsCancellationToExtensionStateRead(t *testing.T) {
68 root := t.TempDir()
69 session, err := NewSession(root, "/workspace", "provider", "model", "test")
70 if err != nil {
71 t.Fatal(err)
72 }
73 if err := session.AppendExtensionState("extension", json.RawMessage(`{"version":1}`)); err != nil {
74 t.Fatal(err)
75 }
76 path := session.Path
77 if err := session.Close(); err != nil {
78 t.Fatal(err)
79 }
80
81 ctx, cancel := context.WithCancel(context.Background())
82 cancel()
83 if _, err := branchSession(ctx, path, root, "/workspace", "test", 0, false); !errors.Is(err, context.Canceled) {
84 t.Fatalf("branchSession error = %v, want context.Canceled", err)
85 }
86}