Skip to content

Commit 2d59159

Browse files
danbarrclaude
andauthored
Isolate skills client discovery in default-client test (#5417)
TestNewDefaultClient stubbed the TOOLHIVE_API_URL env reader but let the server-discovery step run against real local state. When a thv or Desktop server was running, discovery found it and returned its base URL, so the "falls back to default URL" and "applies options" subtests saw the discovered URL instead of the default and failed. CI passed only because no server is running there. Inject the discovery step into newDefaultClientWithEnv, mirroring the existing env.Reader injection, and have the subtests stub it. Each resolution step (env var, discovery, default fallback) is now tested in isolation, and a new subtest covers the discovered-server path. Co-authored-by: Dan Barr <6922515+danbarr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 50a6c8b commit 2d59159

2 files changed

Lines changed: 43 additions & 7 deletions

File tree

pkg/skills/client/client.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,18 +83,26 @@ func NewClient(baseURL string, opts ...Option) *Client {
8383
//
8484
// The context is used for the server discovery health check; it is not stored.
8585
func NewDefaultClient(ctx context.Context, opts ...Option) *Client {
86-
return newDefaultClientWithEnv(ctx, &env.OSReader{}, opts...)
86+
return newDefaultClientWithEnv(ctx, &env.OSReader{}, resolveViaDiscovery, opts...)
8787
}
8888

89-
// newDefaultClientWithEnv is the testable core of NewDefaultClient.
90-
func newDefaultClientWithEnv(ctx context.Context, envReader env.Reader, opts ...Option) *Client {
89+
// discoverFunc resolves a running server's base URL and any transport options
90+
// (e.g. a Unix socket client). It returns an empty base URL when no running
91+
// server is found. resolveViaDiscovery is the production implementation; tests
92+
// inject a stub so the discovery step does not read real local state.
93+
type discoverFunc func(ctx context.Context) (string, []Option)
94+
95+
// newDefaultClientWithEnv is the testable core of NewDefaultClient. The
96+
// envReader and discover dependencies are injected so each resolution step
97+
// can be exercised in isolation.
98+
func newDefaultClientWithEnv(ctx context.Context, envReader env.Reader, discover discoverFunc, opts ...Option) *Client {
9199
// 1. Explicit env var override always wins.
92100
if base := envReader.Getenv(envAPIURL); base != "" {
93101
return NewClient(base, opts...)
94102
}
95103

96104
// 2. Try server discovery.
97-
if base, httpOpts := resolveViaDiscovery(ctx); base != "" {
105+
if base, httpOpts := discover(ctx); base != "" {
98106
// Discovery opts go first so caller-supplied opts can override them
99107
// (e.g. a caller-provided WithTimeout replaces the discovery default).
100108
merged := make([]Option, 0, len(httpOpts)+len(opts))

pkg/skills/client/client_test.go

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package client
55

66
import (
7+
"context"
78
"encoding/json"
89
"errors"
910
"io"
@@ -636,13 +637,27 @@ func TestConnectionError(t *testing.T) {
636637
func TestNewDefaultClient(t *testing.T) {
637638
t.Parallel()
638639

640+
// noDiscovery stubs server discovery to report no running server, isolating
641+
// the test from any real local server (e.g. a running Desktop app).
642+
noDiscovery := func(context.Context) (string, []Option) { return "", nil }
643+
644+
// failDiscovery fails the test if discovery is consulted, asserting that an
645+
// earlier resolution step short-circuited.
646+
failDiscovery := func(t *testing.T) discoverFunc {
647+
t.Helper()
648+
return func(context.Context) (string, []Option) {
649+
t.Error("discovery should not be called when TOOLHIVE_API_URL is set")
650+
return "", nil
651+
}
652+
}
653+
639654
t.Run("falls back to default URL when env is empty", func(t *testing.T) {
640655
t.Parallel()
641656
ctrl := gomock.NewController(t)
642657
mockEnv := envmocks.NewMockReader(ctrl)
643658
mockEnv.EXPECT().Getenv(envAPIURL).Return("")
644659

645-
c := newDefaultClientWithEnv(t.Context(), mockEnv)
660+
c := newDefaultClientWithEnv(t.Context(), mockEnv, noDiscovery)
646661
assert.Equal(t, defaultBaseURL, c.baseURL)
647662
})
648663

@@ -652,17 +667,30 @@ func TestNewDefaultClient(t *testing.T) {
652667
mockEnv := envmocks.NewMockReader(ctrl)
653668
mockEnv.EXPECT().Getenv(envAPIURL).Return("http://localhost:9999")
654669

655-
c := newDefaultClientWithEnv(t.Context(), mockEnv)
670+
c := newDefaultClientWithEnv(t.Context(), mockEnv, failDiscovery(t))
656671
assert.Equal(t, "http://localhost:9999", c.baseURL)
657672
})
658673

674+
t.Run("uses discovered server when env is empty", func(t *testing.T) {
675+
t.Parallel()
676+
ctrl := gomock.NewController(t)
677+
mockEnv := envmocks.NewMockReader(ctrl)
678+
mockEnv.EXPECT().Getenv(envAPIURL).Return("")
679+
680+
discover := func(context.Context) (string, []Option) {
681+
return "http://127.0.0.1:54321", nil
682+
}
683+
c := newDefaultClientWithEnv(t.Context(), mockEnv, discover)
684+
assert.Equal(t, "http://127.0.0.1:54321", c.baseURL)
685+
})
686+
659687
t.Run("applies options", func(t *testing.T) {
660688
t.Parallel()
661689
ctrl := gomock.NewController(t)
662690
mockEnv := envmocks.NewMockReader(ctrl)
663691
mockEnv.EXPECT().Getenv(envAPIURL).Return("")
664692

665-
c := newDefaultClientWithEnv(t.Context(), mockEnv, WithTimeout(5*time.Second))
693+
c := newDefaultClientWithEnv(t.Context(), mockEnv, noDiscovery, WithTimeout(5*time.Second))
666694
assert.Equal(t, 5*time.Second, c.httpClient.Timeout)
667695
})
668696
}

0 commit comments

Comments
 (0)