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
2 changes: 1 addition & 1 deletion pkg/cli/add_interactive_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func (c *AddInteractiveConfig) selectAIEngineAndKey() error {
if opt.EnvVarName != "" {
envVar = opt.EnvVarName
}
if os.Getenv(envVar) != "" {
if lookupEnv(envVar) != "" {
defaultEngine = opt.Value
addInteractiveLog.Printf("Found env var %s, recommending engine: %s", envVar, opt.Value)
break
Expand Down
2 changes: 1 addition & 1 deletion pkg/cli/outcomes_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ func RunOutcomes(config OutcomesConfig) error {
// The --outcomes-dir flag takes precedence over the GH_AW_OUTCOMES_DIR env var.
outcomesDir := config.OutcomesDir
if outcomesDir == "" {
outcomesDir, _ = os.LookupEnv("GH_AW_OUTCOMES_DIR")
outcomesDir = lookupEnv("GH_AW_OUTCOMES_DIR")
}
if outcomesDir != "" {
writeOutcomeJSONL(outcomesDir, config.RunID, reports)
Expand Down
40 changes: 40 additions & 0 deletions pkg/cli/process_env_lookup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package cli

import (
"os"
"sync"
)

type envLookupFunc func(string) (string, bool)

var (
processEnvLookupMu sync.RWMutex
processEnvLookup envLookupFunc = os.LookupEnv
)

// SetProcessEnvLookup configures how CLI helpers resolve environment values.
// Passing nil restores the default process environment lookup.
func SetProcessEnvLookup(lookup func(string) (string, bool)) {
processEnvLookupMu.Lock()
defer processEnvLookupMu.Unlock()
if lookup == nil {
processEnvLookup = os.LookupEnv
return
}
processEnvLookup = lookup
}

func lookupEnv(key string) string {
processEnvLookupMu.RLock()
defer processEnvLookupMu.RUnlock()
// Intentionally ignore the existence flag to preserve os.Getenv semantics:
// missing variables and explicitly empty variables are both treated as "".
value, _ := processEnvLookup(key)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] lookupEnv silently discards the bool existence flag. The inline comment explains the intent (match os.Getenv semantics), which is correct — but it is worth a test that verifies an explicitly-set empty string ("") is returned as "" (not treated as missing), so the semantics stay locked in.

💡 Suggested edge-case test
func TestLookupEnv_emptyStringIsNotMissing(t *testing.T) {
    t.Cleanup(func() { SetEnvLookup(nil) })
    SetEnvLookup(func(key string) (string, bool) {
        return "", true // key exists but is empty
    })
    if got := lookupEnv("ANY"); got != "" {
        t.Fatalf("want empty string, got %q", got)
    }
}

@copilot please address this.

return value
}

func lookupEnvOk(key string) (string, bool) {
processEnvLookupMu.RLock()
defer processEnvLookupMu.RUnlock()
return processEnvLookup(key)
}
Comment on lines +27 to +40

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No test coverage for the injectable override

pkg/workflow/process_env_lookup.go is exercised in github_cli_test.go and features_test.go (using SetProcessEnvLookup). This new pkg/cli counterpart has no equivalent tests, leaving the thread-safe injection path untested.

Please add at least a simple test similar to the workflow package pattern — for example, verifying that lookupEnv and lookupEnvOk delegate to an injected function, and that passing nil restores the default.

@copilot please address this.

67 changes: 67 additions & 0 deletions pkg/cli/process_env_lookup_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//go:build !integration

package cli

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestSetProcessEnvLookup(t *testing.T) {
t.Cleanup(func() { SetProcessEnvLookup(nil) })

SetProcessEnvLookup(func(key string) (string, bool) {
env := map[string]string{
"PRESENT": "hello",
"EMPTY": "",
}
v, ok := env[key]
return v, ok
})

t.Run("lookupEnv returns value for present key", func(t *testing.T) {
assert.Equal(t, "hello", lookupEnv("PRESENT"))
})

t.Run("lookupEnv returns empty string for missing key", func(t *testing.T) {
assert.Empty(t, lookupEnv("MISSING"))
})

t.Run("lookupEnv returns empty string for explicitly empty key", func(t *testing.T) {
assert.Empty(t, lookupEnv("EMPTY"))
})

t.Run("lookupEnvOk returns value and true for present key", func(t *testing.T) {
val, ok := lookupEnvOk("PRESENT")
assert.Equal(t, "hello", val)
assert.True(t, ok)
})

t.Run("lookupEnvOk returns empty string and false for missing key", func(t *testing.T) {
val, ok := lookupEnvOk("MISSING")
assert.Empty(t, val)
assert.False(t, ok)
})

t.Run("lookupEnvOk returns empty string and true for explicitly empty key", func(t *testing.T) {
val, ok := lookupEnvOk("EMPTY")
assert.Empty(t, val)
assert.True(t, ok)
})
}

func TestSetProcessEnvLookupNilRestoresDefault(t *testing.T) {
t.Cleanup(func() { SetProcessEnvLookup(nil) })

SetProcessEnvLookup(func(key string) (string, bool) {
return "overridden", true
})

SetProcessEnvLookup(nil)

// After restoring the default, missing keys should return "" / false.
val, ok := lookupEnvOk("__GH_AW_TEST_KEY_THAT_MUST_NOT_EXIST__")
assert.Empty(t, val)
assert.False(t, ok)
}
3 changes: 1 addition & 2 deletions pkg/workflow/workflow_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package workflow

import (
"context"
"os"

actionpins "github.com/github/gh-aw/pkg/actionpins"
"github.com/github/gh-aw/pkg/logger"
Expand Down Expand Up @@ -228,7 +227,7 @@ func (d *WorkflowData) PinContext() *actionpins.PinContext {
// for example from auto-detected git remotes). Mirror setupGHCommand's
// (github_cli.go) precedence: GH_HOST wins when present; default host is
// only consulted when GH_HOST is absent.
if ghHost := os.Getenv("GH_HOST"); ghHost != "" {
if ghHost := lookupProcessEnv("GH_HOST"); ghHost != "" {
if ghHost != "github.com" {
workflowDataLog.Print("Non-github.com GH_HOST detected; disabling hardcoded action pin fallback")
pinCtx.SkipHardcodedFallback = true
Expand Down
Loading