summaryrefslogtreecommitdiff
path: root/internal/hexaicli
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-06-18 08:05:32 +0300
committerPaul Buetow <paul@buetow.org>2026-06-18 08:05:32 +0300
commita80ad2b4691d0df63f68c6977ee18444e7bb752f (patch)
tree6edc47fcc3c929856826432cf3df3394a14934e8 /internal/hexaicli
parent4ffb22e7f69f1c9c79b095d4e60bad3d97aac55b (diff)
ik0 replace remaining test seams with DI
Diffstat (limited to 'internal/hexaicli')
-rw-r--r--internal/hexaicli/cache_test.go30
-rw-r--r--internal/hexaicli/run.go27
-rw-r--r--internal/hexaicli/run_model_override_test.go7
-rw-r--r--internal/hexaicli/run_output_test.go6
-rw-r--r--internal/hexaicli/run_test.go13
-rw-r--r--internal/hexaicli/run_timeout_test.go8
-rw-r--r--internal/hexaicli/runner.go3
-rw-r--r--internal/hexaicli/simulation_test.go12
8 files changed, 56 insertions, 50 deletions
diff --git a/internal/hexaicli/cache_test.go b/internal/hexaicli/cache_test.go
index 98dfb2d..b01e447 100644
--- a/internal/hexaicli/cache_test.go
+++ b/internal/hexaicli/cache_test.go
@@ -81,30 +81,29 @@ func TestRun_UsesCachedResponseWithoutClientCall(t *testing.T) {
// (the in-code default switched to ollama when no config is present).
t.Setenv("HEXAI_PROVIDER", "openai")
- oldNew := newClientFromApp
- defer func() { newClientFromApp = oldNew }()
-
calls := 0
- newClientFromApp = func(cfg appconfig.App) (llm.Client, error) {
+ clientFactory := func(cfg appconfig.App) (llm.Client, error) {
calls++
return &fakeClient{name: cfg.Provider, model: "gpt-4.1", resp: "cached output"}, nil
}
+ ctx := withCLIClientFactory(context.Background(), clientFactory)
var firstOut, firstErr bytes.Buffer
- if err := Run(context.Background(), []string{"hello"}, strings.NewReader(""), &firstOut, &firstErr); err != nil {
+ if err := Run(ctx, []string{"hello"}, strings.NewReader(""), &firstOut, &firstErr); err != nil {
t.Fatalf("first Run: %v", err)
}
if calls != 1 {
t.Fatalf("expected one live client call, got %d", calls)
}
- newClientFromApp = func(appconfig.App) (llm.Client, error) {
+ clientFactory = func(appconfig.App) (llm.Client, error) {
t.Fatal("client should not be constructed on cache hit")
return nil, nil
}
+ ctx = withCLIClientFactory(context.Background(), clientFactory)
var secondOut, secondErr bytes.Buffer
- if err := Run(context.Background(), []string{"hello"}, strings.NewReader(""), &secondOut, &secondErr); err != nil {
+ if err := Run(ctx, []string{"hello"}, strings.NewReader(""), &secondOut, &secondErr); err != nil {
t.Fatalf("second Run: %v", err)
}
if got := secondOut.String(); got != "cached output" {
@@ -136,9 +135,7 @@ provider = "anthropic"
model = "claude-3-5-sonnet-20240620"
`)
- oldNew := newClientFromApp
- defer func() { newClientFromApp = oldNew }()
- newClientFromApp = func(cfg appconfig.App) (llm.Client, error) {
+ clientFactory := func(cfg appconfig.App) (llm.Client, error) {
switch cfg.Provider {
case "anthropic":
return &fakeClient{name: "anthropic", model: "claude-3-5-sonnet-20240620", resp: "RIGHT"}, nil
@@ -147,16 +144,17 @@ model = "claude-3-5-sonnet-20240620"
}
}
- if err := Run(context.Background(), []string{"hello"}, strings.NewReader(""), &bytes.Buffer{}, &bytes.Buffer{}); err != nil {
+ ctx := withCLIClientFactory(context.Background(), clientFactory)
+ if err := Run(ctx, []string{"hello"}, strings.NewReader(""), &bytes.Buffer{}, &bytes.Buffer{}); err != nil {
t.Fatalf("warm cache Run: %v", err)
}
- newClientFromApp = func(appconfig.App) (llm.Client, error) {
+ clientFactory = func(appconfig.App) (llm.Client, error) {
t.Fatal("client should not be constructed for selected cache hit")
return nil, nil
}
- ctx := WithCLISelection(context.Background(), []int{1})
+ ctx = withCLIClientFactory(WithCLISelection(context.Background(), []int{1}), clientFactory)
var out, errb bytes.Buffer
if err := Run(ctx, []string{"hello"}, strings.NewReader(""), &out, &errb); err != nil {
t.Fatalf("selected Run: %v", err)
@@ -180,11 +178,8 @@ func TestRun_ExpiredCacheFallsBackToProvider(t *testing.T) {
now := time.Date(2026, 3, 15, 10, 0, 0, 0, time.UTC)
ctx := withCLIResponseCacheNow(context.Background(), func() time.Time { return now })
- oldNew := newClientFromApp
- defer func() { newClientFromApp = oldNew }()
-
calls := 0
- newClientFromApp = func(cfg appconfig.App) (llm.Client, error) {
+ clientFactory := func(cfg appconfig.App) (llm.Client, error) {
calls++
resp := "first"
if calls > 1 {
@@ -192,6 +187,7 @@ func TestRun_ExpiredCacheFallsBackToProvider(t *testing.T) {
}
return &fakeClient{name: cfg.Provider, model: "gpt-4.1", resp: resp}, nil
}
+ ctx = withCLIClientFactory(ctx, clientFactory)
if err := Run(ctx, []string{"hello"}, strings.NewReader(""), &bytes.Buffer{}, &bytes.Buffer{}); err != nil {
t.Fatalf("first Run: %v", err)
diff --git a/internal/hexaicli/run.go b/internal/hexaicli/run.go
index 8152618..dafe484 100644
--- a/internal/hexaicli/run.go
+++ b/internal/hexaicli/run.go
@@ -37,8 +37,9 @@ type cliJob struct {
}
type (
- selectionContextKey struct{}
- configPathContextKey struct{}
+ selectionContextKey struct{}
+ configPathContextKey struct{}
+ clientFactoryContextKey struct{}
)
func buildCLIJobs(cfg appconfig.App) ([]cliJob, error) {
@@ -346,6 +347,13 @@ func WithCLIConfigPath(ctx context.Context, path string) context.Context {
return context.WithValue(ctx, configPathContextKey{}, strings.TrimSpace(path))
}
+func withCLIClientFactory(ctx context.Context, factory cliClientFactory) context.Context {
+ if ctx == nil {
+ ctx = context.Background()
+ }
+ return context.WithValue(ctx, clientFactoryContextKey{}, factory)
+}
+
func configPathFromContext(ctx context.Context) string {
if ctx == nil {
return ""
@@ -356,6 +364,16 @@ func configPathFromContext(ctx context.Context) string {
return ""
}
+func clientFactoryFromContext(ctx context.Context) cliClientFactory {
+ if ctx == nil {
+ return nil
+ }
+ if v, ok := ctx.Value(clientFactoryContextKey{}).(cliClientFactory); ok {
+ return v
+ }
+ return nil
+}
+
func selectionFromContext(ctx context.Context) []int {
if ctx == nil {
return nil
@@ -554,8 +572,9 @@ func cacheHitSummary(provider, model string, age time.Duration) string {
return fmt.Sprintf(logging.AnsiBase+"cache hit provider=%s model=%s age=%s"+logging.AnsiReset+"\n", provider, model, age.Round(time.Second))
}
-// newClientFromConfig is kept for tests; delegates to llmutils.
-var newClientFromApp = llmutils.NewClientFromApp
+func newClientFromApp(cfg appconfig.App) (llm.Client, error) {
+ return llmutils.NewClientFromApp(cfg)
+}
// Backcompat for tests referencing the older helper name.
func newClientFromConfig(cfg appconfig.App) (llm.Client, error) { return newClientFromApp(cfg) }
diff --git a/internal/hexaicli/run_model_override_test.go b/internal/hexaicli/run_model_override_test.go
index f669ede..4ddcc92 100644
--- a/internal/hexaicli/run_model_override_test.go
+++ b/internal/hexaicli/run_model_override_test.go
@@ -24,16 +24,15 @@ func TestRun_ModelEnvOverride_FlowsIntoClient(t *testing.T) {
t.Setenv("HEXAI_MODEL", "gpt-5-codex")
t.Setenv("HEXAI_PROVIDER", "openai")
// Replace client constructor to assert model was overridden
- oldNew := newClientFromApp
- defer func() { newClientFromApp = oldNew }()
var seenModel string
- newClientFromApp = func(cfg appconfig.App) (llm.Client, error) {
+ clientFactory := func(cfg appconfig.App) (llm.Client, error) {
seenModel = strings.TrimSpace(cfg.OpenAIModel)
return fakeClientModelEnv{name: "openai", model: cfg.OpenAIModel}, nil
}
var out, errb bytes.Buffer
- if err := Run(context.Background(), []string{"hello"}, strings.NewReader(""), &out, &errb); err != nil {
+ ctx := withCLIClientFactory(context.Background(), clientFactory)
+ if err := Run(ctx, []string{"hello"}, strings.NewReader(""), &out, &errb); err != nil {
t.Fatalf("run error: %v", err)
}
if seenModel != "gpt-5-codex" {
diff --git a/internal/hexaicli/run_output_test.go b/internal/hexaicli/run_output_test.go
index 07a2cab..2168e69 100644
--- a/internal/hexaicli/run_output_test.go
+++ b/internal/hexaicli/run_output_test.go
@@ -342,9 +342,7 @@ func TestCacheHitSummary_NegativeAge(t *testing.T) {
func TestRunCLIJobs_MultiJob_WritesOutputs(t *testing.T) {
// runCLIJobs with multiple jobs should call writeCLIJobOutputs
// (the non-streaming, non-printer path).
- oldNew := newClientFromApp
- defer func() { newClientFromApp = oldNew }()
- newClientFromApp = func(cfg appconfig.App) (llm.Client, error) {
+ clientFactory := func(cfg appconfig.App) (llm.Client, error) {
return &fakeClient{name: cfg.Provider, model: "m", resp: "out-" + cfg.Provider}, nil
}
t.Setenv("XDG_CACHE_HOME", t.TempDir())
@@ -400,7 +398,7 @@ func TestRunCLIJobs_MultiJob_WritesOutputs(t *testing.T) {
}
stdout.Reset()
stderr.Reset()
- if err := runCLIJobs(context.Background(), singleJobs, msgs, "hello", &stdout, &stderr, newClientFromApp, nil); err != nil {
+ if err := runCLIJobs(context.Background(), singleJobs, msgs, "hello", &stdout, &stderr, clientFactory, nil); err != nil {
t.Fatalf("runCLIJobs single: %v", err)
}
if !strings.Contains(stdout.String(), "out-a") {
diff --git a/internal/hexaicli/run_test.go b/internal/hexaicli/run_test.go
index a1bad90..75bced3 100644
--- a/internal/hexaicli/run_test.go
+++ b/internal/hexaicli/run_test.go
@@ -171,9 +171,7 @@ func TestRun_SingleProviderHeaderUsesStderr(t *testing.T) {
// explicitly (the in-code default switched to ollama/gemma4).
t.Setenv("HEXAI_PROVIDER", "openai")
t.Setenv("HEXAI_OPENAI_MODEL", "gpt-4.1")
- oldNew := newClientFromApp
- defer func() { newClientFromApp = oldNew }()
- newClientFromApp = func(_ appconfig.App) (llm.Client, error) {
+ clientFactory := func(_ appconfig.App) (llm.Client, error) {
return &fakeClient{name: "openai", model: "gpt-4.1", resp: "OUT"}, nil
}
@@ -181,7 +179,8 @@ func TestRun_SingleProviderHeaderUsesStderr(t *testing.T) {
defer restore()
var stdout, stderr bytes.Buffer
- if err := Run(context.Background(), nil, f, &stdout, &stderr); err != nil {
+ ctx := withCLIClientFactory(context.Background(), clientFactory)
+ if err := Run(ctx, nil, f, &stdout, &stderr); err != nil {
t.Fatalf("Run: %v", err)
}
if got := stdout.String(); got != "OUT" {
@@ -200,9 +199,7 @@ func TestRun_SingleProviderHeaderUsesStderr(t *testing.T) {
}
func TestExecuteCLIJobs_MultiProviderHeaderUsesStderr(t *testing.T) {
- oldNew := newClientFromApp
- defer func() { newClientFromApp = oldNew }()
- newClientFromApp = func(cfg appconfig.App) (llm.Client, error) {
+ clientFactory := func(cfg appconfig.App) (llm.Client, error) {
switch cfg.Provider {
case "anthropic":
return &fakeClient{name: "anthropic", model: "claude", resp: "RIGHT"}, nil
@@ -233,7 +230,7 @@ func TestExecuteCLIJobs_MultiProviderHeaderUsesStderr(t *testing.T) {
}
var stdout, stderr bytes.Buffer
- results, printer := executeCLIJobs(context.Background(), jobs, buildMessages("hello"), "hello", &stdout, &stderr, false, newClientFromApp, nil)
+ results, printer := executeCLIJobs(context.Background(), jobs, buildMessages("hello"), "hello", &stdout, &stderr, false, clientFactory, nil)
if printer == nil {
t.Fatalf("expected column printer for multi-provider run")
}
diff --git a/internal/hexaicli/run_timeout_test.go b/internal/hexaicli/run_timeout_test.go
index 9561617..7275ddb 100644
--- a/internal/hexaicli/run_timeout_test.go
+++ b/internal/hexaicli/run_timeout_test.go
@@ -16,17 +16,15 @@ func TestRun_DefaultRequestTimeoutIsTenMinutes(t *testing.T) {
t.Setenv("XDG_CACHE_HOME", t.TempDir())
t.Setenv("HEXAI_REQUEST_TIMEOUT", "")
- oldNew := newClientFromApp
- defer func() { newClientFromApp = oldNew }()
-
seenTimeout := 0
- newClientFromApp = func(cfg appconfig.App) (llm.Client, error) {
+ clientFactory := func(cfg appconfig.App) (llm.Client, error) {
seenTimeout = cfg.RequestTimeout
return okClient{}, nil
}
var out, errb bytes.Buffer
- if err := Run(context.Background(), []string{"hello"}, strings.NewReader(""), &out, &errb); err != nil {
+ ctx := withCLIClientFactory(context.Background(), clientFactory)
+ if err := Run(ctx, []string{"hello"}, strings.NewReader(""), &out, &errb); err != nil {
t.Fatalf("Run: %v", err)
}
if seenTimeout != 600 {
diff --git a/internal/hexaicli/runner.go b/internal/hexaicli/runner.go
index 340733c..ba42452 100644
--- a/internal/hexaicli/runner.go
+++ b/internal/hexaicli/runner.go
@@ -69,6 +69,9 @@ func NewRunner() *Runner {
func (r *Runner) Run(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) error {
runner := normalizeRunner(r)
+ if factory := clientFactoryFromContext(ctx); factory != nil {
+ runner.newClient = factory
+ }
if len(args) > 0 {
sub := args[0]
if sub == "config" {
diff --git a/internal/hexaicli/simulation_test.go b/internal/hexaicli/simulation_test.go
index 4c3533d..af7f7c1 100644
--- a/internal/hexaicli/simulation_test.go
+++ b/internal/hexaicli/simulation_test.go
@@ -65,15 +65,13 @@ func TestReadSimulationInput_DefaultsToSampleText(t *testing.T) {
}
func TestRun_TPSSimulationBypassesClientSetup(t *testing.T) {
- oldNew := newClientFromApp
- defer func() { newClientFromApp = oldNew }()
- newClientFromApp = func(appconfig.App) (llm.Client, error) {
+ clientFactory := func(appconfig.App) (llm.Client, error) {
t.Fatalf("client setup should not be called in TPS simulation mode")
return nil, nil
}
var out, errb bytes.Buffer
- ctx := WithCLITPSSimulation(context.Background(), "1000000")
+ ctx := withCLIClientFactory(WithCLITPSSimulation(context.Background(), "1000000"), clientFactory)
if err := Run(ctx, []string{"simulated", "output"}, strings.NewReader(""), &out, &errb); err != nil {
t.Fatalf("Run returned error: %v", err)
}
@@ -86,9 +84,7 @@ func TestRun_TPSSimulationBypassesClientSetup(t *testing.T) {
}
func TestRun_TPSSimulationUsesStdin(t *testing.T) {
- oldNew := newClientFromApp
- defer func() { newClientFromApp = oldNew }()
- newClientFromApp = func(appconfig.App) (llm.Client, error) {
+ clientFactory := func(appconfig.App) (llm.Client, error) {
t.Fatalf("client setup should not be called in TPS simulation mode")
return nil, nil
}
@@ -97,7 +93,7 @@ func TestRun_TPSSimulationUsesStdin(t *testing.T) {
defer restore()
var out, errb bytes.Buffer
- ctx := WithCLITPSSimulation(context.Background(), "1000000")
+ ctx := withCLIClientFactory(WithCLITPSSimulation(context.Background(), "1000000"), clientFactory)
if err := Run(ctx, nil, f, &out, &errb); err != nil {
t.Fatalf("Run returned error: %v", err)
}