Skip to content

fix(ui): stop the Reflector page giving two opposite instructions - #1293

Merged
krisarmstrong merged 1 commit into
mainfrom
fix/ui-stem-2-reflector-banners
Sep 16, 2026
Merged

krisarmstrong merged 1 commit into
mainfrom
fix/ui-stem-2-reflector-banners

Conversation

@krisarmstrong

@krisarmstrong krisarmstrong commented Sep 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

Plan row UI-STEM-2 (high). With the role set to Test Master on a platform
without the reflector dataplane (macOS / Windows builds), the Reflector page
stacked two banners telling the operator opposite things: RoleGuard's
role.guard.needReflector ("Switch to Reflector…") directly above
role.platform.bannerTitle/bannerBody ("…switch this stem to Test Master
mode").

  • RoleGuard takes a superseded prop and renders children only when another
    banner on the page already speaks for it. ReflectorPage passes
    superseded={!reflectorSupported}: where there is no dataplane, "switch to
    Reflector" is advice the operator cannot act on, so the platform banner —
    the more specific of the two — is the one that stays.

Extension beyond the row's one line, same defect class. Once the role
banner is gone, the surviving platform banner still told a Test Master to
"switch this stem to Test Master mode" and offered a Switch to Test Master
button that switches to the role they already hold — a banner instructing an
action that is already taken. The trailing clause moves out of
role.platform.bannerBody into a new role.platform.bannerSwitchHint
(en + es), and hint and button render only when role !== 'test_master'.

ReflectorPage.test.tsx's renderPage hardcoded role: 'reflector', which is
why no existing test could see either state; it takes a role option now.
scripts/ui-coverage-baseline.txt floors raised as check-ui-coverage.sh
asks (it prints the ratchet instruction while exiting 0).

Linked Issue

Fixes #1258

Type of Change

Bug fix (UI, no API or schema change). One new locale key in en and es; one
existing locale string shortened (its removed clause is the new key).

Risk

Low and contained to the Reflector page. RoleGuard's default behaviour is
unchanged — superseded is optional and every other caller omits it, so the
role banner still appears on every module page. The regression that matters
(losing the role banner where the reflector can run) is asserted by a new
test. No backend, no Go changes.

Testing Evidence

RED first — the two new banner assertions fail on the unfixed tree, and the
regression guard already passes:

$ npx vitest run src/pages/ReflectorPage.test.tsx
 FAIL  src/pages/ReflectorPage.test.tsx > ReflectorPage — role and platform banners together > leaves only the platform banner when the dataplane is missing on a Test Master
 ❯ src/pages/ReflectorPage.test.tsx:253:55
    252|     expect(screen.getByTestId('reflector-platform-banner')).toBeInTheD…
    253|     expect(screen.queryByTestId('role-guard-banner')).toBeNull();
       |                                                       ^

 FAIL  src/pages/ReflectorPage.test.tsx > ReflectorPage — role and platform banners together > drops the switch action the operator has already taken
AssertionError: expected <button type="button" …(1)></button> to be null
+ Received:
<button … >
  Switch to Test Master
</button>

 Test Files  1 failed (1)
      Tests  2 failed | 19 passed (21)

GREEN after the fix:

$ npx vitest run src/pages/ReflectorPage.test.tsx src/components/RoleGuard.test.tsx
 RUN  v4.1.11

 Test Files  2 passed (2)
      Tests  32 passed (32)
   Duration  1.94s

Full gates on this branch:

$ golangci-lint version
golangci-lint has version 2.13.2 built with go1.27.0 from 27774aa on 2026-08-27T22:52:01Z

$ make lint-go            -> rc=0   ("0 issues.")
$ make lint-frontend      -> rc=0   (Biome: checked 255 files, no fixes applied)
$ make fmt-check          -> rc=0   (Go + Biome)
$ npm run typecheck       -> rc=0   (tsc --build --noEmit)
$ govulncheck ./...       -> rc=0   (0 vulnerabilities in called code)

$ make test               -> rc=0
   Backend coverage 78.6%; Frontend 62 test files pass

$ LOCALES_DIR=ui/locales ./scripts/i18n/validate.sh   -> rc=0
=== Interpolation parity ({{var}} tokens match) ===
✓ interpolation vars match across locales
=== t() call ↔ EN locale key cross-reference ===
✓ every t() call resolves to an EN locale key
=== Hardcoded English JSX text ===
✓ no hardcoded English JSX text
=== Summary ===
OK with 1 warning(s)     (pre-existing: this repo defines no npm i18n:* script)

$ node --test scripts/check-help-i18n.test.ts          -> rc=0
$ node scripts/check-help-i18n.ts                      -> rc=0
$ ./scripts/check-json-casing.sh                       -> rc=0
$ ./scripts/check-types-drift.sh                       -> rc=0
$ ./scripts/check-output-escaping.sh                   -> rc=0
$ ./scripts/check-route-policy.sh                      -> rc=0
$ ./scripts/check-request-fields.sh                    -> rc=0
$ ./scripts/check-schema-drift.sh                      -> rc=0
$ ./scripts/check-service-restart-policy.sh            -> rc=0
$ ./scripts/check-filename-policy.sh                   -> rc=0
$ ./scripts/check-package-reachability.sh              -> rc=0
$ ./scripts/check-release-workflow-contract.sh         -> rc=0
$ ./scripts/check-stale-tests.sh                       -> rc=0
$ ./scripts/check-file-size.sh                         -> rc=0
$ python3 scripts/check-banned-vocabulary.py           -> rc=0
$ python3 scripts/check-tsconfig-flags.py              -> rc=0
$ (cd ui && ../scripts/check-token-discipline.sh)      -> rc=0

$ ./scripts/check-ui-coverage.sh                       -> rc=0
  lines        81.85%  floor  81.8%  target  88.0%  ok
  statements   70.22%  floor  70.2%  target  88.0%  ok
OK: UI coverage is at or above every floor.

Row acceptance ("ReflectorPage test: platform-unsupported + Test Master
renders exactly one banner") is the first new test above.

Security and Release Checklist

  • No new dependency, no network call, no credential or secret touched.
  • No change to auth, CSRF, roles as a security boundary: RoleGuard is an
    advisory banner, not an authorization gate. The Start control stays
    disabled on an unsupported platform (existing test unchanged).
  • No banned vocabulary; check-banned-vocabulary.py green.
  • govulncheck ./... green; no Go code changed.
  • i18n parity holds in en and es.
  • No API, schema or config surface changed — nothing to migrate, nothing
    to document in release notes beyond the changelog entry.

On a platform without the reflector dataplane, a Test Master saw the role
banner ("Switch to Reflector…") stacked directly above the platform banner
("…switch this stem to Test Master mode"). The platform banner is the more
specific of the two — where there is no dataplane, switching to Reflector is
advice the operator cannot act on — so RoleGuard now yields to it.

The remaining banner then still offered a "Switch to Test Master" button to an
operator who already held that role, and its body said the same. The switch
hint moves into its own key and, with the button, renders only when the switch
is still available.

The test harness pinned role to 'reflector', which is why no existing test saw
either state; it takes a role now.

Fixes #1258
@krisarmstrong
krisarmstrong added this pull request to the merge queue Sep 16, 2026
Merged via the queue into main with commit 4fbaddc Sep 16, 2026
41 checks passed
@krisarmstrong
krisarmstrong deleted the fix/ui-stem-2-reflector-banners branch September 16, 2026 11:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(ui): Reflector page shows two banners with opposite instructions

1 participant