|
1 | 1 | // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. |
2 | 2 | // |
3 | | -// Tombstone for four dead `apps.setup.navigation` translation keys (#6660). |
| 3 | +// Tombstone for four dead `apps.setup.navigation` translation keys (#6660), and |
| 4 | +// its converse: the labels of conditionally-contributed entries, which must |
| 5 | +// STAY (#6659). |
4 | 6 | // |
5 | 7 | // --------------------------------------------------------------------------- |
6 | 8 | // Why a hard-coded id list instead of the general reverse direction |
|
14 | 16 | // something", and why `pnpm check:app-nav-i18n` (which does boot) still refuses |
15 | 17 | // the reverse direction: from one composition a dead key and a |
16 | 18 | // conditionally-contributed key are indistinguishable (`nav_sso_providers` is |
17 | | -// contributed only when an external IdP is wired). Making that gate |
18 | | -// union-aware is tracked as #6659. |
| 19 | +// contributed only when an external IdP is wired). Teaching that gate to |
| 20 | +// enumerate conditional contributions — a union-aware gate — was considered and |
| 21 | +// deliberately NOT built (#6659's triage): it is a separate maintainer-facing |
| 22 | +// call, not a prerequisite for labelling the ids it cannot see. |
19 | 23 | // |
20 | 24 | // This file makes no general claim. It pins exactly four ids that were checked |
21 | 25 | // ONE BY ONE against a repo-wide grep — `id: '<key>'` returned zero hits for |
@@ -92,3 +96,53 @@ describe('removed Setup nav ids stay removed (#6660)', () => { |
92 | 96 | expect(DEAD_SETUP_NAV_IDS.filter((id) => declared.has(id))).toEqual([]); |
93 | 97 | }); |
94 | 98 | }); |
| 99 | + |
| 100 | +// --------------------------------------------------------------------------- |
| 101 | +// The converse case (#6659): a label that must STAY although no boot sees it |
| 102 | +// --------------------------------------------------------------------------- |
| 103 | +// `pnpm check:app-nav-i18n` boots the real composition and asserts every MERGED |
| 104 | +// Setup nav id carries a label in every locale. That is the right shape for the |
| 105 | +// eleven contributors whose entries always merge — and it is structurally blind |
| 106 | +// to the ones that do not. `@objectstack/plugin-auth` spreads its |
| 107 | +// `navigationContributions` in only when `authManager.isSsoWired()` is true |
| 108 | +// (`OS_SSO_ENABLED` self-host, or the cloud per-env `planAllowsSso`), so in the |
| 109 | +// composition that gate boots, `nav_sso_providers` is never contributed, never |
| 110 | +// merged, and therefore never judged. It had no label in ANY of the four |
| 111 | +// locales while that gate reported OK, and a deployment with an external IdP |
| 112 | +// wired rendered `SSO Providers` in English inside an otherwise translated menu. |
| 113 | +// |
| 114 | +// So this case is a hand-kept list, exactly like `DEAD_SETUP_NAV_IDS` above and |
| 115 | +// for the same reason: one composition cannot decide the question, so a human |
| 116 | +// decided it per id. It is deliberately NOT a general union-aware gate — that |
| 117 | +// was ruled a separate maintainer-facing call (#6659's triage) and is not built. |
| 118 | +// |
| 119 | +// Bound worth stating: this file asserts only the LABEL half. The declaring |
| 120 | +// contribution lives in `@objectstack/plugin-auth`, which depends on this |
| 121 | +// package and so cannot be imported from here — the same import direction that |
| 122 | +// puts `check:app-nav-i18n` in `packages/cli`. A grep is what confirms the |
| 123 | +// declaring side; on `ea1d9165d` it sits at `auth-plugin.ts:552`. |
| 124 | +// |
| 125 | +// What to do when this test goes red: it goes red when a label is dropped, or |
| 126 | +// when a locale is added to the bundle without translating this id. Both are |
| 127 | +// bugs. If the CONTRIBUTION is ever retired, this list loses its entry in the |
| 128 | +// same commit that removes the nav item, and the id moves up to |
| 129 | +// `DEAD_SETUP_NAV_IDS` — the two lists are the two halves of one ledger. |
| 130 | +const CONDITIONAL_SETUP_NAV_IDS = ['nav_sso_providers'] as const; |
| 131 | + |
| 132 | +describe('conditionally-contributed Setup nav ids stay labelled (#6659)', () => { |
| 133 | + for (const [locale, data] of Object.entries(LOCALES)) { |
| 134 | + it(`${locale} carries a label for every conditionally-contributed Setup nav id`, () => { |
| 135 | + const nav = (data.apps?.setup?.navigation ?? {}) as Record<string, { label?: string }>; |
| 136 | + |
| 137 | + // Control: the subtree really resolved, so a missing/renamed |
| 138 | + // `apps.setup.navigation` cannot make the assertion below pass by vacuity |
| 139 | + // (it would instead report every id as unlabelled — which is the point). |
| 140 | + expect(nav.nav_api_keys?.label, 'nav_api_keys anchors this subtree').toBeTruthy(); |
| 141 | + |
| 142 | + expect( |
| 143 | + CONDITIONAL_SETUP_NAV_IDS.filter((id) => !nav[id]?.label), |
| 144 | + 'Setup nav ids with no label — no boot-time gate can see these; see this file header', |
| 145 | + ).toEqual([]); |
| 146 | + }); |
| 147 | + } |
| 148 | +}); |
0 commit comments