diff --git a/internal/archtest/baseline/no-raw-http.txt b/internal/archtest/baseline/no-raw-http.txt index 1a417fd..3d1a936 100644 --- a/internal/archtest/baseline/no-raw-http.txt +++ b/internal/archtest/baseline/no-raw-http.txt @@ -12,4 +12,4 @@ internal/config/remote.go:233 internal/search/search.go:46 internal/shell/shell.go:28 internal/shell/shell.go:62 -internal/updater/updater.go:785 +internal/updater/updater.go:792 diff --git a/internal/cli/update.go b/internal/cli/update.go index 2a18050..67a707e 100644 --- a/internal/cli/update.go +++ b/internal/cli/update.go @@ -22,6 +22,7 @@ var ( updateIsHomebrewInstall = updater.IsHomebrewInstall updateGetLatestVersion = updater.GetLatestVersion updateDownloadAndReplace = updater.DownloadAndReplace + updateBrewUpgrade = updater.BrewUpgrade updateRollbackFn = updater.Rollback updateListBackupsFn = updater.ListBackups updateGetBackupDir = updater.GetBackupDir @@ -154,9 +155,16 @@ func runPinnedUpgrade(v string) error { func runLatestUpgrade() error { if updateIsHomebrewInstall() { - ui.Warn("OpenBoot is managed by Homebrew.") - ui.Muted("Run 'brew upgrade openboot' to update.") - return fmt.Errorf("use 'brew upgrade openboot'") + if updateDryRun { + ui.Info("Dry-run: would run 'brew upgrade openboot'.") + return nil + } + ui.Info("Updating OpenBoot via Homebrew...") + if err := updateBrewUpgrade(); err != nil { + return fmt.Errorf("update failed: %w", err) + } + ui.Success("Update complete.") + return nil } if updateDryRun { ui.Info("Dry-run: would check GitHub for the latest release and upgrade.") diff --git a/internal/cli/update_test.go b/internal/cli/update_test.go index 139278b..30629d6 100644 --- a/internal/cli/update_test.go +++ b/internal/cli/update_test.go @@ -121,13 +121,45 @@ func TestRunUpdate_HomebrewRefusesRollback(t *testing.T) { assert.Contains(t, err.Error(), "Homebrew") } -func TestRunUpdate_HomebrewRefusesLatest(t *testing.T) { +func TestRunUpdate_HomebrewLatestCallsBrew(t *testing.T) { resetUpdateFlags(t) stubSeams(t, func() bool { return true }, nil, nil, nil, nil, nil) + called := 0 + orig := updateBrewUpgrade + updateBrewUpgrade = func() error { called++; return nil } + t.Cleanup(func() { updateBrewUpgrade = orig }) + + err := runUpdateCmd(nil, nil) + require.NoError(t, err) + assert.Equal(t, 1, called, "brew upgrade should be invoked exactly once") +} + +func TestRunUpdate_HomebrewLatestPropagatesBrewError(t *testing.T) { + resetUpdateFlags(t) + stubSeams(t, func() bool { return true }, nil, nil, nil, nil, nil) + + orig := updateBrewUpgrade + updateBrewUpgrade = func() error { return errors.New("brew exit 1") } + t.Cleanup(func() { updateBrewUpgrade = orig }) + err := runUpdateCmd(nil, nil) require.Error(t, err) - assert.Contains(t, err.Error(), "brew upgrade openboot") + assert.Contains(t, err.Error(), "update failed") + assert.Contains(t, err.Error(), "brew exit 1") +} + +func TestRunUpdate_HomebrewLatestDryRunSkipsBrew(t *testing.T) { + resetUpdateFlags(t) + updateDryRun = true + stubSeams(t, func() bool { return true }, nil, nil, nil, nil, nil) + + orig := updateBrewUpgrade + updateBrewUpgrade = func() error { t.Fatal("brew should not run in dry-run"); return nil } + t.Cleanup(func() { updateBrewUpgrade = orig }) + + err := runUpdateCmd(nil, nil) + require.NoError(t, err) } func TestRunUpdate_HomebrewAllowsListBackups(t *testing.T) { diff --git a/internal/updater/updater.go b/internal/updater/updater.go index 288d7f5..d611660 100644 --- a/internal/updater/updater.go +++ b/internal/updater/updater.go @@ -217,6 +217,13 @@ var execBrewUpgrade = func(formula string) error { return nil } +// BrewUpgrade pulls the openboot tap and runs `brew upgrade openboot`. +// It is the public entry point used by the manual `openboot update` command; +// AutoUpgrade calls execBrewUpgrade directly. +func BrewUpgrade() error { + return execBrewUpgrade(brewFormula) +} + func doBrewUpgrade(currentVersion, latestVersion string) { latestClean := TrimVersionPrefix(latestVersion) currentClean := TrimVersionPrefix(currentVersion)