返回 DeepSeek-Reasonix
session_purge_restart_test.go
根目录 / desktop / session_purge_restart_test.go
1 package main
2
3 import (
4 "context"
5 "encoding/json"
6 "errors"
7 "os"
8 "os/exec"
9 "path/filepath"
10 "testing"
11 "time"
12
13 "reasonix/desktop/internal/workspacestate"
14 )
15
16 func TestPurgeCommandCrashRestart(t *testing.T) {
17 for _, phase := range []string{"before-tombstone", "after-tombstone", "after-file-cleanup", "after-content-removed", "after-purge-committed", "before-command-result", "after-command-result"} {
18 t.Run(phase, func(t *testing.T) {
19 a, ref := lifecycleFixture(t)
20 if err := a.ArchiveCanonicalSession(ref); err != nil {
21 t.Fatal(err)
22 }
23 req := lifecycleRequest(t, a, ref, "crash-purge", "purge")
24 body, err := json.Marshal(req)
25 if err != nil {
26 t.Fatal(err)
27 }
28 root, registry := a.desktopSessions.root, a.workspaceRegistry().Path()
29 a.closeSessionServices()
30 run := func(point string, crash bool) {
31 ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
32 defer cancel()
33 cmd := exec.CommandContext(ctx, os.Args[0], "-test.run=^TestPurgeCommandCrashHelper$")
34 cmd.Env = append(os.Environ(), "REASONIX_PURGE_APP_ROOT="+root, "REASONIX_PURGE_APP_REGISTRY="+registry, "REASONIX_PURGE_APP_REQUEST="+string(body), "REASONIX_PURGE_APP_POINT="+point)
35 output, err := cmd.CombinedOutput()
36 var exit *exec.ExitError
37 if crash {
38 if !errors.As(err, &exit) || exit.ExitCode() != 23 {
39 t.Fatalf("checkpoint not reached: %v %s", err, output)
40 }
41 } else if err != nil {
42 t.Fatalf("restart: %v %s", err, output)
43 }
44 }
45 run(phase, true)
46 store := workspacestate.NewStore(registry)
47 state, err := store.Load(t.Context())
48 if err != nil {
49 t.Fatal(err)
50 }
51 if phase == "before-tombstone" {
52 if state.SessionStates[ref.SessionID].Lifecycle != workspacestate.Archived {
53 t.Fatal("precommit lifecycle changed")
54 }
55 if _, err := os.Stat(filepath.Join(root, ref.SessionID)); err != nil {
56 t.Fatalf("precommit content lost: %v", err)
57 }
58 } else {
59 if state.SessionStates[ref.SessionID].Lifecycle != workspacestate.Deleted {
60 t.Fatal("tombstone lost")
61 }
62 if err := store.RestoreSession(t.Context(), ref.SessionID); !errors.Is(err, workspacestate.ErrMutationConflict) {
63 t.Fatalf("deleted session restored: %v", err)
64 }
65 }
66 run("", false)
67 first, err := os.ReadFile(registry)
68 if err != nil {
69 t.Fatal(err)
70 }
71 run("", false)
72 second, err := os.ReadFile(registry)
73 if err != nil || string(first) != string(second) {
74 t.Fatalf("repeated replay rewrote registry: %v", err)
75 }
76 state, err = store.Load(t.Context())
77 if err != nil {
78 t.Fatal(err)
79 }
80 if workspacestate.ClassifyPurge(state, ref.SessionID) != workspacestate.PurgeCommitted || state.PendingOperations["command-"+req.OperationID].Phase != "committed" {
81 t.Fatal("incomplete receipts")
82 }
83 if _, err := os.Stat(filepath.Join(root, ref.SessionID)); !os.IsNotExist(err) {
84 t.Fatalf("body survived deletion: %v", err)
85 }
86 })
87 }
88 }
89
90 func TestPurgeCommandCrashHelper(t *testing.T) {
91 root := os.Getenv("REASONIX_PURGE_APP_ROOT")
92 if root == "" {
93 return
94 }
95 a := NewApp()
96 a.ctx = t.Context()
97 a.desktopSessions.root = root
98 a.desktopSessions.workspaceState = workspacestate.NewStore(os.Getenv("REASONIX_PURGE_APP_REGISTRY"))
99 installNoopRuntimeEvents(a)
100 defer a.closeSessionServices()
101 var req SessionLifecycleRequest
102 if err := json.Unmarshal([]byte(os.Getenv("REASONIX_PURGE_APP_REQUEST")), &req); err != nil {
103 t.Fatal(err)
104 }
105 point := os.Getenv("REASONIX_PURGE_APP_POINT")
106 if point != "" {
107 a.lifecycleCheckpointHook = func(phase string) {
108 if phase == point {
109 os.Exit(23)
110 }
111 }
112 } else {
113 if err := a.recoverDesktopSessionOperations(t.Context()); err != nil {
114 t.Fatal(err)
115 }
116 }
117 result, err := a.ApplySessionLifecycle(req)
118 if err != nil || !result.Committed {
119 t.Fatalf("result=%+v err=%v", result, err)
120 }
121 }
122
122 lines GO