diff options
Diffstat (limited to 'internal/hexaicli')
| -rw-r--r-- | internal/hexaicli/cache_test.go | 30 | ||||
| -rw-r--r-- | internal/hexaicli/run.go | 27 | ||||
| -rw-r--r-- | internal/hexaicli/run_model_override_test.go | 7 | ||||
| -rw-r--r-- | internal/hexaicli/run_output_test.go | 6 | ||||
| -rw-r--r-- | internal/hexaicli/run_test.go | 13 | ||||
| -rw-r--r-- | internal/hexaicli/run_timeout_test.go | 8 | ||||
| -rw-r--r-- | internal/hexaicli/runner.go | 3 | ||||
| -rw-r--r-- | internal/hexaicli/simulation_test.go | 12 |
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) } |
