Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- Update command no longer migrates provider models using stale defaults from the pre-update binary

## [2.3.5] - 2026-03-29

### Changed
Expand Down
30 changes: 15 additions & 15 deletions cmd/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ var httpClient = &http.Client{
Timeout: requestTimeout,
}

var (
getLatestReleaseFn = getLatestRelease
confirmUpdateFn = ui.Confirm
downloadToTempFileFn = downloadToTempFile
downloadAndParseChecksumsFn = downloadAndParseChecksums
verifyChecksumFn = verifyChecksum
runInstallScriptFn = runInstallScript
)

type release struct {
TagName string `json:"tag_name"`
HTMLURL string `json:"html_url"`
Expand Down Expand Up @@ -363,7 +372,7 @@ https://github.com/dkmnx/kairo/blob/<tag>/scripts/checksums.txt`,
return
}

latest, err := getLatestRelease()
latest, err := getLatestReleaseFn()
if err != nil {
ui.PrintError(fmt.Sprintf("Error checking for updates: %v", err))

Expand All @@ -380,7 +389,7 @@ https://github.com/dkmnx/kairo/blob/<tag>/scripts/checksums.txt`,

installScriptURL := getInstallScriptURL(runtime.GOOS, latest.TagName)

confirmed, err := ui.Confirm("Do you want to proceed with installation?")
confirmed, err := confirmUpdateFn("Do you want to proceed with installation?")
if err != nil {
ui.PrintError(fmt.Sprintf("Error reading input: %v", err))

Expand All @@ -394,7 +403,7 @@ https://github.com/dkmnx/kairo/blob/<tag>/scripts/checksums.txt`,

cmd.Printf("\nDownloading install script from: %s\n", installScriptURL)

tempFile, err := downloadToTempFile(installScriptURL)
tempFile, err := downloadToTempFileFn(installScriptURL)
if err != nil {
ui.PrintError(fmt.Sprintf("Error downloading install script: %v", err))

Expand All @@ -407,7 +416,7 @@ https://github.com/dkmnx/kairo/blob/<tag>/scripts/checksums.txt`,

cmd.Printf("Downloading checksums from: %s\n", checksumsURL)

checksums, err := downloadAndParseChecksums(checksumsURL)
checksums, err := downloadAndParseChecksumsFn(checksumsURL)
if err != nil {
ui.PrintError(fmt.Sprintf("Error downloading checksums: %v", err))

Expand All @@ -423,7 +432,7 @@ https://github.com/dkmnx/kairo/blob/<tag>/scripts/checksums.txt`,

cmd.Printf("Verifying script integrity...\n")

if err := verifyChecksum(tempFile, expectedHash); err != nil {
if err := verifyChecksumFn(tempFile, expectedHash); err != nil {
ui.PrintError(fmt.Sprintf("Security verification failed: %v", err))
cmd.Println("Downloaded script has been removed. Please try again later or report this issue.")

Expand All @@ -432,21 +441,12 @@ https://github.com/dkmnx/kairo/blob/<tag>/scripts/checksums.txt`,

cmd.Printf("Running install script...\n\n")

if err := runInstallScript(tempFile); err != nil {
if err := runInstallScriptFn(tempFile); err != nil {
ui.PrintError(fmt.Sprintf("Error during installation: %v", err))

return
}

dir := GetCLIContext(cmd).GetConfigDir()
if dir != "" {
changes, err := config.MigrateConfigOnUpdate(context.Background(), dir)
if err != nil {
cmd.Printf("Warning: config migration failed: %v\n", err)
} else if len(changes) > 0 {
cmd.Printf("%s\n", config.FormatMigrationChanges(changes))
}
}
},
}

Expand Down
71 changes: 71 additions & 0 deletions cmd/update_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cmd

import (
"context"
"maps"
"net/http"
"net/http/httptest"
Expand All @@ -26,6 +27,76 @@ func HijackAndClose(w http.ResponseWriter) {
conn.Close()
}

func TestUpdateCommand_DoesNotMigrateConfigAfterInstall(t *testing.T) {
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, "config.yaml")
configContent := `default_provider: zai
providers:
zai:
name: Z.AI
base_url: https://api.z.ai/api/anthropic
model: glm-4.7
`
if err := os.WriteFile(configPath, []byte(configContent), 0600); err != nil {
t.Fatalf("failed to write config: %v", err)
}

originalVersion := version.Version
version.Version = "v2.3.4"
defer func() { version.Version = originalVersion }()

originalGetLatestReleaseFn := getLatestReleaseFn
originalConfirmUpdateFn := confirmUpdateFn
originalDownloadToTempFileFn := downloadToTempFileFn
originalDownloadAndParseChecksumsFn := downloadAndParseChecksumsFn
originalVerifyChecksumFn := verifyChecksumFn
originalRunInstallScriptFn := runInstallScriptFn
defer func() {
getLatestReleaseFn = originalGetLatestReleaseFn
confirmUpdateFn = originalConfirmUpdateFn
downloadToTempFileFn = originalDownloadToTempFileFn
downloadAndParseChecksumsFn = originalDownloadAndParseChecksumsFn
verifyChecksumFn = originalVerifyChecksumFn
runInstallScriptFn = originalRunInstallScriptFn
}()

getLatestReleaseFn = func() (*release, error) {
return &release{TagName: "v2.3.5"}, nil
}
confirmUpdateFn = func(string) (bool, error) {
return true, nil
}
tempScriptPath := filepath.Join(tmpDir, "install.sh")
if err := os.WriteFile(tempScriptPath, []byte("#!/bin/sh\nexit 0\n"), 0755); err != nil {
t.Fatalf("failed to write temp script: %v", err)
}
downloadToTempFileFn = func(string) (string, error) {
return tempScriptPath, nil
}
downloadAndParseChecksumsFn = func(string) (map[string]string, error) {
return map[string]string{getScriptNameForChecksums(runtime.GOOS): "ignored"}, nil
}
verifyChecksumFn = func(string, string) error {
return nil
}
runInstallScriptFn = func(string) error {
return nil
}

cliCtx := NewCLIContext()
cliCtx.SetConfigDir(tmpDir)
updateCmd.SetContext(WithCLIContext(context.Background(), cliCtx))
updateCmd.Run(updateCmd, nil)

updatedConfig, err := os.ReadFile(configPath)
if err != nil {
t.Fatalf("failed to read config after update: %v", err)
}
if !strings.Contains(string(updatedConfig), "model: glm-4.7") {
t.Fatalf("update command should not migrate config after install, got: %s", string(updatedConfig))
}
}

func TestUpdateCommand(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/repos/dkmnx/kairo/releases/latest" {
Expand Down
Loading