From 0f791a4484d1b1eed7285c980bd1d90ed6a4542b Mon Sep 17 00:00:00 2001 From: Asad Iqbal Date: Tue, 14 Jul 2026 15:00:40 +0500 Subject: [PATCH] fix(cli): offline-vs-no-env keys on the probe's name, not the remembered label MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #267 made resolveHomeModel overwrite env.name with the remembered client name for display. But the offline-vs-no-env classifier reads `env.name != ""` to mean "the probe surfaced an environment name" — so a leftover ActiveClientName/ID with no cached namespace (provisioned=false) now masquerades as a reachable environment and renders Offline + full menu instead of the no-environment installer path. Capture the probe's own surfaced name (probeNamedEnv) BEFORE the display-name override and classify on that: `provisioned || probeNamedEnv`. A remembered display label with no namespace is no longer evidence of an environment. Adds a regression test for the leftover-name-without-namespace case. Fixes the Cursor Bugbot "Remembered name skews offline detection" finding on #266 (learned rule: commands targeting the active client must use bindActiveClientNamespace). Co-Authored-By: Claude Opus 4.8 --- internal/cli/home.go | 18 ++++++++++++++---- internal/cli/home_test.go | 18 ++++++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/internal/cli/home.go b/internal/cli/home.go index ea9f3105..515cfa0a 100644 --- a/internal/cli/home.go +++ b/internal/cli/home.go @@ -249,6 +249,13 @@ func resolveHomeModel(ctx context.Context, d homeDeps) homeModel { env, beat := collectProbes(bctx, envCh, beatCh) + // Whether the PROBE itself surfaced an environment name — the signal the + // offline-vs-no-env classifier keys on (alongside `provisioned`). Captured + // BEFORE the display-name override below: otherwise a remembered client label + // left in config without a cached namespace would masquerade as a + // probe-surfaced environment and flip the no-env installer path to Offline. + probeNamedEnv := env.name != "" + // The environment's display name is the remembered client name (e.g. // "acme-01") — the friendly, per-client identity provisioned on this machine. // Prefer it over whatever the probe surfaced, which is the Helm RELEASE name @@ -300,12 +307,15 @@ func resolveHomeModel(ctx context.Context, d homeDeps) homeModel { // explain this as "runs elsewhere"). Offline vs. "no environment": it's an // environment we just can't reach (offline) if EITHER this machine is // PROVISIONED — a cached active-client namespace, the same signal the - // probe's ownership gate uses — OR the probe itself surfaced an environment + // probe's ownership gate uses — OR the PROBE itself surfaced an environment // name. Adding the provisioned test (not name alone) is the fix for a // provisioned-but-unnamed profile that used to misread as "no environment / - // run the installer"; keeping the name test preserves the case where the - // probe surfaced a name without one being cached. - if provisioned || env.name != "" { + // run the installer"; keeping the probe-name test preserves the case where + // the probe surfaced a name without one being cached. It must be the + // probe's own name (probeNamedEnv), NOT the post-override env.name — a + // remembered display label with no cached namespace is not evidence of a + // reachable environment and must fall through to the installer path. + if provisioned || probeNamedEnv { m.state = homeOffline m.fullMenu = true } else { diff --git a/internal/cli/home_test.go b/internal/cli/home_test.go index e3640c65..470e3cc2 100644 --- a/internal/cli/home_test.go +++ b/internal/cli/home_test.go @@ -381,6 +381,24 @@ func TestResolveHomeModel_PrefersRememberedNameOverReleaseName(t *testing.T) { } } +// TestResolveHomeModel_LeftoverNameNoNamespaceIsNoEnv guards the offline-vs-no-env +// classifier against the display-name override: a leftover ActiveClientName/ID +// with NO cached namespace (provisioned=false) is NOT a reachable environment and +// must fall through to the no-env installer path, not Offline. The split keys off +// the namespace + the PROBE's own surfaced name — never the remembered label that +// resolveHomeModel writes onto env.name for display. Mutation guard: classify on +// env.name (post-override) instead of probeNamedEnv and this flips to homeOffline. +func TestResolveHomeModel_LeftoverNameNoNamespaceIsNoEnv(t *testing.T) { + d := baseDeps() + d.probeEnv = func(context.Context) envProbe { return envProbe{local: localNoRelease} } // probe surfaced no name + // Not provisioned (no cached namespace), but a stale client id lingers in config. + d.rememberedClient = func() (bool, string) { return false, "stale-id" } + m := resolveHomeModel(context.Background(), d) + if m.state != homeNoEnv { + t.Fatalf("leftover name without a cached namespace must be homeNoEnv (installer path), got state %d (envName %q)", m.state, m.envName) + } +} + // TestResolveHomeModel_PassesThroughFields checks the model carries email, name, // and compute from the probes into the render input. func TestResolveHomeModel_PassesThroughFields(t *testing.T) {