返回 DeepSeek-Reasonix
checkpoint_test.go
根目录 / internal / transcript / checkpoint_test.go
1 package transcript
2
3 import (
4 "slices"
5 "testing"
6
7 "reasonix/internal/provider"
8 )
9
10 func TestRestoreCheckpointRepairsLegacyMissingRecordIdentities(t *testing.T) {
11 state := Checkpoint{
12 Version: ProtocolVersion, Identity: testIdentity, CoveredThroughSeq: 7,
13 Records: []Message{
14 {Role: "notice", Content: "first"},
15 {Role: "notice", Content: "second"},
16 {Role: "assistant", MessageID: "answer", Content: "answer"},
17 {Role: "tool", ToolCallID: "call", Content: "result"},
18 },
19 }
20 want := []string{"view:checkpoint:7:0", "view:checkpoint:7:1", "m:answer", "tool:call"}
21 for range 2 {
22 projection, err := RestoreCheckpoint(state, testIdentity)
23 if err != nil {
24 t.Fatal(err)
25 }
26 messages := projection.buffer.Messages()
27 got := make([]string, len(messages))
28 for index := range messages {
29 got[index] = messages[index].RecordID
30 }
31 if !slices.Equal(got, want) {
32 t.Fatalf("repaired identities = %v, want %v", got, want)
33 }
34 }
35 }
36
37 func TestRestoreCheckpointStillRejectsNonEmptyDuplicateIdentity(t *testing.T) {
38 state := Checkpoint{Version: ProtocolVersion, Identity: testIdentity, Records: []Message{
39 {RecordID: "duplicate", Role: "notice", Content: "first"},
40 {RecordID: "duplicate", Role: "notice", Content: "second"},
41 }}
42 if _, err := RestoreCheckpoint(state, testIdentity); err == nil {
43 t.Fatal("duplicate checkpoint identity was accepted")
44 }
45 }
46
47 func TestRepairCheckpointToolResultsRestoresUniqueFormalIdentityAndMetadata(t *testing.T) {
48 execution := &provider.ToolExecution{Kind: "shell", State: "completed", DurationMs: 42}
49 records := []Message{{RecordID: "tool:call-1", Role: "tool", ToolCallID: "call-1", ToolName: "PowerShell",
50 Content: "event display", ToolResultError: "", HistoryTurn: 1}}
51 canonical := []Message{
52 {RecordID: "m:user-1", MessageID: "user-1", Role: "user"},
53 {RecordID: "tool:call-1", MessageID: "result-1", Role: "tool", ToolCallID: "call-1", ToolName: "PowerShell",
54 Content: "canonical body", CreatedAt: 123, Execution: execution, ToolResultArchived: true},
55 }
56
57 got, stats := RepairCheckpointToolResults(records, canonical)
58 if stats.Repaired != 1 || stats.Missing != 0 || stats.Conflicts != 0 {
59 t.Fatalf("repair stats = %+v", stats)
60 }
61 if len(got) != 1 || got[0].MessageID != "result-1" || got[0].RecordID != "tool:call-1" || got[0].Content != "event display" ||
62 got[0].HistoryTurn != 1 || got[0].CreatedAt != 123 || got[0].Execution != execution || !got[0].ToolResultArchived {
63 t.Fatalf("repaired checkpoint row = %+v", got)
64 }
65 if records[0].MessageID != "" {
66 t.Fatal("repair mutated the caller-owned checkpoint slice")
67 }
68 }
69
70 func TestRepairCheckpointToolResultsRefusesAmbiguousOrConflictingIdentity(t *testing.T) {
71 records := []Message{
72 {RecordID: "tool:ambiguous", Role: "tool", ToolCallID: "ambiguous", HistoryTurn: 1},
73 {RecordID: "tool:wrong-turn", Role: "tool", ToolCallID: "wrong-turn", HistoryTurn: 2},
74 }
75 canonical := []Message{
76 {MessageID: "user-1", Role: "user"},
77 {MessageID: "a", Role: "tool", ToolCallID: "ambiguous"},
78 {MessageID: "b", Role: "tool", ToolCallID: "ambiguous"},
79 {MessageID: "c", Role: "tool", ToolCallID: "wrong-turn"},
80 }
81
82 got, stats := RepairCheckpointToolResults(records, canonical)
83 if stats.Repaired != 0 || stats.Conflicts != 1 || stats.Missing != 1 {
84 t.Fatalf("repair stats = %+v", stats)
85 }
86 if got[0].MessageID != "" || got[1].MessageID != "" {
87 t.Fatalf("inconclusive rows were guessed: %+v", got)
88 }
89 }
90
91 func TestRepairCheckpointToolResultsDoesNotReuseOccupiedFormalIdentity(t *testing.T) {
92 records := []Message{
93 {RecordID: "m:result", MessageID: "result", Role: "tool", ToolCallID: "call"},
94 {RecordID: "tool:call", Role: "tool", ToolCallID: "call"},
95 }
96 canonical := []Message{{RecordID: "m:result", MessageID: "result", Role: "tool", ToolCallID: "call"}}
97 got, stats := RepairCheckpointToolResults(records, canonical)
98 if stats.Repaired != 0 || stats.Conflicts != 1 || got[1].MessageID != "" {
99 t.Fatalf("occupied formal identity was reused: stats=%+v rows=%+v", stats, got)
100 }
101 if !NeedsToolResultRepair(records) || NeedsToolResultRepair(records[:1]) {
102 t.Fatal("repair fast-path predicate does not match missing tool identities")
103 }
104 }
105
105 lines GO