返回 DeepSeek-Reasonix
session_ids_test.go
根目录 / internal / agent / session_ids_test.go
1 package agent
2
3 import (
4 "path/filepath"
5 "strings"
6 "testing"
7
8 "reasonix/internal/provider"
9 )
10
11 func idFixtureMessages() []provider.Message {
12 return []provider.Message{
13 {Role: provider.RoleSystem, Content: "sys"},
14 {Role: provider.RoleUser, Content: "hi", CreatedAt: 1},
15 {Role: provider.RoleAssistant, ToolCalls: []provider.ToolCall{{ID: "call_1", Name: "ls", Arguments: `{}`}}},
16 {Role: provider.RoleTool, Content: "main.go", ToolCallID: "call_1", Name: "ls", ToolExecution: &provider.ToolExecution{}},
17 {Role: provider.RoleAssistant, Content: "done", ReasoningContent: "r"},
18 }
19 }
20
21 func withIDs(msgs []provider.Message) []provider.Message {
22 out := append([]provider.Message(nil), msgs...)
23 for i := range out {
24 out[i].ID = NewMessageID()
25 }
26 return out
27 }
28
29 func TestNewMessageIDShapeAndUniqueness(t *testing.T) {
30 seen := make(map[string]struct{}, 2000)
31 prev := ""
32 for range 2000 {
33 id := NewMessageID()
34 if len(id) != messageIDLen {
35 t.Fatalf("len(%q) = %d, want %d", id, len(id), messageIDLen)
36 }
37 for _, r := range id {
38 if !strings.ContainsRune(messageIDAlphabet, r) {
39 t.Fatalf("id %q contains %q outside the alphabet", id, r)
40 }
41 }
42 if _, dup := seen[id]; dup {
43 t.Fatalf("duplicate id %q", id)
44 }
45 seen[id] = struct{}{}
46 if prev != "" && id[:10] < prev[:10] {
47 t.Fatalf("time prefix went backwards: %q after %q", id, prev)
48 }
49 prev = id
50 }
51 }
52
53 func TestLegacyMessageIDsAreDeterministicAndPositional(t *testing.T) {
54 a := idFixtureMessages()
55 b := idFixtureMessages()
56 assignLegacyMessageIDs("/tmp/x/sess-1.jsonl", a)
57 assignLegacyMessageIDs("/other/dir/sess-1.jsonl", b)
58 for i := range a {
59 if a[i].ID == "" || len(a[i].ID) != messageIDLen {
60 t.Fatalf("message %d id %q malformed", i, a[i].ID)
61 }
62 if a[i].ID != b[i].ID {
63 t.Fatalf("message %d: same branch id and bytes derived %q vs %q", i, a[i].ID, b[i].ID)
64 }
65 for j := range a[:i] {
66 if a[j].ID == a[i].ID {
67 t.Fatalf("messages %d and %d share id %q", j, i, a[i].ID)
68 }
69 }
70 }
71 c := idFixtureMessages()
72 assignLegacyMessageIDs("/tmp/x/sess-2.jsonl", c)
73 if c[0].ID == a[0].ID {
74 t.Fatal("different branch ids must derive different message ids")
75 }
76 d := idFixtureMessages()
77 d[1].Content = "changed"
78 assignLegacyMessageIDs("/tmp/x/sess-1.jsonl", d)
79 if d[0].ID != a[0].ID {
80 t.Fatal("an edit after message 0 must not change message 0's id")
81 }
82 if d[2].ID == a[2].ID {
83 t.Fatal("an edited prefix must change the ids that follow it")
84 }
85 e := idFixtureMessages()
86 e[3].ID = "PRESET"
87 assignLegacyMessageIDs("/tmp/x/sess-1.jsonl", e)
88 if e[3].ID != "PRESET" || e[4].ID != a[4].ID {
89 t.Fatalf("preset ids must be kept and must not perturb later ids: %q %q", e[3].ID, e[4].ID)
90 }
91 }
92
93 func TestLoadSessionDerivesStableIDsForLegacyTranscripts(t *testing.T) {
94 dir := t.TempDir()
95 path := filepath.Join(dir, "legacy.jsonl")
96 s := &Session{Messages: idFixtureMessages()}
97 if err := s.Save(path); err != nil {
98 t.Fatalf("save: %v", err)
99 }
100 first, err := LoadSession(path)
101 if err != nil {
102 t.Fatalf("load: %v", err)
103 }
104 second, err := LoadSession(path)
105 if err != nil {
106 t.Fatalf("reload: %v", err)
107 }
108 if len(first.Messages) != len(second.Messages) || len(first.Messages) == 0 {
109 t.Fatalf("lengths %d vs %d", len(first.Messages), len(second.Messages))
110 }
111 for i := range first.Messages {
112 if first.Messages[i].ID == "" || first.Messages[i].ID != second.Messages[i].ID {
113 t.Fatalf("message %d id unstable across loads: %q vs %q", i, first.Messages[i].ID, second.Messages[i].ID)
114 }
115 }
116 if got := first.LeafID(); got != first.Messages[len(first.Messages)-1].ID {
117 t.Fatalf("LeafID = %q", got)
118 }
119 if idx := first.IndexOfID(first.Messages[2].ID); idx != 2 {
120 t.Fatalf("IndexOfID = %d, want 2", idx)
121 }
122 if idx := first.IndexOfID("missing"); idx != -1 {
123 t.Fatalf("IndexOfID(missing) = %d", idx)
124 }
125 }
126
127 func TestMessageIDsStayOutOfTranscriptIdentity(t *testing.T) {
128 plain := idFixtureMessages()
129 tagged := withIDs(plain)
130 dp, err := digestSessionMessages(plain)
131 if err != nil {
132 t.Fatal(err)
133 }
134 dt, err := digestSessionMessages(tagged)
135 if err != nil {
136 t.Fatal(err)
137 }
138 if dp != dt {
139 t.Fatal("digestSessionMessages must ignore message ids")
140 }
141 if !messagesHavePrefix(tagged, plain) || !messagesHavePrefix(plain, tagged) {
142 t.Fatal("prefix comparison must ignore message ids")
143 }
144 if providerVisibleFingerprint(plain) != providerVisibleFingerprint(tagged) {
145 t.Fatal("providerVisibleFingerprint must ignore message ids")
146 }
147 if coveredPrefixHash(plain, 3) != coveredPrefixHash(tagged, 3) {
148 t.Fatal("coveredPrefixHash must ignore message ids")
149 }
150 hp, ht := newSessionTranscriptHasher(), newSessionTranscriptHasher()
151 hp.addAll(plain)
152 ht.addAll(tagged)
153 sp, _ := hp.sum()
154 st, _ := ht.sum()
155 if sp != st {
156 t.Fatal("sessionTranscriptHasher must ignore message ids")
157 }
158 }
159
160 func TestSessionMutatorsMintMissingIDs(t *testing.T) {
161 s := NewSession("sys")
162 if s.Messages[0].ID == "" {
163 t.Fatal("NewSession must mint the system message id")
164 }
165 s.Add(provider.Message{Role: provider.RoleUser, Content: "a"})
166 s.AddBatch(provider.Message{Role: provider.RoleUser, Content: "b"}, provider.Message{Role: provider.RoleAssistant, Content: "c", ID: "KEEP"})
167 if s.Messages[1].ID == "" || s.Messages[2].ID == "" {
168 t.Fatal("Add/AddBatch must mint ids")
169 }
170 if s.Messages[3].ID != "KEEP" {
171 t.Fatalf("AddBatch must keep a caller id, got %q", s.Messages[3].ID)
172 }
173 if s.LeafID() != "KEEP" {
174 t.Fatalf("LeafID = %q", s.LeafID())
175 }
176 kept := s.Messages[1].ID
177 s.Rewrite([]provider.Message{s.Messages[0], s.Messages[1], {Role: provider.RoleAssistant, Content: "summary"}}, "test")
178 if s.Messages[1].ID != kept || s.Messages[2].ID == "" {
179 t.Fatal("Rewrite must keep existing ids and mint new ones")
180 }
181 s.Replace([]provider.Message{{Role: provider.RoleUser, Content: "x"}})
182 s.ReplaceLocalMetadata([]provider.Message{s.Messages[0], {Role: provider.RoleAssistant, Content: "y"}})
183 for i, m := range s.Messages {
184 if m.ID == "" {
185 t.Fatalf("message %d has no id after Replace/ReplaceLocalMetadata", i)
186 }
187 }
188 empty := NewSession("")
189 if empty.LeafID() != "" || empty.IndexOfID("x") != -1 {
190 t.Fatal("empty session must report no leaf")
191 }
192 empty.SetLeadingSystemPrompt("p")
193 if empty.Messages[0].ID == "" {
194 t.Fatal("SetLeadingSystemPrompt must mint the prepended system id")
195 }
196 }
197
197 lines GO