返回 DeepSeek-Reasonix
session_purge_admission_test.go
根目录 / desktop / session_purge_admission_test.go
1 package main
2
3 import (
4 "encoding/json"
5 "errors"
6 "testing"
7
8 "reasonix/desktop/internal/workspacestate"
9 "reasonix/internal/session"
10 )
11
12 func TestPurgeCommandUnrelatedSessionMustNotBlockPurge(t *testing.T) {
13 a, victim := lifecycleFixture(t)
14 other := addLifecycleFixtureSession(t, a, "review-other")
15 if err := a.ArchiveCanonicalSession(victim); err != nil {
16 t.Fatal(err)
17 }
18 req := lifecycleRequest(t, a, victim, "review-unrelated", "purge")
19 if err := a.workspaceRegistry().ArchiveSession(t.Context(), other.SessionID); err != nil {
20 t.Fatal(err)
21 }
22 result, err := a.ApplySessionLifecycle(req)
23 if err != nil || !result.Committed {
24 t.Fatalf("unrelated session blocked purge: result=%+v err=%v", result, err)
25 }
26 }
27
28 func TestPurgeCommandValidPreparedAfterStartupWriterReleased(t *testing.T) {
29 a, ref := lifecycleFixture(t)
30 store := a.workspaceRegistry()
31 if err := a.ArchiveCanonicalSession(ref); err != nil {
32 t.Fatal(err)
33 }
34 state, err := store.Load(t.Context())
35 if err != nil {
36 t.Fatal(err)
37 }
38 legacy := workspacestate.Operation{ID: "purge-" + ref.SessionID, Kind: "purge", Phase: "prepared", Lifecycle: workspacestate.Deleted, SessionIDs: []string{ref.SessionID}, ExpectedGeneration: state.SessionStates[ref.SessionID].Generation}
39 rewriteLifecycleRegistry(t, store, func(s *workspacestate.State) { s.PendingOperations[legacy.ID] = legacy })
40 fs := session.NewFilesystemPersistence(a.desktopSessions.root)
41 release, err := fs.AcquireMaintenance(ref.SessionID)
42 if err != nil {
43 t.Fatal(err)
44 }
45 replayErr := a.recoverDesktopSessionOperations(t.Context())
46 release()
47 if replayErr == nil {
48 t.Fatal("expected startup writer conflict")
49 }
50 page, err := a.ListTrashEntries("", "", 50)
51 if err != nil || len(page.Items) != 1 || !page.Items[0].CanPurge {
52 t.Fatalf("page=%+v err=%v", page, err)
53 }
54 req := lifecycleRequest(t, a, ref, "resume-prepared", "purge")
55 for range 2 {
56 result, err := a.ApplySessionLifecycle(req)
57 if err != nil || !result.Committed {
58 t.Errorf("fresh delete after writer released: result=%+v err=%v", result, err)
59 }
60 }
61 page, err = a.ListTrashEntries("", "", 50)
62 if err != nil || len(page.Items) != 0 {
63 t.Fatalf("completed purge still in trash: %+v %v", page, err)
64 }
65 }
66
67 func TestPurgeCommandSnapshotConflictIsTerminal(t *testing.T) {
68 a, ref := lifecycleFixture(t)
69 if err := a.ArchiveCanonicalSession(ref); err != nil {
70 t.Fatal(err)
71 }
72 req := lifecycleRequest(t, a, ref, "old-confirmation", "purge")
73 store := a.workspaceRegistry()
74 if err := store.RestoreSession(t.Context(), ref.SessionID); err != nil {
75 t.Fatal(err)
76 }
77 if err := store.ArchiveSession(t.Context(), ref.SessionID); err != nil {
78 t.Fatal(err)
79 }
80 first, err := a.ApplySessionLifecycle(req)
81 if err != nil || len(first.Items) != 1 || first.Items[0].ErrorCode != "state_conflict" || first.Items[0].Retryable {
82 t.Fatalf("result=%+v err=%v", first, err)
83 }
84 second, err := a.ApplySessionLifecycle(req)
85 b1, _ := json.Marshal(first)
86 b2, _ := json.Marshal(second)
87 if err != nil || string(b1) != string(b2) {
88 t.Fatalf("terminal result changed: %s %s %v", b1, b2, err)
89 }
90 if history, err := a.desktopSessionService("").Query().History(t.Context(), ref); err != nil || len(history) == 0 {
91 t.Fatalf("lost history: %v", err)
92 }
93 for _, change := range []func(*SessionLifecycleRequest){
94 func(r *SessionLifecycleRequest) { r.ExpectedGeneration++ },
95 func(r *SessionLifecycleRequest) { r.Action = "restore" },
96 func(r *SessionLifecycleRequest) {
97 r.Targets = []SessionLifecycleTarget{{Ref: &session.SessionRef{HostID: localDesktopHostID, SessionID: "other"}}}
98 },
99 } {
100 changed := req
101 change(&changed)
102 if _, err := a.ApplySessionLifecycle(changed); !errors.Is(err, workspacestate.ErrMutationConflict) {
103 t.Fatalf("changed request accepted: %v", err)
104 }
105 }
106 }
107
108 func TestPurgeCommandFirstDeliverySeparatesChangedTargets(t *testing.T) {
109 a, unchanged := lifecycleFixture(t)
110 changed := addLifecycleFixtureSession(t, a, "changed-target")
111 for _, ref := range []session.SessionRef{unchanged, changed} {
112 if err := a.ArchiveCanonicalSession(ref); err != nil {
113 t.Fatal(err)
114 }
115 }
116 req := lifecycleRequest(t, a, unchanged, "mixed-first-delivery", "purge")
117 req.Targets = append(req.Targets, SessionLifecycleTarget{Ref: &changed})
118 if err := a.workspaceRegistry().RestoreSession(t.Context(), changed.SessionID); err != nil {
119 t.Fatal(err)
120 }
121 result, err := a.ApplySessionLifecycle(req)
122 if err != nil || len(result.Items) != 2 || !result.Items[0].Committed || result.Items[1].ErrorCode != "state_conflict" || result.Items[1].Retryable {
123 t.Fatalf("mixed admission result=%+v err=%v", result, err)
124 }
125 if history, err := a.desktopSessionService("").Query().History(t.Context(), changed); err != nil || len(history) == 0 {
126 t.Fatalf("changed target lost body: %v", err)
127 }
128 }
129
130 func TestPurgeCommandRestoreWhileWaitingForRuntimeLock(t *testing.T) {
131 a, ref := lifecycleFixture(t)
132 if err := a.ArchiveCanonicalSession(ref); err != nil {
133 t.Fatal(err)
134 }
135 req := lifecycleRequest(t, a, ref, "restore-before-runtime-lock", "purge")
136 a.runtimeMutationBeforeLockHook = func(operation string) {
137 if operation == "purge lifecycle command" {
138 if err := a.workspaceRegistry().RestoreSession(t.Context(), ref.SessionID); err != nil {
139 t.Fatal(err)
140 }
141 }
142 }
143 result, err := a.ApplySessionLifecycle(req)
144 if err != nil || len(result.Items) != 1 || result.Items[0].ErrorCode != "state_conflict" || result.Items[0].Retryable {
145 t.Fatalf("late conflict=%+v err=%v", result, err)
146 }
147 if history, err := a.desktopSessionService("").Query().History(t.Context(), ref); err != nil || len(history) == 0 {
148 t.Fatalf("restore lost body: %v", err)
149 }
150 }
151
151 lines GO