返回 DeepSeek-Reasonix
recovery_lifecycle_diagnostic_test.go
根目录 / desktop / recovery_lifecycle_diagnostic_test.go
1 package main
2
3 import (
4 "bufio"
5 "context"
6 "encoding/json"
7 "os"
8 "path/filepath"
9 "strings"
10 "testing"
11 "time"
12
13 "reasonix/internal/agent"
14 "reasonix/internal/provider"
15 "reasonix/internal/sessioncatalog"
16 "reasonix/internal/store"
17 )
18
19 func recoveryLifecycleOutcomes(t *testing.T, sessionPaths ...string) []string {
20 t.Helper()
21 out := []string{}
22 for _, sessionPath := range sessionPaths {
23 file, err := os.Open(store.SessionConflictLog(sessionPath))
24 if os.IsNotExist(err) {
25 continue
26 }
27 if err != nil {
28 t.Fatal(err)
29 }
30 scanner := bufio.NewScanner(file)
31 for scanner.Scan() {
32 var record struct {
33 Outcome string `json:"outcome"`
34 }
35 if err := json.Unmarshal(scanner.Bytes(), &record); err != nil {
36 _ = file.Close()
37 t.Fatal(err)
38 }
39 out = append(out, record.Outcome)
40 }
41 if err := scanner.Err(); err != nil {
42 _ = file.Close()
43 t.Fatal(err)
44 }
45 if err := file.Close(); err != nil {
46 t.Fatal(err)
47 }
48 }
49 return out
50 }
51
52 func TestGetRecoveryLineagePersistsRequestedFinalClassification(t *testing.T) {
53 dir := schemaOneTempDir(t)
54 root := filepath.Join(dir, "private-root.jsonl")
55 branch := filepath.Join(dir, "private-branch.jsonl")
56 save := func(path, unique string) {
57 t.Helper()
58 session := agent.NewSession("system")
59 session.Add(provider.Message{Role: provider.RoleUser, Content: "shared"})
60 session.Add(provider.Message{Role: provider.RoleAssistant, Content: "answer"})
61 session.Add(provider.Message{Role: provider.RoleUser, Content: unique})
62 if err := session.Save(path); err != nil {
63 t.Fatal(err)
64 }
65 }
66 save(root, "private root content")
67 save(branch, "private branch content")
68 now := time.Now()
69 for path, meta := range map[string]agent.BranchMeta{
70 root: {ID: "private-root", Scope: "global", TopicID: "private-topic", TopicTitle: "private title", CreatedAt: now, UpdatedAt: now},
71 branch: {ID: "private-branch", Scope: "global", TopicID: "private-topic", TopicTitle: "private title", Recovered: true, ParentID: "private-root", RecoveryDepth: 1, CreatedAt: now, UpdatedAt: now},
72 } {
73 if err := agent.SaveBranchMetaPreserveUpdated(path, meta); err != nil {
74 t.Fatal(err)
75 }
76 }
77 catalog, err := sessioncatalog.Open(context.Background(), sessioncatalog.Options{InMemory: true, DisableRepair: true})
78 if err != nil {
79 t.Fatal(err)
80 }
81 t.Cleanup(func() { _ = catalog.Close(context.Background()) })
82 if err := catalog.ReconcileDirectory(context.Background(), sessioncatalog.DirectoryTarget{Path: dir, Scope: "global"}); err != nil {
83 t.Fatal(err)
84 }
85 app := &App{tabs: map[string]*WorkspaceTab{}, detachedSessions: map[string]*WorkspaceTab{}}
86 app.sessionCatalog.Store(catalog)
87
88 plain := app.GetRecoveryLineage(ProjectTopicKey{Scope: "global", TopicID: "private-topic", Path: branch})
89 if plain.State != "diverged" {
90 t.Fatalf("plain classification = %+v", plain)
91 }
92 if _, err := os.Stat(store.SessionConflictLog(branch)); !os.IsNotExist(err) {
93 t.Fatalf("ordinary lineage read wrote diagnostics: %v", err)
94 }
95 view := app.GetRecoveryLineage(ProjectTopicKey{
96 Scope: "global", TopicID: "private-topic", Path: branch, RecordClassification: true,
97 })
98 if view.State != "diverged" {
99 t.Fatalf("recorded classification = %+v", view)
100 }
101 raw, err := os.ReadFile(store.SessionConflictLog(branch))
102 if err != nil {
103 t.Fatal(err)
104 }
105 if !strings.Contains(string(raw), `"outcome":"classified_diverged"`) {
106 t.Fatalf("classification diagnostic = %s", raw)
107 }
108 for _, secret := range []string{dir, "private-topic", "private title", "private root content", "private branch content"} {
109 if strings.Contains(string(raw), secret) {
110 t.Fatalf("classification diagnostic leaked %q: %s", secret, raw)
111 }
112 }
113 }
114
115 func TestRecoveryCopySweepPersistsCleanupOutcomes(t *testing.T) {
116 isolateDesktopUserDirs(t)
117 dir := desktopSessionDir(globalTabWorkspaceRoot())
118 if err := os.MkdirAll(dir, 0o755); err != nil {
119 t.Fatal(err)
120 }
121 parent, copies := forkThreeIdenticalRecoveryCopies(t, dir, "diagnostics")
122 catalog := openSweepTestCatalog(t, dir)
123 app := sweepTestApp(catalog)
124 app.tabs["open"] = &WorkspaceTab{ID: "open", Scope: "global", SessionPath: copies[1], Ready: true}
125
126 if moved := app.sweepExcessRecoveryCopiesIn(catalog, sessioncatalog.DirectoryTarget{Path: dir, Scope: "global"}, time.Now(), 0); moved != 1 {
127 t.Fatalf("moved = %d, want one moved and one in-use skip", moved)
128 }
129 outcomes := strings.Join(recoveryLifecycleOutcomes(t, append([]string{parent}, copies...)...), ",")
130 for _, want := range []string{"cleanup_kept", "cleanup_skipped_in_use", "cleanup_moved"} {
131 if !strings.Contains(outcomes, want) {
132 t.Fatalf("cleanup outcomes %q omit %q", outcomes, want)
133 }
134 }
135 }
136
137 func TestRecoveryCopySweepRecordsFailedRevalidation(t *testing.T) {
138 isolateDesktopUserDirs(t)
139 dir := desktopSessionDir(globalTabWorkspaceRoot())
140 if err := os.MkdirAll(dir, 0o755); err != nil {
141 t.Fatal(err)
142 }
143 parent, copies := forkThreeIdenticalRecoveryCopies(t, dir, "revalidation")
144 catalog := openSweepTestCatalog(t, dir)
145 changed, err := agent.LoadSession(copies[0])
146 if err != nil {
147 t.Fatal(err)
148 }
149 changed.Add(provider.Message{Role: provider.RoleUser, Content: "new unique content after catalog classification"})
150 if err := changed.SaveRewrite(copies[0]); err != nil {
151 t.Fatal(err)
152 }
153 app := sweepTestApp(catalog)
154 app.sweepExcessRecoveryCopiesIn(catalog, sessioncatalog.DirectoryTarget{Path: dir, Scope: "global"}, time.Now(), 0)
155
156 outcomes := strings.Join(recoveryLifecycleOutcomes(t, append([]string{parent}, copies...)...), ",")
157 if !strings.Contains(outcomes, "cleanup_revalidation_failed") {
158 t.Fatalf("cleanup outcomes %q omit failed revalidation", outcomes)
159 }
160 if _, err := os.Stat(copies[0]); err != nil {
161 t.Fatalf("diverged copy was moved despite failed revalidation: %v", err)
162 }
163 }
164
164 lines GO