diff --git a/sdk/go/openshell/v1/gateway/gateway.go b/sdk/go/openshell/v1/gateway/gateway.go index dde3545664..888769c1c1 100644 --- a/sdk/go/openshell/v1/gateway/gateway.go +++ b/sdk/go/openshell/v1/gateway/gateway.go @@ -13,7 +13,7 @@ import ( // NewClient creates a fully wired SDK client from an on-disk gateway // configuration. If name is empty, the active gateway (set via -// `openshell gateway use`) is used. +// `openshell gateway use`, or the system default) is used. // // The function resolves the gateway directory, parses metadata.json, // loads tokens lazily, maps the auth mode to an SDK auth provider, and @@ -66,8 +66,8 @@ func NewClient(name string, opts ...ClientOption) (*v1.Client, error) { } // LoadConfig reads and parses a gateway's on-disk configuration without -// creating a client connection. If name is empty, the active gateway is -// used. +// creating a client connection. If name is empty, the active gateway (user +// selection, then system default) is used. // // The returned [Config] is an immutable snapshot; changes to the on-disk // files after this call are not reflected. diff --git a/sdk/go/openshell/v1/gateway/gateway_test.go b/sdk/go/openshell/v1/gateway/gateway_test.go index e22a0a7873..72cb17f672 100644 --- a/sdk/go/openshell/v1/gateway/gateway_test.go +++ b/sdk/go/openshell/v1/gateway/gateway_test.go @@ -387,6 +387,7 @@ func TestNewClient_ActiveGateway(t *testing.T) { func TestNewClient_NoActiveGateway(t *testing.T) { tmp := t.TempDir() t.Setenv("XDG_CONFIG_HOME", tmp) + t.Setenv(systemGatewayDirEnv, t.TempDir()) require.NoError(t, os.MkdirAll(filepath.Join(tmp, "openshell"), 0o755)) _, err := NewClient("") @@ -394,6 +395,21 @@ func TestNewClient_NoActiveGateway(t *testing.T) { assert.ErrorIs(t, err, ErrNoActiveGateway) } +func TestLoadConfig_SystemActiveGateway(t *testing.T) { + user, system := t.TempDir(), t.TempDir() + t.Setenv("XDG_CONFIG_HOME", user) + t.Setenv(systemGatewayDirEnv, system) + writeFile(t, system, "active_gateway", "corp") + gwDir := filepath.Join(system, "gateways", "corp") + require.NoError(t, os.MkdirAll(gwDir, 0o755)) + writeFile(t, gwDir, "metadata.json", `{"gateway_endpoint":"host:443"}`) + + cfg, err := LoadConfig("") + require.NoError(t, err) + assert.Equal(t, "corp", cfg.Name) + assert.Equal(t, SourceSystem, cfg.Source) +} + func TestLoadConfig_ActiveGateway(t *testing.T) { tmp := setupGateway(t, "my-active", `{"gateway_endpoint":"host:443","auth_mode":"oidc"}`) writeFile(t, filepath.Join(tmp, "openshell"), "active_gateway", "my-active") diff --git a/sdk/go/openshell/v1/gateway/paths.go b/sdk/go/openshell/v1/gateway/paths.go index 7107db8bc7..00c67d050b 100644 --- a/sdk/go/openshell/v1/gateway/paths.go +++ b/sdk/go/openshell/v1/gateway/paths.go @@ -137,16 +137,29 @@ func validateGatewayName(name string) error { return nil } -// resolveActiveGateway reads the active_gateway file from the user -// config directory and returns the validated gateway name. Returns -// ErrNoActiveGateway if the file is missing or empty. +// resolveActiveGateway returns the validated active gateway name. It reads +// the user active_gateway file and falls back to the system one when the user +// file is missing, empty, or invalid, matching the CLI. Returns +// ErrNoActiveGateway describing the user file when neither yields a name. func resolveActiveGateway() (string, error) { userBase, err := userConfigDir() if err != nil { - return "", fmt.Errorf("%w: cannot determine config directory: %v", ErrNoActiveGateway, err) + err = fmt.Errorf("%w: cannot determine config directory: %v", ErrNoActiveGateway, err) + } else { + var name string + if name, err = readActiveGatewayFile(filepath.Join(userBase, activeGatewayFile)); err == nil { + return name, nil + } + } + + if name, sysErr := readActiveGatewayFile(filepath.Join(systemConfigDir(), activeGatewayFile)); sysErr == nil { + return name, nil } + return "", err +} - path := filepath.Join(userBase, activeGatewayFile) +// readActiveGatewayFile reads and validates the gateway name stored at path. +func readActiveGatewayFile(path string) (string, error) { data, err := os.ReadFile(path) if err != nil { return "", fmt.Errorf("%w", ErrNoActiveGateway) diff --git a/sdk/go/openshell/v1/gateway/paths_test.go b/sdk/go/openshell/v1/gateway/paths_test.go index ab22b85f97..524d521f66 100644 --- a/sdk/go/openshell/v1/gateway/paths_test.go +++ b/sdk/go/openshell/v1/gateway/paths_test.go @@ -198,6 +198,7 @@ func TestResolveActiveGateway_WhitespaceHandling(t *testing.T) { func TestResolveActiveGateway_FileMissing(t *testing.T) { tmp := t.TempDir() t.Setenv("XDG_CONFIG_HOME", tmp) + t.Setenv(systemGatewayDirEnv, t.TempDir()) require.NoError(t, os.MkdirAll(filepath.Join(tmp, "openshell"), 0o755)) _, err := resolveActiveGateway() @@ -208,6 +209,7 @@ func TestResolveActiveGateway_FileMissing(t *testing.T) { func TestResolveActiveGateway_EmptyFile(t *testing.T) { tmp := t.TempDir() t.Setenv("XDG_CONFIG_HOME", tmp) + t.Setenv(systemGatewayDirEnv, t.TempDir()) require.NoError(t, os.MkdirAll(filepath.Join(tmp, "openshell"), 0o755)) require.NoError(t, os.WriteFile(filepath.Join(tmp, "openshell", "active_gateway"), []byte(" \n"), 0o644)) @@ -216,6 +218,53 @@ func TestResolveActiveGateway_EmptyFile(t *testing.T) { assert.ErrorIs(t, err, ErrNoActiveGateway) } +func TestResolveActiveGateway_FallsBackToSystem(t *testing.T) { + user, system := t.TempDir(), t.TempDir() + t.Setenv("XDG_CONFIG_HOME", user) + t.Setenv(systemGatewayDirEnv, system) + require.NoError(t, os.WriteFile(filepath.Join(system, "active_gateway"), []byte("corp\n"), 0o644)) + + name, err := resolveActiveGateway() + require.NoError(t, err) + assert.Equal(t, "corp", name) +} + +func TestResolveActiveGateway_PrefersUser(t *testing.T) { + user, system := t.TempDir(), t.TempDir() + t.Setenv("XDG_CONFIG_HOME", user) + t.Setenv(systemGatewayDirEnv, system) + require.NoError(t, os.MkdirAll(filepath.Join(user, "openshell"), 0o755)) + require.NoError(t, os.WriteFile(filepath.Join(user, "openshell", "active_gateway"), []byte("mine"), 0o644)) + require.NoError(t, os.WriteFile(filepath.Join(system, "active_gateway"), []byte("corp"), 0o644)) + + name, err := resolveActiveGateway() + require.NoError(t, err) + assert.Equal(t, "mine", name) +} + +func TestResolveActiveGateway_InvalidUserFallsBackToSystem(t *testing.T) { + user, system := t.TempDir(), t.TempDir() + t.Setenv("XDG_CONFIG_HOME", user) + t.Setenv(systemGatewayDirEnv, system) + require.NoError(t, os.MkdirAll(filepath.Join(user, "openshell"), 0o755)) + require.NoError(t, os.WriteFile(filepath.Join(user, "openshell", "active_gateway"), []byte("../escape"), 0o644)) + require.NoError(t, os.WriteFile(filepath.Join(system, "active_gateway"), []byte("corp"), 0o644)) + + name, err := resolveActiveGateway() + require.NoError(t, err) + assert.Equal(t, "corp", name) +} + +func TestResolveActiveGateway_InvalidSystemIsIgnored(t *testing.T) { + user, system := t.TempDir(), t.TempDir() + t.Setenv("XDG_CONFIG_HOME", user) + t.Setenv(systemGatewayDirEnv, system) + require.NoError(t, os.WriteFile(filepath.Join(system, "active_gateway"), []byte("../escape"), 0o644)) + + _, err := resolveActiveGateway() + assert.ErrorIs(t, err, ErrNoActiveGateway) +} + func TestValidateGatewayName_SpecialCharacters(t *testing.T) { cases := []struct { name string