返回 DeepSeek-Reasonix
persist_test.go
根目录 / internal / recovery / persist_test.go
1 package recovery
2
3 import (
4 "encoding/json"
5 "fmt"
6 "os"
7 "path/filepath"
8 "runtime"
9 "sync"
10 "testing"
11 )
12
13 func TestTaskScopePersistenceIsBackwardCompatible(t *testing.T) {
14 const oldJSON = `{"tasks":{"root":{"phase":"diagnosing","failure":{"tool":"write_file","task_id":"root"},"consecutive_fails":3}}}`
15 var old Snapshot
16 if err := json.Unmarshal([]byte(oldJSON), &old); err != nil {
17 t.Fatalf("decode old snapshot: %v", err)
18 }
19 if got := old.Tasks["root"].Failure.TaskScopeID; got != "" {
20 t.Fatalf("old snapshot task scope = %q, want zero value", got)
21 }
22
23 newJSON, err := json.Marshal(Snapshot{Tasks: map[string]*TaskState{
24 "root": {
25 Phase: PhaseDiagnosing,
26 Failure: &FailureEvent{
27 Tool: "write_file", TaskID: "root", TaskScopeID: "goal:ship",
28 },
29 },
30 }})
31 if err != nil {
32 t.Fatalf("encode new snapshot: %v", err)
33 }
34 var legacy struct {
35 Tasks map[string]struct {
36 Phase Phase `json:"phase"`
37 Failure struct {
38 Tool string `json:"tool"`
39 TaskID string `json:"task_id,omitempty"`
40 } `json:"failure"`
41 } `json:"tasks"`
42 }
43 if err := json.Unmarshal(newJSON, &legacy); err != nil {
44 t.Fatalf("legacy reader rejected new snapshot: %v", err)
45 }
46 if got := legacy.Tasks["root"].Failure.Tool; got != "write_file" {
47 t.Fatalf("legacy reader lost known fields: %q", got)
48 }
49 }
50
51 func TestFailureClassPersistenceIsBackwardCompatible(t *testing.T) {
52 const oldJSON = `{"tasks":{"root":{"phase":"diagnosing","last_failure":{"tool":"bash","err_summary":"command timed out"}}}}`
53 var old Snapshot
54 if err := json.Unmarshal([]byte(oldJSON), &old); err != nil {
55 t.Fatalf("decode old snapshot: %v", err)
56 }
57 if got := old.Tasks["root"].LastFailure.Class; got != "" {
58 t.Fatalf("old snapshot failure class = %q, want zero value", got)
59 }
60
61 newJSON, err := json.Marshal(Snapshot{Tasks: map[string]*TaskState{
62 "root": {
63 Phase: PhaseDiagnosing,
64 LastFailure: &FailureEvent{
65 Class: FailureClassTransient, Tool: "bash", ErrSummary: "command timed out",
66 },
67 },
68 }})
69 if err != nil {
70 t.Fatalf("encode new snapshot: %v", err)
71 }
72 var legacy struct {
73 Tasks map[string]struct {
74 LastFailure struct {
75 Tool string `json:"tool"`
76 ErrSummary string `json:"err_summary,omitempty"`
77 } `json:"last_failure"`
78 } `json:"tasks"`
79 }
80 if err := json.Unmarshal(newJSON, &legacy); err != nil {
81 t.Fatalf("legacy reader rejected failure class: %v", err)
82 }
83 if got := legacy.Tasks["root"].LastFailure.Tool; got != "bash" {
84 t.Fatalf("legacy reader lost known failure fields: %q", got)
85 }
86 }
87
88 func TestSaveSnapshotIsAtomicAndOwnerOnly(t *testing.T) {
89 sessionPath := filepath.Join(t.TempDir(), "session.jsonl")
90 const writers = 24
91 var wg sync.WaitGroup
92 for i := 0; i < writers; i++ {
93 wg.Add(1)
94 go func(i int) {
95 defer wg.Done()
96 snap := Snapshot{Tasks: map[string]*TaskState{
97 "root": {Phase: PhaseDiagnosing, Failure: &FailureEvent{ErrSummary: fmt.Sprintf("failure-%d", i)}},
98 }}
99 if err := SaveSnapshot(sessionPath, snap); err != nil {
100 t.Errorf("SaveSnapshot(%d): %v", i, err)
101 }
102 }(i)
103 }
104 wg.Wait()
105
106 snap, err := LoadSnapshot(sessionPath)
107 if err != nil {
108 t.Fatalf("LoadSnapshot after concurrent writes: %v", err)
109 }
110 if st := snap.Tasks["root"]; st == nil || st.Failure == nil || st.Failure.ErrSummary == "" {
111 t.Fatalf("loaded snapshot = %+v", snap)
112 }
113 info, err := os.Stat(PathFor(sessionPath))
114 if err != nil {
115 t.Fatal(err)
116 }
117 // Windows reports synthetic permission bits for NTFS files; the requested
118 // mode is enforced by the inherited ACL rather than FileMode.Perm.
119 if got := info.Mode().Perm(); runtime.GOOS != "windows" && got != 0o600 {
120 t.Fatalf("recovery state permissions = %o, want 600", got)
121 }
122 }
123
123 lines GO