-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathstorage_test.go
More file actions
396 lines (315 loc) · 12.7 KB
/
storage_test.go
File metadata and controls
396 lines (315 loc) · 12.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
package contextwindow
import (
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
_ "modernc.org/sqlite"
)
func TestValidateResponseIDChain_ValidChain(t *testing.T) {
path := filepath.Join(t.TempDir(), "cw.db")
db, err := NewContextDB(path)
assert.NoError(t, err)
defer db.Close()
// Create context with threading enabled
ctx, err := CreateContextWithThreading(db, "test-context", true)
assert.NoError(t, err)
// Add a prompt
_, err = InsertRecord(db, ctx.ID, Prompt, "test prompt", true)
assert.NoError(t, err)
// Add a model response with response_id
responseID := "resp-123"
_, err = InsertRecordWithResponseID(db, ctx.ID, ModelResp, "test response", true, &responseID)
assert.NoError(t, err)
// Update context with last response ID
err = UpdateContextLastResponseID(db, ctx.ID, responseID)
assert.NoError(t, err)
// Validate chain - should be valid
valid, reason := ValidateResponseIDChain(db, ctx)
assert.True(t, valid)
assert.Equal(t, "chain valid", reason)
}
func TestValidateResponseIDChain_MissingLastResponseID(t *testing.T) {
path := filepath.Join(t.TempDir(), "cw.db")
db, err := NewContextDB(path)
assert.NoError(t, err)
defer db.Close()
// Create context with threading enabled but no LastResponseID set
ctx, err := CreateContextWithThreading(db, "test-context", true)
assert.NoError(t, err)
// Add a prompt
_, err = InsertRecord(db, ctx.ID, Prompt, "test prompt", true)
assert.NoError(t, err)
// Validate chain - should be invalid (no LastResponseID)
valid, reason := ValidateResponseIDChain(db, ctx)
assert.False(t, valid)
assert.Equal(t, "no last_response_id set", reason)
}
func TestValidateResponseIDChain_ToolCallsPresent(t *testing.T) {
path := filepath.Join(t.TempDir(), "cw.db")
db, err := NewContextDB(path)
assert.NoError(t, err)
defer db.Close()
// Create context with threading enabled
ctx, err := CreateContextWithThreading(db, "test-context", true)
assert.NoError(t, err)
// Add a prompt
_, err = InsertRecord(db, ctx.ID, Prompt, "test prompt", true)
assert.NoError(t, err)
// Add a model response with response_id
responseID := "resp-123"
_, err = InsertRecordWithResponseID(db, ctx.ID, ModelResp, "test response", true, &responseID)
assert.NoError(t, err)
// Update context with last response ID
err = UpdateContextLastResponseID(db, ctx.ID, responseID)
assert.NoError(t, err)
// Add a tool call - this should break server-side threading
_, err = InsertRecord(db, ctx.ID, ToolCall, "tool call", true)
assert.NoError(t, err)
// Validate chain - should be invalid (tool calls present)
valid, reason := ValidateResponseIDChain(db, ctx)
assert.False(t, valid)
assert.Equal(t, "tool calls present (break server-side threading)", reason)
}
func TestValidateResponseIDChain_ToolOutputPresent(t *testing.T) {
path := filepath.Join(t.TempDir(), "cw.db")
db, err := NewContextDB(path)
assert.NoError(t, err)
defer db.Close()
// Create context with threading enabled
ctx, err := CreateContextWithThreading(db, "test-context", true)
assert.NoError(t, err)
// Add a prompt
_, err = InsertRecord(db, ctx.ID, Prompt, "test prompt", true)
assert.NoError(t, err)
// Add a model response with response_id
responseID := "resp-123"
_, err = InsertRecordWithResponseID(db, ctx.ID, ModelResp, "test response", true, &responseID)
assert.NoError(t, err)
// Update context with last response ID
err = UpdateContextLastResponseID(db, ctx.ID, responseID)
assert.NoError(t, err)
// Add a tool output - this should break server-side threading
_, err = InsertRecord(db, ctx.ID, ToolOutput, "tool output", true)
assert.NoError(t, err)
// Validate chain - should be invalid (tool calls present)
valid, reason := ValidateResponseIDChain(db, ctx)
assert.False(t, valid)
assert.Equal(t, "tool calls present (break server-side threading)", reason)
}
func TestValidateResponseIDChain_MixedResponseIDState(t *testing.T) {
path := filepath.Join(t.TempDir(), "cw.db")
db, err := NewContextDB(path)
assert.NoError(t, err)
defer db.Close()
// Create context with threading enabled
ctx, err := CreateContextWithThreading(db, "test-context", true)
assert.NoError(t, err)
// Add a prompt
_, err = InsertRecord(db, ctx.ID, Prompt, "test prompt", true)
assert.NoError(t, err)
// Add a model response with response_id
responseID1 := "resp-123"
_, err = InsertRecordWithResponseID(db, ctx.ID, ModelResp, "response 1", true, &responseID1)
assert.NoError(t, err)
// Add a model response WITHOUT response_id (mixed state)
_, err = InsertRecord(db, ctx.ID, ModelResp, "response 2", true)
assert.NoError(t, err)
// Update context with last response ID
err = UpdateContextLastResponseID(db, ctx.ID, responseID1)
assert.NoError(t, err)
// Validate chain - should be invalid (mixed state)
valid, reason := ValidateResponseIDChain(db, ctx)
assert.False(t, valid)
assert.Equal(t, "mixed response_id state (some records missing IDs)", reason)
}
func TestValidateResponseIDChain_LastResponseIDMismatch(t *testing.T) {
path := filepath.Join(t.TempDir(), "cw.db")
db, err := NewContextDB(path)
assert.NoError(t, err)
defer db.Close()
// Create context with threading enabled
ctx, err := CreateContextWithThreading(db, "test-context", true)
assert.NoError(t, err)
// Add a prompt
_, err = InsertRecord(db, ctx.ID, Prompt, "test prompt", true)
assert.NoError(t, err)
// Add a model response with response_id
responseID1 := "resp-123"
_, err = InsertRecordWithResponseID(db, ctx.ID, ModelResp, "response 1", true, &responseID1)
assert.NoError(t, err)
// Add another model response with different response_id
responseID2 := "resp-456"
_, err = InsertRecordWithResponseID(db, ctx.ID, ModelResp, "response 2", true, &responseID2)
assert.NoError(t, err)
// Update context with a different response ID that doesn't exist in records
// This is more serious than a mismatch - the ID doesn't exist at all
wrongResponseID := "resp-wrong"
err = UpdateContextLastResponseID(db, ctx.ID, wrongResponseID)
assert.NoError(t, err)
// Validate chain - should be invalid (LastResponseID doesn't exist in records)
valid, reason := ValidateResponseIDChain(db, ctx)
assert.False(t, valid)
assert.Contains(t, reason, "does not exist in records")
assert.Contains(t, reason, "export/import")
}
func TestValidateResponseIDChain_LastResponseIDMismatchWithExistingID(t *testing.T) {
path := filepath.Join(t.TempDir(), "cw.db")
db, err := NewContextDB(path)
assert.NoError(t, err)
defer db.Close()
// Create context with threading enabled
ctx, err := CreateContextWithThreading(db, "test-context", true)
assert.NoError(t, err)
// Add a prompt
_, err = InsertRecord(db, ctx.ID, Prompt, "test prompt", true)
assert.NoError(t, err)
// Add a model response with response_id
responseID1 := "resp-123"
_, err = InsertRecordWithResponseID(db, ctx.ID, ModelResp, "response 1", true, &responseID1)
assert.NoError(t, err)
// Add another model response with different response_id (this is the last one)
responseID2 := "resp-456"
_, err = InsertRecordWithResponseID(db, ctx.ID, ModelResp, "response 2", true, &responseID2)
assert.NoError(t, err)
// Update context with responseID1 (exists but doesn't match the last response)
err = UpdateContextLastResponseID(db, ctx.ID, responseID1)
assert.NoError(t, err)
// Validate chain - should be invalid (LastResponseID doesn't match last response)
valid, reason := ValidateResponseIDChain(db, ctx)
assert.False(t, valid)
assert.Contains(t, reason, "does not match context")
}
func TestValidateResponseIDChain_EmptyContext(t *testing.T) {
path := filepath.Join(t.TempDir(), "cw.db")
db, err := NewContextDB(path)
assert.NoError(t, err)
defer db.Close()
// Create context with threading enabled
ctx, err := CreateContextWithThreading(db, "test-context", true)
assert.NoError(t, err)
// Set a LastResponseID even though there are no records
responseID := "resp-123"
err = UpdateContextLastResponseID(db, ctx.ID, responseID)
assert.NoError(t, err)
// Validate chain - should be valid (no model responses yet, first call)
valid, reason := ValidateResponseIDChain(db, ctx)
assert.True(t, valid)
assert.Equal(t, "no model responses yet (first call)", reason)
}
func TestValidateResponseIDChain_NoModelResponsesButHasPrompt(t *testing.T) {
path := filepath.Join(t.TempDir(), "cw.db")
db, err := NewContextDB(path)
assert.NoError(t, err)
defer db.Close()
// Create context with threading enabled
ctx, err := CreateContextWithThreading(db, "test-context", true)
assert.NoError(t, err)
// Add a prompt but no model responses
_, err = InsertRecord(db, ctx.ID, Prompt, "test prompt", true)
assert.NoError(t, err)
// Set a LastResponseID
responseID := "resp-123"
err = UpdateContextLastResponseID(db, ctx.ID, responseID)
assert.NoError(t, err)
// Validate chain - should be valid (no model responses yet, first call)
valid, reason := ValidateResponseIDChain(db, ctx)
assert.True(t, valid)
assert.Equal(t, "no model responses yet (first call)", reason)
}
func TestValidateResponseIDChain_MultipleValidResponses(t *testing.T) {
path := filepath.Join(t.TempDir(), "cw.db")
db, err := NewContextDB(path)
assert.NoError(t, err)
defer db.Close()
// Create context with threading enabled
ctx, err := CreateContextWithThreading(db, "test-context", true)
assert.NoError(t, err)
// Add a prompt
_, err = InsertRecord(db, ctx.ID, Prompt, "test prompt", true)
assert.NoError(t, err)
// Add multiple model responses with response_ids
responseID1 := "resp-123"
_, err = InsertRecordWithResponseID(db, ctx.ID, ModelResp, "response 1", true, &responseID1)
assert.NoError(t, err)
responseID2 := "resp-456"
_, err = InsertRecordWithResponseID(db, ctx.ID, ModelResp, "response 2", true, &responseID2)
assert.NoError(t, err)
// Update context with the last response ID
err = UpdateContextLastResponseID(db, ctx.ID, responseID2)
assert.NoError(t, err)
// Validate chain - should be valid (last response matches)
valid, reason := ValidateResponseIDChain(db, ctx)
assert.True(t, valid)
assert.Equal(t, "chain valid", reason)
}
func TestGetLastResponseID(t *testing.T) {
path := filepath.Join(t.TempDir(), "cw.db")
db, err := NewContextDB(path)
assert.NoError(t, err)
defer db.Close()
// Create context
ctx, err := CreateContextWithThreading(db, "test-context", true)
assert.NoError(t, err)
// Add records
_, err = InsertRecord(db, ctx.ID, Prompt, "prompt", true)
assert.NoError(t, err)
responseID1 := "resp-1"
_, err = InsertRecordWithResponseID(db, ctx.ID, ModelResp, "response 1", true, &responseID1)
assert.NoError(t, err)
responseID2 := "resp-2"
_, err = InsertRecordWithResponseID(db, ctx.ID, ModelResp, "response 2", true, &responseID2)
assert.NoError(t, err)
// Get records
records, err := ListLiveRecords(db, ctx.ID)
assert.NoError(t, err)
// Test getLastResponseID helper
lastID := getLastResponseID(records)
assert.NotNil(t, lastID)
assert.Equal(t, responseID2, *lastID)
}
func TestGetLastResponseID_NoResponseIDs(t *testing.T) {
path := filepath.Join(t.TempDir(), "cw.db")
db, err := NewContextDB(path)
assert.NoError(t, err)
defer db.Close()
// Create context
ctx, err := CreateContextWithThreading(db, "test-context", true)
assert.NoError(t, err)
// Add records without response IDs
_, err = InsertRecord(db, ctx.ID, Prompt, "prompt", true)
assert.NoError(t, err)
_, err = InsertRecord(db, ctx.ID, ModelResp, "response", true)
assert.NoError(t, err)
// Get records
records, err := ListLiveRecords(db, ctx.ID)
assert.NoError(t, err)
// Test getLastResponseID helper - should return nil
lastID := getLastResponseID(records)
assert.Nil(t, lastID)
}
// TestValidateResponseIDChain_LastResponseIDNoMatchingRecord tests the edge case
// where a context has a LastResponseID but no matching record exists in the database.
// This can happen after export/import or manual database edits.
func TestValidateResponseIDChain_LastResponseIDNoMatchingRecord(t *testing.T) {
path := filepath.Join(t.TempDir(), "cw.db")
db, err := NewContextDB(path)
assert.NoError(t, err)
defer db.Close()
// Create context with threading enabled
ctx, err := CreateContextWithThreading(db, "test-context", true)
assert.NoError(t, err)
// Add a prompt
_, err = InsertRecord(db, ctx.ID, Prompt, "test prompt", true)
assert.NoError(t, err)
// Add a model response with a different response_id
responseID1 := "resp-123"
_, err = InsertRecordWithResponseID(db, ctx.ID, ModelResp, "response 1", true, &responseID1)
assert.NoError(t, err)
// Set a LastResponseID that doesn't match any record (simulating export/import scenario)
wrongResponseID := "resp-nonexistent-999"
err = UpdateContextLastResponseID(db, ctx.ID, wrongResponseID)
assert.NoError(t, err)
// Validate chain - should be invalid (LastResponseID doesn't exist in records)
valid, _ := ValidateResponseIDChain(db, ctx)
assert.False(t, valid)
}