data-api: route recap through context-aware resolution; make tests hermetic · Entire

data-api: route recap through context-aware resolution; make tests hermetic

e170454→main·

toothbrush·1mo ago·3 files·+42 added/-11 removed

Audit of outbound API calls found newRecapClient still resolving its bearer via static auth.TokenForResource against the data host (api.BaseURL()), bypassing the discovery + context-selection path the other data-API commands use. Switch it to auth.ResolveDataAPIToken so recap follows the active auth context like activity/search/dispatch.

The audit also surfaced that the activity unit tests were no longer hermetic: now that activity/recap/search go through ResolveDataAPIToken, their resolution does a live /.well-known/entire-api.json fetch against the configured data host — which bypasses SetManagerForTest and hit the real entire.io once #2277 deployed. Add an explicit discovery seam auth.SetResolveContextForAPIForTest / DiscoveryUnavailableForTest and use it in the two runActivity tests so they exercise the static fallback through the singleton test manager with no network.

Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com

Sessions

3d80b3e46e06View transcript

[?
Auth Refactoring and Discovery CachingClaude Code·Opus 4.8[1m]·1 step](/content/gh/entireio/cli/session/6b26d89b-433f-4dab-8cf7-8faa6ae827a5#timeline-3d80b3e46e06/index.html)

Changes

3

35 unmodified lines

36
37
38
39
40
41
42
43
44
45
25 unmodified lines

71
72
73
74
75
76
77
78
79
80

35 unmodified lines

return nil, context.Canceled
    })
t.Cleanup(auth.SetManagerForTest(t, mgr))
    // Force discovery-unavailable so ResolveDataAPIToken takes the static
    // fallback through the singleton test manager above, rather than making a
    // real network fetch to the configured data host.
t.Cleanup(auth.SetResolveContextForAPIForTest(t, auth.DiscoveryUnavailableForTest))

var out, errOut bytes.Buffer
    err := runActivity(t.Context(), &out, &errOut)
25 unmodified lines

return nil, errors.New("unreachable")
    })
t.Cleanup(auth.SetManagerForTest(t, mgr))
    // Force discovery-unavailable so ResolveDataAPIToken takes the static
    // fallback through the singleton test manager above, rather than making a
    // real network fetch to the configured data host.
t.Cleanup(auth.SetResolveContextForAPIForTest(t, auth.DiscoveryUnavailableForTest))

var out, errOut bytes.Buffer
    err := runActivity(t.Context(), &out, &errOut)

Mcmd/entire/cli/activity_cmd_test.go+8

17 unmodified lines

18
19
20
21
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

17 unmodified lines

const dataAPIDiscoveryTimeout = 8 * time.Second

// resolveContextForAPI is the discovery seam, swapped in tests so they don't
// reach the network.
// reach the network. See SetResolveContextForAPIForTest for cross-package tests.
var resolveContextForAPI = clusterdiscovery.ResolveContextForAPI

// SetResolveContextForAPIForTest overrides the /.well-known/entire-api.json
// discovery seam and returns a cleanup func. Tests in other packages that
// exercise a data-API command (activity/search/dispatch/recap) MUST install
// this — otherwise ResolveDataAPIToken makes a real network call to the
// configured data host and bypasses any SetManagerForTest fallback seam. Pass
// a func returning clusterdiscovery.ErrDiscoveryUnavailable to force the static
// fallback path. Test-only.
func SetResolveContextForAPIForTest(t interface{ Helper() }, fn func(context.Context, string, string, *http.Client, clusterdiscovery.DebugFunc) (*contexts.Context, *clusterdiscovery.APIResponse, error)) func() {
    t.Helper()
    prev := resolveContextForAPI
    resolveContextForAPI = fn
    return func() { resolveContextForAPI = prev }
}

// DiscoveryUnavailableForTest is a ready-made SetResolveContextForAPIForTest
// value that forces the discovery-unavailable fallback (no network), so a
// cross-package test exercises the static TokenForResource path deterministically.
func DiscoveryUnavailableForTest(context.Context, string, string, *http.Client, clusterdiscovery.DebugFunc) (*contexts.Context, *clusterdiscovery.APIResponse, error) {
    return nil, nil, clusterdiscovery.ErrDiscoveryUnavailable
}

// ResolveDataAPIToken returns a bearer for the data API at dataBaseURL.
//
// It dials the API's /.well-known/entire-api.json to learn which login

Mcmd/entire/cli/auth/data_api.go+22/-1

167 unmodified lines

168
169
170
171
172
173
174
175
176
177
178
179
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
184
186
187
188
189

167 unmodified lines

// 401s via recapLoadErrorMessage so flag effects (--week, --agent, ...)
// and the real auth error are not collapsed into one "sign in" hint.
//
// Goes through auth.TokenForResource so split-host deployments get a
// resource-scoped bearer via RFC 8693 exchange. ErrNotLoggedIn is
// collapsed back into an empty token so the caller's "render with no
// bearer, let the server respond 401" path still fires. Every other
// resolution failure (STS exchange rejected, network error, audience
// misconfiguration, keyring locked) surfaces verbatim to the caller —
// previously these were all relabelled as keyring read failures via
// keyringReadError, which sent users on wild goose chases when the
// keyring was fine and the real problem was downstream.
// Goes through auth.ResolveDataAPIToken (the same context-aware path as
// activity/search/dispatch) so the data host's /.well-known/entire-api.json
// picks the matching login context and exchanges for the advertised audience,
// falling back to static resolution when discovery is unavailable.
// ErrNotLoggedIn is collapsed back into an empty token so the caller's "render
// with no bearer, let the server respond 401" path still fires. Every other
// resolution failure (no eligible/ambiguous context, STS exchange rejected,
// network error, keyring locked) surfaces verbatim to the caller — previously
// these were all relabelled as keyring read failures via keyringReadError,
// which sent users on wild goose chases when the keyring was fine and the real
// problem was downstream.
func newRecapClient(ctx context.Context, insecureHTTP bool) (*api.Client, error) {
    if insecureHTTP {
        auth.EnableInsecureHTTP()
    }
    token, err := auth.TokenForResource(ctx, api.OriginOnly(api.BaseURL()))
token, err := auth.ResolveDataAPIToken(ctx, api.BaseURL())
    if errors.Is(err, auth.ErrNotLoggedIn) {
        token = ""
        err = nil

Mcmd/entire/cli/recap.go+12/-10