From c2919b090f52548831b9ca7e9949bd9c698fe3d7 Mon Sep 17 00:00:00 2001 From: Marko Bevc Date: Fri, 17 Apr 2026 17:47:05 +0100 Subject: [PATCH 01/15] feat: improve version checking --- cmd/kosli/main.go | 10 +++++++++ cmd/kosli/root.go | 18 +++++++++------- cmd/kosli/root_test.go | 36 ++++++++++++++++++++++++++++++++ internal/version/update_check.go | 14 ++++++++++--- internal/version/version.go | 2 +- 5 files changed, 69 insertions(+), 11 deletions(-) diff --git a/cmd/kosli/main.go b/cmd/kosli/main.go index ef60bd0cc..a18161da4 100644 --- a/cmd/kosli/main.go +++ b/cmd/kosli/main.go @@ -7,6 +7,7 @@ import ( log "github.com/kosli-dev/cli/internal/logger" "github.com/kosli-dev/cli/internal/requests" + "github.com/kosli-dev/cli/internal/version" "github.com/spf13/cobra" _ "k8s.io/client-go/plugin/pkg/client/auth" ) @@ -43,6 +44,15 @@ func main() { func innerMain(cmd *cobra.Command, args []string) error { err := cmd.Execute() if err == nil { + // Cobra handles --version internally and bypasses all hooks, so we print + // the update notice here after the fact. + if cmd.Root().Flags().Changed("version") { + notice, _ := version.CheckForUpdate(version.GetVersion()) + if notice != "" { + _, _ = fmt.Fprint(logger.ErrOut, notice) + } + } + return nil } diff --git a/cmd/kosli/root.go b/cmd/kosli/root.go index 655e610c1..17224f66b 100644 --- a/cmd/kosli/root.go +++ b/cmd/kosli/root.go @@ -329,13 +329,17 @@ func newRootCmd(out, errOut io.Writer, args []string) (*cobra.Command, error) { // Skip when: // - "version" subcommand: runs the check synchronously itself // - "__complete*": Cobra shell-completion commands fire on every Tab press - // - --version flag: Cobra handles it internally and skips PersistentPostRun, - // so the goroutine result would always be silently discarded - if cmd.Name() != "version" && !strings.HasPrefix(cmd.Name(), "__") && !cmd.Root().Flags().Changed("version") { - go func() { - notice, _ := version.CheckForUpdate(version.GetVersion()) - updateNoticeCh <- notice - }() + // Note: --version is handled by Cobra before any hooks run, so it never + // reaches this point; innerMain handles the notice for that case. + if cmd.Name() != "version" && !strings.HasPrefix(cmd.Name(), "__") { + f := cmd.Flags().Lookup("output") + // skip version checks if using JSON output (programmatic usage) + if f == nil || f.Value.String() != "json" { + go func() { + notice, _ := version.CheckForUpdate(version.GetVersion()) + updateNoticeCh <- notice + }() + } } if global.ApiToken == "DRY_RUN" { diff --git a/cmd/kosli/root_test.go b/cmd/kosli/root_test.go index aa65f130b..5d609dcdf 100644 --- a/cmd/kosli/root_test.go +++ b/cmd/kosli/root_test.go @@ -1,8 +1,10 @@ package main import ( + "fmt" "testing" + "github.com/kosli-dev/cli/internal/version" "github.com/stretchr/testify/suite" ) @@ -30,3 +32,37 @@ func (suite *RootCommandTestSuite) TestConfigProcessing() { func TestRootCommandTestSuite(t *testing.T) { suite.Run(t, new(RootCommandTestSuite)) } + +type UpdateNoticeTestSuite struct { + suite.Suite + defaultKosliArguments string +} + +func (suite *UpdateNoticeTestSuite) SetupTest() { + suite.defaultKosliArguments = fmt.Sprintf("--host %s --org %s --api-token %s", + global.Host, global.Org, global.ApiToken) +} + +func (suite *UpdateNoticeTestSuite) TestVersionNoticeSkippedForJSON() { + const fakeNotice = "\nA new version of the Kosli CLI is available: v9.99.0 (you have v0.0.1)\nUpgrade: https://docs.kosli.com/getting_started/install/\n" + + orig := version.OverrideCheckForUpdate + version.OverrideCheckForUpdate = func(string) (string, error) { return fakeNotice, nil } + defer func() { version.OverrideCheckForUpdate = orig }() + + // with --output json: no notice in stderr + _, _, _, stderr, err := executeCommandC( + fmt.Sprintf("list flows --output json %s", suite.defaultKosliArguments)) + suite.NoError(err) + suite.Empty(stderr) + + // with --output table: notice IS in stderr + _, _, _, stderr, err = executeCommandC( + fmt.Sprintf("list flows --output table %s", suite.defaultKosliArguments)) + suite.NoError(err) + suite.Contains(stderr, "A new version") +} + +func TestUpdateNoticeTestSuite(t *testing.T) { + suite.Run(t, new(UpdateNoticeTestSuite)) +} diff --git a/internal/version/update_check.go b/internal/version/update_check.go index 1cc9ea2e0..1ef52f11a 100644 --- a/internal/version/update_check.go +++ b/internal/version/update_check.go @@ -14,15 +14,21 @@ import ( const ( githubLatestReleaseURL = "https://api.github.com/repos/kosli-dev/cli/releases/latest" - updateCheckTimeout = 2 * time.Second + updateCheckTimeout = 1 * time.Second // max timeout when checking version ) type githubRelease struct { TagName string `json:"tag_name"` } +// OverrideCheckForUpdate may be set in tests to replace the real HTTP check. +var OverrideCheckForUpdate func(currentVersion string) (string, error) + // CheckForUpdate is the public entry point — uses the real GitHub URL func CheckForUpdate(currentVersion string) (string, error) { + if OverrideCheckForUpdate != nil { + return OverrideCheckForUpdate(currentVersion) + } return checkForUpdateWithURL(currentVersion, githubLatestReleaseURL) } @@ -33,11 +39,13 @@ func CheckForUpdate(currentVersion string) (string, error) { // so it never blocks or fails a command. // Set KOSLI_NO_UPDATE_CHECK=1 to skip entirely. func checkForUpdateWithURL(currentVersion string, apiURL string) (string, error) { + // checks disabled -skip if os.Getenv("KOSLI_NO_UPDATE_CHECK") != "" { return "", nil } - if currentVersion == "" || strings.HasPrefix(currentVersion, "main") || strings.Contains(currentVersion, "+unreleased") { - return "", nil // dev build — skip + // dev build — skip + if currentVersion == "" || strings.HasPrefix(currentVersion, "dev") { + return "", nil } // context provides the timeout and not http.Client diff --git a/internal/version/version.go b/internal/version/version.go index d9721821c..6678b8f57 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -11,7 +11,7 @@ var ( // // Increment major number for new feature additions and behavioral changes. // Increment minor number for bug fixes and performance enhancements. - version = "main" // this is overwritten with a release tag in the makefile + version = "dev" // this is overwritten with a release tag in the makefile // metadata is extra build time data metadata = "" From e67765407e9f82a9356bd398b3168e7b061f49cf Mon Sep 17 00:00:00 2001 From: Marko Bevc Date: Fri, 17 Apr 2026 17:57:55 +0100 Subject: [PATCH 02/15] Update internal/version/update_check.go Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> --- internal/version/update_check.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/version/update_check.go b/internal/version/update_check.go index 1ef52f11a..bcc480b68 100644 --- a/internal/version/update_check.go +++ b/internal/version/update_check.go @@ -39,7 +39,7 @@ func CheckForUpdate(currentVersion string) (string, error) { // so it never blocks or fails a command. // Set KOSLI_NO_UPDATE_CHECK=1 to skip entirely. func checkForUpdateWithURL(currentVersion string, apiURL string) (string, error) { - // checks disabled -skip + // checks disabled — skip if os.Getenv("KOSLI_NO_UPDATE_CHECK") != "" { return "", nil } From 346d90fe22ee0ab0a4b08d1016cfdefc5d4862cc Mon Sep 17 00:00:00 2001 From: Marko Bevc Date: Fri, 17 Apr 2026 18:56:43 +0100 Subject: [PATCH 03/15] feat: improve version checking - fix tests --- internal/version/update_check_test.go | 2 +- internal/version/version_test.go | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/version/update_check_test.go b/internal/version/update_check_test.go index 31cd503a9..241cf97d5 100644 --- a/internal/version/update_check_test.go +++ b/internal/version/update_check_test.go @@ -55,7 +55,7 @@ func TestCheckForUpdate_OptOut(t *testing.T) { func TestCheckForUpdate_DevBuild(t *testing.T) { // dev builds should be skipped without any HTTP call - notice, err := checkForUpdateWithURL("main", "http://should-not-be-called") + notice, err := checkForUpdateWithURL("dev", "http://should-not-be-called") assert.NoError(t, err) assert.Empty(t, notice) } diff --git a/internal/version/version_test.go b/internal/version/version_test.go index 44a25fc7c..94024c9e6 100644 --- a/internal/version/version_test.go +++ b/internal/version/version_test.go @@ -18,7 +18,7 @@ type VersionTestSuite struct { // reset the variables before each test func (suite *VersionTestSuite) SetupTest() { - version = "main" + version = "dev" metadata = "" gitCommit = "" gitTreeState = "" @@ -37,18 +37,18 @@ func (suite *VersionTestSuite) TestGetVersion() { want string }{ { - name: "version is main when metadata is empty.", + name: "version is dev when metadata is empty.", args: args{ metadata: "", }, - want: "main", + want: "dev", }, { name: "version is suffixed with metadat when metadata is not empty.", args: args{ metadata: "bla", }, - want: "main+bla", + want: "dev+bla", }, { name: "default version is overwritten when provided and there is metadata.", From c738eff7387dfce44e79d95294ae8eef8ab8f052 Mon Sep 17 00:00:00 2001 From: Marko Bevc Date: Fri, 17 Apr 2026 19:32:23 +0100 Subject: [PATCH 04/15] feat: improve version checking - review improvements --- cmd/kosli/root_test.go | 9 ++++++--- internal/version/update_check.go | 17 +++++++++++++---- internal/version/update_check_test.go | 2 +- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/cmd/kosli/root_test.go b/cmd/kosli/root_test.go index 5d609dcdf..5bc4b79af 100644 --- a/cmd/kosli/root_test.go +++ b/cmd/kosli/root_test.go @@ -39,6 +39,11 @@ type UpdateNoticeTestSuite struct { } func (suite *UpdateNoticeTestSuite) SetupTest() { + global = &GlobalOpts{ + ApiToken: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6ImNkNzg4OTg5In0.e8i_lA_QrEhFncb05Xw6E_tkCHU9QfcY4OLTVUCHffY", + Org: "docs-cmd-test-user", + Host: "http://localhost:8001", + } suite.defaultKosliArguments = fmt.Sprintf("--host %s --org %s --api-token %s", global.Host, global.Org, global.ApiToken) } @@ -46,9 +51,7 @@ func (suite *UpdateNoticeTestSuite) SetupTest() { func (suite *UpdateNoticeTestSuite) TestVersionNoticeSkippedForJSON() { const fakeNotice = "\nA new version of the Kosli CLI is available: v9.99.0 (you have v0.0.1)\nUpgrade: https://docs.kosli.com/getting_started/install/\n" - orig := version.OverrideCheckForUpdate - version.OverrideCheckForUpdate = func(string) (string, error) { return fakeNotice, nil } - defer func() { version.OverrideCheckForUpdate = orig }() + defer version.SetCheckForUpdateOverride(func(string) (string, error) { return fakeNotice, nil })() // with --output json: no notice in stderr _, _, _, stderr, err := executeCommandC( diff --git a/internal/version/update_check.go b/internal/version/update_check.go index bcc480b68..f801e36dc 100644 --- a/internal/version/update_check.go +++ b/internal/version/update_check.go @@ -21,13 +21,22 @@ type githubRelease struct { TagName string `json:"tag_name"` } -// OverrideCheckForUpdate may be set in tests to replace the real HTTP check. -var OverrideCheckForUpdate func(currentVersion string) (string, error) +// overrideCheckForUpdate may be set by tests (via SetCheckForUpdateOverride) +// to replace the real HTTP check. +var overrideCheckForUpdate func(currentVersion string) (string, error) + +// SetCheckForUpdateOverride replaces the implementation used by CheckForUpdate +// with fn and returns a function that restores the previous value. Tests only. +func SetCheckForUpdateOverride(fn func(currentVersion string) (string, error)) func() { + old := overrideCheckForUpdate + overrideCheckForUpdate = fn + return func() { overrideCheckForUpdate = old } +} // CheckForUpdate is the public entry point — uses the real GitHub URL func CheckForUpdate(currentVersion string) (string, error) { - if OverrideCheckForUpdate != nil { - return OverrideCheckForUpdate(currentVersion) + if overrideCheckForUpdate != nil { + return overrideCheckForUpdate(currentVersion) } return checkForUpdateWithURL(currentVersion, githubLatestReleaseURL) } diff --git a/internal/version/update_check_test.go b/internal/version/update_check_test.go index 241cf97d5..3da26ec12 100644 --- a/internal/version/update_check_test.go +++ b/internal/version/update_check_test.go @@ -61,7 +61,7 @@ func TestCheckForUpdate_DevBuild(t *testing.T) { } func TestCheckForUpdate_UnreleasedBuild(t *testing.T) { - notice, err := checkForUpdateWithURL("v1.0.0+unreleased", "http://should-not-be-called") + notice, err := checkForUpdateWithURL("dev+unreleased", "http://should-not-be-called") assert.NoError(t, err) assert.Empty(t, notice) } From 0974bbe1f043fb545a5e847bca97b6c23d07cfc4 Mon Sep 17 00:00:00 2001 From: Marko Bevc Date: Fri, 17 Apr 2026 23:04:06 +0100 Subject: [PATCH 05/15] feat: improve version checking - fix testing --- cmd/kosli/version_test.go | 4 ++-- internal/version/update_check_test.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/kosli/version_test.go b/cmd/kosli/version_test.go index 52b272427..0339857e0 100644 --- a/cmd/kosli/version_test.go +++ b/cmd/kosli/version_test.go @@ -20,11 +20,11 @@ func (suite *VersionTestSuite) TestVersionCmd() { { name: "default", cmd: "version", - golden: fmt.Sprintf("version.BuildInfo{Version:\"main\", GitCommit:\"\", GitTreeState:\"\", GoVersion:\"%s\"}\n", runtime.Version()), + golden: fmt.Sprintf("version.BuildInfo{Version:\"dev\", GitCommit:\"\", GitTreeState:\"\", GoVersion:\"%s\"}\n", runtime.Version()), }, { name: "short", cmd: "version --short", - golden: "main\n", + golden: "dev\n", }, } runTestCmd(suite.T(), tests) diff --git a/internal/version/update_check_test.go b/internal/version/update_check_test.go index 3da26ec12..180d5a0aa 100644 --- a/internal/version/update_check_test.go +++ b/internal/version/update_check_test.go @@ -60,7 +60,7 @@ func TestCheckForUpdate_DevBuild(t *testing.T) { assert.Empty(t, notice) } -func TestCheckForUpdate_UnreleasedBuild(t *testing.T) { +func TestCheckForUpdate_DevBuildWithMetadata(t *testing.T) { notice, err := checkForUpdateWithURL("dev+unreleased", "http://should-not-be-called") assert.NoError(t, err) assert.Empty(t, notice) From 8691d3f91cb4c37e851b809b211728194acad43d Mon Sep 17 00:00:00 2001 From: Marko Bevc Date: Fri, 17 Apr 2026 23:22:11 +0100 Subject: [PATCH 06/15] feat: improve version checking - add version test --- cmd/kosli/root_test.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/cmd/kosli/root_test.go b/cmd/kosli/root_test.go index 5bc4b79af..d86202fe7 100644 --- a/cmd/kosli/root_test.go +++ b/cmd/kosli/root_test.go @@ -1,7 +1,9 @@ package main import ( + "bytes" "fmt" + "io" "testing" "github.com/kosli-dev/cli/internal/version" @@ -48,6 +50,22 @@ func (suite *UpdateNoticeTestSuite) SetupTest() { global.Host, global.Org, global.ApiToken) } +func (suite *UpdateNoticeTestSuite) TestVersionFlagPrintsNotice() { + const fakeNotice = "\nA new version of the Kosli CLI is available: v9.99.0 (you have v0.0.1)\nUpgrade: https://docs.kosli.com/getting_started/install/\n" + defer version.SetCheckForUpdateOverride(func(string) (string, error) { return fakeNotice, nil })() + + var errBuf bytes.Buffer + origErrOut := logger.ErrOut + logger.ErrOut = &errBuf + defer func() { logger.ErrOut = origErrOut }() + + cmd, err := newRootCmd(io.Discard, &errBuf, []string{"--version"}) + suite.Require().NoError(err) + + suite.NoError(innerMain(cmd, []string{"kosli", "--version"})) + suite.Contains(errBuf.String(), "A new version") +} + func (suite *UpdateNoticeTestSuite) TestVersionNoticeSkippedForJSON() { const fakeNotice = "\nA new version of the Kosli CLI is available: v9.99.0 (you have v0.0.1)\nUpgrade: https://docs.kosli.com/getting_started/install/\n" From 3f0f2957fac4797c3463778bae29e1ffb8b89175 Mon Sep 17 00:00:00 2001 From: Marko Bevc Date: Fri, 17 Apr 2026 23:37:38 +0100 Subject: [PATCH 07/15] feat: improve version checking - fix version test --- cmd/kosli/root_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/kosli/root_test.go b/cmd/kosli/root_test.go index d86202fe7..543c80baa 100644 --- a/cmd/kosli/root_test.go +++ b/cmd/kosli/root_test.go @@ -62,6 +62,7 @@ func (suite *UpdateNoticeTestSuite) TestVersionFlagPrintsNotice() { cmd, err := newRootCmd(io.Discard, &errBuf, []string{"--version"}) suite.Require().NoError(err) + cmd.SetArgs([]string{"--version"}) suite.NoError(innerMain(cmd, []string{"kosli", "--version"})) suite.Contains(errBuf.String(), "A new version") } From beb12940e93853c4f81d1efc3b8689dca4da9129 Mon Sep 17 00:00:00 2001 From: Marko Bevc Date: Sat, 18 Apr 2026 13:57:11 +0100 Subject: [PATCH 08/15] chore: Update internal/version/update_check.go --- internal/version/update_check.go | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/internal/version/update_check.go b/internal/version/update_check.go index f801e36dc..3db28aede 100644 --- a/internal/version/update_check.go +++ b/internal/version/update_check.go @@ -23,20 +23,32 @@ type githubRelease struct { // overrideCheckForUpdate may be set by tests (via SetCheckForUpdateOverride) // to replace the real HTTP check. -var overrideCheckForUpdate func(currentVersion string) (string, error) +var ( + overrideMu sync.RWMutex + overrideCheckForUpdate func(currentVersion string) (string, error) +) // SetCheckForUpdateOverride replaces the implementation used by CheckForUpdate // with fn and returns a function that restores the previous value. Tests only. func SetCheckForUpdateOverride(fn func(currentVersion string) (string, error)) func() { + overrideMu.Lock() old := overrideCheckForUpdate overrideCheckForUpdate = fn - return func() { overrideCheckForUpdate = old } + overrideMu.Unlock() + return func() { + overrideMu.Lock() + overrideCheckForUpdate = old + overrideMu.Unlock() + } } // CheckForUpdate is the public entry point — uses the real GitHub URL func CheckForUpdate(currentVersion string) (string, error) { - if overrideCheckForUpdate != nil { - return overrideCheckForUpdate(currentVersion) + overrideMu.RLock() + fn := overrideCheckForUpdate + overrideMu.RUnlock() + if fn != nil { + return fn(currentVersion) } return checkForUpdateWithURL(currentVersion, githubLatestReleaseURL) } From b8b063e83bd1ef1244a127fca66325f331009ac4 Mon Sep 17 00:00:00 2001 From: Marko Bevc Date: Sat, 18 Apr 2026 13:59:16 +0100 Subject: [PATCH 09/15] feat: improve version checking - fix missing import --- internal/version/update_check.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/version/update_check.go b/internal/version/update_check.go index 3db28aede..e8eb50130 100644 --- a/internal/version/update_check.go +++ b/internal/version/update_check.go @@ -7,6 +7,7 @@ import ( "net/http" "os" "strings" + "sync" "time" semver "github.com/Masterminds/semver/v3" From 9a4e45e76c1b28f2293e7dd9ff78a274bb2727ba Mon Sep 17 00:00:00 2001 From: Marko Bevc Date: Sat, 18 Apr 2026 14:05:57 +0100 Subject: [PATCH 10/15] chore: Update cmd/kosli/root.go future prof machine formatting Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> --- cmd/kosli/root.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/kosli/root.go b/cmd/kosli/root.go index 17224f66b..4ef01637a 100644 --- a/cmd/kosli/root.go +++ b/cmd/kosli/root.go @@ -334,7 +334,7 @@ func newRootCmd(out, errOut io.Writer, args []string) (*cobra.Command, error) { if cmd.Name() != "version" && !strings.HasPrefix(cmd.Name(), "__") { f := cmd.Flags().Lookup("output") // skip version checks if using JSON output (programmatic usage) - if f == nil || f.Value.String() != "json" { + if f == nil || f.Value.String() == "table" { go func() { notice, _ := version.CheckForUpdate(version.GetVersion()) updateNoticeCh <- notice From 618fdd9878bc72f3da538da27d1937dedee91a90 Mon Sep 17 00:00:00 2001 From: Marko Bevc Date: Sat, 18 Apr 2026 14:12:06 +0100 Subject: [PATCH 11/15] chore: Update cmd/kosli/root.go update the comment as well Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> --- cmd/kosli/root.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cmd/kosli/root.go b/cmd/kosli/root.go index 4ef01637a..1eb589dbb 100644 --- a/cmd/kosli/root.go +++ b/cmd/kosli/root.go @@ -332,8 +332,7 @@ func newRootCmd(out, errOut io.Writer, args []string) (*cobra.Command, error) { // Note: --version is handled by Cobra before any hooks run, so it never // reaches this point; innerMain handles the notice for that case. if cmd.Name() != "version" && !strings.HasPrefix(cmd.Name(), "__") { - f := cmd.Flags().Lookup("output") - // skip version checks if using JSON output (programmatic usage) + // skip version checks if not using table output (programmatic usage) if f == nil || f.Value.String() == "table" { go func() { notice, _ := version.CheckForUpdate(version.GetVersion()) From 0480c6d7ec6e3445eb5cfca0db5a3a7025c6962f Mon Sep 17 00:00:00 2001 From: Marko Bevc Date: Sat, 18 Apr 2026 14:16:11 +0100 Subject: [PATCH 12/15] feat: improve version checking - add race test --- internal/version/update_check_test.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/internal/version/update_check_test.go b/internal/version/update_check_test.go index 180d5a0aa..fca7dfb73 100644 --- a/internal/version/update_check_test.go +++ b/internal/version/update_check_test.go @@ -4,6 +4,7 @@ import ( "fmt" "net/http" "net/http/httptest" + "sync" "testing" "github.com/stretchr/testify/assert" @@ -93,3 +94,24 @@ func TestCheckForUpdate_Non200(t *testing.T) { assert.NoError(t, err) assert.Empty(t, notice) } + +func TestSetCheckForUpdateOverride_Race(t *testing.T) { + fake := func(string) (string, error) { return "notice", nil } + var wg sync.WaitGroup + for i := 0; i < 50; i++ { + wg.Add(1) + go func() { + defer wg.Done() + _, _ = CheckForUpdate("v1.2.3") + }() + } + for i := 0; i < 50; i++ { + wg.Add(1) + go func() { + defer wg.Done() + restore := SetCheckForUpdateOverride(fake) + restore() + }() + } + wg.Wait() +} From e32e285dcdf72ee5815b3267fa842ae3f4232592 Mon Sep 17 00:00:00 2001 From: Marko Bevc Date: Sat, 18 Apr 2026 14:18:57 +0100 Subject: [PATCH 13/15] fix: AI broken suggestion --- cmd/kosli/root.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/kosli/root.go b/cmd/kosli/root.go index 1eb589dbb..bf9f1fe5e 100644 --- a/cmd/kosli/root.go +++ b/cmd/kosli/root.go @@ -332,6 +332,7 @@ func newRootCmd(out, errOut io.Writer, args []string) (*cobra.Command, error) { // Note: --version is handled by Cobra before any hooks run, so it never // reaches this point; innerMain handles the notice for that case. if cmd.Name() != "version" && !strings.HasPrefix(cmd.Name(), "__") { + f := cmd.Flags().Lookup("output") // skip version checks if not using table output (programmatic usage) if f == nil || f.Value.String() == "table" { go func() { From d0ed8a8cc294e3c09f9e115d6a88769eb5a03a73 Mon Sep 17 00:00:00 2001 From: Marko Bevc Date: Sat, 18 Apr 2026 14:28:53 +0100 Subject: [PATCH 14/15] feat: improve version checking - improve fragile testing --- cmd/kosli/root_test.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cmd/kosli/root_test.go b/cmd/kosli/root_test.go index 543c80baa..024c54c7e 100644 --- a/cmd/kosli/root_test.go +++ b/cmd/kosli/root_test.go @@ -62,8 +62,15 @@ func (suite *UpdateNoticeTestSuite) TestVersionFlagPrintsNotice() { cmd, err := newRootCmd(io.Discard, &errBuf, []string{"--version"}) suite.Require().NoError(err) + var called bool + defer version.SetCheckForUpdateOverride(func(string) (string, error) { + called = true + return fakeNotice, nil + })() + cmd.SetArgs([]string{"--version"}) suite.NoError(innerMain(cmd, []string{"kosli", "--version"})) + suite.True(called, "expected CheckForUpdate override to be called for --version") suite.Contains(errBuf.String(), "A new version") } From 92066b8eab738d32fc7edaf0235203eada891372 Mon Sep 17 00:00:00 2001 From: Marko Bevc Date: Sat, 18 Apr 2026 14:39:21 +0100 Subject: [PATCH 15/15] feat: improve version checking - remove duplicated and add empty version test --- cmd/kosli/root_test.go | 1 - internal/version/update_check_test.go | 6 ++++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/cmd/kosli/root_test.go b/cmd/kosli/root_test.go index 024c54c7e..9c4d13f56 100644 --- a/cmd/kosli/root_test.go +++ b/cmd/kosli/root_test.go @@ -52,7 +52,6 @@ func (suite *UpdateNoticeTestSuite) SetupTest() { func (suite *UpdateNoticeTestSuite) TestVersionFlagPrintsNotice() { const fakeNotice = "\nA new version of the Kosli CLI is available: v9.99.0 (you have v0.0.1)\nUpgrade: https://docs.kosli.com/getting_started/install/\n" - defer version.SetCheckForUpdateOverride(func(string) (string, error) { return fakeNotice, nil })() var errBuf bytes.Buffer origErrOut := logger.ErrOut diff --git a/internal/version/update_check_test.go b/internal/version/update_check_test.go index fca7dfb73..5dc14b397 100644 --- a/internal/version/update_check_test.go +++ b/internal/version/update_check_test.go @@ -54,6 +54,12 @@ func TestCheckForUpdate_OptOut(t *testing.T) { assert.Empty(t, notice) } +func TestCheckForUpdate_EmptyVersion(t *testing.T) { + notice, err := checkForUpdateWithURL("", "http://should-not-be-called") + assert.NoError(t, err) + assert.Empty(t, notice) +} + func TestCheckForUpdate_DevBuild(t *testing.T) { // dev builds should be skipped without any HTTP call notice, err := checkForUpdateWithURL("dev", "http://should-not-be-called")