Skip to content

Commit 67b677b

Browse files
refactor(updater): expose state and config functions as public API
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
1 parent 468e6a7 commit 67b677b

1 file changed

Lines changed: 189 additions & 0 deletions

File tree

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
//go:build integration
2+
3+
package integration
4+
5+
import (
6+
"encoding/json"
7+
"net/http"
8+
"net/http/httptest"
9+
"os"
10+
"path/filepath"
11+
"testing"
12+
"time"
13+
14+
"github.com/openbootdotdev/openboot/internal/updater"
15+
"github.com/stretchr/testify/assert"
16+
"github.com/stretchr/testify/require"
17+
)
18+
19+
func TestIntegration_Updater_IsHomebrewInstall(t *testing.T) {
20+
// Given: the test binary is running
21+
// When: we check if it's a Homebrew install
22+
result := updater.IsHomebrewInstall()
23+
24+
// Then: returns a bool without panicking (result depends on environment)
25+
assert.IsType(t, false, result)
26+
t.Logf("IsHomebrewInstall: %v", result)
27+
}
28+
29+
func TestIntegration_Updater_AutoUpgrade_DisabledByEnv(t *testing.T) {
30+
// Given: the disable env var is set
31+
t.Setenv("OPENBOOT_DISABLE_AUTOUPDATE", "1")
32+
33+
// When: AutoUpgrade is called
34+
// Then: returns immediately without error or network calls
35+
updater.AutoUpgrade("1.0.0")
36+
}
37+
38+
func TestIntegration_Updater_AutoUpgrade_DevVersion(t *testing.T) {
39+
// Given: auto-update is not disabled
40+
// When: current version is "dev"
41+
// Then: no update attempted (dev builds skip update)
42+
updater.AutoUpgrade("dev")
43+
}
44+
45+
func TestIntegration_Updater_SaveAndLoadState_RoundTrip(t *testing.T) {
46+
// Given: a temp home directory
47+
tmpDir := t.TempDir()
48+
t.Setenv("HOME", tmpDir)
49+
50+
snap := &updater.CheckState{
51+
LastCheck: time.Now().Truncate(time.Second),
52+
LatestVersion: "v99.0.0",
53+
UpdateAvailable: true,
54+
}
55+
56+
// When: we save then load state
57+
require.NoError(t, updater.SaveState(snap))
58+
loaded, err := updater.LoadState()
59+
60+
// Then: data survives the round-trip
61+
require.NoError(t, err)
62+
assert.Equal(t, snap.LatestVersion, loaded.LatestVersion)
63+
assert.Equal(t, snap.UpdateAvailable, loaded.UpdateAvailable)
64+
assert.Equal(t, snap.LastCheck, loaded.LastCheck.Truncate(time.Second))
65+
}
66+
67+
func TestIntegration_Updater_LoadState_FileNotFound(t *testing.T) {
68+
// Given: empty home directory with no state file
69+
t.Setenv("HOME", t.TempDir())
70+
71+
// When: we try to load state
72+
state, err := updater.LoadState()
73+
74+
// Then: returns an error, not a panic
75+
assert.Error(t, err)
76+
assert.Nil(t, state)
77+
}
78+
79+
func TestIntegration_Updater_LoadUserConfig_Default(t *testing.T) {
80+
// Given: no config file exists
81+
t.Setenv("HOME", t.TempDir())
82+
83+
// When: we load user config
84+
cfg := updater.LoadUserConfig()
85+
86+
// Then: defaults to auto-update enabled
87+
assert.Equal(t, updater.AutoUpdateEnabled, cfg.AutoUpdate)
88+
}
89+
90+
func TestIntegration_Updater_LoadUserConfig_AllModes(t *testing.T) {
91+
modes := []updater.AutoUpdateMode{
92+
updater.AutoUpdateEnabled,
93+
updater.AutoUpdateNotify,
94+
updater.AutoUpdateDisabled,
95+
}
96+
97+
for _, mode := range modes {
98+
t.Run(string(mode), func(t *testing.T) {
99+
// Given: a config file with the specified mode
100+
tmpDir := t.TempDir()
101+
t.Setenv("HOME", tmpDir)
102+
cfgDir := filepath.Join(tmpDir, ".openboot")
103+
require.NoError(t, os.MkdirAll(cfgDir, 0755))
104+
data, err := json.Marshal(updater.UserConfig{AutoUpdate: mode})
105+
require.NoError(t, err)
106+
require.NoError(t, os.WriteFile(filepath.Join(cfgDir, "config.json"), data, 0644))
107+
108+
// When: we load the config
109+
cfg := updater.LoadUserConfig()
110+
111+
// Then: mode matches what was written
112+
assert.Equal(t, mode, cfg.AutoUpdate)
113+
})
114+
}
115+
}
116+
117+
func TestIntegration_Updater_GetLatestVersion_RealAPI(t *testing.T) {
118+
if testing.Short() {
119+
t.Skip("skipping real GitHub API call in short mode")
120+
}
121+
122+
// Given: internet access to api.github.com
123+
// When: we fetch the latest release tag
124+
version, err := updater.GetLatestVersion()
125+
126+
// Then: returns a semver-like tag without error
127+
require.NoError(t, err)
128+
assert.NotEmpty(t, version, "latest version should not be empty")
129+
assert.Regexp(t, `^v?\d+\.\d+\.\d+`, version, "version should be semver")
130+
t.Logf("Latest release: %s", version)
131+
}
132+
133+
func TestIntegration_Updater_GetLatestVersion_MockServer(t *testing.T) {
134+
// Given: a mock GitHub API server returning a known version
135+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
136+
assert.Equal(t, "/repos/openbootdotdev/openboot/releases/latest", r.URL.Path)
137+
w.Header().Set("Content-Type", "application/json")
138+
json.NewEncoder(w).Encode(updater.Release{TagName: "v9.9.9"})
139+
}))
140+
defer server.Close()
141+
142+
// When: we hit the mock server directly
143+
resp, err := http.Get(server.URL + "/repos/openbootdotdev/openboot/releases/latest")
144+
require.NoError(t, err)
145+
defer resp.Body.Close()
146+
147+
var release updater.Release
148+
require.NoError(t, json.NewDecoder(resp.Body).Decode(&release))
149+
150+
// Then: tag is parsed correctly
151+
assert.Equal(t, "v9.9.9", release.TagName)
152+
}
153+
154+
func TestIntegration_Updater_NotifyIfUpdateAvailable_WithState(t *testing.T) {
155+
// Given: state file indicates update is available
156+
tmpDir := t.TempDir()
157+
t.Setenv("HOME", tmpDir)
158+
require.NoError(t, updater.SaveState(&updater.CheckState{
159+
LastCheck: time.Now(),
160+
LatestVersion: "v99.0.0",
161+
UpdateAvailable: true,
162+
}))
163+
164+
// When: notify is called with an older current version
165+
// Then: does not panic; prints notification to stdout
166+
updater.NotifyIfUpdateAvailable("1.0.0")
167+
}
168+
169+
func TestIntegration_Updater_NotifyIfUpdateAvailable_AlreadyCurrent(t *testing.T) {
170+
// Given: state file says no update available
171+
tmpDir := t.TempDir()
172+
t.Setenv("HOME", tmpDir)
173+
require.NoError(t, updater.SaveState(&updater.CheckState{
174+
LastCheck: time.Now(),
175+
LatestVersion: "v1.0.0",
176+
UpdateAvailable: false,
177+
}))
178+
179+
// When: notify is called with current version matching latest
180+
// Then: no output, no panic
181+
updater.NotifyIfUpdateAvailable("1.0.0")
182+
}
183+
184+
func TestIntegration_Updater_CheckInterval_Constant(t *testing.T) {
185+
// Given: the check interval constant
186+
// When: we verify its value
187+
// Then: it should be 24 hours (once-a-day check)
188+
assert.Equal(t, 24*time.Hour, updater.CheckInterval)
189+
}

0 commit comments

Comments
 (0)