sensorium-mcp 3.0.4 → 3.0.6
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.
- package/Install-Sensorium.ps1 +102 -209
- package/dist/dashboard/routes/data.d.ts.map +1 -1
- package/dist/dashboard/routes/data.js +2 -1
- package/dist/dashboard/routes/data.js.map +1 -1
- package/dist/dashboard/routes/threads.js +1 -1
- package/dist/dashboard/routes/threads.js.map +1 -1
- package/dist/dashboard/routes.d.ts.map +1 -1
- package/dist/dashboard/routes.js +1 -3
- package/dist/dashboard/routes.js.map +1 -1
- package/dist/data/memory/migration-runner.d.ts +1 -1
- package/dist/data/memory/migration-runner.d.ts.map +1 -1
- package/dist/data/memory/migration-runner.js +59 -3
- package/dist/data/memory/migration-runner.js.map +1 -1
- package/dist/data/memory/narrative.d.ts.map +1 -1
- package/dist/data/memory/narrative.js +43 -6
- package/dist/data/memory/narrative.js.map +1 -1
- package/dist/data/memory/reflection.d.ts +24 -0
- package/dist/data/memory/reflection.d.ts.map +1 -1
- package/dist/data/memory/reflection.js +65 -1
- package/dist/data/memory/reflection.js.map +1 -1
- package/dist/data/memory/schema-ddl.d.ts +1 -1
- package/dist/data/memory/schema-ddl.d.ts.map +1 -1
- package/dist/data/memory/schema-ddl.js +2 -1
- package/dist/data/memory/schema-ddl.js.map +1 -1
- package/dist/data/memory/thread-registry.js +1 -1
- package/dist/data/memory/thread-registry.js.map +1 -1
- package/dist/http-server.d.ts.map +1 -1
- package/dist/http-server.js +1 -9
- package/dist/http-server.js.map +1 -1
- package/dist/index.js +3 -6
- package/dist/index.js.map +1 -1
- package/dist/server/factory.js +1 -1
- package/dist/server/factory.js.map +1 -1
- package/dist/services/agent-spawn.service.d.ts +7 -1
- package/dist/services/agent-spawn.service.d.ts.map +1 -1
- package/dist/services/agent-spawn.service.js +69 -45
- package/dist/services/agent-spawn.service.js.map +1 -1
- package/dist/services/consolidation.service.d.ts.map +1 -1
- package/dist/services/consolidation.service.js +88 -35
- package/dist/services/consolidation.service.js.map +1 -1
- package/dist/services/keeper.service.d.ts +21 -0
- package/dist/services/keeper.service.d.ts.map +1 -0
- package/dist/services/keeper.service.js +195 -0
- package/dist/services/keeper.service.js.map +1 -0
- package/dist/services/maintenance-signal.d.ts +2 -0
- package/dist/services/maintenance-signal.d.ts.map +1 -1
- package/dist/services/maintenance-signal.js +7 -1
- package/dist/services/maintenance-signal.js.map +1 -1
- package/dist/services/memory-briefing.service.d.ts.map +1 -1
- package/dist/services/memory-briefing.service.js +17 -1
- package/dist/services/memory-briefing.service.js.map +1 -1
- package/dist/services/process.service.d.ts +19 -2
- package/dist/services/process.service.d.ts.map +1 -1
- package/dist/services/process.service.js +104 -10
- package/dist/services/process.service.js.map +1 -1
- package/dist/services/thread-lifecycle.service.d.ts +5 -0
- package/dist/services/thread-lifecycle.service.d.ts.map +1 -1
- package/dist/services/thread-lifecycle.service.js +33 -8
- package/dist/services/thread-lifecycle.service.js.map +1 -1
- package/dist/services/worker-cleanup.service.d.ts +14 -1
- package/dist/services/worker-cleanup.service.d.ts.map +1 -1
- package/dist/services/worker-cleanup.service.js +36 -38
- package/dist/services/worker-cleanup.service.js.map +1 -1
- package/dist/sessions.d.ts +0 -5
- package/dist/sessions.d.ts.map +1 -1
- package/dist/sessions.js +0 -7
- package/dist/sessions.js.map +1 -1
- package/dist/stdio-server.d.ts.map +1 -1
- package/dist/stdio-server.js +1 -7
- package/dist/stdio-server.js.map +1 -1
- package/dist/tools/delegate-tool.d.ts.map +1 -1
- package/dist/tools/delegate-tool.js +2 -2
- package/dist/tools/delegate-tool.js.map +1 -1
- package/dist/tools/session-tools.js +1 -1
- package/dist/tools/session-tools.js.map +1 -1
- package/dist/tools/start-session-tool.d.ts.map +1 -1
- package/dist/tools/start-session-tool.js +8 -9
- package/dist/tools/start-session-tool.js.map +1 -1
- package/dist/tools/wait/message-processing.d.ts.map +1 -1
- package/dist/tools/wait/message-processing.js +28 -0
- package/dist/tools/wait/message-processing.js.map +1 -1
- package/dist/tools/wait/poll-loop.js +1 -1
- package/dist/tools/wait/poll-loop.js.map +1 -1
- package/package.json +1 -1
- package/dist/tools/thread-lifecycle.d.ts +0 -6
- package/dist/tools/thread-lifecycle.d.ts.map +0 -1
- package/dist/tools/thread-lifecycle.js +0 -6
- package/dist/tools/thread-lifecycle.js.map +0 -1
- package/supervisor/config.go +0 -253
- package/supervisor/config_test.go +0 -78
- package/supervisor/go.mod +0 -15
- package/supervisor/go.sum +0 -20
- package/supervisor/health.go +0 -433
- package/supervisor/health_test.go +0 -93
- package/supervisor/keeper.go +0 -309
- package/supervisor/keeper_test.go +0 -27
- package/supervisor/lock.go +0 -57
- package/supervisor/lock_test.go +0 -54
- package/supervisor/log.go +0 -195
- package/supervisor/log_test.go +0 -125
- package/supervisor/main.go +0 -475
- package/supervisor/main_test.go +0 -130
- package/supervisor/notify.go +0 -53
- package/supervisor/process.go +0 -294
- package/supervisor/process_test.go +0 -108
- package/supervisor/process_unix.go +0 -14
- package/supervisor/process_windows.go +0 -15
- package/supervisor/secrets.go +0 -95
- package/supervisor/secrets_securevault_test.go +0 -98
- package/supervisor/secrets_test.go +0 -119
- package/supervisor/self_update.go +0 -282
- package/supervisor/self_update_test.go +0 -177
- package/supervisor/service_restart_stub.go +0 -9
- package/supervisor/service_restart_windows.go +0 -63
- package/supervisor/service_stub.go +0 -15
- package/supervisor/service_windows.go +0 -194
- package/supervisor/update_state.go +0 -264
- package/supervisor/update_state_test.go +0 -306
- package/supervisor/updater.go +0 -613
- package/supervisor/updater_test.go +0 -64
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
package main
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"encoding/json"
|
|
5
|
-
"os"
|
|
6
|
-
"path/filepath"
|
|
7
|
-
"testing"
|
|
8
|
-
|
|
9
|
-
sv "github.com/andriyshevchenko/SecureVault/securevault-go"
|
|
10
|
-
"github.com/zalando/go-keyring"
|
|
11
|
-
)
|
|
12
|
-
|
|
13
|
-
// writeProfileFixture writes a minimal profiles.json to dir and returns the dir.
|
|
14
|
-
func writeProfileFixture(t *testing.T, dir string, profiles []sv.Profile) {
|
|
15
|
-
t.Helper()
|
|
16
|
-
data, err := json.Marshal(profiles)
|
|
17
|
-
if err != nil {
|
|
18
|
-
t.Fatalf("marshal profiles: %v", err)
|
|
19
|
-
}
|
|
20
|
-
if err := os.WriteFile(filepath.Join(dir, "profiles.json"), data, 0600); err != nil {
|
|
21
|
-
t.Fatalf("write profiles.json: %v", err)
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
func TestResolveStringChain_EnvWins(t *testing.T) {
|
|
26
|
-
t.Setenv("TELEGRAM_TOKEN", "env-value")
|
|
27
|
-
dir := t.TempDir()
|
|
28
|
-
writeProfileFixture(t, dir, []sv.Profile{
|
|
29
|
-
{ID: "p1", Name: "TEST", Mappings: []sv.ProfileMapping{
|
|
30
|
-
{EnvVar: "TELEGRAM_TOKEN", SecretID: "no-cred"},
|
|
31
|
-
}},
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
got := resolveStringChain("TELEGRAM_TOKEN", "TEST", dir, "")
|
|
35
|
-
if got != "env-value" {
|
|
36
|
-
t.Errorf("resolveStringChain = %q, want env-value", got)
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
func TestResolveStringChain_SecureVaultFallback_MissingCred_UsesKeyring(t *testing.T) {
|
|
41
|
-
t.Setenv("TELEGRAM_TOKEN", "")
|
|
42
|
-
dir := t.TempDir()
|
|
43
|
-
writeProfileFixture(t, dir, []sv.Profile{
|
|
44
|
-
{ID: "p1", Name: "TEST", Mappings: []sv.ProfileMapping{
|
|
45
|
-
{EnvVar: "TELEGRAM_TOKEN", SecretID: "non-existent-cred"},
|
|
46
|
-
}},
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
orig := keyringGet
|
|
50
|
-
keyringGet = func(service, user string) (string, error) {
|
|
51
|
-
if service != "sensorium-supervisor" {
|
|
52
|
-
t.Fatalf("service = %q, want %q", service, "sensorium-supervisor")
|
|
53
|
-
}
|
|
54
|
-
if user != "TELEGRAM_TOKEN" {
|
|
55
|
-
t.Fatalf("user = %q, want %q", user, "TELEGRAM_TOKEN")
|
|
56
|
-
}
|
|
57
|
-
return "from-keyring", nil
|
|
58
|
-
}
|
|
59
|
-
t.Cleanup(func() { keyringGet = orig })
|
|
60
|
-
|
|
61
|
-
// Credential not in store → falls through to keyring.
|
|
62
|
-
got := resolveStringChain("TELEGRAM_TOKEN", "TEST", dir, "sensorium-supervisor")
|
|
63
|
-
if got != "from-keyring" {
|
|
64
|
-
t.Errorf("resolveStringChain = %q, want from-keyring", got)
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
func TestResolveStringChain_NoProfile_FallsToKeyring(t *testing.T) {
|
|
69
|
-
t.Setenv("TELEGRAM_TOKEN", "")
|
|
70
|
-
|
|
71
|
-
orig := keyringGet
|
|
72
|
-
keyringGet = func(service, user string) (string, error) {
|
|
73
|
-
return "", keyring.ErrNotFound
|
|
74
|
-
}
|
|
75
|
-
t.Cleanup(func() { keyringGet = orig })
|
|
76
|
-
|
|
77
|
-
// No profiles file in dir → securevault returns empty → keyring path taken.
|
|
78
|
-
got := resolveStringChain("TELEGRAM_TOKEN", "NONEXISTENT_PROFILE", t.TempDir(), "sensorium-supervisor")
|
|
79
|
-
if got != "" {
|
|
80
|
-
t.Errorf("resolveStringChain = %q, want empty", got)
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
func TestResolveIntChain_EnvWins(t *testing.T) {
|
|
85
|
-
t.Setenv("MCP_HTTP_PORT", "4567")
|
|
86
|
-
got := resolveIntChain("MCP_HTTP_PORT", "TEST", t.TempDir(), "", 0)
|
|
87
|
-
if got != 4567 {
|
|
88
|
-
t.Errorf("resolveIntChain = %d, want 4567", got)
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
func TestResolveIntChain_InvalidFallback(t *testing.T) {
|
|
93
|
-
t.Setenv("MCP_HTTP_PORT", "not-a-number")
|
|
94
|
-
got := resolveIntChain("MCP_HTTP_PORT", "TEST", t.TempDir(), "", 9999)
|
|
95
|
-
if got != 9999 {
|
|
96
|
-
t.Errorf("resolveIntChain invalid = %d, want fallback 9999", got)
|
|
97
|
-
}
|
|
98
|
-
}
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
package main
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"errors"
|
|
5
|
-
"testing"
|
|
6
|
-
|
|
7
|
-
"github.com/zalando/go-keyring"
|
|
8
|
-
)
|
|
9
|
-
|
|
10
|
-
func TestResolveSecretWithKeyring_EnvWins(t *testing.T) {
|
|
11
|
-
t.Setenv("MCP_HTTP_SECRET", "env-secret")
|
|
12
|
-
|
|
13
|
-
orig := keyringGet
|
|
14
|
-
keyringGet = func(service, user string) (string, error) {
|
|
15
|
-
return "keyring-secret", nil
|
|
16
|
-
}
|
|
17
|
-
t.Cleanup(func() { keyringGet = orig })
|
|
18
|
-
|
|
19
|
-
got := resolveSecretWithKeyring("MCP_HTTP_SECRET", "sensorium-supervisor")
|
|
20
|
-
if got != "env-secret" {
|
|
21
|
-
t.Fatalf("resolveSecretWithKeyring() = %q, want %q", got, "env-secret")
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
func TestResolveSecretWithKeyring_FallbackToKeyring(t *testing.T) {
|
|
26
|
-
t.Setenv("MCP_HTTP_SECRET", "")
|
|
27
|
-
|
|
28
|
-
orig := keyringGet
|
|
29
|
-
keyringGet = func(service, user string) (string, error) {
|
|
30
|
-
if service != "sensorium-supervisor" {
|
|
31
|
-
t.Fatalf("service = %q, want %q", service, "sensorium-supervisor")
|
|
32
|
-
}
|
|
33
|
-
if user != "MCP_HTTP_SECRET" {
|
|
34
|
-
t.Fatalf("user = %q, want %q", user, "MCP_HTTP_SECRET")
|
|
35
|
-
}
|
|
36
|
-
return "keyring-secret", nil
|
|
37
|
-
}
|
|
38
|
-
t.Cleanup(func() { keyringGet = orig })
|
|
39
|
-
|
|
40
|
-
got := resolveSecretWithKeyring("MCP_HTTP_SECRET", "sensorium-supervisor")
|
|
41
|
-
if got != "keyring-secret" {
|
|
42
|
-
t.Fatalf("resolveSecretWithKeyring() = %q, want %q", got, "keyring-secret")
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
func TestResolveSecretWithKeyring_NotFound(t *testing.T) {
|
|
47
|
-
t.Setenv("MCP_HTTP_SECRET", "")
|
|
48
|
-
|
|
49
|
-
orig := keyringGet
|
|
50
|
-
keyringGet = func(service, user string) (string, error) {
|
|
51
|
-
return "", keyring.ErrNotFound
|
|
52
|
-
}
|
|
53
|
-
t.Cleanup(func() { keyringGet = orig })
|
|
54
|
-
|
|
55
|
-
got := resolveSecretWithKeyring("MCP_HTTP_SECRET", "sensorium-supervisor")
|
|
56
|
-
if got != "" {
|
|
57
|
-
t.Fatalf("resolveSecretWithKeyring() = %q, want empty", got)
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
func TestResolveSecretWithKeyring_OtherError(t *testing.T) {
|
|
62
|
-
t.Setenv("MCP_HTTP_SECRET", "")
|
|
63
|
-
|
|
64
|
-
orig := keyringGet
|
|
65
|
-
keyringGet = func(service, user string) (string, error) {
|
|
66
|
-
return "", errors.New("keyring backend unavailable")
|
|
67
|
-
}
|
|
68
|
-
t.Cleanup(func() { keyringGet = orig })
|
|
69
|
-
|
|
70
|
-
got := resolveSecretWithKeyring("MCP_HTTP_SECRET", "sensorium-supervisor")
|
|
71
|
-
if got != "" {
|
|
72
|
-
t.Fatalf("resolveSecretWithKeyring() = %q, want empty", got)
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
func TestResolveIntWithKeyring_EnvWins(t *testing.T) {
|
|
77
|
-
t.Setenv("MCP_HTTP_PORT", "3847")
|
|
78
|
-
|
|
79
|
-
orig := keyringGet
|
|
80
|
-
keyringGet = func(service, user string) (string, error) {
|
|
81
|
-
return "9999", nil
|
|
82
|
-
}
|
|
83
|
-
t.Cleanup(func() { keyringGet = orig })
|
|
84
|
-
|
|
85
|
-
got := resolveIntWithKeyring("MCP_HTTP_PORT", "sensorium-supervisor", 0)
|
|
86
|
-
if got != 3847 {
|
|
87
|
-
t.Fatalf("resolveIntWithKeyring() = %d, want %d", got, 3847)
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
func TestResolveIntWithKeyring_FallbackToKeyring(t *testing.T) {
|
|
92
|
-
t.Setenv("MCP_HTTP_PORT", "")
|
|
93
|
-
|
|
94
|
-
orig := keyringGet
|
|
95
|
-
keyringGet = func(service, user string) (string, error) {
|
|
96
|
-
return "5001", nil
|
|
97
|
-
}
|
|
98
|
-
t.Cleanup(func() { keyringGet = orig })
|
|
99
|
-
|
|
100
|
-
got := resolveIntWithKeyring("MCP_HTTP_PORT", "sensorium-supervisor", 0)
|
|
101
|
-
if got != 5001 {
|
|
102
|
-
t.Fatalf("resolveIntWithKeyring() = %d, want %d", got, 5001)
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
func TestResolveIntWithKeyring_InvalidFallback(t *testing.T) {
|
|
107
|
-
t.Setenv("MCP_HTTP_PORT", "")
|
|
108
|
-
|
|
109
|
-
orig := keyringGet
|
|
110
|
-
keyringGet = func(service, user string) (string, error) {
|
|
111
|
-
return "not-a-number", nil
|
|
112
|
-
}
|
|
113
|
-
t.Cleanup(func() { keyringGet = orig })
|
|
114
|
-
|
|
115
|
-
got := resolveIntWithKeyring("MCP_HTTP_PORT", "sensorium-supervisor", 3847)
|
|
116
|
-
if got != 3847 {
|
|
117
|
-
t.Fatalf("resolveIntWithKeyring() = %d, want fallback %d", got, 3847)
|
|
118
|
-
}
|
|
119
|
-
}
|
|
@@ -1,282 +0,0 @@
|
|
|
1
|
-
package main
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"errors"
|
|
5
|
-
"fmt"
|
|
6
|
-
"os"
|
|
7
|
-
"os/exec"
|
|
8
|
-
"path/filepath"
|
|
9
|
-
"runtime"
|
|
10
|
-
"strings"
|
|
11
|
-
)
|
|
12
|
-
|
|
13
|
-
const supervisorRollbackAttemptedMarker = "rollback_attempted=true"
|
|
14
|
-
const supervisorWindowsServiceName = "SensoriumSupervisor"
|
|
15
|
-
|
|
16
|
-
// applyPendingSupervisorUpdate applies a downloaded supervisor binary before the
|
|
17
|
-
// rest of the process starts. On Windows, a detached helper performs the swap
|
|
18
|
-
// after this bootstrap process exits because a running .exe cannot overwrite
|
|
19
|
-
// itself in place.
|
|
20
|
-
func applyPendingSupervisorUpdate(cfg Config, log *Logger) (bool, error) {
|
|
21
|
-
recordPendingSupervisorApplyFailureIfPresent(cfg, log)
|
|
22
|
-
|
|
23
|
-
if _, err := os.Stat(cfg.Paths.PendingBinary); err != nil {
|
|
24
|
-
if errors.Is(err, os.ErrNotExist) {
|
|
25
|
-
if _, versionErr := os.Stat(cfg.Paths.PendingVersion); versionErr == nil {
|
|
26
|
-
log.Warn("Removing stale pending supervisor version file %s", cfg.Paths.PendingVersion)
|
|
27
|
-
_ = os.Remove(cfg.Paths.PendingVersion)
|
|
28
|
-
}
|
|
29
|
-
return false, nil
|
|
30
|
-
}
|
|
31
|
-
return false, fmt.Errorf("stat pending supervisor binary: %w", err)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
exePath, err := os.Executable()
|
|
35
|
-
if err != nil {
|
|
36
|
-
cleanupPendingSupervisorUpdate(cfg, log)
|
|
37
|
-
return false, fmt.Errorf("resolve current executable: %w", err)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if runtime.GOOS == "windows" {
|
|
41
|
-
isService, serviceErr := isWindowsService()
|
|
42
|
-
if serviceErr != nil {
|
|
43
|
-
markSupervisorApplyFailure(cfg, log, fmt.Sprintf("detect service mode for pending supervisor apply: %v", serviceErr))
|
|
44
|
-
cleanupPendingSupervisorUpdate(cfg, log)
|
|
45
|
-
return false, fmt.Errorf("detect service mode for pending supervisor apply: %w", serviceErr)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if err := launchWindowsApplyHelper(cfg, exePath, isService); err != nil {
|
|
49
|
-
markSupervisorApplyFailure(cfg, log, fmt.Sprintf("schedule pending supervisor apply: %v", err))
|
|
50
|
-
cleanupPendingSupervisorUpdate(cfg, log)
|
|
51
|
-
return false, fmt.Errorf("schedule pending supervisor apply: %w", err)
|
|
52
|
-
}
|
|
53
|
-
log.Info("Pending supervisor update detected; restarting to apply")
|
|
54
|
-
return true, nil
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if err := os.Rename(cfg.Paths.PendingBinary, exePath); err != nil {
|
|
58
|
-
cleanupPendingSupervisorUpdate(cfg, log)
|
|
59
|
-
return false, fmt.Errorf("apply pending supervisor binary: %w", err)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if err := finalizePendingSupervisorVersion(cfg); err != nil {
|
|
63
|
-
log.Warn("Applied supervisor update but failed to persist version: %v", err)
|
|
64
|
-
} else {
|
|
65
|
-
log.Info("Applied supervisor update")
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return false, nil
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
func finalizePendingSupervisorVersion(cfg Config) error {
|
|
72
|
-
data, err := os.ReadFile(cfg.Paths.PendingVersion)
|
|
73
|
-
if err != nil {
|
|
74
|
-
if errors.Is(err, os.ErrNotExist) {
|
|
75
|
-
return nil
|
|
76
|
-
}
|
|
77
|
-
return fmt.Errorf("read pending supervisor version: %w", err)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if err := atomicWrite(cfg.Paths.SupervisorVersion, data); err != nil {
|
|
81
|
-
return fmt.Errorf("write supervisor version: %w", err)
|
|
82
|
-
}
|
|
83
|
-
if err := os.Remove(cfg.Paths.PendingVersion); err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
84
|
-
return fmt.Errorf("remove pending supervisor version: %w", err)
|
|
85
|
-
}
|
|
86
|
-
return nil
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
func cleanupPendingSupervisorUpdate(cfg Config, log *Logger) {
|
|
90
|
-
for _, path := range []string{cfg.Paths.PendingBinary, cfg.Paths.PendingVersion} {
|
|
91
|
-
if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
92
|
-
log.Warn("Failed to remove stale supervisor update artifact %s: %v", path, err)
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
func launchWindowsApplyHelper(cfg Config, exePath string, restartViaService bool) error {
|
|
98
|
-
if err := os.MkdirAll(filepath.Dir(cfg.Paths.SupervisorVersion), 0755); err != nil {
|
|
99
|
-
return fmt.Errorf("create supervisor version directory: %w", err)
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
scriptFile, err := os.CreateTemp("", "sensorium-supervisor-apply-*.cmd")
|
|
103
|
-
if err != nil {
|
|
104
|
-
return fmt.Errorf("create apply helper script: %w", err)
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
script := buildWindowsApplyHelperScript(cfg, exePath, restartViaService)
|
|
108
|
-
|
|
109
|
-
if _, err := scriptFile.WriteString(script); err != nil {
|
|
110
|
-
scriptFile.Close()
|
|
111
|
-
_ = os.Remove(scriptFile.Name())
|
|
112
|
-
return fmt.Errorf("write apply helper script: %w", err)
|
|
113
|
-
}
|
|
114
|
-
if err := scriptFile.Close(); err != nil {
|
|
115
|
-
_ = os.Remove(scriptFile.Name())
|
|
116
|
-
return fmt.Errorf("close apply helper script: %w", err)
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
cmd := exec.Command("cmd", "/c", scriptFile.Name())
|
|
120
|
-
cmd.Env = os.Environ()
|
|
121
|
-
cmd.Stdin = nil
|
|
122
|
-
cmd.Stdout = nil
|
|
123
|
-
cmd.Stderr = nil
|
|
124
|
-
setSysProcAttr(cmd)
|
|
125
|
-
|
|
126
|
-
if err := cmd.Start(); err != nil {
|
|
127
|
-
_ = os.Remove(scriptFile.Name())
|
|
128
|
-
return fmt.Errorf("start apply helper: %w", err)
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
_ = cmd.Process.Release()
|
|
132
|
-
return nil
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
func buildWindowsApplyHelperScript(cfg Config, exePath string, restartViaService bool) string {
|
|
136
|
-
failReason := fmt.Sprintf("helper failed to swap pending supervisor binary after retries (pending=%s current=%s)", cfg.Paths.PendingBinary, exePath)
|
|
137
|
-
escapedFailReason := batchEscapeForSetValue(failReason)
|
|
138
|
-
|
|
139
|
-
failLines := []string{
|
|
140
|
-
fmt.Sprintf(`set "FAIL_REASON=%s"`, escapedFailReason),
|
|
141
|
-
fmt.Sprintf(`<nul set /p "=%%FAIL_REASON%%" > %s`, batchQuote(supervisorApplyFailureMarkerPath(cfg))),
|
|
142
|
-
fmt.Sprintf(`if exist %s del /F /Q %s`, batchQuote(cfg.Paths.PendingBinary), batchQuote(cfg.Paths.PendingBinary)),
|
|
143
|
-
fmt.Sprintf(`if exist %s del /F /Q %s`, batchQuote(cfg.Paths.PendingVersion), batchQuote(cfg.Paths.PendingVersion)),
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if restartViaService {
|
|
147
|
-
failLines = append(failLines,
|
|
148
|
-
fmt.Sprintf(`sc start %s >NUL 2>&1`, batchQuote(supervisorWindowsServiceName)),
|
|
149
|
-
"if errorlevel 1 (",
|
|
150
|
-
" timeout /T 2 /NOBREAK >NUL",
|
|
151
|
-
fmt.Sprintf(` sc start %s >NUL 2>&1`, batchQuote(supervisorWindowsServiceName)),
|
|
152
|
-
")",
|
|
153
|
-
)
|
|
154
|
-
} else {
|
|
155
|
-
failLines = append(failLines, fmt.Sprintf(`start "" %s`, batchQuote(exePath)))
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
failLines = append(failLines, "exit /b 1")
|
|
159
|
-
|
|
160
|
-
scriptLines := []string{
|
|
161
|
-
"@echo off",
|
|
162
|
-
"setlocal",
|
|
163
|
-
":wait",
|
|
164
|
-
fmt.Sprintf(`tasklist /FI "PID eq %d" 2>NUL | find "%d" >NUL`, os.Getpid(), os.Getpid()),
|
|
165
|
-
"if not errorlevel 1 (",
|
|
166
|
-
" timeout /T 1 /NOBREAK >NUL",
|
|
167
|
-
" goto wait",
|
|
168
|
-
")",
|
|
169
|
-
"set attempts=0",
|
|
170
|
-
":move",
|
|
171
|
-
fmt.Sprintf(`move /Y %s %s >NUL`, batchQuote(cfg.Paths.PendingBinary), batchQuote(exePath)),
|
|
172
|
-
"if not errorlevel 1 goto applied",
|
|
173
|
-
"set /a attempts+=1",
|
|
174
|
-
"if %attempts% GEQ 5 goto fail",
|
|
175
|
-
"timeout /T 1 /NOBREAK >NUL",
|
|
176
|
-
"goto move",
|
|
177
|
-
":applied",
|
|
178
|
-
fmt.Sprintf(`if exist %s move /Y %s %s >NUL`, batchQuote(cfg.Paths.PendingVersion), batchQuote(cfg.Paths.PendingVersion), batchQuote(cfg.Paths.SupervisorVersion)),
|
|
179
|
-
fmt.Sprintf(`if exist %s del /F /Q %s`, batchQuote(supervisorApplyFailureMarkerPath(cfg)), batchQuote(supervisorApplyFailureMarkerPath(cfg))),
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
if !restartViaService {
|
|
183
|
-
scriptLines = append(scriptLines, fmt.Sprintf(`start "" %s`, batchQuote(exePath)))
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
scriptLines = append(scriptLines,
|
|
187
|
-
"exit /b 0",
|
|
188
|
-
":fail",
|
|
189
|
-
)
|
|
190
|
-
|
|
191
|
-
scriptLines = append(scriptLines, failLines...)
|
|
192
|
-
scriptLines = append(scriptLines, "")
|
|
193
|
-
|
|
194
|
-
return strings.Join(scriptLines, "\r\n")
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
func supervisorApplyFailureMarkerPath(cfg Config) string {
|
|
198
|
-
return cfg.Paths.PendingBinary + ".failed"
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
func recordPendingSupervisorApplyFailureIfPresent(cfg Config, log *Logger) {
|
|
202
|
-
markerPath := supervisorApplyFailureMarkerPath(cfg)
|
|
203
|
-
data, err := os.ReadFile(markerPath)
|
|
204
|
-
if err != nil {
|
|
205
|
-
if !errors.Is(err, os.ErrNotExist) {
|
|
206
|
-
log.Warn("Failed to read supervisor apply failure marker %s: %v", markerPath, err)
|
|
207
|
-
}
|
|
208
|
-
return
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
reason := strings.TrimSpace(string(data))
|
|
212
|
-
if reason == "" {
|
|
213
|
-
reason = "pending supervisor apply helper reported failure"
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
store := NewUpdateStateStore(cfg.Paths.UpdateState, log)
|
|
217
|
-
state, stateErr := store.Load()
|
|
218
|
-
if stateErr != nil {
|
|
219
|
-
log.Warn("Failed to load update state while reconciling supervisor apply failure marker: %v", stateErr)
|
|
220
|
-
}
|
|
221
|
-
targetVersion := strings.TrimSpace(state.TargetVersion)
|
|
222
|
-
if targetVersion == "" {
|
|
223
|
-
targetVersion = strings.TrimSpace(readTrimmedFile(cfg.Paths.PendingVersion))
|
|
224
|
-
}
|
|
225
|
-
if targetVersion != "" {
|
|
226
|
-
currentVersion := strings.TrimSpace(readTrimmedFile(cfg.Paths.SupervisorVersion))
|
|
227
|
-
if currentVersion == targetVersion {
|
|
228
|
-
log.Warn("Ignoring stale supervisor apply failure marker because supervisor version already matches target %s", targetVersion)
|
|
229
|
-
store.Transition(updateScopeSupervisor, updatePhaseIdle, targetVersion, state.PreviousVersion, "")
|
|
230
|
-
if err := os.Remove(markerPath); err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
231
|
-
log.Warn("Failed to remove stale supervisor apply failure marker %s: %v", markerPath, err)
|
|
232
|
-
}
|
|
233
|
-
return
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
markSupervisorApplyFailure(cfg, log, reason)
|
|
238
|
-
|
|
239
|
-
if err := os.Remove(markerPath); err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
240
|
-
log.Warn("Failed to remove supervisor apply failure marker %s: %v", markerPath, err)
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
func markSupervisorApplyFailure(cfg Config, log *Logger, reason string) {
|
|
245
|
-
store := NewUpdateStateStore(cfg.Paths.UpdateState, log)
|
|
246
|
-
state, err := store.Load()
|
|
247
|
-
targetVersion := ""
|
|
248
|
-
previousVersion := ""
|
|
249
|
-
if err != nil {
|
|
250
|
-
log.Warn("Failed to load update state while marking supervisor apply failure: %v", err)
|
|
251
|
-
} else {
|
|
252
|
-
targetVersion = state.TargetVersion
|
|
253
|
-
previousVersion = state.PreviousVersion
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
if targetVersion == "" {
|
|
257
|
-
targetVersion = strings.TrimSpace(readTrimmedFile(cfg.Paths.PendingVersion))
|
|
258
|
-
}
|
|
259
|
-
if previousVersion == "" {
|
|
260
|
-
previousVersion = strings.TrimSpace(readTrimmedFile(cfg.Paths.SupervisorVersion))
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
lastError := reason
|
|
264
|
-
if !strings.Contains(lastError, supervisorRollbackAttemptedMarker) {
|
|
265
|
-
lastError = strings.TrimSpace(lastError + "; " + supervisorRollbackAttemptedMarker)
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
store.Transition(updateScopeSupervisor, updatePhaseRollback, targetVersion, previousVersion, lastError)
|
|
269
|
-
store.Transition(updateScopeSupervisor, updatePhaseFailed, targetVersion, previousVersion, lastError)
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
func batchQuote(path string) string {
|
|
273
|
-
return `"` + strings.ReplaceAll(path, `"`, `""`) + `"`
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
func batchEscapeForSetValue(value string) string {
|
|
277
|
-
value = strings.ReplaceAll(value, "\r", " ")
|
|
278
|
-
value = strings.ReplaceAll(value, "\n", " ")
|
|
279
|
-
value = strings.ReplaceAll(value, `%`, `%%`)
|
|
280
|
-
value = strings.ReplaceAll(value, `"`, `'`)
|
|
281
|
-
return value
|
|
282
|
-
}
|
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
package main
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"os"
|
|
5
|
-
"path/filepath"
|
|
6
|
-
"strings"
|
|
7
|
-
"testing"
|
|
8
|
-
)
|
|
9
|
-
|
|
10
|
-
func TestApplyPendingSupervisorUpdate_ConvergesToFailedFromRollbackMarker(t *testing.T) {
|
|
11
|
-
dir := t.TempDir()
|
|
12
|
-
log := NewLogger(filepath.Join(dir, "test.log"))
|
|
13
|
-
defer log.Close()
|
|
14
|
-
|
|
15
|
-
cfg := Config{
|
|
16
|
-
Paths: Paths{
|
|
17
|
-
PendingBinary: filepath.Join(dir, "bin", "sensorium-supervisor.new.exe"),
|
|
18
|
-
PendingVersion: filepath.Join(dir, "bin", "sensorium-supervisor.new.exe.version"),
|
|
19
|
-
SupervisorVersion: filepath.Join(dir, "supervisor-version.txt"),
|
|
20
|
-
UpdateState: filepath.Join(dir, "update-state.json"),
|
|
21
|
-
},
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
if err := os.MkdirAll(filepath.Dir(cfg.Paths.PendingBinary), 0755); err != nil {
|
|
25
|
-
t.Fatalf("create pending dir: %v", err)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
store := NewUpdateStateStore(cfg.Paths.UpdateState, log)
|
|
29
|
-
store.Transition(updateScopeSupervisor, updatePhaseRestarting, "2.0.0", "1.0.0", "")
|
|
30
|
-
|
|
31
|
-
reason := "helper failed to swap pending supervisor binary after retries"
|
|
32
|
-
if err := os.WriteFile(supervisorApplyFailureMarkerPath(cfg), []byte(reason), 0644); err != nil {
|
|
33
|
-
t.Fatalf("write apply failure marker: %v", err)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
shouldRestart, err := applyPendingSupervisorUpdate(cfg, log)
|
|
37
|
-
if err != nil {
|
|
38
|
-
t.Fatalf("applyPendingSupervisorUpdate() error = %v", err)
|
|
39
|
-
}
|
|
40
|
-
if shouldRestart {
|
|
41
|
-
t.Fatal("applyPendingSupervisorUpdate() shouldRestart = true, want false")
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
state, err := store.Load()
|
|
45
|
-
if err != nil {
|
|
46
|
-
t.Fatalf("load state: %v", err)
|
|
47
|
-
}
|
|
48
|
-
if state.Scope != updateScopeSupervisor {
|
|
49
|
-
t.Fatalf("scope = %q, want %q", state.Scope, updateScopeSupervisor)
|
|
50
|
-
}
|
|
51
|
-
if state.Phase != updatePhaseFailed {
|
|
52
|
-
t.Fatalf("phase = %q, want %q", state.Phase, updatePhaseFailed)
|
|
53
|
-
}
|
|
54
|
-
if !strings.Contains(state.LastError, reason) {
|
|
55
|
-
t.Fatalf("last error = %q, want reason %q", state.LastError, reason)
|
|
56
|
-
}
|
|
57
|
-
if !strings.Contains(state.LastError, supervisorRollbackAttemptedMarker) {
|
|
58
|
-
t.Fatalf("last error = %q, missing rollback marker %q", state.LastError, supervisorRollbackAttemptedMarker)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if _, statErr := os.Stat(supervisorApplyFailureMarkerPath(cfg)); !os.IsNotExist(statErr) {
|
|
62
|
-
t.Fatalf("expected apply failure marker to be removed, got: %v", statErr)
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
func TestBuildWindowsApplyHelperScript_TaskModeFailPathRestartsCurrentBinary(t *testing.T) {
|
|
67
|
-
cfg := Config{
|
|
68
|
-
Paths: Paths{
|
|
69
|
-
PendingBinary: `C:\data\bin\sensorium-supervisor.new.exe`,
|
|
70
|
-
PendingVersion: `C:\data\bin\sensorium-supervisor.new.exe.version`,
|
|
71
|
-
SupervisorVersion: `C:\data\supervisor-version.txt`,
|
|
72
|
-
},
|
|
73
|
-
}
|
|
74
|
-
exePath := `C:\data\sensorium-supervisor.exe`
|
|
75
|
-
|
|
76
|
-
script := buildWindowsApplyHelperScript(cfg, exePath, false)
|
|
77
|
-
|
|
78
|
-
failStart := `:fail` + "\r\n" + `set "FAIL_REASON=helper failed to swap pending supervisor binary after retries (pending=C:\data\bin\sensorium-supervisor.new.exe current=C:\data\sensorium-supervisor.exe)"` + "\r\n" + `<nul set /p "=%FAIL_REASON%" > "C:\data\bin\sensorium-supervisor.new.exe.failed"` + "\r\n" + `if exist "C:\data\bin\sensorium-supervisor.new.exe" del /F /Q "C:\data\bin\sensorium-supervisor.new.exe"` + "\r\n" + `if exist "C:\data\bin\sensorium-supervisor.new.exe.version" del /F /Q "C:\data\bin\sensorium-supervisor.new.exe.version"` + "\r\n" + `start "" "C:\data\sensorium-supervisor.exe"`
|
|
79
|
-
if !strings.Contains(script, failStart) {
|
|
80
|
-
t.Fatalf("task-mode fail fallback restart block missing\nscript:\n%s", script)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if !strings.Contains(script, `:applied`+"\r\n"+`if exist "C:\data\bin\sensorium-supervisor.new.exe.version" move /Y "C:\data\bin\sensorium-supervisor.new.exe.version" "C:\data\supervisor-version.txt" >NUL`+"\r\n"+`if exist "C:\data\bin\sensorium-supervisor.new.exe.failed" del /F /Q "C:\data\bin\sensorium-supervisor.new.exe.failed"`+"\r\n"+`start "" "C:\data\sensorium-supervisor.exe"`) {
|
|
84
|
-
t.Fatalf("task-mode applied restart block missing\nscript:\n%s", script)
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
func TestBuildWindowsApplyHelperScript_ServiceModeDoesNotStartBinary(t *testing.T) {
|
|
89
|
-
cfg := Config{
|
|
90
|
-
Paths: Paths{
|
|
91
|
-
PendingBinary: `C:\data\bin\sensorium-supervisor.new.exe`,
|
|
92
|
-
PendingVersion: `C:\data\bin\sensorium-supervisor.new.exe.version`,
|
|
93
|
-
SupervisorVersion: `C:\data\supervisor-version.txt`,
|
|
94
|
-
},
|
|
95
|
-
}
|
|
96
|
-
exePath := `C:\data\sensorium-supervisor.exe`
|
|
97
|
-
|
|
98
|
-
script := buildWindowsApplyHelperScript(cfg, exePath, true)
|
|
99
|
-
if strings.Contains(script, `start "" "C:\data\sensorium-supervisor.exe"`) {
|
|
100
|
-
t.Fatalf("service-mode helper unexpectedly starts supervisor binary\nscript:\n%s", script)
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
serviceFailRestart := `:fail` + "\r\n" + `set "FAIL_REASON=helper failed to swap pending supervisor binary after retries (pending=C:\data\bin\sensorium-supervisor.new.exe current=C:\data\sensorium-supervisor.exe)"` + "\r\n" + `<nul set /p "=%FAIL_REASON%" > "C:\data\bin\sensorium-supervisor.new.exe.failed"` + "\r\n" + `if exist "C:\data\bin\sensorium-supervisor.new.exe" del /F /Q "C:\data\bin\sensorium-supervisor.new.exe"` + "\r\n" + `if exist "C:\data\bin\sensorium-supervisor.new.exe.version" del /F /Q "C:\data\bin\sensorium-supervisor.new.exe.version"` + "\r\n" + `sc start "SensoriumSupervisor" >NUL 2>&1` + "\r\n" + `if errorlevel 1 (` + "\r\n" + ` timeout /T 2 /NOBREAK >NUL` + "\r\n" + ` sc start "SensoriumSupervisor" >NUL 2>&1` + "\r\n" + `)`
|
|
104
|
-
if !strings.Contains(script, serviceFailRestart) {
|
|
105
|
-
t.Fatalf("service-mode fail recovery assist block missing\nscript:\n%s", script)
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
func TestBuildWindowsApplyHelperScript_EscapesFailReasonForCmdSafety(t *testing.T) {
|
|
110
|
-
cfg := Config{
|
|
111
|
-
Paths: Paths{
|
|
112
|
-
PendingBinary: `C:\data\bin\sensorium&supervisor.new%TMP%.exe`,
|
|
113
|
-
PendingVersion: `C:\data\bin\sensorium-supervisor.new.exe.version`,
|
|
114
|
-
SupervisorVersion: `C:\data\supervisor-version.txt`,
|
|
115
|
-
},
|
|
116
|
-
}
|
|
117
|
-
exePath := `C:\data\sensorium"supervisor.exe`
|
|
118
|
-
|
|
119
|
-
script := buildWindowsApplyHelperScript(cfg, exePath, false)
|
|
120
|
-
|
|
121
|
-
if !strings.Contains(script, `set "FAIL_REASON=helper failed to swap pending supervisor binary after retries (pending=C:\data\bin\sensorium&supervisor.new%%TMP%%.exe current=C:\data\sensorium'supervisor.exe)"`) {
|
|
122
|
-
t.Fatalf("expected escaped quoted FAIL_REASON assignment\nscript:\n%s", script)
|
|
123
|
-
}
|
|
124
|
-
if strings.Contains(script, `set FAIL_REASON=`) {
|
|
125
|
-
t.Fatalf("found unsafe unquoted FAIL_REASON assignment\nscript:\n%s", script)
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
func TestRecordPendingSupervisorApplyFailureIfPresent_IgnoresStaleMarkerWhenTargetAlreadyApplied(t *testing.T) {
|
|
130
|
-
dir := t.TempDir()
|
|
131
|
-
log := NewLogger(filepath.Join(dir, "test.log"))
|
|
132
|
-
defer log.Close()
|
|
133
|
-
|
|
134
|
-
targetVersion := "2.0.0"
|
|
135
|
-
cfg := Config{
|
|
136
|
-
Paths: Paths{
|
|
137
|
-
PendingBinary: filepath.Join(dir, "bin", "sensorium-supervisor.new.exe"),
|
|
138
|
-
PendingVersion: filepath.Join(dir, "bin", "sensorium-supervisor.new.exe.version"),
|
|
139
|
-
SupervisorVersion: filepath.Join(dir, "supervisor-version.txt"),
|
|
140
|
-
UpdateState: filepath.Join(dir, "update-state.json"),
|
|
141
|
-
},
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if err := os.MkdirAll(filepath.Dir(cfg.Paths.PendingBinary), 0755); err != nil {
|
|
145
|
-
t.Fatalf("create pending dir: %v", err)
|
|
146
|
-
}
|
|
147
|
-
if err := os.WriteFile(cfg.Paths.SupervisorVersion, []byte(targetVersion), 0644); err != nil {
|
|
148
|
-
t.Fatalf("write supervisor version: %v", err)
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
store := NewUpdateStateStore(cfg.Paths.UpdateState, log)
|
|
152
|
-
store.Transition(updateScopeSupervisor, updatePhaseFailed, targetVersion, "1.0.0", "old failure")
|
|
153
|
-
|
|
154
|
-
if err := os.WriteFile(supervisorApplyFailureMarkerPath(cfg), []byte("stale helper failure"), 0644); err != nil {
|
|
155
|
-
t.Fatalf("write apply failure marker: %v", err)
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
recordPendingSupervisorApplyFailureIfPresent(cfg, log)
|
|
159
|
-
|
|
160
|
-
state, err := store.Load()
|
|
161
|
-
if err != nil {
|
|
162
|
-
t.Fatalf("load state: %v", err)
|
|
163
|
-
}
|
|
164
|
-
if state.Phase != updatePhaseIdle {
|
|
165
|
-
t.Fatalf("phase = %q, want %q", state.Phase, updatePhaseIdle)
|
|
166
|
-
}
|
|
167
|
-
if state.TargetVersion != targetVersion {
|
|
168
|
-
t.Fatalf("target = %q, want %q", state.TargetVersion, targetVersion)
|
|
169
|
-
}
|
|
170
|
-
if state.LastError != "" {
|
|
171
|
-
t.Fatalf("last error = %q, want empty", state.LastError)
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
if _, statErr := os.Stat(supervisorApplyFailureMarkerPath(cfg)); !os.IsNotExist(statErr) {
|
|
175
|
-
t.Fatalf("expected apply failure marker to be removed, got: %v", statErr)
|
|
176
|
-
}
|
|
177
|
-
}
|