| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "os" |
| 5 | "path/filepath" |
| 6 | "strings" |
| 7 | "testing" |
| 8 | |
| 9 | "reasonix/internal/agent" |
| 10 | "reasonix/internal/provider" |
| 11 | ) |
| 12 | |
| 13 | func TestPreviewSessionMessagesLoadsWithoutResuming(t *testing.T) { |
| 14 | dir := t.TempDir() |
| 15 | session := agent.NewSession("") |
| 16 | session.Add(provider.Message{Role: provider.RoleUser, Content: "show history"}) |
| 17 | session.Add(provider.Message{Role: provider.RoleAssistant, Content: "answer", ReasoningContent: "saved reasoning"}) |
| 18 | path := filepath.Join(dir, "session.jsonl") |
| 19 | if err := session.Save(path); err != nil { |
| 20 | t.Fatalf("Save: %v", err) |
| 21 | } |
| 22 | |
| 23 | got, err := previewSessionMessages(dir, path) |
| 24 | if err != nil { |
| 25 | t.Fatalf("previewSessionMessages: %v", err) |
| 26 | } |
| 27 | if len(got) != 2 { |
| 28 | t.Fatalf("preview history length = %d, want 2", len(got)) |
| 29 | } |
| 30 | if got[1].Reasoning != "saved reasoning" { |
| 31 | t.Fatalf("preview reasoning = %q, want saved reasoning", got[1].Reasoning) |
| 32 | } |
| 33 | } |
| 34 | |
| 35 | func TestPreviewSessionMessagesUpgradesLegacyExpandedPaste(t *testing.T) { |
| 36 | const label = "[Pasted text #1 · 2 lines]" |
| 37 | const display = "inspect this\n\n" + label |
| 38 | const expanded = display + "\n\n--- Begin " + label + " ---\none\ntwo\n--- End " + label + " ---" |
| 39 | const rendered = "<capability-route version=\"1\">\nuse review\n</capability-route>\n\n" + expanded |
| 40 | |
| 41 | dir := t.TempDir() |
| 42 | path := filepath.Join(dir, "legacy.jsonl") |
| 43 | session := agent.NewSession("") |
| 44 | session.Add(provider.Message{Role: provider.RoleUser, Content: rendered, RawContent: expanded}) |
| 45 | if err := session.Save(path); err != nil { |
| 46 | t.Fatalf("Save legacy session: %v", err) |
| 47 | } |
| 48 | if err := recordSessionDisplay(dir, path, rendered, display); err != nil { |
| 49 | t.Fatalf("record legacy display: %v", err) |
| 50 | } |
| 51 | |
| 52 | got, err := previewSessionMessages(dir, path) |
| 53 | if err != nil { |
| 54 | t.Fatalf("previewSessionMessages: %v", err) |
| 55 | } |
| 56 | if len(got) != 1 || got[0].Content != display || got[0].SubmitText != expanded { |
| 57 | t.Fatalf("upgraded legacy preview = %+v, want display %q and expanded replay", got, display) |
| 58 | } |
| 59 | if strings.Contains(got[0].SubmitText, "capability-route") { |
| 60 | t.Fatalf("provider-only wrapper leaked after session restart: %+v", got[0]) |
| 61 | } |
| 62 | } |
| 63 | |
| 64 | func TestPreviewSessionMessagesUpgradesContentOnlyExpandedPaste(t *testing.T) { |
| 65 | const label = "[Pasted text #1 · 2 lines]" |
| 66 | const display = "inspect this\n\n" + label |
| 67 | const expanded = display + "\n\n--- Begin " + label + " ---\none\ntwo\n--- End " + label + " ---" |
| 68 | |
| 69 | dir := t.TempDir() |
| 70 | path := filepath.Join(dir, "content-only.jsonl") |
| 71 | session := agent.NewSession("") |
| 72 | // Releases before Context Engine v2 persisted user turns without RawContent. |
| 73 | session.Add(provider.Message{Role: provider.RoleUser, Content: expanded}) |
| 74 | if err := session.Save(path); err != nil { |
| 75 | t.Fatalf("Save content-only session: %v", err) |
| 76 | } |
| 77 | |
| 78 | got, err := previewSessionMessages(dir, path) |
| 79 | if err != nil { |
| 80 | t.Fatalf("previewSessionMessages: %v", err) |
| 81 | } |
| 82 | if len(got) != 1 || got[0].Content != display || got[0].SubmitText != expanded { |
| 83 | t.Fatalf("upgraded content-only preview = %+v, want display %q and expanded replay", got, display) |
| 84 | } |
| 85 | } |
| 86 | |
| 87 | func TestPreviewSessionMessagesIncludesProcessEvents(t *testing.T) { |
| 88 | dir := t.TempDir() |
| 89 | path := filepath.Join(dir, "events.jsonl") |
| 90 | body := strings.Join([]string{ |
| 91 | `{"kind":"phase","text":"Preparing context"}`, |
| 92 | `{"kind":"notice","level":"warn","text":"Network changed"}`, |
| 93 | `{"kind":"compaction_started","compaction":{"trigger":"manual"}}`, |
| 94 | `{"kind":"compaction_done","compaction":{"trigger":"manual","messages":6,"summary":"Kept the current task.","archive":"/tmp/archive.jsonl"}}`, |
| 95 | `{"type":"user.message","text":"hello","ts":1718000000000}`, |
| 96 | `{"type":"model.final","content":"hi","reasoningContent":"thinking"}`, |
| 97 | }, "\n") + "\n" |
| 98 | if err := os.WriteFile(path, []byte(body), 0o644); err != nil { |
| 99 | t.Fatal(err) |
| 100 | } |
| 101 | |
| 102 | got, err := previewSessionMessages(dir, path) |
| 103 | if err != nil { |
| 104 | t.Fatalf("previewSessionMessages: %v", err) |
| 105 | } |
| 106 | if len(got) != 6 { |
| 107 | t.Fatalf("preview history length = %d, want 6: %+v", len(got), got) |
| 108 | } |
| 109 | if got[0].Role != "phase" || got[0].Content != "Preparing context" { |
| 110 | t.Fatalf("phase event not preserved: %+v", got[0]) |
| 111 | } |
| 112 | if got[1].Role != "notice" || got[1].Level != "warn" || got[1].Content != "Network changed" { |
| 113 | t.Fatalf("notice event not preserved: %+v", got[1]) |
| 114 | } |
| 115 | if got[2].Role != "compaction" || !got[2].Pending || got[2].Trigger != "manual" { |
| 116 | t.Fatalf("pending compaction event not preserved: %+v", got[2]) |
| 117 | } |
| 118 | if got[3].Role != "compaction" || got[3].Pending || got[3].Messages != 6 || got[3].Summary != "Kept the current task." || got[3].Archive != "/tmp/archive.jsonl" { |
| 119 | t.Fatalf("finished compaction event not preserved: %+v", got[3]) |
| 120 | } |
| 121 | if got[4].Role != "user" || got[5].Reasoning != "thinking" { |
| 122 | t.Fatalf("conversation events not preserved: %+v", got[4:]) |
| 123 | } |
| 124 | if got[4].CreatedAt != 1_718_000_000_000 { |
| 125 | t.Fatalf("event user createdAt = %d, want 1718000000000", got[4].CreatedAt) |
| 126 | } |
| 127 | } |
| 128 | |
| 129 | func TestPreviewSessionMessagesRestoresAppendEventUserTime(t *testing.T) { |
| 130 | dir := t.TempDir() |
| 131 | path := filepath.Join(dir, "session.jsonl") |
| 132 | session := agent.NewSession("") |
| 133 | session.Add(provider.Message{Role: provider.RoleUser, Content: "first"}) |
| 134 | if err := session.SaveSnapshot(path); err != nil { |
| 135 | t.Fatalf("SaveSnapshot first: %v", err) |
| 136 | } |
| 137 | session.Add(provider.Message{Role: provider.RoleAssistant, Content: "one"}) |
| 138 | session.Add(provider.Message{Role: provider.RoleUser, Content: "second"}) |
| 139 | if err := session.SaveSnapshot(path); err != nil { |
| 140 | t.Fatalf("SaveSnapshot second: %v", err) |
| 141 | } |
| 142 | |
| 143 | got, err := previewSessionMessages(dir, path) |
| 144 | if err != nil { |
| 145 | t.Fatalf("previewSessionMessages: %v", err) |
| 146 | } |
| 147 | if len(got) != 3 || got[2].Role != "user" || got[2].Content != "second" { |
| 148 | t.Fatalf("preview history = %+v, want second user at index 2", got) |
| 149 | } |
| 150 | if got[2].CreatedAt <= 0 { |
| 151 | t.Fatalf("append-event user timestamp was not restored: %+v", got[2]) |
| 152 | } |
| 153 | } |
| 154 |