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 {name: "changed session id", contents: string(metaLine) + "\n" + `{"type":"meta","meta":{"id":"session-2","cwd":"/workspace"}}` + "\n", want: `session id changed from "session-1" to "session-2"`},
32 }
33
34 for _, test := range tests {
35 t.Run(test.name, func(t *testing.T) {
36 path := filepath.Join(t.TempDir(), "session.jsonl")
37 if err := os.WriteFile(path, []byte(test.contents), 0o644); err != nil {
38 t.Fatal(err)
39 }
40
41 _, snapshotErr := ReadSessionSnapshot(path)
42 _, historyErr := ReadSessionHistory(path)
43 _, branchErr := readExtensionStateAtFork(context.Background(), path, 0)
44 for name, err := range map[string]error{
45 "snapshot": snapshotErr,
46 "history": historyErr,
47 "branch": branchErr,
48 } {
49 if err == nil || !strings.Contains(err.Error(), test.want) {
50 t.Errorf("%s error = %v, want substring %q", name, err, test.want)
51 }
52 }
53 })
54 }
55}
56
57func TestBranchSessionHonorsCancellationBeforeSnapshotRead(t *testing.T) {
58 root := t.TempDir()
59 ctx, cancel := context.WithCancel(context.Background())
60 cancel()
61
62 missingParent := filepath.Join(root, "missing.jsonl")
63 if _, err := branchSession(ctx, missingParent, root, "/workspace", "test", 0, false); !errors.Is(err, context.Canceled) {
64 t.Fatalf("branchSession error = %v, want context.Canceled", err)
65 }
66}
67
68func TestBranchSessionThreadsCancellationToExtensionStateRead(t *testing.T) {
69 root := t.TempDir()
70 session, err := NewSession(root, "/workspace", "provider", "model", "test")
71 if err != nil {
72 t.Fatal(err)
73 }
74 if err := session.AppendExtensionState("extension", json.RawMessage(`{"version":1}`)); err != nil {
75 t.Fatal(err)
76 }
77 path := session.Path
78 if err := session.Close(); err != nil {
79 t.Fatal(err)
80 }
81
82 ctx, cancel := context.WithCancel(context.Background())
83 cancel()
84 if _, err := branchSession(ctx, path, root, "/workspace", "test", 0, false); !errors.Is(err, context.Canceled) {
85 t.Fatalf("branchSession error = %v, want context.Canceled", err)
86 }
87}