Skip to content

Commit 71b446a

Browse files
authored
fix(config): return ("", nil) from GlobalVersion when unconfigured (#221)
1 parent 455617d commit 71b446a

4 files changed

Lines changed: 79 additions & 9 deletions

File tree

src/cmd/install_test.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cmd
22

33
import (
4+
"fmt"
45
"testing"
56

67
"github.com/CodingWithCalvin/dtvem.cli/src/internal/runtime"
@@ -11,6 +12,7 @@ type mockProvider struct {
1112
name string
1213
displayName string
1314
globalVersion string
15+
globalVersionErr error
1416
globalSetError error
1517
setGlobalCalls []string
1618
availableVersions []runtime.AvailableVersion
@@ -53,7 +55,7 @@ func (m *mockProvider) GetEnvironment(_ string) (map[string]string, error) {
5355
}
5456

5557
func (m *mockProvider) GlobalVersion() (string, error) {
56-
return m.globalVersion, nil
58+
return m.globalVersion, m.globalVersionErr
5759
}
5860

5961
func (m *mockProvider) SetGlobalVersion(version string) error {
@@ -225,6 +227,21 @@ func TestResolveVersionForProvider_NoMatch(t *testing.T) {
225227
}
226228
}
227229

230+
func TestAutoSetGlobalIfNeeded_GlobalVersionError(t *testing.T) {
231+
provider := &mockProvider{
232+
name: "test",
233+
displayName: "Test",
234+
globalVersion: "",
235+
globalVersionErr: fmt.Errorf("permission denied"),
236+
}
237+
238+
autoSetGlobalIfNeeded(provider, "1.0.0")
239+
240+
if len(provider.setGlobalCalls) != 0 {
241+
t.Errorf("Expected SetGlobalVersion to not be called when GlobalVersion returns error, got %d calls", len(provider.setGlobalCalls))
242+
}
243+
}
244+
228245
func TestResolveVersionForProvider_PythonVersions(t *testing.T) {
229246
provider := &mockProvider{
230247
name: "python",

src/internal/config/version.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"os"
77
"path/filepath"
8+
"strings"
89
)
910

1011
// RuntimesConfig represents the flat structure of runtimes.json
@@ -142,16 +143,24 @@ func LocalVersion(runtimeName string) (string, error) {
142143
return findLocalVersion(runtimeName)
143144
}
144145

145-
// GlobalVersion reads the global version for a runtime
146+
// GlobalVersion reads the global version for a runtime.
147+
// Returns ("", nil) when no version is configured (file missing or runtime not in config).
148+
// Returns an error only for actual failures (I/O errors, corrupt JSON).
146149
func GlobalVersion(runtimeName string) (string, error) {
147150
configPath := GlobalConfigPath()
148151

149-
// Check if config file exists
152+
// No config file yet — valid "unconfigured" state
150153
if _, err := os.Stat(configPath); os.IsNotExist(err) {
151-
return "", fmt.Errorf("no global version configured")
154+
return "", nil
152155
}
153156

154-
return readVersionFile(configPath, runtimeName)
157+
version, err := readVersionFile(configPath, runtimeName)
158+
if err != nil && strings.Contains(err.Error(), "not found in config file") {
159+
// Runtime not in config — valid "unconfigured" state
160+
return "", nil
161+
}
162+
163+
return version, err
155164
}
156165

157166
// SetGlobalVersion sets the global version for a runtime

src/internal/config/version_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,46 @@ func TestSetGlobalVersion_MultipleRuntimes(t *testing.T) {
442442
}
443443
}
444444

445+
func TestGlobalVersion_NoConfigFile(t *testing.T) {
446+
// On a clean system with no config file, GlobalVersion should return ("", nil)
447+
tmpRoot := t.TempDir()
448+
t.Setenv("HOME", tmpRoot)
449+
t.Setenv("USERPROFILE", tmpRoot)
450+
ResetPathsCache()
451+
defer ResetPathsCache()
452+
453+
version, err := GlobalVersion("python")
454+
if err != nil {
455+
t.Errorf("GlobalVersion() with no config file should return nil error, got: %v", err)
456+
}
457+
if version != "" {
458+
t.Errorf("GlobalVersion() with no config file should return empty string, got: %q", version)
459+
}
460+
}
461+
462+
func TestGlobalVersion_RuntimeNotInConfig(t *testing.T) {
463+
// When config exists but runtime is not in it, GlobalVersion should return ("", nil)
464+
tmpRoot := t.TempDir()
465+
t.Setenv("HOME", tmpRoot)
466+
t.Setenv("USERPROFILE", tmpRoot)
467+
ResetPathsCache()
468+
defer ResetPathsCache()
469+
470+
// Set a version for python so the config file exists
471+
if err := SetGlobalVersion("python", "3.11.0"); err != nil {
472+
t.Fatalf("SetGlobalVersion() setup error: %v", err)
473+
}
474+
475+
// Ask for node which is not in the config
476+
version, err := GlobalVersion("node")
477+
if err != nil {
478+
t.Errorf("GlobalVersion() for missing runtime should return nil error, got: %v", err)
479+
}
480+
if version != "" {
481+
t.Errorf("GlobalVersion() for missing runtime should return empty string, got: %q", version)
482+
}
483+
}
484+
445485
func TestSetLocalVersion_CreatesDirectoryAndFile(t *testing.T) {
446486
// Create temp directory and change to it
447487
tmpRoot := t.TempDir()

src/internal/runtime/provider_test_harness.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -250,10 +250,14 @@ func (h *ProviderTestHarness) TestIsInstalled(t *testing.T) {
250250
func (h *ProviderTestHarness) TestGetGlobalVersion(t *testing.T) {
251251
version, err := h.Provider.GlobalVersion()
252252

253-
// It's OK to have error if no global version is set
254-
// But if version is returned, it should be non-empty
255-
if err == nil && version == "" {
256-
t.Error("GetGlobalVersion() returned empty string without error")
253+
// ("", nil) is valid — means no global version configured
254+
// If a version is returned, it should be non-empty
255+
if err == nil && version != "" {
256+
// Valid: a global version is configured
257+
}
258+
if err != nil {
259+
// Valid: an actual error occurred (I/O, corrupt config)
260+
t.Logf("GetGlobalVersion() returned error (may be expected): %v", err)
257261
}
258262
}
259263

0 commit comments

Comments
 (0)