返回 DeepSeek-Reasonix
topic_archive_runtime_bounded_test.go
根目录 / desktop / topic_archive_runtime_bounded_test.go
1 package main
2
3 import (
4 "errors"
5 "fmt"
6 "testing"
7 "time"
8
9 "reasonix/internal/agent"
10 "reasonix/internal/identitylock"
11 "reasonix/internal/session"
12 )
13
14 func TestArchiveRuntimeMutationOutlastsBriefBackgroundHolders(t *testing.T) {
15 a := &App{}
16 // A background owner that keeps re-taking the lock for short slices is
17 // what a single TryLock loses to on every attempt.
18 stop := make(chan struct{})
19 held := make(chan struct{})
20 go func() {
21 first := true
22 for {
23 select {
24 case <-stop:
25 return
26 default:
27 }
28 a.runtimeRebuildMu.Lock()
29 if first {
30 close(held)
31 first = false
32 time.Sleep(80 * time.Millisecond)
33 }
34 time.Sleep(20 * time.Millisecond)
35 a.runtimeRebuildMu.Unlock()
36 time.Sleep(30 * time.Millisecond)
37 }
38 }()
39 defer close(stop)
40 <-held
41 if _, ok := a.tryLockRuntimeMutation("probe"); ok {
42 t.Fatal("fixture did not hold the runtime mutation lock")
43 }
44 release, ok := a.tryLockRuntimeMutationBounded("archive session")
45 if !ok {
46 t.Fatal("user archive gave up while the holder only held brief slices")
47 }
48 release()
49 }
50
51 func TestArchiveRuntimeMutationStillRefusesAPersistentHolder(t *testing.T) {
52 a := &App{}
53 a.runtimeAdmissionMu.RLock()
54 defer a.runtimeAdmissionMu.RUnlock()
55 start := time.Now()
56 if _, ok := a.tryLockRuntimeMutationBounded("archive session"); ok {
57 t.Fatal("archive acquired the lock while a turn held admission")
58 }
59 if elapsed := time.Since(start); elapsed > runtimeMutationUserWait+time.Second {
60 t.Fatalf("bounded wait overran its budget: %v", elapsed)
61 }
62 // The failed attempt must leave both locks free for the next owner.
63 if !a.runtimeRebuildMu.TryLock() {
64 t.Fatal("refused archive leaked runtimeRebuildMu")
65 }
66 a.runtimeRebuildMu.Unlock()
67 }
68
69 func TestSessionBusyCauseKeepsTheHolderIdentity(t *testing.T) {
70 for _, tc := range []struct {
71 err error
72 want string
73 }{
74 {errTopicArchiveBusy, "runtime_mutation"},
75 {errTopicHasActiveWork, "active_work"},
76 {fmt.Errorf("start: %w", agent.ErrSessionLeaseHeld), "session_lease"},
77 {fmt.Errorf("maintenance: %w", session.ErrWriterOwned), "writer_owned"},
78 {fmt.Errorf("source: %w", identitylock.ErrHeld), "source_lock"},
79 {errHistoricalSourceBusy, "historical_source"},
80 } {
81 if got := sessionBusyCause(tc.err); got != tc.want {
82 t.Errorf("sessionBusyCause(%v) = %q, want %q", tc.err, got, tc.want)
83 }
84 }
85 }
86
87 // holdRuntimeMutationBriefly keeps runtimeRebuildMu for 100ms, longer than a
88 // single TryLock tolerates and well inside the bounded user wait.
89 func holdRuntimeMutationBriefly(t *testing.T, a *App) {
90 t.Helper()
91 a.runtimeRebuildMu.Lock()
92 timer := time.AfterFunc(100*time.Millisecond, a.runtimeRebuildMu.Unlock)
93 t.Cleanup(func() { timer.Stop() })
94 }
95
96 func TestTopicRemovalEntryPointsOutlastBriefRuntimeHolders(t *testing.T) {
97 t.Run("trash topic", func(t *testing.T) {
98 a := &App{}
99 a.sessionRemovalMu.Lock()
100 defer a.sessionRemovalMu.Unlock()
101 holdRuntimeMutationBriefly(t, a)
102 trace := topicArchiveTrace{}
103 if _, _, err := a.commitTopicArchive("topic", &trace); trace.phase != "removal_lock" {
104 t.Fatalf("trash gave up at the runtime lock: phase=%q err=%v", trace.phase, err)
105 }
106 })
107 t.Run("archive topic", func(t *testing.T) {
108 a := &App{}
109 holdRuntimeMutationBriefly(t, a)
110 if err := a.archiveCompatibleTopic(""); errors.Is(err, errTopicArchiveBusy) {
111 t.Fatal("archive gave up at the runtime lock")
112 }
113 })
114 t.Run("remove topic", func(t *testing.T) {
115 a := NewApp()
116 a.ctx = t.Context()
117 installNoopRuntimeEvents(a)
118 defer a.closeSessionServices()
119 holdRuntimeMutationBriefly(t, a)
120 out, _ := a.RemoveTopic(TopicRemovalRequest{OperationID: "op", ExpectedToken: "token", Target: TopicRemovalTarget{WorkspaceID: "ws", TopicID: "topic"}})
121 if out.ErrorCode == "busy" {
122 t.Fatal("remove topic gave up at the runtime lock")
123 }
124 })
125 }
126
126 lines GO