sensorium-mcp 2.17.27 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (224) hide show
  1. package/Install-Sensorium.ps1 +327 -0
  2. package/README.md +14 -0
  3. package/dist/config.d.ts +16 -1
  4. package/dist/config.d.ts.map +1 -1
  5. package/dist/config.js +39 -2
  6. package/dist/config.js.map +1 -1
  7. package/dist/daily-session.d.ts +2 -1
  8. package/dist/daily-session.d.ts.map +1 -1
  9. package/dist/daily-session.js +23 -26
  10. package/dist/daily-session.js.map +1 -1
  11. package/dist/dashboard/routes/settings.d.ts +4 -0
  12. package/dist/dashboard/routes/settings.d.ts.map +1 -1
  13. package/dist/dashboard/routes/settings.js +57 -1
  14. package/dist/dashboard/routes/settings.js.map +1 -1
  15. package/dist/dashboard/routes/threads.d.ts +1 -0
  16. package/dist/dashboard/routes/threads.d.ts.map +1 -1
  17. package/dist/dashboard/routes/threads.js +23 -27
  18. package/dist/dashboard/routes/threads.js.map +1 -1
  19. package/dist/dashboard/routes.d.ts.map +1 -1
  20. package/dist/dashboard/routes.js +7 -2
  21. package/dist/dashboard/routes.js.map +1 -1
  22. package/dist/dashboard/spa.html +11 -11
  23. package/dist/data/interfaces.d.ts +36 -0
  24. package/dist/data/interfaces.d.ts.map +1 -0
  25. package/dist/data/interfaces.js +2 -0
  26. package/dist/data/interfaces.js.map +1 -0
  27. package/dist/data/memory/bootstrap.d.ts +36 -16
  28. package/dist/data/memory/bootstrap.d.ts.map +1 -1
  29. package/dist/data/memory/bootstrap.js +71 -217
  30. package/dist/data/memory/bootstrap.js.map +1 -1
  31. package/dist/data/memory/consolidation.d.ts +35 -34
  32. package/dist/data/memory/consolidation.d.ts.map +1 -1
  33. package/dist/data/memory/consolidation.js +43 -555
  34. package/dist/data/memory/consolidation.js.map +1 -1
  35. package/dist/data/memory/index.d.ts +0 -1
  36. package/dist/data/memory/index.d.ts.map +1 -1
  37. package/dist/data/memory/index.js +0 -1
  38. package/dist/data/memory/index.js.map +1 -1
  39. package/dist/data/memory/migration-runner.d.ts +5 -0
  40. package/dist/data/memory/migration-runner.d.ts.map +1 -0
  41. package/dist/data/memory/migration-runner.js +403 -0
  42. package/dist/data/memory/migration-runner.js.map +1 -0
  43. package/dist/data/memory/reflection.js +1 -1
  44. package/dist/data/memory/schema-ddl.d.ts +4 -0
  45. package/dist/data/memory/schema-ddl.d.ts.map +1 -0
  46. package/dist/data/memory/schema-ddl.js +194 -0
  47. package/dist/data/memory/schema-ddl.js.map +1 -0
  48. package/dist/data/memory/schema-guard.d.ts +3 -0
  49. package/dist/data/memory/schema-guard.d.ts.map +1 -0
  50. package/dist/data/memory/schema-guard.js +184 -0
  51. package/dist/data/memory/schema-guard.js.map +1 -0
  52. package/dist/data/memory/schema.d.ts +2 -5
  53. package/dist/data/memory/schema.d.ts.map +1 -1
  54. package/dist/data/memory/schema.js +6 -834
  55. package/dist/data/memory/schema.js.map +1 -1
  56. package/dist/data/memory/semantic.d.ts +0 -1
  57. package/dist/data/memory/semantic.d.ts.map +1 -1
  58. package/dist/data/memory/semantic.js +2 -8
  59. package/dist/data/memory/semantic.js.map +1 -1
  60. package/dist/data/memory/synthesis.js +2 -2
  61. package/dist/data/memory/synthesis.js.map +1 -1
  62. package/dist/data/memory/thread-registry.d.ts +18 -4
  63. package/dist/data/memory/thread-registry.d.ts.map +1 -1
  64. package/dist/data/memory/thread-registry.js +25 -0
  65. package/dist/data/memory/thread-registry.js.map +1 -1
  66. package/dist/data/sent-message.repository.d.ts +12 -0
  67. package/dist/data/sent-message.repository.d.ts.map +1 -0
  68. package/dist/data/sent-message.repository.js +31 -0
  69. package/dist/data/sent-message.repository.js.map +1 -0
  70. package/dist/http-server.d.ts.map +1 -1
  71. package/dist/http-server.js +23 -2
  72. package/dist/http-server.js.map +1 -1
  73. package/dist/index.js +27 -48
  74. package/dist/index.js.map +1 -1
  75. package/dist/logger.d.ts +7 -2
  76. package/dist/logger.d.ts.map +1 -1
  77. package/dist/logger.js +89 -12
  78. package/dist/logger.js.map +1 -1
  79. package/dist/scheduler.d.ts +8 -0
  80. package/dist/scheduler.d.ts.map +1 -1
  81. package/dist/scheduler.js +15 -0
  82. package/dist/scheduler.js.map +1 -1
  83. package/dist/server/factory.d.ts +2 -1
  84. package/dist/server/factory.d.ts.map +1 -1
  85. package/dist/server/factory.js +11 -4
  86. package/dist/server/factory.js.map +1 -1
  87. package/dist/services/agent-spawn.service.d.ts +39 -0
  88. package/dist/services/agent-spawn.service.d.ts.map +1 -0
  89. package/dist/services/agent-spawn.service.js +348 -0
  90. package/dist/services/agent-spawn.service.js.map +1 -0
  91. package/dist/services/background-runner.d.ts +26 -0
  92. package/dist/services/background-runner.d.ts.map +1 -0
  93. package/dist/services/background-runner.js +71 -0
  94. package/dist/services/background-runner.js.map +1 -0
  95. package/dist/services/consolidation.service.d.ts +16 -0
  96. package/dist/services/consolidation.service.d.ts.map +1 -0
  97. package/dist/services/consolidation.service.js +508 -0
  98. package/dist/services/consolidation.service.js.map +1 -0
  99. package/dist/services/dispatcher/broker.d.ts +2 -0
  100. package/dist/services/dispatcher/broker.d.ts.map +1 -1
  101. package/dist/services/dispatcher/broker.js +5 -10
  102. package/dist/services/dispatcher/broker.js.map +1 -1
  103. package/dist/services/dispatcher/index.d.ts +1 -1
  104. package/dist/services/dispatcher/index.d.ts.map +1 -1
  105. package/dist/services/dispatcher/index.js +1 -1
  106. package/dist/services/dispatcher/index.js.map +1 -1
  107. package/dist/services/dispatcher/lock.d.ts.map +1 -1
  108. package/dist/services/dispatcher/lock.js +7 -11
  109. package/dist/services/dispatcher/lock.js.map +1 -1
  110. package/dist/services/maintenance-signal.d.ts +18 -0
  111. package/dist/services/maintenance-signal.d.ts.map +1 -0
  112. package/dist/services/maintenance-signal.js +48 -0
  113. package/dist/services/maintenance-signal.js.map +1 -0
  114. package/dist/services/memory-briefing.service.d.ts +4 -0
  115. package/dist/services/memory-briefing.service.d.ts.map +1 -0
  116. package/dist/services/memory-briefing.service.js +143 -0
  117. package/dist/services/memory-briefing.service.js.map +1 -0
  118. package/dist/services/process.service.d.ts +31 -0
  119. package/dist/services/process.service.d.ts.map +1 -0
  120. package/dist/services/process.service.js +100 -0
  121. package/dist/services/process.service.js.map +1 -0
  122. package/dist/services/thread-health.service.d.ts +18 -0
  123. package/dist/services/thread-health.service.d.ts.map +1 -0
  124. package/dist/services/thread-health.service.js +118 -0
  125. package/dist/services/thread-health.service.js.map +1 -0
  126. package/dist/services/thread-lifecycle.service.d.ts +52 -0
  127. package/dist/services/thread-lifecycle.service.d.ts.map +1 -0
  128. package/dist/services/thread-lifecycle.service.js +174 -0
  129. package/dist/services/thread-lifecycle.service.js.map +1 -0
  130. package/dist/services/topic.service.d.ts +25 -0
  131. package/dist/services/topic.service.d.ts.map +1 -0
  132. package/dist/services/topic.service.js +65 -0
  133. package/dist/services/topic.service.js.map +1 -0
  134. package/dist/services/worker-cleanup.service.d.ts +8 -0
  135. package/dist/services/worker-cleanup.service.d.ts.map +1 -0
  136. package/dist/services/worker-cleanup.service.js +82 -0
  137. package/dist/services/worker-cleanup.service.js.map +1 -0
  138. package/dist/sessions.d.ts +14 -0
  139. package/dist/sessions.d.ts.map +1 -1
  140. package/dist/sessions.js +55 -0
  141. package/dist/sessions.js.map +1 -1
  142. package/dist/telegram.d.ts +13 -6
  143. package/dist/telegram.d.ts.map +1 -1
  144. package/dist/telegram.js +43 -14
  145. package/dist/telegram.js.map +1 -1
  146. package/dist/tools/defs/memory-defs.d.ts.map +1 -1
  147. package/dist/tools/defs/memory-defs.js +0 -19
  148. package/dist/tools/defs/memory-defs.js.map +1 -1
  149. package/dist/tools/delegate-tool.d.ts +4 -0
  150. package/dist/tools/delegate-tool.d.ts.map +1 -1
  151. package/dist/tools/delegate-tool.js +48 -109
  152. package/dist/tools/delegate-tool.js.map +1 -1
  153. package/dist/tools/memory-tools.d.ts.map +1 -1
  154. package/dist/tools/memory-tools.js +1 -16
  155. package/dist/tools/memory-tools.js.map +1 -1
  156. package/dist/tools/shared-agent-utils.d.ts +9 -1
  157. package/dist/tools/shared-agent-utils.d.ts.map +1 -1
  158. package/dist/tools/shared-agent-utils.js +24 -42
  159. package/dist/tools/shared-agent-utils.js.map +1 -1
  160. package/dist/tools/start-session-tool.d.ts +2 -0
  161. package/dist/tools/start-session-tool.d.ts.map +1 -1
  162. package/dist/tools/start-session-tool.js +66 -106
  163. package/dist/tools/start-session-tool.js.map +1 -1
  164. package/dist/tools/thread-lifecycle.d.ts +5 -127
  165. package/dist/tools/thread-lifecycle.d.ts.map +1 -1
  166. package/dist/tools/thread-lifecycle.js +5 -1163
  167. package/dist/tools/thread-lifecycle.js.map +1 -1
  168. package/dist/tools/utility-tools.js +5 -2
  169. package/dist/tools/utility-tools.js.map +1 -1
  170. package/dist/tools/wait/drive-handler.d.ts +0 -1
  171. package/dist/tools/wait/drive-handler.d.ts.map +1 -1
  172. package/dist/tools/wait/drive-handler.js +5 -22
  173. package/dist/tools/wait/drive-handler.js.map +1 -1
  174. package/dist/tools/wait/message-delivery.js +1 -1
  175. package/dist/tools/wait/message-delivery.js.map +1 -1
  176. package/dist/tools/wait/message-processing.d.ts.map +1 -1
  177. package/dist/tools/wait/message-processing.js +9 -8
  178. package/dist/tools/wait/message-processing.js.map +1 -1
  179. package/dist/tools/wait/poll-loop.d.ts +2 -0
  180. package/dist/tools/wait/poll-loop.d.ts.map +1 -1
  181. package/dist/tools/wait/poll-loop.js +27 -29
  182. package/dist/tools/wait/poll-loop.js.map +1 -1
  183. package/dist/tools/wait/task-handler.d.ts +0 -3
  184. package/dist/tools/wait/task-handler.d.ts.map +1 -1
  185. package/dist/tools/wait/task-handler.js +3 -2
  186. package/dist/tools/wait/task-handler.js.map +1 -1
  187. package/dist/types.d.ts +0 -1
  188. package/dist/types.d.ts.map +1 -1
  189. package/package.json +4 -8
  190. package/supervisor/config.go +182 -69
  191. package/supervisor/config_test.go +78 -0
  192. package/supervisor/go.mod +12 -0
  193. package/supervisor/go.sum +20 -0
  194. package/supervisor/health.go +60 -11
  195. package/supervisor/health_test.go +29 -0
  196. package/supervisor/keeper.go +15 -10
  197. package/supervisor/log.go +109 -28
  198. package/supervisor/log_test.go +86 -6
  199. package/supervisor/main.go +150 -19
  200. package/supervisor/main_test.go +130 -0
  201. package/supervisor/process.go +47 -4
  202. package/supervisor/process_test.go +14 -0
  203. package/supervisor/secrets.go +95 -0
  204. package/supervisor/secrets_securevault_test.go +98 -0
  205. package/supervisor/secrets_test.go +119 -0
  206. package/supervisor/self_update.go +282 -0
  207. package/supervisor/self_update_test.go +177 -0
  208. package/supervisor/service_restart_stub.go +9 -0
  209. package/supervisor/service_restart_windows.go +63 -0
  210. package/supervisor/service_stub.go +15 -0
  211. package/supervisor/service_windows.go +216 -0
  212. package/supervisor/update_state.go +264 -0
  213. package/supervisor/update_state_test.go +306 -0
  214. package/supervisor/updater.go +311 -10
  215. package/supervisor/updater_test.go +64 -0
  216. package/dist/data/memory/quality-scoring.d.ts +0 -32
  217. package/dist/data/memory/quality-scoring.d.ts.map +0 -1
  218. package/dist/data/memory/quality-scoring.js +0 -182
  219. package/dist/data/memory/quality-scoring.js.map +0 -1
  220. package/scripts/install-supervisor.ps1 +0 -67
  221. package/scripts/install-supervisor.sh +0 -43
  222. package/scripts/start-supervisor.ps1 +0 -46
  223. package/scripts/start-supervisor.sh +0 -20
  224. package/templates/coding-task.default.md +0 -12
@@ -0,0 +1,306 @@
1
+ package main
2
+
3
+ import (
4
+ "encoding/json"
5
+ "errors"
6
+ "os"
7
+ "path/filepath"
8
+ "testing"
9
+ "time"
10
+ )
11
+
12
+ func TestUpdateStateStore_TransitionAndLoad(t *testing.T) {
13
+ dir := t.TempDir()
14
+ log := NewLogger(filepath.Join(dir, "test.log"))
15
+ defer log.Close()
16
+
17
+ store := NewUpdateStateStore(filepath.Join(dir, "update-state.json"), log)
18
+ store.Transition(updateScopeMCP, updatePhaseApplying, "2.0.0", "1.0.0", "")
19
+
20
+ state, err := store.Load()
21
+ if err != nil {
22
+ t.Fatalf("Load() after applying transition failed: %v", err)
23
+ }
24
+ if state.Scope != updateScopeMCP {
25
+ t.Fatalf("Scope = %q, want %q", state.Scope, updateScopeMCP)
26
+ }
27
+ if state.Phase != updatePhaseApplying {
28
+ t.Fatalf("Phase = %q, want %q", state.Phase, updatePhaseApplying)
29
+ }
30
+ if state.TargetVersion != "2.0.0" {
31
+ t.Fatalf("TargetVersion = %q, want 2.0.0", state.TargetVersion)
32
+ }
33
+ if state.PreviousVersion != "1.0.0" {
34
+ t.Fatalf("PreviousVersion = %q, want 1.0.0", state.PreviousVersion)
35
+ }
36
+ if !state.UpdatedAt.UTC().Equal(state.UpdatedAt) {
37
+ t.Fatal("UpdatedAt must be UTC")
38
+ }
39
+
40
+ store.Transition(updateScopeMCP, updatePhaseFailed, "2.0.0", "1.0.0", "boom")
41
+ state, err = store.Load()
42
+ if err != nil {
43
+ t.Fatalf("Load() after failed transition failed: %v", err)
44
+ }
45
+ if state.Phase != updatePhaseFailed {
46
+ t.Fatalf("Phase = %q, want %q", state.Phase, updatePhaseFailed)
47
+ }
48
+ if state.LastError != "boom" {
49
+ t.Fatalf("LastError = %q, want boom", state.LastError)
50
+ }
51
+
52
+ store.Transition(updateScopeMCP, updatePhaseIdle, "2.0.0", "1.0.0", "")
53
+ state, err = store.Load()
54
+ if err != nil {
55
+ t.Fatalf("Load() after idle transition failed: %v", err)
56
+ }
57
+ if state.Phase != updatePhaseIdle {
58
+ t.Fatalf("Phase = %q, want %q", state.Phase, updatePhaseIdle)
59
+ }
60
+ if state.LastError != "" {
61
+ t.Fatalf("LastError = %q, want empty", state.LastError)
62
+ }
63
+ }
64
+
65
+ func TestAcquireUpdateCoordinatorLock_SerializesScopes(t *testing.T) {
66
+ dir := t.TempDir()
67
+ log := NewLogger(filepath.Join(dir, "test.log"))
68
+ defer log.Close()
69
+
70
+ lockPath := filepath.Join(dir, "update-apply.lock")
71
+
72
+ mcpLock, ok := AcquireUpdateCoordinatorLock(lockPath, updateScopeMCP, log)
73
+ if !ok || mcpLock == nil {
74
+ t.Fatal("expected MCP scope lock acquisition to succeed")
75
+ }
76
+
77
+ supervisorLock, ok := AcquireUpdateCoordinatorLock(lockPath, updateScopeSupervisor, log)
78
+ if ok || supervisorLock != nil {
79
+ t.Fatal("expected supervisor scope lock acquisition to fail while MCP lock is held")
80
+ }
81
+
82
+ mcpLock.Release()
83
+
84
+ supervisorLock, ok = AcquireUpdateCoordinatorLock(lockPath, updateScopeSupervisor, log)
85
+ if !ok || supervisorLock == nil {
86
+ t.Fatal("expected supervisor scope lock acquisition to succeed after release")
87
+ }
88
+ supervisorLock.Release()
89
+ }
90
+
91
+ type fakeUpdateLockFile struct {
92
+ writeErr error
93
+ closeErr error
94
+ }
95
+
96
+ func (f *fakeUpdateLockFile) Write(_ []byte) (int, error) {
97
+ if f.writeErr != nil {
98
+ return 0, f.writeErr
99
+ }
100
+ return 1, nil
101
+ }
102
+
103
+ func (f *fakeUpdateLockFile) Close() error {
104
+ return f.closeErr
105
+ }
106
+
107
+ func TestWriteUpdateLockMetadata_CleansUpOnWriteError(t *testing.T) {
108
+ dir := t.TempDir()
109
+ lockPath := filepath.Join(dir, "update-apply.lock")
110
+ if err := os.WriteFile(lockPath, []byte("partial"), 0644); err != nil {
111
+ t.Fatalf("seed lock file: %v", err)
112
+ }
113
+
114
+ originalOpen := openUpdateLockFile
115
+ defer func() { openUpdateLockFile = originalOpen }()
116
+ openUpdateLockFile = func(string) (updateLockFile, error) {
117
+ return &fakeUpdateLockFile{writeErr: errors.New("write fail")}, nil
118
+ }
119
+
120
+ err := writeUpdateLockMetadata(lockPath, []byte(`{"pid":1}`))
121
+ if err == nil {
122
+ t.Fatal("expected writeUpdateLockMetadata to fail")
123
+ }
124
+ if _, statErr := os.Stat(lockPath); !errors.Is(statErr, os.ErrNotExist) {
125
+ t.Fatalf("expected lock file to be removed after write error, statErr=%v", statErr)
126
+ }
127
+ }
128
+
129
+ func TestWriteUpdateLockMetadata_CleansUpOnCloseError(t *testing.T) {
130
+ dir := t.TempDir()
131
+ lockPath := filepath.Join(dir, "update-apply.lock")
132
+ if err := os.WriteFile(lockPath, []byte("partial"), 0644); err != nil {
133
+ t.Fatalf("seed lock file: %v", err)
134
+ }
135
+
136
+ originalOpen := openUpdateLockFile
137
+ defer func() { openUpdateLockFile = originalOpen }()
138
+ openUpdateLockFile = func(string) (updateLockFile, error) {
139
+ return &fakeUpdateLockFile{closeErr: errors.New("close fail")}, nil
140
+ }
141
+
142
+ err := writeUpdateLockMetadata(lockPath, []byte(`{"pid":1}`))
143
+ if err == nil {
144
+ t.Fatal("expected writeUpdateLockMetadata to fail")
145
+ }
146
+ if _, statErr := os.Stat(lockPath); !errors.Is(statErr, os.ErrNotExist) {
147
+ t.Fatalf("expected lock file to be removed after close error, statErr=%v", statErr)
148
+ }
149
+ }
150
+
151
+ func TestAcquireUpdateCoordinatorLock_ReclaimsWhenOwnerDead(t *testing.T) {
152
+ dir := t.TempDir()
153
+ log := NewLogger(filepath.Join(dir, "test.log"))
154
+ defer log.Close()
155
+
156
+ lockPath := filepath.Join(dir, "update-apply.lock")
157
+ owner := updateLockOwner{
158
+ Scope: updateScopeMCP,
159
+ PID: 999999,
160
+ UpdatedAt: time.Now().UTC(),
161
+ }
162
+ data, _ := json.Marshal(owner)
163
+ if err := os.WriteFile(lockPath, data, 0644); err != nil {
164
+ t.Fatalf("seed lock owner: %v", err)
165
+ }
166
+
167
+ lock, ok := AcquireUpdateCoordinatorLock(lockPath, updateScopeSupervisor, log)
168
+ if !ok || lock == nil {
169
+ t.Fatal("expected lock to be reclaimed for dead owner PID")
170
+ }
171
+ lock.Release()
172
+ }
173
+
174
+ func TestAcquireUpdateCoordinatorLock_DoesNotReclaimFromAliveOwnerEvenWhenStaleByAge(t *testing.T) {
175
+ dir := t.TempDir()
176
+ log := NewLogger(filepath.Join(dir, "test.log"))
177
+ defer log.Close()
178
+
179
+ lockPath := filepath.Join(dir, "update-apply.lock")
180
+ owner := updateLockOwner{
181
+ Scope: updateScopeMCP,
182
+ PID: os.Getpid(),
183
+ UpdatedAt: time.Now().UTC().Add(-updateCoordinatorLockMaxAge - time.Minute),
184
+ }
185
+ data, _ := json.Marshal(owner)
186
+ if err := os.WriteFile(lockPath, data, 0644); err != nil {
187
+ t.Fatalf("seed lock owner: %v", err)
188
+ }
189
+
190
+ lock, ok := AcquireUpdateCoordinatorLock(lockPath, updateScopeSupervisor, log)
191
+ if ok || lock != nil {
192
+ t.Fatal("expected lock acquisition to fail while alive owner still holds lock")
193
+ }
194
+
195
+ raw, err := os.ReadFile(lockPath)
196
+ if err != nil {
197
+ t.Fatalf("read original lock: %v", err)
198
+ }
199
+ var current updateLockOwner
200
+ if err := json.Unmarshal(raw, &current); err != nil {
201
+ t.Fatalf("unmarshal lock owner: %v", err)
202
+ }
203
+ if current.Scope != updateScopeMCP {
204
+ t.Fatalf("scope = %q, want %q", current.Scope, updateScopeMCP)
205
+ }
206
+ if current.PID != os.Getpid() {
207
+ t.Fatalf("pid = %d, want %d", current.PID, os.Getpid())
208
+ }
209
+ }
210
+
211
+ func TestRecoverPersistedUpdateStateOnStartup_StaleNonIdleBecomesFailed(t *testing.T) {
212
+ dir := t.TempDir()
213
+ log := NewLogger(filepath.Join(dir, "test.log"))
214
+ defer log.Close()
215
+
216
+ cfg := Config{Paths: Paths{UpdateState: filepath.Join(dir, "update-state.json")}}
217
+ store := NewUpdateStateStore(cfg.Paths.UpdateState, log)
218
+ store.Transition(updateScopeMCP, updatePhaseApplying, "2.0.0", "1.0.0", "")
219
+
220
+ recoverPersistedUpdateStateOnStartup(cfg, log)
221
+
222
+ state, err := store.Load()
223
+ if err != nil {
224
+ t.Fatalf("load recovered state: %v", err)
225
+ }
226
+ if state.Phase != updatePhaseFailed {
227
+ t.Fatalf("phase = %q, want %q", state.Phase, updatePhaseFailed)
228
+ }
229
+ if state.Scope != updateScopeMCP {
230
+ t.Fatalf("scope = %q, want %q", state.Scope, updateScopeMCP)
231
+ }
232
+ if state.LastError == "" {
233
+ t.Fatal("expected recovery reason in LastError")
234
+ }
235
+ }
236
+
237
+ func TestRecoverPersistedUpdateStateOnStartup_SupervisorRestartingReachesIdleWhenVersionApplied(t *testing.T) {
238
+ dir := t.TempDir()
239
+ log := NewLogger(filepath.Join(dir, "test.log"))
240
+ defer log.Close()
241
+
242
+ target := "2.0.0"
243
+ cfg := Config{Paths: Paths{
244
+ UpdateState: filepath.Join(dir, "update-state.json"),
245
+ SupervisorVersion: filepath.Join(dir, "supervisor-version.txt"),
246
+ }}
247
+ if err := os.WriteFile(cfg.Paths.SupervisorVersion, []byte(target), 0644); err != nil {
248
+ t.Fatalf("seed supervisor version: %v", err)
249
+ }
250
+
251
+ store := NewUpdateStateStore(cfg.Paths.UpdateState, log)
252
+ store.Transition(updateScopeSupervisor, updatePhaseRestarting, target, "1.0.0", "")
253
+
254
+ recoverPersistedUpdateStateOnStartup(cfg, log)
255
+
256
+ state, err := store.Load()
257
+ if err != nil {
258
+ t.Fatalf("load recovered state: %v", err)
259
+ }
260
+ if state.Scope != updateScopeSupervisor {
261
+ t.Fatalf("scope = %q, want %q", state.Scope, updateScopeSupervisor)
262
+ }
263
+ if state.Phase != updatePhaseIdle {
264
+ t.Fatalf("phase = %q, want %q", state.Phase, updatePhaseIdle)
265
+ }
266
+ if state.TargetVersion != target {
267
+ t.Fatalf("target version = %q, want %q", state.TargetVersion, target)
268
+ }
269
+ }
270
+
271
+ func TestRecoverPersistedUpdateStateOnStartup_SupervisorFailedReachesIdleWhenVersionApplied(t *testing.T) {
272
+ dir := t.TempDir()
273
+ log := NewLogger(filepath.Join(dir, "test.log"))
274
+ defer log.Close()
275
+
276
+ target := "2.1.0"
277
+ cfg := Config{Paths: Paths{
278
+ UpdateState: filepath.Join(dir, "update-state.json"),
279
+ SupervisorVersion: filepath.Join(dir, "supervisor-version.txt"),
280
+ }}
281
+ if err := os.WriteFile(cfg.Paths.SupervisorVersion, []byte(target), 0644); err != nil {
282
+ t.Fatalf("seed supervisor version: %v", err)
283
+ }
284
+
285
+ store := NewUpdateStateStore(cfg.Paths.UpdateState, log)
286
+ store.Transition(updateScopeSupervisor, updatePhaseFailed, target, "2.0.0", "previous helper failure")
287
+
288
+ recoverPersistedUpdateStateOnStartup(cfg, log)
289
+
290
+ state, err := store.Load()
291
+ if err != nil {
292
+ t.Fatalf("load recovered state: %v", err)
293
+ }
294
+ if state.Scope != updateScopeSupervisor {
295
+ t.Fatalf("scope = %q, want %q", state.Scope, updateScopeSupervisor)
296
+ }
297
+ if state.Phase != updatePhaseIdle {
298
+ t.Fatalf("phase = %q, want %q", state.Phase, updatePhaseIdle)
299
+ }
300
+ if state.TargetVersion != target {
301
+ t.Fatalf("target version = %q, want %q", state.TargetVersion, target)
302
+ }
303
+ if state.LastError != "" {
304
+ t.Fatalf("last error = %q, want empty", state.LastError)
305
+ }
306
+ }