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
cmd/entire/cli
Mactivity_cmd_test.go+8
auth
Mdata_api.go+22/-1
Mrecap.go+12/-10
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