From 0183a4b638e0d179edc3addfa10a2d41a13e403f Mon Sep 17 00:00:00 2001 From: Elber Ribeiro Date: Sun, 2 Aug 2026 21:19:28 -0400 Subject: [PATCH 01/38] docs(fleet): spec one app across every cluster, and the matrix cell that lies The Show HN thread asked twice for one app across many clusters. The docs already promised it at 04-multi-cluster.md:20 and :334, and promotion.go:144 builds its per-environment map with byEnv[in.Env] = in, so a prod environment holding two contexts renders one cell and drops the other without saying so. Specs a fleet screen, one row per cluster, and a split cell that refuses to collapse a disagreement. Records the TUI decline and the missing Argo CD comparison from the same thread. Leaves --serve under Beyond v1. --- .../specs/2026-08-02-fleet-design.md | 294 ++++++++++++++++++ 1 file changed, 294 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-02-fleet-design.md diff --git a/docs/superpowers/specs/2026-08-02-fleet-design.md b/docs/superpowers/specs/2026-08-02-fleet-design.md new file mode 100644 index 0000000..10cda71 --- /dev/null +++ b/docs/superpowers/specs/2026-08-02-fleet-design.md @@ -0,0 +1,294 @@ +# Fleet: one app across every cluster + +Design spec, 2026-08-02. Source: the Show HN thread for kubeside, +[item 49139573](https://news.ycombinator.com/item?id=49139573), 25 points and 8 +comments. + +## Why + +Two people asked the same question independently in the thread. + +maxweisel runs singular apps deployed across multiple clusters and wants to see +"if all clusters are running the latest version." The maintainer confirmed the +same pain from the other side: clusters per team, plus uat, plus prod, and the +recurring need to check one service across all of them. + +A third commenter read the README tagline, "Your apps, across every cluster, +without thinking in ReplicaSets," and assumed the capability already shipped. + +It has not, and the current behavior is worse than absent. + +## The defect this uncovers + +`docs/04-multi-cluster.md:20` states one environment maps to one or more +contexts, and the promotion view "aggregates them with per-context detail on +expansion." Line 334 repeats it as a handled failure mode for an environment +holding 40 contexts. + +`internal/promotion/promotion.go:144` builds its per-environment map with +`byEnv[in.Env] = in`. Last write wins. A `prod` environment holding +`prod-us-east` and `prod-eu-west` renders one cell showing whichever context +arrived last, and discards the other silently. + +`promotion.Env` carries a single `Context string`. `promotion.Instance` carries +no context at all. The aggregation was specified and never built. + +So the promotion view answers "is the fix in prod" with a confident version it +did not verify. Compare that against the rule at +`docs/04-multi-cluster.md:216`, where unauthorized renders distinctly from +absent because "I cannot see it" and "it is not there" are different facts. +Reporting one cluster's version as the environment's version breaks the same +principle in a way harder to notice. + +## Scope + +In: + +- A fleet screen: one app, one row per cluster. +- The promotion matrix refusing to collapse a disagreement. +- Argo CD entering the tool comparison. +- The TUI request declined in writing. + +Out: + +- `--serve` in-cluster mode. It stays under Beyond v1 in + `docs/06-roadmap.md:111`. The thread raised demand for it. Demand does not + shrink the OIDC and Helm work it requires. + +## Anti-persona check + +The fleet screen is app-scoped across clusters. It adds no node view, no +capacity data, no RBAC visualization, and no cost attribution. It clears A1, A2, +and A3 in `docs/02-personas.md:249`. It serves Rafael and Bruno, the same +personas the promotion view serves. + +## Model + +New package `internal/fleet`. One type carries one app in one cluster. + +```go +// Placement is one app as it exists in one cluster. +type Placement struct { + Context string // kubeconfig context name, personal and unstable + ClusterID string // cluster UID, the identity surviving a rename + Env string // resolved environment name, "unclassified" when unmatched + Namespace string + + State string // present | absent | denied | unreachable | pending + Reason string + + Image string + Tag string + Digest string + // DigestPending marks a placement whose digest has not arrived. A missing + // digest never reads as a match. + DigestPending bool + + Health string + Ready string + RevisionAt string +} +``` + +The view derives two facts across the set: the newest tag among present +placements, and every placement behind it. "Are all clusters on the latest +version" reduces to one count. + +Tag ordering already exists and is tested in `promotion.compareTags`. Fleet +reuses it. Two comparators disagreeing about which version is newer would be the +worst defect this feature can carry, so `CompareTags` and the namespace-token +stripping inside `promotion.identity` get exported rather than reimplemented. + +Cross-cluster app matching reuses the existing rules: name plus namespace, +tolerating environment-suffix conventions, with `apps.match` in the config file +overriding. No new matching mechanism. + +## Screen + +One app, one row per cluster, environment demoted to a column. + +``` +checkout FLEET + +CLUSTER ENV VERSION HEALTH +qa-cluster qa v2.14.0 ok 2/2 +staging-eks stg v2.13.1 ok 3/3 +prod-us-east prod v2.13.1 ok 6/6 +prod-eu-west prod v2.12.0 ok 4/4 behind +prod-ap-south prod v2.13.1 ok 3/3 +team-b unclas no access pods list denied +``` + +An unmatched context renders as `unclassified` and carries prod guardrails, per +`docs/04-multi-cluster.md:70`. Unknown risk stays high risk. + +Sorting puts disagreement first, matching the promotion view's default. Nothing +sits ahead of the newest tag, so `behind` is the only relative state the screen +carries. The promotion view keeps `ahead` because it compares against the +environment to its left rather than against a maximum. + +## Data flow + +Opening the screen states intent to ask every cluster. Under the connection +lifecycle in `docs/04-multi-cluster.md:168` this is the one screen where waking +every context is the point rather than an accident. + +1. The server walks every context in the kubeconfig chain, honoring `KUBECONFIG`. +2. Each context connects lazily and resolves the app by the existing identity + rules. +3. Pod metadata for the digest is fetched on demand and memoized for the + session, the same mechanism the promotion view uses at + `docs/04-multi-cluster.md:225`. +4. Each context streams its own answer over the existing websocket. Rows land as + clusters reply. +5. A cluster behind a VPN never blocks the rest. +6. Idle disconnect resumes when the screen closes. + +Two contexts pointing at one cluster merge on `ClusterID` before rendering, per +the failure mode at `docs/04-multi-cluster.md:331`. Without the merge one +cluster appears twice and inflates the behind count, which turns the feature's +headline number into a lie. + +## The promotion matrix fix + +`promotion.Env` gains `Contexts []string`. `promotion.Instance` gains `Context`. +The overwrite at `promotion.go:144` becomes a grouping. + +Cell behavior for a multi-context environment: + +| Contexts in the environment | Cell | +| --- | --- | +| All present and agreeing on tag and digest | Collapses to one version, identical to today | +| Present, disagreeing on tag or digest | `StateSplit`, severe, naming the count, linking to fleet | +| Mixed present and absent | `StateSplit`. Deployed in two of three clusters is not deployed | +| Mixed readable and denied | `StateSplit`. Partial visibility never collapses | +| All denied | `StateDenied`, identical to today | +| All absent | `StateAbsent`, identical to today | + +A split cell never becomes the upstream for the next column. This follows the +existing rule at `promotion.go:151`, where an unreadable environment is not an +agreement and so does not become the comparison basis. + +A single-context environment behaves exactly as it does today. That is the +regression guard protecting every closed issue in the promotion feature. + +## Error handling + +Five row states, none conflated. + +| State | Meaning | +| --- | --- | +| `present` | The app is deployed here and readable | +| `absent` | The cluster answered and the app is not there | +| `denied` | RBAC refused the read. Different from absent | +| `unreachable` | The cluster did not answer. Different from absent | +| `pending` | The request is in flight | + +`unreachable` is new to the codebase and carries the weight here. A cluster +behind a VPN is not a cluster missing the app, and rendering both blank answers +"is it everywhere" with a guess. + +Further cases: + +- The app found in no cluster renders the count of clusters asked and their + states. Never an empty window, per the hard rule in CLAUDE.md. A mistyped app + name reaches this case immediately. +- The same tag resolving to different digests across clusters is the loudest + state on the screen, ranked above behind. Two clusters claiming `v2.13.1` + while running different code is worse than one cluster openly sitting on + `v2.12.0`. +- A 403 on the namespace list falls through the existing discovery chain and + names the active mode on the row. +- Expired SSO credentials reuse the inline prompt shipped in #37 rather than + failing the row. + +## Testing + +Tests before implementation, per CLAUDE.md. + +`internal/fleet`: + +- Newest-tag derivation across present placements. +- Behind counting. +- `ClusterID` dedupe when two contexts reach one cluster. +- The five states staying distinct. +- A pending digest never reading as a match. +- Same tag with different digests ranking above behind. + +`internal/promotion`: + +- A single-context environment unchanged. This test comes first. +- Collapse on full agreement. +- Split on tag disagreement, on digest disagreement, on mixed presence, and on + partial denial. +- A split cell refusing to serve as upstream for the next column. + +Degraded mode, extending the suite from #31: one context unreachable, one +denied, one slow. The screen fills. + +Playwright screenshot gate for the new screen, extending the suite from #30. + +## The design constraint + +CLAUDE.md requires porting from the Claude Design project named `kubeside` and +forbids inventing new UI. That project's `screens/` holds apps, app-detail, +config, config-diff, logs, palette, promotion, and first-run-light. There is no +fleet screen. + +The first issue therefore authors `screens/fleet.html` in the design project, +built from `tokens.css` and the row patterns already in `screens/promotion.html`. +Human review of that screen gates the React work. Building the UI first and +backfilling the design would invert the rule the project runs on. + +## Issues + +Under a new `feature:fleet` label, dependencies before dependents. + +| # | Issue | Package | +| --- | --- | --- | +| 1 | Author `screens/fleet.html` in the design project, human-reviewed | design | +| 2 | Export `CompareTags` and identity matching from `promotion` | `internal/promotion` | +| 3 | `internal/fleet`: placement model and derivation | `internal/fleet` | +| 4 | Fleet query across contexts, lazy connect, `ClusterID` dedupe | `internal/fleet`, `internal/clusters` | +| 5 | Promotion matrix stops collapsing a disagreement | `internal/promotion`, web | +| 6 | Fleet deltas over the existing websocket | `internal/api` | +| 7 | Screen 5: fleet, ported from issue 1 | web | +| 8 | Palette entry and app-detail entry point | web | +| 9 | Degraded-mode and Playwright coverage | tests | +| 10 | Docs: spec, multi-cluster, and a guide page | docs | + +Issue 2 blocks 3 and 5. Issue 3 blocks 4. Issue 1 blocks 7. Issues 2 and 5 touch +`internal/promotion` and stay sequential, per the parallelism rule in CLAUDE.md. + +Standalone, outside the label: + +| # | Issue | +| --- | --- | +| 11 | Argo CD joins the reference table in `docs/01-problem.md:184` | +| 12 | TUI declined in writing against `docs/05-architecture.md:20` | + +Issue 11 answers the first objection a reader raised, and answering it in a +comment thread does not scale. The prose separates GitOps sync state, which is +1:1 with the cluster, from the developer's app view. + +Issue 12 records the decline rather than dropping it. `docs/05-architecture.md:20` +already rejects a terminal UI: the four gaps are layout problems a monospace grid +cannot render, and k9s owns the terminal. The decline lands as a closed issue and +a `docs/guide/12-troubleshooting.md` entry, since the question will recur. + +## Doc changes this forces + +- `docs/03-product-spec.md` commits to four screens. Fleet makes five. +- `docs/04-multi-cluster.md:20` and `:334` promise aggregation with per-context + expansion inside the promotion view. That promise is replaced by the split + cell plus the fleet screen, and both lines get rewritten. +- `README.md:5` carries the tagline a commenter read as a shipped promise. It + stays once the feature ships. + +## Done condition + +A developer opens kubeside against a kubeconfig holding three prod clusters, +selects one app, and sees which clusters run the newest version and which do +not, with unreachable and denied clusters named rather than blank. The promotion +matrix, on the same data, refuses to show a single version for the environment +holding those three clusters. From 7ec1eead96d74347c482418efd95850da5af360d Mon Sep 17 00:00:00 2001 From: Elber Ribeiro Date: Sun, 2 Aug 2026 22:03:56 -0400 Subject: [PATCH 02/38] docs(fleet): implementation plan, fourteen tasks Backend first, since the matrix cannot render data nobody gathered: export the shared comparators, teach the engine to split, stop the service dropping contexts, then the fleet package, gatherer, and transport. The design screen gates the React work, per the rule against inventing UI. Three spec corrections found while reading the code. The drop starts at service.go:1006, not promotion.go:144. No cluster UID exists, so dedupe keys off the API server URL. Screen numbering is taken through 6, so fleet is Screen 7. --- docs/superpowers/plans/2026-08-02-fleet.md | 2131 +++++++++++++++++ .../specs/2026-08-02-fleet-design.md | 42 +- 2 files changed, 2167 insertions(+), 6 deletions(-) create mode 100644 docs/superpowers/plans/2026-08-02-fleet.md diff --git a/docs/superpowers/plans/2026-08-02-fleet.md b/docs/superpowers/plans/2026-08-02-fleet.md new file mode 100644 index 0000000..592505c --- /dev/null +++ b/docs/superpowers/plans/2026-08-02-fleet.md @@ -0,0 +1,2131 @@ +# Fleet Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Show one app across every cluster, and stop the promotion matrix reporting a version it never verified. + +**Architecture:** A new `internal/fleet` package derives "which clusters run the newest version" from one placement per cluster. `internal/api` gathers placements by walking every kubeconfig context, connecting lazily, and deduping on API server URL. Separately, the promotion path stops discarding contexts: `service.go` gathers every context in an environment instead of the first, and `promotion.row` renders a split cell when they disagree. + +**Tech Stack:** Go 1.26.2, client-go informers, React 19 with a custom design system, Vitest, Playwright. Module path `github.com/dynaum/kubeside`. + +## Global Constraints + +- No commit trailers of any kind. No `Co-Authored-By`, no `Generated with`. +- Tests written before implementation. +- `go build ./...` and `go test ./...` pass before every commit. +- One commit per task, message ending with `Closes #N` once issues are filed. +- Never write to disk at runtime. +- Never hide a control for lack of permission. Disable it and name the verb. +- Never render an unknown window as an empty one. +- Port UI from the Claude Design project named `kubeside`. Do not invent new UI. +- Spec: `docs/superpowers/specs/2026-08-02-fleet-design.md`. + +--- + +### Task 1: Export the shared version and identity helpers + +`internal/fleet` must order tags the same way `internal/promotion` does. Two comparators disagreeing about which version is newer would be the worst defect this feature can carry. This task exports, and changes no behavior. + +**Files:** +- Modify: `internal/promotion/promotion.go:238-261` (identity), `:294-297` (compareTags) +- Test: `internal/promotion/promotion_test.go` + +**Interfaces:** +- Consumes: nothing +- Produces: + - `promotion.CompareTags(a, b string) int` — negative when `a` is older, positive when newer, zero when the two cannot be ordered + - `promotion.Identity(app, namespace string) string` — the cross-environment key + - `promotion.StripEnvToken(ns string) string` — namespace with `qa`, `prod`, and friends removed + +- [ ] **Step 1: Write the failing test** + +Add to `internal/promotion/promotion_test.go`: + +```go +func TestIdentityMatchesAcrossEnvSuffixedNamespaces(t *testing.T) { + qa := Identity("checkout", "team-a-qa") + prod := Identity("checkout", "team-a-prod") + if qa != prod { + t.Errorf("Identity qa = %q, prod = %q; one team's namespace in two places is one app", qa, prod) + } +} + +func TestCompareTagsIsExported(t *testing.T) { + if CompareTags("v1.2.0", "v1.10.0") >= 0 { + t.Error("v1.2.0 should order below v1.10.0") + } + if CompareTags("sha-abc", "sha-def") != 0 { + t.Error("build ids cannot be ordered, and claiming a direction invents one") + } +} + +func TestStripEnvTokenIsExported(t *testing.T) { + if got := StripEnvToken("team-a-prod"); got != "team-a" { + t.Errorf("StripEnvToken = %q, want team-a", got) + } +} +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `go test ./internal/promotion/ -run 'TestIdentity|TestCompareTagsIsExported|TestStripEnvTokenIsExported' -v` +Expected: FAIL, `undefined: Identity`, `undefined: CompareTags`, `undefined: StripEnvToken` + +- [ ] **Step 3: Rename to exported, keep the unexported call sites working** + +In `internal/promotion/promotion.go`, replace the `identity`, `stripEnvToken`, and `compareTags` definitions: + +```go +// Identity is how one app is recognized across environments. +// +// Name plus namespace, with an environment suffix or prefix tolerated, because +// team-a-qa and team-a-prod are one team's namespace in two places and lining +// those up is the whole point of the view. +func Identity(app, namespace string) string { + return app + "|" + StripEnvToken(namespace) +} + +func identity(in Instance) string { return Identity(in.App, in.Namespace) } + +// StripEnvToken removes the environment token from a namespace, so that +// team-a-qa and team-a-prod resolve to one identity. +func StripEnvToken(ns string) string { + parts := strings.Split(ns, "-") + kept := make([]string, 0, len(parts)) + for _, p := range parts { + if envTokens[strings.ToLower(strings.TrimRight(p, "0123456789"))] { + continue + } + kept = append(kept, p) + } + if len(kept) == 0 { + return ns + } + return strings.Join(kept, "-") +} + +// CompareTags orders two version tags, returning zero when they cannot be +// ordered. Claiming a direction we cannot establish would be worse than saying +// they merely differ. +func CompareTags(a, b string) int { + an, aok := versionParts(a) + bn, bok := versionParts(b) + // ... body unchanged from compareTags +} +``` + +Then replace the one call site at `promotion.go:204`, `compareTags(in.Tag, upstream.Tag)`, with `CompareTags(in.Tag, upstream.Tag)`. Delete the old `stripEnvToken` and `compareTags` definitions. + +- [ ] **Step 4: Run the whole promotion suite** + +Run: `go test ./internal/promotion/ -v` +Expected: PASS, including every pre-existing test. This task changes no behavior, so a failure here is a rename mistake. + +- [ ] **Step 5: Build and commit** + +```bash +go build ./... && go test ./... +git add internal/promotion/ +git commit -m "refactor(promotion): export tag ordering and app identity for reuse + +internal/fleet must order tags the same way the promotion matrix does. Two +comparators disagreeing about which version is newer would be the worst +defect the fleet view can carry, so it reuses these rather than growing a +second implementation. No behavior change." +``` + +--- + +### Task 2: The promotion engine renders a split cell + +The engine stops assuming one context per environment. A cell collapses only when every context agrees. + +**Files:** +- Modify: `internal/promotion/promotion.go:23-32` (states), `:34-39` (Env), `:41-60` (Instance), `:62-96` (Cell, Summary), `:99-110` (Summarize), `:143-188` (row) +- Test: `internal/promotion/promotion_test.go` + +**Interfaces:** +- Consumes: `promotion.CompareTags` from Task 1 +- Produces: + - `promotion.StateSplit = "split"` + - `promotion.Env{Name, Risk string, Context string, Contexts []string, Unreadable []string}` + - `promotion.Instance` gains `Context string` + - `promotion.Cell` gains `Clusters int` + - `promotion.Summary` gains `Split int` + +- [ ] **Step 1: Write the failing tests** + +Add to `internal/promotion/promotion_test.go`: + +```go +func multiEnv() []Env { + return []Env{ + {Name: "qa", Contexts: []string{"qa-cluster"}}, + {Name: "prod", Contexts: []string{"prod-us-east", "prod-eu-west", "prod-ap-south"}}, + } +} + +func prodInstance(ctx, tag, digest string) Instance { + return Instance{ + Env: "prod", Context: ctx, App: "checkout", Namespace: "shop", + Present: true, Image: "reg/checkout:" + tag, Tag: tag, Digest: digest, + Health: "healthy", Ready: "3/3", + } +} + +func cellFor(t *testing.T, rows []Row, env string) Cell { + t.Helper() + for _, c := range rows[0].Cells { + if c.Env == env { + return c + } + } + t.Fatalf("no cell for env %q", env) + return Cell{} +} + +func TestAgreeingContextsCollapseToOneVersion(t *testing.T) { + rows := Build(multiEnv(), []Instance{ + {Env: "qa", Context: "qa-cluster", App: "checkout", Namespace: "shop", Present: true, Tag: "v2.14.0", Digest: "sha256:aa"}, + prodInstance("prod-us-east", "v2.13.1", "sha256:bb"), + prodInstance("prod-eu-west", "v2.13.1", "sha256:bb"), + prodInstance("prod-ap-south", "v2.13.1", "sha256:bb"), + }) + c := cellFor(t, rows, "prod") + if c.State != StateBehind { + t.Errorf("state = %q, want %q: three clusters agreeing is one version", c.State, StateBehind) + } + if c.Tag != "v2.13.1" { + t.Errorf("tag = %q, want v2.13.1", c.Tag) + } +} + +func TestDisagreeingContextsNeverCollapse(t *testing.T) { + rows := Build(multiEnv(), []Instance{ + prodInstance("prod-us-east", "v2.13.1", "sha256:bb"), + prodInstance("prod-eu-west", "v2.12.0", "sha256:cc"), + prodInstance("prod-ap-south", "v2.13.1", "sha256:bb"), + }) + c := cellFor(t, rows, "prod") + if c.State != StateSplit { + t.Fatalf("state = %q, want %q", c.State, StateSplit) + } + if c.Tag != "" { + t.Errorf("tag = %q; a split cell must not pick a winner", c.Tag) + } + if !c.Severe { + t.Error("a split prod is severe") + } + if c.Clusters != 3 { + t.Errorf("clusters = %d, want 3", c.Clusters) + } + if !strings.Contains(c.Note, "2 versions") { + t.Errorf("note = %q, want it to name the version count", c.Note) + } +} + +func TestSameTagDifferentDigestSplitsLoudly(t *testing.T) { + rows := Build(multiEnv(), []Instance{ + prodInstance("prod-us-east", "v2.13.1", "sha256:bb"), + prodInstance("prod-eu-west", "v2.13.1", "sha256:cc"), + }) + c := cellFor(t, rows, "prod") + if c.State != StateSplit || !c.Severe { + t.Fatalf("state = %q severe = %v, want a severe split", c.State, c.Severe) + } + if !strings.Contains(c.Note, "digest") { + t.Errorf("note = %q, want it to name the mutable tag", c.Note) + } +} + +func TestPendingDigestDoesNotSplitAnAgreement(t *testing.T) { + rows := Build(multiEnv(), []Instance{ + prodInstance("prod-us-east", "v2.13.1", "sha256:bb"), + prodInstance("prod-eu-west", "v2.13.1", ""), + }) + c := cellFor(t, rows, "prod") + if c.State == StateSplit { + t.Fatal("a digest still in flight is not a disagreement") + } + if !c.DigestPending { + t.Error("a missing digest must read as pending, never as a match") + } + if c.Digest != "" { + t.Errorf("digest = %q; claiming one cluster's digest for the pair would be a guess", c.Digest) + } +} + +func TestPartialDeploymentSplits(t *testing.T) { + rows := Build(multiEnv(), []Instance{ + prodInstance("prod-us-east", "v2.13.1", "sha256:bb"), + prodInstance("prod-eu-west", "v2.13.1", "sha256:bb"), + }) + c := cellFor(t, rows, "prod") + if c.State != StateSplit { + t.Fatalf("state = %q; deployed in two of three clusters is not deployed", c.State) + } + if !strings.Contains(c.Note, "2 of 3") { + t.Errorf("note = %q, want it to name the coverage", c.Note) + } +} + +func TestUnreadableContextBlocksCollapse(t *testing.T) { + envs := multiEnv() + envs[1].Unreadable = []string{"prod-ap-south"} + rows := Build(envs, []Instance{ + prodInstance("prod-us-east", "v2.13.1", "sha256:bb"), + prodInstance("prod-eu-west", "v2.13.1", "sha256:bb"), + }) + c := cellFor(t, rows, "prod") + if c.State != StateSplit { + t.Fatalf("state = %q; a cluster nobody could read is not an agreement", c.State) + } + if !strings.Contains(c.Note, "readable") { + t.Errorf("note = %q, want it to say how many clusters answered", c.Note) + } +} + +func TestSplitCellIsNotUpstreamForTheNextColumn(t *testing.T) { + envs := []Env{ + {Name: "qa", Contexts: []string{"qa-cluster"}}, + {Name: "stg", Contexts: []string{"stg-a", "stg-b"}}, + {Name: "prod", Contexts: []string{"prod-only"}}, + } + rows := Build(envs, []Instance{ + {Env: "qa", Context: "qa-cluster", App: "checkout", Namespace: "shop", Present: true, Tag: "v2.0.0", Digest: "sha256:aa"}, + {Env: "stg", Context: "stg-a", App: "checkout", Namespace: "shop", Present: true, Tag: "v3.0.0", Digest: "sha256:bb"}, + {Env: "stg", Context: "stg-b", App: "checkout", Namespace: "shop", Present: true, Tag: "v1.0.0", Digest: "sha256:cc"}, + {Env: "prod", Context: "prod-only", App: "checkout", Namespace: "shop", Present: true, Tag: "v2.0.0", Digest: "sha256:aa"}, + }) + c := cellFor(t, rows, "prod") + if c.State != StateSame { + t.Errorf("prod state = %q, want %q: prod matches qa, and the split stg never became the basis", c.State, StateSame) + } +} + +func TestSingleContextEnvironmentIsUnchanged(t *testing.T) { + envs := []Env{ + {Name: "qa", Contexts: []string{"qa-cluster"}}, + {Name: "prod", Contexts: []string{"prod-cluster"}}, + } + rows := Build(envs, []Instance{ + {Env: "qa", Context: "qa-cluster", App: "checkout", Namespace: "shop", Present: true, Tag: "v2.0.0", Digest: "sha256:aa"}, + {Env: "prod", Context: "prod-cluster", App: "checkout", Namespace: "shop", Present: true, Tag: "v1.0.0", Digest: "sha256:bb"}, + }) + c := cellFor(t, rows, "prod") + if c.State != StateBehind { + t.Errorf("state = %q, want %q: one context per environment behaves exactly as before", c.State, StateBehind) + } +} +``` + +Add `"strings"` to the test file's imports if absent. + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `go test ./internal/promotion/ -run 'TestAgreeing|TestDisagreeing|TestSameTag|TestPending|TestPartial|TestUnreadable|TestSplitCell|TestSingleContext' -v` +Expected: FAIL, `unknown field Contexts in struct literal`, `undefined: StateSplit` + +- [ ] **Step 3: Add the state, the fields, and the grouping** + +In `internal/promotion/promotion.go`, add the state to the const block: + +```go + StateDenied = "denied" + // StateSplit is an environment whose clusters do not agree. It never + // carries a version, because picking one would be the guess this state + // exists to prevent. + StateSplit = "split" +``` + +Widen `Env`: + +```go +// Env is one column. +type Env struct { + Name string `json:"name"` + Risk string `json:"risk"` + // Context is the first context bound to this environment, kept so a cell + // can still be opened. Contexts is all of them. + Context string `json:"context,omitempty"` + Contexts []string `json:"contexts,omitempty"` + // Unreadable names the contexts that did not answer. A cell in an + // environment with any unreadable context can never claim agreement, + // because the clusters nobody read might disagree. + Unreadable []string `json:"unreadable,omitempty"` +} +``` + +Add `Context` to `Instance`, after `Env`: + +```go +type Instance struct { + Env string + Context string + App string + // ... rest unchanged +``` + +Add `Clusters` to `Cell`, after `Env`: + +```go + // Clusters is how many contexts back this cell. Zero and one both mean a + // single cluster; the UI shows the count only above one. + Clusters int `json:"clusters,omitempty"` +``` + +Add `Split` to `Summary` and count it in `Summarize`: + +```go +type Summary struct { + Apps int `json:"apps"` + Drifted int `json:"drifted"` + Ahead int `json:"ahead"` + Split int `json:"split"` +} + +func Summarize(rows []Row) Summary { + s := Summary{Apps: len(rows)} + for _, r := range rows { + if r.Drift > 0 { + s.Drifted++ + } + if r.Ahead { + s.Ahead++ + } + for _, c := range r.Cells { + if c.State == StateSplit { + s.Split++ + break + } + } + } + return s +} +``` + +- [ ] **Step 4: Rewrite `row` to group rather than overwrite** + +Replace `row` at `internal/promotion/promotion.go:143-188`: + +```go +func row(envs []Env, instances []Instance) Row { + byEnv := map[string][]Instance{} + for _, in := range instances { + byEnv[in.Env] = append(byEnv[in.Env], in) + } + + out := Row{App: instances[0].App, Namespace: instances[0].Namespace} + + // upstream is the last environment that actually told us something. An + // unreadable environment is not an agreement, and neither is a split one, + // so neither becomes the thing the next column is compared against. + var upstream *Instance + + for _, env := range envs { + rep, cell, ok := resolve(env, byEnv[env.Name], upstream) + out.Cells = append(out.Cells, cell) + if cell.State != StateSame { + out.Drift++ + } + if cell.State == StateAhead { + out.Ahead = true + } + if ok { + copyOf := rep + upstream = ©Of + } + } + return out +} + +// resolve turns one environment's clusters into one cell. +// +// The third return says whether the cell may serve as upstream for the next +// column. Absent, denied, and split all say no: a column the matrix could not +// read, or whose clusters disagree, is not a version the next column can be +// compared against. +func resolve(env Env, group []Instance, upstream *Instance) (Instance, Cell, bool) { + clusters := len(env.Contexts) + if clusters == 0 { + clusters = 1 + } + + var present []Instance + denied := 0 + for _, in := range group { + switch { + case in.Denied: + denied++ + case in.Present: + present = append(present, in) + } + } + + if len(present) == 0 { + if denied > 0 { + reason := "not readable" + for _, in := range group { + if in.Denied && in.DeniedReason != "" { + reason = in.DeniedReason + break + } + } + return Instance{}, Cell{Env: env.Name, State: StateDenied, Note: reason, Clusters: clusters}, false + } + return Instance{}, Cell{Env: env.Name, State: StateAbsent, Note: "not deployed here", Clusters: clusters}, false + } + + readable := clusters - len(env.Unreadable) + if rep, agreed := consensus(present); agreed && len(present) == readable && len(env.Unreadable) == 0 { + c := compare(rep, upstream) + c.Clusters = clusters + return rep, c, true + } + + return Instance{}, split(env, present, clusters, readable), false +} + +// consensus collapses clusters that agree. +// +// Tags must match exactly. Digests must match only when both arrived, because +// a digest still in flight is not a disagreement. The representative carries +// no digest unless every cluster reported the same one, so a pending fetch +// renders as pending instead of borrowing one cluster's answer for the rest. +func consensus(present []Instance) (Instance, bool) { + rep := present[0] + anyPending := rep.Digest == "" + for _, in := range present[1:] { + if in.Tag != rep.Tag { + return Instance{}, false + } + if in.Digest == "" { + anyPending = true + continue + } + if rep.Digest != "" && in.Digest != rep.Digest { + return Instance{}, false + } + if rep.Digest == "" { + rep.Digest = in.Digest + } + } + if anyPending { + rep.Digest = "" + } + rep.Context = "" + return rep, true +} + +// split is the cell for an environment whose clusters do not agree. It names +// the disagreement and never names a version. +func split(env Env, present []Instance, clusters, readable int) Cell { + c := Cell{Env: env.Name, State: StateSplit, Severe: true, Clusters: clusters} + if len(present) > 0 { + c.Namespace = present[0].Namespace + } + + tags := map[string]bool{} + digests := map[string]bool{} + for _, in := range present { + tags[in.Tag] = true + if in.Digest != "" { + digests[in.Digest] = true + } + } + + switch { + case len(env.Unreadable) > 0: + c.Note = fmt.Sprintf("%d of %d clusters readable; the rest did not answer", readable, clusters) + case len(present) < readable: + c.Note = fmt.Sprintf("deployed in %d of %d clusters", len(present), clusters) + case len(tags) == 1 && len(digests) > 1: + c.Tag = "" + c.Note = fmt.Sprintf("one tag across %d clusters but %d digests: the tag is mutable and these are not the same code", clusters, len(digests)) + default: + c.Note = fmt.Sprintf("%d versions across %d clusters", len(tags), clusters) + } + return c +} +``` + +- [ ] **Step 5: Run the whole promotion suite** + +Run: `go test ./internal/promotion/ -v` +Expected: PASS. `TestSingleContextEnvironmentIsUnchanged` and every pre-existing test are the regression guard; a failure there means the collapse path drifted. + +- [ ] **Step 6: Commit** + +```bash +go build ./... && go test ./... +git add internal/promotion/ +git commit -m "feat(promotion): a multi-context environment stops collapsing a disagreement + +byEnv[in.Env] = in kept the last context and dropped the rest, so a prod +environment holding three clusters rendered one confident version nobody +verified. The cell now collapses only when every cluster agrees on tag and +digest, and renders a split naming the disagreement otherwise. A split never +becomes upstream for the next column, for the same reason an unreadable +column never did." +``` + +--- + +### Task 3: The service gathers every context in an environment + +The engine can now render a split. It never will until the service stops skipping contexts. + +**Files:** +- Modify: `internal/api/service.go:992-1047` (Promotion), `:1055-1083` (instanceOf) +- Test: `internal/api/service_test.go` + +**Interfaces:** +- Consumes: `promotion.Env{Contexts, Unreadable}` and `promotion.Instance{Context}` from Task 2 +- Produces: `Service.Promotion()` returning `PromotionView` whose `Envs` carry every bound context + +- [ ] **Step 1: Write the failing test** + +Read `internal/api/service_test.go` first to reuse its existing fake-cluster construction rather than inventing a second one. Add: + +```go +func TestPromotionKeepsEveryContextInAnEnvironment(t *testing.T) { + // Two contexts bound to one environment, running different versions. + // Build the service with both contexts pointing at prod, the second + // serving checkout at v2.12.0 and the first at v2.13.1. + s := serviceWithContexts(t, map[string]fakeCluster{ + "prod-us-east": {env: "prod", apps: []fakeApp{{name: "checkout", ns: "shop", image: "reg/checkout:v2.13.1"}}}, + "prod-eu-west": {env: "prod", apps: []fakeApp{{name: "checkout", ns: "shop", image: "reg/checkout:v2.12.0"}}}, + }) + + v := s.Promotion() + + if len(v.Envs) != 1 { + t.Fatalf("envs = %d, want 1: both contexts bind to prod", len(v.Envs)) + } + if got := len(v.Envs[0].Contexts); got != 2 { + t.Errorf("prod contexts = %d, want 2: the second context was dropped", got) + } + if len(v.Rows) != 1 { + t.Fatalf("rows = %d, want 1", len(v.Rows)) + } + c := v.Rows[0].Cells[0] + if c.State != promotion.StateSplit { + t.Errorf("state = %q, want %q: two clusters on different versions is not one version", c.State, promotion.StateSplit) + } +} + +func TestPromotionMarksAnUnreachableContextOnItsEnvironment(t *testing.T) { + s := serviceWithContexts(t, map[string]fakeCluster{ + "prod-us-east": {env: "prod", apps: []fakeApp{{name: "checkout", ns: "shop", image: "reg/checkout:v2.13.1"}}}, + "prod-eu-west": {env: "prod", unreachable: true}, + }) + + v := s.Promotion() + + if len(v.Envs) != 1 { + t.Fatalf("envs = %d, want 1", len(v.Envs)) + } + if got := v.Envs[0].Unreadable; len(got) != 1 || got[0] != "prod-eu-west" { + t.Errorf("unreadable = %v, want [prod-eu-west]", got) + } + if v.Rows[0].Cells[0].State != promotion.StateSplit { + t.Error("a cluster nobody could read is not an agreement") + } +} +``` + +Add `serviceWithContexts`, `fakeCluster`, and `fakeApp` helpers only if the file has no equivalent. If it does, adapt the tests to the existing helpers rather than adding a parallel set. + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `go test ./internal/api/ -run 'TestPromotionKeepsEveryContext|TestPromotionMarksAnUnreachable' -v` +Expected: FAIL, `prod contexts = 1, want 2` + +- [ ] **Step 3: Replace the first-one-wins skip with grouping** + +Replace `Service.Promotion` at `internal/api/service.go:992-1047`: + +```go +// Promotion compares every app across every environment. +// +// Opening this view is what connects the environments: the matrix cannot answer +// "is the fix in prod yet" without reading prod. An environment that will not +// connect becomes a column of unreadable cells naming the reason, never an +// empty column that reads as "nothing is deployed there". +// +// Every context bound to an environment is read, not the first. A team running +// prod in three regions has three answers, and reporting one of them as the +// environment's version is the defect this view exists to prevent. +func (s *Service) Promotion() PromotionView { + ctx, cancel := context.WithTimeout(context.Background(), s.timeout) + defer cancel() + + type accum struct { + risk string + contexts []string + unreadable []string + } + byEnv := map[string]*accum{} + var order []string + + var instances []promotion.Instance + var unreachable []string + + for _, name := range s.mgr.ConnectOrder() { + kctx := s.cfg.MustGet(name) + env := s.conf.Environment(kctx) + + acc, ok := byEnv[env.Name] + if !ok { + acc = &accum{risk: env.Risk.String()} + byEnv[env.Name] = acc + order = append(order, env.Name) + } + acc.contexts = append(acc.contexts, name) + + reason := "" + if err := s.mgr.Connect(ctx, name); err != nil { + reason = err.Error() + } + client, ok := s.mgr.ClientFor(name) + if !ok { + acc.unreadable = append(acc.unreadable, name) + unreachable = append(unreachable, env.Name) + instances = append(instances, promotion.Instance{ + Env: env.Name, Context: name, App: "", Denied: true, + DeniedReason: orElseString(reason, "not connected"), + }) + continue + } + + snap, err := clusters.Fetch(ctx, client, kctx, clusters.FetchOptions{Tier: s.mgr.Tier(name)}) + if err != nil { + acc.unreadable = append(acc.unreadable, name) + unreachable = append(unreachable, env.Name) + continue + } + for _, a := range snap.Apps { + in := instanceOf(env.Name, a) + in.Context = name + instances = append(instances, in) + } + } + + envs := make([]promotion.Env, 0, len(order)) + for _, name := range order { + acc := byEnv[name] + envs = append(envs, promotion.Env{ + Name: name, Risk: acc.risk, + Context: acc.contexts[0], Contexts: acc.contexts, Unreadable: acc.unreadable, + }) + } + + // Instances with no app name are placeholders for a cluster nobody could + // read; they marked the environment unreadable and nothing else. + real := instances[:0] + for _, in := range instances { + if in.App != "" { + real = append(real, in) + } + } + + rows := promotion.Build(envs, real) + return PromotionView{Envs: envs, Rows: rows, Summary: promotion.Summarize(rows), Unreachable: dedupeStrings(unreachable)} +} + +// dedupeStrings keeps the first occurrence of each value. An environment with +// three unreachable clusters is one unreachable environment in the banner. +func dedupeStrings(in []string) []string { + if len(in) == 0 { + return nil + } + seen := map[string]bool{} + out := make([]string, 0, len(in)) + for _, v := range in { + if seen[v] { + continue + } + seen[v] = true + out = append(out, v) + } + return out +} +``` + +- [ ] **Step 4: Run the api suite** + +Run: `go test ./internal/api/ -v` +Expected: PASS. Pre-existing promotion tests in this package assert single-context behavior and must still pass. + +- [ ] **Step 5: Commit** + +```bash +go build ./... && go test ./... +git add internal/api/ +git commit -m "fix(promotion): read every context in an environment, not the first + +service.go skipped a second context bound to the same environment before it +was ever connected, on the assumption two contexts in one environment are +identical. A team running prod in three regions disproves that, and the +matrix rendered one region's version as prod's. Every context is now read, +and the ones that do not answer are named on the environment." +``` + +--- + +### Task 4: The fleet derivation + +**Files:** +- Create: `internal/fleet/fleet.go`, `internal/fleet/fleet_test.go` + +**Interfaces:** +- Consumes: `promotion.CompareTags` from Task 1 +- Produces: + - `fleet.Placement` struct, states `StatePresent`, `StateAbsent`, `StateDenied`, `StateUnreachable`, `StatePending` + - `fleet.Row{Placement, Behind bool, MutableTag bool, Note string}` + - `fleet.View{App, Namespace, Rows, Newest, Behind, Present, Clusters, MutableTag}` + - `fleet.Build(app, namespace string, ps []Placement) View` + +- [ ] **Step 1: Write the failing test** + +Create `internal/fleet/fleet_test.go`: + +```go +package fleet + +import "testing" + +func present(ctx, server, env, tag, digest string) Placement { + return Placement{ + Context: ctx, ClusterID: server, Env: env, Namespace: "shop", + State: StatePresent, Image: "reg/checkout:" + tag, Tag: tag, Digest: digest, + Health: "healthy", Ready: "3/3", + } +} + +func rowFor(t *testing.T, v View, ctx string) Row { + t.Helper() + for _, r := range v.Rows { + if r.Context == ctx { + return r + } + } + t.Fatalf("no row for context %q", ctx) + return Row{} +} + +func TestNewestIsTheHighestTagAcrossPresentClusters(t *testing.T) { + v := Build("checkout", "shop", []Placement{ + present("prod-us-east", "https://a", "prod", "v2.13.1", "sha256:bb"), + present("prod-eu-west", "https://b", "prod", "v2.12.0", "sha256:cc"), + present("qa-cluster", "https://c", "qa", "v2.14.0", "sha256:dd"), + }) + if v.Newest != "v2.14.0" { + t.Errorf("newest = %q, want v2.14.0", v.Newest) + } + if v.Behind != 2 { + t.Errorf("behind = %d, want 2", v.Behind) + } + if !rowFor(t, v, "prod-eu-west").Behind { + t.Error("prod-eu-west runs v2.12.0 and is behind") + } + if rowFor(t, v, "qa-cluster").Behind { + t.Error("the newest cluster is not behind itself") + } +} + +func TestUnorderableTagsAreNeverCalledBehind(t *testing.T) { + v := Build("checkout", "shop", []Placement{ + present("a", "https://a", "qa", "sha-abc123", "sha256:aa"), + present("b", "https://b", "qa", "sha-def456", "sha256:bb"), + }) + if v.Behind != 0 { + t.Errorf("behind = %d, want 0: two build ids cannot be ordered, and claiming one is behind invents a direction", v.Behind) + } +} + +func TestSameTagDifferentDigestOutranksBehind(t *testing.T) { + v := Build("checkout", "shop", []Placement{ + present("a", "https://a", "prod", "v2.13.1", "sha256:bb"), + present("b", "https://b", "prod", "v2.13.1", "sha256:cc"), + present("c", "https://c", "prod", "v2.12.0", "sha256:dd"), + }) + if !v.MutableTag { + t.Fatal("one tag resolving to two digests is the loudest state on the screen") + } + if v.Rows[0].Context != "a" && v.Rows[0].Context != "b" { + t.Errorf("first row = %q; the mutable-tag clusters sort above the behind one", v.Rows[0].Context) + } +} + +func TestTwoContextsOnOneClusterMergeToOneRow(t *testing.T) { + v := Build("checkout", "shop", []Placement{ + {Context: "prod-alias", ClusterID: "https://a", Env: "prod", State: StateUnreachable, Reason: "credentials expired"}, + present("prod-us-east", "https://a", "prod", "v2.13.1", "sha256:bb"), + }) + if len(v.Rows) != 1 { + t.Fatalf("rows = %d, want 1: one cluster reached by two contexts is one cluster", len(v.Rows)) + } + if v.Rows[0].State != StatePresent { + t.Errorf("state = %q, want %q: the context that answered wins over the one that did not", v.Rows[0].State, StatePresent) + } + if v.Clusters != 1 { + t.Errorf("clusters = %d, want 1; counting it twice inflates the headline number", v.Clusters) + } +} + +func TestTheFiveStatesStayDistinct(t *testing.T) { + v := Build("checkout", "shop", []Placement{ + present("a", "https://a", "qa", "v1.0.0", "sha256:aa"), + {Context: "b", ClusterID: "https://b", Env: "stg", State: StateAbsent}, + {Context: "c", ClusterID: "https://c", Env: "prod", State: StateDenied, Reason: "pods list refused"}, + {Context: "d", ClusterID: "https://d", Env: "prod", State: StateUnreachable, Reason: "dial timeout"}, + {Context: "e", ClusterID: "https://e", Env: "prod", State: StatePending}, + }) + if v.Clusters != 5 { + t.Fatalf("clusters = %d, want 5", v.Clusters) + } + if v.Present != 1 { + t.Errorf("present = %d, want 1", v.Present) + } + seen := map[string]bool{} + for _, r := range v.Rows { + seen[r.State] = true + } + for _, s := range []string{StatePresent, StateAbsent, StateDenied, StateUnreachable, StatePending} { + if !seen[s] { + t.Errorf("state %q was collapsed into another; absent, denied, and unreachable are different facts", s) + } + } +} + +func TestNoClusterRunsTheAppAtAll(t *testing.T) { + v := Build("checkout", "shop", []Placement{ + {Context: "a", ClusterID: "https://a", Env: "qa", State: StateAbsent}, + {Context: "b", ClusterID: "https://b", Env: "prod", State: StateAbsent}, + }) + if v.Newest != "" { + t.Errorf("newest = %q, want empty", v.Newest) + } + if v.Clusters != 2 || v.Present != 0 { + t.Errorf("clusters = %d present = %d, want 2 and 0: the screen names what it asked", v.Clusters, v.Present) + } +} +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `go test ./internal/fleet/ -v` +Expected: FAIL, no such package + +- [ ] **Step 3: Write the implementation** + +Create `internal/fleet/fleet.go`: + +```go +// Package fleet answers "is every cluster running the latest version". +// +// One app, one row per cluster, environment demoted to a column. The promotion +// matrix compares environments side by side; this compares the clusters inside +// and across them, which is the question a team running prod in three regions +// asks and the matrix cannot phrase. +// +// Two facts carry the screen: the newest tag among the clusters that answered, +// and every cluster behind it. Everything else is there so a cluster that did +// not answer is never mistaken for a cluster without the app. +package fleet + +import ( + "fmt" + "sort" + + "github.com/dynaum/kubeside/internal/promotion" +) + +// Placement states. +const ( + StatePresent = "present" + StateAbsent = "absent" + StateDenied = "denied" + StateUnreachable = "unreachable" + StatePending = "pending" +) + +// Placement is one app as it exists in one cluster. +type Placement struct { + Context string `json:"context"` + ClusterID string `json:"clusterId"` + Env string `json:"env"` + Namespace string `json:"namespace,omitempty"` + + State string `json:"state"` + Reason string `json:"reason,omitempty"` + + Image string `json:"image,omitempty"` + Tag string `json:"tag,omitempty"` + Digest string `json:"digest,omitempty"` + // DigestPending marks a placement whose digest has not arrived. A missing + // digest never reads as a match. + DigestPending bool `json:"digestPending,omitempty"` + + Health string `json:"health,omitempty"` + Ready string `json:"ready,omitempty"` + RevisionAt string `json:"revisionAt,omitempty"` + // Aliases names other kubeconfig contexts pointing at this same cluster, + // so a merged row says what it merged. + Aliases []string `json:"aliases,omitempty"` +} + +// Row is one cluster, judged against the newest version. +type Row struct { + Placement + Behind bool `json:"behind,omitempty"` + // MutableTag marks a cluster carrying the newest tag with a digest another + // cluster's copy of that tag does not share. + MutableTag bool `json:"mutableTag,omitempty"` + Note string `json:"note,omitempty"` +} + +// View is the whole screen. +type View struct { + App string `json:"app"` + Namespace string `json:"namespace"` + Rows []Row `json:"rows"` + + Newest string `json:"newest,omitempty"` + Clusters int `json:"clusters"` + Present int `json:"present"` + Behind int `json:"behind"` + // MutableTag is true when one tag resolved to more than one digest. It + // outranks being behind: a cluster openly on an older version is a + // schedule, two clusters claiming one version while running different + // code is a defect. + MutableTag bool `json:"mutableTag,omitempty"` +} + +// Build derives the screen from one placement per context. +func Build(app, namespace string, ps []Placement) View { + merged := mergeByCluster(ps) + + v := View{App: app, Namespace: namespace, Clusters: len(merged)} + + newest := "" + for _, p := range merged { + if p.State != StatePresent { + continue + } + v.Present++ + if p.Tag == "" { + continue + } + if newest == "" || promotion.CompareTags(p.Tag, newest) > 0 { + newest = p.Tag + } + } + v.Newest = newest + + // A tag carrying two digests is worse than a cluster openly behind. + digests := map[string]bool{} + for _, p := range merged { + if p.State == StatePresent && p.Tag == newest && p.Digest != "" { + digests[p.Digest] = true + } + } + v.MutableTag = len(digests) > 1 + + v.Rows = make([]Row, 0, len(merged)) + for _, p := range merged { + r := Row{Placement: p} + switch p.State { + case StatePresent: + r.DigestPending = p.Digest == "" + if p.Tag == newest { + if v.MutableTag && p.Digest != "" { + r.MutableTag = true + r.Note = fmt.Sprintf("runs %s, and so does another cluster with a different digest", newest) + } + break + } + // Behind only when the tags can be ordered. Two build ids differ + // without one being older, and calling either behind would invent + // a direction. + if p.Tag != "" && newest != "" && promotion.CompareTags(p.Tag, newest) < 0 { + r.Behind = true + v.Behind++ + r.Note = fmt.Sprintf("behind %s", newest) + } else if p.Tag != newest { + r.Note = fmt.Sprintf("runs %s, which cannot be ordered against %s", p.Tag, newest) + } + case StateUnreachable: + r.Note = orElse(p.Reason, "the cluster did not answer") + case StateDenied: + r.Note = orElse(p.Reason, "not readable here") + case StateAbsent: + r.Note = "not deployed here" + case StatePending: + r.Note = "asking" + } + v.Rows = append(v.Rows, r) + } + + sort.SliceStable(v.Rows, func(i, j int) bool { + si, sj := severity(v.Rows[i]), severity(v.Rows[j]) + if si != sj { + return si > sj + } + return v.Rows[i].Context < v.Rows[j].Context + }) + return v +} + +// severity sorts disagreement to the top, matching the promotion matrix's +// default of showing what needs attention first. +func severity(r Row) int { + switch { + case r.MutableTag: + return 5 + case r.Behind: + return 4 + case r.State == StateUnreachable: + return 3 + case r.State == StateDenied: + return 2 + case r.State == StatePending: + return 1 + default: + return 0 + } +} + +// mergeByCluster collapses contexts pointing at one cluster. +// +// A kubeconfig commonly holds the same cluster twice under two names, and one +// of the two may carry credentials that no longer work. Counting it twice +// inflates every number on the screen, so the most informative answer wins and +// the other context is recorded as an alias. +func mergeByCluster(ps []Placement) []Placement { + at := map[string]int{} + out := make([]Placement, 0, len(ps)) + for _, p := range ps { + key := p.ClusterID + if key == "" { + key = "ctx:" + p.Context + } + i, seen := at[key] + if !seen { + at[key] = len(out) + out = append(out, p) + continue + } + if informativeness(p.State) > informativeness(out[i].State) { + p.Aliases = append(p.Aliases, out[i].Context) + p.Aliases = append(p.Aliases, out[i].Aliases...) + out[i] = p + continue + } + out[i].Aliases = append(out[i].Aliases, p.Context) + } + return out +} + +// informativeness ranks what a cluster told us. Present beats absent, because +// a version is more than a confirmed nothing. Absent beats denied and +// unreachable, because the cluster answered at all. +func informativeness(state string) int { + switch state { + case StatePresent: + return 4 + case StateAbsent: + return 3 + case StateDenied: + return 2 + case StateUnreachable: + return 1 + default: + return 0 + } +} + +func orElse(v, fallback string) string { + if v == "" { + return fallback + } + return v +} +``` + +Confirm the module path in `go.mod` and correct the `promotion` import if it differs from `github.com/dynaum/kubeside`. + +- [ ] **Step 4: Run test to verify it passes** + +Run: `go test ./internal/fleet/ -v` +Expected: PASS, all seven tests + +- [ ] **Step 5: Commit** + +```bash +go build ./... && go test ./... +git add internal/fleet/ +git commit -m "feat(fleet): derive which clusters run the newest version + +One app, one row per cluster. Newest tag across the clusters that answered, +and every cluster behind it. Five states stay distinct so a cluster behind a +VPN is never read as a cluster without the app, and two contexts aimed at one +cluster merge so the headline count stays honest." +``` + +--- + +### Task 5: Gather placements across every context + +**Files:** +- Create: `internal/api/fleet.go`, `internal/api/fleet_test.go` +- Modify: `internal/config/config.go:298-305` (export `normalizeURL`) + +**Interfaces:** +- Consumes: `fleet.Build`, `fleet.Placement`, `promotion.Identity` from Tasks 1 and 4 +- Produces: `Service.Fleet(app, namespace string) fleet.View`, `config.NormalizeURL(u string) string` + +- [ ] **Step 1: Write the failing test** + +Create `internal/api/fleet_test.go` using the same fake-cluster helpers Task 3 used: + +```go +func TestFleetAsksEveryContext(t *testing.T) { + s := serviceWithContexts(t, map[string]fakeCluster{ + "qa-cluster": {env: "qa", apps: []fakeApp{{name: "checkout", ns: "shop", image: "reg/checkout:v2.14.0"}}}, + "prod-us-east": {env: "prod", apps: []fakeApp{{name: "checkout", ns: "shop", image: "reg/checkout:v2.13.1"}}}, + "prod-eu-west": {env: "prod", apps: []fakeApp{{name: "checkout", ns: "shop", image: "reg/checkout:v2.12.0"}}}, + }) + + v := s.Fleet("checkout", "shop") + + if v.Clusters != 3 { + t.Fatalf("clusters = %d, want 3", v.Clusters) + } + if v.Newest != "v2.14.0" { + t.Errorf("newest = %q, want v2.14.0", v.Newest) + } + if v.Behind != 2 { + t.Errorf("behind = %d, want 2", v.Behind) + } +} + +func TestFleetNamesAnUnreachableClusterRatherThanOmittingIt(t *testing.T) { + s := serviceWithContexts(t, map[string]fakeCluster{ + "qa-cluster": {env: "qa", apps: []fakeApp{{name: "checkout", ns: "shop", image: "reg/checkout:v2.14.0"}}}, + "prod-eu-west": {env: "prod", unreachable: true}, + }) + + v := s.Fleet("checkout", "shop") + + if v.Clusters != 2 { + t.Fatalf("clusters = %d, want 2: a cluster that did not answer is still a cluster we asked", v.Clusters) + } + var found bool + for _, r := range v.Rows { + if r.Context == "prod-eu-west" { + found = true + if r.State != fleet.StateUnreachable { + t.Errorf("state = %q, want %q", r.State, fleet.StateUnreachable) + } + if r.Note == "" { + t.Error("an unreachable cluster names why") + } + } + } + if !found { + t.Error("prod-eu-west was omitted; a blank row would read as 'not deployed there'") + } +} + +func TestFleetMatchesAcrossEnvSuffixedNamespaces(t *testing.T) { + s := serviceWithContexts(t, map[string]fakeCluster{ + "qa-cluster": {env: "qa", apps: []fakeApp{{name: "checkout", ns: "team-a-qa", image: "reg/checkout:v2.14.0"}}}, + "prod-cluster": {env: "prod", apps: []fakeApp{{name: "checkout", ns: "team-a-prod", image: "reg/checkout:v2.13.1"}}}, + }) + + v := s.Fleet("checkout", "team-a-qa") + + if v.Present != 2 { + t.Errorf("present = %d, want 2: team-a-qa and team-a-prod are one app in two places", v.Present) + } +} + +func TestFleetFoundNowhereStillNamesWhatItAsked(t *testing.T) { + s := serviceWithContexts(t, map[string]fakeCluster{ + "qa-cluster": {env: "qa"}, + "prod-cluster": {env: "prod"}, + }) + + v := s.Fleet("typo", "shop") + + if v.Clusters != 2 { + t.Errorf("clusters = %d, want 2: never render an unknown window as an empty one", v.Clusters) + } + if len(v.Rows) != 2 { + t.Errorf("rows = %d, want 2", len(v.Rows)) + } +} +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `go test ./internal/api/ -run TestFleet -v` +Expected: FAIL, `s.Fleet undefined` + +- [ ] **Step 3: Export the URL normalizer** + +In `internal/config/config.go`, rename `normalizeURL` to `NormalizeURL`, update its doc comment and all call sites in the package (`config.go:282` among them): + +```go +// NormalizeURL makes server URLs comparable across the small differences that +// mean nothing: a trailing slash, or a host written in a different case. +func NormalizeURL(u string) string { + // body unchanged +} +``` + +- [ ] **Step 4: Write the gatherer** + +Create `internal/api/fleet.go`: + +```go +package api + +import ( + "context" + + "github.com/dynaum/kubeside/internal/apps" + "github.com/dynaum/kubeside/internal/clusters" + "github.com/dynaum/kubeside/internal/config" + "github.com/dynaum/kubeside/internal/fleet" + "github.com/dynaum/kubeside/internal/promotion" +) + +// Fleet answers "is every cluster running the latest version" for one app. +// +// Opening this view states the intent to ask every cluster, so unlike the app +// list it deliberately wakes every context in the kubeconfig. Rows land as +// clusters answer and a cluster behind a VPN never blocks the rest. +func (s *Service) Fleet(app, namespace string) fleet.View { + ctx, cancel := context.WithTimeout(context.Background(), s.timeout) + defer cancel() + + want := promotion.Identity(app, namespace) + var placements []fleet.Placement + + for _, name := range s.mgr.ConnectOrder() { + kctx := s.cfg.MustGet(name) + env := s.conf.Environment(kctx) + + p := fleet.Placement{ + Context: name, + ClusterID: config.NormalizeURL(kctx.Server), + Env: env.Name, + } + + if err := s.mgr.Connect(ctx, name); err != nil { + p.State = fleet.StateUnreachable + p.Reason = err.Error() + placements = append(placements, p) + continue + } + client, ok := s.mgr.ClientFor(name) + if !ok { + p.State = fleet.StateUnreachable + p.Reason = "not connected" + placements = append(placements, p) + continue + } + + snap, err := clusters.Fetch(ctx, client, kctx, clusters.FetchOptions{Tier: s.mgr.Tier(name)}) + if err != nil { + p.State = fleet.StateUnreachable + p.Reason = err.Error() + placements = append(placements, p) + continue + } + + match, found := findApp(snap.Apps, want) + if !found { + // The cluster answered and does not run this app. That is a fact, + // and a different one from a cluster that would not answer. + p.State = fleet.StateAbsent + placements = append(placements, p) + continue + } + + in := instanceOf(env.Name, match) + p.State = fleet.StatePresent + p.Namespace = in.Namespace + p.Image = in.Image + p.Tag = in.Tag + p.Digest = in.Digest + p.DigestPending = in.Digest == "" + p.Health = in.Health + p.Ready = in.Ready + p.RevisionAt = in.RevisionAt + placements = append(placements, p) + } + + return fleet.Build(app, namespace, placements) +} + +// findApp matches by the same identity the promotion matrix uses, so one app +// is one app on both screens. +func findApp(list []apps.App, want string) (apps.App, bool) { + for _, a := range list { + if promotion.Identity(a.Key.Name, a.Key.Namespace) == want { + return a, true + } + } + return apps.App{}, false +} +``` + +- [ ] **Step 5: Run test to verify it passes** + +Run: `go test ./internal/api/ -run TestFleet -v` +Expected: PASS, all four tests + +- [ ] **Step 6: Commit** + +```bash +go build ./... && go test ./... +git add internal/api/fleet.go internal/api/fleet_test.go internal/config/ +git commit -m "feat(fleet): gather one app from every context + +Walks every kubeconfig context, connects lazily, and matches by the same +identity the promotion matrix uses. A cluster that will not answer becomes an +unreachable row naming the reason, never an omitted one, because a missing +row reads as 'not deployed there'. Deduping keys off the API server URL, +which config already normalizes." +``` + +--- + +### Task 6: Transport + +**Files:** +- Modify: `internal/api/server.go:73-103` (API interface), `:142` (routes), add `handleFleet` +- Modify: `internal/api/view.go` (re-export the view type for the OpenAPI-shaped surface) +- Test: `internal/api/server_test.go` + +**Interfaces:** +- Consumes: `Service.Fleet` from Task 5 +- Produces: `GET /api/fleet?app=&namespace=` returning `fleet.View` as JSON + +- [ ] **Step 1: Write the failing test** + +Add to `internal/api/server_test.go`, following the existing handler-test pattern in that file: + +```go +func TestFleetEndpointRequiresAnApp(t *testing.T) { + s := newTestServer(t) + rec := httptest.NewRecorder() + s.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/api/fleet", nil)) + if rec.Code != http.StatusBadRequest { + t.Errorf("status = %d, want 400: the screen is about one app and needs its name", rec.Code) + } +} + +func TestFleetEndpointReturnsTheView(t *testing.T) { + s := newTestServer(t) + rec := httptest.NewRecorder() + s.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/api/fleet?app=checkout&namespace=shop", nil)) + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, want 200", rec.Code) + } + var v fleet.View + if err := json.NewDecoder(rec.Body).Decode(&v); err != nil { + t.Fatalf("decode: %v", err) + } + if v.App != "checkout" { + t.Errorf("app = %q, want checkout", v.App) + } +} +``` + +Add `Fleet(app, namespace string) fleet.View` to the fake API the test file already uses. + +- [ ] **Step 2: Run test to verify it fails** + +Run: `go test ./internal/api/ -run TestFleetEndpoint -v` +Expected: FAIL, 404 rather than 400 + +- [ ] **Step 3: Add the interface method, the route, and the handler** + +In `internal/api/server.go`, add to the `API` interface after `Promotion()`: + +```go + // Fleet is one app across every cluster, for the question the matrix + // cannot phrase: are all of them on the newest version. + Fleet(app, namespace string) fleet.View +``` + +Register the route beside the others at `:142`: + +```go + mux.HandleFunc("/api/fleet", s.handleFleet) +``` + +Add the handler beside `handlePromotion`: + +```go +// handleFleet answers "is every cluster running the latest version". +func (s *Server) handleFleet(w http.ResponseWriter, r *http.Request) { + app := r.URL.Query().Get("app") + if app == "" { + writeError(w, http.StatusBadRequest, "app is required") + return + } + writeJSON(w, http.StatusOK, s.api.Fleet(app, r.URL.Query().Get("namespace"))) +} +``` + +Match `writeError` to whatever the file's existing error helper is named; reuse it rather than adding one. + +- [ ] **Step 4: Run the api suite** + +Run: `go test ./internal/api/ -v` +Expected: PASS + +- [ ] **Step 5: Commit** + +```bash +go build ./... && go test ./... +git add internal/api/ +git commit -m "feat(fleet): serve the fleet view over HTTP + +GET /api/fleet?app=&namespace=. A request without an app is a 400 rather +than an empty screen, because the view is about one app by construction." +``` + +--- + +### Task 7: Author the fleet screen in the design project + +CLAUDE.md requires porting UI from the Claude Design project and forbids inventing it. The project's `screens/` has no fleet screen. This task creates it and gates the React work on human review. + +**Files:** +- Create, in design project `6546cea1-fa99-441a-97c6-4317b127d9a7`: `screens/fleet.html` +- Reference: `screens/promotion.html`, `tokens.css`, `components/tables-timeline.html` + +- [ ] **Step 1: Read the existing promotion screen** + +Use `DesignSync` with `method: get_file`, `path: screens/promotion.html`. Read `tokens.css` and `components/tables-timeline.html` the same way. Build the fleet screen from the row, status glyph, and environment-color patterns already there. Introduce no new color, type size, or spacing value. + +- [ ] **Step 2: Write the screen** + +Author `screens/fleet.html` locally with the first line carrying the card marker: + +```html + +``` + +Content: the topbar and page-head structure from `promotion.html`, then a table with columns CLUSTER, ENV, VERSION, HEALTH, and NOTE. Render every row state at least once so the card demonstrates all five: present on newest, behind, mutable-tag, unreachable, denied, absent. Include the header summary line reading `N clusters · M behind`. + +- [ ] **Step 3: Publish it** + +``` +DesignSync method=finalize_plan projectId=6546cea1-fa99-441a-97c6-4317b127d9a7 writes=["screens/fleet.html"] localDir= +DesignSync method=write_files planId= files=[{path: "screens/fleet.html", localPath: "screens/fleet.html"}] +``` + +- [ ] **Step 4: Human review gate** + +Stop here. Ask for review of the published screen before any React work begins. Building the UI first and backfilling the design inverts the rule the project runs on. + +- [ ] **Step 5: Commit nothing** + +This task touches no repository file. It ends with an approved screen in the design project. + +--- + +### Task 8: The fleet screen in React + +**Files:** +- Create: `web/src/FleetScreen.tsx` +- Modify: `web/src/api.ts` (types and client), `web/src/route.ts:4-42`, `web/src/App.tsx:53-126` +- Test: `web/src/route.test.ts` + +**Interfaces:** +- Consumes: `GET /api/fleet` from Task 6, the approved `screens/fleet.html` from Task 7 +- Produces: route `{ screen: "fleet"; app: string; namespace: string }` at hash `#fleet//`, `api.fleet(app, namespace)` + +- [ ] **Step 1: Write the failing route test** + +Add to `web/src/route.test.ts`: + +```ts +it("round-trips the fleet route", () => { + const r = { screen: "fleet", app: "checkout", namespace: "shop" } as const; + expect(parseRoute(routeHash(r))).toEqual(r); +}); + +it("falls back to apps when the fleet route is missing its app", () => { + expect(parseRoute("#fleet")).toEqual({ screen: "apps" }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cd web && npx vitest run src/route.test.ts` +Expected: FAIL + +- [ ] **Step 3: Add the route** + +In `web/src/route.ts`, add to the `Route` union: + +```ts + | { screen: "fleet"; app: string; namespace: string } +``` + +Add to `parseRoute`, before the `apps` branch: + +```ts + if (parts[0] === "fleet" && parts.length >= 3) { + return { screen: "fleet", app: parts[1], namespace: parts[2] }; + } +``` + +Add to `routeHash`, before the promotion branch: + +```ts + if (r.screen === "fleet") return `#fleet/${enc(r.app)}/${enc(r.namespace)}`; +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `cd web && npx vitest run src/route.test.ts` +Expected: PASS + +- [ ] **Step 5: Add the API types and client** + +In `web/src/api.ts`, after `PromotionView`: + +```ts +export interface FleetRow { + context: string; + clusterId: string; + env: string; + namespace?: string; + state: string; // present, absent, denied, unreachable, pending + reason?: string; + image?: string; + tag?: string; + digest?: string; + digestPending?: boolean; + health?: string; + ready?: string; + revisionAt?: string; + aliases?: string[]; + behind?: boolean; + mutableTag?: boolean; + note?: string; +} + +export interface FleetView { + app: string; + namespace: string; + rows: FleetRow[]; + newest?: string; + clusters: number; + present: number; + behind: number; + mutableTag?: boolean; +} +``` + +Add the client method beside `promotion()`, matching that method's existing fetch and error-handling shape: + +```ts + fleet: (app: string, namespace: string): Promise => + get(`/api/fleet?app=${encodeURIComponent(app)}&namespace=${encodeURIComponent(namespace)}`), +``` + +Read `api.ts` to confirm the helper is named `get`; reuse whatever the file already uses. + +- [ ] **Step 6: Write the screen** + +Create `web/src/FleetScreen.tsx`, porting the markup and class names from the approved `screens/fleet.html`. Follow `PromotionScreen.tsx` for the loading, error, and empty-state structure. + +```tsx +import { useEffect, useState } from "react"; +import { api, type FleetView } from "./api"; +import { Glyph } from "./Status"; +import { envToken } from "./health"; + +// Screen 7. Is every cluster running the latest version. +// +// The promotion matrix compares environments side by side. This compares the +// clusters inside and across them, which is what a team running prod in three +// regions asks and the matrix cannot phrase. +// +// Five states, never conflated. A cluster behind a VPN is not a cluster +// without the app, and rendering both blank would answer the screen's own +// question with a guess. + +export function FleetScreen({ app, namespace }: { app: string; namespace: string }) { + const [view, setView] = useState(null); + const [err, setErr] = useState(null); + + useEffect(() => { + let alive = true; + setView(null); + setErr(null); + api.fleet(app, namespace) + .then((v) => { if (alive) setView(v); }) + .catch((e) => { if (alive) setErr(String(e.message ?? e)); }); + return () => { alive = false; }; + }, [app, namespace]); + + return ( + <> +
+
+ {app} + · + every cluster +
+ + {view && ( + + {view.clusters} clusters · {view.present} running it · {view.behind} behind + + )} +
+ +
+
+

{app}

+ {view?.newest ? `newest is ${view.newest}` : "asking every cluster"} +
+ + {err && ( +
+
Could not ask the clusters
+
{err}
+
+ )} + {!err && !view && ( +
asking every cluster…
+ )} + + {view?.mutableTag && ( +
+ + + One tag resolves to more than one digest. These clusters claim the same + version and are not running the same code. + +
+ )} + + {view && view.present === 0 && ( +
+
Not found in any of {view.clusters} clusters
+
+ Every cluster below answered. Check the name, or the namespace it lives in. +
+
+ )} + + {view && ( + + + + + + + + {view.rows.map((r) => ( + + + + + + + + ))} + +
ClusterEnvVersionHealth
+ {r.context} + {r.aliases && r.aliases.length > 0 && ( + also {r.aliases.join(", ")} + )} + {r.env} + {r.state === "present" ? (r.tag || "—") : "—"} + {r.digestPending && digest pending} + + {r.state === "present" + ? <> {r.ready} + : {r.state}} + {r.note}
+ )} +
+ + ); +} +``` + +Replace `className` values with the exact ones from the approved design screen wherever they differ. Confirm `Glyph` and `envToken` signatures against `Status.tsx` and `health.ts` before use. + +- [ ] **Step 7: Render it from App** + +In `web/src/App.tsx`, add a branch beside the promotion one: + +```tsx + {route.screen === "fleet" && ( + + )} +``` + +Update the `route.screen !== "promotion" && !current && ` guard at `App.tsx:126` so the fleet screen does not render the no-selection placeholder underneath itself: + +```tsx + {route.screen !== "promotion" && route.screen !== "fleet" && !current && } +``` + +Check the `App.tsx:53` route-narrowing branch and add `fleet` alongside `promotion` there too. + +- [ ] **Step 8: Run the web suite and build** + +Run: `cd web && npx vitest run && npm run build` +Expected: PASS, and a clean build + +- [ ] **Step 9: Commit** + +```bash +go build ./... && go test ./... +git add web/src/ +git commit -m "feat(fleet): screen 7, one app across every cluster + +Ported from screens/fleet.html in the design project. Five row states render +distinctly, an app found nowhere names the clusters it asked rather than +showing an empty window, and a tag resolving to two digests raises a banner +above everything else on the page." +``` + +--- + +### Task 9: The split cell in the promotion screen + +**Files:** +- Modify: `web/src/api.ts` (PromotionCell, PromotionView.envs, Summary), `web/src/PromotionScreen.tsx` + +**Interfaces:** +- Consumes: `promotion.StateSplit`, `Cell.Clusters`, `Summary.Split` from Task 2, the fleet route from Task 8 +- Produces: a split cell rendering its note and linking to the fleet screen + +- [ ] **Step 1: Extend the types** + +In `web/src/api.ts`: + +```ts +export interface PromotionCell { + env: string; + namespace?: string; + state: string; // same, behind, ahead, differs, digest-differs, absent, denied, split + clusters?: number; + // ... rest unchanged +} +``` + +```ts +export interface PromotionView { + envs: { name: string; risk: string; context?: string; contexts?: string[]; unreadable?: string[] }[]; + rows: PromotionRow[]; + summary: { apps: number; drifted: number; ahead: number; split: number }; + unreachable?: string[]; +} +``` + +- [ ] **Step 2: Render the split cell** + +In `web/src/PromotionScreen.tsx`, find the cell-rendering branch and add `split` beside `absent` and `denied`. A split cell shows no version, shows its note, carries the severe styling the screen already applies via `cell.severe`, and links to fleet: + +```tsx +{cell.state === "split" && ( + +)} +``` + +Thread `onOpenFleet` in from `App.tsx` the same way `onOpenApp` already is, wiring it to `go({ screen: "fleet", app, namespace })`. + +- [ ] **Step 3: Extend the header count** + +Update the summary line at `PromotionScreen.tsx:36` to name splits, since an environment whose clusters disagree is the reason to open the page: + +```tsx +{view.summary.apps} apps · {view.summary.drifted} with drift · {view.summary.ahead} ahead of upstream +{view.summary.split > 0 && ` · ${view.summary.split} split across clusters`} +``` + +- [ ] **Step 4: Run the web suite and build** + +Run: `cd web && npx vitest run && npm run build` +Expected: PASS + +- [ ] **Step 5: Commit** + +```bash +git add web/src/ +git commit -m "feat(promotion): render the split cell and link it to fleet + +An environment whose clusters disagree shows the disagreement and offers the +screen that explains it, rather than a version the matrix never verified." +``` + +--- + +### Task 10: Reaching the screen + +**Files:** +- Modify: `web/src/commands.ts`, `web/src/AppDetailScreen.tsx` +- Test: `web/src/commands.test.ts` + +**Interfaces:** +- Consumes: the fleet route from Task 8 +- Produces: a palette command and an app-detail action, both opening fleet for the app in hand + +- [ ] **Step 1: Write the failing test** + +Read `web/src/commands.test.ts` for the existing command-list assertion shape, then add: + +```ts +it("offers fleet for the app in hand", () => { + const cmds = buildCommands({ ...baseContext, app: { context: "qa", namespace: "shop", workload: "checkout" } }); + const fleet = cmds.find((c) => c.id === "fleet"); + expect(fleet).toBeTruthy(); + expect(fleet!.title.toLowerCase()).toContain("cluster"); +}); + +it("hides fleet when no app is selected", () => { + const cmds = buildCommands(baseContext); + expect(cmds.find((c) => c.id === "fleet")).toBeUndefined(); +}); +``` + +Adapt the argument shape to whatever `buildCommands` actually takes. + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cd web && npx vitest run src/commands.test.ts` +Expected: FAIL + +- [ ] **Step 3: Add the command** + +In `web/src/commands.ts`, add a command matching the file's existing entry shape: + +```ts + if (ctx.app) { + out.push({ + id: "fleet", + title: `${ctx.app.workload} across every cluster`, + hint: "asks every context in your kubeconfig", + run: () => ctx.go({ screen: "fleet", app: ctx.app!.workload, namespace: ctx.app!.namespace }), + }); + } +``` + +- [ ] **Step 4: Add the app-detail entry point** + +In `web/src/AppDetailScreen.tsx`, add an action beside the existing ones opening the fleet route for the current app. Match the surrounding button markup exactly. + +- [ ] **Step 5: Run the web suite and build** + +Run: `cd web && npx vitest run && npm run build` +Expected: PASS + +- [ ] **Step 6: Commit** + +```bash +git add web/src/ +git commit -m "feat(fleet): reach the screen from the palette and app detail + +The command appears only with an app in hand, because the view is about one +app and a palette entry that opens an empty one is a dead end." +``` + +--- + +### Task 11: Degraded-mode and browser coverage + +**Files:** +- Modify: `internal/api/degraded_test.go` +- Modify: the Playwright suite from #30 (locate with `git log --oneline --all -- '*spec.ts' | head`) + +**Interfaces:** +- Consumes: everything above + +- [ ] **Step 1: Write the degraded-mode test** + +Add to `internal/api/degraded_test.go`, following the file's existing construction: + +```go +func TestFleetFillsWithOneClusterUnreachableAndOneDenied(t *testing.T) { + s := serviceWithContexts(t, map[string]fakeCluster{ + "qa-cluster": {env: "qa", apps: []fakeApp{{name: "checkout", ns: "shop", image: "reg/checkout:v2.14.0"}}}, + "prod-us-east": {env: "prod", unreachable: true}, + "prod-eu-west": {env: "prod", denied: true}, + }) + + v := s.Fleet("checkout", "shop") + + if v.Clusters != 3 { + t.Fatalf("clusters = %d, want 3: every cluster asked appears", v.Clusters) + } + if v.Newest != "v2.14.0" { + t.Errorf("newest = %q; the reachable cluster still answers", v.Newest) + } + states := map[string]string{} + for _, r := range v.Rows { + states[r.Context] = r.State + } + if states["prod-us-east"] == states["prod-eu-west"] { + t.Error("unreachable and denied render the same; they are different facts") + } +} +``` + +If the fake cluster helper has no `denied` field, add one that makes `clusters.Fetch` return a 403-shaped error. + +- [ ] **Step 2: Run it** + +Run: `go test ./internal/api/ -run TestFleetFills -v` +Expected: PASS once the helper supports `denied`, FAIL first if it does not + +- [ ] **Step 3: Add the Playwright screenshot** + +Add a spec navigating to `#fleet/checkout/shop` against the existing fixture server, waiting for the table, and taking a screenshot into the diff gate. Mirror the promotion spec in that suite exactly. + +- [ ] **Step 4: Run both suites** + +Run: `go test ./... && cd web && npx playwright test` +Expected: PASS. A new screenshot baseline is expected on first run; commit it. + +- [ ] **Step 5: Commit** + +```bash +git add internal/api/ web/ +git commit -m "test(fleet): degraded mode and a screenshot baseline + +One cluster unreachable, one denied, and the screen still fills. The two +render differently, because conflating them is the failure the five states +exist to prevent." +``` + +--- + +### Task 12: Documentation + +**Files:** +- Modify: `docs/03-product-spec.md`, `docs/04-multi-cluster.md:20-22`, `:331`, `:334`, `README.md` +- Create: `docs/guide/13-fleet.md` + +- [ ] **Step 1: Correct the aggregation promise** + +`docs/04-multi-cluster.md:20-22` currently reads that the promotion view "aggregates them with per-context detail on expansion." Replace with the shipped behavior: the cell collapses only on full agreement, renders a split otherwise, and the fleet screen carries per-cluster detail. + +Do the same for the table row at `:334`. + +- [ ] **Step 2: Correct the dedupe claim** + +`docs/04-multi-cluster.md:331` claims detection "by API server URL and cluster UID". Only the URL ships. Rewrite to say so and record why: no cluster UID exists in the codebase, and reading one needs a `kube-system` namespace permission kubeside does not request and a namespace-scoped developer would be refused. + +- [ ] **Step 3: Add the screen to the product spec** + +`docs/03-product-spec.md` commits to four screens while the code carries seven numbered views. Reconcile the count and add fleet as Screen 7 with its job: is every cluster running the latest version. + +- [ ] **Step 4: Write the guide page** + +Create `docs/guide/13-fleet.md` following the structure of `docs/guide/07-promotion.md`: what the screen answers, how to open it, what each state means, and the explicit warning that opening it wakes every context in the kubeconfig. + +- [ ] **Step 5: Commit** + +```bash +git add docs/ README.md +git commit -m "docs(fleet): record what shipped, and correct two claims that did not + +04-multi-cluster promised per-context expansion inside the promotion view; +what shipped is a split cell plus a dedicated screen. It also claimed dedupe +by API server URL and cluster UID; only the URL ships, because no cluster UID +exists here and reading one needs a permission a namespace-scoped developer +would be refused." +``` + +--- + +### Task 13: Argo CD enters the comparison + +Answers the first objection the Show HN thread raised. Standalone, no dependency on the fleet work. + +**Files:** +- Modify: `docs/01-problem.md:176`, `:184-193` + +- [ ] **Step 1: Add the reference row** + +Add Argo CD to the table at `docs/01-problem.md:184`, with current star and open-issue counts read from GitHub at the time of writing rather than estimated. + +- [ ] **Step 2: Write the distinction** + +Extend the prose near `:176`. The argument, in the maintainer's own words from the thread: Argo CD is 1:1 with Kubernetes and is the tool for operators. It shows sync state against git, which is a delivery question. kubeside shows the app a developer owns across the environments it runs in, which is a different question with a different reader. State that kubeside reads the `argocd.argoproj.io/instance` label as a grouping signal, per `docs/03-product-spec.md:44`, so the two coexist. + +- [ ] **Step 3: Commit** + +```bash +git add docs/01-problem.md +git commit -m "docs: answer the Argo CD question on the page, not in a comment thread + +It was the first objection raised on Show HN and the comparison never +mentioned it. Sync state against git is a delivery question; the app across +its environments is a different one." +``` + +--- + +### Task 14: Record the TUI decline + +**Files:** +- Modify: `docs/guide/12-troubleshooting.md` + +- [ ] **Step 1: Add the entry** + +Add a short section answering "Is there a TUI, like k9s?" with the reasoning already recorded at `docs/05-architecture.md:20`: the four gaps are layout problems, a monospace grid cannot render a timeline, a side-by-side config diff, or a promotion matrix, and k9s owns the terminal. Note that `--serve` mode, listed under Beyond v1 in `docs/06-roadmap.md:111`, is the answer for reaching kubeside without a local kubeconfig. + +- [ ] **Step 2: Commit** + +```bash +git add docs/guide/12-troubleshooting.md +git commit -m "docs: record why there is no TUI + +Asked on Show HN. 05-architecture already rejected a terminal UI and the +reasoning was not reachable from the docs a user reads." +``` + +--- + +## Self-review + +**Spec coverage.** Model, Task 4. Screen 7, Tasks 7 and 8. Data flow and wake policy, Task 5. Cluster dedupe, Tasks 4 and 5. Matrix fix, Tasks 2, 3, and 9. Five states, Tasks 4 and 11. App found nowhere, Tasks 4, 5, and 8. Same tag different digest, Tasks 2, 4, and 8. Testing, every task plus 11. Design constraint, Task 7. Argo CD, Task 13. TUI, Task 14. Doc changes, Task 12. + +**Two spec items with no task, resolved.** The spec mentions a 403 on the namespace list falling through the discovery chain and expired SSO reusing the inline prompt from #37. Both are existing behavior in `clusters.Fetch` and the connect path, reached unchanged by `Service.Fleet`, which surfaces whatever error they return as an `unreachable` or `denied` row. No new work, and Task 11 covers the denied case. + +**Type consistency.** `promotion.CompareTags`, `promotion.Identity`, and `promotion.StripEnvToken` are defined in Task 1 and consumed in Tasks 2, 4, and 5. `fleet.Placement`, `fleet.Row`, `fleet.View`, and the five state constants are defined in Task 4 and consumed in Tasks 5, 6, 8, and 11. `config.NormalizeURL` is defined in Task 5 and consumed there. `Env.Contexts` and `Env.Unreadable` are defined in Task 2, populated in Task 3, and read in Task 9. `Cell.Clusters` and `Summary.Split` are defined in Task 2 and read in Task 9. The fleet route shape is defined in Task 8 and consumed in Tasks 9 and 10. + +**Known gaps the implementer must close.** These are verified, not suspected. + +`serviceWithContexts`, `fakeCluster`, and `fakeApp` do not exist. The closest is `serviceWith` at `internal/api/tier_test.go:34`, which builds a `*Service` from one `*fake.Clientset` and a clock. Every test in Tasks 3, 5, and 11 needs several contexts with different apps and different reachability, so building `serviceWithContexts` on top of `serviceWith` is the first step of Task 3, and Tasks 5 and 11 reuse it. Task 11 additionally needs a `denied` field on `fakeCluster` making `clusters.Fetch` return a 403-shaped error. + +`newTestServer` at `internal/api/server_test.go:214` does exist and Task 6 uses it as written. + +Task 6's `writeError` and Task 8's `get` are stand-in names for helpers those files already carry under some name. Read `internal/api/server.go` and `web/src/api.ts` and use the real ones. diff --git a/docs/superpowers/specs/2026-08-02-fleet-design.md b/docs/superpowers/specs/2026-08-02-fleet-design.md index 10cda71..95cb85a 100644 --- a/docs/superpowers/specs/2026-08-02-fleet-design.md +++ b/docs/superpowers/specs/2026-08-02-fleet-design.md @@ -33,6 +33,23 @@ arrived last, and discards the other silently. `promotion.Env` carries a single `Context string`. `promotion.Instance` carries no context at all. The aggregation was specified and never built. +The overwrite is the second place the data dies. The first is +`internal/api/service.go:1006`: + +```go +// Two contexts bound to one environment would produce two identical +// columns; the first one wins, which is what the binding is for. +if seen[env.Name] { + continue +} +``` + +A second context in an environment is skipped before it is connected or +fetched. Its assumption, that two contexts in one environment are identical, is +exactly what a team running prod in three regions disproves. Both layers get +fixed, and the service layer comes first, since the matrix cannot render data +nobody gathered. + So the promotion view answers "is the fix in prod" with a confident version it did not verify. Compare that against the rule at `docs/04-multi-cluster.md:216`, where unauthorized renders distinctly from @@ -70,7 +87,7 @@ New package `internal/fleet`. One type carries one app in one cluster. // Placement is one app as it exists in one cluster. type Placement struct { Context string // kubeconfig context name, personal and unstable - ClusterID string // cluster UID, the identity surviving a rename + ClusterID string // API server URL, from kubeconfig.Context.Server Env string // resolved environment name, "unclassified" when unmatched Namespace string @@ -103,7 +120,7 @@ Cross-cluster app matching reuses the existing rules: name plus namespace, tolerating environment-suffix conventions, with `apps.match` in the config file overriding. No new matching mechanism. -## Screen +## Screen 7: fleet One app, one row per cluster, environment demoted to a column. @@ -149,6 +166,14 @@ the failure mode at `docs/04-multi-cluster.md:331`. Without the merge one cluster appears twice and inflates the behind count, which turns the feature's headline number into a lie. +That line names two signals, API server URL and cluster UID. Only the first +ships here. `kubeconfig.Context.Server` already holds the URL and costs nothing +to read. No cluster UID exists anywhere in the codebase, and obtaining one means +reading the `kube-system` namespace UID, which needs a permission kubeside does +not currently request and which a namespace-scoped developer would be refused. +The URL alone covers the case the failure mode describes, two kubeconfig entries +aimed at one server. `docs/04-multi-cluster.md:331` gets corrected to say so. + ## The promotion matrix fix `promotion.Env` gains `Contexts []string`. `promotion.Instance` gains `Context`. @@ -249,10 +274,10 @@ Under a new `feature:fleet` label, dependencies before dependents. | 1 | Author `screens/fleet.html` in the design project, human-reviewed | design | | 2 | Export `CompareTags` and identity matching from `promotion` | `internal/promotion` | | 3 | `internal/fleet`: placement model and derivation | `internal/fleet` | -| 4 | Fleet query across contexts, lazy connect, `ClusterID` dedupe | `internal/fleet`, `internal/clusters` | -| 5 | Promotion matrix stops collapsing a disagreement | `internal/promotion`, web | +| 4 | Fleet query across contexts, lazy connect, `ClusterID` dedupe | `internal/fleet`, `internal/api` | +| 5 | Promotion matrix stops collapsing a disagreement | `internal/promotion`, `internal/api`, web | | 6 | Fleet deltas over the existing websocket | `internal/api` | -| 7 | Screen 5: fleet, ported from issue 1 | web | +| 7 | Screen 7: fleet, ported from issue 1 | web | | 8 | Palette entry and app-detail entry point | web | | 9 | Degraded-mode and Playwright coverage | tests | | 10 | Docs: spec, multi-cluster, and a guide page | docs | @@ -278,10 +303,15 @@ a `docs/guide/12-troubleshooting.md` entry, since the question will recur. ## Doc changes this forces -- `docs/03-product-spec.md` commits to four screens. Fleet makes five. +- `docs/03-product-spec.md` commits to four screens. The code already carries + seven numbered views: apps, app detail, config, diff as 3b, logs, promotion, + and the palette. Fleet becomes Screen 7 and the spec's count gets reconciled + with what shipped. - `docs/04-multi-cluster.md:20` and `:334` promise aggregation with per-context expansion inside the promotion view. That promise is replaced by the split cell plus the fleet screen, and both lines get rewritten. +- `docs/04-multi-cluster.md:331` claims dedupe by API server URL and cluster + UID. Only the URL ships. The line gets corrected rather than left aspirational. - `README.md:5` carries the tagline a commenter read as a shipped promise. It stays once the feature ships. From 43f1ad772e52aa3a7473ecf25b340cf7d33b7a3b Mon Sep 17 00:00:00 2001 From: Elber Ribeiro Date: Sun, 2 Aug 2026 22:10:38 -0400 Subject: [PATCH 03/38] refactor(promotion): export tag ordering and app identity for reuse internal/fleet must order tags the same way the promotion matrix does. Two comparators disagreeing about which version is newer would be the worst defect the fleet view can carry, so it reuses these rather than growing a second implementation. No behavior change. Closes #61 --- internal/promotion/promotion.go | 18 +++++++++++------- internal/promotion/promotion_test.go | 23 +++++++++++++++++++++++ 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/internal/promotion/promotion.go b/internal/promotion/promotion.go index b1bfb4a..83fc5ff 100644 --- a/internal/promotion/promotion.go +++ b/internal/promotion/promotion.go @@ -201,7 +201,7 @@ func compare(in Instance, upstream *Instance) Cell { } if in.Tag != upstream.Tag { - switch order := compareTags(in.Tag, upstream.Tag); { + switch order := CompareTags(in.Tag, upstream.Tag); { case order < 0: c.State = StateBehind c.Note = fmt.Sprintf("behind %s, which runs %s", upstream.Env, upstream.Tag) @@ -230,22 +230,26 @@ func compare(in Instance, upstream *Instance) Cell { return c } -// identity is how one app is recognized across environments. +// Identity is how one app is recognized across environments. // // Name plus namespace, with an environment suffix or prefix tolerated, because // team-a-qa and team-a-prod are one team's namespace in two places and lining // those up is the whole point of the view. -func identity(in Instance) string { - return in.App + "|" + stripEnvToken(in.Namespace) +func Identity(app, namespace string) string { + return app + "|" + StripEnvToken(namespace) } +func identity(in Instance) string { return Identity(in.App, in.Namespace) } + var envTokens = map[string]bool{ "qa": true, "stg": true, "stage": true, "staging": true, "prod": true, "production": true, "prd": true, "dev": true, "test": true, "uat": true, "preprod": true, "sandbox": true, } -func stripEnvToken(ns string) string { +// StripEnvToken removes the environment token from a namespace, so that +// team-a-qa and team-a-prod resolve to one identity. +func StripEnvToken(ns string) string { parts := strings.Split(ns, "-") kept := make([]string, 0, len(parts)) for _, p := range parts { @@ -291,10 +295,10 @@ var versionLike = regexp.MustCompile(`^v?\d+(\.\d+)*([-+._].*)?$`) var numeric = regexp.MustCompile(`\d+`) -// compareTags orders two version tags, returning zero when they cannot be +// CompareTags orders two version tags, returning zero when they cannot be // ordered. Claiming a direction we cannot establish would be worse than saying // they merely differ. -func compareTags(a, b string) int { +func CompareTags(a, b string) int { an, aok := versionParts(a) bn, bok := versionParts(b) if !aok || !bok { diff --git a/internal/promotion/promotion_test.go b/internal/promotion/promotion_test.go index 48b1580..8288406 100644 --- a/internal/promotion/promotion_test.go +++ b/internal/promotion/promotion_test.go @@ -235,3 +235,26 @@ func TestTagIsReadFromTheImage(t *testing.T) { t.Errorf("tag = %q, want none when the image is unknown", got) } } + +func TestIdentityMatchesAcrossEnvSuffixedNamespaces(t *testing.T) { + qa := Identity("checkout", "team-a-qa") + prod := Identity("checkout", "team-a-prod") + if qa != prod { + t.Errorf("Identity qa = %q, prod = %q; one team's namespace in two places is one app", qa, prod) + } +} + +func TestCompareTagsIsExported(t *testing.T) { + if CompareTags("v1.2.0", "v1.10.0") >= 0 { + t.Error("v1.2.0 should order below v1.10.0") + } + if CompareTags("sha-abc", "sha-def") != 0 { + t.Error("build ids cannot be ordered, and claiming a direction invents one") + } +} + +func TestStripEnvTokenIsExported(t *testing.T) { + if got := StripEnvToken("team-a-prod"); got != "team-a" { + t.Errorf("StripEnvToken = %q, want team-a", got) + } +} From 9a5394b16a522109d88369856e3c5ff4241bb995 Mon Sep 17 00:00:00 2001 From: Elber Ribeiro Date: Sun, 2 Aug 2026 22:17:18 -0400 Subject: [PATCH 04/38] feat(promotion): a multi-context environment stops collapsing a disagreement byEnv[in.Env] = in kept the last context and dropped the rest, so a prod environment holding three clusters rendered one confident version nobody verified. The cell now collapses only when every cluster agrees on tag and digest, and renders a split naming the disagreement otherwise. A split never becomes upstream for the next column, for the same reason an unreadable column never did. Closes #62 --- internal/promotion/promotion.go | 176 ++++++++++++++++++++++----- internal/promotion/promotion_test.go | 167 ++++++++++++++++++++++++- 2 files changed, 311 insertions(+), 32 deletions(-) diff --git a/internal/promotion/promotion.go b/internal/promotion/promotion.go index 83fc5ff..1b2e473 100644 --- a/internal/promotion/promotion.go +++ b/internal/promotion/promotion.go @@ -29,18 +29,30 @@ const ( StateDigestDiffers = "digest-differs" StateAbsent = "absent" StateDenied = "denied" + // StateSplit is an environment whose clusters do not agree. It never + // carries a version, because picking one would be the guess this state + // exists to prevent. + StateSplit = "split" ) // Env is one column. type Env struct { - Name string `json:"name"` - Risk string `json:"risk"` - Context string `json:"context,omitempty"` + Name string `json:"name"` + Risk string `json:"risk"` + // Context is the first context bound to this environment, kept so a cell + // can still be opened. Contexts is all of them. + Context string `json:"context,omitempty"` + Contexts []string `json:"contexts,omitempty"` + // Unreadable names the contexts that did not answer. A cell in an + // environment with any unreadable context can never claim agreement, + // because the clusters nobody read might disagree. + Unreadable []string `json:"unreadable,omitempty"` } // Instance is one app as it exists in one environment. type Instance struct { Env string + Context string App string Namespace string @@ -77,6 +89,9 @@ type Cell struct { // Severe marks the states that deserve the error hue rather than a warning: // ahead of upstream, and a tag that resolves to a different digest. Severe bool `json:"severe,omitempty"` + // Clusters is how many contexts back this cell. Zero and one both mean a + // single cluster; the UI shows the count only above one. + Clusters int `json:"clusters,omitempty"` } // Row is one app across every environment. @@ -93,6 +108,7 @@ type Summary struct { Apps int `json:"apps"` Drifted int `json:"drifted"` Ahead int `json:"ahead"` + Split int `json:"split"` } // Summarize counts the rows worth looking at. @@ -105,6 +121,12 @@ func Summarize(rows []Row) Summary { if r.Ahead { s.Ahead++ } + for _, c := range r.Cells { + if c.State == StateSplit { + s.Split++ + break + } + } } return s } @@ -141,50 +163,142 @@ func Build(envs []Env, instances []Instance) []Row { } func row(envs []Env, instances []Instance) Row { - byEnv := map[string]Instance{} + byEnv := map[string][]Instance{} for _, in := range instances { - byEnv[in.Env] = in + byEnv[in.Env] = append(byEnv[in.Env], in) } out := Row{App: instances[0].App, Namespace: instances[0].Namespace} // upstream is the last environment that actually told us something. An - // unreadable environment is not an agreement, so it does not become the - // thing the next column is compared against. + // unreadable environment is not an agreement, and neither is a split one, + // so neither becomes the thing the next column is compared against. var upstream *Instance for _, env := range envs { - in, ok := byEnv[env.Name] - if !ok { - out.Cells = append(out.Cells, Cell{Env: env.Name, State: StateAbsent, Note: "not deployed here"}) - continue + rep, cell, ok := resolve(env, byEnv[env.Name], upstream) + out.Cells = append(out.Cells, cell) + if cell.State != StateSame { + out.Drift++ } - if in.Denied { - out.Cells = append(out.Cells, Cell{ - Env: env.Name, Namespace: in.Namespace, State: StateDenied, - Note: orElse(in.DeniedReason, "not readable"), - }) - continue + if cell.State == StateAhead { + out.Ahead = true } - if !in.Present { - out.Cells = append(out.Cells, Cell{ - Env: env.Name, Namespace: in.Namespace, State: StateAbsent, Note: "not deployed here", - }) - continue + if ok { + copyOf := rep + upstream = ©Of } + } + return out +} - c := compare(in, upstream) - out.Cells = append(out.Cells, c) - if c.State != StateSame { - out.Drift++ +// resolve turns one environment's clusters into one cell. +// +// The third return says whether the cell may serve as upstream for the next +// column. Absent, denied, and split all say no: a column the matrix could not +// read, or whose clusters disagree, is not a version the next column can be +// compared against. +func resolve(env Env, group []Instance, upstream *Instance) (Instance, Cell, bool) { + clusters := len(env.Contexts) + if clusters == 0 { + clusters = 1 + } + + var present []Instance + denied := 0 + for _, in := range group { + switch { + case in.Denied: + denied++ + case in.Present: + present = append(present, in) } - if c.State == StateAhead { - out.Ahead = true + } + + if len(present) == 0 { + if denied > 0 { + reason := "not readable" + for _, in := range group { + if in.Denied && in.DeniedReason != "" { + reason = in.DeniedReason + break + } + } + return Instance{}, Cell{Env: env.Name, State: StateDenied, Note: reason, Clusters: clusters}, false } - copyOf := in - upstream = ©Of + return Instance{}, Cell{Env: env.Name, State: StateAbsent, Note: "not deployed here", Clusters: clusters}, false } - return out + + readable := clusters - len(env.Unreadable) + if rep, agreed := consensus(present); agreed && len(present) == readable && len(env.Unreadable) == 0 { + c := compare(rep, upstream) + c.Clusters = clusters + return rep, c, true + } + + return Instance{}, split(env, present, clusters, readable), false +} + +// consensus collapses clusters that agree. +// +// Tags must match exactly. Digests must match only when both arrived, because +// a digest still in flight is not a disagreement. The representative carries +// no digest unless every cluster reported the same one, so a pending fetch +// renders as pending instead of borrowing one cluster's answer for the rest. +func consensus(present []Instance) (Instance, bool) { + rep := present[0] + anyPending := rep.Digest == "" + for _, in := range present[1:] { + if in.Tag != rep.Tag { + return Instance{}, false + } + if in.Digest == "" { + anyPending = true + continue + } + if rep.Digest != "" && in.Digest != rep.Digest { + return Instance{}, false + } + if rep.Digest == "" { + rep.Digest = in.Digest + } + } + if anyPending { + rep.Digest = "" + } + rep.Context = "" + return rep, true +} + +// split is the cell for an environment whose clusters do not agree. It names +// the disagreement and never names a version. +func split(env Env, present []Instance, clusters, readable int) Cell { + c := Cell{Env: env.Name, State: StateSplit, Severe: true, Clusters: clusters} + if len(present) > 0 { + c.Namespace = present[0].Namespace + } + + tags := map[string]bool{} + digests := map[string]bool{} + for _, in := range present { + tags[in.Tag] = true + if in.Digest != "" { + digests[in.Digest] = true + } + } + + switch { + case len(env.Unreadable) > 0: + c.Note = fmt.Sprintf("%d of %d clusters readable; the rest did not answer", readable, clusters) + case len(present) < readable: + c.Note = fmt.Sprintf("deployed in %d of %d clusters", len(present), clusters) + case len(tags) == 1 && len(digests) > 1: + c.Tag = "" + c.Note = fmt.Sprintf("one tag across %d clusters but %d digests: the tag is mutable and these are not the same code", clusters, len(digests)) + default: + c.Note = fmt.Sprintf("%d versions across %d clusters", len(tags), clusters) + } + return c } func compare(in Instance, upstream *Instance) Cell { diff --git a/internal/promotion/promotion_test.go b/internal/promotion/promotion_test.go index 8288406..4a2cc0f 100644 --- a/internal/promotion/promotion_test.go +++ b/internal/promotion/promotion_test.go @@ -1,6 +1,9 @@ package promotion -import "testing" +import ( + "strings" + "testing" +) func envs() []Env { return []Env{{Name: "qa", Risk: "low"}, {Name: "stg", Risk: "medium"}, {Name: "prod", Risk: "high"}} @@ -258,3 +261,165 @@ func TestStripEnvTokenIsExported(t *testing.T) { t.Errorf("StripEnvToken = %q, want team-a", got) } } + +func multiEnv() []Env { + return []Env{ + {Name: "qa", Contexts: []string{"qa-cluster"}}, + {Name: "prod", Contexts: []string{"prod-us-east", "prod-eu-west", "prod-ap-south"}}, + } +} + +func prodInstance(ctx, tag, digest string) Instance { + return Instance{ + Env: "prod", Context: ctx, App: "checkout", Namespace: "shop", + Present: true, Image: "reg/checkout:" + tag, Tag: tag, Digest: digest, + Health: "healthy", Ready: "3/3", + } +} + +func cellFor(t *testing.T, rows []Row, env string) Cell { + t.Helper() + for _, c := range rows[0].Cells { + if c.Env == env { + return c + } + } + t.Fatalf("no cell for env %q", env) + return Cell{} +} + +func TestAgreeingContextsCollapseToOneVersion(t *testing.T) { + rows := Build(multiEnv(), []Instance{ + {Env: "qa", Context: "qa-cluster", App: "checkout", Namespace: "shop", Present: true, Tag: "v2.14.0", Digest: "sha256:aa"}, + prodInstance("prod-us-east", "v2.13.1", "sha256:bb"), + prodInstance("prod-eu-west", "v2.13.1", "sha256:bb"), + prodInstance("prod-ap-south", "v2.13.1", "sha256:bb"), + }) + c := cellFor(t, rows, "prod") + if c.State != StateBehind { + t.Errorf("state = %q, want %q: three clusters agreeing is one version", c.State, StateBehind) + } + if c.Tag != "v2.13.1" { + t.Errorf("tag = %q, want v2.13.1", c.Tag) + } +} + +func TestDisagreeingContextsNeverCollapse(t *testing.T) { + rows := Build(multiEnv(), []Instance{ + prodInstance("prod-us-east", "v2.13.1", "sha256:bb"), + prodInstance("prod-eu-west", "v2.12.0", "sha256:cc"), + prodInstance("prod-ap-south", "v2.13.1", "sha256:bb"), + }) + c := cellFor(t, rows, "prod") + if c.State != StateSplit { + t.Fatalf("state = %q, want %q", c.State, StateSplit) + } + if c.Tag != "" { + t.Errorf("tag = %q; a split cell must not pick a winner", c.Tag) + } + if !c.Severe { + t.Error("a split prod is severe") + } + if c.Clusters != 3 { + t.Errorf("clusters = %d, want 3", c.Clusters) + } + if !strings.Contains(c.Note, "2 versions") { + t.Errorf("note = %q, want it to name the version count", c.Note) + } +} + +func TestSameTagDifferentDigestSplitsLoudly(t *testing.T) { + rows := Build(multiEnv(), []Instance{ + prodInstance("prod-us-east", "v2.13.1", "sha256:bb"), + prodInstance("prod-eu-west", "v2.13.1", "sha256:cc"), + prodInstance("prod-ap-south", "v2.13.1", "sha256:bb"), + }) + c := cellFor(t, rows, "prod") + if c.State != StateSplit || !c.Severe { + t.Fatalf("state = %q severe = %v, want a severe split", c.State, c.Severe) + } + if !strings.Contains(c.Note, "digest") { + t.Errorf("note = %q, want it to name the mutable tag", c.Note) + } +} + +func TestPendingDigestDoesNotSplitAnAgreement(t *testing.T) { + rows := Build(multiEnv(), []Instance{ + prodInstance("prod-us-east", "v2.13.1", "sha256:bb"), + prodInstance("prod-eu-west", "v2.13.1", ""), + prodInstance("prod-ap-south", "v2.13.1", "sha256:bb"), + }) + c := cellFor(t, rows, "prod") + if c.State == StateSplit { + t.Fatal("a digest still in flight is not a disagreement") + } + if !c.DigestPending { + t.Error("a missing digest must read as pending, never as a match") + } + if c.Digest != "" { + t.Errorf("digest = %q; claiming one cluster's digest for the pair would be a guess", c.Digest) + } +} + +func TestPartialDeploymentSplits(t *testing.T) { + rows := Build(multiEnv(), []Instance{ + prodInstance("prod-us-east", "v2.13.1", "sha256:bb"), + prodInstance("prod-eu-west", "v2.13.1", "sha256:bb"), + }) + c := cellFor(t, rows, "prod") + if c.State != StateSplit { + t.Fatalf("state = %q; deployed in two of three clusters is not deployed", c.State) + } + if !strings.Contains(c.Note, "2 of 3") { + t.Errorf("note = %q, want it to name the coverage", c.Note) + } +} + +func TestUnreadableContextBlocksCollapse(t *testing.T) { + envs := multiEnv() + envs[1].Unreadable = []string{"prod-ap-south"} + rows := Build(envs, []Instance{ + prodInstance("prod-us-east", "v2.13.1", "sha256:bb"), + prodInstance("prod-eu-west", "v2.13.1", "sha256:bb"), + }) + c := cellFor(t, rows, "prod") + if c.State != StateSplit { + t.Fatalf("state = %q; a cluster nobody could read is not an agreement", c.State) + } + if !strings.Contains(c.Note, "readable") { + t.Errorf("note = %q, want it to say how many clusters answered", c.Note) + } +} + +func TestSplitCellIsNotUpstreamForTheNextColumn(t *testing.T) { + envs := []Env{ + {Name: "qa", Contexts: []string{"qa-cluster"}}, + {Name: "stg", Contexts: []string{"stg-a", "stg-b"}}, + {Name: "prod", Contexts: []string{"prod-only"}}, + } + rows := Build(envs, []Instance{ + {Env: "qa", Context: "qa-cluster", App: "checkout", Namespace: "shop", Present: true, Tag: "v2.0.0", Digest: "sha256:aa"}, + {Env: "stg", Context: "stg-a", App: "checkout", Namespace: "shop", Present: true, Tag: "v3.0.0", Digest: "sha256:bb"}, + {Env: "stg", Context: "stg-b", App: "checkout", Namespace: "shop", Present: true, Tag: "v1.0.0", Digest: "sha256:cc"}, + {Env: "prod", Context: "prod-only", App: "checkout", Namespace: "shop", Present: true, Tag: "v2.0.0", Digest: "sha256:aa"}, + }) + c := cellFor(t, rows, "prod") + if c.State != StateSame { + t.Errorf("prod state = %q, want %q: prod matches qa, and the split stg never became the basis", c.State, StateSame) + } +} + +func TestSingleContextEnvironmentIsUnchanged(t *testing.T) { + envs := []Env{ + {Name: "qa", Contexts: []string{"qa-cluster"}}, + {Name: "prod", Contexts: []string{"prod-cluster"}}, + } + rows := Build(envs, []Instance{ + {Env: "qa", Context: "qa-cluster", App: "checkout", Namespace: "shop", Present: true, Tag: "v2.0.0", Digest: "sha256:aa"}, + {Env: "prod", Context: "prod-cluster", App: "checkout", Namespace: "shop", Present: true, Tag: "v1.0.0", Digest: "sha256:bb"}, + }) + c := cellFor(t, rows, "prod") + if c.State != StateBehind { + t.Errorf("state = %q, want %q: one context per environment behaves exactly as before", c.State, StateBehind) + } +} From 3dcade7ee39049dd6cb51754c6eff092b3e1083b Mon Sep 17 00:00:00 2001 From: Elber Ribeiro Date: Sun, 2 Aug 2026 23:32:10 -0400 Subject: [PATCH 05/38] fix(promotion): absent/denied stop counting as drift, split priority and coverage math corrected Four review findings on 9a5394b: - row() counted absent and denied cells as drift, since resolve() routes them through the same "State != StateSame" check the old code used to continue past. Only cells actually evaluated should move drift; a split cell is a real disagreement and still counts. - resolve()'s collapse guard required len(present) == readable, which a namespace collapse (e.g. "shop" and "shop-prod" stripping to one identity in one environment) can never satisfy once Contexts is empty and clusters defaults to 1. That rendered a severe split citing a bogus cluster count. Guard on len(present) > clusters instead, and decide those cells on consensus alone without citing the count. - split()'s priority checked partial coverage before a same-tag/different- digest mismatch, hiding the worse fact (a mutable tag) behind the lesser one (a cluster not caught up yet). Digest mismatch now outranks coverage. - Cell.Namespace was dropped from absent/denied cells; restored where the instance is known. Removed the now-dead orElse helper. --- internal/promotion/promotion.go | 66 +++++++++--- internal/promotion/promotion_test.go | 149 +++++++++++++++++++++++++++ 2 files changed, 203 insertions(+), 12 deletions(-) diff --git a/internal/promotion/promotion.go b/internal/promotion/promotion.go index 1b2e473..97f2892 100644 --- a/internal/promotion/promotion.go +++ b/internal/promotion/promotion.go @@ -178,7 +178,11 @@ func row(envs []Env, instances []Instance) Row { for _, env := range envs { rep, cell, ok := resolve(env, byEnv[env.Name], upstream) out.Cells = append(out.Cells, cell) - if cell.State != StateSame { + // Absent and denied are not comparisons: there is nothing to compare + // against, or nothing we were allowed to read. Only a cell that was + // actually evaluated moves the drift count. A split cell was + // evaluated, and disagreed, so it counts. + if cell.State != StateSame && cell.State != StateAbsent && cell.State != StateDenied { out.Drift++ } if cell.State == StateAhead { @@ -215,6 +219,14 @@ func resolve(env Env, group []Instance, upstream *Instance) (Instance, Cell, boo } } + // Namespace, when the instance is known: an absent or denied cell still + // names where it would have lived, because the app identity that got us + // here came from a real instance somewhere in this group. + ns := "" + if len(group) > 0 { + ns = group[0].Namespace + } + if len(present) == 0 { if denied > 0 { reason := "not readable" @@ -224,9 +236,22 @@ func resolve(env Env, group []Instance, upstream *Instance) (Instance, Cell, boo break } } - return Instance{}, Cell{Env: env.Name, State: StateDenied, Note: reason, Clusters: clusters}, false + return Instance{}, Cell{Env: env.Name, Namespace: ns, State: StateDenied, Note: reason, Clusters: clusters}, false + } + return Instance{}, Cell{Env: env.Name, Namespace: ns, State: StateAbsent, Note: "not deployed here", Clusters: clusters}, false + } + + // More present instances than the environment declares clusters means the + // declared count cannot be trusted here: this is typically two namespaces + // (e.g. "shop" and "shop-prod") collapsing to one app identity inside a + // single environment, not genuinely distinct clusters. A coverage + // fraction built from that count would be a number known to be wrong, so + // decide on consensus alone and never cite the cluster count. + if len(present) > clusters { + if rep, agreed := consensus(present); agreed { + return rep, compare(rep, upstream), true } - return Instance{}, Cell{Env: env.Name, State: StateAbsent, Note: "not deployed here", Clusters: clusters}, false + return Instance{}, splitOnConsensusOnly(env, present), false } readable := clusters - len(env.Unreadable) @@ -289,18 +314,42 @@ func split(env Env, present []Instance, clusters, readable int) Cell { switch { case len(env.Unreadable) > 0: + // A cluster nobody read might disagree, so unreadable always wins: + // neither agreement nor a defect can be claimed from clusters we + // never saw. c.Note = fmt.Sprintf("%d of %d clusters readable; the rest did not answer", readable, clusters) - case len(present) < readable: - c.Note = fmt.Sprintf("deployed in %d of %d clusters", len(present), clusters) case len(tags) == 1 && len(digests) > 1: + // A mutable tag outranks partial coverage. One tag resolving to two + // digests is a defect; a cluster not having the app yet is a + // schedule, and the defect is the worse fact to hide. c.Tag = "" c.Note = fmt.Sprintf("one tag across %d clusters but %d digests: the tag is mutable and these are not the same code", clusters, len(digests)) + case len(present) < readable: + c.Note = fmt.Sprintf("deployed in %d of %d clusters", len(present), clusters) default: c.Note = fmt.Sprintf("%d versions across %d clusters", len(tags), clusters) } return c } +// splitOnConsensusOnly is the cell for an environment where more instances +// are present than the environment declares clusters for (see resolve). The +// declared cluster count is not meaningful in that case, so this never prints +// a coverage fraction or a cluster count. +func splitOnConsensusOnly(env Env, present []Instance) Cell { + c := Cell{Env: env.Name, State: StateSplit, Severe: true} + if len(present) > 0 { + c.Namespace = present[0].Namespace + } + + tags := map[string]bool{} + for _, in := range present { + tags[in.Tag] = true + } + c.Note = fmt.Sprintf("%d versions present in %s", len(tags), env.Name) + return c +} + func compare(in Instance, upstream *Instance) Cell { c := Cell{ Env: in.Env, Namespace: in.Namespace, State: StateSame, @@ -475,10 +524,3 @@ func hasPrerelease(tag string) bool { } return false } - -func orElse(v, fallback string) string { - if v == "" { - return fallback - } - return v -} diff --git a/internal/promotion/promotion_test.go b/internal/promotion/promotion_test.go index 4a2cc0f..17d9f6d 100644 --- a/internal/promotion/promotion_test.go +++ b/internal/promotion/promotion_test.go @@ -423,3 +423,152 @@ func TestSingleContextEnvironmentIsUnchanged(t *testing.T) { t.Errorf("state = %q, want %q: one context per environment behaves exactly as before", c.State, StateBehind) } } + +// --- Regression tests added while fixing review findings on task #62 --- + +// Finding 1: absent and denied cells are not comparisons, so they must not +// move the drift count. The old (pre-multi-cluster) code `continue`d past the +// drift increment for both; the multi-cluster rewrite briefly regressed this. +func TestAbsentAndDeniedDoNotCountAsDrift(t *testing.T) { + rows := Build(envs(), []Instance{ + running("qa", "notifications", "v0.9.1"), + {Env: "stg", App: "notifications", Namespace: "team-a", Denied: true, DeniedReason: "needs get deployments"}, + }) + r := at(t, rows, "notifications") + if cell(r, "stg").State != StateDenied { + t.Fatalf("stg cell = %+v, want denied", cell(r, "stg")) + } + if cell(r, "prod").State != StateAbsent { + t.Fatalf("prod cell = %+v, want absent", cell(r, "prod")) + } + if r.Drift != 0 { + t.Fatalf("drift = %d, want 0: a denied cell and an absent cell are not disagreements to count", r.Drift) + } +} + +// A split cell is a genuine disagreement, unlike absent or denied, and must +// still count toward drift. +func TestSplitCellCountsAsDrift(t *testing.T) { + rows := Build(multiEnv(), []Instance{ + prodInstance("prod-us-east", "v2.13.1", "sha256:bb"), + prodInstance("prod-eu-west", "v2.12.0", "sha256:cc"), + prodInstance("prod-ap-south", "v2.13.1", "sha256:bb"), + }) + r := at(t, rows, "checkout") + if r.Drift != 1 { + t.Fatalf("drift = %d, want 1: a split prod cell is a real disagreement, and qa is absent (not counted)", r.Drift) + } +} + +// Finding 2: Identity() strips the environment token from the namespace, so +// "shop" and "shop-prod" in the SAME cluster/environment collapse to one app +// identity with two instances in one env. Env.Contexts is empty here, so +// clusters defaults to 1 and len(present) (2) exceeds it. The collapse guard +// must not treat that as "1 of 1 clusters"; it must decide on consensus +// alone. +func TestMoreInstancesThanClustersCollapsesOnAgreement(t *testing.T) { + rows := Build([]Env{{Name: "prod"}}, []Instance{ + {Env: "prod", App: "checkout", Namespace: "shop", Present: true, Tag: "v1.0.0", Digest: "sha256:aa"}, + {Env: "prod", App: "checkout", Namespace: "shop-prod", Present: true, Tag: "v1.0.0", Digest: "sha256:aa"}, + }) + c := cellFor(t, rows, "prod") + if c.State == StateSplit { + t.Fatalf("state = %q; two agreeing instances must not split just because there are more of them than the environment declares clusters", c.State) + } + if c.Severe { + t.Error("agreeing instances should not render severe") + } +} + +func TestMoreInstancesThanClustersSplitsOnDisagreementWithoutABogusCount(t *testing.T) { + rows := Build([]Env{{Name: "prod"}}, []Instance{ + {Env: "prod", App: "checkout", Namespace: "shop", Present: true, Tag: "v1.0.0", Digest: "sha256:aa"}, + {Env: "prod", App: "checkout", Namespace: "shop-prod", Present: true, Tag: "v2.0.0", Digest: "sha256:bb"}, + }) + c := cellFor(t, rows, "prod") + if c.State != StateSplit || !c.Severe { + t.Fatalf("state = %q severe = %v, want a severe split: two instances in one environment disagree", c.State, c.Severe) + } + if strings.Contains(c.Note, "1 cluster") || strings.Contains(c.Note, "of 1") { + t.Errorf("note = %q; the environment declares one cluster but two instances exist here, so citing that count would be nonsense on a red cell", c.Note) + } + if c.Clusters != 0 { + t.Errorf("clusters = %d, want 0: the declared cluster count cannot be trusted once instances outnumber it", c.Clusters) + } +} + +// Finding 3: a mutable tag (same tag, different digest) is a defect. Partial +// coverage is a schedule. When both are true at once, the defect must win the +// note, not the coverage fraction. +func TestDigestMismatchOutranksPartialCoverage(t *testing.T) { + rows := Build(multiEnv(), []Instance{ + prodInstance("prod-us-east", "v2.13.1", "sha256:bb"), + prodInstance("prod-eu-west", "v2.13.1", "sha256:cc"), + }) + c := cellFor(t, rows, "prod") + if c.State != StateSplit || !c.Severe { + t.Fatalf("state = %q severe = %v, want a severe split", c.State, c.Severe) + } + if !strings.Contains(c.Note, "digest") { + t.Errorf("note = %q, want the digest mismatch named even though coverage is also partial", c.Note) + } + if strings.Contains(c.Note, "of 3") { + t.Errorf("note = %q; a mutable tag is a defect and should outrank a coverage fraction, not share the note with it", c.Note) + } +} + +// Finding 4: Summary.Split is a named deliverable with no assertions on it. +// Summarize breaks on the first split cell in a row, so a row with two split +// cells must still count once. +func TestSummaryCountsARowWithTwoSplitCellsOnce(t *testing.T) { + twoSplitEnvs := []Env{ + {Name: "stg", Contexts: []string{"stg-a", "stg-b"}}, + {Name: "prod", Contexts: []string{"prod-a", "prod-b"}}, + } + rows := Build(twoSplitEnvs, []Instance{ + {Env: "stg", Context: "stg-a", App: "checkout", Namespace: "shop", Present: true, Tag: "v1.0.0", Digest: "sha256:aa"}, + {Env: "stg", Context: "stg-b", App: "checkout", Namespace: "shop", Present: true, Tag: "v2.0.0", Digest: "sha256:bb"}, + {Env: "prod", Context: "prod-a", App: "checkout", Namespace: "shop", Present: true, Tag: "v1.0.0", Digest: "sha256:aa"}, + {Env: "prod", Context: "prod-b", App: "checkout", Namespace: "shop", Present: true, Tag: "v3.0.0", Digest: "sha256:cc"}, + }) + s := Summarize(rows) + if s.Split != 1 { + t.Fatalf("split = %d, want 1: one row with two split cells still counts once", s.Split) + } +} + +func TestSummaryCountsOneSplitRow(t *testing.T) { + rows := Build(multiEnv(), []Instance{ + prodInstance("prod-us-east", "v2.13.1", "sha256:bb"), + prodInstance("prod-eu-west", "v2.12.0", "sha256:cc"), + prodInstance("prod-ap-south", "v2.13.1", "sha256:bb"), + }) + s := Summarize(rows) + if s.Split != 1 { + t.Fatalf("split = %d, want 1", s.Split) + } +} + +// Finding 5: the old code set Namespace on both absent and denied cells when +// the instance was known. resolve() dropped it. +func TestDeniedCellKeepsNamespace(t *testing.T) { + rows := Build(envs(), []Instance{ + running("qa", "notifications", "v0.9.1"), + {Env: "prod", App: "notifications", Namespace: "team-a", Denied: true, DeniedReason: "needs get deployments"}, + }) + c := cell(at(t, rows, "notifications"), "prod") + if c.Namespace != "team-a" { + t.Errorf("namespace = %q, want team-a: the instance is known, even though it is denied", c.Namespace) + } +} + +func TestAbsentCellKeepsNamespaceWhenTheInstanceIsKnown(t *testing.T) { + rows := Build(envs(), []Instance{ + running("qa", "notifications", "v0.9.1"), + {Env: "prod", App: "notifications", Namespace: "team-a", Present: false}, + }) + c := cell(at(t, rows, "notifications"), "prod") + if c.Namespace != "team-a" { + t.Errorf("namespace = %q, want team-a: the instance is known even though it is not present", c.Namespace) + } +} From 1a5fbfa4898a0a44fe2aa520d0f3eb95eb259f9b Mon Sep 17 00:00:00 2001 From: Elber Ribeiro Date: Sun, 2 Aug 2026 23:32:58 -0400 Subject: [PATCH 06/38] docs(fleet): correct the plan where review proved it wrong Three defects the plan taught. Two test cases supplied 2 of prod's 3 contexts, so the partial-coverage branch preempted the behavior each test named. row() counted absent and denied cells as drift, which broke the shipped single-context matrix the task was told to protect. And split() reported partial coverage ahead of a mutable tag, hiding the worse fact. Also ignores .superpowers/, the per-plan execution scratch. --- .gitignore | 1 + docs/superpowers/plans/2026-08-02-fleet.md | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index e08f55a..f369e5d 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ web/playwright-report/ .DS_Store .env .env.local +.superpowers/ web/tsconfig.tsbuildinfo HANDOFF.md diff --git a/docs/superpowers/plans/2026-08-02-fleet.md b/docs/superpowers/plans/2026-08-02-fleet.md index 592505c..a7908b6 100644 --- a/docs/superpowers/plans/2026-08-02-fleet.md +++ b/docs/superpowers/plans/2026-08-02-fleet.md @@ -228,6 +228,7 @@ func TestSameTagDifferentDigestSplitsLoudly(t *testing.T) { rows := Build(multiEnv(), []Instance{ prodInstance("prod-us-east", "v2.13.1", "sha256:bb"), prodInstance("prod-eu-west", "v2.13.1", "sha256:cc"), + prodInstance("prod-ap-south", "v2.13.1", "sha256:bb"), }) c := cellFor(t, rows, "prod") if c.State != StateSplit || !c.Severe { @@ -242,6 +243,7 @@ func TestPendingDigestDoesNotSplitAnAgreement(t *testing.T) { rows := Build(multiEnv(), []Instance{ prodInstance("prod-us-east", "v2.13.1", "sha256:bb"), prodInstance("prod-eu-west", "v2.13.1", ""), + prodInstance("prod-ap-south", "v2.13.1", "sha256:bb"), }) c := cellFor(t, rows, "prod") if c.State == StateSplit { @@ -425,7 +427,11 @@ func row(envs []Env, instances []Instance) Row { for _, env := range envs { rep, cell, ok := resolve(env, byEnv[env.Name], upstream) out.Cells = append(out.Cells, cell) - if cell.State != StateSame { + // Absent and denied are not comparisons: there is nothing to compare + // against, or nothing we were allowed to read. Only a cell that was + // actually evaluated moves the drift count. A split cell was + // evaluated, and disagreed, so it counts. + if cell.State != StateSame && cell.State != StateAbsent && cell.State != StateDenied { out.Drift++ } if cell.State == StateAhead { @@ -537,11 +543,14 @@ func split(env Env, present []Instance, clusters, readable int) Cell { switch { case len(env.Unreadable) > 0: c.Note = fmt.Sprintf("%d of %d clusters readable; the rest did not answer", readable, clusters) - case len(present) < readable: - c.Note = fmt.Sprintf("deployed in %d of %d clusters", len(present), clusters) case len(tags) == 1 && len(digests) > 1: + // A mutable tag outranks partial coverage. One tag resolving to two + // digests is a defect; a cluster not having the app yet is a + // schedule, and the defect is the worse fact to hide. c.Tag = "" c.Note = fmt.Sprintf("one tag across %d clusters but %d digests: the tag is mutable and these are not the same code", clusters, len(digests)) + case len(present) < readable: + c.Note = fmt.Sprintf("deployed in %d of %d clusters", len(present), clusters) default: c.Note = fmt.Sprintf("%d versions across %d clusters", len(tags), clusters) } From 1b9535dcf790aaed150a7ab619bd432f9ec6619a Mon Sep 17 00:00:00 2001 From: Elber Ribeiro Date: Sun, 2 Aug 2026 23:40:47 -0400 Subject: [PATCH 07/38] fix(promotion): unread cluster never collapses regardless of instance count An unreadable context could still be collapsed into agreement when the environment also had more present instances than declared clusters, because that branch returned before the Unreadable guard ran. The unreadable rule wins in that case too: clusters nobody read might disagree. Also fixes two smaller defects in the same code: the digest-mismatch note cited the declared cluster count instead of the count that actually reported the tag, and the group namespace still dropped when the first instance in the group had an empty one. --- internal/promotion/promotion.go | 28 ++++++++++---- internal/promotion/promotion_test.go | 58 ++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 7 deletions(-) diff --git a/internal/promotion/promotion.go b/internal/promotion/promotion.go index 97f2892..71edc26 100644 --- a/internal/promotion/promotion.go +++ b/internal/promotion/promotion.go @@ -221,10 +221,15 @@ func resolve(env Env, group []Instance, upstream *Instance) (Instance, Cell, boo // Namespace, when the instance is known: an absent or denied cell still // names where it would have lived, because the app identity that got us - // here came from a real instance somewhere in this group. + // here came from a real instance somewhere in this group. Take the first + // non-empty one, since a denied instance can carry an unknown (empty) + // namespace even when a later instance in the same group knows it. ns := "" - if len(group) > 0 { - ns = group[0].Namespace + for _, in := range group { + if in.Namespace != "" { + ns = in.Namespace + break + } } if len(present) == 0 { @@ -247,9 +252,15 @@ func resolve(env Env, group []Instance, upstream *Instance) (Instance, Cell, boo // single environment, not genuinely distinct clusters. A coverage // fraction built from that count would be a number known to be wrong, so // decide on consensus alone and never cite the cluster count. + // + // But an unreadable context still wins over all of that: the clusters + // nobody read might disagree, so this can never collapse to agreement + // just because the ones that did answer agree with each other. if len(present) > clusters { - if rep, agreed := consensus(present); agreed { - return rep, compare(rep, upstream), true + if len(env.Unreadable) == 0 { + if rep, agreed := consensus(present); agreed { + return rep, compare(rep, upstream), true + } } return Instance{}, splitOnConsensusOnly(env, present), false } @@ -321,9 +332,12 @@ func split(env Env, present []Instance, clusters, readable int) Cell { case len(tags) == 1 && len(digests) > 1: // A mutable tag outranks partial coverage. One tag resolving to two // digests is a defect; a cluster not having the app yet is a - // schedule, and the defect is the worse fact to hide. + // schedule, and the defect is the worse fact to hide. Cite the + // clusters that actually reported the tag, not the declared total — + // this branch is reachable under partial coverage too, where fewer + // clusters answered than the environment declares. c.Tag = "" - c.Note = fmt.Sprintf("one tag across %d clusters but %d digests: the tag is mutable and these are not the same code", clusters, len(digests)) + c.Note = fmt.Sprintf("one tag across %d clusters but %d digests: the tag is mutable and these are not the same code", len(present), len(digests)) case len(present) < readable: c.Note = fmt.Sprintf("deployed in %d of %d clusters", len(present), clusters) default: diff --git a/internal/promotion/promotion_test.go b/internal/promotion/promotion_test.go index 17d9f6d..02c67c3 100644 --- a/internal/promotion/promotion_test.go +++ b/internal/promotion/promotion_test.go @@ -572,3 +572,61 @@ func TestAbsentCellKeepsNamespaceWhenTheInstanceIsKnown(t *testing.T) { t.Errorf("namespace = %q, want team-a: the instance is known even though it is not present", c.Namespace) } } + +// --- Regression tests added while fixing round-2 review findings --- + +// Finding A: round 1's "more present instances than declared clusters" +// branch checked len(present) > clusters and decided on consensus alone, +// but returned before env.Unreadable was consulted. An environment with an +// unreadable context must never collapse to agreement, no matter how many +// instances are present, because the clusters nobody read might disagree. +// It must also never become upstream for the next column. +func TestUnreadableBlocksCollapseEvenWhenInstancesOutnumberClusters(t *testing.T) { + env := Env{Name: "prod", Contexts: []string{"prod-a"}, Unreadable: []string{"prod-a"}} + group := []Instance{ + {Env: "prod", App: "checkout", Namespace: "shop", Present: true, Tag: "v1.0.0", Digest: "sha256:aa"}, + {Env: "prod", App: "checkout", Namespace: "shop", Present: true, Tag: "v1.0.0", Digest: "sha256:aa"}, + } + _, c, ok := resolve(env, group, nil) + if ok { + t.Fatal("ok = true; an environment with an unreadable context must never become upstream, even when the present instances agree and outnumber the declared clusters") + } + if c.State != StateSplit { + t.Fatalf("state = %q, want split: unreadable must block collapse even when instances outnumber declared clusters", c.State) + } +} + +// Finding B: the digest-mismatch note in split() cited the declared cluster +// count, not the number of clusters that actually reported the tag. Round 1 +// made this branch reachable under partial coverage, where those two numbers +// differ, and the note ends up describing a set of clusters that isn't the +// one it's talking about. +func TestDigestMismatchNoteCitesReportedClustersNotDeclared(t *testing.T) { + env := Env{Name: "prod", Contexts: []string{"prod-a", "prod-b", "prod-c"}} + group := []Instance{ + {Env: "prod", Context: "prod-a", App: "checkout", Namespace: "shop", Present: true, Tag: "v1.0.0", Digest: "sha256:aa"}, + {Env: "prod", Context: "prod-b", App: "checkout", Namespace: "shop", Present: true, Tag: "v1.0.0", Digest: "sha256:bb"}, + } + _, c, _ := resolve(env, group, nil) + if !strings.Contains(c.Note, "across 2 clusters") { + t.Errorf("note = %q, want it to cite the 2 clusters that actually reported the tag", c.Note) + } + if strings.Contains(c.Note, "across 3 clusters") { + t.Errorf("note = %q; 3 is the declared total, not the count that reported the tag", c.Note) + } +} + +// Finding C: resolve() took group[0].Namespace unconditionally, so a denied +// instance with an unknown (empty) namespace ahead of a later instance that +// does know its namespace still dropped the namespace from the cell. +func TestNamespaceIsFirstNonEmptyInTheGroup(t *testing.T) { + env := Env{Name: "prod"} + group := []Instance{ + {Env: "prod", App: "notifications", Namespace: "", Denied: true, DeniedReason: "needs get deployments"}, + {Env: "prod", App: "notifications", Namespace: "team-a", Denied: true, DeniedReason: "still denied"}, + } + _, c, _ := resolve(env, group, nil) + if c.Namespace != "team-a" { + t.Errorf("namespace = %q, want team-a: the first non-empty namespace in the group, not always group[0]", c.Namespace) + } +} From ad2fc6f1ea72384fdf5c8453aca8f27a1dc442fc Mon Sep 17 00:00:00 2001 From: Elber Ribeiro Date: Sun, 2 Aug 2026 23:49:12 -0400 Subject: [PATCH 08/38] fix(promotion): read every context in an environment, not the first service.go skipped a second context bound to the same environment before it was ever connected, on the assumption two contexts in one environment are identical. A team running prod in three regions disproves that, and the matrix rendered one region's version as prod's. Every context is now read, and the ones that do not answer are named on the environment. Closes #63 --- internal/api/service.go | 68 ++++++++++++--- internal/api/service_test.go | 161 +++++++++++++++++++++++++++++++++++ 2 files changed, 216 insertions(+), 13 deletions(-) diff --git a/internal/api/service.go b/internal/api/service.go index 0234bbc..b89193d 100644 --- a/internal/api/service.go +++ b/internal/api/service.go @@ -989,25 +989,36 @@ func (s *Service) Close() { s.forwards.StopAll() } // "is the fix in prod yet" without reading prod. An environment that will not // connect becomes a column of unreadable cells naming the reason, never an // empty column that reads as "nothing is deployed there". +// +// Every context bound to an environment is read, not the first. A team running +// prod in three regions has three answers, and reporting one of them as the +// environment's version is the defect this view exists to prevent. func (s *Service) Promotion() PromotionView { ctx, cancel := context.WithTimeout(context.Background(), s.timeout) defer cancel() - var envs []promotion.Env + type accum struct { + risk string + contexts []string + unreadable []string + } + byEnv := map[string]*accum{} + var order []string + var instances []promotion.Instance var unreachable []string - seen := map[string]bool{} for _, name := range s.mgr.ConnectOrder() { kctx := s.cfg.MustGet(name) env := s.conf.Environment(kctx) - // Two contexts bound to one environment would produce two identical - // columns; the first one wins, which is what the binding is for. - if seen[env.Name] { - continue + + acc, ok := byEnv[env.Name] + if !ok { + acc = &accum{risk: env.Risk.String()} + byEnv[env.Name] = acc + order = append(order, env.Name) } - seen[env.Name] = true - envs = append(envs, promotion.Env{Name: env.Name, Risk: env.Risk.String(), Context: name}) + acc.contexts = append(acc.contexts, name) reason := "" if err := s.mgr.Connect(ctx, name); err != nil { @@ -1015,9 +1026,10 @@ func (s *Service) Promotion() PromotionView { } client, ok := s.mgr.ClientFor(name) if !ok { + acc.unreadable = append(acc.unreadable, name) unreachable = append(unreachable, env.Name) instances = append(instances, promotion.Instance{ - Env: env.Name, App: "", Denied: true, + Env: env.Name, Context: name, App: "", Denied: true, DeniedReason: orElseString(reason, "not connected"), }) continue @@ -1025,16 +1037,28 @@ func (s *Service) Promotion() PromotionView { snap, err := clusters.Fetch(ctx, client, kctx, clusters.FetchOptions{Tier: s.mgr.Tier(name)}) if err != nil { + acc.unreadable = append(acc.unreadable, name) unreachable = append(unreachable, env.Name) continue } for _, a := range snap.Apps { - instances = append(instances, instanceOf(env.Name, a)) + in := instanceOf(env.Name, a) + in.Context = name + instances = append(instances, in) } } - // Instances with no app name are placeholders for an environment nobody - // could read; they gave us the column and nothing else. + envs := make([]promotion.Env, 0, len(order)) + for _, name := range order { + acc := byEnv[name] + envs = append(envs, promotion.Env{ + Name: name, Risk: acc.risk, + Context: acc.contexts[0], Contexts: acc.contexts, Unreadable: acc.unreadable, + }) + } + + // Instances with no app name are placeholders for a cluster nobody could + // read; they marked the environment unreadable and nothing else. real := instances[:0] for _, in := range instances { if in.App != "" { @@ -1043,7 +1067,25 @@ func (s *Service) Promotion() PromotionView { } rows := promotion.Build(envs, real) - return PromotionView{Envs: envs, Rows: rows, Summary: promotion.Summarize(rows), Unreachable: unreachable} + return PromotionView{Envs: envs, Rows: rows, Summary: promotion.Summarize(rows), Unreachable: dedupeStrings(unreachable)} +} + +// dedupeStrings keeps the first occurrence of each value. An environment with +// three unreachable clusters is one unreachable environment in the banner. +func dedupeStrings(in []string) []string { + if len(in) == 0 { + return nil + } + seen := map[string]bool{} + out := make([]string, 0, len(in)) + for _, v := range in { + if seen[v] { + continue + } + seen[v] = true + out = append(out, v) + } + return out } // instanceOf reads one app's version from the snapshot. diff --git a/internal/api/service_test.go b/internal/api/service_test.go index 749c483..8ba8122 100644 --- a/internal/api/service_test.go +++ b/internal/api/service_test.go @@ -1,8 +1,10 @@ package api import ( + "fmt" "os" "path/filepath" + "sort" "strings" "testing" "time" @@ -10,10 +12,169 @@ import ( "github.com/dynaum/kubeside/internal/clusters" "github.com/dynaum/kubeside/internal/config" "github.com/dynaum/kubeside/internal/kubeconfig" + "github.com/dynaum/kubeside/internal/promotion" "github.com/dynaum/kubeside/internal/session" "github.com/dynaum/kubeside/internal/timeline" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/fake" ) +// fakeApp is one workload to seed into a fake cluster: a Deployment running +// one image, which is all the promotion matrix needs to compare versions. +type fakeApp struct { + name, ns, image string +} + +// fakeCluster is one kubeconfig context's worth of fixture: which environment +// it binds to, what it runs, and whether it can be reached or read at all. +type fakeCluster struct { + env string + apps []fakeApp + // unreachable fails the connection itself, the way an expired token or an + // off-VPN cluster does. + unreachable bool + // denied connects, but as someone the cluster refuses to answer, the way a + // revoked RBAC binding does. Distinct from unreachable so a future test can + // tell "could not dial it" from "dialed it and was refused" apart. + denied bool +} + +// serviceWithContexts builds a Service over several kubeconfig contexts, each +// bound to an environment and backed by its own fake clientset. It is the +// multi-cluster sibling of serviceWith: where that helper proves one context +// works, this one proves several contexts bound to the same environment are +// all read, not just the first. +func serviceWithContexts(t *testing.T, byContext map[string]fakeCluster) *Service { + t.Helper() + + names := make([]string, 0, len(byContext)) + for name := range byContext { + names = append(names, name) + } + sort.Strings(names) + if len(names) == 0 { + t.Fatal("serviceWithContexts needs at least one context") + } + + var envOrder []string + envContexts := map[string][]string{} + contexts := make([]kubeconfig.Context, 0, len(names)) + for _, name := range names { + fc := byContext[name] + if _, ok := envContexts[fc.env]; !ok { + envOrder = append(envOrder, fc.env) + } + envContexts[fc.env] = append(envContexts[fc.env], name) + contexts = append(contexts, kubeconfig.Context{ + Name: name, IsCurrent: name == names[0], Server: "https://" + name, + }) + } + + var b strings.Builder + b.WriteString("environments:\n") + for _, env := range envOrder { + b.WriteString(" - name: " + env + "\n") + b.WriteString(" contexts: [" + strings.Join(envContexts[env], ", ") + "]\n") + } + + p := filepath.Join(t.TempDir(), "config.yaml") + if err := os.WriteFile(p, []byte(b.String()), 0o600); err != nil { + t.Fatalf("write config: %v", err) + } + conf, err := config.Load(config.Options{Path: p}) + if err != nil { + t.Fatalf("config.Load: %v", err) + } + + connector := clusters.KubeConnector{ + NewClient: func(kctx kubeconfig.Context, _ kubeconfig.Options) (kubernetes.Interface, error) { + fc := byContext[kctx.Name] + switch { + case fc.unreachable: + return nil, fmt.Errorf("dial %s: connection refused", kctx.Name) + case fc.denied: + return nil, fmt.Errorf("forbidden: %s refuses this identity", kctx.Name) + } + return fakeClientFor(fc.apps), nil + }, + } + + kcfg := &kubeconfig.Config{Current: names[0], Contexts: contexts} + mgr := clusters.New(kcfg, connector, clusters.Options{}) + t.Cleanup(mgr.Close) + return NewService(kcfg, mgr, kubeconfig.Options{}, conf, time.Second) +} + +// fakeClientFor builds a fake clientset with one Deployment per app, mirroring +// clusterWithApp: a selector matching its own pod template is enough for the +// grouping engine to recognize it, and no pod is needed for a version compare. +func fakeClientFor(apps []fakeApp) *fake.Clientset { + objs := make([]runtime.Object, 0, len(apps)) + for _, a := range apps { + objs = append(objs, &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Name: a.name, Namespace: a.ns, UID: types.UID(a.ns + "/" + a.name)}, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": a.name}}, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": a.name}}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "app", Image: a.image}}}, + }, + }, + }) + } + return fake.NewSimpleClientset(objs...) +} + +// Two contexts bound to one environment, running different versions. Build +// the service with both contexts pointing at prod, the second serving +// checkout at v2.12.0 and the first at v2.13.1. +func TestPromotionKeepsEveryContextInAnEnvironment(t *testing.T) { + s := serviceWithContexts(t, map[string]fakeCluster{ + "prod-us-east": {env: "prod", apps: []fakeApp{{name: "checkout", ns: "shop", image: "reg/checkout:v2.13.1"}}}, + "prod-eu-west": {env: "prod", apps: []fakeApp{{name: "checkout", ns: "shop", image: "reg/checkout:v2.12.0"}}}, + }) + + v := s.Promotion() + + if len(v.Envs) != 1 { + t.Fatalf("envs = %d, want 1: both contexts bind to prod", len(v.Envs)) + } + if got := len(v.Envs[0].Contexts); got != 2 { + t.Errorf("prod contexts = %d, want 2: the second context was dropped", got) + } + if len(v.Rows) != 1 { + t.Fatalf("rows = %d, want 1", len(v.Rows)) + } + c := v.Rows[0].Cells[0] + if c.State != promotion.StateSplit { + t.Errorf("state = %q, want %q: two clusters on different versions is not one version", c.State, promotion.StateSplit) + } +} + +func TestPromotionMarksAnUnreachableContextOnItsEnvironment(t *testing.T) { + s := serviceWithContexts(t, map[string]fakeCluster{ + "prod-us-east": {env: "prod", apps: []fakeApp{{name: "checkout", ns: "shop", image: "reg/checkout:v2.13.1"}}}, + "prod-eu-west": {env: "prod", unreachable: true}, + }) + + v := s.Promotion() + + if len(v.Envs) != 1 { + t.Fatalf("envs = %d, want 1", len(v.Envs)) + } + if got := v.Envs[0].Unreadable; len(got) != 1 || got[0] != "prod-eu-west" { + t.Errorf("unreadable = %v, want [prod-eu-west]", got) + } + if v.Rows[0].Cells[0].State != promotion.StateSplit { + t.Error("a cluster nobody could read is not an agreement") + } +} + func serviceFor(t *testing.T, body string, contexts ...kubeconfig.Context) *Service { t.Helper() From 272215b4ec7abbf8c6180c2d43396fed80ece868 Mon Sep 17 00:00:00 2001 From: Elber Ribeiro Date: Sun, 2 Aug 2026 23:51:10 -0400 Subject: [PATCH 09/38] docs(fleet): a refused read is denied, not absent Task 3 turned up that clusters.Fetch never errors on an RBAC refusal. Every list reads err == nil and records what it could not read in Snapshot.Partial. The plan mapped 'app not in snapshot' straight to absent, so a developer without list permission on Deployments would have seen 'not deployed here' for a cluster nobody was allowed to look at. That is the exact conflation the five states exist to prevent, and service.go:196 already draws the line correctly for the apps list. --- docs/superpowers/plans/2026-08-02-fleet.md | 57 ++++++++++++++++++++-- 1 file changed, 53 insertions(+), 4 deletions(-) diff --git a/docs/superpowers/plans/2026-08-02-fleet.md b/docs/superpowers/plans/2026-08-02-fleet.md index a7908b6..f53f076 100644 --- a/docs/superpowers/plans/2026-08-02-fleet.md +++ b/docs/superpowers/plans/2026-08-02-fleet.md @@ -1261,6 +1261,30 @@ func TestFleetMatchesAcrossEnvSuffixedNamespaces(t *testing.T) { } } +func TestARefusedReadIsDeniedNotAbsent(t *testing.T) { + // clusters.Fetch returns no error when RBAC refuses a list; it records the + // kind in Snapshot.Partial. An app missing from that snapshot was never + // looked for, so calling it absent would be a lie. + s := serviceWithContexts(t, map[string]fakeCluster{ + "qa-cluster": {env: "qa", apps: []fakeApp{{name: "checkout", ns: "shop", image: "reg/checkout:v2.14.0"}}}, + "prod-cluster": {env: "prod", refusedKinds: []string{"Deployment"}}, + }) + + v := s.Fleet("checkout", "shop") + + for _, r := range v.Rows { + if r.Context != "prod-cluster" { + continue + } + if r.State != fleet.StateDenied { + t.Fatalf("state = %q, want %q: we were not allowed to look, which is not the same as it not being there", r.State, fleet.StateDenied) + } + if !strings.Contains(r.Note, "Deployment") { + t.Errorf("note = %q, want it to name the kind that was refused", r.Note) + } + } +} + func TestFleetFoundNowhereStillNamesWhatItAsked(t *testing.T) { s := serviceWithContexts(t, map[string]fakeCluster{ "qa-cluster": {env: "qa"}, @@ -1358,9 +1382,20 @@ func (s *Service) Fleet(app, namespace string) fleet.View { match, found := findApp(snap.Apps, want) if !found { - // The cluster answered and does not run this app. That is a fact, - // and a different one from a cluster that would not answer. - p.State = fleet.StateAbsent + // Not in the snapshot has two causes and they are not the same + // fact. clusters.Fetch never errors on an RBAC refusal: it records + // the kinds it could not read in Snapshot.Partial and returns what + // it got. So an app missing from a snapshot with refused kinds is + // an app we were not allowed to look for, and calling that "not + // deployed here" is the conflation this screen exists to prevent. + // internal/api/service.go:196 already draws the same line for the + // apps list. + if len(snap.Partial) > 0 || len(snap.Scope.Namespaces) == 0 && !snap.Scope.ClusterWide { + p.State = fleet.StateDenied + p.Reason = refusedReason(snap) + } else { + p.State = fleet.StateAbsent + } placements = append(placements, p) continue } @@ -1391,6 +1426,18 @@ func findApp(list []apps.App, want string) (apps.App, bool) { } return apps.App{}, false } + +// refusedReason names what the cluster would not let us read, so a denied row +// says which verb to ask for rather than just refusing to answer. +func refusedReason(snap clusters.Snapshot) string { + if len(snap.Partial) > 0 { + return "could not read " + strings.Join(snap.Partial, ", ") + } + if snap.Scope.Reason != "" { + return snap.Scope.Reason + } + return "no readable namespace" +} ``` - [ ] **Step 5: Run test to verify it passes** @@ -2133,7 +2180,9 @@ reasoning was not reachable from the docs a user reads." **Known gaps the implementer must close.** These are verified, not suspected. -`serviceWithContexts`, `fakeCluster`, and `fakeApp` do not exist. The closest is `serviceWith` at `internal/api/tier_test.go:34`, which builds a `*Service` from one `*fake.Clientset` and a clock. Every test in Tasks 3, 5, and 11 needs several contexts with different apps and different reachability, so building `serviceWithContexts` on top of `serviceWith` is the first step of Task 3, and Tasks 5 and 11 reuse it. Task 11 additionally needs a `denied` field on `fakeCluster` making `clusters.Fetch` return a 403-shaped error. +`serviceWithContexts`, `fakeCluster`, and `fakeApp` do not exist. The closest is `serviceWith` at `internal/api/tier_test.go:34`, which builds a `*Service` from one `*fake.Clientset` and a clock. Every task needing several contexts builds on it; Task 3 creates it and Tasks 5 and 11 reuse it. + +Corrected 2026-08-02, after Task 3 landed: `clusters.Fetch` has NO path returning a hard error for an RBAC refusal. Every list reads `if l, err := ...List(); err == nil`, and refusals are recorded in `Snapshot.Partial` while the call returns a nil error. So a `denied` fixture cannot be built by making Fetch fail. It is built by making the fake clientset refuse a list, which populates `Partial`. Task 5 reads `Partial` to tell denied from absent, and Task 11's fixture needs `refusedKinds` rather than a fabricated error. `newTestServer` at `internal/api/server_test.go:214` does exist and Task 6 uses it as written. From af66813bc8fa5a80e42e037925fca7a449d0c663 Mon Sep 17 00:00:00 2001 From: Elber Ribeiro Date: Mon, 3 Aug 2026 00:02:45 -0400 Subject: [PATCH 10/38] docs(fleet): correct the all-denied claim in the spec The spec said an all-denied environment renders StateDenied 'identical to today'. It does not. Service.Promotion builds denied placeholders with an empty app name, the filter strips them before the engine sees them, and resolve() returns StateAbsent with 'not deployed here' for an environment nobody could read. Tracked as #75 and treated as a ship blocker for the feature, since populating Env.Unreadable and then ignoring it is worse than never having gathered it. --- .../specs/2026-08-02-fleet-design.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/docs/superpowers/specs/2026-08-02-fleet-design.md b/docs/superpowers/specs/2026-08-02-fleet-design.md index 95cb85a..5a0b5aa 100644 --- a/docs/superpowers/specs/2026-08-02-fleet-design.md +++ b/docs/superpowers/specs/2026-08-02-fleet-design.md @@ -187,9 +187,26 @@ Cell behavior for a multi-context environment: | Present, disagreeing on tag or digest | `StateSplit`, severe, naming the count, linking to fleet | | Mixed present and absent | `StateSplit`. Deployed in two of three clusters is not deployed | | Mixed readable and denied | `StateSplit`. Partial visibility never collapses | -| All denied | `StateDenied`, identical to today | +| All denied | `StateDenied`. Not identical to today, see below | | All absent | `StateAbsent`, identical to today | +The all-denied row needs a correction, found while reviewing the service +wiring on 2026-08-03. Today an environment nobody could read does NOT render +denied. `Service.Promotion` builds its denied placeholders with an empty app +name, the `in.App != ""` filter strips them before `promotion.Build` sees +them, and `resolve()` receives an empty group and returns `StateAbsent` with +the note "not deployed here". Only a banner says otherwise. + +The same path also mislabels a partially readable environment: an app absent +from the clusters that answered renders "not deployed here" while an unread +cluster may be running it. + +That conflates the two facts this whole feature exists to keep apart, and it +breaks the hard rule against rendering an unknown window as an empty one. It +predates this work, and populating `Env.Unreadable` is what makes it fixable. +Tracked as issue #75 and treated as a ship blocker: shipping with the +information present and ignored is worse than shipping without it. + A split cell never becomes the upstream for the next column. This follows the existing rule at `promotion.go:151`, where an unreadable environment is not an agreement and so does not become the comparison basis. From 903b55aa68f4a27b3bcca760ece5051fdf6ede28 Mon Sep 17 00:00:00 2001 From: Elber Ribeiro Date: Mon, 3 Aug 2026 00:05:28 -0400 Subject: [PATCH 11/38] fix(promotion): unreachable banner only for fully-unread environments PromotionView.Unreachable was appending an environment's name whenever any one of its bound contexts failed, even when its other contexts read fine. That renders a split cell full of real version data next to a banner claiming the same column is unknown. Unreachable now names an environment only when every context bound to it failed, matching its doc comment and what the split cell/Env.Unreadable already communicate for a partial failure. Also covers dedupeStrings directly (previously never exercised), records that an environment's risk comes from its first-seen context, and corrects the fakeCluster.denied doc comment: it takes the same Connect-failure branch as unreachable today, so the distinction is cosmetic until a fixture returns apierrors.NewForbidden from a list call instead. --- internal/api/service.go | 16 ++++++++-- internal/api/service_test.go | 62 +++++++++++++++++++++++++++++++++--- 2 files changed, 70 insertions(+), 8 deletions(-) diff --git a/internal/api/service.go b/internal/api/service.go index b89193d..c16f6d0 100644 --- a/internal/api/service.go +++ b/internal/api/service.go @@ -1006,7 +1006,6 @@ func (s *Service) Promotion() PromotionView { var order []string var instances []promotion.Instance - var unreachable []string for _, name := range s.mgr.ConnectOrder() { kctx := s.cfg.MustGet(name) @@ -1014,6 +1013,11 @@ func (s *Service) Promotion() PromotionView { acc, ok := byEnv[env.Name] if !ok { + // The first context seen for this environment sets its risk; every + // context bound to the same environment name carries the same + // config-derived risk today, so which one "wins" has no effect + // unless a config-bound name later collides with a + // Classify-derived one. acc = &accum{risk: env.Risk.String()} byEnv[env.Name] = acc order = append(order, env.Name) @@ -1027,7 +1031,6 @@ func (s *Service) Promotion() PromotionView { client, ok := s.mgr.ClientFor(name) if !ok { acc.unreadable = append(acc.unreadable, name) - unreachable = append(unreachable, env.Name) instances = append(instances, promotion.Instance{ Env: env.Name, Context: name, App: "", Denied: true, DeniedReason: orElseString(reason, "not connected"), @@ -1038,7 +1041,6 @@ func (s *Service) Promotion() PromotionView { snap, err := clusters.Fetch(ctx, client, kctx, clusters.FetchOptions{Tier: s.mgr.Tier(name)}) if err != nil { acc.unreadable = append(acc.unreadable, name) - unreachable = append(unreachable, env.Name) continue } for _, a := range snap.Apps { @@ -1048,13 +1050,21 @@ func (s *Service) Promotion() PromotionView { } } + // Unreachable names only the environments where every bound context + // failed. A split cell and Env.Unreadable already tell the story for an + // environment that is merely partly degraded; adding it here too would + // have the banner call a column unknown while it renders real data. envs := make([]promotion.Env, 0, len(order)) + var unreachable []string for _, name := range order { acc := byEnv[name] envs = append(envs, promotion.Env{ Name: name, Risk: acc.risk, Context: acc.contexts[0], Contexts: acc.contexts, Unreadable: acc.unreadable, }) + if len(acc.unreadable) > 0 && len(acc.unreadable) == len(acc.contexts) { + unreachable = append(unreachable, name) + } } // Instances with no app name are placeholders for a cluster nobody could diff --git a/internal/api/service_test.go b/internal/api/service_test.go index 8ba8122..1d64e8f 100644 --- a/internal/api/service_test.go +++ b/internal/api/service_test.go @@ -33,14 +33,18 @@ type fakeApp struct { // fakeCluster is one kubeconfig context's worth of fixture: which environment // it binds to, what it runs, and whether it can be reached or read at all. type fakeCluster struct { - env string - apps []fakeApp + env string + apps []fakeApp // unreachable fails the connection itself, the way an expired token or an // off-VPN cluster does. unreachable bool - // denied connects, but as someone the cluster refuses to answer, the way a - // revoked RBAC binding does. Distinct from unreachable so a future test can - // tell "could not dial it" from "dialed it and was refused" apart. + // denied also fails at NewClient today, with a different error string + // only; clusters.Fetch has no path that returns a hard error, so this + // flag takes the identical branch as unreachable in Service.Promotion() + // and the distinction is cosmetic. A genuine "connected, then refused" + // fixture needs a clientset whose list calls return + // apierrors.NewForbidden, which lands in Snapshot.Partial rather than an + // error — that fixture belongs to whichever task first needs it. denied bool } @@ -175,6 +179,54 @@ func TestPromotionMarksAnUnreachableContextOnItsEnvironment(t *testing.T) { } } +// A partially-unreachable environment (one context down, one reads fine) must +// not appear in PromotionView.Unreachable: the split cell and Env.Unreadable +// already say a cluster there could not be read, and the banner claiming the +// whole column is unknown while it renders real split data would contradict +// what is on screen. A fully-unreachable environment, where every bound +// context failed, still belongs in the banner. +func TestPromotionUnreachableOnlyForFullyUnreadableEnvironments(t *testing.T) { + s := serviceWithContexts(t, map[string]fakeCluster{ + "prod-us-east": {env: "prod", apps: []fakeApp{{name: "checkout", ns: "shop", image: "reg/checkout:v2.13.1"}}}, + "prod-eu-west": {env: "prod", unreachable: true}, + "stg-a": {env: "stg", unreachable: true}, + "stg-b": {env: "stg", denied: true}, + }) + + v := s.Promotion() + + for _, name := range v.Unreachable { + if name == "prod" { + t.Errorf("Unreachable = %v, must not include prod: one of its two contexts reads fine", v.Unreachable) + } + } + found := false + for _, name := range v.Unreachable { + if name == "stg" { + found = true + } + } + if !found { + t.Errorf("Unreachable = %v, want stg: every context bound to it failed", v.Unreachable) + } +} + +// dedupeStrings is what keeps an environment with several unreachable +// contexts down to one banner entry. Exercise it directly, since nothing +// about the code path it serves proves the collapsing behavior on its own. +func TestDedupeStrings(t *testing.T) { + got := dedupeStrings([]string{"prod", "stg", "prod", "prod", "qa"}) + want := []string{"prod", "stg", "qa"} + if len(got) != len(want) { + t.Fatalf("dedupeStrings = %v, want %v", got, want) + } + for i := range want { + if got[i] != want[i] { + t.Fatalf("dedupeStrings = %v, want %v", got, want) + } + } +} + func serviceFor(t *testing.T, body string, contexts ...kubeconfig.Context) *Service { t.Helper() From aa2e5e6df288fe037341fc24f7e2ebe8beafedc4 Mon Sep 17 00:00:00 2001 From: Elber Ribeiro Date: Mon, 3 Aug 2026 00:13:34 -0400 Subject: [PATCH 12/38] fix(promotion): an unread environment renders denied, not absent resolve()'s empty-present path checked only Denied:true instances, which service.go strips before Build ever sees them (they carry no App name). An environment nobody could read fell through to StateAbsent, "not deployed here", the exact conflation the package doc warns against. resolve() now consults Env.Unreadable when no instances are present: a fully unread environment renders StateDenied naming how many clusters did not answer; a partially unread one renders StateDenied naming what was read and what was not, since the readable clusters agreeing on absence still cannot rule out a cluster nobody read. StateSplit was considered and rejected for the partial case: nothing was observed to disagree, so claiming a split would invent a conflict, and it would wrongly count as drift. Only an environment where every context answered may still say "not deployed here". Spec corrected alongside the fix. Closes #75 --- .../specs/2026-08-02-fleet-design.md | 46 ++++---- internal/promotion/promotion.go | 22 ++++ internal/promotion/promotion_test.go | 102 ++++++++++++++++++ 3 files changed, 150 insertions(+), 20 deletions(-) diff --git a/docs/superpowers/specs/2026-08-02-fleet-design.md b/docs/superpowers/specs/2026-08-02-fleet-design.md index 5a0b5aa..4154765 100644 --- a/docs/superpowers/specs/2026-08-02-fleet-design.md +++ b/docs/superpowers/specs/2026-08-02-fleet-design.md @@ -186,26 +186,32 @@ Cell behavior for a multi-context environment: | All present and agreeing on tag and digest | Collapses to one version, identical to today | | Present, disagreeing on tag or digest | `StateSplit`, severe, naming the count, linking to fleet | | Mixed present and absent | `StateSplit`. Deployed in two of three clusters is not deployed | -| Mixed readable and denied | `StateSplit`. Partial visibility never collapses | -| All denied | `StateDenied`. Not identical to today, see below | -| All absent | `StateAbsent`, identical to today | - -The all-denied row needs a correction, found while reviewing the service -wiring on 2026-08-03. Today an environment nobody could read does NOT render -denied. `Service.Promotion` builds its denied placeholders with an empty app -name, the `in.App != ""` filter strips them before `promotion.Build` sees -them, and `resolve()` receives an empty group and returns `StateAbsent` with -the note "not deployed here". Only a banner says otherwise. - -The same path also mislabels a partially readable environment: an app absent -from the clusters that answered renders "not deployed here" while an unread -cluster may be running it. - -That conflates the two facts this whole feature exists to keep apart, and it -breaks the hard rule against rendering an unknown window as an empty one. It -predates this work, and populating `Env.Unreadable` is what makes it fixable. -Tracked as issue #75 and treated as a ship blocker: shipping with the -information present and ignored is worse than shipping without it. +| Mixed readable and denied, at least one present | `StateSplit`. Partial visibility never collapses | +| No instances present, some or all contexts unread | `StateDenied`, naming what was read and what was not. Never `StateAbsent` | +| All contexts read, no instances present | `StateAbsent`, identical to today | + +The no-instances-present row was wrong until issue #75. `Service.Promotion` +builds its denied placeholders with an empty app name, the `in.App != ""` +filter strips them before `promotion.Build` sees them, and `resolve()` +received an empty group with no way to tell "nobody answered" from "everybody +answered and found nothing". It returned `StateAbsent` with the note "not +deployed here" in both cases. Only a banner said otherwise. + +The same path also mislabeled a partially readable environment: an app absent +from the clusters that answered rendered "not deployed here" while an unread +cluster might be running it. + +That conflated the two facts this whole feature exists to keep apart, and it +broke the hard rule against rendering an unknown window as an empty one. +`resolve()` now consults `Env.Unreadable` before deciding: any unread context +in the group, full or partial, renders `StateDenied` instead, with a note +naming how many clusters answered and how many did not. `StateSplit` was +considered for the partial case and rejected, because `StateSplit` means +instances were observed and disagreed; here the readable clusters unanimously +agree the app is absent; the only open question is a cluster nobody read, and +that is what `StateDenied` already means everywhere else in this matrix. Only +an environment where every context actually answered, and none of them have +the app, may still say "not deployed here". Fixed and closed. A split cell never becomes the upstream for the next column. This follows the existing rule at `promotion.go:151`, where an unreadable environment is not an diff --git a/internal/promotion/promotion.go b/internal/promotion/promotion.go index 71edc26..4bb16b4 100644 --- a/internal/promotion/promotion.go +++ b/internal/promotion/promotion.go @@ -243,6 +243,28 @@ func resolve(env Env, group []Instance, upstream *Instance) (Instance, Cell, boo } return Instance{}, Cell{Env: env.Name, Namespace: ns, State: StateDenied, Note: reason, Clusters: clusters}, false } + + // service.go's denied placeholders carry no App name and are filtered + // out before Build ever sees them, so a fully or partially unread + // environment reaches here with denied == 0 and an empty group. Absent + // requires every context to have actually answered; anything less is a + // fact this cell must not erase. + if len(env.Unreadable) > 0 { + unread := len(env.Unreadable) + if unread >= len(env.Contexts) { + note := fmt.Sprintf("%d of %d clusters did not answer", unread, clusters) + return Instance{}, Cell{Env: env.Name, Namespace: ns, State: StateDenied, Note: note, Clusters: clusters}, false + } + // Some, but not all, contexts answered, and none of them have the + // app. That is not "not deployed here": the clusters that stayed + // silent might. Name what was read and what was not, same as the + // fully-unread case, rather than inventing a disagreement no + // instance actually showed. + readable := clusters - unread + note := fmt.Sprintf("not found in the %d of %d clusters read; %d did not answer and may have it", readable, clusters, unread) + return Instance{}, Cell{Env: env.Name, Namespace: ns, State: StateDenied, Note: note, Clusters: clusters}, false + } + return Instance{}, Cell{Env: env.Name, Namespace: ns, State: StateAbsent, Note: "not deployed here", Clusters: clusters}, false } diff --git a/internal/promotion/promotion_test.go b/internal/promotion/promotion_test.go index 02c67c3..04693ce 100644 --- a/internal/promotion/promotion_test.go +++ b/internal/promotion/promotion_test.go @@ -630,3 +630,105 @@ func TestNamespaceIsFirstNonEmptyInTheGroup(t *testing.T) { t.Errorf("namespace = %q, want team-a: the first non-empty namespace in the group, not always group[0]", c.Namespace) } } + +// --- Issue #75: an unread environment must not render "not deployed here" --- +// +// service.go builds its denied placeholders with an empty App name, and the +// `in.App != ""` filter strips them before promotion.Build ever sees them. So +// resolve()'s len(present) == 0 path saw denied == 0 for an environment +// nobody could read, and returned StateAbsent. Env.Unreadable now carries +// exactly the information resolve() was ignoring. + +// A fully unreadable environment (every context in Unreadable) with no +// present instances must render denied, not absent, and must name how many +// clusters did not answer. +func TestFullyUnreadableEnvironmentIsDeniedNotAbsent(t *testing.T) { + env := Env{Name: "prod", Contexts: []string{"prod-a", "prod-b", "prod-c"}, Unreadable: []string{"prod-a", "prod-b", "prod-c"}} + _, c, ok := resolve(env, nil, nil) + if c.State == StateAbsent { + t.Fatalf("state = %q, want anything but absent: no context in this environment answered", c.State) + } + if c.State != StateDenied { + t.Fatalf("state = %q, want denied: every context failed to answer, same fact as a single denied cluster", c.State) + } + if !strings.Contains(c.Note, "3") { + t.Errorf("note = %q, want it to name how many clusters did not answer", c.Note) + } + if ok { + t.Error("ok = true; a fully unreadable environment must never become the upstream for the next column") + } +} + +// The broader form of the same bug: an app absent from the READABLE clusters +// of a partially readable environment might still be running in the cluster +// nobody read. This must never render absent either. +func TestPartiallyUnreadableEnvironmentIsNotAbsentEither(t *testing.T) { + env := Env{Name: "prod", Contexts: []string{"prod-a", "prod-b", "prod-c"}, Unreadable: []string{"prod-c"}} + _, c, ok := resolve(env, nil, nil) + if c.State == StateAbsent { + t.Fatalf("state = %q, want anything but absent: prod-c never answered and might have it", c.State) + } + if c.Note == "" { + t.Fatal("a cell that cannot claim absence should say what was read and what was not") + } + if !strings.Contains(c.Note, "2") || !strings.Contains(c.Note, "1") { + t.Errorf("note = %q, want it to name both the clusters read (2) and the cluster that did not answer (1)", c.Note) + } + if ok { + t.Error("ok = true; a partially unreadable environment must never become the upstream for the next column") + } +} + +// Regression guard: when every context in the environment actually answered +// and none of them have the app, "not deployed here" still stands. This is +// the ordinary case the fix must not disturb. +func TestFullyReadableAbsentEnvironmentStillReadsAbsent(t *testing.T) { + env := Env{Name: "prod", Contexts: []string{"prod-a", "prod-b", "prod-c"}} + _, c, ok := resolve(env, nil, nil) + if c.State != StateAbsent || c.Note != "not deployed here" { + t.Fatalf("cell = %+v, want absent/\"not deployed here\": every context answered and found nothing", c) + } + if ok { + t.Error("ok = true; an absent cell must never become upstream") + } +} + +// A cell rendered denied because its environment was unreadable (fully or +// partially, with zero present instances) must not become the basis the +// next column is compared against, exactly like the pre-existing denied and +// absent paths. +func TestUnreadableEmptyEnvironmentDoesNotBecomeUpstream(t *testing.T) { + envs := []Env{ + {Name: "qa", Contexts: []string{"qa-cluster"}}, + {Name: "stg", Contexts: []string{"stg-a", "stg-b", "stg-c"}, Unreadable: []string{"stg-a", "stg-b", "stg-c"}}, + {Name: "prod", Contexts: []string{"prod-cluster"}}, + } + rows := Build(envs, []Instance{ + {Env: "qa", Context: "qa-cluster", App: "checkout", Namespace: "shop", Present: true, Tag: "v2.0.0", Digest: "sha256:aa"}, + {Env: "prod", Context: "prod-cluster", App: "checkout", Namespace: "shop", Present: true, Tag: "v2.0.0", Digest: "sha256:aa"}, + }) + c := cellFor(t, rows, "prod") + if c.State != StateSame { + t.Errorf("prod state = %q, want %q: prod matches qa, and the unreadable stg column never became the basis", c.State, StateSame) + } + stg := cell(at(t, rows, "checkout"), "stg") + if stg.State != StateDenied { + t.Errorf("stg state = %q, want denied", stg.State) + } +} + +// Finding-5-style guard: the pre-existing denied path, where a real named +// instance carries Denied: true, must be untouched by this fix. +func TestNamedDeniedInstanceStillDeniedAlongsideUnreadable(t *testing.T) { + env := Env{Name: "prod", Contexts: []string{"prod-a", "prod-b"}, Unreadable: []string{"prod-a"}} + group := []Instance{ + {Env: "prod", App: "checkout", Namespace: "shop", Denied: true, DeniedReason: "needs get deployments"}, + } + _, c, ok := resolve(env, group, nil) + if c.State != StateDenied || c.Note != "needs get deployments" { + t.Fatalf("cell = %+v, want the named denial reason preserved untouched", c) + } + if ok { + t.Error("ok = true; a denied cell must never become upstream") + } +} From b34bf24eec75e4a44b65bc42bd96056ac0fbe544 Mon Sep 17 00:00:00 2001 From: Elber Ribeiro Date: Mon, 3 Aug 2026 00:22:06 -0400 Subject: [PATCH 13/38] fix(promotion): pin drift/split behavior, fix cluster count, trim note Round-1 review on task-3b (#75): add a test asserting Row.Drift and Summary.Split for the unread case the commit's reasoning rests on; strengthen the "not absent" test to pin StateDenied exactly so a future StateSplit refactor can't slip through green. Widen resolve()'s clusters so a comparison and its printed message never disagree ("2 of 1 clusters did not answer" when Unreadable outgrows Contexts). Shorten the partial- read note to match every other terse split() note. --- internal/promotion/promotion.go | 15 ++++-- internal/promotion/promotion_test.go | 72 ++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 3 deletions(-) diff --git a/internal/promotion/promotion.go b/internal/promotion/promotion.go index 4bb16b4..2f6a8b0 100644 --- a/internal/promotion/promotion.go +++ b/internal/promotion/promotion.go @@ -207,6 +207,14 @@ func resolve(env Env, group []Instance, upstream *Instance) (Instance, Cell, boo if clusters == 0 { clusters = 1 } + // Unreadable can outgrow the declared Contexts in a synthetically built + // Env (Env.Contexts unset but Unreadable populated); service.go never + // produces that shape today, since acc.contexts is always a superset of + // acc.unreadable, but resolve() must not print a cluster count smaller + // than the number of contexts it names as unread. + if unread := len(env.Unreadable); unread > clusters { + clusters = unread + } var present []Instance denied := 0 @@ -251,7 +259,7 @@ func resolve(env Env, group []Instance, upstream *Instance) (Instance, Cell, boo // fact this cell must not erase. if len(env.Unreadable) > 0 { unread := len(env.Unreadable) - if unread >= len(env.Contexts) { + if unread >= clusters { note := fmt.Sprintf("%d of %d clusters did not answer", unread, clusters) return Instance{}, Cell{Env: env.Name, Namespace: ns, State: StateDenied, Note: note, Clusters: clusters}, false } @@ -259,9 +267,10 @@ func resolve(env Env, group []Instance, upstream *Instance) (Instance, Cell, boo // app. That is not "not deployed here": the clusters that stayed // silent might. Name what was read and what was not, same as the // fully-unread case, rather than inventing a disagreement no - // instance actually showed. + // instance actually showed. Terse, like every other split() note: + // this renders inside a matrix cell. readable := clusters - unread - note := fmt.Sprintf("not found in the %d of %d clusters read; %d did not answer and may have it", readable, clusters, unread) + note := fmt.Sprintf("not in the %d clusters read; %d did not answer", readable, unread) return Instance{}, Cell{Env: env.Name, Namespace: ns, State: StateDenied, Note: note, Clusters: clusters}, false } diff --git a/internal/promotion/promotion_test.go b/internal/promotion/promotion_test.go index 04693ce..546e32e 100644 --- a/internal/promotion/promotion_test.go +++ b/internal/promotion/promotion_test.go @@ -668,17 +668,61 @@ func TestPartiallyUnreadableEnvironmentIsNotAbsentEither(t *testing.T) { if c.State == StateAbsent { t.Fatalf("state = %q, want anything but absent: prod-c never answered and might have it", c.State) } + // Round-1 review finding: this test asserted only "not absent," so a + // future refactor routing this case through StateSplit (a genuine + // disagreement) instead of StateDenied (an unknown) would flip + // Row.Drift, Summary.Split, and the matrix sort order, and this test + // would stay green. Pin the exact state. + if c.State != StateDenied { + t.Fatalf("state = %q, want denied: the silent cluster is an unknown, not a disagreement observed among present instances", c.State) + } if c.Note == "" { t.Fatal("a cell that cannot claim absence should say what was read and what was not") } if !strings.Contains(c.Note, "2") || !strings.Contains(c.Note, "1") { t.Errorf("note = %q, want it to name both the clusters read (2) and the cluster that did not answer (1)", c.Note) } + // Round-1 review finding 3: the original note ("not found in the 2 of 3 + // clusters read; 1 did not answer and may have it") was the longest + // string in the file and renders inside a matrix cell. Every other split() + // note is terse; this one must carry the same two facts without the + // padding. + if c.Note != "not in the 2 clusters read; 1 did not answer" { + t.Errorf("note = %q, want a terse note carrying the same two facts (clusters read, clusters unread)", c.Note) + } if ok { t.Error("ok = true; a partially unreadable environment must never become the upstream for the next column") } } +// Round-1 review finding 1: the drift behavior the commit and spec both rest +// on (routing the unread case through StateDenied avoids wrongly inflating +// drift) was never asserted. An app present in one environment, with the +// other environment partially unreadable and nothing present there, must not +// count as drift, and must not be counted as a split row either: the silent +// cluster is a fact nobody observed, not a disagreement. +func TestPartiallyUnreadableEnvironmentDoesNotInflateDriftOrSplit(t *testing.T) { + envs := []Env{ + {Name: "qa", Contexts: []string{"qa-a"}}, + {Name: "prod", Contexts: []string{"p-a", "p-b", "p-c"}, Unreadable: []string{"p-c"}}, + } + rows := Build(envs, []Instance{ + {Env: "qa", Context: "qa-a", App: "checkout", Namespace: "shop", Present: true, Tag: "v1.0.0", Digest: "sha256:aa"}, + }) + r := at(t, rows, "checkout") + prod := cell(r, "prod") + if prod.State != StateDenied { + t.Fatalf("prod state = %q, want denied", prod.State) + } + if r.Drift != 0 { + t.Fatalf("drift = %d, want 0: the unread cluster is an unknown, not a disagreement, and must not float this row up the matrix", r.Drift) + } + s := Summarize(rows) + if s.Split != 0 { + t.Fatalf("split = %d, want 0: a denied cell is not a split cell and must not be counted in Summary.Split", s.Split) + } +} + // Regression guard: when every context in the environment actually answered // and none of them have the app, "not deployed here" still stands. This is // the ordinary case the fix must not disturb. @@ -732,3 +776,31 @@ func TestNamedDeniedInstanceStillDeniedAlongsideUnreadable(t *testing.T) { t.Error("ok = true; a denied cell must never become upstream") } } + +// --- Round-1 review findings on task #75 --- + +// Finding 2: the fully-unread branch compared unread against len(env.Contexts) +// but printed clusters (max(len(env.Contexts), 1)). An Env with no Contexts +// but a populated Unreadable — the shape most direct-call tests in this file +// use — printed a nonsensical "2 of 1 clusters did not answer". clusters must +// account for Unreadable outgrowing the declared Contexts, and the comparison +// and the message must use that same number. +func TestUnreadableCountNeverExceedsPrintedClusterCount(t *testing.T) { + env := Env{Name: "prod", Unreadable: []string{"prod-a", "prod-b"}} + _, c, ok := resolve(env, nil, nil) + if c.State != StateDenied { + t.Fatalf("state = %q, want denied", c.State) + } + if strings.Contains(c.Note, "of 1") { + t.Errorf("note = %q; 1 is the bogus single-cluster default, not the 2 unread contexts actually named", c.Note) + } + if !strings.Contains(c.Note, "2 of 2") { + t.Errorf("note = %q, want \"2 of 2 clusters did not answer\": the comparison and the message must use the same cluster count", c.Note) + } + if c.Clusters != 2 { + t.Errorf("clusters = %d, want 2: the declared count must grow to cover the named unreadable contexts", c.Clusters) + } + if ok { + t.Error("ok = true; a denied cell must never become upstream") + } +} From 24ada32084f0369be85468b732de23c692fa6a36 Mon Sep 17 00:00:00 2001 From: Elber Ribeiro Date: Mon, 3 Aug 2026 00:27:16 -0400 Subject: [PATCH 14/38] feat(fleet): derive which clusters run the newest version One app, one row per cluster. Newest tag across the clusters that answered, and every cluster behind it. Five states stay distinct so a cluster behind a VPN is never read as a cluster without the app, and two contexts aimed at one cluster merge so the headline count stays honest. Closes #64 --- internal/fleet/fleet.go | 229 +++++++++++++++++++++++++++++++++++ internal/fleet/fleet_test.go | 120 ++++++++++++++++++ 2 files changed, 349 insertions(+) create mode 100644 internal/fleet/fleet.go create mode 100644 internal/fleet/fleet_test.go diff --git a/internal/fleet/fleet.go b/internal/fleet/fleet.go new file mode 100644 index 0000000..2a14b0d --- /dev/null +++ b/internal/fleet/fleet.go @@ -0,0 +1,229 @@ +// Package fleet answers "is every cluster running the latest version". +// +// One app, one row per cluster, environment demoted to a column. The promotion +// matrix compares environments side by side; this compares the clusters inside +// and across them, which is the question a team running prod in three regions +// asks and the matrix cannot phrase. +// +// Two facts carry the screen: the newest tag among the clusters that answered, +// and every cluster behind it. Everything else is there so a cluster that did +// not answer is never mistaken for a cluster without the app. +package fleet + +import ( + "fmt" + "sort" + + "github.com/dynaum/kubeside/internal/promotion" +) + +// Placement states. +const ( + StatePresent = "present" + StateAbsent = "absent" + StateDenied = "denied" + StateUnreachable = "unreachable" + StatePending = "pending" +) + +// Placement is one app as it exists in one cluster. +type Placement struct { + Context string `json:"context"` + ClusterID string `json:"clusterId"` + Env string `json:"env"` + Namespace string `json:"namespace,omitempty"` + + State string `json:"state"` + Reason string `json:"reason,omitempty"` + + Image string `json:"image,omitempty"` + Tag string `json:"tag,omitempty"` + Digest string `json:"digest,omitempty"` + // DigestPending marks a placement whose digest has not arrived. A missing + // digest never reads as a match. + DigestPending bool `json:"digestPending,omitempty"` + + Health string `json:"health,omitempty"` + Ready string `json:"ready,omitempty"` + RevisionAt string `json:"revisionAt,omitempty"` + // Aliases names other kubeconfig contexts pointing at this same cluster, + // so a merged row says what it merged. + Aliases []string `json:"aliases,omitempty"` +} + +// Row is one cluster, judged against the newest version. +type Row struct { + Placement + Behind bool `json:"behind,omitempty"` + // MutableTag marks a cluster carrying the newest tag with a digest another + // cluster's copy of that tag does not share. + MutableTag bool `json:"mutableTag,omitempty"` + Note string `json:"note,omitempty"` +} + +// View is the whole screen. +type View struct { + App string `json:"app"` + Namespace string `json:"namespace"` + Rows []Row `json:"rows"` + + Newest string `json:"newest,omitempty"` + Clusters int `json:"clusters"` + Present int `json:"present"` + Behind int `json:"behind"` + // MutableTag is true when one tag resolved to more than one digest. It + // outranks being behind: a cluster openly on an older version is a + // schedule, two clusters claiming one version while running different + // code is a defect. + MutableTag bool `json:"mutableTag,omitempty"` +} + +// Build derives the screen from one placement per context. +func Build(app, namespace string, ps []Placement) View { + merged := mergeByCluster(ps) + + v := View{App: app, Namespace: namespace, Clusters: len(merged)} + + newest := "" + for _, p := range merged { + if p.State != StatePresent { + continue + } + v.Present++ + if p.Tag == "" { + continue + } + if newest == "" || promotion.CompareTags(p.Tag, newest) > 0 { + newest = p.Tag + } + } + v.Newest = newest + + // A tag carrying two digests is worse than a cluster openly behind. + digests := map[string]bool{} + for _, p := range merged { + if p.State == StatePresent && p.Tag == newest && p.Digest != "" { + digests[p.Digest] = true + } + } + v.MutableTag = len(digests) > 1 + + v.Rows = make([]Row, 0, len(merged)) + for _, p := range merged { + r := Row{Placement: p} + switch p.State { + case StatePresent: + r.DigestPending = p.Digest == "" + if p.Tag == newest { + if v.MutableTag && p.Digest != "" { + r.MutableTag = true + r.Note = fmt.Sprintf("runs %s, and so does another cluster with a different digest", newest) + } + break + } + // Behind only when the tags can be ordered. Two build ids differ + // without one being older, and calling either behind would invent + // a direction. + if p.Tag != "" && newest != "" && promotion.CompareTags(p.Tag, newest) < 0 { + r.Behind = true + v.Behind++ + r.Note = fmt.Sprintf("behind %s", newest) + } else if p.Tag != newest { + r.Note = fmt.Sprintf("runs %s, which cannot be ordered against %s", p.Tag, newest) + } + case StateUnreachable: + r.Note = orElse(p.Reason, "the cluster did not answer") + case StateDenied: + r.Note = orElse(p.Reason, "not readable here") + case StateAbsent: + r.Note = "not deployed here" + case StatePending: + r.Note = "asking" + } + v.Rows = append(v.Rows, r) + } + + sort.SliceStable(v.Rows, func(i, j int) bool { + si, sj := severity(v.Rows[i]), severity(v.Rows[j]) + if si != sj { + return si > sj + } + return v.Rows[i].Context < v.Rows[j].Context + }) + return v +} + +// severity sorts disagreement to the top, matching the promotion matrix's +// default of showing what needs attention first. +func severity(r Row) int { + switch { + case r.MutableTag: + return 5 + case r.Behind: + return 4 + case r.State == StateUnreachable: + return 3 + case r.State == StateDenied: + return 2 + case r.State == StatePending: + return 1 + default: + return 0 + } +} + +// mergeByCluster collapses contexts pointing at one cluster. +// +// A kubeconfig commonly holds the same cluster twice under two names, and one +// of the two may carry credentials that no longer work. Counting it twice +// inflates every number on the screen, so the most informative answer wins and +// the other context is recorded as an alias. +func mergeByCluster(ps []Placement) []Placement { + at := map[string]int{} + out := make([]Placement, 0, len(ps)) + for _, p := range ps { + key := p.ClusterID + if key == "" { + key = "ctx:" + p.Context + } + i, seen := at[key] + if !seen { + at[key] = len(out) + out = append(out, p) + continue + } + if informativeness(p.State) > informativeness(out[i].State) { + p.Aliases = append(p.Aliases, out[i].Context) + p.Aliases = append(p.Aliases, out[i].Aliases...) + out[i] = p + continue + } + out[i].Aliases = append(out[i].Aliases, p.Context) + } + return out +} + +// informativeness ranks what a cluster told us. Present beats absent, because +// a version is more than a confirmed nothing. Absent beats denied and +// unreachable, because the cluster answered at all. +func informativeness(state string) int { + switch state { + case StatePresent: + return 4 + case StateAbsent: + return 3 + case StateDenied: + return 2 + case StateUnreachable: + return 1 + default: + return 0 + } +} + +func orElse(v, fallback string) string { + if v == "" { + return fallback + } + return v +} diff --git a/internal/fleet/fleet_test.go b/internal/fleet/fleet_test.go new file mode 100644 index 0000000..dd7a283 --- /dev/null +++ b/internal/fleet/fleet_test.go @@ -0,0 +1,120 @@ +package fleet + +import "testing" + +func present(ctx, server, env, tag, digest string) Placement { + return Placement{ + Context: ctx, ClusterID: server, Env: env, Namespace: "shop", + State: StatePresent, Image: "reg/checkout:" + tag, Tag: tag, Digest: digest, + Health: "healthy", Ready: "3/3", + } +} + +func rowFor(t *testing.T, v View, ctx string) Row { + t.Helper() + for _, r := range v.Rows { + if r.Context == ctx { + return r + } + } + t.Fatalf("no row for context %q", ctx) + return Row{} +} + +func TestNewestIsTheHighestTagAcrossPresentClusters(t *testing.T) { + v := Build("checkout", "shop", []Placement{ + present("prod-us-east", "https://a", "prod", "v2.13.1", "sha256:bb"), + present("prod-eu-west", "https://b", "prod", "v2.12.0", "sha256:cc"), + present("qa-cluster", "https://c", "qa", "v2.14.0", "sha256:dd"), + }) + if v.Newest != "v2.14.0" { + t.Errorf("newest = %q, want v2.14.0", v.Newest) + } + if v.Behind != 2 { + t.Errorf("behind = %d, want 2", v.Behind) + } + if !rowFor(t, v, "prod-eu-west").Behind { + t.Error("prod-eu-west runs v2.12.0 and is behind") + } + if rowFor(t, v, "qa-cluster").Behind { + t.Error("the newest cluster is not behind itself") + } +} + +func TestUnorderableTagsAreNeverCalledBehind(t *testing.T) { + v := Build("checkout", "shop", []Placement{ + present("a", "https://a", "qa", "sha-abc123", "sha256:aa"), + present("b", "https://b", "qa", "sha-def456", "sha256:bb"), + }) + if v.Behind != 0 { + t.Errorf("behind = %d, want 0: two build ids cannot be ordered, and claiming one is behind invents a direction", v.Behind) + } +} + +func TestSameTagDifferentDigestOutranksBehind(t *testing.T) { + v := Build("checkout", "shop", []Placement{ + present("a", "https://a", "prod", "v2.13.1", "sha256:bb"), + present("b", "https://b", "prod", "v2.13.1", "sha256:cc"), + present("c", "https://c", "prod", "v2.12.0", "sha256:dd"), + }) + if !v.MutableTag { + t.Fatal("one tag resolving to two digests is the loudest state on the screen") + } + if v.Rows[0].Context != "a" && v.Rows[0].Context != "b" { + t.Errorf("first row = %q; the mutable-tag clusters sort above the behind one", v.Rows[0].Context) + } +} + +func TestTwoContextsOnOneClusterMergeToOneRow(t *testing.T) { + v := Build("checkout", "shop", []Placement{ + {Context: "prod-alias", ClusterID: "https://a", Env: "prod", State: StateUnreachable, Reason: "credentials expired"}, + present("prod-us-east", "https://a", "prod", "v2.13.1", "sha256:bb"), + }) + if len(v.Rows) != 1 { + t.Fatalf("rows = %d, want 1: one cluster reached by two contexts is one cluster", len(v.Rows)) + } + if v.Rows[0].State != StatePresent { + t.Errorf("state = %q, want %q: the context that answered wins over the one that did not", v.Rows[0].State, StatePresent) + } + if v.Clusters != 1 { + t.Errorf("clusters = %d, want 1; counting it twice inflates the headline number", v.Clusters) + } +} + +func TestTheFiveStatesStayDistinct(t *testing.T) { + v := Build("checkout", "shop", []Placement{ + present("a", "https://a", "qa", "v1.0.0", "sha256:aa"), + {Context: "b", ClusterID: "https://b", Env: "stg", State: StateAbsent}, + {Context: "c", ClusterID: "https://c", Env: "prod", State: StateDenied, Reason: "pods list refused"}, + {Context: "d", ClusterID: "https://d", Env: "prod", State: StateUnreachable, Reason: "dial timeout"}, + {Context: "e", ClusterID: "https://e", Env: "prod", State: StatePending}, + }) + if v.Clusters != 5 { + t.Fatalf("clusters = %d, want 5", v.Clusters) + } + if v.Present != 1 { + t.Errorf("present = %d, want 1", v.Present) + } + seen := map[string]bool{} + for _, r := range v.Rows { + seen[r.State] = true + } + for _, s := range []string{StatePresent, StateAbsent, StateDenied, StateUnreachable, StatePending} { + if !seen[s] { + t.Errorf("state %q was collapsed into another; absent, denied, and unreachable are different facts", s) + } + } +} + +func TestNoClusterRunsTheAppAtAll(t *testing.T) { + v := Build("checkout", "shop", []Placement{ + {Context: "a", ClusterID: "https://a", Env: "qa", State: StateAbsent}, + {Context: "b", ClusterID: "https://b", Env: "prod", State: StateAbsent}, + }) + if v.Newest != "" { + t.Errorf("newest = %q, want empty", v.Newest) + } + if v.Clusters != 2 || v.Present != 0 { + t.Errorf("clusters = %d present = %d, want 2 and 0: the screen names what it asked", v.Clusters, v.Present) + } +} From dc29396c7e4a686957ce72823388aa4ae76125d4 Mon Sep 17 00:00:00 2001 From: Elber Ribeiro Date: Mon, 3 Aug 2026 00:28:17 -0400 Subject: [PATCH 15/38] docs(fleet): the plan promised seven tests and listed six --- docs/superpowers/plans/2026-08-02-fleet.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/superpowers/plans/2026-08-02-fleet.md b/docs/superpowers/plans/2026-08-02-fleet.md index f53f076..6ae6f43 100644 --- a/docs/superpowers/plans/2026-08-02-fleet.md +++ b/docs/superpowers/plans/2026-08-02-fleet.md @@ -1168,7 +1168,7 @@ Confirm the module path in `go.mod` and correct the `promotion` import if it dif - [ ] **Step 4: Run test to verify it passes** Run: `go test ./internal/fleet/ -v` -Expected: PASS, all seven tests +Expected: PASS, all six tests - [ ] **Step 5: Commit** From 825b9e86f0ecd2b62b706e6c1ae70a649c35b245 Mon Sep 17 00:00:00 2001 From: Elber Ribeiro Date: Mon, 3 Aug 2026 00:42:52 -0400 Subject: [PATCH 16/38] fix(fleet): an unorderable tag must not become the yardstick CompareTags returns zero for a pair it refuses to order, and zero is not greater than zero, so an incumbent newest that nothing can be ordered against was never displaced. Placements [sha-x, v1.0.0, v2.0.0] answered "newest sha-x, nothing behind" while one cluster sat two minors back, and the same three in another order answered correctly. The headline moved with the caller's slice order. newest is now taken over orderable tags only, via a new promotion.Orderable that reuses the exact predicate CompareTags applies, so the two cannot drift. A build id is labelled in its own row and never counted behind. A present cluster with no tag is its own fact. A digest-pinned image is ordinary GitOps and TagOf returns empty for it; the empty tag then matched the empty newest and every row rendered as up to date, or shipped the literal note "runs , which cannot be ordered against v2.14.0". It now says the version is unknown and names the digest pin as the cause, and a tag that does not exist is never called mutable. Digests are grouped per tag rather than only under the newest one, so an older tag resolving to two digests is the defect it is. An incoming DigestPending is believed instead of overwritten, and View.DigestUnverified counts the present rows whose digest never arrived, so a headline reporting agreement can say how much of it rests on unread digests. A cluster nobody reached now sorts above a cluster whose older version you can see, matching the promotion matrix: neither agreement nor a defect can be claimed from clusters we never saw. An unrecognized state reads as pending and says so rather than rendering blank among the healthy rows. Merging builds a fresh alias slice instead of appending through the caller's backing array, and mergeByCluster now records the upstream invariant that ClusterID must come from the kubeconfig, not from a successful connection. Tests: newest independent of placement order, no newest when nothing is orderable, both empty-tag paths, a mutable tag on a non-newest tag, DigestPending respected and counted, alias contents across three merge shapes, every note string including reason passthrough and fallback, unreadable sorting above behind, and the unrecognized state. --- .../specs/2026-08-02-fleet-design.md | 8 + internal/fleet/fleet.go | 148 ++++++-- internal/fleet/fleet_test.go | 320 ++++++++++++++++++ internal/promotion/promotion.go | 11 + internal/promotion/promotion_test.go | 17 + 5 files changed, 477 insertions(+), 27 deletions(-) diff --git a/docs/superpowers/specs/2026-08-02-fleet-design.md b/docs/superpowers/specs/2026-08-02-fleet-design.md index 4154765..430500b 100644 --- a/docs/superpowers/specs/2026-08-02-fleet-design.md +++ b/docs/superpowers/specs/2026-08-02-fleet-design.md @@ -245,6 +245,14 @@ Further cases: state on the screen, ranked above behind. Two clusters claiming `v2.13.1` while running different code is worse than one cluster openly sitting on `v2.12.0`. +- A cluster nobody reached sorts above a cluster whose older version you can + see. The promotion matrix already decides this way: neither agreement nor a + defect can be claimed from clusters we never saw, so the version we could not + read might be worse than the old one we could. Both screens rank the same + facts by one rule. +- A present cluster whose image is pinned by digest has no tag to compare. It + reads as a version nobody could name, never as a cluster on the newest one, + and a tag that does not exist is never called mutable. - A 403 on the namespace list falls through the existing discovery chain and names the active mode on the row. - Expired SSO credentials reuse the inline prompt shipped in #37 rather than diff --git a/internal/fleet/fleet.go b/internal/fleet/fleet.go index 2a14b0d..ca983e9 100644 --- a/internal/fleet/fleet.go +++ b/internal/fleet/fleet.go @@ -55,8 +55,9 @@ type Placement struct { type Row struct { Placement Behind bool `json:"behind,omitempty"` - // MutableTag marks a cluster carrying the newest tag with a digest another - // cluster's copy of that tag does not share. + // MutableTag marks a cluster whose tag another cluster also runs under a + // different digest. It is not restricted to the newest tag: an older tag + // resolving to two digests is the same defect. MutableTag bool `json:"mutableTag,omitempty"` Note string `json:"note,omitempty"` } @@ -76,6 +77,11 @@ type View struct { // schedule, two clusters claiming one version while running different // code is a defect. MutableTag bool `json:"mutableTag,omitempty"` + // DigestUnverified counts present rows whose digest never arrived. A + // mutable tag cannot be ruled out from a cluster whose image nobody + // resolved, so a headline reporting agreement can say how much of that + // agreement rests on digests still in flight. + DigestUnverified int `json:"digestUnverified,omitempty"` } // Build derives the screen from one placement per context. @@ -84,13 +90,19 @@ func Build(app, namespace string, ps []Placement) View { v := View{App: app, Namespace: namespace, Clusters: len(merged)} + // The newest tag is picked from orderable tags only. A build id loses to + // nothing, because promotion.CompareTags refuses to order it, so letting + // it into the maximum would leave it standing as a yardstick every real + // version then "cannot be ordered against" — one odd tag hiding the whole + // fleet's drift, and doing it differently depending on the order the + // caller happened to pass the placements in. newest := "" for _, p := range merged { if p.State != StatePresent { continue } v.Present++ - if p.Tag == "" { + if !promotion.Orderable(p.Tag) { continue } if newest == "" || promotion.CompareTags(p.Tag, newest) > 0 { @@ -99,37 +111,76 @@ func Build(app, namespace string, ps []Placement) View { } v.Newest = newest - // A tag carrying two digests is worse than a cluster openly behind. - digests := map[string]bool{} + // A tag carrying two digests is worse than a cluster openly behind, and it + // is that whichever tag it happens to. An older tag resolving to two + // digests is the same defect as the newest one doing it, so every tag is + // grouped, not only the leader. An absent tag is grouped with nothing: a + // tag that does not exist cannot be mutable. + byTag := map[string]map[string]bool{} for _, p := range merged { - if p.State == StatePresent && p.Tag == newest && p.Digest != "" { - digests[p.Digest] = true + if p.State != StatePresent || p.Tag == "" || p.Digest == "" { + continue + } + if byTag[p.Tag] == nil { + byTag[p.Tag] = map[string]bool{} + } + byTag[p.Tag][p.Digest] = true + } + mutable := map[string]bool{} + for tag, digests := range byTag { + if len(digests) > 1 { + mutable[tag] = true + v.MutableTag = true } } - v.MutableTag = len(digests) > 1 v.Rows = make([]Row, 0, len(merged)) for _, p := range merged { r := Row{Placement: p} switch p.State { case StatePresent: - r.DigestPending = p.Digest == "" - if p.Tag == newest { - if v.MutableTag && p.Digest != "" { - r.MutableTag = true - r.Note = fmt.Sprintf("runs %s, and so does another cluster with a different digest", newest) - } - break + // A caller that already knows the digest is in flight is believed; + // a row with no digest is pending whether or not it said so. + r.DigestPending = p.DigestPending || p.Digest == "" + if r.DigestPending { + v.DigestUnverified++ } - // Behind only when the tags can be ordered. Two build ids differ - // without one being older, and calling either behind would invent - // a direction. - if p.Tag != "" && newest != "" && promotion.CompareTags(p.Tag, newest) < 0 { + r.MutableTag = p.Digest != "" && mutable[p.Tag] + + verdict := "" + switch { + case p.Tag == "": + // An image pinned by digest carries no tag to compare, and + // pretending it matches the newest would render the deepest + // unknown on the screen as the calmest row on it. + verdict = "version unknown: the image is pinned by digest, so there is no tag to compare" + case p.Tag == newest: + // On the newest version anyone could name. + case promotion.CompareTags(p.Tag, newest) < 0: r.Behind = true v.Behind++ - r.Note = fmt.Sprintf("behind %s", newest) - } else if p.Tag != newest { - r.Note = fmt.Sprintf("runs %s, which cannot be ordered against %s", p.Tag, newest) + verdict = fmt.Sprintf("behind %s", newest) + case !promotion.Orderable(p.Tag): + // Two build ids differ without one being older, and calling + // either behind would invent a direction. + if newest == "" { + verdict = fmt.Sprintf("runs %s, which cannot be ordered against any other version here", p.Tag) + } else { + verdict = fmt.Sprintf("runs %s, which cannot be ordered against %s", p.Tag, newest) + } + default: + // Orderable, not behind, yet not the newest string: the same + // version spelled differently, such as v1.0 beside v1.0.0. + verdict = fmt.Sprintf("runs %s, the same version as %s", p.Tag, newest) + } + + switch { + case r.MutableTag && verdict != "": + r.Note = fmt.Sprintf("%s, and another cluster runs %s with a different digest", verdict, p.Tag) + case r.MutableTag: + r.Note = fmt.Sprintf("runs %s, and so does another cluster with a different digest", p.Tag) + default: + r.Note = verdict } case StateUnreachable: r.Note = orElse(p.Reason, "the cluster did not answer") @@ -139,6 +190,12 @@ func Build(app, namespace string, ps []Placement) View { r.Note = "not deployed here" case StatePending: r.Note = "asking" + default: + // A state nobody set is a question nobody answered. Rendering it + // as a blank row would put an unknown cluster among the healthy + // ones, so it reads as pending and says why. + r.State = StatePending + r.Note = "state not reported; treated as not yet answered" } v.Rows = append(v.Rows, r) } @@ -155,15 +212,33 @@ func Build(app, namespace string, ps []Placement) View { // severity sorts disagreement to the top, matching the promotion matrix's // default of showing what needs attention first. +// +// A cluster nobody reached outranks a cluster whose older version you can see. +// The promotion matrix decides the same way for the same reason: "neither +// agreement nor a defect can be claimed from clusters we never saw", so the +// version we could not read might be worse than the old one we could. The two +// screens rank the same facts by one rule. +// +// The same rule orders the rest: the less a cluster told us, the higher it +// sits. A denied read says nothing about the app, so it outranks a present +// cluster whose version could not be named, which at least confirmed the app +// runs there. +// +// Absent shares the bottom bucket with a cluster on the newest version. That is +// deliberate: an app not deployed somewhere is a schedule, not a disagreement, +// so "not deployed here" rows interleave alphabetically with the healthy ones +// rather than climbing the screen. The states stay distinct in the data. func severity(r Row) int { switch { case r.MutableTag: + return 6 + case r.State == StateUnreachable: return 5 case r.Behind: return 4 - case r.State == StateUnreachable: - return 3 case r.State == StateDenied: + return 3 + case r.State == StatePresent && r.Tag == "": return 2 case r.State == StatePending: return 1 @@ -178,6 +253,16 @@ func severity(r Row) int { // of the two may carry credentials that no longer work. Counting it twice // inflates every number on the screen, so the most informative answer wins and // the other context is recorded as an alias. +// +// A placement with no ClusterID keys on its context name instead, so each +// unidentified context keeps its own row. Sharing one empty key would merge +// every unidentified cluster into a single row and delete clusters outright, +// which is the worse error for a package whose job is to lose none. This puts +// an invariant on whoever builds the placements: ClusterID must come from the +// kubeconfig cluster entry, not from a successful connection. Populate it from +// a connection and an unreachable duplicate context arrives with an empty +// ClusterID, fails to merge with its reachable twin, and inflates Clusters in +// exactly the case this merge exists to prevent. func mergeByCluster(ps []Placement) []Placement { at := map[string]int{} out := make([]Placement, 0, len(ps)) @@ -192,17 +277,26 @@ func mergeByCluster(ps []Placement) []Placement { out = append(out, p) continue } + // Aliases are rebuilt into a fresh slice on both paths. Appending to + // the caller's slice would write through the backing array it still + // holds. if informativeness(p.State) > informativeness(out[i].State) { - p.Aliases = append(p.Aliases, out[i].Context) - p.Aliases = append(p.Aliases, out[i].Aliases...) + p.Aliases = appendAliases(p.Aliases, append([]string{out[i].Context}, out[i].Aliases...)...) out[i] = p continue } - out[i].Aliases = append(out[i].Aliases, p.Context) + out[i].Aliases = appendAliases(out[i].Aliases, p.Context) } return out } +// appendAliases returns a new slice, never extending the one passed in. +func appendAliases(existing []string, add ...string) []string { + out := make([]string, 0, len(existing)+len(add)) + out = append(out, existing...) + return append(out, add...) +} + // informativeness ranks what a cluster told us. Present beats absent, because // a version is more than a confirmed nothing. Absent beats denied and // unreachable, because the cluster answered at all. diff --git a/internal/fleet/fleet_test.go b/internal/fleet/fleet_test.go index dd7a283..71dd356 100644 --- a/internal/fleet/fleet_test.go +++ b/internal/fleet/fleet_test.go @@ -118,3 +118,323 @@ func TestNoClusterRunsTheAppAtAll(t *testing.T) { t.Errorf("clusters = %d present = %d, want 2 and 0: the screen names what it asked", v.Clusters, v.Present) } } + +// C1: an unorderable tag must never become the yardstick, whatever order the +// caller hands the placements over in. +func TestOneUnorderableTagDoesNotSilenceTheDrift(t *testing.T) { + orders := map[string][]Placement{ + "build id first": { + present("a", "https://a", "qa", "sha-x", "sha256:aa"), + present("b", "https://b", "stg", "v1.0.0", "sha256:bb"), + present("c", "https://c", "prod", "v2.0.0", "sha256:cc"), + }, + "build id last": { + present("b", "https://b", "stg", "v1.0.0", "sha256:bb"), + present("c", "https://c", "prod", "v2.0.0", "sha256:cc"), + present("a", "https://a", "qa", "sha-x", "sha256:aa"), + }, + } + for name, ps := range orders { + t.Run(name, func(t *testing.T) { + v := Build("checkout", "shop", ps) + if v.Newest != "v2.0.0" { + t.Errorf("newest = %q, want v2.0.0: a tag nothing can be ordered against is not the newest version", v.Newest) + } + if v.Behind != 1 { + t.Errorf("behind = %d, want 1: v1.0.0 is behind v2.0.0 whether or not a build id is in the fleet", v.Behind) + } + if !rowFor(t, v, "b").Behind { + t.Error("b runs v1.0.0 while c runs v2.0.0 and is behind") + } + if rowFor(t, v, "a").Behind { + t.Error("a build id is not behind anything; claiming it is invents a direction") + } + if got, want := rowFor(t, v, "a").Note, "runs sha-x, which cannot be ordered against v2.0.0"; got != want { + t.Errorf("note = %q, want %q", got, want) + } + if got, want := rowFor(t, v, "b").Note, "behind v2.0.0"; got != want { + t.Errorf("note = %q, want %q", got, want) + } + if got := rowFor(t, v, "c").Note; got != "" { + t.Errorf("note = %q, want empty: c runs the newest version", got) + } + }) + } +} + +// C1: with nothing orderable anywhere, there is no newest and no row claims to +// be on it. +func TestAllBuildIdsLeaveNoNewestAndNoBehind(t *testing.T) { + v := Build("checkout", "shop", []Placement{ + present("a", "https://a", "qa", "sha-abc123", "sha256:aa"), + present("b", "https://b", "qa", "sha-def456", "sha256:bb"), + }) + if v.Newest != "" { + t.Errorf("newest = %q, want empty: neither build id can be ordered against the other", v.Newest) + } + if v.Behind != 0 { + t.Errorf("behind = %d, want 0", v.Behind) + } + if got, want := rowFor(t, v, "a").Note, "runs sha-abc123, which cannot be ordered against any other version here"; got != want { + t.Errorf("note = %q, want %q", got, want) + } +} + +// C2a: a fleet of digest-pinned images knows nothing, and must not render as a +// fleet that agrees. +func TestPresentWithoutATagIsNotUpToDate(t *testing.T) { + untagged := func(ctx, server, digest string) Placement { + return Placement{ + Context: ctx, ClusterID: server, Env: "prod", Namespace: "shop", + State: StatePresent, Image: "reg/checkout@" + digest, Digest: digest, + Health: "healthy", Ready: "3/3", + } + } + v := Build("checkout", "shop", []Placement{ + untagged("a", "https://a", "sha256:aa"), + untagged("b", "https://b", "sha256:bb"), + }) + if v.MutableTag { + t.Error("mutableTag = true, want false: there is no tag to call mutable") + } + if v.Behind != 0 { + t.Errorf("behind = %d, want 0", v.Behind) + } + want := "version unknown: the image is pinned by digest, so there is no tag to compare" + for _, ctx := range []string{"a", "b"} { + r := rowFor(t, v, ctx) + if r.Note != want { + t.Errorf("%s note = %q, want %q", ctx, r.Note, want) + } + if r.MutableTag { + t.Errorf("%s mutableTag = true, want false", ctx) + } + } + if severity(v.Rows[0]) == 0 { + t.Error("a cluster whose version nobody could name sorts with the up-to-date clusters") + } +} + +// C2b: the untagged row's note must not ship an empty version into prose. +func TestUntaggedClusterBesideAKnownNewest(t *testing.T) { + v := Build("checkout", "shop", []Placement{ + present("a", "https://a", "prod", "v2.14.0", "sha256:aa"), + {Context: "b", ClusterID: "https://b", Env: "prod", Namespace: "shop", + State: StatePresent, Image: "reg/checkout@sha256:bb", Digest: "sha256:bb"}, + }) + if v.Newest != "v2.14.0" { + t.Errorf("newest = %q, want v2.14.0", v.Newest) + } + got := rowFor(t, v, "b").Note + if got == "runs , which cannot be ordered against v2.14.0" { + t.Fatalf("note = %q: an empty tag reached the UI as prose", got) + } + if want := "version unknown: the image is pinned by digest, so there is no tag to compare"; got != want { + t.Errorf("note = %q, want %q", got, want) + } + if rowFor(t, v, "b").Behind { + t.Error("a version nobody could read is not a version behind") + } +} + +// I1: a mutable tag is a defect wherever it happens, not only on the newest tag. +func TestMutableTagOnAnOlderTagIsStillADefect(t *testing.T) { + v := Build("checkout", "shop", []Placement{ + present("a", "https://a", "prod", "v2.12.0", "sha256:aa"), + present("b", "https://b", "prod", "v2.12.0", "sha256:zz"), + present("c", "https://c", "prod", "v2.12.0", "sha256:aa"), + present("d", "https://d", "qa", "v2.14.0", "sha256:dd"), + }) + if !v.MutableTag { + t.Fatal("one tag resolving to two digests is a defect on v2.12.0 as much as on the newest tag") + } + for _, ctx := range []string{"a", "b", "c"} { + if !rowFor(t, v, ctx).MutableTag { + t.Errorf("%s carries the conflicting tag and is not marked", ctx) + } + } + if rowFor(t, v, "d").MutableTag { + t.Error("d runs a tag with one digest and must not be marked") + } + if got, want := rowFor(t, v, "a").Note, "behind v2.14.0, and another cluster runs v2.12.0 with a different digest"; got != want { + t.Errorf("note = %q, want %q", got, want) + } + if v.Rows[0].Context == "d" { + t.Error("the newest cluster sorts above a mutable tag; the defect is the louder fact") + } +} + +// I2: a caller-supplied DigestPending survives, and the view says how much of +// the answer rests on digests that never arrived. +func TestUnverifiedDigestsAreCountedAndNotOverwritten(t *testing.T) { + claimed := present("a", "https://a", "prod", "v2.13.1", "sha256:aa") + claimed.DigestPending = true + v := Build("checkout", "shop", []Placement{ + claimed, + present("b", "https://b", "prod", "v2.13.1", ""), + present("c", "https://c", "prod", "v2.13.1", "sha256:cc"), + {Context: "d", ClusterID: "https://d", Env: "prod", State: StateAbsent}, + }) + if !rowFor(t, v, "a").DigestPending { + t.Error("a caller that says the digest is still in flight is ignored") + } + if !rowFor(t, v, "b").DigestPending { + t.Error("a present cluster with no digest is pending") + } + if rowFor(t, v, "c").DigestPending { + t.Error("c reported a digest and is not pending") + } + if v.DigestUnverified != 2 { + t.Errorf("digestUnverified = %d, want 2: the headline rests on two digests nobody confirmed", v.DigestUnverified) + } +} + +// I3: a merged row says what it merged, through every reshuffle. +func TestMergedRowNamesEveryContextItMerged(t *testing.T) { + t.Run("winner holds", func(t *testing.T) { + v := Build("checkout", "shop", []Placement{ + {Context: "prod-alias", ClusterID: "https://a", Env: "prod", State: StateUnreachable, Reason: "credentials expired"}, + present("prod-us-east", "https://a", "prod", "v2.13.1", "sha256:bb"), + }) + assertAliases(t, v.Rows[0], "prod-alias") + }) + t.Run("three contexts, one cluster", func(t *testing.T) { + v := Build("checkout", "shop", []Placement{ + {Context: "a", ClusterID: "https://a", Env: "prod", State: StateUnreachable}, + present("b", "https://a", "prod", "v2.13.1", "sha256:bb"), + {Context: "c", ClusterID: "https://a", Env: "prod", State: StateDenied}, + }) + if len(v.Rows) != 1 { + t.Fatalf("rows = %d, want 1", len(v.Rows)) + } + if v.Rows[0].Context != "b" { + t.Errorf("context = %q, want b: the context that answered wins", v.Rows[0].Context) + } + assertAliases(t, v.Rows[0], "a", "c") + }) + t.Run("winner changes twice", func(t *testing.T) { + v := Build("checkout", "shop", []Placement{ + {Context: "a", ClusterID: "https://a", Env: "prod", State: StateUnreachable}, + {Context: "b", ClusterID: "https://a", Env: "prod", State: StateDenied}, + present("c", "https://a", "prod", "v2.13.1", "sha256:bb"), + }) + if len(v.Rows) != 1 { + t.Fatalf("rows = %d, want 1", len(v.Rows)) + } + if v.Rows[0].Context != "c" { + t.Errorf("context = %q, want c", v.Rows[0].Context) + } + assertAliases(t, v.Rows[0], "b", "a") + }) +} + +func assertAliases(t *testing.T, r Row, want ...string) { + t.Helper() + if len(r.Aliases) != len(want) { + t.Fatalf("aliases = %v, want %v: a merged row says what it merged", r.Aliases, want) + } + for i, w := range want { + if r.Aliases[i] != w { + t.Errorf("aliases = %v, want %v", r.Aliases, want) + return + } + } +} + +// M5: merging must not write through the caller's slice. +func TestMergeDoesNotWriteThroughTheCallersAliasArray(t *testing.T) { + backing := make([]string, 1, 4) + backing[0] = "seed" + winner := present("winner", "https://a", "prod", "v1.0.0", "sha256:aa") + winner.Aliases = backing + + Build("checkout", "shop", []Placement{ + {Context: "loser", ClusterID: "https://a", Env: "prod", State: StateUnreachable}, + winner, + }) + + if grown := backing[:cap(backing)]; grown[1] != "" { + t.Errorf("Build wrote %q into the caller's alias array", grown[1]) + } +} + +// I4: every sentence the screen shows is a promise, so every sentence is tested. +func TestEveryStateNamesItselfInTheNote(t *testing.T) { + v := Build("checkout", "shop", []Placement{ + present("a", "https://a", "qa", "v1.0.0", "sha256:aa"), + {Context: "b", ClusterID: "https://b", Env: "stg", State: StateAbsent}, + {Context: "c", ClusterID: "https://c", Env: "prod", State: StateDenied, Reason: "pods list refused"}, + {Context: "d", ClusterID: "https://d", Env: "prod", State: StateUnreachable, Reason: "dial timeout"}, + {Context: "e", ClusterID: "https://e", Env: "prod", State: StatePending}, + }) + want := map[string]string{ + "a": "", + "b": "not deployed here", + "c": "pods list refused", + "d": "dial timeout", + "e": "asking", + } + for ctx, w := range want { + if got := rowFor(t, v, ctx).Note; got != w { + t.Errorf("%s note = %q, want %q", ctx, got, w) + } + } +} + +// I4: with no reason given, the note still says something true. +func TestNotesFallBackWhenNoReasonArrives(t *testing.T) { + v := Build("checkout", "shop", []Placement{ + {Context: "c", ClusterID: "https://c", Env: "prod", State: StateDenied}, + {Context: "d", ClusterID: "https://d", Env: "prod", State: StateUnreachable}, + }) + if got, want := rowFor(t, v, "c").Note, "not readable here"; got != want { + t.Errorf("denied note = %q, want %q", got, want) + } + if got, want := rowFor(t, v, "d").Note, "the cluster did not answer"; got != want { + t.Errorf("unreachable note = %q, want %q", got, want) + } +} + +// M4: a cluster nobody read outranks one whose older version you can see. The +// promotion matrix takes the same position for the same reason. +func TestUnreadableSortsAboveBehind(t *testing.T) { + v := Build("checkout", "shop", []Placement{ + present("behind", "https://a", "prod", "v1.0.0", "sha256:aa"), + {Context: "silent", ClusterID: "https://b", Env: "prod", State: StateUnreachable, Reason: "dial timeout"}, + present("newest", "https://c", "prod", "v2.0.0", "sha256:cc"), + }) + if v.Rows[0].Context != "silent" { + t.Errorf("first row = %q, want silent: the version we could not read might be worse than the old one we could", v.Rows[0].Context) + } + if v.Rows[1].Context != "behind" { + t.Errorf("second row = %q, want behind", v.Rows[1].Context) + } +} + +// M2: a placement whose state nobody set is unknown, not healthy. +func TestUnrecognizedStateIsNotRenderedAsHealthy(t *testing.T) { + v := Build("checkout", "shop", []Placement{ + present("a", "https://a", "prod", "v1.0.0", "sha256:aa"), + {Context: "b", ClusterID: "https://b", Env: "prod"}, + }) + r := rowFor(t, v, "b") + if r.Note == "" { + t.Error("a row with no state renders blank beside a healthy one") + } + if r.State != StatePending { + t.Errorf("state = %q, want %q: an unrecognized state is one nobody has answered yet", r.State, StatePending) + } + if v.Present != 1 { + t.Errorf("present = %d, want 1", v.Present) + } +} + +func TestNoPlacementsAtAll(t *testing.T) { + v := Build("checkout", "shop", nil) + if v.Rows == nil { + t.Error("rows is nil and marshals as null; an empty fleet is an empty list") + } + if v.Clusters != 0 || v.Present != 0 || v.Newest != "" { + t.Errorf("clusters = %d present = %d newest = %q, want 0, 0, empty", v.Clusters, v.Present, v.Newest) + } +} diff --git a/internal/promotion/promotion.go b/internal/promotion/promotion.go index 2f6a8b0..b20e8b5 100644 --- a/internal/promotion/promotion.go +++ b/internal/promotion/promotion.go @@ -541,6 +541,17 @@ func CompareTags(a, b string) int { return 0 } +// Orderable reports whether a tag can take part in an ordering at all. +// +// CompareTags returns zero both for two tags that are equal and for a pair it +// refuses to order, so zero alone cannot tell those apart. A caller picking a +// maximum needs the difference: an unorderable tag that wins the maximum by +// never losing would become a yardstick nothing can be measured against. +func Orderable(tag string) bool { + _, ok := versionParts(tag) + return ok +} + func versionParts(tag string) ([]int, bool) { if !versionLike.MatchString(tag) { return nil, false diff --git a/internal/promotion/promotion_test.go b/internal/promotion/promotion_test.go index 546e32e..933a32f 100644 --- a/internal/promotion/promotion_test.go +++ b/internal/promotion/promotion_test.go @@ -256,6 +256,23 @@ func TestCompareTagsIsExported(t *testing.T) { } } +func TestOrderableSeparatesEqualFromIncomparable(t *testing.T) { + // CompareTags returns zero for both, which is why Orderable exists. + if CompareTags("v1.0.0", "v1.0.0") != 0 || CompareTags("sha-abc", "sha-def") != 0 { + t.Fatal("CompareTags no longer returns zero for equal and for incomparable alike") + } + for _, tag := range []string{"v1.0.0", "1.0", "2.14.0-rc1", "3"} { + if !Orderable(tag) { + t.Errorf("Orderable(%q) = false, want true", tag) + } + } + for _, tag := range []string{"sha-abc123", "latest", "main", ""} { + if Orderable(tag) { + t.Errorf("Orderable(%q) = true, want false", tag) + } + } +} + func TestStripEnvTokenIsExported(t *testing.T) { if got := StripEnvToken("team-a-prod"); got != "team-a" { t.Errorf("StripEnvToken = %q, want team-a", got) From 24e62d3ff2573ef48d5cd278b32f4901643a5d6f Mon Sep 17 00:00:00 2001 From: Elber Ribeiro Date: Mon, 3 Aug 2026 00:53:28 -0400 Subject: [PATCH 17/38] feat(fleet): gather one app from every context Walks every kubeconfig context, connects lazily, and matches by the same identity the promotion matrix uses. A cluster that will not answer becomes an unreachable row naming the reason, never an omitted one, because a missing row reads as "not deployed there". Deduping keys off the API server URL, which config already normalizes, read before any connection attempt so an unreachable duplicate context still merges with its reachable twin. Replaces the cosmetic denied flag on the shared test fixture, which took the identical branch as unreachable, with refusedKinds: a fake clientset that connects but refuses specific List calls, the only way to exercise a genuine RBAC-denied row rather than an absent one. Closes #65 --- internal/api/fleet.go | 122 ++++++++++++++++++++++++++ internal/api/fleet_test.go | 165 +++++++++++++++++++++++++++++++++++ internal/api/service_test.go | 73 ++++++++++++---- internal/config/config.go | 8 +- 4 files changed, 346 insertions(+), 22 deletions(-) create mode 100644 internal/api/fleet.go create mode 100644 internal/api/fleet_test.go diff --git a/internal/api/fleet.go b/internal/api/fleet.go new file mode 100644 index 0000000..4a00927 --- /dev/null +++ b/internal/api/fleet.go @@ -0,0 +1,122 @@ +package api + +import ( + "context" + "strings" + + "github.com/dynaum/kubeside/internal/apps" + "github.com/dynaum/kubeside/internal/clusters" + "github.com/dynaum/kubeside/internal/config" + "github.com/dynaum/kubeside/internal/fleet" + "github.com/dynaum/kubeside/internal/promotion" +) + +// Fleet answers "is every cluster running the latest version" for one app. +// +// Opening this view states the intent to ask every cluster, so unlike the app +// list it deliberately wakes every context in the kubeconfig. Rows land as +// clusters answer and a cluster behind a VPN never blocks the rest. +func (s *Service) Fleet(app, namespace string) fleet.View { + ctx, cancel := context.WithTimeout(context.Background(), s.timeout) + defer cancel() + + want := promotion.Identity(app, namespace) + var placements []fleet.Placement + + for _, name := range s.mgr.ConnectOrder() { + kctx := s.cfg.MustGet(name) + env := s.conf.Environment(kctx) + + // ClusterID is read from the kubeconfig cluster entry before any + // connection attempt, on every path including the failure paths + // below. fleet.mergeByCluster keys on this to collapse two contexts + // aimed at one cluster; populating it from a successful connection + // instead would leave an unreachable duplicate with an empty + // ClusterID, which fails to merge with its reachable twin and + // inflates Clusters in exactly the case the merge exists to prevent. + p := fleet.Placement{ + Context: name, + ClusterID: config.NormalizeURL(kctx.Server), + Env: env.Name, + } + + if err := s.mgr.Connect(ctx, name); err != nil { + p.State = fleet.StateUnreachable + p.Reason = err.Error() + placements = append(placements, p) + continue + } + client, ok := s.mgr.ClientFor(name) + if !ok { + p.State = fleet.StateUnreachable + p.Reason = "not connected" + placements = append(placements, p) + continue + } + + snap, err := clusters.Fetch(ctx, client, kctx, clusters.FetchOptions{Tier: s.mgr.Tier(name)}) + if err != nil { + p.State = fleet.StateUnreachable + p.Reason = err.Error() + placements = append(placements, p) + continue + } + + match, found := findApp(snap.Apps, want) + if !found { + // Not in the snapshot has two causes and they are not the same + // fact. clusters.Fetch never errors on an RBAC refusal: it records + // the kinds it could not read in Snapshot.Partial and returns what + // it got. So an app missing from a snapshot with refused kinds is + // an app we were not allowed to look for, and calling that "not + // deployed here" is the conflation this screen exists to prevent. + // internal/api/service.go:196 already draws the same line for the + // apps list. + if len(snap.Partial) > 0 || (len(snap.Scope.Namespaces) == 0 && !snap.Scope.ClusterWide) { + p.State = fleet.StateDenied + p.Reason = refusedReason(snap) + } else { + p.State = fleet.StateAbsent + } + placements = append(placements, p) + continue + } + + in := instanceOf(env.Name, match) + p.State = fleet.StatePresent + p.Namespace = in.Namespace + p.Image = in.Image + p.Tag = in.Tag + p.Digest = in.Digest + p.DigestPending = in.Digest == "" + p.Health = in.Health + p.Ready = in.Ready + p.RevisionAt = in.RevisionAt + placements = append(placements, p) + } + + return fleet.Build(app, namespace, placements) +} + +// findApp matches by the same identity the promotion matrix uses, so one app +// is one app on both screens. +func findApp(list []apps.App, want string) (apps.App, bool) { + for _, a := range list { + if promotion.Identity(a.Key.Name, a.Key.Namespace) == want { + return a, true + } + } + return apps.App{}, false +} + +// refusedReason names what the cluster would not let us read, so a denied row +// says which verb to ask for rather than just refusing to answer. +func refusedReason(snap clusters.Snapshot) string { + if len(snap.Partial) > 0 { + return "could not read " + strings.Join(snap.Partial, ", ") + } + if snap.Scope.Reason != "" { + return snap.Scope.Reason + } + return "no readable namespace" +} diff --git a/internal/api/fleet_test.go b/internal/api/fleet_test.go new file mode 100644 index 0000000..d3e3df5 --- /dev/null +++ b/internal/api/fleet_test.go @@ -0,0 +1,165 @@ +package api + +import ( + "strings" + "testing" + + "github.com/dynaum/kubeside/internal/fleet" +) + +// TestFleetAsksEveryContext proves the basic shape: one row per cluster, the +// newest tag named, and every cluster behind it counted. +func TestFleetAsksEveryContext(t *testing.T) { + s := serviceWithContexts(t, map[string]fakeCluster{ + "qa-cluster": {env: "qa", apps: []fakeApp{{name: "checkout", ns: "shop", image: "reg/checkout:v2.14.0"}}}, + "prod-us-east": {env: "prod", apps: []fakeApp{{name: "checkout", ns: "shop", image: "reg/checkout:v2.13.1"}}}, + "prod-eu-west": {env: "prod", apps: []fakeApp{{name: "checkout", ns: "shop", image: "reg/checkout:v2.12.0"}}}, + }) + + v := s.Fleet("checkout", "shop") + + if v.Clusters != 3 { + t.Fatalf("clusters = %d, want 3", v.Clusters) + } + if v.Newest != "v2.14.0" { + t.Errorf("newest = %q, want v2.14.0", v.Newest) + } + if v.Behind != 2 { + t.Errorf("behind = %d, want 2", v.Behind) + } +} + +// TestFleetNamesAnUnreachableClusterRatherThanOmittingIt proves a cluster that +// never answered still gets a row: dropping it would read as "not deployed +// there", which is a different fact from "we could not ask". +func TestFleetNamesAnUnreachableClusterRatherThanOmittingIt(t *testing.T) { + s := serviceWithContexts(t, map[string]fakeCluster{ + "qa-cluster": {env: "qa", apps: []fakeApp{{name: "checkout", ns: "shop", image: "reg/checkout:v2.14.0"}}}, + "prod-eu-west": {env: "prod", unreachable: true}, + }) + + v := s.Fleet("checkout", "shop") + + if v.Clusters != 2 { + t.Fatalf("clusters = %d, want 2: a cluster that did not answer is still a cluster we asked", v.Clusters) + } + var found bool + for _, r := range v.Rows { + if r.Context == "prod-eu-west" { + found = true + if r.State != fleet.StateUnreachable { + t.Errorf("state = %q, want %q", r.State, fleet.StateUnreachable) + } + if r.Note == "" { + t.Error("an unreachable cluster names why") + } + } + } + if !found { + t.Error("prod-eu-west was omitted; a blank row would read as 'not deployed there'") + } +} + +// TestFleetMatchesAcrossEnvSuffixedNamespaces proves the fleet view matches +// by the same identity the promotion matrix uses, so team-a-qa and +// team-a-prod are one app in two places rather than two different apps. +func TestFleetMatchesAcrossEnvSuffixedNamespaces(t *testing.T) { + s := serviceWithContexts(t, map[string]fakeCluster{ + "qa-cluster": {env: "qa", apps: []fakeApp{{name: "checkout", ns: "team-a-qa", image: "reg/checkout:v2.14.0"}}}, + "prod-cluster": {env: "prod", apps: []fakeApp{{name: "checkout", ns: "team-a-prod", image: "reg/checkout:v2.13.1"}}}, + }) + + v := s.Fleet("checkout", "team-a-qa") + + if v.Present != 2 { + t.Errorf("present = %d, want 2: team-a-qa and team-a-prod are one app in two places", v.Present) + } +} + +// TestARefusedReadIsDeniedNotAbsent proves the line the brief draws: clusters.Fetch +// never returns an error for an RBAC refusal, it records the kind in +// Snapshot.Partial. An app missing from that snapshot was never looked for, so +// calling it absent would be a lie. +func TestARefusedReadIsDeniedNotAbsent(t *testing.T) { + s := serviceWithContexts(t, map[string]fakeCluster{ + "qa-cluster": {env: "qa", apps: []fakeApp{{name: "checkout", ns: "shop", image: "reg/checkout:v2.14.0"}}}, + "prod-cluster": {env: "prod", refusedKinds: []string{"Deployment"}}, + }) + + v := s.Fleet("checkout", "shop") + + for _, r := range v.Rows { + if r.Context != "prod-cluster" { + continue + } + if r.State != fleet.StateDenied { + t.Fatalf("state = %q, want %q: we were not allowed to look, which is not the same as it not being there", r.State, fleet.StateDenied) + } + if !strings.Contains(r.Note, "Deployment") { + t.Errorf("note = %q, want it to name the kind that was refused", r.Note) + } + } +} + +// TestFleetFoundNowhereStillNamesWhatItAsked proves a typo'd or genuinely +// absent app still renders one row per cluster asked, never an empty screen: +// never render an unknown window as an empty one. +func TestFleetFoundNowhereStillNamesWhatItAsked(t *testing.T) { + s := serviceWithContexts(t, map[string]fakeCluster{ + "qa-cluster": {env: "qa"}, + "prod-cluster": {env: "prod"}, + }) + + v := s.Fleet("typo", "shop") + + if v.Clusters != 2 { + t.Errorf("clusters = %d, want 2: never render an unknown window as an empty one", v.Clusters) + } + if len(v.Rows) != 2 { + t.Errorf("rows = %d, want 2", len(v.Rows)) + } +} + +// TestFleetMergesAnUnreachableDuplicateWithItsReachableTwin pins the invariant +// fleet.mergeByCluster documents: ClusterID must come from the kubeconfig +// cluster entry, read before any connection attempt, on every path including +// the failure paths. Populate it from a successful connection instead and an +// unreachable duplicate context arrives with an empty ClusterID, fails to +// merge with its reachable twin, and inflates Clusters in exactly the case +// the merge exists to prevent. +func TestFleetMergesAnUnreachableDuplicateWithItsReachableTwin(t *testing.T) { + s := serviceWithContexts(t, map[string]fakeCluster{ + "prod-primary": {env: "prod", apps: []fakeApp{{name: "checkout", ns: "shop", image: "reg/checkout:v2.14.0"}}}, + "prod-stale": {env: "prod", unreachable: true}, + }) + // Both contexts in serviceWithContexts get distinct servers by + // construction ("https://" + name), so point the stale one at the same + // server the primary uses to simulate one cluster reachable under two + // kubeconfig entries. + for i := range s.cfg.Contexts { + if s.cfg.Contexts[i].Name == "prod-stale" { + s.cfg.Contexts[i].Server = "https://prod-primary" + } + } + + v := s.Fleet("checkout", "shop") + + if v.Clusters != 1 { + t.Fatalf("clusters = %d, want 1: an unreachable duplicate context on the same server must merge with its reachable twin", v.Clusters) + } + if len(v.Rows) != 1 { + t.Fatalf("rows = %d, want 1", len(v.Rows)) + } + if v.Rows[0].State != fleet.StatePresent { + t.Errorf("state = %q, want %q: the reachable twin's answer must win the merge", v.Rows[0].State, fleet.StatePresent) + } + found := false + for _, a := range v.Rows[0].Aliases { + if a == "prod-stale" { + found = true + } + } + if !found { + t.Errorf("aliases = %v, want prod-stale recorded as an alias of the merged row", v.Rows[0].Aliases) + } +} diff --git a/internal/api/service_test.go b/internal/api/service_test.go index 1d64e8f..1a74abf 100644 --- a/internal/api/service_test.go +++ b/internal/api/service_test.go @@ -17,11 +17,14 @@ import ( "github.com/dynaum/kubeside/internal/timeline" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/fake" + ktesting "k8s.io/client-go/testing" ) // fakeApp is one workload to seed into a fake cluster: a Deployment running @@ -35,17 +38,19 @@ type fakeApp struct { type fakeCluster struct { env string apps []fakeApp - // unreachable fails the connection itself, the way an expired token or an - // off-VPN cluster does. + // unreachable fails the connection itself, the way an expired token, a + // refused credential, or an off-VPN cluster does. clusters.Fetch has no + // path that returns a hard error, so every connect-time failure takes + // this one branch in Service.Promotion() and Service.Fleet() regardless + // of which of those causes produced it; the distinction between them is + // not one either method can observe. unreachable bool - // denied also fails at NewClient today, with a different error string - // only; clusters.Fetch has no path that returns a hard error, so this - // flag takes the identical branch as unreachable in Service.Promotion() - // and the distinction is cosmetic. A genuine "connected, then refused" - // fixture needs a clientset whose list calls return - // apierrors.NewForbidden, which lands in Snapshot.Partial rather than an - // error — that fixture belongs to whichever task first needs it. - denied bool + // refusedKinds fails List for exactly these kinds, once connected, the + // way a namespace-scoped role refuses a cluster-scoped read. Unlike + // unreachable, clusters.Fetch has no error path for this: the refused + // kinds land in Snapshot.Partial and Fetch returns a nil error, which is + // what lets a caller tell "denied" apart from "absent". + refusedKinds []string } // serviceWithContexts builds a Service over several kubeconfig contexts, each @@ -98,13 +103,10 @@ func serviceWithContexts(t *testing.T, byContext map[string]fakeCluster) *Servic connector := clusters.KubeConnector{ NewClient: func(kctx kubeconfig.Context, _ kubeconfig.Options) (kubernetes.Interface, error) { fc := byContext[kctx.Name] - switch { - case fc.unreachable: + if fc.unreachable { return nil, fmt.Errorf("dial %s: connection refused", kctx.Name) - case fc.denied: - return nil, fmt.Errorf("forbidden: %s refuses this identity", kctx.Name) } - return fakeClientFor(fc.apps), nil + return fakeClientFor(fc.apps, fc.refusedKinds), nil }, } @@ -117,7 +119,13 @@ func serviceWithContexts(t *testing.T, byContext map[string]fakeCluster) *Servic // fakeClientFor builds a fake clientset with one Deployment per app, mirroring // clusterWithApp: a selector matching its own pod template is enough for the // grouping engine to recognize it, and no pod is needed for a version compare. -func fakeClientFor(apps []fakeApp) *fake.Clientset { +// +// refusedKinds makes List fail for exactly those kinds with the same +// apierrors.NewForbidden a namespace-scoped role produces, so a caller can +// build the "connected, but this kind is unreadable" fixture that a plain +// unreachable context cannot: clusters.Fetch turns this into an entry in +// Snapshot.Partial rather than an error. +func fakeClientFor(apps []fakeApp, refusedKinds []string) *fake.Clientset { objs := make([]runtime.Object, 0, len(apps)) for _, a := range apps { objs = append(objs, &appsv1.Deployment{ @@ -131,7 +139,36 @@ func fakeClientFor(apps []fakeApp) *fake.Clientset { }, }) } - return fake.NewSimpleClientset(objs...) + client := fake.NewSimpleClientset(objs...) + for _, kind := range refusedKinds { + resource := resourceOfKind(kind) + client.PrependReactor("list", resource, func(ktesting.Action) (bool, runtime.Object, error) { + return true, nil, apierrors.NewForbidden(schema.GroupResource{Resource: resource}, "", nil) + }) + } + return client +} + +// resourceOfKind maps the kind names clusters.Fetch reports in Snapshot.Partial +// to the plural resource name a fake clientset reactor matches on. +func resourceOfKind(kind string) string { + switch kind { + case "Deployment": + return "deployments" + case "StatefulSet": + return "statefulsets" + case "DaemonSet": + return "daemonsets" + case "CronJob": + return "cronjobs" + case "Job": + return "jobs" + case "ReplicaSet": + return "replicasets" + case "Pod": + return "pods" + } + return strings.ToLower(kind) + "s" } // Two contexts bound to one environment, running different versions. Build @@ -190,7 +227,7 @@ func TestPromotionUnreachableOnlyForFullyUnreadableEnvironments(t *testing.T) { "prod-us-east": {env: "prod", apps: []fakeApp{{name: "checkout", ns: "shop", image: "reg/checkout:v2.13.1"}}}, "prod-eu-west": {env: "prod", unreachable: true}, "stg-a": {env: "stg", unreachable: true}, - "stg-b": {env: "stg", denied: true}, + "stg-b": {env: "stg", unreachable: true}, }) v := s.Promotion() diff --git a/internal/config/config.go b/internal/config/config.go index 1242739..c9bdc05 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -177,7 +177,7 @@ func (c *Config) apply(f file, path string) error { c.byContext[name] = i } for _, u := range e.Clusters { - k := normalizeURL(u) + k := NormalizeURL(u) if prev, dup := c.byCluster[k]; dup { return fmt.Errorf("%s: cluster %q is bound to both %q and %q", path, u, c.Environments[prev].Name, env.Name) @@ -279,7 +279,7 @@ func writeForRisk(r environments.Risk) environments.WritePolicy { // classifies by name, so configuring one environment does not blind kubeside to // the others. func (c *Config) Environment(kctx kubeconfig.Context) environments.Environment { - if i, ok := c.byCluster[normalizeURL(kctx.Server)]; ok { + if i, ok := c.byCluster[NormalizeURL(kctx.Server)]; ok { return c.Environments[i] } if i, ok := c.byContext[kctx.Name]; ok { @@ -295,9 +295,9 @@ func (c *Config) AppRef(app, env string) (Ref, bool) { return r, ok } -// normalizeURL makes server URLs comparable across the small differences that +// NormalizeURL makes server URLs comparable across the small differences that // mean nothing: a trailing slash, or a host written in a different case. -func normalizeURL(u string) string { +func NormalizeURL(u string) string { s := strings.TrimSpace(u) if s == "" { return "" From 8521e9ab9c78a67ab986a6dab352d58333c5f85e Mon Sep 17 00:00:00 2001 From: Elber Ribeiro Date: Mon, 3 Aug 2026 01:10:50 -0400 Subject: [PATCH 18/38] fix(fleet): absent is not denied, and one timeout per cluster Every cluster that simply did not run the app reported "denied". The snapshot was taken at the background tier, which appends "Pod" to Snapshot.Partial unconditionally as a deliberate skip, so the len(Partial) > 0 test was constant-true and StateAbsent was dead code. Two changes, either of which would have hidden the other's absence: - Fetch at clusters.TierActive, the way cmd/kubeside's gather already does for this kind of sweep. That drops the phantom Pod entry and brings the digest, which lives only in pod status, so a tag resolving to two digests is findable at all. - Test denial only on kinds that could carry the app. Partial unions a refusal with a skip and only the first implies denial; a refused Pod or ReplicaSet list hides health, never existence. The Scope half of that test could never fire and was inverted besides. A namespace-scoped fallback means the cluster refused to enumerate its namespaces and only one was read, so an app living elsewhere was never looked for. scopeCovers asks whether the namespace we did read would have carried the identity we asked for, using the rule findApp matches with. A denied row now names a verb, a resource and a scope, which is the shape of a Role rule, and never names a kind nobody refused. The sweep gives every context its own goroutine and its own copy of the timeout, because the flag means per-cluster connect and fetch. One budget spent serially let a few slow clusters report every cluster behind them as unreachable, inventing the fact this screen exists to keep honest. Rows are written by index, so the order is the kubeconfig's and not the scheduler's. findApp no longer drops a second match in silence: it prefers the namespace asked for, falls back to name order, and says what it skipped. fleet.Build surfaces a present row's Reason so that choice is visible. --- internal/api/fleet.go | 311 +++++++++++++++++++++++++---------- internal/api/fleet_test.go | 239 +++++++++++++++++++++++++++ internal/api/service_test.go | 39 +++++ internal/fleet/fleet.go | 18 +- 4 files changed, 519 insertions(+), 88 deletions(-) diff --git a/internal/api/fleet.go b/internal/api/fleet.go index 4a00927..34f93d5 100644 --- a/internal/api/fleet.go +++ b/internal/api/fleet.go @@ -2,7 +2,10 @@ package api import ( "context" + "fmt" + "sort" "strings" + "sync" "github.com/dynaum/kubeside/internal/apps" "github.com/dynaum/kubeside/internal/clusters" @@ -14,109 +17,243 @@ import ( // Fleet answers "is every cluster running the latest version" for one app. // // Opening this view states the intent to ask every cluster, so unlike the app -// list it deliberately wakes every context in the kubeconfig. Rows land as -// clusters answer and a cluster behind a VPN never blocks the rest. +// list it deliberately wakes every context in the kubeconfig. +// +// Each context gets its own goroutine and its own copy of the timeout, the way +// cmd/kubeside's gather does, because the flag means "per-cluster connect and +// fetch". One budget spent serially would let a few slow clusters exhaust it +// and report every cluster behind them as unreachable — an invented unreachable +// on the one screen whose job is keeping unreachable honest, and one whose +// answer would change with ConnectOrder. +// +// Rows are still assembled once, when every context has answered or given up. +// Each goroutine writes to its own index rather than appending, so the view is +// built from the kubeconfig's order and never from the scheduler's. +// +// Every context is read at clusters.TierActive. This screen compares digests +// across clusters and the digest exists only in pod status, so a background +// read, which skips pods, would leave every digest permanently pending and make +// the worst thing this screen can find — one tag running as two different +// images — impossible to find. The tier is passed to the read rather than set +// on the connection: a sweep is not attention, and promoting ten contexts would +// keep them all out of the idle reaper. func (s *Service) Fleet(app, namespace string) fleet.View { - ctx, cancel := context.WithTimeout(context.Background(), s.timeout) - defer cancel() - + order := s.mgr.ConnectOrder() want := promotion.Identity(app, namespace) - var placements []fleet.Placement - - for _, name := range s.mgr.ConnectOrder() { - kctx := s.cfg.MustGet(name) - env := s.conf.Environment(kctx) - - // ClusterID is read from the kubeconfig cluster entry before any - // connection attempt, on every path including the failure paths - // below. fleet.mergeByCluster keys on this to collapse two contexts - // aimed at one cluster; populating it from a successful connection - // instead would leave an unreachable duplicate with an empty - // ClusterID, which fails to merge with its reachable twin and - // inflates Clusters in exactly the case the merge exists to prevent. - p := fleet.Placement{ - Context: name, - ClusterID: config.NormalizeURL(kctx.Server), - Env: env.Name, - } - if err := s.mgr.Connect(ctx, name); err != nil { - p.State = fleet.StateUnreachable - p.Reason = err.Error() - placements = append(placements, p) - continue - } - client, ok := s.mgr.ClientFor(name) - if !ok { - p.State = fleet.StateUnreachable - p.Reason = "not connected" - placements = append(placements, p) - continue - } + placements := make([]fleet.Placement, len(order)) + var wg sync.WaitGroup + for i, name := range order { + wg.Add(1) + go func(i int, name string) { + defer wg.Done() - snap, err := clusters.Fetch(ctx, client, kctx, clusters.FetchOptions{Tier: s.mgr.Tier(name)}) - if err != nil { - p.State = fleet.StateUnreachable - p.Reason = err.Error() - placements = append(placements, p) - continue - } - - match, found := findApp(snap.Apps, want) - if !found { - // Not in the snapshot has two causes and they are not the same - // fact. clusters.Fetch never errors on an RBAC refusal: it records - // the kinds it could not read in Snapshot.Partial and returns what - // it got. So an app missing from a snapshot with refused kinds is - // an app we were not allowed to look for, and calling that "not - // deployed here" is the conflation this screen exists to prevent. - // internal/api/service.go:196 already draws the same line for the - // apps list. - if len(snap.Partial) > 0 || (len(snap.Scope.Namespaces) == 0 && !snap.Scope.ClusterWide) { - p.State = fleet.StateDenied - p.Reason = refusedReason(snap) - } else { - p.State = fleet.StateAbsent - } - placements = append(placements, p) - continue - } + ctx, cancel := context.WithTimeout(context.Background(), s.timeout) + defer cancel() - in := instanceOf(env.Name, match) - p.State = fleet.StatePresent - p.Namespace = in.Namespace - p.Image = in.Image - p.Tag = in.Tag - p.Digest = in.Digest - p.DigestPending = in.Digest == "" - p.Health = in.Health - p.Ready = in.Ready - p.RevisionAt = in.RevisionAt - placements = append(placements, p) + placements[i] = s.placeApp(ctx, name, app, namespace, want) + }(i, name) } + wg.Wait() return fleet.Build(app, namespace, placements) } -// findApp matches by the same identity the promotion matrix uses, so one app -// is one app on both screens. -func findApp(list []apps.App, want string) (apps.App, bool) { +// placeApp asks one cluster about one app and always returns a row. +func (s *Service) placeApp(ctx context.Context, name, app, namespace, want string) fleet.Placement { + kctx := s.cfg.MustGet(name) + env := s.conf.Environment(kctx) + + // ClusterID is read from the kubeconfig cluster entry before any + // connection attempt, on every path including the failure paths + // below. fleet.mergeByCluster keys on this to collapse two contexts + // aimed at one cluster; populating it from a successful connection + // instead would leave an unreachable duplicate with an empty + // ClusterID, which fails to merge with its reachable twin and + // inflates Clusters in exactly the case the merge exists to prevent. + p := fleet.Placement{ + Context: name, + ClusterID: config.NormalizeURL(kctx.Server), + Env: env.Name, + } + + if err := s.mgr.Connect(ctx, name); err != nil { + p.State = fleet.StateUnreachable + p.Reason = err.Error() + return p + } + client, ok := s.mgr.ClientFor(name) + if !ok { + p.State = fleet.StateUnreachable + p.Reason = "not connected" + return p + } + + snap, err := clusters.Fetch(ctx, client, kctx, clusters.FetchOptions{Tier: clusters.TierActive}) + if err != nil { + p.State = fleet.StateUnreachable + p.Reason = err.Error() + return p + } + + match, skipped, found := findApp(snap.Apps, app, namespace, want) + if !found { + p.State, p.Reason = missingBecause(snap, app, namespace, want) + return p + } + + in := instanceOf(env.Name, match) + p.State = fleet.StatePresent + p.Reason = skipped + p.Namespace = in.Namespace + p.Image = in.Image + p.Tag = in.Tag + p.Digest = in.Digest + p.DigestPending = in.Digest == "" + p.Health = in.Health + p.Ready = in.Ready + p.RevisionAt = in.RevisionAt + return p +} + +// findApp matches by the same identity the promotion matrix uses, so one app is +// one app on both screens. +// +// Two namespaces in one cluster can strip to a single identity — team-a-qa and +// team-a-prod side by side, the arrangement StripEnvToken exists for — and a +// row holds one placement, so one match has to win. The namespace actually +// asked for wins; failing that, the first in name order, so the answer never +// depends on the order the snapshot happened to arrive in. The runner-up is +// returned rather than dropped: a match that disappears without a word is the +// failure this screen exists to prevent. +func findApp(list []apps.App, app, namespace, want string) (match apps.App, skipped string, found bool) { + var matches []apps.App for _, a := range list { if promotion.Identity(a.Key.Name, a.Key.Namespace) == want { - return a, true + matches = append(matches, a) + } + } + if len(matches) == 0 { + return apps.App{}, "", false + } + sort.SliceStable(matches, func(i, j int) bool { + exactI, exactJ := matches[i].Key.Namespace == namespace, matches[j].Key.Namespace == namespace + if exactI != exactJ { + return exactI } + return matches[i].Key.Namespace < matches[j].Key.Namespace + }) + if len(matches) == 1 { + return matches[0], "", true } - return apps.App{}, false + + others := make([]string, 0, len(matches)-1) + for _, a := range matches[1:] { + others = append(others, a.Key.Namespace) + } + return matches[0], fmt.Sprintf("%d namespaces here hold this app; showing %s, not %s", + len(matches), matches[0].Key.Namespace, strings.Join(others, ", ")), true +} + +// missingBecause tells "we were not allowed to look" apart from "it is not +// here", which is the conflation this screen exists to prevent. +// +// clusters.Fetch never returns an error for an RBAC refusal: it records the +// kinds it could not read in Snapshot.Partial and returns what it got. Two +// things, and only two, turn a miss into a denial: +// +// - A kind that could carry the app went unread. Snapshot.Partial unions two +// different facts, a refusal and a deliberate skip, and only the first +// implies denial — so the test is on the kinds apps.Group builds apps out +// of. A refused Pod or ReplicaSet list hides how healthy an app is, never +// whether it exists. +// - The cluster refused to enumerate its namespaces, so the read fell back to +// a single namespace. If the app lives anywhere else we never looked, which +// is denied. If the namespace we did read is the one asked about, we looked +// and found nothing, and absent is the honest answer. +// +// Service.Promotion is not a precedent for either line: it never inspects +// Partial, so a refused read reads as absent there. Reconciling the two screens +// is its own change, not this one. +func missingBecause(snap clusters.Snapshot, app, namespace, want string) (state, reason string) { + if refused := refusedCarriers(snap.Partial); len(refused) > 0 { + return fleet.StateDenied, refusedReason(refused, snap.Scope) + } + if !scopeCovers(snap.Scope, app, want) { + return fleet.StateDenied, unreadNamespaceReason(snap.Scope, namespace) + } + return fleet.StateAbsent, "" +} + +// appCarrying maps the kinds an app can be built out of to the resource names a +// Role spells them with. Pod and ReplicaSet are deliberately absent: a +// ReplicaSet whose Deployment went unread still names its owner, and a pod list +// says nothing about whether a workload exists. +var appCarrying = map[string]string{ + "Deployment": "deployments", + "StatefulSet": "statefulsets", + "DaemonSet": "daemonsets", + "CronJob": "cronjobs", + "Job": "jobs", +} + +// refusedCarriers keeps only the unread kinds that could have hidden the app. +func refusedCarriers(partial []string) []string { + out := make([]string, 0, len(partial)) + for _, kind := range partial { + if _, ok := appCarrying[kind]; ok { + out = append(out, kind) + } + } + return out } -// refusedReason names what the cluster would not let us read, so a denied row -// says which verb to ask for rather than just refusing to answer. -func refusedReason(snap clusters.Snapshot) string { - if len(snap.Partial) > 0 { - return "could not read " + strings.Join(snap.Partial, ", ") +// refusedReason names what to ask for, not only what failed. An RBAC message a +// developer can act on carries a verb, a resource and a scope, because that is +// the shape of a Role rule; a bare kind name is half of it and the wrong half. +func refusedReason(kinds []string, scope clusters.Scope) string { + resources := make([]string, 0, len(kinds)) + for _, kind := range kinds { + resources = append(resources, appCarrying[kind]) } - if snap.Scope.Reason != "" { - return snap.Scope.Reason + return fmt.Sprintf("cannot read %s here; the role needs: list %s %s", + strings.Join(kinds, ", "), strings.Join(resources, ", "), scopePhrase(scope)) +} + +// unreadNamespaceReason explains a miss nobody could have seen: the cluster +// refused to list its namespaces, so only the fallback namespace was read, and +// the one asked about was never opened. +func unreadNamespaceReason(scope clusters.Scope, namespace string) string { + reason := scope.Reason + if reason == "" { + reason = "the namespace list was not readable" + } + return fmt.Sprintf("%s; only %s was read, so %s was never looked in", + reason, strings.Join(scope.Namespaces, ", "), namespace) +} + +// scopeCovers answers whether a read could have found the app at all. +// +// A cluster-wide read looked everywhere. A fallback read looked in one +// namespace, and that only counts if the app would have carried the identity we +// asked for had it been there. promotion.Identity is the rule findApp matches +// with, so asking it about the namespace we did read answers "would we have +// seen it" without guessing at environment suffixes. +func scopeCovers(scope clusters.Scope, app, want string) bool { + if scope.ClusterWide { + return true + } + for _, ns := range scope.Namespaces { + if promotion.Identity(app, ns) == want { + return true + } + } + return false +} + +func scopePhrase(scope clusters.Scope) string { + if scope.ClusterWide || len(scope.Namespaces) == 0 { + return "across the cluster" } - return "no readable namespace" + return "in " + strings.Join(scope.Namespaces, ", ") } diff --git a/internal/api/fleet_test.go b/internal/api/fleet_test.go index d3e3df5..4610315 100644 --- a/internal/api/fleet_test.go +++ b/internal/api/fleet_test.go @@ -3,10 +3,24 @@ package api import ( "strings" "testing" + "time" "github.com/dynaum/kubeside/internal/fleet" ) +// rowFor returns the row for one context, failing the test when the sweep lost +// it: every context asked gets a row, so a missing one is the bug itself. +func rowFor(t *testing.T, v fleet.View, ctx string) fleet.Row { + t.Helper() + for _, r := range v.Rows { + if r.Context == ctx { + return r + } + } + t.Fatalf("no row for %q; rows = %+v", ctx, v.Rows) + return fleet.Row{} +} + // TestFleetAsksEveryContext proves the basic shape: one row per cluster, the // newest tag named, and every cluster behind it counted. func TestFleetAsksEveryContext(t *testing.T) { @@ -118,6 +132,12 @@ func TestFleetFoundNowhereStillNamesWhatItAsked(t *testing.T) { if len(v.Rows) != 2 { t.Errorf("rows = %d, want 2", len(v.Rows)) } + for _, r := range v.Rows { + if r.State != fleet.StateAbsent { + t.Errorf("%s state = %q, want %q: nobody refused anything, the app simply is not there", + r.Context, r.State, fleet.StateAbsent) + } + } } // TestFleetMergesAnUnreachableDuplicateWithItsReachableTwin pins the invariant @@ -163,3 +183,222 @@ func TestFleetMergesAnUnreachableDuplicateWithItsReachableTwin(t *testing.T) { t.Errorf("aliases = %v, want prod-stale recorded as an alias of the merged row", v.Rows[0].Aliases) } } + +// TestADigestPinnedImageIsPresentWithNoTagToCompare proves an image pinned by +// digest is reported as running with its version openly unknown. Fabricating a +// tag would be an invention, and letting it match the newest would render the +// deepest unknown on the screen as the calmest row on it. +func TestADigestPinnedImageIsPresentWithNoTagToCompare(t *testing.T) { + const digest = "sha256:9f2c0a1b3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8" + s := serviceWithContexts(t, map[string]fakeCluster{ + "qa-cluster": {env: "qa", apps: []fakeApp{{name: "checkout", ns: "shop", image: "reg/checkout:v2.14.0"}}}, + "prod-cluster": {env: "prod", apps: []fakeApp{{name: "checkout", ns: "shop", image: "reg/checkout@" + digest}}}, + }) + + v := s.Fleet("checkout", "shop") + + r := rowFor(t, v, "prod-cluster") + if r.State != fleet.StatePresent { + t.Fatalf("state = %q, want %q: a pinned image is running, whatever it is called", r.State, fleet.StatePresent) + } + if r.Tag != "" { + t.Errorf("tag = %q, want empty: there is no tag in a digest-pinned image to invent one from", r.Tag) + } + if !strings.Contains(r.Note, "version unknown") { + t.Errorf("note = %q, want it to say the version is unknown", r.Note) + } + if r.Behind { + t.Error("a version nobody can name is not a version behind: that would invent a direction") + } + if v.Newest != "v2.14.0" { + t.Errorf("newest = %q, want v2.14.0: an untagged image must not become the yardstick", v.Newest) + } +} + +// TestAResolvedDigestLeavesNothingUnverified proves the digest actually arrives. +// It lives only in pod status, so a sweep that skips pods reports every row's +// digest as pending forever, and one tag running as two different images — the +// worst thing this screen can find — becomes impossible to find. +func TestAResolvedDigestLeavesNothingUnverified(t *testing.T) { + const digest = "sha256:11223344556677889900aabbccddeeff11223344556677889900aabbccddeeff" + s := serviceWithContexts(t, map[string]fakeCluster{ + "qa-cluster": {env: "qa", apps: []fakeApp{{ + name: "checkout", ns: "shop", + image: "reg/checkout:v2.14.0", + imageID: "reg/checkout@" + digest, + }}}, + }) + + v := s.Fleet("checkout", "shop") + + r := rowFor(t, v, "qa-cluster") + if r.Digest != digest { + t.Fatalf("digest = %q, want %q: the pod resolved it and the row must carry it", r.Digest, digest) + } + if r.DigestPending { + t.Error("digestPending = true for a digest that arrived") + } + if v.DigestUnverified != 0 { + t.Errorf("digestUnverified = %d, want 0: nothing is in flight once the pod answered", v.DigestUnverified) + } +} + +// TestOneTagRunningTwoDigestsIsFound is the top of the severity ladder and it +// is only reachable if digests arrive. Two clusters claiming one version while +// running different code is a defect, not a schedule. +func TestOneTagRunningTwoDigestsIsFound(t *testing.T) { + s := serviceWithContexts(t, map[string]fakeCluster{ + "prod-us-east": {env: "prod", apps: []fakeApp{{ + name: "checkout", ns: "shop", image: "reg/checkout:v2.14.0", + imageID: "reg/checkout@sha256:aaaa000000000000000000000000000000000000000000000000000000000000", + }}}, + "prod-eu-west": {env: "prod", apps: []fakeApp{{ + name: "checkout", ns: "shop", image: "reg/checkout:v2.14.0", + imageID: "reg/checkout@sha256:bbbb000000000000000000000000000000000000000000000000000000000000", + }}}, + }) + + v := s.Fleet("checkout", "shop") + + if !v.MutableTag { + t.Fatal("mutableTag = false: one tag resolved to two digests and the screen said nothing") + } +} + +// TestADeniedRowNamesOnlyKindsThatWereRefused proves the note is a fact rather +// than an artefact of how much the sweep chose to read. Snapshot.Partial unions +// refusals with deliberate skips, and a row that names a kind nobody refused +// sends a developer to ask for permission they already have. +func TestADeniedRowNamesOnlyKindsThatWereRefused(t *testing.T) { + s := serviceWithContexts(t, map[string]fakeCluster{ + "qa-cluster": {env: "qa"}, + "prod-cluster": {env: "prod", refusedKinds: []string{"CronJob"}}, + }) + + v := s.Fleet("checkout", "shop") + + denied := rowFor(t, v, "prod-cluster") + if denied.State != fleet.StateDenied { + t.Fatalf("state = %q, want %q", denied.State, fleet.StateDenied) + } + if !strings.Contains(denied.Note, "CronJob") { + t.Errorf("note = %q, want it to name CronJob, the kind that was refused", denied.Note) + } + for _, kind := range []string{"Pod", "ReplicaSet", "Deployment"} { + if strings.Contains(denied.Note, kind) { + t.Errorf("note = %q names %s, which nobody refused", denied.Note, kind) + } + } + // Verb, resource and scope: what a Role rule is made of. A bare kind name + // is half an RBAC message and the wrong half. + if !strings.Contains(denied.Note, "list cronjobs") { + t.Errorf("note = %q, want the verb and resource a developer would ask for", denied.Note) + } + + clean := rowFor(t, v, "qa-cluster") + if clean.State != fleet.StateAbsent { + t.Fatalf("state = %q, want %q: nothing was refused here", clean.State, fleet.StateAbsent) + } + if strings.Contains(clean.Note, "Pod") { + t.Errorf("note = %q mentions Pod, which was never asked for", clean.Note) + } +} + +// TestANamespaceWeNeverOpenedIsDeniedNotAbsent covers the case a cluster that +// refuses to list its namespaces produces: the read falls back to one namespace, +// and an app living anywhere else was never looked for. Calling that "not +// deployed here" is a claim nobody earned. +func TestANamespaceWeNeverOpenedIsDeniedNotAbsent(t *testing.T) { + s := serviceWithContexts(t, map[string]fakeCluster{ + "prod-cluster": {env: "prod", refusedKinds: []string{"Namespace"}}, + }) + + v := s.Fleet("checkout", "shop") + + r := rowFor(t, v, "prod-cluster") + if r.State != fleet.StateDenied { + t.Fatalf("state = %q, want %q: only the fallback namespace was read, and shop was not it", + r.State, fleet.StateDenied) + } + if !strings.Contains(r.Note, "namespace list forbidden") { + t.Errorf("note = %q, want the refusal that caused the fallback", r.Note) + } + if !strings.Contains(r.Note, "shop") { + t.Errorf("note = %q, want it to name the namespace nobody opened", r.Note) + } +} + +// TestTheFallbackNamespaceWeDidReadIsAbsentNotDenied is the other half: a +// namespace-scoped read that covered exactly the namespace asked about did look, +// and found nothing. Denied there would be the same conflation in reverse. +func TestTheFallbackNamespaceWeDidReadIsAbsentNotDenied(t *testing.T) { + s := serviceWithContexts(t, map[string]fakeCluster{ + "prod-cluster": {env: "prod", refusedKinds: []string{"Namespace"}}, + }) + + v := s.Fleet("checkout", "default") + + r := rowFor(t, v, "prod-cluster") + if r.State != fleet.StateAbsent { + t.Fatalf("state = %q, want %q: default is the namespace the fallback read", r.State, fleet.StateAbsent) + } +} + +// TestTwoNamespacesInOneClusterSayWhichOneWon covers the arrangement +// StripEnvToken exists for, colocated in one cluster: team-a-qa and +// team-a-prod are one identity, a row holds one placement, and the match that +// did not win must not vanish without a word. +func TestTwoNamespacesInOneClusterSayWhichOneWon(t *testing.T) { + s := serviceWithContexts(t, map[string]fakeCluster{ + "shared-cluster": {env: "prod", apps: []fakeApp{ + {name: "checkout", ns: "team-a-prod", image: "reg/checkout:v2.13.1"}, + {name: "checkout", ns: "team-a-qa", image: "reg/checkout:v2.14.0"}, + }}, + }) + + v := s.Fleet("checkout", "team-a-qa") + + r := rowFor(t, v, "shared-cluster") + if r.Namespace != "team-a-qa" { + t.Errorf("namespace = %q, want team-a-qa: the namespace asked for wins the tie", r.Namespace) + } + if !strings.Contains(r.Note, "team-a-prod") { + t.Errorf("note = %q, want it to name the match that was not shown", r.Note) + } +} + +// TestFleetSpendsTheTimeoutPerClusterNotAcrossThem pins what the flag means. +// Four clusters that each take a moment must cost one moment, not four: a +// serial sweep on one shared budget hands the last clusters an expired deadline +// and reports them unreachable, which invents the exact fact this screen exists +// to keep honest, and does it differently depending on ConnectOrder. +func TestFleetSpendsTheTimeoutPerClusterNotAcrossThem(t *testing.T) { + const delay = 200 * time.Millisecond + s := serviceWithContexts(t, map[string]fakeCluster{ + "prod-a": {env: "prod", slow: delay, apps: []fakeApp{{name: "checkout", ns: "shop", image: "reg/checkout:v2.14.0"}}}, + "prod-b": {env: "prod", slow: delay, apps: []fakeApp{{name: "checkout", ns: "shop", image: "reg/checkout:v2.14.0"}}}, + "prod-c": {env: "prod", slow: delay, apps: []fakeApp{{name: "checkout", ns: "shop", image: "reg/checkout:v2.14.0"}}}, + "prod-d": {env: "prod", slow: delay, apps: []fakeApp{{name: "checkout", ns: "shop", image: "reg/checkout:v2.14.0"}}}, + }) + // Under the serial sweep this budget expires two clusters in; under a + // per-cluster one every cluster gets the whole of it. + s.timeout = 3 * delay + + start := time.Now() + v := s.Fleet("checkout", "shop") + elapsed := time.Since(start) + + if elapsed > s.timeout { + t.Errorf("sweep took %v with a %v per-cluster timeout: four slow clusters spent one budget in turn", + elapsed, s.timeout) + } + if v.Present != 4 { + t.Errorf("present = %d, want 4", v.Present) + } + for _, r := range v.Rows { + if r.State == fleet.StateUnreachable { + t.Errorf("%s = unreachable (%q): a cluster that answered was reported as one that did not", + r.Context, r.Note) + } + } +} diff --git a/internal/api/service_test.go b/internal/api/service_test.go index 1a74abf..60a5dbd 100644 --- a/internal/api/service_test.go +++ b/internal/api/service_test.go @@ -31,6 +31,11 @@ import ( // one image, which is all the promotion matrix needs to compare versions. type fakeApp struct { name, ns, image string + // imageID seeds a running pod owned by the Deployment, reporting this as + // the image it actually resolved to. The digest lives in pod status and + // nowhere else, so a fixture without one can only ever produce a placement + // whose digest is still pending. + imageID string } // fakeCluster is one kubeconfig context's worth of fixture: which environment @@ -51,6 +56,10 @@ type fakeCluster struct { // kinds land in Snapshot.Partial and Fetch returns a nil error, which is // what lets a caller tell "denied" apart from "absent". refusedKinds []string + // slow delays the connection by this much, the way a cluster behind a VPN + // that is off does. It is what separates a sweep that spends one budget + // serially from one that gives every cluster its own. + slow time.Duration } // serviceWithContexts builds a Service over several kubeconfig contexts, each @@ -103,6 +112,9 @@ func serviceWithContexts(t *testing.T, byContext map[string]fakeCluster) *Servic connector := clusters.KubeConnector{ NewClient: func(kctx kubeconfig.Context, _ kubeconfig.Options) (kubernetes.Interface, error) { fc := byContext[kctx.Name] + if fc.slow > 0 { + time.Sleep(fc.slow) + } if fc.unreachable { return nil, fmt.Errorf("dial %s: connection refused", kctx.Name) } @@ -139,6 +151,33 @@ func fakeClientFor(apps []fakeApp, refusedKinds []string) *fake.Clientset { }, }) } + for _, a := range apps { + if a.imageID == "" { + continue + } + // The pod is owned by its Deployment, so the grouping engine walks up + // to the same app rather than inventing a second one, and its container + // status carries the digest the node actually pulled. + yes := true + objs = append(objs, &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: a.name + "-abc123", Namespace: a.ns, UID: types.UID(a.ns + "/" + a.name + "/pod"), + Labels: map[string]string{"app": a.name}, + OwnerReferences: []metav1.OwnerReference{{ + Kind: "Deployment", Name: a.name, + UID: types.UID(a.ns + "/" + a.name), Controller: &yes, + }}, + }, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "app", Image: a.image}}}, + Status: corev1.PodStatus{ + Phase: corev1.PodRunning, + Conditions: []corev1.PodCondition{{Type: corev1.PodReady, Status: corev1.ConditionTrue}}, + ContainerStatuses: []corev1.ContainerStatus{{ + Name: "app", Image: a.image, ImageID: a.imageID, Ready: true, + }}, + }, + }) + } client := fake.NewSimpleClientset(objs...) for _, kind := range refusedKinds { resource := resourceOfKind(kind) diff --git a/internal/fleet/fleet.go b/internal/fleet/fleet.go index ca983e9..1c8096f 100644 --- a/internal/fleet/fleet.go +++ b/internal/fleet/fleet.go @@ -33,7 +33,10 @@ type Placement struct { Env string `json:"env"` Namespace string `json:"namespace,omitempty"` - State string `json:"state"` + State string `json:"state"` + // Reason explains what the state cannot: why a cluster went unreachable or + // denied, and on a present row, anything the caller had to choose between + // to produce it. Reason string `json:"reason,omitempty"` Image string `json:"image,omitempty"` @@ -182,6 +185,19 @@ func Build(app, namespace string, ps []Placement) View { default: r.Note = verdict } + + // A present row can still carry a reason: the caller had two + // candidates for this one row and says which it took. It is appended + // rather than substituted, because the version verdict is what the + // screen is for and a choice nobody sees is a choice nobody can + // challenge. + if p.Reason != "" { + if r.Note == "" { + r.Note = p.Reason + } else { + r.Note += "; " + p.Reason + } + } case StateUnreachable: r.Note = orElse(p.Reason, "the cluster did not answer") case StateDenied: From 1d17a1ad450fa26b42028fe2bb494d12bcd35c6f Mon Sep 17 00:00:00 2001 From: Elber Ribeiro Date: Mon, 3 Aug 2026 01:12:24 -0400 Subject: [PATCH 19/38] docs(fleet): the plan's gatherer traded one conflation for its mirror Step 4 tested len(snap.Partial) > 0, which is constant true: fetch.go appends Pod to Partial at any non-active tier as a deliberate skip, not a refusal, and Fleet reads every context at background tier. So absent was dead code and every app not deployed somewhere reported as denied. Records that, the unreachable Scope clause, and the shared serial timeout, with a pointer to what shipped instead. --- docs/superpowers/plans/2026-08-02-fleet.md | 23 ++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/docs/superpowers/plans/2026-08-02-fleet.md b/docs/superpowers/plans/2026-08-02-fleet.md index 6ae6f43..69d222f 100644 --- a/docs/superpowers/plans/2026-08-02-fleet.md +++ b/docs/superpowers/plans/2026-08-02-fleet.md @@ -1187,6 +1187,29 @@ cluster merge so the headline count stays honest." ### Task 5: Gather placements across every context +> **Corrected 2026-08-03, after review of the shipped commit. The Step 4 code +> below is WRONG in three ways and is kept only as a record of what was tried. +> Read `internal/api/fleet.go` for what actually ships.** +> +> 1. `len(snap.Partial) > 0` is constant true. `clusters/fetch.go:292` appends +> `"Pod"` to `Partial` at any non-active tier, deliberately unread and +> explicitly not a refusal, and `Fleet` runs every context at +> `TierBackground`. So `StateAbsent` was dead code and every app simply not +> deployed somewhere reported as denied. The denial test must name only the +> kinds that could carry the app: Deployment, StatefulSet, DaemonSet, +> CronJob, Job. Never Pod or ReplicaSet, which hide health rather than +> existence. The fetch also moves to `clusters.TierActive`, matching +> `cmd/kubeside/main.go:194`, because the digest lives only in pod status and +> without it the mutable-tag detection can never fire. +> 2. The `Scope` clause `len(snap.Scope.Namespaces) == 0 && !snap.Scope.ClusterWide` +> is unreachable. `fallbackScope` always sets exactly one namespace. The real +> condition is `!ClusterWide` qualified by whether the requested namespace is +> among the ones actually read. +> 3. One shared timeout across a serial loop spends the whole budget on the +> first slow clusters and invents `unreachable` rows for the rest, in the one +> view whose job is keeping unreachable honest. One goroutine and one +> `WithTimeout` per context, per `cmd/kubeside/main.go:183-196`. + **Files:** - Create: `internal/api/fleet.go`, `internal/api/fleet_test.go` - Modify: `internal/config/config.go:298-305` (export `normalizeURL`) From b698d769dee32c8d8ac7c893d5a23c1335239363 Mon Sep 17 00:00:00 2001 From: Elber Ribeiro Date: Mon, 3 Aug 2026 01:21:23 -0400 Subject: [PATCH 20/38] feat(fleet): serve the fleet view over HTTP MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GET /api/fleet?app=&namespace=. A request without an app is a 400 rather than an empty screen, because the view is about one app by construction and an empty fleet screen would read as "this app runs nowhere" — the conflation this feature exists to prevent. Namespace is optional: Service.Fleet already accepts "" and matches whichever namespace each cluster holds the app in, so the transport does not add a requirement the service does not have. Widens the API interface with Fleet(app, namespace string) fleet.View and updates both test fakes (stubAPI, streamStub) that implement it. Closes #66 --- internal/api/server.go | 20 ++++++++++++ internal/api/server_test.go | 64 +++++++++++++++++++++++++++++++++++++ internal/api/stream_test.go | 5 +++ 3 files changed, 89 insertions(+) diff --git a/internal/api/server.go b/internal/api/server.go index 3887633..b869ac5 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -33,6 +33,7 @@ import ( "github.com/coder/websocket" "github.com/coder/websocket/wsjson" "github.com/dynaum/kubeside/internal/exec" + "github.com/dynaum/kubeside/internal/fleet" "github.com/dynaum/kubeside/internal/forward" "github.com/dynaum/kubeside/internal/logs" ) @@ -94,6 +95,9 @@ type API interface { StopForward(id string) error // Promotion compares every app across every environment. Promotion() PromotionView + // Fleet is one app across every cluster, for the question the matrix + // cannot phrase: are all of them on the newest version. + Fleet(app, namespace string) fleet.View // Capabilities resolves what this reader may do in one namespace, so every // control renders disabled-with-a-reason rather than hidden. Capabilities(contextName, namespace string) CapabilitiesView @@ -140,6 +144,7 @@ func New(a API, ui http.Handler, opts ...Option) (*Server, error) { mux.HandleFunc("/api/diff", s.handleDiff) mux.HandleFunc("/api/forwards", s.handleForwards) mux.HandleFunc("/api/promotion", s.handlePromotion) + mux.HandleFunc("/api/fleet", s.handleFleet) mux.HandleFunc("/api/can", s.handleCapabilities) mux.HandleFunc("/api/gate", s.handleGate) mux.HandleFunc("/api/exec", s.handleExec) @@ -565,6 +570,21 @@ func (s *Server) handlePromotion(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, s.api.Promotion()) } +// handleFleet answers "is every cluster running the latest version". The app +// is required because the view is about one app by construction: an app-less +// request would have nothing to compare across clusters, and an empty screen +// would read as "this app runs nowhere" rather than as an unaddressed +// question. Namespace is optional; Service.Fleet accepts "" and matches +// against whichever namespace each cluster actually holds the app in. +func (s *Server) handleFleet(w http.ResponseWriter, r *http.Request) { + app := r.URL.Query().Get("app") + if app == "" { + writeJSON(w, http.StatusBadRequest, map[string]string{"error": "app is required"}) + return + } + writeJSON(w, http.StatusOK, s.api.Fleet(app, r.URL.Query().Get("namespace"))) +} + func (s *Server) handleForwards(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: diff --git a/internal/api/server_test.go b/internal/api/server_test.go index 5e7e410..30e8107 100644 --- a/internal/api/server_test.go +++ b/internal/api/server_test.go @@ -13,6 +13,7 @@ import ( "time" "github.com/dynaum/kubeside/internal/exec" + "github.com/dynaum/kubeside/internal/fleet" "github.com/dynaum/kubeside/internal/forward" "github.com/dynaum/kubeside/internal/guard" "github.com/dynaum/kubeside/internal/logs" @@ -92,6 +93,13 @@ func (s stubAPI) Promotion() PromotionView { return PromotionView{Envs: envs, Rows: rows, Summary: promotion.Summarize(rows)} } +func (s stubAPI) Fleet(app, namespace string) fleet.View { + return fleet.View{App: app, Namespace: namespace, Clusters: 2, Present: 2, Newest: "v2", Rows: []fleet.Row{ + {Placement: fleet.Placement{Context: "qa", State: fleet.StatePresent, Tag: "v2"}}, + {Placement: fleet.Placement{Context: "prod", State: fleet.StatePresent, Tag: "v2"}}, + }} +} + func (s stubAPI) StartForward(req ForwardRequest) (forward.Forward, error) { if req.RemotePort == 0 { return forward.Forward{}, errors.New("a container port is required") @@ -1039,3 +1047,59 @@ func TestAWrongCookieIsRefused(t *testing.T) { t.Fatalf("code = %d, want 401", rec.Code) } } + +// TestFleetEndpointRequiresAnApp proves a request naming no app is a 400, not +// an empty screen: the view is about one app by construction, and an empty +// fleet screen would read as "this app runs nowhere", the exact conflation +// this feature exists to prevent. +func TestFleetEndpointRequiresAnApp(t *testing.T) { + s := newTestServer(t) + rec := do(t, s, "GET", "/api/fleet?"+tokenParam+"="+s.Token(), nil) + if rec.Code != http.StatusBadRequest { + t.Errorf("status = %d, want 400: the screen is about one app and needs its name", rec.Code) + } +} + +func TestFleetEndpointReturnsTheView(t *testing.T) { + s := newTestServer(t) + rec := do(t, s, "GET", "/api/fleet?app=checkout&namespace=shop&"+tokenParam+"="+s.Token(), nil) + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, want 200: %s", rec.Code, rec.Body.String()) + } + var v fleet.View + if err := json.NewDecoder(rec.Body).Decode(&v); err != nil { + t.Fatalf("decode: %v", err) + } + if v.App != "checkout" { + t.Errorf("app = %q, want checkout", v.App) + } + if v.Namespace != "shop" { + t.Errorf("namespace = %q, want shop", v.Namespace) + } +} + +// TestFleetEndpointNamespaceIsOptional proves the endpoint does not reject an +// absent namespace: Service.Fleet accepts "" and answers across whichever +// namespace each cluster holds the app in, so the transport must not add a +// requirement the service itself does not have. +func TestFleetEndpointNamespaceIsOptional(t *testing.T) { + s := newTestServer(t) + rec := do(t, s, "GET", "/api/fleet?app=checkout&"+tokenParam+"="+s.Token(), nil) + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, want 200: %s", rec.Code, rec.Body.String()) + } + var v fleet.View + if err := json.NewDecoder(rec.Body).Decode(&v); err != nil { + t.Fatalf("decode: %v", err) + } + if v.Namespace != "" { + t.Errorf("namespace = %q, want empty when not asked for", v.Namespace) + } +} + +func TestFleetEndpointNeedsTheToken(t *testing.T) { + s := newTestServer(t) + if got := do(t, s, "GET", "/api/fleet?app=checkout", nil).Code; got != http.StatusUnauthorized { + t.Fatalf("status = %d, want 401", got) + } +} diff --git a/internal/api/stream_test.go b/internal/api/stream_test.go index da20c07..3cb98f7 100644 --- a/internal/api/stream_test.go +++ b/internal/api/stream_test.go @@ -13,6 +13,7 @@ import ( "github.com/coder/websocket" "github.com/coder/websocket/wsjson" "github.com/dynaum/kubeside/internal/exec" + "github.com/dynaum/kubeside/internal/fleet" "github.com/dynaum/kubeside/internal/forward" "github.com/dynaum/kubeside/internal/guard" "github.com/dynaum/kubeside/internal/logs" @@ -113,6 +114,10 @@ func (s *streamStub) Promotion() PromotionView { return PromotionView{Envs: envs, Rows: rows, Summary: promotion.Summarize(rows)} } +func (s *streamStub) Fleet(app, namespace string) fleet.View { + return fleet.View{App: app, Namespace: namespace} +} + func (s *streamStub) StartForward(req ForwardRequest) (forward.Forward, error) { if req.RemotePort == 0 { return forward.Forward{}, errors.New("a container port is required") From d902bccdf84b9cd3c05e3d5cc5d73687fae23f68 Mon Sep 17 00:00:00 2001 From: Elber Ribeiro Date: Mon, 3 Aug 2026 21:45:27 -0400 Subject: [PATCH 21/38] feat(fleet): screen 7, one app across every cluster Ported from screens/fleet.html in the design project. Five row states render distinctly, an app found nowhere names the clusters it asked rather than showing an empty window, and a tag resolving to two digests raises a banner above everything else on the page. Closes #68 --- web/dist/index.html | 2 +- web/src/App.tsx | 10 +- web/src/FleetScreen.tsx | 240 ++++++++++++++++++++++++++++++++++++++++ web/src/api.ts | 39 +++++++ web/src/commands.ts | 2 +- web/src/route.test.ts | 9 ++ web/src/route.ts | 7 +- 7 files changed, 303 insertions(+), 6 deletions(-) create mode 100644 web/src/FleetScreen.tsx diff --git a/web/dist/index.html b/web/dist/index.html index bdc8a71..66716c9 100644 --- a/web/dist/index.html +++ b/web/dist/index.html @@ -7,7 +7,7 @@ kubeside - + diff --git a/web/src/App.tsx b/web/src/App.tsx index ffc36cd..3311920 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -8,6 +8,7 @@ import { ConfigScreen } from "./ConfigScreen"; import { DiffScreen } from "./DiffScreen"; import { ExecScreen } from "./ExecScreen"; import { PromotionScreen } from "./PromotionScreen"; +import { FleetScreen } from "./FleetScreen"; import { envToken } from "./health"; import { parseRoute, routeHash, type Route } from "./route"; import { Palette } from "./Palette"; @@ -50,7 +51,7 @@ export function App() { // The current context leads; select it so the developer's usual // workspace renders first, unless a permalink named another. setRoute((r) => { - if (r.screen === "promotion") return r; + if (r.screen === "promotion" || r.screen === "fleet") return r; if (r.context && cs.some((c) => c.name === r.context)) return r; const cur = cs.find((c) => c.current) ?? cs[0]; return cur ? { screen: "apps", context: cur.name } : r; @@ -93,7 +94,7 @@ export function App() { ); } - const current = route.screen === "promotion" + const current = route.screen === "promotion" || route.screen === "fleet" ? null : (contexts.find((c) => c.name === route.context) ?? null); @@ -123,7 +124,10 @@ export function App() { } /> )} - {route.screen !== "promotion" && !current && } + {route.screen === "fleet" && ( + + )} + {route.screen !== "promotion" && route.screen !== "fleet" && !current && } {current && route.screen === "apps" && ( = { + unreachable: "no answer", + denied: "no access", + pending: "asking", +}; + +export function FleetScreen({ app, namespace }: { app: string; namespace: string }) { + const [view, setView] = useState(null); + const [err, setErr] = useState(null); + + useEffect(() => { + let alive = true; + setView(null); + setErr(null); + api.fleet(app, namespace) + .then((v) => { if (alive) setView(v); }) + .catch((e) => { if (alive) setErr(String(e.message ?? e)); }); + return () => { alive = false; }; + }, [app, namespace]); + + return ( + <> +
+
+ {app} + · + every cluster +
+ + +
+ +
+
+

{app}

+ + {view + ? `${view.clusters} clusters · ${view.present} running it · ${view.behind} behind${view.newest ? ` · newest is ${view.newest}` : ""}` + : "asking every cluster"} + +
+ + {err && ( +
+
Could not ask the clusters
+
{err}
+
+ )} + {!err && !view && ( +
asking every cluster…
+ )} + + {view?.mutableTag && ( +
+ + + One tag resolves to more than one digest. These clusters claim the same + version and are not running the same code. + +
+ )} + + {view && view.present === 0 && ( +
+
Not found in any of {view.clusters} clusters
+
+ The table below shows what each cluster answered. Check the name, or the namespace it lives in. +
+
+ )} + + {view && ( + <> +
+
+ + + + + + + + + + + + {view.rows.map((r) => ( + + + + + + + + ))} + +
ClusterEnvVersionHealthVerdict
+ {r.context} + {r.aliases && r.aliases.length > 0 && ( + also {r.aliases.join(", ")} + )} + {fleetEnvLabel(r.env)} + + + +
+
+
+ +
+
+
Sorted by what needs attention
+

A mutable tag outranks everything, then a cluster that never answered, then one that is + behind, then one we are not allowed to read, then one present without a tag we can name, then + one still asking. An unread cluster beats a known-old one: the version you cannot see could be + worse than the one you can.

+
+
+
Four ways to have no version
+

A cluster can carry no version for four different reasons: it did not answer, it refused to + let us look, its image is pinned by digest, or the app simply is not deployed there. Rendering + those the same would answer the screen's own question with a guess.

+
+
+
The refusal names the verb
+

A denied row carries the rule that would fix it, not just the word no. A developer can paste + it into a Role request without a second round trip.

+
+
+
Pending is not agreement
+

{view.digestUnverified ?? 0} present row{(view.digestUnverified ?? 0) === 1 ? "" : "s"} still{" "} + {(view.digestUnverified ?? 0) === 1 ? "has" : "have"} no digest, so {(view.digestUnverified ?? 0) === 1 ? "it counts" : "they count"} as + unverified rather than matching. A digest that never arrived cannot be evidence that two + clusters agree.

+
+
+ + )} +
+ + ); +} + +function VersionCell({ r }: { r: FleetRow }) { + if (r.state !== "present") { + return ; + } + if (!r.tag) { + return no tag; + } + const color = r.mutableTag ? "var(--err)" : r.behind ? "var(--warn)" : undefined; + return {r.tag}; +} + +function HealthCell({ r }: { r: FleetRow }) { + if (r.state === "present") { + return ( + <> + + {r.ready && } + + ); + } + if (r.state === "absent") { + return ; + } + const label = NOT_PRESENT_LABEL[r.state] ?? r.state; + if (r.state === "denied") { + return {label}; + } + return ( + <> + + {label} + + ); +} + +function Ratio({ ready }: { ready: string }) { + const i = ready.indexOf("/"); + if (i === -1) { + return {ready}; + } + return ( + + {ready.slice(0, i)} + {ready.slice(i)} + + ); +} + +function Verdict({ r }: { r: FleetRow }) { + if (r.state !== "present") { + return <>{r.note}; + } + const flagged = r.mutableTag || r.behind; + return ( + <> + {r.mutableTag && mutable tag} + {!r.mutableTag && r.behind && behind} + {r.note && {r.note}} + {r.digestPending && ( + digest pending + )} + + ); +} diff --git a/web/src/api.ts b/web/src/api.ts index fdb4355..d0df092 100644 --- a/web/src/api.ts +++ b/web/src/api.ts @@ -287,6 +287,43 @@ export interface PromotionView { unreachable?: string[]; } +export interface FleetRow { + context: string; + clusterId: string; + env: string; + namespace?: string; + state: string; // present, absent, denied, unreachable, pending + reason?: string; + image?: string; + tag?: string; + digest?: string; + digestPending?: boolean; + health?: string; + ready?: string; + revisionAt?: string; + // Other kubeconfig contexts pointing at this same cluster, merged into this row. + aliases?: string[]; + behind?: boolean; + // This row's tag also resolves to a different digest on another cluster. + mutableTag?: boolean; + // The verdict sentence for this row, already composed by the backend. + note?: string; +} + +export interface FleetView { + app: string; + namespace: string; + rows: FleetRow[]; + newest?: string; + clusters: number; + present: number; + behind: number; + mutableTag?: boolean; + // Present rows whose digest never arrived, so cannot be ruled out of a + // mutable-tag match. + digestUnverified?: number; +} + export interface MetricsInfo { source: string; available: boolean; @@ -350,6 +387,8 @@ export const api = { reveal: (context: string, namespace: string, secret: string, key: string, workload: string) => post("/api/secret", { context, namespace, secret, key, workload }), promotion: () => get("/api/promotion"), + fleet: (app: string, namespace: string) => + get(`/api/fleet?app=${encodeURIComponent(app)}&namespace=${encodeURIComponent(namespace)}`), gate: (req: { context: string; namespace: string; diff --git a/web/src/commands.ts b/web/src/commands.ts index 4030682..8cded89 100644 --- a/web/src/commands.ts +++ b/web/src/commands.ts @@ -162,7 +162,7 @@ export function commands( // routeTarget is the app the current screen is about, when it is about one. function routeTarget(route: Route): { namespace: string; workload: string } | null { - if (route.screen === "apps" || route.screen === "promotion") return null; + if (route.screen === "apps" || route.screen === "promotion" || route.screen === "fleet") return null; return { namespace: route.namespace, workload: route.workload }; } diff --git a/web/src/route.test.ts b/web/src/route.test.ts index 98fbc31..0e3b355 100644 --- a/web/src/route.test.ts +++ b/web/src/route.test.ts @@ -31,4 +31,13 @@ describe("parseRoute", () => { screen: "logs", context: "arn:aws:eks:us-east-1/x", namespace: "team-a", workload: "check/out", }); }); + + it("round-trips the fleet route", () => { + const r = { screen: "fleet", app: "checkout", namespace: "shop" } as const; + expect(parseRoute(routeHash(r))).toEqual(r); + }); + + it("falls back to apps when the fleet route is missing its app", () => { + expect(parseRoute("#fleet")).toEqual({ screen: "apps" }); + }); }); diff --git a/web/src/route.ts b/web/src/route.ts index f8abfe6..833fa3b 100644 --- a/web/src/route.ts +++ b/web/src/route.ts @@ -8,7 +8,8 @@ export type Route = | { screen: "config"; context: string; namespace: string; workload: string } | { screen: "exec"; context: string; namespace: string; workload: string } | { screen: "diff"; context: string; namespace: string; workload: string; other?: string } - | { screen: "logs"; context: string; namespace: string; workload: string }; + | { screen: "logs"; context: string; namespace: string; workload: string } + | { screen: "fleet"; app: string; namespace: string }; export function parseRoute(hash: string): Route { const parts = hash.replace(/^#\/?/, "").split("/").filter(Boolean).map(decodeURIComponent); @@ -22,6 +23,9 @@ export function parseRoute(hash: string): Route { context: parts[1], namespace: parts[2], workload: parts[3], }; } + if (parts[0] === "fleet" && parts.length >= 3) { + return { screen: "fleet", app: parts[1], namespace: parts[2] }; + } if (parts[0] === "apps" && parts.length >= 2) { return { screen: "apps", context: parts[1] }; } @@ -38,5 +42,6 @@ export function routeHash(r: Route): string { return `#${r.screen}/${enc(r.context)}/${enc(r.namespace)}/${enc(r.workload)}`; } if (r.screen === "promotion") return "#promotion"; + if (r.screen === "fleet") return `#fleet/${enc(r.app)}/${enc(r.namespace)}`; return r.context ? `#apps/${enc(r.context)}` : "#apps"; } From 4484d7530b3ace5ea2042197a9915567079b115b Mon Sep 17 00:00:00 2001 From: Elber Ribeiro Date: Mon, 3 Aug 2026 22:00:51 -0400 Subject: [PATCH 22/38] fix(fleet): the fleet screen called prod clusters unclassified environments.Classify matches by keyword token and returns the name it was handed, so prod-us-east, production, live, staging-eks, uat and sandbox-3 are all classified while spelling no tier a screen could compare against. The fleet screen tested the name against the literal set {qa, stg, prod}, painted everything else violet, and printed the word "unclassified" over a cluster the promotion matrix and the shell both render red. Environment colour is a safety layer in docs/04-multi-cluster.md, so that was a false statement about a safety property, not a missing colour. Placement now carries EnvColor and EnvRisk beside Env, populated in the one literal every state path returns, so an unreachable and a denied row keep their colour too. The screen reads envToken off the row and prints the resolved name verbatim, the way every other screen does. The rule stays in one language: a tokenizer reimplemented in TypeScript would disagree with the binary the first time someone edits tierRules. Also on the screen: a row whose namespace differs from the one asked for now says so, since identity matching strips the environment token and the substitution was invisible; the mutable-tag banner derives its edge from the rows actually in conflict rather than inheriting the shell's; the digest note stops reporting zero as a problem; the severity card stops enumerating a ladder it cannot keep in step with; and an empty kubeconfig gets a card naming the cause instead of a header-only table. --- .../specs/2026-08-02-fleet-design.md | 10 ++- internal/api/fleet.go | 6 ++ internal/api/fleet_test.go | 47 +++++++++++ internal/fleet/fleet.go | 8 ++ web/dist/index.html | 2 +- web/src/FleetScreen.tsx | 84 ++++++++++++------- web/src/api.ts | 5 ++ web/src/health.test.ts | 56 ++++++++++++- web/src/health.ts | 15 ++++ 9 files changed, 201 insertions(+), 32 deletions(-) diff --git a/docs/superpowers/specs/2026-08-02-fleet-design.md b/docs/superpowers/specs/2026-08-02-fleet-design.md index 430500b..cb53a6a 100644 --- a/docs/superpowers/specs/2026-08-02-fleet-design.md +++ b/docs/superpowers/specs/2026-08-02-fleet-design.md @@ -88,7 +88,15 @@ New package `internal/fleet`. One type carries one app in one cluster. type Placement struct { Context string // kubeconfig context name, personal and unstable ClusterID string // API server URL, from kubeconfig.Context.Server - Env string // resolved environment name, "unclassified" when unmatched + // Env is the resolved environment name, untouched: environments.Classify + // matches by keyword token and returns the name it was handed, so + // "prod-us-east" is high risk while spelling no tier a reader could + // compare against. EnvColor and EnvRisk carry that classification, so the + // UI maps a decision rather than re-deriving one from the string. + Env string + EnvColor string // red | amber | green | violet + EnvRisk string // low | medium | high, unknown collapsing to high + Namespace string State string // present | absent | denied | unreachable | pending diff --git a/internal/api/fleet.go b/internal/api/fleet.go index 34f93d5..53398f1 100644 --- a/internal/api/fleet.go +++ b/internal/api/fleet.go @@ -71,10 +71,16 @@ func (s *Service) placeApp(ctx context.Context, name, app, namespace, want strin // instead would leave an unreachable duplicate with an empty // ClusterID, which fails to merge with its reachable twin and // inflates Clusters in exactly the case the merge exists to prevent. + // Colour and risk travel with the name on every path below, including the + // failure paths: a cluster that refused to answer is exactly the row a + // developer needs to place as prod at a glance, and the resolved name alone + // does not say so. p := fleet.Placement{ Context: name, ClusterID: config.NormalizeURL(kctx.Server), Env: env.Name, + EnvColor: env.Color, + EnvRisk: env.Risk.String(), } if err := s.mgr.Connect(ctx, name); err != nil { diff --git a/internal/api/fleet_test.go b/internal/api/fleet_test.go index 4610315..2ce0788 100644 --- a/internal/api/fleet_test.go +++ b/internal/api/fleet_test.go @@ -367,6 +367,53 @@ func TestTwoNamespacesInOneClusterSayWhichOneWon(t *testing.T) { } } +// TestEveryRowCarriesTheEnvironmentColour pins the fact the UI cannot infer. +// +// environments.Classify matches by keyword token and returns the name it was +// given, so "production", "staging" and "sandbox" are red, amber and green +// while being none of the three tier strings a screen could compare against. +// A UI left to guess from the name alone paints a prod cluster violet and calls +// it unclassified, which is a false statement about the safety property +// docs/04-multi-cluster.md makes environment colour carry. +// +// Every state path goes through one placement literal, so a cluster that never +// answered and a cluster that refused to be read keep their colour too. Those +// are the rows a developer most needs to place. +func TestEveryRowCarriesTheEnvironmentColour(t *testing.T) { + s := serviceWithContexts(t, map[string]fakeCluster{ + "prod-us-east": {env: "production", apps: []fakeApp{{name: "checkout", ns: "shop", image: "reg/checkout:v2.14.0"}}}, + "staging-eks": {env: "staging", unreachable: true}, + "qa-cluster": {env: "sandbox", refusedKinds: []string{"Deployment"}}, + "dr-frankfurt": {env: "dr-frankfurt"}, + }) + + v := s.Fleet("checkout", "shop") + + for _, want := range []struct { + ctx, state, env, color, risk string + }{ + {"prod-us-east", fleet.StatePresent, "production", "red", "high"}, + {"staging-eks", fleet.StateUnreachable, "staging", "amber", "medium"}, + {"qa-cluster", fleet.StateDenied, "sandbox", "green", "low"}, + {"dr-frankfurt", fleet.StateAbsent, "dr-frankfurt", "violet", "high"}, + } { + r := rowFor(t, v, want.ctx) + if r.State != want.state { + t.Errorf("%s state = %q, want %q", want.ctx, r.State, want.state) + } + if r.Env != want.env { + t.Errorf("%s env = %q, want %q: the resolved name travels untouched", want.ctx, r.Env, want.env) + } + if r.EnvColor != want.color { + t.Errorf("%s envColor = %q, want %q: the UI must not re-derive the tier from the name", + want.ctx, r.EnvColor, want.color) + } + if r.EnvRisk != want.risk { + t.Errorf("%s envRisk = %q, want %q", want.ctx, r.EnvRisk, want.risk) + } + } +} + // TestFleetSpendsTheTimeoutPerClusterNotAcrossThem pins what the flag means. // Four clusters that each take a moment must cost one moment, not four: a // serial sweep on one shared budget hands the last clusters an expired deadline diff --git a/internal/fleet/fleet.go b/internal/fleet/fleet.go index 1c8096f..7f9f87d 100644 --- a/internal/fleet/fleet.go +++ b/internal/fleet/fleet.go @@ -31,6 +31,14 @@ type Placement struct { Context string `json:"context"` ClusterID string `json:"clusterId"` Env string `json:"env"` + // EnvColor and EnvRisk carry the classification the resolved name cannot. + // environments.Classify matches by keyword token and returns the name it + // was handed, so "prod-us-east" and "production" are red without spelling + // any tier a reader could compare against. Shipping the decision instead of + // the name keeps that rule in one language: a UI re-deriving it from the + // string disagrees with this binary the first time a keyword is added. + EnvColor string `json:"envColor,omitempty"` + EnvRisk string `json:"envRisk,omitempty"` Namespace string `json:"namespace,omitempty"` State string `json:"state"` diff --git a/web/dist/index.html b/web/dist/index.html index 66716c9..2443034 100644 --- a/web/dist/index.html +++ b/web/dist/index.html @@ -7,7 +7,7 @@ kubeside - + diff --git a/web/src/FleetScreen.tsx b/web/src/FleetScreen.tsx index 4311164..916d026 100644 --- a/web/src/FleetScreen.tsx +++ b/web/src/FleetScreen.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from "react"; import { api, type FleetRow, type FleetView } from "./api"; import { Glyph } from "./Status"; import { routeHash } from "./route"; -import type { EnvToken } from "./health"; +import { envToken, loudestEnv } from "./health"; // Screen 7. Is every cluster running the latest version. // @@ -13,21 +13,12 @@ import type { EnvToken } from "./health"; // Five states, never conflated. A cluster behind a VPN is not a cluster // without the app, and rendering both blank would answer the screen's own // question with a guess. - -const KNOWN_ENVS = new Set(["qa", "stg", "prod"]); - -// Row.env is a resolved environment name, not necessarily one of the four -// design tokens: a context left unbound in the config file carries its own -// name (or the config's own label) here. Anything that is not one of the -// three classified tiers renders as unclassified, the same fallback the rest -// of the app gives an environment nobody named. -function fleetEnvToken(env: string): EnvToken { - return KNOWN_ENVS.has(env) ? (env as EnvToken) : "unc"; -} - -function fleetEnvLabel(env: string): string { - return KNOWN_ENVS.has(env) ? env : "unclassified"; -} +// +// Environment colour is read off the row, never derived from its name. The +// backend classifies by keyword token and returns the name untouched, so +// prod-us-east and production are red while spelling no tier this screen could +// compare against. The chip prints the resolved name verbatim, the way every +// other screen does, and data-env carries the colour. const NOT_PRESENT_LABEL: Record = { unreachable: "no answer", @@ -84,7 +75,11 @@ export function FleetScreen({ app, namespace }: { app: string; namespace: string )} {view?.mutableTag && ( -
+
r.mutableTag).map((r) => ({ color: r.envColor, risk: r.envRisk })))} + style={{ marginBottom: "var(--s4)" }} + > One tag resolves to more than one digest. These clusters claim the same @@ -93,7 +88,17 @@ export function FleetScreen({ app, namespace }: { app: string; namespace: string
)} - {view && view.present === 0 && ( + {view && view.clusters === 0 && ( +
+
No clusters to ask
+
+ The kubeconfig names no contexts, so nothing was asked about {app}. A table of nothing would + read as an answer, and there is none yet. +
+
+ )} + + {view && view.clusters > 0 && view.present === 0 && (
Not found in any of {view.clusters} clusters
@@ -102,7 +107,7 @@ export function FleetScreen({ app, namespace }: { app: string; namespace: string
)} - {view && ( + {view && view.clusters > 0 && ( <>
@@ -118,14 +123,21 @@ export function FleetScreen({ app, namespace }: { app: string; namespace: string {view.rows.map((r) => ( - + {r.context} {r.aliases && r.aliases.length > 0 && ( also {r.aliases.join(", ")} )} + {/* The match is by identity, which strips the environment token, so + a row can report a different namespace from the one asked for. + Nothing else on the screen names it, and a substitution nobody + sees is a substitution nobody can challenge. */} + {r.namespace && r.namespace !== namespace && ( + in {r.namespace}, not {namespace} + )} - {fleetEnvLabel(r.env)} + {r.env} @@ -143,10 +155,9 @@ export function FleetScreen({ app, namespace }: { app: string; namespace: string
Sorted by what needs attention
-

A mutable tag outranks everything, then a cluster that never answered, then one that is - behind, then one we are not allowed to read, then one present without a tag we can name, then - one still asking. An unread cluster beats a known-old one: the version you cannot see could be - worse than the one you can.

+

A mutable tag sits at the top, and below it the less a cluster told us the higher it sits: + the version you cannot see could be worse than the old one you can. A cluster the app is not + deployed to stays with the healthy ones, because that is a schedule and not a disagreement.

Four ways to have no version
@@ -161,10 +172,7 @@ export function FleetScreen({ app, namespace }: { app: string; namespace: string
Pending is not agreement
-

{view.digestUnverified ?? 0} present row{(view.digestUnverified ?? 0) === 1 ? "" : "s"} still{" "} - {(view.digestUnverified ?? 0) === 1 ? "has" : "have"} no digest, so {(view.digestUnverified ?? 0) === 1 ? "it counts" : "they count"} as - unverified rather than matching. A digest that never arrived cannot be evidence that two - clusters agree.

+
@@ -174,6 +182,24 @@ export function FleetScreen({ app, namespace }: { app: string; namespace: string ); } +// A fleet with every digest resolved has no count to report, and printing +// "0 present rows still have no digest" states a problem nobody has. The rule +// still belongs on the screen, so it falls back to the rule itself. +function DigestNote({ count }: { count: number }) { + if (count === 0) { + return ( +

A present row whose digest never arrived counts as unverified rather than matching. A digest that + never arrived cannot be evidence that two clusters agree.

+ ); + } + const one = count === 1; + return ( +

{count} present row{one ? "" : "s"} still {one ? "has" : "have"} no digest, so {one ? "it counts" : "they count"} as + unverified rather than matching. A digest that never arrived cannot be evidence that two clusters + agree.

+ ); +} + function VersionCell({ r }: { r: FleetRow }) { if (r.state !== "present") { return ; diff --git a/web/src/api.ts b/web/src/api.ts index d0df092..7602d79 100644 --- a/web/src/api.ts +++ b/web/src/api.ts @@ -291,6 +291,11 @@ export interface FleetRow { context: string; clusterId: string; env: string; + // The backend's classification of that environment. The name alone cannot be + // classified here: it is matched by keyword token and returned untouched, so + // prod-us-east is red while spelling no tier the UI could compare against. + envColor?: string; + envRisk?: string; namespace?: string; state: string; // present, absent, denied, unreachable, pending reason?: string; diff --git a/web/src/health.test.ts b/web/src/health.test.ts index 46cff11..2351f18 100644 --- a/web/src/health.test.ts +++ b/web/src/health.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from "vitest"; -import { envToken } from "./health"; +import { envToken, loudestEnv } from "./health"; +import type { FleetRow } from "./api"; describe("envToken", () => { it("maps the colours the backend names", () => { @@ -23,3 +24,56 @@ describe("envToken", () => { expect(envToken({ risk: "" })).toBe("unc"); }); }); + +// A fleet row names its environment and carries the backend's verdict on it +// under its own fields. The name alone cannot be classified here: Classify +// matches by keyword token and returns the name untouched, so prod-us-east is +// red while spelling no tier a screen could compare against. Reading colour and +// risk keeps one rule in one language. +describe("envToken on a fleet row", () => { + const row = (env: string, envColor: string, envRisk: string): FleetRow => + ({ context: env, clusterId: "https://" + env, env, envColor, envRisk, state: "present" }); + + it("colours a cluster the backend classified, whatever the cluster is called", () => { + const cases: [FleetRow, string][] = [ + [row("prod-us-east", "red", "high"), "prod"], + [row("production", "red", "high"), "prod"], + [row("staging-eks", "amber", "medium"), "stg"], + [row("uat", "amber", "medium"), "stg"], + [row("sandbox-3", "green", "low"), "qa"], + [row("dev-cluster", "green", "low"), "qa"], + [row("dr-frankfurt", "violet", "high"), "unc"], + ]; + for (const [r, want] of cases) { + expect(envToken({ color: r.envColor, risk: r.envRisk })).toBe(want); + } + }); + + // A row from an older server, or one whose environment nobody resolved, + // still lands on unclassified rather than on a tier it did not earn. + it("falls back to unclassified when a row carries no classification", () => { + const bare: FleetRow = { context: "edge", clusterId: "", env: "edge", state: "absent" }; + expect(envToken({ color: bare.envColor, risk: bare.envRisk })).toBe("unc"); + }); +}); + +// One banner speaks for several clusters, so it takes the edge of the loudest +// environment it names rather than inheriting the shell's. +describe("loudestEnv", () => { + it("takes the loudest environment in the set", () => { + expect(loudestEnv([{ color: "green" }, { color: "red" }, { color: "amber" }])).toBe("prod"); + expect(loudestEnv([{ color: "green" }, { color: "amber" }])).toBe("stg"); + expect(loudestEnv([{ color: "green" }])).toBe("qa"); + }); + + // Unclassified carries prod-strength styling, so it outranks the tiers below + // it. A named prod cluster still wins: red is the edge that reads. + it("ranks unclassified above the tiers it outranks and below prod", () => { + expect(loudestEnv([{ color: "violet" }, { color: "green" }])).toBe("unc"); + expect(loudestEnv([{ color: "violet" }, { color: "red" }])).toBe("prod"); + }); + + it("treats an empty set as unclassified", () => { + expect(loudestEnv([])).toBe("unc"); + }); +}); diff --git a/web/src/health.ts b/web/src/health.ts index bbef7ba..a557e4f 100644 --- a/web/src/health.ts +++ b/web/src/health.ts @@ -30,3 +30,18 @@ const BY_RISK: Record = { export function envToken(env: { color?: string; risk?: string }): EnvToken { return BY_COLOR[env.color ?? ""] ?? BY_RISK[env.risk ?? ""] ?? "unc"; } + +// One warning can name several clusters at once, and it belongs to the loudest +// of them rather than to whichever environment the shell happens to be in. +// +// Unclassified outranks the tiers below it, because it carries prod-strength +// styling everywhere else in the app. A cluster the backend did classify as +// prod still wins: when a red cluster is in the set, red is the edge that +// reads. An empty set is unclassified, never safe. +const LOUDNESS: Record = { prod: 3, unc: 2, stg: 1, qa: 0 }; + +export function loudestEnv(envs: { color?: string; risk?: string }[]): EnvToken { + const tokens = envs.map(envToken); + if (tokens.length === 0) return "unc"; + return tokens.reduce((a, b) => (LOUDNESS[b] > LOUDNESS[a] ? b : a)); +} From b38d244e5aa3262e78fe799ab79783918e93c59b Mon Sep 17 00:00:00 2001 From: Elber Ribeiro Date: Mon, 3 Aug 2026 22:09:16 -0400 Subject: [PATCH 23/38] feat(promotion): render the split cell and link it to fleet An environment whose clusters disagree shows the disagreement and offers the screen that explains it, rather than a version the matrix never verified. Closes #69 --- web/dist/index.html | 2 +- web/src/App.tsx | 1 + web/src/PromotionScreen.tsx | 32 ++++++++++++++++++++++++++++++-- web/src/api.ts | 18 +++++++++++++++--- 4 files changed, 47 insertions(+), 6 deletions(-) diff --git a/web/dist/index.html b/web/dist/index.html index 2443034..4934457 100644 --- a/web/dist/index.html +++ b/web/dist/index.html @@ -7,7 +7,7 @@ kubeside - + diff --git a/web/src/App.tsx b/web/src/App.tsx index 3311920..1db9b87 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -122,6 +122,7 @@ export function App() { onOpenApp={(context, namespace, workload) => context && go({ screen: "app", context, namespace, workload }) } + onOpenFleet={(app, namespace) => go({ screen: "fleet", app, namespace })} /> )} {route.screen === "fleet" && ( diff --git a/web/src/PromotionScreen.tsx b/web/src/PromotionScreen.tsx index 3b184c8..3cc2016 100644 --- a/web/src/PromotionScreen.tsx +++ b/web/src/PromotionScreen.tsx @@ -10,7 +10,13 @@ import { age } from "./detail"; // because deploying belongs to the pipeline that owns it, and a bulk action // across environments is precisely the button nobody should have. -export function PromotionScreen({ onOpenApp }: { onOpenApp: (context: string, namespace: string, workload: string) => void }) { +export function PromotionScreen({ + onOpenApp, + onOpenFleet, +}: { + onOpenApp: (context: string, namespace: string, workload: string) => void; + onOpenFleet: (app: string, namespace: string) => void; +}) { const [view, setView] = useState(null); const [err, setErr] = useState(null); @@ -34,6 +40,7 @@ export function PromotionScreen({ onOpenApp }: { onOpenApp: (context: string, na {view && ( {view.summary.apps} apps · {view.summary.drifted} with drift · {view.summary.ahead} ahead of upstream + {view.summary.split > 0 && ` · ${view.summary.split} split across clusters`} )}
@@ -88,7 +95,11 @@ export function PromotionScreen({ onOpenApp }: { onOpenApp: (context: string, na onOpenApp(view.envs[i]?.context ?? "", c.namespace ?? r.namespace, r.app)} + onOpen={() => + c.state === "split" + ? onOpenFleet(r.app, c.namespace ?? r.namespace) + : onOpenApp(view.envs[i]?.context ?? "", c.namespace ?? r.namespace, r.app) + } /> ))} @@ -140,6 +151,23 @@ function CellBody({ cell: c, onOpen }: { cell: PromotionCell; onOpen: () => void ); } + if (c.state === "split") { + // A split cell carries no tag: picking one is the guess this state exists + // to prevent. The note is composed by the backend and rendered verbatim. + return ( + <> + + {c.note} + + {c.clusters ? {c.clusters} clusters : null} + + ); + } const colour = c.severe ? "var(--err)" : c.state === "behind" || c.state === "differs" ? "var(--warn)" : undefined; diff --git a/web/src/api.ts b/web/src/api.ts index 7602d79..d2d44fe 100644 --- a/web/src/api.ts +++ b/web/src/api.ts @@ -260,7 +260,7 @@ export interface GateView { export interface PromotionCell { env: string; namespace?: string; - state: string; // same, behind, ahead, differs, digest-differs, absent, denied + state: string; // same, behind, ahead, differs, digest-differs, absent, denied, split tag?: string; image?: string; digest?: string; @@ -270,6 +270,9 @@ export interface PromotionCell { revisionAt?: string; note?: string; severe?: boolean; + // Clusters is how many contexts back this cell. Set on split (and other) + // cells; the UI shows the count only above one. + clusters?: number; } export interface PromotionRow { @@ -281,9 +284,18 @@ export interface PromotionRow { } export interface PromotionView { - envs: { name: string; risk: string; context?: string }[]; + envs: { + name: string; + risk: string; + context?: string; + // Contexts is every context bound to this environment. Unreadable names + // the ones that did not answer; a column with any of those can never + // claim agreement, because the clusters nobody read might disagree. + contexts?: string[]; + unreadable?: string[]; + }[]; rows: PromotionRow[]; - summary: { apps: number; drifted: number; ahead: number }; + summary: { apps: number; drifted: number; ahead: number; split: number }; unreachable?: string[]; } From bbff0b9f230997f1924d8ef5e6202443d8770235 Mon Sep 17 00:00:00 2001 From: Elber Ribeiro Date: Mon, 3 Aug 2026 22:10:55 -0400 Subject: [PATCH 24/38] feat(fleet): reach the screen from the palette and app detail The command appears only with an app in hand, because the view is about one app and a palette entry that opens an empty one is a dead end. Closes #70 --- web/dist/index.html | 2 +- web/src/App.tsx | 1 + web/src/AppDetailScreen.tsx | 5 +++++ web/src/commands.test.ts | 15 +++++++++++++++ web/src/commands.ts | 7 +++++++ 5 files changed, 29 insertions(+), 1 deletion(-) diff --git a/web/dist/index.html b/web/dist/index.html index 4934457..04cf4b2 100644 --- a/web/dist/index.html +++ b/web/dist/index.html @@ -7,7 +7,7 @@ kubeside - + diff --git a/web/src/App.tsx b/web/src/App.tsx index 1db9b87..c408236 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -148,6 +148,7 @@ export function App() { onNavigate={(screen) => go({ screen, context: current.name, namespace: route.namespace, workload: route.workload }) } + onOpenFleet={() => go({ screen: "fleet", app: route.workload, namespace: route.namespace })} onBack={() => go({ screen: "apps", context: current.name })} /> )} diff --git a/web/src/AppDetailScreen.tsx b/web/src/AppDetailScreen.tsx index 8c80c24..4bb5097 100644 --- a/web/src/AppDetailScreen.tsx +++ b/web/src/AppDetailScreen.tsx @@ -22,12 +22,14 @@ export function AppDetailScreen({ namespace, workload, onNavigate, + onOpenFleet, onBack, }: { context: ContextView; namespace: string; workload: string; onNavigate: (screen: "config" | "logs" | "diff" | "exec") => void; + onOpenFleet: () => void; onBack: () => void; }) { const [view, setView] = useState(null); @@ -154,6 +156,9 @@ export function AppDetailScreen({ +
{confirmingForward && ( diff --git a/web/src/commands.test.ts b/web/src/commands.test.ts index cc7c0ef..945abb0 100644 --- a/web/src/commands.test.ts +++ b/web/src/commands.test.ts @@ -36,6 +36,21 @@ describe("commands", () => { expect(some.filter((c) => c.group === "Actions").length).toBeGreaterThan(2); }); + // The fleet view is about one app; a command that opens it with nothing in + // hand would be a dead end, so it only appears when a screen names one. + it("offers fleet for the app in hand", () => { + const list = commands([], ctx("prod", "prod"), [], appRoute); + const fleet = list.find((c) => c.id === "action:fleet"); + expect(fleet).toBeTruthy(); + expect(fleet!.label.toLowerCase()).toContain("cluster"); + expect(fleet!.route).toEqual({ screen: "fleet", app: "payments", namespace: "team-a" }); + }); + + it("hides fleet when no app is selected", () => { + const list = commands([], ctx("prod", "prod"), [], appsRoute); + expect(list.find((c) => c.id === "action:fleet")).toBeUndefined(); + }); + it("offers the other environments as switches", () => { const list = commands([ctx("prod", "prod"), ctx("stg", "stg")], ctx("prod", "prod"), [], appsRoute); const envs = list.filter((c) => c.group === "Environments"); diff --git a/web/src/commands.ts b/web/src/commands.ts index 8cded89..92c0253 100644 --- a/web/src/commands.ts +++ b/web/src/commands.ts @@ -98,6 +98,13 @@ export function commands( hint: "what changed, and when", route: { screen: "app", context: current.name, namespace, workload }, }, + { + id: "action:fleet", + group: "Actions", + label: `${workload} across every cluster`, + hint: "asks every context in your kubeconfig", + route: { screen: "fleet", app: workload, namespace }, + }, ); } From cfe61881d60aa84eb7cef9de9798ca978706f531 Mon Sep 17 00:00:00 2001 From: Elber Ribeiro Date: Tue, 4 Aug 2026 15:46:26 -0400 Subject: [PATCH 25/38] test(fleet): degraded mode, extracted UI logic, and a screenshot baseline One cluster unreachable, one denied, and the screen still fills, with the two rendering differently: conflating them is the failure the five states exist to prevent. FleetScreen.tsx carried logic vitest could never reach, since the include pattern skips .tsx and there is no jsdom in this package. The not-present label map, the ready-ratio split, and the verdict badge precedence now live in fleet.ts beside it, unit tested, with rendered output unchanged. A Playwright spec exercises every reachable row state in one view and carries the fix for the layout bug that state made visible: the Env column was sized for prod/stg/qa, and a resolved name like prod-us-east overflowed it and pushed Verdict out of place. Closes #71 --- internal/api/degraded_test.go | 39 ++++++++++++ web/dist/index.html | 2 +- web/e2e/fixtures.mjs | 41 +++++++++++++ web/e2e/screens.spec.ts | 17 ++++++ .../fleet-chromium-darwin.png | Bin 0 -> 98007 bytes web/e2e/server.mjs | 1 + web/src/FleetScreen.tsx | 28 ++++----- web/src/fleet.test.ts | 56 ++++++++++++++++++ web/src/fleet.ts | 46 ++++++++++++++ 9 files changed, 213 insertions(+), 17 deletions(-) create mode 100644 web/e2e/screens.spec.ts-snapshots/fleet-chromium-darwin.png create mode 100644 web/src/fleet.test.ts create mode 100644 web/src/fleet.ts diff --git a/internal/api/degraded_test.go b/internal/api/degraded_test.go index ece20a6..63cd503 100644 --- a/internal/api/degraded_test.go +++ b/internal/api/degraded_test.go @@ -9,6 +9,7 @@ import ( "github.com/dynaum/kubeside/internal/clusters" "github.com/dynaum/kubeside/internal/config" + "github.com/dynaum/kubeside/internal/fleet" "github.com/dynaum/kubeside/internal/kubeconfig" "github.com/dynaum/kubeside/internal/timeline" appsv1 "k8s.io/api/apps/v1" @@ -419,6 +420,44 @@ func TestExecRefusesOnAnUnreachableCluster(t *testing.T) { } } +// The fleet screen exists precisely because a developer's clusters are never +// uniformly reachable: one is behind an expired VPN session, one refuses an +// RBAC-scoped read, and the rest just answer. All three sit on one screen at +// once, and the two kinds of silence must not look like each other. +func TestFleetFillsWithOneClusterUnreachableAndOneDenied(t *testing.T) { + s := serviceWithContexts(t, map[string]fakeCluster{ + "qa-cluster": {env: "qa", apps: []fakeApp{{name: "checkout", ns: "shop", image: "reg/checkout:v2.14.0"}}}, + "prod-us-east": {env: "prod", unreachable: true}, + // refusedKinds is the genuine denial fixture: clusters.Fetch never + // errors on an RBAC refusal, it records the kind in Snapshot.Partial. + // Deployment can carry an app, unlike Pod or ReplicaSet, which the + // fleet sweep deliberately excludes from denial. + "prod-eu-west": {env: "prod", refusedKinds: []string{"Deployment"}}, + }) + + v := s.Fleet("checkout", "shop") + + if v.Clusters != 3 { + t.Fatalf("clusters = %d, want 3: every cluster asked appears", v.Clusters) + } + if v.Newest != "v2.14.0" { + t.Errorf("newest = %q; the reachable cluster still answers", v.Newest) + } + states := map[string]string{} + for _, r := range v.Rows { + states[r.Context] = r.State + } + if states["prod-us-east"] != fleet.StateUnreachable { + t.Errorf("prod-us-east state = %q, want %q", states["prod-us-east"], fleet.StateUnreachable) + } + if states["prod-eu-west"] != fleet.StateDenied { + t.Errorf("prod-eu-west state = %q, want %q", states["prod-eu-west"], fleet.StateDenied) + } + if states["prod-us-east"] == states["prod-eu-west"] { + t.Error("unreachable and denied render the same; they are different facts") + } +} + // denied answers a SelfSubjectAccessReview with a no, the way a cluster does // for a reader whose role does not cover the question. func denied(a ktesting.Action) runtime.Object { diff --git a/web/dist/index.html b/web/dist/index.html index 04cf4b2..fd2d50a 100644 --- a/web/dist/index.html +++ b/web/dist/index.html @@ -7,7 +7,7 @@ kubeside - + diff --git a/web/e2e/fixtures.mjs b/web/e2e/fixtures.mjs index 52d5f97..42f4ca5 100644 --- a/web/e2e/fixtures.mjs +++ b/web/e2e/fixtures.mjs @@ -142,6 +142,47 @@ export const promotionView = { summary: { apps: 4, drifted: 3, ahead: 1 }, }; +// One row per reachable state the fleet screen distinguishes: present (one +// healthy and current, one present but behind with its digest still +// unverified), absent, denied, and unreachable. Pending has no producer from +// Service.Fleet by construction, so it is not represented here. +export const fleetView = { + app: "payments", namespace: "team-a", + newest: "v1.8.2", clusters: 5, present: 2, behind: 1, mutableTag: false, digestUnverified: 1, + rows: [ + { + context: "qa1", clusterId: "qa1", env: "qa", envColor: "green", envRisk: "low", + namespace: "team-a", state: "present", + image: "ghcr.io/acme/payments:1.8.2", tag: "v1.8.2", + digest: "sha256:aaaa000000000000000000000000000000000000000000000000000000000000", + health: "healthy", ready: "2/2", revisionAt: "2026-07-24T09:00:00Z", behind: false, + }, + { + // The env column prints the resolved name verbatim rather than a + // qa/stg/prod token, and this is the row the layout bug lived on: + // "prod-us-east" uppercase with .09em tracking overflowed the old + // 84px Env column and pushed Verdict out of place. + context: "prod-us-east", clusterId: "prod-us-east", env: "prod-us-east", envColor: "red", envRisk: "high", + namespace: "team-a", state: "present", + image: "ghcr.io/acme/payments:1.8.0", tag: "v1.8.0", digestPending: true, + health: "degraded", ready: "5/6", revisionAt: "2026-07-14T09:12:00Z", + behind: true, note: "behind v1.8.2", + }, + { + context: "prod-eu-west", clusterId: "", env: "prod-eu-west", envColor: "red", envRisk: "high", + state: "unreachable", note: "the cluster did not answer", + }, + { + context: "stg1", clusterId: "stg1", env: "stg", envColor: "amber", envRisk: "medium", + state: "denied", note: "list deployments forbidden; ask for list deployments in team-a", + }, + { + context: "dr-frankfurt", clusterId: "dr-frankfurt", env: "dr-frankfurt", envColor: "violet", envRisk: "high", + namespace: "team-a", state: "absent", note: "not deployed here", + }, + ], +}; + export const capabilities = { context: "qa1", namespace: "team-a", can: { diff --git a/web/e2e/screens.spec.ts b/web/e2e/screens.spec.ts index 32fdce8..809eafb 100644 --- a/web/e2e/screens.spec.ts +++ b/web/e2e/screens.spec.ts @@ -70,6 +70,23 @@ test("promotion matrix", async ({ page }) => { await expect(page).toHaveScreenshot("promotion.png", { fullPage: true }); }); +test("fleet", async ({ page }) => { + await page.goto(`/${token}#fleet/payments/team-a`); + await expect(page.getByRole("heading", { name: "payments" })).toBeVisible(); + await expect(page.locator("table.tbl tbody tr")).toHaveCount(5); + // Unreachable and denied are different facts and must read differently, not + // as two spellings of "no answer". + await expect(page.locator("table.tbl tbody tr", { hasText: "prod-eu-west" })).toContainText("no answer"); + await expect(page.locator("table.tbl tbody tr", { hasText: "stg1" })).toContainText("no access"); + // A cluster the app is not deployed to is neither: a plain dash, no note. + await expect(page.locator("table.tbl tbody tr", { hasText: "dr-frankfurt" }).locator(".cell-none")).toHaveCount(2); + // The row behind the newest tag says so, and the env chip for a resolved + // name like prod-us-east fits its column rather than pushing Verdict out of + // place. + await expect(page.locator("table.tbl tbody tr", { hasText: "prod-us-east" })).toContainText("behind"); + await expect(page).toHaveScreenshot("fleet.png", { fullPage: true }); +}); + test("command palette", async ({ page }) => { await page.goto(`/${token}#apps/qa1`); await expect(page.locator("table.tbl tbody tr")).toHaveCount(4); diff --git a/web/e2e/screens.spec.ts-snapshots/fleet-chromium-darwin.png b/web/e2e/screens.spec.ts-snapshots/fleet-chromium-darwin.png new file mode 100644 index 0000000000000000000000000000000000000000..7448fb67b5c8bc68e0c7bcac7e43ed950fff4846 GIT binary patch literal 98007 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 Vk{pNDJpEw63ldX3x(1JhZzTaswCHaW12U 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 oBdxC^CX*h6ws$l3U^} { case "/api/app": return json(res, fx.appDetail); case "/api/config": return json(res, fx.configView); case "/api/promotion": return json(res, fx.promotionView); + case "/api/fleet": return json(res, fx.fleetView); case "/api/can": return json(res, fx.capabilities); case "/api/forwards": return json(res, fx.forwards); case "/api/timeline": return json(res, fx.appDetail.timeline); diff --git a/web/src/FleetScreen.tsx b/web/src/FleetScreen.tsx index 916d026..c2a40bc 100644 --- a/web/src/FleetScreen.tsx +++ b/web/src/FleetScreen.tsx @@ -3,6 +3,7 @@ import { api, type FleetRow, type FleetView } from "./api"; import { Glyph } from "./Status"; import { routeHash } from "./route"; import { envToken, loudestEnv } from "./health"; +import { notPresentLabel, readySplit, verdictBadge } from "./fleet"; // Screen 7. Is every cluster running the latest version. // @@ -20,12 +21,6 @@ import { envToken, loudestEnv } from "./health"; // compare against. The chip prints the resolved name verbatim, the way every // other screen does, and data-env carries the colour. -const NOT_PRESENT_LABEL: Record = { - unreachable: "no answer", - denied: "no access", - pending: "asking", -}; - export function FleetScreen({ app, namespace }: { app: string; namespace: string }) { const [view, setView] = useState(null); const [err, setErr] = useState(null); @@ -115,7 +110,7 @@ export function FleetScreen({ app, namespace }: { app: string; namespace: string Cluster - Env + Env Version Health Verdict @@ -223,7 +218,7 @@ function HealthCell({ r }: { r: FleetRow }) { if (r.state === "absent") { return ; } - const label = NOT_PRESENT_LABEL[r.state] ?? r.state; + const label = notPresentLabel(r.state); if (r.state === "denied") { return {label}; } @@ -236,14 +231,14 @@ function HealthCell({ r }: { r: FleetRow }) { } function Ratio({ ready }: { ready: string }) { - const i = ready.indexOf("/"); - if (i === -1) { - return {ready}; + const { whole, of } = readySplit(ready); + if (of === null) { + return {whole}; } return ( - {ready.slice(0, i)} - {ready.slice(i)} + {whole} + {of} ); } @@ -252,11 +247,12 @@ function Verdict({ r }: { r: FleetRow }) { if (r.state !== "present") { return <>{r.note}; } - const flagged = r.mutableTag || r.behind; + const badge = verdictBadge(r); + const flagged = badge !== null; return ( <> - {r.mutableTag && mutable tag} - {!r.mutableTag && r.behind && behind} + {badge === "mutable-tag" && mutable tag} + {badge === "behind" && behind} {r.note && {r.note}} {r.digestPending && ( digest pending diff --git a/web/src/fleet.test.ts b/web/src/fleet.test.ts new file mode 100644 index 0000000..f48ef3d --- /dev/null +++ b/web/src/fleet.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from "vitest"; +import { notPresentLabel, readySplit, verdictBadge } from "./fleet"; + +// These three functions were stranded inside FleetScreen.tsx: the vitest +// include in vite.config.ts does not match .tsx, and there is no jsdom in +// this package, so nothing reached from the component was ever reachable +// from a test. This file is new coverage of behavior that already shipped +// correctly; the extraction did not change what renders. + +describe("notPresentLabel", () => { + it("names the three ways a row has no version", () => { + expect(notPresentLabel("unreachable")).toBe("no answer"); + expect(notPresentLabel("denied")).toBe("no access"); + expect(notPresentLabel("pending")).toBe("asking"); + }); + + // A blank cell here would read as "checked and found nothing". A state + // nobody wrote into the map must never produce that lie, so it falls back + // to naming itself. + it("surfaces an unknown state as itself, never blank", () => { + expect(notPresentLabel("quarantined")).toBe("quarantined"); + expect(notPresentLabel("")).toBe(""); + }); +}); + +describe("readySplit", () => { + it("splits a ratio at the slash", () => { + expect(readySplit("4/4")).toEqual({ whole: "4", of: "/4" }); + }); + + it("keeps a value with no separator whole", () => { + expect(readySplit("asking")).toEqual({ whole: "asking", of: null }); + }); + + it("keeps an empty string whole rather than slicing it", () => { + expect(readySplit("")).toEqual({ whole: "", of: null }); + }); +}); + +describe("verdictBadge", () => { + // One tag resolving to two digests is the worse fact: two clusters claiming + // this version are not running the same code. Showing "behind" alongside it + // would suggest two independent problems, so mutableTag wins outright. + it("suppresses the behind badge when the tag is mutable", () => { + expect(verdictBadge({ mutableTag: true, behind: true })).toBe("mutable-tag"); + }); + + it("shows behind on its own", () => { + expect(verdictBadge({ mutableTag: false, behind: true })).toBe("behind"); + }); + + it("shows no badge when neither applies", () => { + expect(verdictBadge({ mutableTag: false, behind: false })).toBeNull(); + expect(verdictBadge({})).toBeNull(); + }); +}); diff --git a/web/src/fleet.ts b/web/src/fleet.ts new file mode 100644 index 0000000..744692f --- /dev/null +++ b/web/src/fleet.ts @@ -0,0 +1,46 @@ +/** + * Logic stranded inside FleetScreen.tsx, extracted so it can be tested without + * a DOM. web/vite.config.ts's vitest include does not match .tsx, and there is + * no jsdom in this package, so none of a component's inline logic is + * reachable from a test until it lives in a plain .ts module. + */ + +/** + * notPresentLabel names a row that never ran the app: it did not answer, it + * refused to let us look, or it is still asking. A state this map has never + * heard of surfaces as itself rather than going blank, because a blank cell + * on this screen reads as "checked and found nothing", which is a claim + * nobody earned about a state nobody recognizes. + */ +const NOT_PRESENT_LABEL: Record = { + unreachable: "no answer", + denied: "no access", + pending: "asking", +}; + +export function notPresentLabel(state: string): string { + return NOT_PRESENT_LABEL[state] ?? state; +} + +/** readySplit is where the Ratio cell's "/" lives, so the whole and the + * denominator can be styled apart. A value with no separator, or none at all, + * renders as one piece rather than losing text to a slice that assumed a "/" + * that was not there. */ +export function readySplit(ready: string): { whole: string; of: string | null } { + const i = ready.indexOf("/"); + if (i === -1) return { whole: ready, of: null }; + return { whole: ready.slice(0, i), of: ready.slice(i) }; +} + +/** + * verdictBadge picks the one flag a present row shows, in precedence order. A + * mutable tag is the worse fact: it means two clusters claiming this version + * are not running the same code, and "behind" undersells that as a mere + * schedule. Showing both would suggest they are independent problems, so + * mutableTag suppresses behind rather than sitting beside it. + */ +export function verdictBadge(r: { mutableTag?: boolean; behind?: boolean }): "mutable-tag" | "behind" | null { + if (r.mutableTag) return "mutable-tag"; + if (r.behind) return "behind"; + return null; +} From 6d4142e5f19ac1b97e1bdea75d8e263f7040274e Mon Sep 17 00:00:00 2001 From: Elber Ribeiro Date: Tue, 4 Aug 2026 15:53:16 -0400 Subject: [PATCH 26/38] docs(fleet): record what shipped, and correct two claims that did not 04-multi-cluster promised per-context expansion inside the promotion view; what shipped is a split cell plus a dedicated screen. It also claimed dedupe by API server URL and cluster UID; only the URL ships, because no cluster UID exists here and reading one needs a permission a namespace-scoped developer would be refused. Closes #72 --- README.md | 5 +-- docs/03-product-spec.md | 26 ++++++++++++-- docs/04-multi-cluster.md | 14 +++++--- docs/guide/13-fleet.md | 75 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 111 insertions(+), 9 deletions(-) create mode 100644 docs/guide/13-fleet.md diff --git a/README.md b/README.md index e218101..744a007 100644 --- a/README.md +++ b/README.md @@ -156,7 +156,7 @@ already enough of. ## Status Released. The app list, whole-workload logs, the reconstructed timeline, -resolved configuration, cross-environment diff, the promotion matrix, +resolved configuration, cross-environment diff, the promotion matrix, fleet, port-forward, the command palette, prod guardrails, and exec are built, tested, and driven against real clusters. @@ -183,7 +183,8 @@ The full site is at [keyboard](docs/guide/09-keyboard.md) · [the config file](docs/guide/10-config-file.md) · [permissions](docs/guide/11-permissions.md) · -[when things are missing](docs/guide/12-troubleshooting.md) +[when things are missing](docs/guide/12-troubleshooting.md) · +[fleet, one app across every cluster](docs/guide/13-fleet.md) **Design notes** — [the problem](docs/01-problem.md) · [personas](docs/02-personas.md) · diff --git a/docs/03-product-spec.md b/docs/03-product-spec.md index d8fcca0..9d03fdc 100644 --- a/docs/03-product-spec.md +++ b/docs/03-product-spec.md @@ -23,9 +23,16 @@ One sentence for the README, the launch post, and every scope argument: 6. Teach kubectl, do not replace it. Every view exposes the equivalent command. 7. Trust the numbers. A metric that could be wrong is not displayed. -## The four screens +## The screens -Nothing else ships in v1. +Seven views ship in v1. Four get a full design here, since their content +belongs to one app in one place: the app list, app detail, resolved +configuration, and logs. The other three live where their content forces +them to: promotion and the cross-environment config diff need every +environment already modeled, so [04-multi-cluster.md](04-multi-cluster.md) +specifies them; the command palette is under Cross-cutting below. + +Nothing beyond these seven ships in v1. ### Screen 1: Apps @@ -187,6 +194,21 @@ Every view exposes "show kubectl", printing the equivalent command. Environment switching and comparison are covered in [04-multi-cluster.md](04-multi-cluster.md). +Screen numbering, for cross-reference: 1 the app list, 2 app detail, 3 +resolved configuration, 3b the cross-environment config diff, 4 logs, 5 +promotion, 6 this palette. + +### Screen 7: Fleet + +One app, one row per cluster, answering a question promotion cannot phrase: +is every cluster running the latest version. Promotion compares environments +side by side; a team running prod across two regions asks about the clusters +inside one environment, or about clusters that sit outside the environment +axis entirely. Fleet reuses promotion's five states, present, absent, denied, +unreachable, pending, one row per cluster instead of one column per +environment. Opened from a split promotion cell or the app detail screen. +Guide: `docs/guide/13-fleet.md`. + ## Explicit non-goals for v1 No node view. No PersistentVolume or StorageClass browsing. No RBAC editor. No diff --git a/docs/04-multi-cluster.md b/docs/04-multi-cluster.md index eb4cca5..44c484c 100644 --- a/docs/04-multi-cluster.md +++ b/docs/04-multi-cluster.md @@ -18,8 +18,12 @@ Three concepts, deliberately separate. | App | A logical service, present in zero or more environments. Identity is namespace plus name within a cluster, matched across environments by the rules below. | One environment maps to one or more contexts. A team running prod across two -regions gets one `prod` environment holding `prod-us-east` and `prod-eu-west`, -and the promotion view aggregates them with per-context detail on expansion. +regions gets one `prod` environment holding `prod-us-east` and `prod-eu-west`. +The promotion cell for that environment collapses to one version only when +every context inside it agrees on tag and digest. When they disagree, the cell +renders `split` and names the disagreement instead of guessing at a version. +Per-context detail lives on its own screen, fleet: one app, one row per +cluster, opened from the split cell or from the app detail screen. ### An environment is not always a context @@ -328,10 +332,10 @@ cluster behind a VPN never blocks the rest of the grid. | --- | --- | | Prod reachable only over VPN, currently off | Two states, never conflated. Connected earlier this session: in-memory snapshot with its age. Never connected this session: "nothing known yet" with a reconnect action. Other environments unaffected either way. | | Credential plugin prompts for SSO in a browser | Inline prompt on the panel. No modal blocking the whole app. | -| Two contexts point at the same cluster | Detected by API server URL and cluster UID, merged with a notice. | -| Context renamed in kubeconfig | Match on cluster UID first, name second, so history survives a rename. | +| Two contexts point at the same cluster | Detected by API server URL alone (`config.NormalizeURL`), merged with a notice. No cluster UID exists anywhere in kubeside: reading one needs `get` on a `kube-system` namespace object, a permission kubeside does not request and a namespace-scoped developer would be refused for asking. | +| Context renamed in kubeconfig | Environment classification matches on API server URL first, context name second (`Config.Environment`), so a rename keeps its environment and guardrails. There is no stored history for a rename to survive: kubeside writes nothing to disk, and the timeline is reconstructed from the live cluster on every read. | | Same app name, unrelated services in two environments | Config `apps.match` overrides. A mismatch warning appears when image repositories differ entirely. | -| An environment has 40 contexts | Environment panel paginates and the promotion view aggregates, with per-context detail on expansion. | +| An environment has 40 contexts | Environment panel paginates. The promotion cell for it collapses only if all 40 agree; otherwise it splits. Fleet lists all 40 with their own state. | ## Resolved questions diff --git a/docs/guide/13-fleet.md b/docs/guide/13-fleet.md new file mode 100644 index 0000000..164c6fd --- /dev/null +++ b/docs/guide/13-fleet.md @@ -0,0 +1,75 @@ +# Fleet + +Is every cluster running the latest version? One app, one row per cluster. + +Promotion compares environments side by side. Fleet answers a question +promotion cannot phrase: a team running prod across two regions, or any +environment holding more than one cluster, needs to know whether those +clusters agree with each other, not only whether prod agrees with stg. + +## Opening it + +Three ways in: + +- The command palette (`cmd+k`), from any app: "*app* across every cluster". +- The "Fleet" button on the app detail screen. +- Directly, at `#fleet//`. +- From the promotion screen, click a `split` cell. A split cell means the + clusters inside that environment disagree, and fleet is where you see which + ones. + +Opening this screen wakes every context in your kubeconfig, not only the one +you were looking at. No other screen does this. The app list and app detail +connect lazily, one context at a time, as you look at them. Fleet asks all of +them at once, on open, because "every cluster" is the question it exists to +answer. + +## Reading the table + +| State | Means | +| --- | --- | +| `present` | The cluster runs the app. | +| `absent` | The cluster answered, and does not run it. A schedule, not a problem: the app just is not deployed there yet. | +| `denied` | The cluster refused the read. The note names the RBAC rule that would fix it, for example "cannot read Deployment here; the role needs: list deployments in team-a". | +| `unreachable` | The cluster did not answer at all. VPN, DNS, or the cluster is down. | +| `pending` | Still asking. | + +The distinction that matters most is `unreachable` against `absent`. A prod +cluster reachable only over VPN, currently off, is `unreachable`: kubeside +never saw it this time, so it never claims the app is missing there. Only a +cluster that actually answered and does not run the app gets `absent`. +Conflating the two, "I could not look" read as "it is not there", is the +mistake this screen exists to prevent, and it is the reason opening fleet +wakes every context instead of trusting whatever was last connected. + +A present row can also carry: + +- **behind** — an older version than the newest tag another cluster in the + fleet runs. +- **mutable tag** — this cluster's tag also resolves to a different digest + somewhere else in the fleet. Two clusters agree on the version number and + are not running the same code. This outranks behind: a cluster openly a + version behind is a schedule, two clusters claiming agreement while running + different builds is a defect. + +Rows sort by how much the cluster told you, worst first: mutable tag, then +unreachable, then behind, then denied, then present with no comparable +version, then pending. Absent sits with the healthy rows at the bottom, +because "not deployed here" is not a disagreement. + +## Version unknown + +An image pinned by digest (`myapp@sha256:...`) carries no tag. There is +nothing to compare it against, so the row reads "version unknown: the image +is pinned by digest" rather than folding it into the newest tag or treating +it as a match. Pinning by digest is not a defect; reading it as agreement +would be. + +## Clusters that are really one cluster + +A kubeconfig commonly holds one cluster under two context names, often +because one of them carries credentials that stopped working. Fleet detects +this by API server URL and merges the two into one row, so the same cluster +is never counted twice. The row names what it merged: "also +prod-us-east-arn" beside the context it kept. The row it keeps is whichever +context answered with the most, so a stale duplicate never hides a live one. From 8c0c93e03a2940543979aa44c7631daffec207a6 Mon Sep 17 00:00:00 2001 From: Elber Ribeiro Date: Tue, 4 Aug 2026 15:54:20 -0400 Subject: [PATCH 27/38] docs: answer the Argo CD question on the page, not in a comment thread It was the first objection raised on Show HN and the comparison never mentioned it. Sync state against git is a delivery question; the app across its environments is a different one. Closes #73 --- docs/01-problem.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/01-problem.md b/docs/01-problem.md index 9ade741..9d7ced4 100644 --- a/docs/01-problem.md +++ b/docs/01-problem.md @@ -179,15 +179,29 @@ Assume Radar gets topology right. The kubeside wedge is narrower and different: the developer's four questions, resolved configuration, whole-workload logs, and the same application compared across environments. +Argo CD came up first on the [Show HN thread](https://news.ycombinator.com/item?id=49139573) +and it is a different kind of tool, not a missed competitor. It runs 1:1 against +one cluster and shows sync state against git: is the live state what the repo +says it should be. That is a delivery question, and its reader is the person who +owns the pipeline. kubeside shows the app a developer owns across every +environment it runs in, qa, stg, and prod side by side, which is a different +question asked by a different reader. The two coexist rather than compete: +kubeside already reads the `argocd.argoproj.io/instance` label as a grouping +signal, documented in [03-product-spec.md](03-product-spec.md). + ## Reference points | Tool | Stars | Open issues | License | Note | | --- | --- | --- | --- | --- | | k9s | 34,182 | 135 | Apache-2.0 | Terminal, single maintainer | +| Argo CD | 23,835 | 4,318 | Apache-2.0 | GitOps delivery, 1:1 with the cluster, not a comparison | | kubernetes/dashboard | 15,427 | 166 | Apache-2.0 | Archived 2026-01-21 | | Headlamp | 6,920 | 834 | Apache-2.0 | SIG UI successor, auth-heavy backlog | | Freelens | 5,311 | 190 | MIT | Electron, Lens lineage, metrics bugs | | Kite | 2,937 | 39 | Apache-2.0 | Web, platform positioning | | Radar | 2,645 | 50 | Apache-2.0 | Topology and timeline, direct competitor | | KHI | 2,071 | 21 | Apache-2.0 | Google, log timeline only | + +Argo CD's row was read 2026-08-04, after the rest of this research; every other +row still reflects 2026-07-22, as stated at the top of this document. From 4d5ceaa4728756a844ecf6df174b2f04f6da9f83 Mon Sep 17 00:00:00 2001 From: Elber Ribeiro Date: Tue, 4 Aug 2026 15:54:54 -0400 Subject: [PATCH 28/38] docs: record why there is no TUI Asked on Show HN. 05-architecture already rejected a terminal UI and the reasoning was not reachable from the docs a user reads. Closes #74 --- docs/guide/12-troubleshooting.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/guide/12-troubleshooting.md b/docs/guide/12-troubleshooting.md index ff06e8c..6e3297c 100644 --- a/docs/guide/12-troubleshooting.md +++ b/docs/guide/12-troubleshooting.md @@ -83,3 +83,27 @@ manages your RBAC. See [permissions](11-permissions.html). Look for the edge. `ended` means the container exited, `gone` means the pod was deleted, `error` carries the reason. A log view that simply went quiet would be the failure worth worrying about. + +## Is there a TUI, like k9s? + +No, and this is a decision, not an oversight. Asked on the same +[Show HN thread](https://news.ycombinator.com/item?id=49139573) that raised the +Argo CD question. + +The four questions kubeside answers are layout problems before they are +anything else. A timeline needs a horizontal axis with markers on it. A +side-by-side config diff needs two columns a reader can scan against each +other. A promotion matrix needs a grid with color and drift indicators. A +monospace grid renders none of them well, and forcing them into one would +mean designing worse versions of screens that already work in a browser. + +k9s also already owns the terminal, and it does that job well. Competing with +it on keyboard speed would be a fight against a tool people already like, +over ground kubeside does not need. + +If your kubeconfig is not local, for example a shared cluster with no kubectl +access from your machine, `--serve` mode is the answer. It runs the same +binary and the same screens against the cluster directly, over OIDC. It is +listed under Beyond v1 in +[the roadmap](https://github.com/dynaum/kubeside/blob/main/docs/06-roadmap.md) +and has not shipped yet. From 23d8619bdb435618f91c72535e3e65d05a3825c1 Mon Sep 17 00:00:00 2001 From: Elber Ribeiro Date: Tue, 4 Aug 2026 15:55:56 -0400 Subject: [PATCH 29/38] docs: the last claim about a cluster UID kubeside never reads 05-architecture said contextID derives from a cluster UID and that a rename preserves stored history. Neither is true. Nothing in internal/ reads a cluster UID, and kubeside stores no history to preserve, which is the whole bet recorded in 04-multi-cluster. #72 corrected the same claim in 04-multi-cluster and this was the one remaining spot asserting a mechanism that exists nowhere in the code. --- docs/05-architecture.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/docs/05-architecture.md b/docs/05-architecture.md index d90fd74..23874ee 100644 --- a/docs/05-architecture.md +++ b/docs/05-architecture.md @@ -57,8 +57,17 @@ web/ React frontend, embedded via embed.FS permission cache, and circuit breaker. One goroutine per connection. A dead cluster never blocks a request for another. -`contextID` derives from cluster UID first and kubeconfig name second, so a -context rename in kubeconfig preserves stored history. +`contextID` is the kubeconfig context name. Two contexts aimed at one cluster +are recognized by API server URL, normalized by `config.NormalizeURL`, which is +what the fleet view merges on. + +An earlier draft said this derived from a cluster UID, and that a rename +"preserves stored history". Both were wrong. No cluster UID is read anywhere: +obtaining one means reading the `kube-system` namespace UID, a permission +kubeside does not request and a namespace-scoped developer would be refused. And +there is no stored history to preserve, which is the point of +[04-multi-cluster.md](04-multi-cluster.md)'s decision to write nothing to disk. +A rename costs the session's in-memory buffer, and nothing else. Watch tiers, per [04-multi-cluster.md](04-multi-cluster.md): From 5a5e1c2d95376b194e0997d1f23b01a8b8ffd9e9 Mon Sep 17 00:00:00 2001 From: Elber Ribeiro Date: Tue, 4 Aug 2026 16:05:48 -0400 Subject: [PATCH 30/38] docs: three claims the fleet branch got wrong about itself The product spec said seven views ship and then listed an eighth, while exec was counted nowhere. Nine ship. It also said fleet reuses promotion's five states. Promotion has eight, and none of them are present, unreachable, or pending. Only absent and denied are spelled the same, and they differ: promotion has no unreachable, so a cluster that never answered reads denied there and unreachable here. What fleet reuses is CompareTags, Identity, and Orderable. The guide claimed no other screen wakes every context. Promotion does too. Fleet's real difference is that it connects concurrently and reads pod status everywhere, which is where the digest lives. --- docs/03-product-spec.md | 29 ++++++++++++++++++++--------- docs/guide/13-fleet.md | 19 ++++++++++++++----- 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/docs/03-product-spec.md b/docs/03-product-spec.md index 9d03fdc..457481d 100644 --- a/docs/03-product-spec.md +++ b/docs/03-product-spec.md @@ -25,14 +25,15 @@ One sentence for the README, the launch post, and every scope argument: ## The screens -Seven views ship in v1. Four get a full design here, since their content +Nine views ship in v1. Four get a full design here, since their content belongs to one app in one place: the app list, app detail, resolved -configuration, and logs. The other three live where their content forces -them to: promotion and the cross-environment config diff need every +configuration, and logs. The rest live where their content forces them to: +promotion, fleet, and the cross-environment config diff need every environment already modeled, so [04-multi-cluster.md](04-multi-cluster.md) -specifies them; the command palette is under Cross-cutting below. +specifies them; the command palette is under Cross-cutting below, and exec is +specified with the actions it belongs to. -Nothing beyond these seven ships in v1. +Nothing beyond these nine ships in v1. ### Screen 1: Apps @@ -196,7 +197,8 @@ Environment switching and comparison are covered in Screen numbering, for cross-reference: 1 the app list, 2 app detail, 3 resolved configuration, 3b the cross-environment config diff, 4 logs, 5 -promotion, 6 this palette. +promotion, 6 this palette, 7 fleet, and exec, which carries no number because +it is an action surface rather than a view. ### Screen 7: Fleet @@ -204,9 +206,18 @@ One app, one row per cluster, answering a question promotion cannot phrase: is every cluster running the latest version. Promotion compares environments side by side; a team running prod across two regions asks about the clusters inside one environment, or about clusters that sit outside the environment -axis entirely. Fleet reuses promotion's five states, present, absent, denied, -unreachable, pending, one row per cluster instead of one column per -environment. Opened from a split promotion cell or the app detail screen. +axis entirely. One row per cluster instead of one column per environment. + +Fleet declares its own five states: present, absent, denied, unreachable, and +pending. It does not share promotion's, which are comparisons between columns +rather than facts about one cluster. Only `absent` and `denied` are spelled the +same in both, and they do not mean the same thing: promotion has no +`unreachable`, so a cluster that never answered reads as denied there and as +unreachable here. What fleet does reuse from promotion is the machinery, +`CompareTags`, `Identity`, and `Orderable`, so one app is one app and one +version ordering governs both screens. + +Opened from a split promotion cell or the app detail screen. Guide: `docs/guide/13-fleet.md`. ## Explicit non-goals for v1 diff --git a/docs/guide/13-fleet.md b/docs/guide/13-fleet.md index 164c6fd..d76ae94 100644 --- a/docs/guide/13-fleet.md +++ b/docs/guide/13-fleet.md @@ -9,7 +9,7 @@ clusters agree with each other, not only whether prod agrees with stg. ## Opening it -Three ways in: +Four ways in: - The command palette (`cmd+k`), from any app: "*app* across every cluster". - The "Fleet" button on the app detail screen. @@ -19,10 +19,19 @@ Three ways in: ones. Opening this screen wakes every context in your kubeconfig, not only the one -you were looking at. No other screen does this. The app list and app detail -connect lazily, one context at a time, as you look at them. Fleet asks all of -them at once, on open, because "every cluster" is the question it exists to -answer. +you were looking at. The app list and app detail connect lazily, one context at +a time, as you look at them. Fleet asks all of them at once, on open, because +"every cluster" is the question it exists to answer. + +Promotion also connects every context, so fleet is not alone in that. Two +things make fleet heavier. It connects them concurrently rather than one after +another, and it reads pod status on every cluster it reaches, which promotion +does only for the apps you are comparing. Pod status is where the image digest +lives, and the digest is what catches one tag running as two different builds. + +On a kubeconfig with forty contexts, that is forty connections at once, each +possibly running a credential plugin. Worth knowing before you bind the palette +command to muscle memory. ## Reading the table From 03a11021a9fb1f8337938f5f5e7951a76ca77662 Mon Sep 17 00:00:00 2001 From: Elber Ribeiro Date: Sun, 16 Aug 2026 22:55:18 -0400 Subject: [PATCH 31/38] fix(promotion): an unread cluster is an unknown, not a disagreement byEnv split() treated every unreadable-adjacent cell the same way: severe, counted as drift, counted in Summary.Split. But when the clusters that actually answered fully agree with each other, there is nothing to flag about them, only an unrelated cluster that never spoke. That is the same epistemic state issue #75 already routes to StateDenied when the app is absent everywhere readable. Deciding by whether the app happened to be found, rather than by what was actually observed, was the bug. resolve() now intercepts full agreement plus an unread cluster before split() is ever called, and renders it StateDenied: not severe, excluded from drift, excluded from Summary.Split, and still never upstream, since the silent cluster might disagree with the ones that spoke. split() itself no longer lets an unread cluster override a witnessed disagreement. It used to check Unreadable first and report only "N of M readable", silently dropping a real mutable-tag or tag-mismatch finding whenever any cluster also happened to be unread. Now the disagreement drives the note, with the unread count appended rather than substituted. Closes #76 --- internal/promotion/promotion.go | 42 +++++++++---- internal/promotion/promotion_test.go | 93 ++++++++++++++++++++++++++-- 2 files changed, 119 insertions(+), 16 deletions(-) diff --git a/internal/promotion/promotion.go b/internal/promotion/promotion.go index b20e8b5..9f04f2c 100644 --- a/internal/promotion/promotion.go +++ b/internal/promotion/promotion.go @@ -297,10 +297,21 @@ func resolve(env Env, group []Instance, upstream *Instance) (Instance, Cell, boo } readable := clusters - len(env.Unreadable) - if rep, agreed := consensus(present); agreed && len(present) == readable && len(env.Unreadable) == 0 { - c := compare(rep, upstream) - c.Clusters = clusters - return rep, c, true + if rep, agreed := consensus(present); agreed && len(present) == readable { + if len(env.Unreadable) == 0 { + c := compare(rep, upstream) + c.Clusters = clusters + return rep, c, true + } + // The readable clusters agree with each other and none of them is + // missing the app, but some other cluster never answered. That is an + // unknown, not a disagreement: there is nothing to flag about the + // clusters that spoke, so this must not read as a witnessed defect + // the way split() does — same treatment as the present==0 unreadable + // case above (issue #75), and it cannot become upstream for the same + // reason: the silent cluster might run something different. + note := fmt.Sprintf("%d of %d clusters agree on %s; %d did not answer", len(present), clusters, rep.Tag, len(env.Unreadable)) + return Instance{}, Cell{Env: env.Name, Namespace: ns, State: StateDenied, Tag: rep.Tag, Note: note, Clusters: clusters}, false } return Instance{}, split(env, present, clusters, readable), false @@ -354,12 +365,19 @@ func split(env Env, present []Instance, clusters, readable int) Cell { } } + // A witnessed disagreement is a real defect, and it must not hide behind + // an unrelated cluster that simply never answered. resolve() already + // routes the shape where the readable clusters fully agree and some + // other one is unread to a StateDenied cell before this function is ever + // called (issue #76), so every case below fires only because the + // clusters that DID answer disagree with each other. An unread cluster + // is still worth naming, just never as the reason. + unreadSuffix := "" + if unread := len(env.Unreadable); unread > 0 { + unreadSuffix = fmt.Sprintf("; %d cluster(s) did not answer", unread) + } + switch { - case len(env.Unreadable) > 0: - // A cluster nobody read might disagree, so unreadable always wins: - // neither agreement nor a defect can be claimed from clusters we - // never saw. - c.Note = fmt.Sprintf("%d of %d clusters readable; the rest did not answer", readable, clusters) case len(tags) == 1 && len(digests) > 1: // A mutable tag outranks partial coverage. One tag resolving to two // digests is a defect; a cluster not having the app yet is a @@ -368,11 +386,11 @@ func split(env Env, present []Instance, clusters, readable int) Cell { // this branch is reachable under partial coverage too, where fewer // clusters answered than the environment declares. c.Tag = "" - c.Note = fmt.Sprintf("one tag across %d clusters but %d digests: the tag is mutable and these are not the same code", len(present), len(digests)) + c.Note = fmt.Sprintf("one tag across %d clusters but %d digests: the tag is mutable and these are not the same code%s", len(present), len(digests), unreadSuffix) case len(present) < readable: - c.Note = fmt.Sprintf("deployed in %d of %d clusters", len(present), clusters) + c.Note = fmt.Sprintf("deployed in %d of %d clusters%s", len(present), clusters, unreadSuffix) default: - c.Note = fmt.Sprintf("%d versions across %d clusters", len(tags), clusters) + c.Note = fmt.Sprintf("%d versions across %d clusters%s", len(tags), clusters, unreadSuffix) } return c } diff --git a/internal/promotion/promotion_test.go b/internal/promotion/promotion_test.go index 933a32f..e268da2 100644 --- a/internal/promotion/promotion_test.go +++ b/internal/promotion/promotion_test.go @@ -392,6 +392,13 @@ func TestPartialDeploymentSplits(t *testing.T) { } } +// Issue #76: the readable clusters here fully agree with each other, so there +// is nothing to flag about them. Before the fix this rendered StateSplit, +// Severe, and counted toward drift and Summary.Split — the same treatment as +// a genuine disagreement, decided only by an unrelated cluster that never +// answered. It must render exactly like the present==0 unreadable case (#75): +// StateDenied, not severe, no drift, and it still must never become upstream, +// because the silent cluster might disagree with the two that spoke. func TestUnreadableContextBlocksCollapse(t *testing.T) { envs := multiEnv() envs[1].Unreadable = []string{"prod-ap-south"} @@ -399,12 +406,90 @@ func TestUnreadableContextBlocksCollapse(t *testing.T) { prodInstance("prod-us-east", "v2.13.1", "sha256:bb"), prodInstance("prod-eu-west", "v2.13.1", "sha256:bb"), }) + r := at(t, rows, "checkout") + c := cell(r, "prod") + if c.State != StateDenied { + t.Fatalf("state = %q, want denied: the readable clusters agree, and the unread one is an unknown, not a disagreement", c.State) + } + if c.Severe { + t.Error("an unread cluster among clusters that agree is not severe") + } + if !strings.Contains(c.Note, "2") || !strings.Contains(c.Note, "1") { + t.Errorf("note = %q, want it to name both the 2 that agreed and the 1 that did not answer", c.Note) + } + if r.Drift != 0 { + t.Fatalf("drift = %d, want 0: agreement plus an unknown must not float this row up the matrix", r.Drift) + } + if s := Summarize(rows); s.Split != 0 { + t.Fatalf("split = %d, want 0: this is not a split cell", s.Split) + } +} + +// The mirror of the case above: a real disagreement among the clusters that +// DID answer must still be reported as a severe split, and must not hide +// behind an unrelated cluster that never answered. Before the fix, split() +// checked len(env.Unreadable) > 0 first and reported only "N of M readable", +// silently dropping the witnessed tag disagreement. +func TestGenuineDisagreementSurvivesAnUnreadableContext(t *testing.T) { + envs := multiEnv() + envs[1].Unreadable = []string{"prod-ap-south"} + rows := Build(envs, []Instance{ + prodInstance("prod-us-east", "v2.13.1", "sha256:bb"), + prodInstance("prod-eu-west", "v2.12.0", "sha256:cc"), + }) c := cellFor(t, rows, "prod") - if c.State != StateSplit { - t.Fatalf("state = %q; a cluster nobody could read is not an agreement", c.State) + if c.State != StateSplit || !c.Severe { + t.Fatalf("state = %q severe = %v, want a severe split: two readable clusters disagree", c.State, c.Severe) + } + if !strings.Contains(c.Note, "versions") { + t.Errorf("note = %q, want the disagreement named", c.Note) + } + if !strings.Contains(c.Note, "1") || !strings.Contains(c.Note, "did not answer") { + t.Errorf("note = %q, want the unread cluster named too, not just the disagreement", c.Note) + } +} + +// Same principle for a mutable tag: the defect observed among the readable +// clusters must survive an unrelated unread cluster. +func TestMutableTagSurvivesAnUnreadableContext(t *testing.T) { + envs := multiEnv() + envs[1].Unreadable = []string{"prod-ap-south"} + rows := Build(envs, []Instance{ + prodInstance("prod-us-east", "v2.13.1", "sha256:bb"), + prodInstance("prod-eu-west", "v2.13.1", "sha256:cc"), + }) + c := cellFor(t, rows, "prod") + if c.State != StateSplit || !c.Severe { + t.Fatalf("state = %q severe = %v, want a severe split: same tag, different digest", c.State, c.Severe) + } + if !strings.Contains(c.Note, "digest") { + t.Errorf("note = %q, want the mutable tag named", c.Note) + } + if !strings.Contains(c.Note, "did not answer") { + t.Errorf("note = %q, want the unread cluster named alongside the digest mismatch", c.Note) + } +} + +// And for partial coverage: one readable cluster has the app, another +// readable cluster explicitly does not, and a third cluster never answered. +// The coverage gap between the readable clusters is the real finding. +func TestPartialCoverageSurvivesAnUnreadableContext(t *testing.T) { + envs := []Env{ + {Name: "qa", Contexts: []string{"qa-cluster"}}, + {Name: "prod", Contexts: []string{"prod-a", "prod-b", "prod-c", "prod-d"}, Unreadable: []string{"prod-d"}}, + } + rows := Build(envs, []Instance{ + {Env: "prod", Context: "prod-a", App: "checkout", Namespace: "shop", Present: true, Tag: "v1.0.0", Digest: "sha256:aa"}, + }) + c := cellFor(t, rows, "prod") + if c.State != StateSplit || !c.Severe { + t.Fatalf("state = %q severe = %v, want a severe split: present in one of three readable clusters", c.State, c.Severe) + } + if !strings.Contains(c.Note, "1 of 4") { + t.Errorf("note = %q, want it to name the coverage against the declared total", c.Note) } - if !strings.Contains(c.Note, "readable") { - t.Errorf("note = %q, want it to say how many clusters answered", c.Note) + if !strings.Contains(c.Note, "did not answer") { + t.Errorf("note = %q, want the unread cluster named alongside the coverage gap", c.Note) } } From 5aa1395360ddc36a8717616b7a2dca6c5986da49 Mon Sep 17 00:00:00 2001 From: Elber Ribeiro Date: Sun, 16 Aug 2026 22:56:17 -0400 Subject: [PATCH 32/38] fix(api): update the one Promotion test #76 changed the outcome of TestPromotionMarksAnUnreachableContextOnItsEnvironment fed Service.Promotion a single present cluster plus one unreachable context in the same environment. Under #76, a lone cluster with nothing to disagree with is no longer treated as a disagreement: it now renders StateDenied, matching every other "readable clusters agree, one didn't answer" case in the matrix. The old assertion (StateSplit) encoded the behavior #76 exists to fix. --- internal/api/service_test.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/internal/api/service_test.go b/internal/api/service_test.go index 60a5dbd..9843579 100644 --- a/internal/api/service_test.go +++ b/internal/api/service_test.go @@ -250,8 +250,14 @@ func TestPromotionMarksAnUnreachableContextOnItsEnvironment(t *testing.T) { if got := v.Envs[0].Unreadable; len(got) != 1 || got[0] != "prod-eu-west" { t.Errorf("unreadable = %v, want [prod-eu-west]", got) } - if v.Rows[0].Cells[0].State != promotion.StateSplit { - t.Error("a cluster nobody could read is not an agreement") + // Issue #76: the one readable cluster has nothing to disagree with — it + // is the only cluster that answered. There is no witnessed disagreement + // here, only an unrelated cluster that never spoke, so this must render + // StateDenied (an unknown), not StateSplit (a disagreement). It must + // still never become upstream, since prod-eu-west might run something + // different. + if v.Rows[0].Cells[0].State != promotion.StateDenied { + t.Errorf("state = %q, want denied: one cluster answered and has nothing to disagree with", v.Rows[0].Cells[0].State) } } From dfae3b975a373d65ea8f464c25d12194f16ccf0d Mon Sep 17 00:00:00 2001 From: Elber Ribeiro Date: Sun, 16 Aug 2026 22:58:07 -0400 Subject: [PATCH 33/38] fix(clusters): serialize concurrent Connect calls for one context Connect released mu before dialing, so two concurrent callers for the same context could both see a non-live state and both dial. The second result silently overwrote session set by the first. Harmless today (Session.Close is a no-op), but the outcome depended on which dial finished last, and it wasted a redundant connection attempt every time it happened. dialMu now wraps the whole check-dial-update sequence per conn. A second caller blocks until the first finishes, then re-checks state under mu and returns that outcome instead of starting its own dial. Different contexts have independent conns and independent dialMu locks, so this changes nothing about one cluster never blocking another. Confirmed the race directly: reverting the fix and running the new test under -count=10 failed 4 times with 2-12 duplicate dials for one context. With the fix, 10/10 clean under -race. Closes #78 --- internal/clusters/manager.go | 19 ++++++++++++++++++ internal/clusters/manager_test.go | 32 +++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/internal/clusters/manager.go b/internal/clusters/manager.go index ef84f9f..f8b6e37 100644 --- a/internal/clusters/manager.go +++ b/internal/clusters/manager.go @@ -46,6 +46,15 @@ type conn struct { failures int retryAfter time.Time session Session + + // dialMu serializes Connect for this context alone. mu is released while + // the dial itself is in flight, so without this a second concurrent + // Connect call would see a state other than StateLive and start a + // redundant dial, racing the first to overwrite session. Holding dialMu + // for the whole check-dial-update sequence means a second caller waits + // for the first to finish and then re-checks state under mu, seeing the + // first dial's outcome instead of starting its own. + dialMu sync.Mutex } // Manager owns one connection per kubeconfig context. @@ -124,12 +133,22 @@ func (m *Manager) ConnectOrder() []string { // Connect opens a session for one context, respecting the circuit breaker. // Calling it for an already-live context is a no-op. +// +// Two concurrent calls for the same context never both dial: dialMu +// serializes the whole check-dial-update sequence per context, so a second +// caller blocks until the first finishes and then observes its outcome +// (state, error, or backoff) rather than racing it. Different contexts have +// independent conns and independent dialMu locks, so a stuck dial on one +// context can never block Connect for another. func (m *Manager) Connect(ctx context.Context, name string) error { c, ok := m.conn(name) if !ok { return errUnknownContext(name) } + c.dialMu.Lock() + defer c.dialMu.Unlock() + c.mu.Lock() if c.state == StateLive { c.mu.Unlock() diff --git a/internal/clusters/manager_test.go b/internal/clusters/manager_test.go index 85bfe1b..5a9d2a5 100644 --- a/internal/clusters/manager_test.go +++ b/internal/clusters/manager_test.go @@ -347,6 +347,38 @@ func TestOneStuckClusterDoesNotBlockAnother(t *testing.T) { close(gate) } +// Issue #78: two simultaneous Connect calls for the same context could both +// pass the state check (neither sees StateLive yet) and both dial, the +// second overwriting session set by the first. Twenty concurrent callers for +// one context must still produce exactly one dial. +func TestConcurrentConnectsToOneContextDialOnce(t *testing.T) { + fc := newConnector() + m, _ := newManager(t, fc) + + var wg sync.WaitGroup + errs := make([]error, 20) + for i := 0; i < 20; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + errs[i] = m.Connect(context.Background(), "qa") + }(i) + } + wg.Wait() + + for i, err := range errs { + if err != nil { + t.Fatalf("caller %d: %v", i, err) + } + } + if got := fc.attemptsFor("qa"); got != 1 { + t.Fatalf("attempts = %d, want 1: twenty callers for one context must serialize to a single dial", got) + } + if got := mustStatus(t, m, "qa").State; got != StateLive { + t.Fatalf("qa = %s, want live", got) + } +} + func TestConcurrentConnectsAreRaceFree(t *testing.T) { fc := newConnector() m, _ := newManager(t, fc) From f0f51f92e605feefd27b15cc36e1695858766942 Mon Sep 17 00:00:00 2001 From: Elber Ribeiro Date: Sun, 16 Aug 2026 23:04:49 -0400 Subject: [PATCH 34/38] fix(api): Promotion stops reading a refusal or a stale session as absent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two related conflations in Service.Promotion, both landing on the same principle #75 already established: an environment we could not verify must never render "not deployed here". #77: clusters.Fetch never errors on an RBAC refusal. A forbidden Deployment list lands in Snapshot.Partial with a nil error, and Promotion discarded Partial entirely, so a refused kind looked exactly like an app that does not exist. Now a refused app-carrying kind marks the context unreadable, alongside whatever the read did find, so resolve() can tell "not deployed here" apart from "might be here, we were refused" the same way it already does for a context that never answered. Deliberately coarser than Service.Fleet's per-app precision: Promotion compares every app in bulk, and flagging every namespace-scoped RBAC context (the common case) would bury the signal. #82: Connect's error was captured but never acted on directly — the code proceeded to ClientFor regardless, which returns any existing session whenever one exists, live or not. Connect's error now short-circuits before ClientFor is ever consulted, matching the pattern Service.Fleet already used. Tracing the state machine: the precondition this guards against (state not Live, session still set) was primarily reachable through the double-dial race #78 just closed, where a losing dial's failure could overwrite a winning dial's Live state without touching its session. The check stays as defense in depth regardless. orElseString is now dead code with this rewrite and is removed. Closes #77 Closes #82 --- internal/api/service.go | 44 +++++++++++++++++----- internal/api/service_test.go | 71 ++++++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 10 deletions(-) diff --git a/internal/api/service.go b/internal/api/service.go index c16f6d0..8069671 100644 --- a/internal/api/service.go +++ b/internal/api/service.go @@ -1024,16 +1024,30 @@ func (s *Service) Promotion() PromotionView { } acc.contexts = append(acc.contexts, name) - reason := "" + // A failed Connect must short-circuit here, before ClientFor is ever + // consulted. Connect does not clear a prior session on failure, so a + // context that connected once and later lost its token, an expired + // credential, a VPN drop, an open breaker, still returns a client + // from ClientFor. Trusting that client would read a stale connection + // as fully readable and let the matrix report "not deployed here" + // for an environment it never actually verified this round (issue + // #82). Service.Fleet already avoids this by checking the error the + // same way. if err := s.mgr.Connect(ctx, name); err != nil { - reason = err.Error() + acc.unreadable = append(acc.unreadable, name) + instances = append(instances, promotion.Instance{ + Env: env.Name, Context: name, App: "", Denied: true, + DeniedReason: err.Error(), + }) + continue } + client, ok := s.mgr.ClientFor(name) if !ok { acc.unreadable = append(acc.unreadable, name) instances = append(instances, promotion.Instance{ Env: env.Name, Context: name, App: "", Denied: true, - DeniedReason: orElseString(reason, "not connected"), + DeniedReason: "not connected", }) continue } @@ -1043,6 +1057,23 @@ func (s *Service) Promotion() PromotionView { acc.unreadable = append(acc.unreadable, name) continue } + + // clusters.Fetch never errors on an RBAC refusal: a forbidden list + // lands in snap.Partial with a nil error, and the apps it would have + // carried are simply missing from snap.Apps, indistinguishable from + // apps that genuinely do not exist here. So this context is only + // partially trustworthy for the environment it belongs to. Marking + // it unreadable, alongside whatever instances this read DID find, + // lets resolve() tell "not deployed here" apart from "might be here, + // we were refused" the same way it already does for a context that + // never answered at all (issue #77). The namespace-scoped precision + // fleet.go has for one app does not translate here: Promotion + // compares every app in bulk, and flagging every namespace-scoped + // RBAC context (the common case) as unreadable would bury the signal + // this view exists to surface. + if refused := refusedCarriers(snap.Partial); len(refused) > 0 { + acc.unreadable = append(acc.unreadable, name) + } for _, a := range snap.Apps { in := instanceOf(env.Name, a) in.Context = name @@ -1151,13 +1182,6 @@ func mustTime(v string) time.Time { return t } -func orElseString(v, fallback string) string { - if v == "" { - return fallback - } - return v -} - // The actions the UI renders controls for. Named here so one list drives both // the permission check and the buttons, and a control cannot quietly appear // without a permission behind it. diff --git a/internal/api/service_test.go b/internal/api/service_test.go index 9843579..763e594 100644 --- a/internal/api/service_test.go +++ b/internal/api/service_test.go @@ -293,6 +293,77 @@ func TestPromotionUnreachableOnlyForFullyUnreadableEnvironments(t *testing.T) { } } +// Issue #77: clusters.Fetch never errors on an RBAC refusal — a forbidden +// list lands in Snapshot.Partial with a nil error, and Service.Promotion +// used to discard Partial entirely. So an app hidden by a refused Deployment +// list looked exactly like an app that genuinely does not exist, and the +// matrix rendered "not deployed here" for a cluster it was never allowed to +// check. qa genuinely has checkout; prod-a's Deployment listing is refused, +// so checkout is invisible there for a reason that has nothing to do with +// whether it is deployed. +func TestPromotionMarksARefusedKindDeniedNotAbsent(t *testing.T) { + s := serviceWithContexts(t, map[string]fakeCluster{ + "qa-a": {env: "qa", apps: []fakeApp{{name: "checkout", ns: "shop", image: "reg/checkout:v1.0.0"}}}, + "prod-a": {env: "prod", refusedKinds: []string{"Deployment"}}, + }) + + v := s.Promotion() + + if len(v.Rows) != 1 { + t.Fatalf("rows = %d, want 1", len(v.Rows)) + } + prodEnv := -1 + for i, e := range v.Envs { + if e.Name == "prod" { + prodEnv = i + } + } + if prodEnv == -1 { + t.Fatal("no prod environment in the view") + } + if got := v.Envs[prodEnv].Unreadable; len(got) != 1 || got[0] != "prod-a" { + t.Errorf("prod unreadable = %v, want [prod-a]: the Deployment list was refused, not merely empty", got) + } + c := v.Rows[0].Cells[prodEnv] + if c.State == promotion.StateAbsent { + t.Fatal("state = absent; a refused list means we could not check, not that checkout is missing") + } + if c.State != promotion.StateDenied { + t.Errorf("state = %q, want denied", c.State) + } +} + +// Issue #82, and its relationship to #78: Manager.Connect does not clear a +// prior session on failure, and ClientFor returns any session that exists +// regardless of state. Service.Promotion now checks Connect's own error and +// short-circuits before ever consulting ClientFor, matching the pattern +// Service.Fleet already used. This pins that the check fires directly off +// Connect's return value. +// +// Note on reachability: the state where a session survives while state is no +// longer Live was primarily reachable through the double-dial race fixed in +// #78 — the losing dial's failure could overwrite the winning dial's Live +// state without touching the session the winner had already set. With #78's +// dialMu serializing concurrent dials per context, that specific window is +// closed. This test cannot reproduce a genuinely stale session through the +// public Connect/ClientFor surface for that reason; it proves the defensive +// check itself is wired correctly, which costs nothing and guards against +// any future path that reintroduces the precondition. +func TestPromotionUsesConnectsOwnErrorBeforeClientFor(t *testing.T) { + s := serviceWithContexts(t, map[string]fakeCluster{ + "prod-a": {env: "prod", unreachable: true}, + }) + + v := s.Promotion() + + if len(v.Rows) != 0 { + t.Fatalf("rows = %d, want 0: nothing was ever found", len(v.Rows)) + } + if len(v.Envs) != 1 || len(v.Envs[0].Unreadable) != 1 { + t.Fatalf("unreadable = %v, want [prod-a]", v.Envs[0].Unreadable) + } +} + // dedupeStrings is what keeps an environment with several unreachable // contexts down to one banner entry. Exercise it directly, since nothing // about the code path it serves proves the collapsing behavior on its own. From dfa6a5ce9239f64b25133b5fd899dd3e5e43171f Mon Sep 17 00:00:00 2001 From: Elber Ribeiro Date: Sun, 16 Aug 2026 23:08:12 -0400 Subject: [PATCH 35/38] test(e2e): fleet's row order and the split cell were never actually checked MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #80: fleetView.rows was fixture order (qa1, prod-us-east, prod-eu-west, stg1, dr-frankfurt), not fleet.severity()'s order. The frontend does not sort — Go does — so the committed baseline rendered whatever order the fixture happened to list, directly under a note card titled "Sorted by what needs attention". Reordered to match severity (unreachable, behind, denied, then the two ties broken alphabetically) and pinned it with a row-order assertion, not only a picture. #81: promotionView had no cell with state "split", so PromotionScreen.tsx's split branch, the only new promotion UI on this branch, was exercised by nothing. It renders a full backend sentence into a version-sized column. Added a split cell (api-gateway, a 3-context prod with a mutable tag) and assertions that the note renders verbatim, in the error hue, and that clicking it opens the fleet screen for that app. Both screenshot baselines regenerated and verified with a clean second run. Closes #80 Closes #81 --- web/e2e/fixtures.mjs | 60 ++++++++++++------ web/e2e/screens.spec.ts | 16 +++++ .../fleet-chromium-darwin.png | Bin 98007 -> 98024 bytes .../promotion-chromium-darwin.png | Bin 76489 -> 87862 bytes 4 files changed, 58 insertions(+), 18 deletions(-) diff --git a/web/e2e/fixtures.mjs b/web/e2e/fixtures.mjs index 42f4ca5..f74b17a 100644 --- a/web/e2e/fixtures.mjs +++ b/web/e2e/fixtures.mjs @@ -108,7 +108,12 @@ export const configView = { }; export const promotionView = { - envs: [{ name: "qa", risk: "low", context: "qa1" }, { name: "prod", risk: "high", context: "prod-us-east" }], + envs: [ + { name: "qa", risk: "low", context: "qa1" }, + // Three contexts, so the split cell below (#81) represents a real shape: + // an environment whose clusters disagree, not a single-context column. + { name: "prod", risk: "high", context: "prod-us-east", contexts: ["prod-us-east", "prod-eu-west", "prod-ap-south"] }, + ], rows: [ { app: "search-indexer", namespace: "team-b", drift: 1, ahead: true, @@ -117,6 +122,28 @@ export const promotionView = { { env: "prod", state: "ahead", tag: "v3.1.0-rc4", health: "failed", ready: "0/3", severe: true, note: "ahead of qa; nothing promoted this", revisionAt: "2026-07-25T02:07:00Z" }, ], }, + { + // The only row exercising PromotionScreen.tsx's split branch, added + // for #81: nothing in the Playwright suite rendered it before, and it + // puts a full backend sentence into a matrix column sized for a + // version string. Sorted here (drift 1, ahead false) alphabetically + // ahead of notifications and payments, its drift-1 siblings. + app: "api-gateway", namespace: "team-a", drift: 1, + cells: [ + { env: "qa", state: "same", tag: "v5.2.0", health: "healthy", ready: "2/2", revisionAt: "2026-07-20T09:00:00Z" }, + { + env: "prod", state: "split", severe: true, clusters: 3, + note: "one tag across 3 clusters but 2 digests: the tag is mutable and these are not the same code", + }, + ], + }, + { + app: "notifications", namespace: "team-b", drift: 1, + cells: [ + { env: "qa", state: "same", tag: "v0.9.1", health: "healthy", ready: "2/2", revisionAt: "2026-07-24T12:00:00Z" }, + { env: "prod", state: "absent", note: "not deployed here" }, + ], + }, { app: "payments", namespace: "team-a", drift: 1, cells: [ @@ -131,31 +158,25 @@ export const promotionView = { { env: "prod", state: "same", tag: "v2.14.0", health: "healthy", ready: "6/6", digestPending: true, revisionAt: "2026-07-25T00:00:00Z" }, ], }, - { - app: "notifications", namespace: "team-b", drift: 1, - cells: [ - { env: "qa", state: "same", tag: "v0.9.1", health: "healthy", ready: "2/2", revisionAt: "2026-07-24T12:00:00Z" }, - { env: "prod", state: "absent", note: "not deployed here" }, - ], - }, ], - summary: { apps: 4, drifted: 3, ahead: 1 }, + summary: { apps: 5, drifted: 4, ahead: 1, split: 1 }, }; // One row per reachable state the fleet screen distinguishes: present (one // healthy and current, one present but behind with its digest still // unverified), absent, denied, and unreachable. Pending has no producer from // Service.Fleet by construction, so it is not represented here. +// Row order matches internal/fleet.severity(): unreachable(5), behind(4), +// denied(3), then present-and-current/absent tie at 0, broken alphabetically +// by context name. See #80 — a screenshot approving a row order the backend +// cannot produce approves nothing. export const fleetView = { app: "payments", namespace: "team-a", newest: "v1.8.2", clusters: 5, present: 2, behind: 1, mutableTag: false, digestUnverified: 1, rows: [ { - context: "qa1", clusterId: "qa1", env: "qa", envColor: "green", envRisk: "low", - namespace: "team-a", state: "present", - image: "ghcr.io/acme/payments:1.8.2", tag: "v1.8.2", - digest: "sha256:aaaa000000000000000000000000000000000000000000000000000000000000", - health: "healthy", ready: "2/2", revisionAt: "2026-07-24T09:00:00Z", behind: false, + context: "prod-eu-west", clusterId: "", env: "prod-eu-west", envColor: "red", envRisk: "high", + state: "unreachable", note: "the cluster did not answer", }, { // The env column prints the resolved name verbatim rather than a @@ -168,10 +189,6 @@ export const fleetView = { health: "degraded", ready: "5/6", revisionAt: "2026-07-14T09:12:00Z", behind: true, note: "behind v1.8.2", }, - { - context: "prod-eu-west", clusterId: "", env: "prod-eu-west", envColor: "red", envRisk: "high", - state: "unreachable", note: "the cluster did not answer", - }, { context: "stg1", clusterId: "stg1", env: "stg", envColor: "amber", envRisk: "medium", state: "denied", note: "list deployments forbidden; ask for list deployments in team-a", @@ -180,6 +197,13 @@ export const fleetView = { context: "dr-frankfurt", clusterId: "dr-frankfurt", env: "dr-frankfurt", envColor: "violet", envRisk: "high", namespace: "team-a", state: "absent", note: "not deployed here", }, + { + context: "qa1", clusterId: "qa1", env: "qa", envColor: "green", envRisk: "low", + namespace: "team-a", state: "present", + image: "ghcr.io/acme/payments:1.8.2", tag: "v1.8.2", + digest: "sha256:aaaa000000000000000000000000000000000000000000000000000000000000", + health: "healthy", ready: "2/2", revisionAt: "2026-07-24T09:00:00Z", behind: false, + }, ], }; diff --git a/web/e2e/screens.spec.ts b/web/e2e/screens.spec.ts index 809eafb..d103605 100644 --- a/web/e2e/screens.spec.ts +++ b/web/e2e/screens.spec.ts @@ -67,6 +67,16 @@ test("promotion matrix", async ({ page }) => { await expect(page.locator("table.matrix tbody tr").first()).toContainText("search-indexer"); await expect(page.locator(".drift-flag.ahead")).toHaveText("ahead of upstream"); await expect(page.locator(".cell-none")).toHaveCount(1); + // A split cell renders the backend's note verbatim, in the error hue, and + // clicking it opens the fleet screen for the app it belongs to — this is + // the only promotion UI the fleet branch added, and until #81 nothing in + // this suite rendered it. + const split = page.locator(".cell-tag", { hasText: "one tag across 3 clusters but 2 digests" }); + await expect(split).toBeVisible(); + await expect(split).toHaveCSS("color", "rgb(229, 72, 77)"); // var(--err) + await split.click(); + await expect(page).toHaveURL(/#fleet\/api-gateway\/team-a/); + await page.goBack(); await expect(page).toHaveScreenshot("promotion.png", { fullPage: true }); }); @@ -74,6 +84,12 @@ test("fleet", async ({ page }) => { await page.goto(`/${token}#fleet/payments/team-a`); await expect(page.getByRole("heading", { name: "payments" })).toBeVisible(); await expect(page.locator("table.tbl tbody tr")).toHaveCount(5); + // Sorted by what needs attention, not by fixture order: unreachable first, + // then behind, then denied, then the two clusters with nothing to report. + await expect(page.locator("table.tbl tbody tr").first()).toContainText("prod-eu-west"); + await expect(page.locator("table.tbl tbody tr").nth(1)).toContainText("prod-us-east"); + await expect(page.locator("table.tbl tbody tr").nth(2)).toContainText("stg1"); + await expect(page.locator("table.tbl tbody tr").last()).toContainText("qa1"); // Unreachable and denied are different facts and must read differently, not // as two spellings of "no answer". await expect(page.locator("table.tbl tbody tr", { hasText: "prod-eu-west" })).toContainText("no answer"); diff --git a/web/e2e/screens.spec.ts-snapshots/fleet-chromium-darwin.png b/web/e2e/screens.spec.ts-snapshots/fleet-chromium-darwin.png index 7448fb67b5c8bc68e0c7bcac7e43ed950fff4846..e06a117c200e094b9bedae6b73f9af7513c1b041 100644 GIT binary patch literal 98024 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 ZIDbPDhM>8yw~agWw69($()=y#-vB3VwygjF literal 98007 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 Vk{pNDJpEw63ldX3x(1JhZzTaswCHaW12U z_;|fYxuR3;e#gO~5IWHxl?@=uT7IGO6ZO;P`+3ptB&Ey=Z`>&mNFCy;2YgjK851P3 z6(>v*LC;#(6Vp~DE|m{i{k*}1xCtqW^?;n521|Y%r0cVI4V&rNBF(BuRh~Kpm%YmE zv^fIrJ--Wc%jy#Yq}>)FRaj9`14I2LY!uG7$i!{Qts8>d0;+~Z&e3LbdZ;4jZ~eYm zJVTVIP-B2cD0!zMrJI;a9!vytPfXDz~d3Ch`urRs9#Zf@7h9c@SZTb{^=exJ4ZrwFWF` zt;=P9MTLNwuV%8i$dc2?1ht&bP&;n#Gli@AFpZ3KBcvfRZf;-8wK@XhRN9=$g#5FN z2{&{H`lX)#$cIS1#Vs>=_GAE_+xmJvhmgZp8Q~!D%c#7rca*lmQ0m7i zO|LXo)GS!-&Hz~UA3}FpsSJ0ArFAnqcX>L|-i2l3h zV)K^iF9#qugwBciK_|Q1k{H3AD2i+^4V}mh(LWAmw6bnJqhkW{yvQ+!dTY(P1}2zR zB~ksmc&PQecz9Z+7y9bae}t27F4t~Pa18y|hSF41Y3bGNm<`7kD#rg6bo-&~&i2=j zs&?z9ibI4@I5t|C#plr0Jfl+~_AHYjGT$x8ES53{Jp#V!!h8Ab)!Sk8-2N#t@~w3R zz%D-tC%SUUTwuSiJ$sE_FTCWrWI_+n|AFeQ#$@BMp!(FDRO1(}%Eh#1V5p%^2Y-4j_f@_^`m8fGmD1D+wb*>cM`l2Bc--u= zp?7}u_1ae2u5UC0qi>Vr=7wzXmbOVT*`rRg0B9T%Vv_2bTDuJwUmPQ!I=y45s<{Y^ z3rhVMs=u-Bc$;l=Pe>x><*TBudUD$!yyVh+UqA=V^7FOh0jK-(J`-|8FDaJ+tC_h@ zZKN!S!|h(*K)^TU3AVS6b+;FaJ3e^Z=Hhy`4((cnC6w8eFO5u;&-$aFTzuD#Jm6w# zHxUa{-){`F&?5mXg;K_$#&=@Np8j>gz94z+RNjKw=U*048g%U48z4<5tqX~uodOSi z!sn6;3{UK}zYWikP;yyC6qB8Nl%}Jz+j(=Vu5B5ot^2vDV&*p-TB=i@3(cjcJSH-j zUnQWrqrL5jNQmKi$Bt?>*2GHB@N#I_<*4N(68x9g0D-w)sZfoMuyeF`mvgFEWXLzu zXNl8&cOjc3_gY@*6jqP`GaEHaEIK^Cs^nlB3*#5dx!p@N{5GnV^A43cJZp!Arl60K zYet#p3fnnl>8<4nvjI-l1qsvgP@(*$ens2SqmK%gmzHshLTMpn(u9#+NVww*nH+&H zBqcGyjB9$*Vap%2-{**9c-H0CztE+Z&rW_V^Yy+$a-x5KbV4ESXm=azs}Y-KInplS zs;+tLL)qy6xPlTUhRrG`W@$wL^F{I$?dQF3yO-wK>d+6bij*D82~qrNId{iJohpHfHyv7+ofl<74<6U{ib z!!%t^rG!Suwl%z~Vr>*i*X23G3#U+@dANu!Rk}F3Zwn+YHzNoKJQp zY>-8;YNyja7KPT|!_ixGSJ&cUpc|xg>d>2TZG==Zb48sSqz+-7W#}E_D=!Ad!X1v8 zL{($N0+Ud+uS#rehdwg=9(_XT2tUY?IJd2ebvO%}30-uARTRT;E=!Oi%w%Nexw>o? zJ~3>;JjLEvOsUAZ91HC0F3ng?bE(Q&O>+ijBE7j%%y$Rai3x#sZ_T3yhV?33b+|G6 z#zp=@;Bzx|TT^E%(LiKia>sjRxztfbP}H1#G?O2gw+ z>#rC{L5|nmR%Qhey2|UXTmr1kxs+<*Atku)xhB2;++0C@&=@EkLi<E$iku2EjZ`jZx4Z)$D*85m*=;Vcn^x1L1wMM!N80!evY)bvXgoEOyZx~w?z@= zFdjqq*(Qz!ty2gGSx(De0Oa$Fi>JHAdo*F2%a47~cdwNTm&!SlyQ3ju-Dwo^#cgo~?qvr+{c>KJBfAuM7Og$Q&7e398OObze z4F6xuJN_RAC&r~Wf5i-;1*m08V3-(#|Qp0AmVplDY(xpzrb>F^p#JsO!$j{ zm~hDC)e=NF+Et%l3kM|O1-p9&#HA!Lx{*Cj{^P)S1H0hMf9OJET199;!hqL~?DXMQEU;!>kEOY(U9b>VGWRJhy^dzg=<09SXVBSB( zfGQlGK8W`n-`nnH={5ZIX0%K=Gb$g>B!IM@2^MP)${+uhS9K8rUi; zS8T6l6Ue!D6!S(*7!}9&e^hCeOmVPLiIuGA>>cLZV+W1INjl|@rDFh#46o5dzc2#oLk@Y(bnR{=%QU|5s2AcmD!v94YF zx)|Y@%E}C?k0Q?}&`g7nZxz1c94953W8J_854+dTUlNWnoWFfX$W4TC+65x>Rud^d zcDcXo?d|>W^}P>}UVj-&&9TMe+K^G*bfru79IP7#*L=pKUR2~*i$gVcNA3*^kFGd6 zE(@N0$J`U6Jq((FiV=_qg!nAf{^w%JL78Vj z{zq#sa7RaYF-{TB?ut+{wSaJ9Z}U0Na{k*jw5W|ckInx2^l+M48g^SkbmOn>Ajkp| z#m?P{b?)m-@je1~w}-KB^XA37%dMz>hA**oij#TATy9|-Sc{`Q)zws!6O1f* zuV9kgVkDa=8m&Hs^i{c@oOfkH-QOa=@muO@jk2fOWQvvgw6A4{6%Ch?Eg>qtRQ3Jv zNA@%2L3jUrZ$OrW!UZ6hIEBDe;7x->95&e{{yWz3DZ7)iZ`|fq5_8AVttOSkB*D4y z5(vP)D&c6s2HKbH#lG>CwWqw|QlO}^r|6E1_R)>Br?)J`U@CHpBN|B_25P{ARh2J6<{ew{QgSj}oHCA$}^3OAf!!Hyn!I1kWo_leVx{QAw2ciSfP%Y3~p z!P~q40x~`};Ut`V4kU)k=iJ_LhHavXM{1Hl>w9x*Yu7nk$SG=O64Y{3G~IyuYk%Fh zPh8_;3*(Lov!$W^7W1cm(mc4%cc`+SBDn7wA6AXMn#|3((RVpFTFIP@>C^77bskBX z2mzp`oTC_+^95+%|?oSadm|u3%$66$vpNFa@<=NWNeVjeS>-b&C>Bth< ziQZSD%U4HuS^Dm;iuwspG;Z%o{8g^X0S3nMg3F*?vvctG$}k9J|dHlZU)6aU+;?Mk_t2-QlWqo z>0~W3J8Foe-+RQq-c6$%f7dtWwSwr}j2m830P0H!!?h6?mhe}E6Fys0ljnm4kVJlP zdZCtEW=+;?Ck@Tm=z(Y29YX6lw>Kd{sYnhwx&Qm#w3@vKzah`nv!C>D#hS&$q$crF zlEAN=F__*C9s9p4d)gEhmp{<(OHNM#n+u`mMQD&yv8Iqp)uE$)DJm-`jv`wzS`b<} z33#xSaCWP^HE3nUa9h`h~>S%a`rx z`ksF0XX72UG`!kBX&M{>GsmlHdt+ng%f%cjIi#il9uI&?N z`YwT9E*mw^6&vlN1oM|;pVJqucJRncPj@Z~LsX;Zy)%MV(knxsuQ!x+1narIwtvfd zVZCUb0Udw(Ago#I_ovEr3srNYV{4DSHl=iDP;a{Zlu<~mQrB{T1Uki!V%KBX%SR-?A6PQ0R8qeS zQDvjs3o6;MnlMJBWW#H5-h%^DL3Bc`{Jrn5GT&Ep2@UV)6$?UZE-ie-8h!sO0Ruy_ zwf2o<8;uSc)ada~cM8*dT@)))1`1TWZD{=9mu%@GvaAyM#A08*XjhwhK!QQQ=JT1X zky97G#r`s{v*`~xPUcU}_2_j7Qe5I$XI4bIq zXQ5F917@8Zt^O{;us>OmA>wiZjF3U-kEJxtHsBTL3BioI4c5mJwp5L4;0ZAbw1s!oPma%J}rqp{CV@KY`;P z*dJ`9s4HVbwcTNlC*{_ihLBSA`5Z`4Y_&C9tHzl8fzDkxPYO)snKMH zGxOC^;E@M1IC->{Ptz>oC_!^Q(7(d6P9<(&aG(>_FJ1W#k!A#D42geSXB0d@WDFNl z!XJ40jRaQF`KCa18Z%Y(eRCo7Rp6>Nn%z6kB_S-hLy{*~A%?-Znik9%Yy7f2N_7R7 z`&C9pD^NlQEe0yn=TW1NW`)@7YMb{ipXx*{+Wg340c(wI(kAR~)FQ4v0=|S+y_`q! z!v`4>Pt-HjAIr#3H~5BrMv5j^f8{(3%t|^iALZtOA)UVGe%i%7@%D z$@yokD{Kid-QlslmWqI^qKCh$31i};*91EM=oFOrCxCuK{qZ~+2|i%7%-5Xq=>{3n zn0*kjkvmWHAmWbh`*!mi872bGq5W|M;KOQM73#N{yf<1(2v6o-iyL;l#o=%?K+P!W z1}d`&e-602SV8y0mo1j9!#iPYukC%cza z9^dN2R+R1?^_)w~HoCzOeXW3?#kcF1n>wT*qroJBd$FQT=vVr(&hhHUA?)z~(9a3X z=!x1|Fx7-wHaKql0C8)T_oL6MgwV?gA-$WQt2^IldU~pmBthyi%;#Kvs(1Gu??4@Q zS&^@#sN_4H$4S1~nd8I(U-{-wzmig!PoR;K@@j%Rd6rbA->Pg$5yc7?qtlu;rCm+( zqszu&&L&>$Ac7iFh?1z5Vr7xEqO3z(q|`Xx^KEv9@N`!hLG>=ew3)T9D|~LAXb?O+ zxlh__;KNIs3;#81#V%?lA~X@-qJnBqIOg>uFejI}T_RC)=JWQ*p*=x_I5B6xzq~{X zeQOUa!O&lYQo8j3VDMp5#hTe*V?D>%;KJVV?USOFOSFa!Oka;g&SJ$M3Mf`f?)FE> zF=-$*mHg|PY!upFu&dx>omrf(TmIi>KvH5#)XQx9HEbVdLI(b7XwaY`e){Z;soN}Z zz{{C?A^{*3K(DV|&QmrY)m;l;br+OfPW~>tFxk$HmXv0E|5G`+W>H6%wKXYIfc1Z6 z(Av3lLJ25*gX%vIc6)=hN~nMsy>wSY&lVE=lt}M5z%=>UnTlb`pO%gI`w!)(xnw^; z-1M&1ZyYpm*pcFI>vl|EwK95-U^`a$kC^m>C8^h~8HbPIB3REm(3D)ae4u&x$e=SP zje9%6dKl}OZ9$AHqo(Mi7DfPS=S*ZhY*d3%oHlE#jILnfA(DeH5aArg-_gw`fEd!Q zU^ix&AhC`6d`@K^*|K7M@7xq8p8q%*P32Mru~@m_b<~rf?5u2;`NOi6Ck7patkhy8 zE{Y=n*!e+HR2pju0Yx_8Qab34 zTW^~vW0HX{{h%N`mj{#DLMhfSENS`214D-J_c1?u+Q+pDHyHF8H2NcSPAvVIJBei+ z+Tet+WSFyGq5BpFJ>kf4@p0XiaF;EvTzj)tpLDHACw{JHi-0U?I$L?r+jpJYM9}Rt zCrRVW1(UKl`W78W7dUK3fuY;ZEVRlvxJ16(b-hsSXQ`^pIv3}8U*OFRc7wPBssbaBak9ik>p6MQ`Sz^O|E^yC{ zE$iFTeukV?)6DbB&}pj?s!KY>`KTS-gl+3C{`4}m0 z@C`=sk2HPvvt!%F)bJwNT-kRy;rJeJ3c2}au%4_G-EB>7xhetMLFlJzxRSOlt3^Ja@6S42W8-phGn6%KQ^zDw84xCk%lAU;EYE z;idRFI4z++O(Zr3(38vGq_&&GueXu`UsutiP$<%Ked$F?V{a;Nh)RfItqaGTf`7r_dBL;I^)LvxdUUnWU@IH;{5`0vekl@0tp`Nz;AZlG zUqt3IN{Z&pIIl8^R#D1D8q2EJ6dN@JStQyJa(S+shK}q?>hLoK%BM04>OUI(eMu%y z#E=1S(EkusQ=X`_F~$$9Xzc~oc}?vhUG2i{K>eaz0=h2bSU#{G8FMCUDLxPzq zECI981k0QryJ2PNldc#&oTn7E{j*0_oJ^6GJWYhMg&@kLZ|90qPg^U^7>G)Wz3QW4nG*n(g|w(DB%}u=&4QE+97JH)v4M z*M_ITDjEZ?bh-jqnWEK`-b5nre8s&-(72WSnZob6mnXS($3x7e=*~kjC3YjMK{hv^1-)Dy+ve_c^tD!GA2gCD4|B zU<>0Xz8-ZugSP!F2r4TN!DY~dL!eok%3gM#7^y~8g*anvqE=&<Avoc)KWW(swZ?wk6TIxa05mm zjcN1=eVM#Wf*(I*qhhS=w(+@>gt3!}%`yCLd$d&tsiJ5@C{uVLn?|6=DL#a%CAR$r z$gN@Q-T+XS0>)H;3R!J0`lw)%k&5FYwGazi)5EKi{T?xT%cT#r&l9Zctg&dN$v`*^ zdAZ6>qmk5nA@DRMAkA{n4Zh8q;X_!X@@jyWa=&e7Q zVIU};gBASgAJ`SIn~o{9sKGy`kE*+J#xE8&|19eh0OIkl!arj7W+)yT4SF{pwm=eT zKUuuVSKr+k_0zQuGd5x$WkwzGPo`A-zv~k&2qwQy&+Oj(@>V66{YMM9z9Zv}PfjVE zT_ESu|5LaSvNHmpHN%D1JK=F(7+g~XUIJ;g%LN^X&IVl!DiQK9{95Ee6#0G6?AoNl ze%Bum)rkRtcxgrdWtD?@L4iuu9^f5Ar#4K_+2)lu5lkd7urQEb-viya^f&}TT5^pi zgv4}~;p-S<1(bNJ-I&+X&uXj}ELE>*OY+3n5*#PQ9(|tk*D>mmmByQPg($II>fms8=N=9eR8JFdVO9Yhl4r>2~DVu>t0k zljiUKgzVM8(2+UF{y#BsflSoXvjw=SOC=>>zkVT3UI6Jt09vgZa`nU!r;LnR89VQh znkJg~(zYkih04AY|ct?k#}w!QE;(PM2@1Nq6}S zK+OO)v>JM(QO>J2A^DF|%*INSEdQ#VTB>Rhm&zY3xo0Qc&!LGcz}8PMJy^vo%PuN} zz!rhlF0#GkSEQQINE%Vmu}I`st#IQktn8n&9{`*z=`)Wv_A{~GhTj2r@EZc(iD<)Y zn;5L*blC29YVbU-ydnyAafdB_H#R!?+^P1=lcjPgS7IOM;aK+s&~@$8r1u4VM&cr4 z1CY_jUo3;RDA?*MTvD_Y&VT-7ZJ_$)Z@^ZaM!}z23caN}Ac^(bqeudyQc0^qoeZ8_ zrcgu%I}`Gn(m}u|PV1X6e97X7Lb|L79hXLazU=7;}NRzD1*T+)x)*BUjy-V4~N z=L{TvEOF!7JF6H=caycO8|^h%l@uQWL|RNdrMX(L3Ph0V_qYlqcpw(};=;_qr7k@i z39_oCj&UKp%N6~=CsSdiyX9;(DGVRZw@0-8q0Hxu@Apipt55pxH1nExE-x5S;fDhY zF%C|PyuWsY1Lq6&u2yyUy*WxsYuA2EO%0`X`f60eJ;{fYz&>1Rc)0MZh+_n_bwZOL z3VidnsX9Wm4ET}pKR10qdPJq<_3n<987FS>69OyE#z5zvz+w*?(UqY8-xSG?^HHWg zh&gI&>irH%mp>c>N!;&+k`-*cI){y?O#^fk_`7?Zgg3jrVm-wz`nYjr`tOLfLV_^$ z?A>(3ht}?Q*WNfSHl!q7+B^9SZFO=ziF9}WG2RDea9A+A({)b&DqFU8aJX>1HJpFU z^%O%3yUyQ=d|H~QNCkNZrixT6o^PA|{}a|#wu}0%5qS(Ru&;5Gma^nIBCp5PVLe_f ztp>*#juFTNKRwAg)Us{{y}qba{h{cX;j=W1v$>RFWcxB_=&>xY8iWE?cVKSSC|-T} zis=9qz4%{rE#c=;9{5sig3xxebvrFDlog-IjTV)ADp%nv8oq~n852D)Ac1tjBXrdA z-nZq|#AmITo!6Dw2Feo~bUcmjn`4Eqz7Op^W05us^{#&n+4k?JhmpMas^SMRNA#Kn zEpyw$$Tvia1c-vBFsLwxKb3AjY?~b{-gn==Kf@;?`fhe*5?gWrw~Rv2ZoV~Szu4pj zP52y9n}mdH4@tT$bbXyU*-G9iR!J#0M|Ez}`e0bTY=VJI$>^U29d?qFzPc zpPyU4>Q~WJ>#&Ds`1mU`;L@86jcJl8{H==ZJ1}iB%SzfpSbawb-2v}{MuKwAHAqAG zgRfuCMg!h|_UlNC60*b!Z2g-RC@EIlFM$n;c&Jb9o>QmQD`|oV2@Mg{o-YQG0DCit zbeGeN;=lT%^TF!(pbfOBfjNnT6X(fEjqB=PBR)Ogij)<)b+faDs$pVa480I_7m*Sb z@X8}+prFSaL3lUMju_6W?`YPW5bd`MvX>oSh;yKsaDCKLl2)$tb?3Lv^iPy5Cd$X^ z*m(mSW*-+$xI{e(f|<^KW2DlKqc^f~0*J&fZJHjn&Or5ccc^ElPfj+mkO9#CeYZ_s zjk>$@!vot?SZ<5h_4Tog$sm!p4+xJJRK2cI=ki|k@*R0bk)OYfm2FeWi3`roY43sf zKEEGz&HBVp+g#+@A?IM6lWghNVLr+yQVtfR8AO7lr{q61Cma=``QXX#y0>+BLzUI4 zDG!l2o=ohY6YuihfZ3c<;e%6_yZ2hMC*as*^NrlmOm99Wl}_-f=Gd=mJ{{?f)AHIa zezDq^zCv)J=;(9(@P)3lG;&3+@tGt%V`O{TElVlMNN%ula{nEG#*h5jA~~e_R^GH> zet0}~U?IIZCn@0+?a3eXgE^^K`ejgs8Pb9?Q%z*uNcAwL!^q9idPSUG`iQOhzH>s` zF$T<>RpXAOd{Zw!p8iO%-~rwX0ciB~d|iB8lJ|%1+)mstabzIGONEz)n4>jVN9I;T z@22`Ym>ky~BwWvV2;Q|6K$4egRK(bsE3sN4)2#52G&gIPz8$$ZUk@VF{3}f=w*m_@ zm~N!Sk9-9rqQ8YTZt;?C7xt`tMgTMre&Q=8aXp%aqPN-a&wLZCS!hsEg@qEuHDI)ne=c8A z{GzOg^OOv%)rsf3J*yZ4Il62^1HH*$qL39Qamu+&2%cE`xYCh`V%~SsaA7Cwsw8Lg zfhxw~F{BzI-!kvq`d|e2QXDS{60_00s=ldXD7m9`(04H9C)Pn5^t<`OdC}MOW%x8L zIzJdoH7>NDdi%$t7D!L{EatZsD8tgDlc1RGh{O0xhf!T21pU#tKg|Gcf5RaF_F;zH zk^RN6kdSOUZ`DlA>!u&#@Hpe~YETbNW&RT~I*n-Y^4R`hoho#bhyXx3s=S-Td%m}K z2wF~%$o+{)obz7Tyz=ULv&Fop@@SDGWW8_k6%GPA=ulKfPD$jP3*j>Gt9LsP)gHm@_&Qc7+Jf~8WBf6 zcp1(+8t_Titn?OL6A#5-BxunydNnyYK`@w#Wg$4J{J(}g-s>w!GQzg~eH&(X7xZg5 zq^9XtXR5&P8H*T(thL=ek6{?tY?-H+z;!lHc7FA-qIObQMvj%2iHOG7(RMUN+qVHq zs+)?om6(r zj&!iGS4A`7Sdr1)zGs!i^!PLaRVC5rKOnaKysAHg88~-75AXYVb-jE1IOb$2eM(=- zjg3Xhj}@nIe-V9X+ux5VR^0W6=vpZ@W`hr-#e{Jsl^oM$!tX|vT;;h^nru_r4qTX0 zHy!0m?<2x0EYQ%`eirOAQ{Cdj>qT6$3ila|60V4J4jo&JmSkAeL7ca8$e7A2WbscW znJ_ToFEx_2woVEQ5)3T%bz~oNEiv$^wK+a2!GB_5Py%q=7NBBh~vb=S7p-r%Jz?w^CV2ihJ4Cg>;qf3&mLpRS-Jh=CM1 zfMu}&^63b@963KE1#h7mEjujRMaXMM^bDbZ{j?lY^x((zHpWwD->ROxcpg< z7u_}rJi}2$Xi4T?EQBpMK-V)jD|hFG4z^{IGRroG zr?#5+4;!48uX`|#JZc-(qwX_?6UVhVz>V&iB;^)^5LJsz>zV<#XM`8;6U1(Jg5 z@%u7yN!c!BeHi6*X*HNW!}sAlYy0reJa%i>5KHrN>G{6!Hg(@7@b2kc>!x-tqM*VUJrJXb7C z4$({!ISBOlS8hIM%a%;xot#{wK0({P7{z18e$JCh_Y>U!Ol{>;%|6IoRw=ZB{y()0 z(F$RML5EH>gSs|5_-0Ks$xsSJDg3?Apq;G?NZTBjLgbt{B)yk;xSL87>cRaimJoag zyf33AdZkaloL0#;Qe=)0yO9j%SW6OF+)VdmCEWA~vVbFxj;9aJm2afs}OE$(FBf4759SU~o6gGhO9zXOIZ4 zBZXF)g3CA1?ozz*B}BPG_F*3n#MMwBIwmM?l4^IZ6T`y6x(lipT0HbfAfEhs--8+z zW=N79Y5@i2l<{Qwkj9NK{8}vVW@abg%IewUErWnqN(V&fOjMIn=XOcNrHn0_qmN&; zwC&{SnIoX3+h`ewh1-WB{`sSa|9=h$#J+UnOP{eo(<1~=8r~IEbb+(qp#M9YrJP4_ zh%>Cp?@0mZQ(So&InoAnu4iMHxA!ic+1Jhc*Nz&fk3W64oj*{Qd;y=MtxvF;n~sD= z^DcBjT8c5pR@FZA>jO%3-Dz<SsfN=)4 zz`=$!{6}#)F$hib`@1`83H9G_4?*s4oiJ#Rxxof#pv`rQ3rksP*_zG9&@$jY2g#dq zpWmqa?&{xyQs`3xy04Fl(wF(A3VO@tC8sOb??Bkov*p7jCd~z&F8?je<6jnL9QuDv z!~LPwgUlPCwy(d$1SzJXqGH5W$z?ZnWW`A~fQSQkHPnYa`}zgR0VKFNn*?(Q!{1pm z3o}l+f5K*Gg(dngD>v5jft^f%J$Yox!wv?spI!aeNc8Ke)wzH3ZW@fdO!#IJk^=Ig zhKfdCX&5+MT=g2#viiRt&VUh}v@CZ+Q?!E@-sX@v`WMlP*LR-4KmR}Wpv&@r9J_-P z4~|`h_p5h+D53Aod=Pi+%9Xw=gJ$&vy_A?lQu)4#03r=h0O*3^=c674*vX5!4PEO!Lzmo;d<*xH^E1oT_r zJp>gn3uet)M5P*5C{M5`OtLsPYRy-n&O2!TPr*aFZ)R-Xo70TlS!C8(q8*?=icP1% zwlx0(dHTPaSnX1$-wheoo;`V8Qyt9JwwjjzJNObz1{Fh#No6I?Ih(YqhU&q+i@-+z z;`t@V%B*s*;Tv-Ac|Fc4ljtAAdN;OrLRlrAPYL-MrmgXAeyKpsG_bdx`wQG`+O&D` z#71XlXkiin^=l~J4czBeMS@>e(LcDRM~_I~Glr^C4|KYt-N9B77jb>prTm1B=37LdQvswJ9| zc!9=mF#HqVMfMSO&dCmsX78FTRnmMt^B5kJEE0bDiyj;IUt#BvMcLtAi-0|d-`?{- z!7p;I<}CZXk}IH*w%%16C?9`#{kV7B!%vc9%>L%hzTXF)Z&wB1KFzzFx!T&`Fp<#w zM-hpqX$Id+R5OVopTVB85uk#M)4t}j0drEn6L30A^l#o{rrj4Xv6W~+3X9kh(3O6( z(tkWZeq0_V&a~k7#`(mr^G?~PXL=^}!N$TF73axLBUV(5{>&xdDbRQJmTieO%W3%q zCE0mi&gZ`Qf`W1|JAFWB{yNW^2h4}t9j?7Wxv*!e!tX?rm>+pQe=`Zv!f^SO(!qywCxujNc)U1SbY)^-Kk8uK!zh- zU%-PKi}%-b#pVxRL^x|8XPHhPT#934wx^o|m^D}^{+njasPQxiAqzfyuHJUj{&r@F zCWiP##Kqy@poC8Q_7t!)(JG(8$^SDoOkOAVT|x1K;+ta6n>w>>f-wJ5BSUS@PdQ)# ztN%>)Hp7m`Sl|lCEY}#~pDziK($&NRf8N6>$Hvm8JYcJG^)Dj1`anL(T(C0kCH|Le z=KYodTxu{ZoycX?yX8h3%K}PPR<1@l&OGI?eSFQ~Nx06`BpJ^T)6YMQT8AUQeuH~= zn#s$@-=!t9eC9fU8Pa3mI(iW#`UD43_bj`8{8;DrqeKpK#?>{-CAYuReB;!^tJ;c(_fOU2=(bK_wZ%QwKJ{}_s_JAq3hAu7p&`^AKeQ~~VQ z_a__Wl9hc96{^=L;7NT0L%D=T4jm&G$&lD)nul+Pu%rB^u`ih2t-5X@MMz%K3;O&A ztvx3F_j?DB0SxasDxe&}G=UXdw0>BEcU|4X`{P+%6<>pbQDJZi5vTU!dl0&jDB|j7 zC{7&kY9WB|-BNxAbPaeqfyrx-L8E@FFN?o=;4%!7*3Vt^1`*PM?!?YyPf%a}eT}x> z6G$RSaVCg^tUXFz+iCjU*l00l2s_E;yerDInm{b&xV;61^zy%Me_#IJa*gR-kL_2x;*{bDA zYPZF;LAPNNlZVq)Id+0PSG@QO0?yJyM>nq5fgirNu71o`4DOedR;#Fhvi&{kYXBGN z)6Fp`E$ufLwC&Ar47nym0I;dCNDL9^wO>8?x>v!JoC-ViEL_-+59+`dJc?3*ZwC;l zX{d>FvPg)D4GiYKHe3`t52I>0^mmtWH#iBq5@6~8cIKmEg8B`wE$3O;=eoZM(r8>t z!VJdZe+RLeSfHk8={Wq3uUTieegmB)Y{jU|OCv(7WW@DG*E?!agATvCnY;&XOeM4^ zjsXt8w(d!FR{3m%i70)1?yEIiWgJOTeB8AQGqxdvY8}g){O!;mAPsgmV9BMwigm~T z!X!kbY6gU^#5HK;Z5VJa0Nt5VR`R|Z_Zvk#$dDbfOUzAjzZSt}GT!i0DJTJzVNgEP-B{gl!%SQj&G=?@-_2dhBWC~lrHW0sk-?c`%+25TSOr4g>2@Kic>;h-QHxH zhpo!es8$I?BQuGa{+@yYXBv2;57N0!2$&UfRl8@-M!BzQ=IF$S_z&AH%4$id@+6A_ z;M`s>;NwcrtE)++pnF%C80J99Z#>442yOn2BY|@Y*r;nWt0;8<&*sw*aVw3c&oSdi zyx0RIDq2nd-5`cwe#85clT%HkLZCh3k=~t?_}$_LM!kwuHQV!7Td{U;fhzPH*$0e`u{L&p2mYu~70i^I`c+H@fGr6R#R_ zW+y)Amj%99{e>JomtSbo4?^m{AHHFCWTauj=t%>UM(nF+H->b3iU%@p?c!v`e18A$ z7ku@FcTMJ@A(DsZG5gx7K+K4_Xrg@05d z9_F$;ry24|&LQ8%%zZYal_s#i2=$+lb#?WNN@F0Y&~*|rjY4~6lvfaGZ9GzMyPgzO zReiZtENEAwC$+d-)_+?2>I}nOB`&##ikcA{>IzTI;O|V8Usyds0V*!xs7ZC`-+3*& z?S(gLTSEh@mC$y}E*R=sUUs@#4okwzlMb(_(Nv56w5!X~|9c6d}$M)(ZzB-N>XDflrp?#fxHhEDkZ~a|KschBtYF06cm6fsd*YU|?TeBi6 z+>6I|(*x!EB|eY8>Ckxx^wWDlV`Crvb=}%DNkm1}m;Pq|J#=%;#rDsHXXZr!+_%~d z63;Aij{C`1Wq<8ysekvd99#Wg?OkV7Q%k$H9Z^BB0n)7?AViuF>N%(&AgEL+1`8b$ zq)3N|f~b^;^cs;8>Af2QB49+|NR<*$X+j90hX4t8g5c*m>-@X-`*GLu&+JT+y=Twf z@4PefmggZ&rKOe?3d<;AFU9?lLW7Do3$#wS0@x`1-sz_)(n`^>M_kb`~fkcXAK ztek>$u)`)dwa=M?JkcX7`>~H6Y*-iNwF_98AYzy!@OcWd z_4xUp@kZYlyqpkHQ04dtH1ZpkeCLzgDGm!JU>Qf@R=2aNUi`-I`LX7?qKS#CyjoEG zQ=l@l#3p{VOdLVJQRB&R9Ktz~W)jFb-a91UI88kC@bS8n13Z*fh@r$m4_>R+jBFlz z;0u_Xla$ATyb}QLy3Zo`C6fe3Y!k&dy!P&2)1^`A4M3~rF?aMfqz?wom3pPS*_!G} z0~zu9N95SIN9=M#4jQW5f1r(`V{|*(1%(;|&QwrZN@*aHs&iURoCVK1`?6gnC%@8i zP_1hv(M9UoB28Uq;EfNgx~}h|wzdqv1IR$$i4GszPb z&(}PcATfIQ#K8lR-f)*oYUno6>^-ISitG8uPlQ@Ef6mI;5p(L)`xRe|W*8>`?u*TB zvyrAgmx+A+*R5id`QO)A(tiv?%^Jy9lQ#OISl*qe>XA`GBx#;o-8z;tpV#zdFEPR#+`5HwxOo` z{mEY#p(Dv+U>qB!dR(52+CK4g34#M?B1JCqS26+s4jXjbYIA4L0@MW9mdhVp->1X3cmYNw`wZ{ugpiS3_Rhd)j^pn7?LLEo1E z5K#lyL9hl;@oNm8$BoMzVq;h+^jzg{YF{|43@|!O5y>Fi?^^^2+R`+s+PDSq$Nc@E z-B%BX$kpCh0(pL~=_v_ldg16b1`DGqo{{_KTL<5~=JWBV*N=^q0-kJ4nJw~R@7njl zq`&LxF@u%POVY}b20a{621l=CB3mk=kOn**}gP}4)nN%1+h zHX?%tai;pfu%J}otGCsSo^AgcqRL4mB5x}J4{g;0mQTy^a&Y#Y8_Wfc(0BR4tzHpKfpIzk#5f?r? z6qOk7H4%K3Tw;jf{}sGQVCH!Mx*0Ki(%8-Wv(~OU3Hgyl z4=094SuOR`FWarn`+ZN`4uZUY@xeln6ORFah}8$DxqgAxHXVxcRD6=_(G5b6+&JUq z=eN$Er1^LJT(5FQ8GO{yRu+a2v-1Od#$TMbPZ_XH%yj~Gb>V;>?bgm6fTW#?60h}n zc|WJm`O`Pv-rV7nBGp6Kor)ix-LpA9=HlyiF?kZ4Dhbt(#3 z^m7TB-{S%z@ZY)oT*A3DN_<*rOgON2N z2+{V2{jsVPc>EQ`<5Ai6^thcXP#9m};CDcMTBqQK&NK_}uzGihX zD!(mhVnWxG)bMugidtEV18ds#ttFrW173x~Bx~L&JtY-)_{4dc$69{^E1mFZgDvqk z;@%MN>C#d#eS_3icUwFZdOmB-&0WuBgUb6}NLWsF&#&kN!10nbE4Y1e zMB?av7|Q6Fh3JOIK;ZbreYq%*QOzhCHUm+&z2^*QMr=b_-}JM%`2zDVN}u2I_YUuo(K1@u<`z?K22=q}-0CwnxNXAFrBTk$j>il9 z#;lhPl-0RKTRY#p)SGMQj_#o4lI=8hFfo|8l54kvLx9jdP;UFWmlu?Odi?(B_tW?- z8^4Fc{}1JmF}7W1+qV548W;c4|Exie^xp{%d#-)~<$v!d`h!{td5JjRy?Vu^)82#JNr@k7zDp~g>n5ci z6tW)s&}Fx7nQ|jGHW?4@;z8RI)2^_%J|A&P`Y#W=0uhj89uLPRM01R^S#fu;S#`e;h<9gXvspOXSBvJxZ0JQVNpi!|zuw z7I)B$`fZB?gHZEz-9_VV92(3{mg9TQ$viF-*!Gx3c6P)_Q5&#!;5>t`VeG z|J1G=jQ8AVm!mGu17M!G_Nz$iYnIb^?60<02a9H3$VbI5&J%K542#1Dt8i;)K&1TN z*Y10aP$S44=Z76#{;Vn&KqvaGppoeg&)YZ=tn8HoYwf9K$~fA$ap&}S4&9K;*`oC) z7dtxGdP1G=roCXPr9|OgI$aE?G>TM z=a2=FS}dL2&}43|<6&1;wnRo|-1VaoROFL_SwN>NN)?#Gw3^hJX#I5O(mrOcV73lt z%BzSws-iIou{B0#juiXk2^`5fnQHBb9=j=EGfQjK?m+Z|}Y zFoiXnv)gtOTh#7)r&2zul0AcpJ3hRBe=T#s`|@!#vSI~pf;P+;`7~6selV(Jm5QHD zcto2bHK}QiM(&>Mc`WDY(rM0ky9vV==Fv+%$HYr^M^WpMziOmSFcH?+-x6~xJ(_U9r!!}h?(M(0bzMDv~ zBiBIgdbo}((lN-Ak%bYtmf~K!Xr(BEmE*Mx% z(=3m8`|cK@Vses1e>bu~grSzBR<*D~5Zz7>kPOibVX9VOMj19@GKP5hd2z~@Q_Hzq zeCUL_c4@m{tnIsy8*cP*uAp(wYH!#++m~ z=yqA+dcEP~g5<{Hf$i*+kN`tJd4Mnw8=urX z>b$DZ@4^<%3L||KSA5OFMuuY(W0Rpuel9b1-hNc4nrlESCaC16V+F}AVIim(k*Gj* z(TUN)rDiLv@;@{_ZR+ivN)u8^e-gr-NNuZ35S+QU*zZNlG0Wr?;NzQ~PLlzHr!aHS zS~iT0jm^y+!7HkrQMNEM3G1=)m=7PHHX2ga{Z#p#mqcRK8EY)eYZ-5Bmiswxw4~sy z?kSV2xf@b+;ZB~`cd-OJOVFoUp%88=C(7 zt-6;zkP)B4isfY@9j3loJweWP#8toX7T^{M)wzPmQPODssm5pV6PR_XQJ)GLGBPMT zoRX3xgl*z&|D^l)s7O?kAf^Yo=D1XL4U8Oa-t7GKdl!-UbzH3cP_Zj5-Q9Fr+Y!DR z`m{h&Yr_`{gj~KqwIn{YPVY4EAqLgy?j799m&e}z0pDb-iZ?U!~l~plT z8sq`!J?Y8ubj?znqJCytOpGI+0N(f?HRT-brJ#yAT{oUFx+U7Dn%f(p7%J)2NP3Iq!E0q%hU((~f*nva9et1g=~+$Pz2?_OJnoY4!KDJ(^{MI3GSz`VIuuu?gP|R& zUQoGiAq6F5U`_`Yx~3c`F5e+fCwLCexQjt{?-gWo zHqtQ>eN#Ez8Lo_Jazs>c6JVOq&jbw~)KzU9RPjCNz=N~R+N~|t$d`jOUvWbHx|cS& zM!7ne4#}lY%As@3_>z-z;Y!HpXmufsNY~_G_m~(3+v4L)^(mh(+>31Q+GCwnR#03M z4J4x$Q#F5>*Zc-~5UWryc9?bkbcdRGF$@x|SvpuA;2NYkniQ(A_J9ZE}*oXJ_203eUf(tN+q^_su#m oBdxC^CX*h6ws$l3U^}v)zxci$=khNd7h^`R8d|684({51_lOMN>WT21_nM01_mzREiANWmg!X> z3=A5Kl$fxpTiW3gycglrJi=+Rb3=bGVE>B@a-pKx*!r=HV54qcV^_}-lU2)zrk)l+tn8aXUgNHf<2*P(#~0rMNEi z74&|8ZmGkE@NWx-bo67#-(DDd)K@5fyF)zR+Wl<`EsFZV{cW!k!xH{&qTQga{%!g- z2E)Ah+wSIn&GolQdi;Oj(g^ukm8XY}qhC=z7bnkgi}zf{(8P_2SsSt>7RgAPJY&N~ zQ#ZIZ3XvN8d5SEOPYz8&)bk9X#%l8c{G&yR#dokiH)3@`CWY^a^|Rz9(ZfCs3!v$> z^($Yed`WEV<6{XFSpr(U!OPa9j|~Rxn1iys{kc1ko(oAF-r5PZYBziJ{37wyz3oEH z^f~Ksp3#RK8sX@=t+CJOEwJ*lvN{e<77xGb-Ee6-MDex=ig_+^K6hV=qmHLjW~ojB z2P#raOG{ydbPs?3_Nz9Rdb$hxm>nOp>@IH~Kjbzgijf&TyU-79194*kd^wqS6o$n@ z)OqikO>%MMPBrzEE#`?$^M(k%S3Za*s#fI9nUhhlrtW8FEBY~^T~f)S875t(J+4;; zD=mHlCYxV1<{#GOT@JUiz7=23o0QFj3?a!2f}elK^lt{{_HCgM3?QY6(W;o&$#Qjk z%=7l2Q_%mI@apN3#~ZQcB-yM* z-5x@Nh$HJuTZdj7U)n=;y0+J3(b}a(&)AH?-Xlyh$#6(Rq6RoHFsq7dH+LnjHb&Q( zuf0pAtbWqm5K^utMG`H(7UzWh4g-*5_W!-C3Xyx;V<-_DSRbTG8(%t=Fq(fEmP3HY zeK%=3rol<&-ERp{#j$cKrXdlY#*E9F$0e*SRpwkC8hba<>b-F7os2m#EVT%zFGi* zXRSC8P+r+ISWfiHZBRuN!t`A*wI{`=A35-xwC!Y+g4!qrnO<_rrYp@5#39WyL1i5LzJ#Fnfw|{ z!BX5~Hrs3y2|38_?kXUfTAtVaY*^c6!m!jlVD&ar6P0&8$=XuGlg}n)y3lkGGi-kx zJzksFWzb3b9l{eG&P#uMvY^*DZFNRDE4><823FpW6wVj>@h+v})*uZwAb>y1h0!^}3AbH=Z`{QxZUN3Q`7bUPZn3P8l*U+~)z5 znqgc;vaeYJhaYq92dld$*%G@tcitCSL+=#YO(rVy9v&b1ITur>&h-Nxou04}$I~nP=TvT_-GjX+s#cnKZ`m#I($I@_6^Zfmw>3l>*nc)#nTI6RJ8yNVi%Ei3zb(n4!BliS0feCnwxE zK&JlwAsoaW{)2sbpd4fXM`p_hR9IC&tE=t8XdN5z_$z48fe|Q@ixu7-RF8d3H!+hJ zdf6(Hxsd`vXKjvJ^!Cf}ZUt#BF zTB$YFECz*zuS%C~Jv@omZg)r;(83zAfqWNU-H-3ikWw zR`vjqlyni+?~L)sHa%Y^9K*h-CfXG82kSthwhL^Gw^0Z_#z6OCAf>PY~ZFWKO>8Tku?V3-?YFj-K0Tc5rVL(ZWHYX_`F(LlhY) z5+Qz`@<84|>~uLmW&(BD_oYrlKR!0mb@^@wbZ2zMs=zNE+9eUu_;@~K+qR{jsOO|~ zR{0gP+<>ehfnK}u^x>pEmD_QhtZ1flchgo1$kKp>7>2A}(4 zqy6yj?1}YWHSlw_*(+e5!qDK?AlN4?Ji)6FqSR@ImuCV|%Y)s}V8=bDoWctt3XW-Y z9N_X51EIQ%vPW-4Jk->FZ{;-0x2J}1CS)pZ4(CU#?sHZzP?4IDiyD`ml7=C6|GTgECp2Z_I**;}g!|^r$T=pS{C5aP_*g$^r)uMilBJ8^ttdpt#LMYt7dQoH z=QA_#f@Yeao`SkevC6w<%4+k%wdG!Vpx=JhDt>{pp@A)b2TPGZbAOE^oEv}1fK1TF zsV*=YB*oMuB3z?twICAxp@en9Wa<;;n1yPB)m8JNs{9N{Ayu0!o z@9XDa60bIc&YAb^It?b)T3WZH4Rcl|v099=-VFwwH!HqibCpJB-{~?f!KX_jcP)2I z%Y&UvACK`et;ItuEG+h~Qz(V8@)VfV$$Z8R(C~T!ez9@oTJ}9@GXnBP?GM;A3|Vm~ zA$W|vZu5%PLnm%XEwG+m@)+#Vp1!tTNJ&Z2*4D;;dAyk1o4C21+O-v34jEJJS}__O z@vW%VDen~WgMBq%zBjCxwOY~hZBqn`9V`FcecY}!5kurvwqj*=vvRSzqi+z6I0<)+ z3UR#p&=QfTscYs;*VRJ^;w@YpIuiWuiHfs%XjN{y2v+DlX5q;6^7!G+L#a!`E-On; z=kJUm2cARKkS>$D1^P>zXnaj-W`15!ea_^8{e(#AptN#Lb<>>r6JSk$dQMkAF$q1& zZTya{6}FS~>Br)}{ZI}aqVKm9W$H(9QdhR0&RZm&cc7Tw>6ckS zO+84OAlW?*FmiMwbZJh@GO;TiLnA6qZ;=Y1H5aK0fyL4TrCMyS$z!yvFlLYMpGzE0 zKhq`*?!orX=@dGYw5h1^#Yk8jn-xxF^>80DpMXyvT5EsR+CME)3(Zv|bll?o zg@Hp@at-(q!eHJephRrh6$+`Nam)o((f+hQtwNgIwmhoC1e~ zBt8%Plr)x?U=n-{h;vxVe5m;q4cg1WDkxP1@_Ow_&LoQr*lc86E+XIS>IizP02~mX z6!jn#qxehL4bQtxtfiCMvDyAExXt%RW;W~9_MY(Y{nUa-47JZ|-U?VzDozWfZNxxE zDUiJ6X|pbXDZ-P1BSBLFabjWGr1=rMDq&f{pU8h>u(U$JemgYN`+g3ZcRii8y5}1k zqb|FW-t2lEpF(vb^v8Wo!k+dDfe?htBjp&-Qe2JSlZ3d`$=+n zu4GRcX817O7Bb(498)}=S4y=>WS9j}F3pBnQ&SX_1lsq`I_XvAM<8RVEzJ58`Ux3R z9mJLE=CWOST{_91?;La_XcG%fOiXuhf>tW6oGQ%dkg?{kl(P!cbvPn;4o{;oPzo_D zt;Mx06EiZQQy&eyD-HRLUl9cVL%4)ldW%J8N9_%Jtl|W-+G!Dcdm^5wFma0xugWJ) zp_fXzx+&U7^@{0Fg{DduK5TGPL>5Cb&7(Px{=5>RI&Gy`EOtc|IbaS$%OEO(2)#_? zvQOrUNtIb_%r7}Eb&MLS>?L0RQ#UJWuGetJlNt#RtxrT?R&vm1w9}SfXR!4<`&H3m zh;*-cIjvLxSot?;;l^&AiYIiC>PIw;efrq1$+(H#RZQ_+3@`LRAJBgKLNPoeGezXS zzZ^TWt>`fX(e)_zyM3X=ZbV|of zIGUQ=@lp0heQ2_^oRO%NbBk_gUAndiukwdwM6Ba+z1#W;!H?4zAL=@*X z&27rb!QOsz+_KT9;C8l^NPv&u{=6Q(5heJ1#kt(L`*$Y%jX~9|fg-7u+a+KVNQcerJ&v~C(E+B&hLy+GELZ15xBFU1uYEWTk0QKisrIJYYA8uM_C8?yAsMxF)5l6 zN9B_-X(>&m{xE=GI}FtwHiG%wP~t>?{N5%3OzHS}6OJ*unk>yv(Qdq{bG!F7I&#An zET?yqdW>7|Z&mR=gt=PoyUtmM((JjE&K=c;=g(n=o7>tLAcV{Z#NkJFnPlg?()kMI zEWNq+um5&eFLxPe?4VZ0XH)1ef>dkRYWGb-e8!D`Cx>tFw1|w-=D{g_X{$DPLm1of zR!01O@=`aTZ%>u#&?D*X&0_LtfKazs2DYd@1+Yv0W7KH~Z4gRqcCMYOJ`vr0&9A31 zN6uZiBSOp;=K4CWE?!YuoYmcW@Ks@21~02MPZ)gwZ+T@I$H!3K*IarHGSnYb(Y8Ui)PjDy+Hd zZNYaz<4o^R(t~pG(zXIE1j9VH(;xIzr{Z2y|~ zGK7x~8~sy0+-dzSaCgF#D)3b{n#0*{)#Uu@%|jlGmb&l{VHkVC5kEY z3(Y?Jm0ITTMN%|gv1NT-IB%P$OaLen4jz<1V}>e%wj%jdfMyz}7iPJXpak7%BR_gE zA3JXYH%s$3dh@0`<>H9xw2IF(?Y}j3u#RY~ENn+;2)8fp%;@&eRp^8C)PaZ)T6&aQ z6?Z-#OHlyGv5sxp7zag0Qr=!!&E*E|i=#C%z*jkzRe{v1sAAU{dC7)qB%asmlUk5} z`iA1Yd2}+jVR9T9C3`-vR#0M=f}E^eVZ-7z@ahVhHVo@U(_*eEU273r4?}@AfV_l? z9M;W;qXhH0JEt}|k~+3S{EE7RXKcN%iFDiyD#dpP&nX%D**l|FEVUEhMSt_iqn4}6 zf~WGTX0ir1u}!@UMS!>#q)od_uAgS~%)PBvheP#={Wx^VVo$xS%SHx5R|KMFcd5RU8=(Nza3r+r+_e^kv7UkDxg&8?STTm3~>c&=g;{5z{ zP)%LEzOF7mKlo&piJH1{!3syui#hQ-?N9hXcs3<9HSvi=J(sz)V3PKlyZ}S$p@i$J zJSiK#u_@oj&Nl0V!%zB(xX0BQWiu;YLkw)$V`jT6k3W70z!HYim|w~$5{CD!uyaIE zGSl$EDpHGi)@PTC(?^@3$2Mhtdmj);Vt77}cz4sIPB_rv3W9HvmVKSmokY$K%Ca3GmbX0URp7 z(_;i4+tVQ{q)eyf`jQpWy87dN(KBdj&&|2R6-1aHssqsul#PEsg zYGcHy+v176?ipQu4Xx^>BZmZ=C8x`QLY1NnLkxnK+jP9F`t2_~Q^fY`R+&s*ted^@ z;Hz|ScD>1LlKtYXf-gOQFzdpmL-6P})=bU}uAYs<4!8gY>I?Yn|q4^oIu zA4+5zRGw(H>k2r0OGr3oX3p>8a3j7jA~B4xxsvjwh~nO$Vc7KgEDH;%)y z#vhoO=U6CF<1S|eO>bAo*w2?O`n^{*2hq+=JNI~uihoTHvrcW|vu;@-C46>vIleyN zvaZj_O=eRK@?G3zlDSJ(tZb@iK8U!AH&!)A7I%8MX8@E;KWVM)&K%;?(5*B_@5K(; zoZN>yiDM0%%J<{^*1l>6Ge?iI$EVZDMyANxf(;<)$&(2ud9>D9j?@Vi2SPeAgAv;& zO&SZsy>>6;*`FIde!eu-tpTB!S11o@!H$pT74e={Qb9C+7i|d*%BwE1TNoh({5Nf`S@7 z`LmFeYIL5|P0X=w6a)mhQU%sJ4(5j>F*2;MfTiBoM_5M*_~pH3pIJOh|HKiegm;-W z{-VATSmK-r9$o#*OfqBz9f&mzjY={I7je4Tnw!p^L8tBte-dN~>W+^Z)H*J<3I&um z(E9L4=XYnOz^5OK2#&##Ay{%D?`KyKY?(amY8x6}Htr90mfd%ncG6w%urwZT(p?{Y zz#}Fy2VNomMHk%6N|+eTc#- Q^!k?BfGD+1c4ejknPBu1@>c24-GfUagZ$JM~i2 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%0D3}ncoxhreY$o7$Ewf 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 IP}lo^0FQ2J7XSbN literal 76489 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 Date: Sun, 16 Aug 2026 23:10:55 -0400 Subject: [PATCH 36/38] test(web): a component test harness, so decision logic in a .tsx is reachable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit vite.config.ts's vitest include matched .test.ts only, never .test.tsx, and there was no jsdom or testing-library anywhere in the package. So nothing a .tsx file decided for itself was ever exercised by a unit test. health.test.ts tested envToken directly, but never rendered FleetScreen, so the component's own wiring of envToken shipped the prod-renders-violet bug undetected: the first version of the fleet screen matched environment names against the literal set {qa, stg, prod} instead of using the colour/risk the backend already resolved, and no test in the tree could see it. Adds jsdom, @testing-library/react, and @testing-library/jest-dom. A .test.tsx file opts into jsdom per-file with a @vitest-environment pragma rather than a global environmentMatchGlobs, which vitest 3.2 deprecated in favour of test.projects — per-file is the right grain anyway, since most tests never need a DOM. FleetScreen.test.tsx renders the screen with an environment named "production" and asserts it resolves to the prod token and prints its name verbatim. This is the exact class of bug the harness exists to catch, proven by rendering it, not by asserting the fix exists. Documents three tiers of test in vite.config.ts: pure logic (.test.ts), component decision logic (.test.tsx, jsdom), rendering fidelity (Playwright). Prefer extracting logic out of a component into a plain function next to it, as fleet.ts already does, so it stays in the fast tier. npm audit found one high-severity transient advisory (nanoid) pulled in by these new dev dependencies; npm audit fix resolved it to zero vulnerabilities before this commit. Closes #83 --- web/package-lock.json | 810 ++++++++++++++++++++++++++++++++++- web/package.json | 3 + web/src/FleetScreen.test.tsx | 66 +++ web/vite.config.ts | 27 +- 4 files changed, 902 insertions(+), 4 deletions(-) create mode 100644 web/src/FleetScreen.test.tsx diff --git a/web/package-lock.json b/web/package-lock.json index 7e2bf25..bdb81d9 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -14,15 +14,78 @@ }, "devDependencies": { "@playwright/test": "1.61.0", + "@testing-library/jest-dom": "^7.0.1", + "@testing-library/react": "^16.3.2", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "@vitejs/plugin-react": "^4.3.4", + "jsdom": "^30.0.1", "marked": "^18.0.7", "typescript": "^5.7.2", "vite": "^6.0.7", "vitest": "^3.2.7" } }, + "node_modules/@adobe/css-tools": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@asamuzakjp/css-color": { + "version": "6.0.7", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-6.0.7.tgz", + "integrity": "sha512-vC/bk1Lz7Tn/EfU9/apOTBk80/8dyGyWMowPoV1tJ52muDGsDqt2HPT2klrFUiY60MQmQv9q8yIht15JnBgDGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^3.3.0", + "@csstools/css-color-parser": "^4.1.10", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.5.2" + }, + "engines": { + "node": "^22.13.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-8.3.2.tgz", + "integrity": "sha512-93Z1N+BQNXysodoicpOIyNh2drHfz/CTf9nnT0FEx72GJcIiwgydD7tGAr78j41LsYn3hlRn+LdGPuBLn1Bl8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.5.2" + }, + "engines": { + "node": "^22.13.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -257,6 +320,16 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", @@ -305,6 +378,159 @@ "node": ">=6.9.0" } }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.1.tgz", + "integrity": "sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.3.0.tgz", + "integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.0.tgz", + "integrity": "sha512-5+5LEmFuY1AjXdYhmgjTJogtQnP1evJ1zrBZGUNZ0thkpwnnmKxcHdAMn/OtFjAb25zA+jKDVYVRl+5G7rjv1A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.1.1", + "@csstools/css-calc": "^3.3.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.8.tgz", + "integrity": "sha512-CpMLjAvwQg3BL5S0IeqsZNMH7EQrEWi0kLKOC13ZBF0ZwERiLWlibNPJr8G1kdU3Ms/r2KiNrF81pUh2HwAHdg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", @@ -747,6 +973,24 @@ "node": ">=18" } }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -1484,6 +1728,99 @@ "win32" ] }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-7.0.1.tgz", + "integrity": "sha512-oMDTC3oA+6CXSO2JZnvOI7CA6oVub6kij5ggk9ohwye5slmkwxYDXcPOVxgMw/RQlticjtO0C1RZkR97HgrWMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=22", + "npm": ">=6", + "yarn": ">=1" + }, + "peerDependencies": { + "@testing-library/dom": ">=10 <11", + "vitest": ">= 0.32" + }, + "peerDependenciesMeta": { + "vitest": { + "optional": true + } + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1725,6 +2062,31 @@ "addons/*" ] }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/aria-hidden": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", @@ -1737,6 +2099,16 @@ "node": ">=10" } }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -1760,6 +2132,16 @@ "node": ">=6.0.0" } }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/browserslist": { "version": "4.28.7", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz", @@ -1859,6 +2241,27 @@ "dev": true, "license": "MIT" }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -1866,6 +2269,35 @@ "devOptional": true, "license": "MIT" }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1884,6 +2316,13 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, "node_modules/deep-eql": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", @@ -1894,12 +2333,30 @@ "node": ">=6" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-node-es": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", "license": "MIT" }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/electron-to-chromium": { "version": "1.5.396", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.396.tgz", @@ -1907,6 +2364,19 @@ "dev": true, "license": "ISC" }, + "node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-module-lexer": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", @@ -2038,6 +2508,36 @@ "node": ">=6" } }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -2045,6 +2545,57 @@ "dev": true, "license": "MIT" }, + "node_modules/jsdom": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-30.0.1.tgz", + "integrity": "sha512-52v7mUVUfNQVYYqE1lcdaymWL0njO7lTLUog6ZvW2U5KsbiLk/GnZlVJ+qx0xfNJZ6Gn+KSpPNE52vurbxZwrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^6.0.5", + "@asamuzakjp/dom-selector": "^8.3.0", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.7", + "@exodus/bytes": "^1.15.1", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.5.2", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.2", + "undici": "^8.9.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^17.1.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + }, + "peerDependencies": { + "canvas": "^3.2.3" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -2088,6 +2639,17 @@ "yallist": "^3.0.2" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -2111,6 +2673,23 @@ "node": ">= 20" } }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2119,9 +2698,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "dev": true, "funding": [ { @@ -2147,6 +2726,19 @@ "node": ">=18" } }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -2260,6 +2852,32 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/react": { "version": "19.2.8", "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", @@ -2281,6 +2899,14 @@ "react": "^19.2.8" } }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -2360,6 +2986,30 @@ } } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/rollup": { "version": "4.62.2", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", @@ -2405,6 +3055,19 @@ "fsevents": "~2.3.2" } }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -2452,6 +3115,19 @@ "dev": true, "license": "MIT" }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-literal": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", @@ -2472,6 +3148,13 @@ "dev": true, "license": "MIT" }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -2533,6 +3216,52 @@ "node": ">=14.0.0" } }, + "node_modules/tldts": { + "version": "7.4.10", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.10.tgz", + "integrity": "sha512-GgouD1B+sWwvkaEq8vXC15DjQitxbvs12oIXELpconwm+Tg3zfcEv4jgzq3vtKverDXsg3VI8aRgNL2Nra0Iog==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.10" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.10", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.10.tgz", + "integrity": "sha512-KnQjp53ZekKgm/r3l+u8kJGGzYgrWdP8+Mql7a4vijh2WE0IrZWspQj/TpTxDho/YxO+AnOZnIjQcCD+q6iJsw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", + "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -2553,6 +3282,16 @@ "node": ">=14.17" } }, + "node_modules/undici": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.10.0.tgz", + "integrity": "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -2798,6 +3537,54 @@ } } }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-17.1.0.tgz", + "integrity": "sha512-3GeworPmc2ZfEEHP7lEbUfBX/L75wdEsi0rLNhXcXxnoN5jyq0SL5gCy06SGW2cyTIZdTvWIDQNQoza++vKeaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.15.1", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^22.14.0 || >=24.0.0" + } + }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", @@ -2815,6 +3602,23 @@ "node": ">=8" } }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/web/package.json b/web/package.json index a6c97a8..d92d295 100644 --- a/web/package.json +++ b/web/package.json @@ -20,9 +20,12 @@ }, "devDependencies": { "@playwright/test": "1.61.0", + "@testing-library/jest-dom": "^7.0.1", + "@testing-library/react": "^16.3.2", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "@vitejs/plugin-react": "^4.3.4", + "jsdom": "^30.0.1", "marked": "^18.0.7", "typescript": "^5.7.2", "vite": "^6.0.7", diff --git a/web/src/FleetScreen.test.tsx b/web/src/FleetScreen.test.tsx new file mode 100644 index 0000000..acbd2e0 --- /dev/null +++ b/web/src/FleetScreen.test.tsx @@ -0,0 +1,66 @@ +// @vitest-environment jsdom +// +// Issue #83: no .tsx file was reachable by vitest before this, so the +// component's own wiring — as opposed to the pure helpers beside it — shipped +// a real bug undetected. This test proves the harness would have caught it: +// an environment named "production" must resolve to the red/prod token and +// print its name verbatim, not fall back to "unc" the way the shipped bug did. + +import { cleanup, render, screen } from "@testing-library/react"; +import { afterEach, expect, test, vi } from "vitest"; +import "@testing-library/jest-dom/vitest"; +import { FleetScreen } from "./FleetScreen"; +import { api } from "./api"; +import type { FleetView } from "./api"; + +vi.mock("./api", async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual, api: { ...actual.api, fleet: vi.fn() } }; +}); + +afterEach(cleanup); + +const view: FleetView = { + app: "checkout", + namespace: "shop", + clusters: 1, + present: 1, + behind: 0, + newest: "v2.14.0", + rows: [ + { + context: "prod-us-east", + clusterId: "prod-us-east", + env: "production", + envColor: "red", + envRisk: "high", + namespace: "shop", + state: "present", + tag: "v2.14.0", + health: "healthy", + ready: "3/3", + }, + ], +}; + +test("an environment named production reads as prod, not unclassified", async () => { + vi.mocked(api.fleet).mockResolvedValue(view); + + render(); + + const row = await screen.findByText("production"); + const tr = row.closest("tr"); + if (!tr) throw new Error("no row rendered"); + + // The bug this harness exists to catch: a helper that matched the literal + // strings "qa"/"stg"/"prod" fell back to "unc" for every real environment + // name, since Classify keeps the raw name and only qa/stg/prod match + // verbatim. envToken({color, risk}) is the fix — it must resolve red/high + // to "prod" regardless of what the name spells. + expect(tr).toHaveAttribute("data-env", "prod"); + expect(tr).not.toHaveAttribute("data-env", "unc"); + + // The chip prints the resolved name verbatim, the way every other screen + // does, rather than a synthesized tier label. + expect(row.textContent).toBe("production"); +}); diff --git a/web/vite.config.ts b/web/vite.config.ts index c2f0b9a..87ab6c9 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -8,11 +8,36 @@ export default defineConfig({ // vitest owns the unit tests beside the source, Playwright owns e2e/. The // default vitest glob matches *.spec.ts anywhere, which is exactly the // Playwright naming convention. + // + // Three tiers of test, and where each belongs: + // - Pure logic (.test.ts) -> vitest, default node environment. Fast, no + // DOM. This is almost everything: parsing, formatting, derivations. + // - Decision logic that lives INSIDE a component (.test.tsx) -> vitest + // with jsdom, via @testing-library/react. Use this whenever a .tsx + // file makes a choice a test can't reach any other way — a chip + // mapping a raw value to a token, a label falling back for an unknown + // state, which badge wins when two conditions are both true. This is + // the tier that was missing (issue #83): the fleet screen's own + // env-token wiring shipped a bug that no .test.ts file could see, + // because the logic lived in the component, not beside it. + // - Rendering fidelity (screens.spec.ts et al) -> Playwright, in e2e/. + // Whether it actually looks right, pixel by pixel, in a real browser. + // Prefer moving decision logic OUT of a component into a plain function + // next to it (see fleet.ts) so it can be a fast .test.ts instead — reach + // for jsdom only when the logic cannot be extracted without changing + // what the component does. + // + // A .test.tsx file that needs jsdom opts in per-file with a + // `// @vitest-environment jsdom` pragma at the top (see FleetScreen.test.tsx), + // rather than a global environmentMatchGlobs here: vitest 3.2 deprecated + // that option in favour of test.projects, and per-file is exactly the + // right grain anyway — most .test.ts files never need a DOM and should not + // pay for one. test: { // .mjs beside the source too: a test that reads a file off disk needs node // APIs, which the app's tsconfig does not carry. Leaving the pattern out // does not fail, it silently collects nothing. - include: ["src/**/*.test.ts", "src/**/*.test.mjs", "site/**/*.test.mjs"], + include: ["src/**/*.test.ts", "src/**/*.test.tsx", "src/**/*.test.mjs", "site/**/*.test.mjs"], exclude: ["e2e/**", "node_modules/**", "dist/**"], }, plugins: [react()], From c86e1e3ff47eb4f2bcc5d17e28a3942cfae48d4d Mon Sep 17 00:00:00 2001 From: Elber Ribeiro Date: Sun, 16 Aug 2026 23:11:59 -0400 Subject: [PATCH 37/38] test(e2e): the missing Linux screenshot baselines PR #60's visual CI check has been failing since the fleet screen shipped: only a macOS baseline was ever committed for fleet.png, and CI runs on Linux, so it had nothing to diff against and reported a failure with no underlying regression. promotion.png's Linux baseline also needed regenerating after the split-cell fixture (#81) and the row reorder (#80) changed the pixels. Generated inside mcr.microsoft.com/playwright:v1.61.0-noble, the exact image ci.yml's visual job runs, matching Node 22 and the same npm ci install. Verified stable with a second run, no --update-snapshots, 27/27 clean. --- .../fleet-chromium-linux.png | Bin 0 -> 67718 bytes .../promotion-chromium-linux.png | Bin 55116 -> 61843 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 web/e2e/screens.spec.ts-snapshots/fleet-chromium-linux.png diff --git a/web/e2e/screens.spec.ts-snapshots/fleet-chromium-linux.png b/web/e2e/screens.spec.ts-snapshots/fleet-chromium-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..899c696419769f51c60ad25d2d81193a653a3563 GIT binary patch literal 67718 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 Sp!*6DG}@Z_8pUe1p??E1#`1Fj literal 55116 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 Date: Sat, 22 Aug 2026 16:28:26 -0400 Subject: [PATCH 38/38] docs: add a changelog GoReleaser generates release notes from commit history at tag time, but nothing tracked what a release actually contains between tags. This is the same information in plain language, kept as the branch goes rather than reconstructed from commit subjects later. Unreleased section covers this branch: the fleet screen and the promotion-matrix correctness fixes underneath it. --- CHANGELOG.md | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..22dd3dc --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,60 @@ +# Changelog + +All notable changes to kubeside are recorded here, in the style of +[Keep a Changelog](https://keepachangelog.com/en/1.1.0/). + +Release notes on GitHub are generated from commit history by GoReleaser +(`changelog: use: github` in `.goreleaser.yaml`) and remain the record of +what shipped in each tagged version. This file exists for the same +information in one place across releases, kept in plain language rather than +raw commit subjects. + +## [Unreleased] + +### Added +- Fleet screen (Screen 7): one app, one row per cluster, answering whether + every cluster is running the newest version. Reachable from the command + palette, the app detail screen, and a split promotion cell. +- Five distinct fleet states — present, absent, denied, unreachable, + pending — so a cluster that refused a read, one that never answered, and + one that genuinely does not run the app never render the same way. +- A denied fleet row names the RBAC rule that would fix it, not just that + access was refused. +- A component test harness (jsdom, `@testing-library/react`) for decision + logic that lives inside a `.tsx` file, where a plain unit test cannot + reach it. + +### Fixed +- The promotion matrix silently discarded every context after the first in + a multi-context environment and reported one cluster's version as the + whole environment's. Every bound context is now read, and a genuine + disagreement renders as a distinct split state instead of collapsing to + a guess. +- An environment nobody could read rendered "not deployed here" instead of + naming that it was unreadable, in both the promotion matrix and, by a + related fix, when only some of its clusters answered. +- An unread cluster that produced no disagreement counted as a severe + drift the same way a genuine version disagreement did; it now renders + as an unknown, not a defect, and no longer inflates drift or the split + count. +- A refused RBAC read (an app-carrying resource kind the reader could not + list) was indistinguishable from the app simply not existing there, in + both the promotion matrix and the fleet screen. +- A context that connected once and later failed to reconnect could be + read as fully readable off a stale session; `Service.Promotion` now + checks the connection error directly rather than trusting the client + handle alone. +- Two concurrent connection attempts for the same kubeconfig context could + both dial, racing to overwrite each other's result; connections for one + context now serialize. +- The fleet screen classified environments by matching literal `qa`/`stg`/ + `prod` strings, so any real cluster name (`prod-us-east`, `production`, + `staging-eks`, ...) rendered as an unclassified, unstyled row instead of + its actual risk color. + +### Documentation +- Corrected multi-cluster docs that described per-context aggregation and a + cluster-UID dedupe mechanism that were never implemented; documented what + actually shipped instead. +- Added Argo CD to the tool comparison in the problem statement. +- Recorded why kubeside has no terminal UI.