Skip to content

Commit 1940afd

Browse files
fix(spec,metadata-protocol): a dashboard's packaged translation catalog yields to a published org overlay (#20680) (#20728)
Part of #20680. This PR lands the two producer halves: the translator rule in `@objectstack/spec` and the packaged-base accessor in `@objectstack/metadata-protocol`. Two halves stay open, so the card must stay open after this merges. The first is the `packages/rest` read-path face, which hands the dashboard's packaged base to the translation; triage names it a per-layer child, and the seat files it, dashboards only, blocked by this PR (seat answer 5900364303 on #20680). The second is the console renderer, which is not measured here (see the acceptance notes). Clause-②: yes ## The identity, measured first (as the dispatch ordered) **Harness.** `@objectstack/verify`'s `bootStack(showcaseStack, { orgContext: true })`, which boots the real showcase composition in process: HTTP through the real Hono app, SQLite, an org-bound admin, and a member of the same org who signed up. The tree is `origin/main` at `5757463712`. I spied on `sys_metadata` `findOne` during direct `protocol.getMetaItem` calls to see which branch answered. The probe was a scratch file and is not committed. | step | identity used | branch that answered | widget title served | |---|---|---|---| | `PUT …/system_overview?mode=draft` | row `type: dashboard`, `name: system_overview`, `package_id: null`, `organization_id: ORG`, `state: draft` | write | stored "Total Users (edited-20680)" | | `POST …/publish` | the same row, `state: active` | write | stored the edit | | `protocol.getMetaItem({ organizationId: ORG })` | lookup `type: dashboard, name, state: active, organization_id: ORG` (no package filter) | step 1, the `sys_metadata` overlay lookup, which hit that row | **edited** | | `protocol.getMetaItems({ organizationId: ORG })` | union read | overlay row | **edited** | | `GET …?layers=true` (admin and member) | the same row | layered read | overlay and effective **edited**, `overlayScope: org` | | `GET /api/v1/meta/dashboard/system_overview` (admin and member) | the same protocol read, then the REST translation | overlay, then `translateDashboard` | "Total Users" (en), "用户总数" (zh-CN) | | `GET /api/v1/meta/dashboard` (admin and member; this list is what the console builds the board from) | the same | the same | "Total Users" / "用户总数" | | control `showcase_ops_dashboard`, same boot | identical row shape (`package_id: null`, same org) | the same | **edited**, both doors | The served `_packageId: com.objectstack.plugin-auth` is the code artifact's protection envelope. `mergeArtifactProtection` grafts it onto the overlay body, and plugin-auth's manifest registers `SystemOverviewDashboard`. It is not a second identity that the overlay missed. **Hypotheses.** H1 (the write and the plain read disagree on `package_id`): falsified. H2 (an earlier branch answers before the overlay lookup): falsified, because step 1 answered. H3 (the list read carries its own mismatch): falsified at the protocol; the REST list shows the same translation effect as the item read. H4 (the control works because it is keyed differently): falsified, because both are keyed identically. The difference is that `platform-objects` ships an `en` bundle whose `dashboards.system_overview.widgets.WIDGET_ID.title` repeats each shipped title, while the showcase bundle carries no widget titles for its dashboard. H5 (`packages/rest` is not the cause): partly held. REST is not where the wrong value is produced, but its read path is where the base has to be threaded in, so that half is the per-layer child. **The producer.** `translateDashboard` (`packages/spec/src/system/i18n-resolver.ts`) applied the catalog over whatever the document said. ADR-0029 D9.2a records one rule for both object layers, *an explicit override beats a packaged default*, and #8284 implemented it for objects' three scalars only. This PR applies the same comparison to dashboards. ## What changes - `translateDashboard` takes `TranslateDocumentOptions` (previously `ResolveOptions`; the new type adds only the optional `packagedBase`). When a base is supplied, the catalog replaces a string only while that string still equals its packaged counterpart. The rule covers the dashboard `label` and `description`, each widget's `title`, `description` and sub-caption (`options.description`), each global filter's label, and each static option label. Each string is matched by the key the bundle uses for it: widget id, filter key, or option value. A part that the base does not carry counts as authored, following the object rule "a base that declares no such scalar". With no base, the output is byte-identical to before. - The comparison is still one implementation. `scalarOverridesPackagedBase` now delegates to a private `valueOverridesPackagedBase` that takes any key. Its exported signature is unchanged, and its body is the same three lines. - `ObjectStackProtocolImplementation.getPackagedDashboardBase(name)` is the dashboard twin of `getPackagedObjectBase`. It is `lookupArtifactItem('dashboard', name)` and nothing wider, so an overlay hydrated under the plain registry key can never come back as the base. It answers `undefined` for a dashboard no code package ships, for an unknown or empty name, and for a registry that cannot answer. ## Readers of the changed functions, and which served answers move - `translateDashboard` is reached only through `translateMetadataDocument('dashboard', …)` in `packages/rest/src/meta-item-read-gate.ts`, by both `translateMetaDocument` (item) and `translateMetaList` (list). Both pass `packagedBase: packagedObjectBaseOf(…)`, which is `undefined` for every type but `object`. So no serving path supplies a dashboard base today, and the new branch never runs in production until the REST child lands. objectui imports none of these functions; it mentions them in comments only. - `scalarOverridesPackagedBase` is read by `translateObject` (spec) and by the `SchemaRegistry` fold (`packages/objectql/src/registry.ts`, the ADR-0029 D9.2a precedence). Its semantics are unchanged. The objectql suite, 6682 tests, is green against the rebuilt spec `dist`. - `getPackagedDashboardBase` has no caller yet. The REST child is its first reader. **Before/after, measured in the real boot.** I ran the same probe on two builds. The before build was `5757463712`, in a throwaway worktree. The after build was this branch, with `@objectstack/spec` and `@objectstack/metadata-protocol` rebuilt. Each run covered admin and member, en and zh-CN, and three phases: pristine; after publishing overlays on `system_overview` and `showcase_ops_dashboard`; and after resetting both. Each run read the dashboard list and each item (all 4 dashboards), the view list (35 views), the app list, and 4 object items (`showcase_project`, `showcase_account`, `showcase_task`, `sys_user`). I compared label, title, filter and option strings plus a normalized hash of every body. **Differences: 0.** No other dashboard, view, app or object changes its answer with this PR alone. **Positive controls in the same runs.** Unedited translated items keep their translations in zh-CN: `system_overview` "系统概览" / "用户总数"; the showcase global filters "任务状态" and "区域", with their option labels; views "进行中" and "紧急"; the app "功能展示"; objects "项目", "任务" and "用户". `showcase_account` keeps serving its extension label "Account (Success Overlay)", which is the #8284 object path, unchanged. **The whole chain with the REST face applied** (a throwaway worktree, **not** this branch): a nine-line change to `meta-item-read-gate.ts` passes `getPackagedDashboardBase(name)` for `dashboard`. With it, the same probe differs from this branch in exactly 16 fields, all of them `system_overview`'s edited widget, for admin and member, en and zh-CN, item and list (title, plus the body hashes). The edit is served in both locales, the unedited sibling widget stays translated ("活跃会话"), reset restores "Total Users" / "用户总数", and the showcase control is unchanged. That is the card's pin set, measured one layer up from this PR. ## Published surface (for the Clause-② line) - `@objectstack/spec`: no export is added, removed or renamed (`check:api-surface`: "public API surface + factory signatures unchanged"). One exported function's parameter type widens: `translateDashboard`'s `opts` goes from `ResolveOptions` to `TranslateDocumentOptions`. Every existing call still compiles. `translateMetadataDocument` already declared and passed `packagedBase`. - `@objectstack/metadata-protocol`: one new public method on the exported `ObjectStackProtocolImplementation`, `getPackagedDashboardBase`. - Measured against the question "does this widen an accept set or the public surface", both facts read as a widening. The seat answered (5900364303 on #20680): `Clause-②: yes`, and both changesets are `minor`. The claim's original `no` is corrected there. ## No documented rule is reversed - `content/docs/ui/translations.mdx` says: "The authored label is the default locale's text … Shipping a `zh-CN` bundle … still works and still wins." That describes a package author's own label against the package's own bundle. That case is kept: a value equal to the packaged declaration still gets the catalog, and so does a label written as an inline locale map, because only a string counts as an override. - ADR-0029 D9.2a and the #8284 ruling (2026-08-13) say that an explicit override beats a packaged default, in the source locale as well. This PR applies that ruling to dashboards; it does not reverse it. No ADR states that a catalog wins over a tenant's overlay. ## Tests HEAD `d89ebb8c1c` for everything below. - `packages/spec/src/system/i18n-resolver.test.ts`, a new block of 9 tests. It covers an untouched document (catalog applied to every string), an edited title in en and zh-CN, per-string judgement, every string kind, a tenant-added widget, no base as the pre-change control, the ruled no-op edge, no mutation of inputs, and the generic dispatcher. The whole spec `local` project passed: 575 files, 16969 tests. - `packages/objectql/src/protocol-packaged-dashboard-base.test.ts`, 8 tests against a real `SchemaRegistry`. It covers the packaged body, immunity to an overlay hydrated under the plain key, the three `undefined` answers, the item read serving the overlay (one identity), translation plus base serving the edit in both locales while the sibling stays translated, the no-base control reproducing the defect, and reset. The objectql `local` project passed: 336 files, 6682 tests. - `@objectstack/metadata-protocol` passed: 190 files, 2792 tests. `typecheck` is green for spec (with its test layer), metadata-protocol and objectql (with its test layer). - **Ablation 1** (spec, source-resolved, `scripts/ablation-replace.mjs`): the widget-title guard was replaced with `false && …`; the anchor went from 1 to 0 and the blob changed. The `-t 20680` run turned 3 red: the edited title, the tenant-added widget, and the dispatcher. The file was restored, with blob equal to HEAD and `git diff HEAD` empty. - **Ablation 2** (metadata-protocol, through `dist`): the accessor was made to return `registry.getItem` (the trap). After a rebuild, `ablation-dist-preflight` found the marker in 2 built files, and 2 tests went red: shadow immunity, and the no-code-package case. Restore leg: rebuild, `--absent` over 24 built files, tree clean, 8 of 8 green. - Gates: `node scripts/pm/dispatch-gates.mjs --commands` derived 96 families. I ran all 96 at `d89ebb8c1c` and all exited 0. `--ran` reconciliation: "96 derived, 96 run, 0 NOT-MEASURED, 0 UNRUN". `check:engine-double-contract` asked for the new double to be recorded, so `scripts/engine-double-contract.pinned.json` gains one `findOne` row, written by the gate's `--write`. `check:objectql-double-limit` asked for the double's `find` to honour `limit`. - Narrowed lint: `eslint --no-inline-config --format json` over the 4 TypeScript files in the diff reported 4 files, 0 errors and 0 warnings. That population comes from the config's own `files` glob (`**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}`); the other 3 changed paths are `.md` and `.json`. The config never enables type-aware linting (its own header says so: no `parserOptions.project`), so this diff cannot move the verdict for an untouched file. ## Acceptance notes - **Dormant until the REST child.** With this PR alone, no served answer changes (measured above). The `translations.mdx` sentence describing the rule for dashboards belongs with the child that makes the rule observable. - **The console's board is not measured.** Read from objectui source at `9fd6c2c`, not at the pinned `dd3f7e1`, which is absent locally: `DashboardRenderer`'s `tWidgetTitle` re-resolves `dashboards.NAME.widgets.WIDGET_ID.title` client-side, and a bundle entry wins over the served title. If so, the rendered board would keep the shipped string even after the server serves the edit. This is a read-only inference and no card is filed. The carrier is the REST child's browser verification. - **A same-family reading on views** (measured in this boot): an org overlay on `showcase_task.in_progress` ("In Progress (edited-20680)") is served in en, but a zh-CN reader, admin or member, item or list, gets "进行中". This is the same catalog-over-overlay shape one type over. The seat files it as its own finding (5900364303); it is not fixed here. - The branch is 7 commits behind `origin/main`, and none of them touches these paths. The merge queue rebuilds the PR on the current main. --- _Generated by [Claude Code](https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 99786f9 commit 1940afd

7 files changed

Lines changed: 582 additions & 9 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@objectstack/metadata-protocol': minor
3+
---
4+
5+
fix(metadata-protocol): `getPackagedDashboardBase(name)` answers the dashboard a code package ships, before any overlay (#20680)
6+
7+
`ObjectStackProtocolImplementation` gains `getPackagedDashboardBase(name)`, the dashboard twin of `getPackagedObjectBase`. It returns the packaged (code-layer) declaration of a dashboard, which is the `packagedBase` that `translateDashboard` compares a served dashboard against, so that a tenant's published overlay is not overwritten by the packaged translation catalog.
8+
9+
It reads the artifact registry's code-package entry only. An overlay that was hydrated under the plain registry key can therefore never be returned as the base it is compared against. It returns `undefined` for a dashboard that no code package ships, for an unknown or empty name, and for a registry that cannot answer. A caller treats `undefined` as "no base known", and the catalog applies as before.
10+
11+
The method is additive. No existing read changes answer: the item and list reads already serve a published org overlay by the same identity the write stored (`type`, name, `package_id`, organization). Nothing is removed or renamed.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
fix(spec): `translateDashboard` lets an explicit override beat the packaged catalog when it is handed the packaged base (#20680)
6+
7+
`translateDashboard` (`@objectstack/spec/system`) now follows the rule ADR-0029 D9.2a records for objects: an explicit override beats a packaged default. When the caller supplies `packagedBase` (the dashboard as its code package ships it, before any tenant overlay), a catalog string replaces a served string only while that string still equals its packaged counterpart. The comparison is made per string: the dashboard `label` and `description`, each widget's `title`, `description` and sub-caption (`options.description`), each global filter's `label`, and each static option `label`. Each string is matched by the key the bundle addresses it by (widget `id`, filter key, option value). A widget, filter or option that the packaged base does not carry counts as authored, so its strings keep their values.
8+
9+
Why: an org overlay on a packaged dashboard (ADR-0126 Regime O) published, and `?layers=true` reported it as effective, but the served widget title stayed the shipped one whenever the dashboard's bundle carried that title. The platform's `system_overview` is one such dashboard, because `platform-objects` ships an `en` bundle that repeats every widget title. The catalog translated the packaged declaration and was applied over the tenant's edit.
10+
11+
What changes for a caller:
12+
13+
- `translateDashboard`'s third parameter is typed `TranslateDocumentOptions` (was `ResolveOptions`). That type is `ResolveOptions` plus the optional `packagedBase`, and `translateMetadataDocument` already passed it through. Every existing call compiles unchanged.
14+
- Without `packagedBase` (`undefined` or `null`), the output is byte-identical to before: the catalog applies. No serving layer in this release passes a dashboard base yet, so no served dashboard changes answer with this package alone.
15+
- An edit back to exactly the shipped string is a no-op: the catalog still translates it.
16+
17+
Nothing is removed or renamed, and there is nothing to migrate.

‎packages/metadata-protocol/src/protocol.ts‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7160,6 +7160,48 @@ export class ObjectStackProtocolImplementation implements
71607160
}
71617161
}
71627162

7163+
/**
7164+
* [#20680] The PACKAGED (code-layer) declaration of a DASHBOARD — the
7165+
* body a code package ships, before any tenant `sys_metadata` overlay.
7166+
* The dashboard twin of {@link getPackagedObjectBase}, and the fact
7167+
* `translateDashboard` (`@objectstack/spec/system`) compares against as
7168+
* its `packagedBase`: the i18n catalog is the packaged translation of
7169+
* exactly this body, so it may only replace a string the served
7170+
* dashboard still carries unchanged from it (ADR-0029 D9.2a — an explicit
7171+
* override beats a packaged default; ADR-0126 Regime O — a packaged
7172+
* dashboard is overlay-editable, and a published overlay is what every
7173+
* read serves).
7174+
*
7175+
* Measured before this existed: an org overlay on `system_overview`
7176+
* published and was returned by {@link getMetaItem} and
7177+
* {@link getMetaItems} — the write and every protocol read resolve ONE
7178+
* identity (`type: 'dashboard'`, the name, `package_id: null`, the org) —
7179+
* yet the served widget title stayed the shipped one, because the
7180+
* translation of the served document had no base to compare with.
7181+
*
7182+
* {@link lookupArtifactItem}, and nothing wider: it answers only an item
7183+
* whose `_packageId` marks a genuine code package and is immune to an
7184+
* overlay row hydrated under the plain registry key, so the body it
7185+
* returns can never be the overlay it is meant to be compared against.
7186+
* A dashboard has no extension fold, so — unlike the object case — the
7187+
* artifact item IS the packaged declaration.
7188+
*
7189+
* Returns `undefined` — "no packaged baseline, infer nothing", which the
7190+
* translator reads as the catalog applying unchanged — for a
7191+
* runtime/tenant-authored dashboard (no code package ships it), an
7192+
* unknown name, and a registry double without the lookup.
7193+
*/
7194+
getPackagedDashboardBase(name: string): unknown {
7195+
if (typeof name !== 'string' || name === '') return undefined;
7196+
try {
7197+
return this.lookupArtifactItem('dashboard', name);
7198+
} catch {
7199+
// A read over the in-memory registry; a failure here must never
7200+
// turn a served dashboard into a 5xx.
7201+
return undefined;
7202+
}
7203+
}
7204+
71637205
/**
71647206
* [#8268, generalising #8038] The REGISTRY-SIDE half of
71657207
* {@link governServedItem}'s presence convergence: replay the registry's
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* #20680 — `ObjectStackProtocolImplementation.getPackagedDashboardBase`, against
5+
* a REAL `SchemaRegistry`, and the read it exists to feed.
6+
*
7+
* ## What was measured, and what it rules out
8+
*
9+
* An org overlay on the platform's own `system_overview` dashboard (ADR-0126
10+
* Regime O — a packaged dashboard is overlay-editable) published `200`, and
11+
* `?layers=true` reported it as effective, while the `/meta` item and list
12+
* reads kept serving the shipped widget title. Measured on a booted showcase
13+
* with an org-bound admin and a member of the same org: the write stored ONE
14+
* row (`type: 'dashboard'`, the name, `package_id: null`, the org,
15+
* `state: 'active'` after publish), and `getMetaItem` / `getMetaItems` both
16+
* answered it — the overlay branch of the item read hit that row. So the write
17+
* and every protocol read agree on the item's identity; the plain read's
18+
* `_packageId: com.objectstack.plugin-auth` is only the code artifact's
19+
* protection envelope, grafted onto the overlay body by
20+
* `mergeArtifactProtection`.
21+
*
22+
* What replaced the title was the i18n catalog: `platform-objects` ships an
23+
* `en` bundle whose `dashboards.system_overview.widgets.<id>.title` repeats
24+
* the shipped string, and `translateDashboard` applied it over whatever the
25+
* document said. The showcase dashboard (the control) ships no widget titles
26+
* in its bundle, so the same overlay reached its reader untouched.
27+
*
28+
* The rule that fixes it is ADR-0029 D9.2a's — an explicit override beats a
29+
* packaged default, decided by comparing against the PACKAGED declaration —
30+
* and that comparison is only as good as the body this accessor returns. Hence
31+
* a real registry: the body must be the one the code package shipped, never an
32+
* overlay the protocol hydrated under the plain registry key.
33+
*/
34+
35+
import { describe, it, expect } from 'vitest';
36+
import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol';
37+
import { translateDashboard, type TranslationBundle } from '@objectstack/spec/system';
38+
import { SchemaRegistry } from './registry.js';
39+
import { assertEngineFindOnePredicate } from './engine-findone-predicate.js';
40+
41+
const PKG = 'com.objectstack.plugin-auth';
42+
const DASH = 'system_overview';
43+
const ORG = 'org_acme';
44+
45+
const SHIPPED_TITLE = 'Total Users';
46+
const EDITED_TITLE = 'Total Users (edited)';
47+
48+
/** The dashboard as the code package ships it. */
49+
const packagedBody = () => ({
50+
name: DASH,
51+
label: 'System Overview',
52+
columns: 12,
53+
widgets: [
54+
{ id: 'widget_total_users', type: 'metric', title: SHIPPED_TITLE, object: 'sys_user', layout: { x: 0, y: 0, w: 3, h: 2 } },
55+
{ id: 'widget_organizations', type: 'metric', title: 'Organizations', object: 'sys_organization', layout: { x: 3, y: 0, w: 3, h: 2 } },
56+
],
57+
});
58+
59+
/** The same dashboard as the org's published overlay stores it — one title edited. */
60+
const overlayBody = () => {
61+
const body = packagedBody();
62+
body.widgets[0] = { ...body.widgets[0], title: EDITED_TITLE };
63+
return body;
64+
};
65+
66+
/**
67+
* The catalog `platform-objects` ships: `en` repeats the shipped strings,
68+
* `zh-CN` translates them. Both locales, because the defect showed in the
69+
* SOURCE locale — an `en` reader got the shipped English back over the edit.
70+
*/
71+
const BUNDLE: TranslationBundle = {
72+
en: {
73+
dashboards: {
74+
[DASH]: {
75+
widgets: {
76+
widget_total_users: { title: SHIPPED_TITLE },
77+
widget_organizations: { title: 'Organizations' },
78+
},
79+
},
80+
},
81+
} as any,
82+
'zh-CN': {
83+
dashboards: {
84+
[DASH]: {
85+
widgets: {
86+
widget_total_users: { title: '用户总数' },
87+
widget_organizations: { title: '组织' },
88+
},
89+
},
90+
},
91+
} as any,
92+
};
93+
94+
const matchesWhere = (row: Record<string, unknown>, where: Record<string, unknown>) =>
95+
Object.entries(where ?? {}).every(([k, v]) => {
96+
if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`);
97+
if (v === null) return row[k] === null || row[k] === undefined;
98+
return row[k] === v;
99+
});
100+
101+
/**
102+
* A real registry and the real protocol over a READ-ONLY in-memory
103+
* `sys_metadata`: the rows are seeded exactly as the measured write left them,
104+
* so no write verb is doubled here at all.
105+
*/
106+
function makeSession() {
107+
const registry = new SchemaRegistry({ multiTenant: false });
108+
registry.logLevel = 'silent';
109+
const rows: Record<string, unknown>[] = [];
110+
const engine: any = {
111+
registry,
112+
async findOne(table: string, o: { where: Record<string, unknown> }) {
113+
// The real engine's #4419 predicate: a `findOne` naming no record is refused.
114+
assertEngineFindOnePredicate(table, o);
115+
if (table !== 'sys_metadata') return null;
116+
return rows.find((r) => matchesWhere(r, o.where)) ?? null;
117+
},
118+
async find(table: string, o: { where: Record<string, unknown>; limit?: number }) {
119+
if (table !== 'sys_metadata') return [];
120+
const matched = rows.filter((r) => matchesWhere(r, o.where));
121+
// The caller's bound, applied after the filter, by presence.
122+
return typeof o?.limit === 'number' ? matched.slice(0, o.limit) : matched;
123+
},
124+
};
125+
const protocol: any = new ObjectStackProtocolImplementation(engine, undefined, 'env_test');
126+
return { protocol, registry, rows };
127+
}
128+
129+
/** The code package's registration (plugin-auth's manifest, in the measured boot). */
130+
function registerPackaged(registry: SchemaRegistry) {
131+
registry.registerItem('dashboard', packagedBody(), 'name', PKG);
132+
}
133+
134+
/** The published org overlay row, in the identity the measured write stored. */
135+
function seedOrgOverlay(rows: Record<string, unknown>[]) {
136+
rows.push({
137+
id: 'r_1',
138+
type: 'dashboard',
139+
name: DASH,
140+
package_id: null,
141+
organization_id: ORG,
142+
state: 'active',
143+
metadata: JSON.stringify(overlayBody()),
144+
});
145+
}
146+
147+
const titleOf = (doc: any, id = 'widget_total_users') =>
148+
(doc?.widgets as any[] | undefined)?.find((w) => w?.id === id)?.title;
149+
150+
describe('#20680 getPackagedDashboardBase — the packaged declaration, never an overlay', () => {
151+
it('answers the body the code package shipped', () => {
152+
const s = makeSession();
153+
registerPackaged(s.registry);
154+
const base = s.protocol.getPackagedDashboardBase(DASH);
155+
expect(titleOf(base)).toBe(SHIPPED_TITLE);
156+
expect(base?._packageId).toBe(PKG);
157+
});
158+
159+
it('is immune to an overlay hydrated under the plain registry key', () => {
160+
// An env-wide overlay is hydrated into the registry's PLAIN key on
161+
// publish. The accessor must still answer the packaged body — a base
162+
// that returned the overlay would compare the edit equal to itself and
163+
// hand the catalog straight back.
164+
const s = makeSession();
165+
registerPackaged(s.registry);
166+
s.registry.registerItem('dashboard', overlayBody(), 'name');
167+
168+
// The trap is live: the plain registry read answers the overlay…
169+
expect(titleOf(s.registry.getItem('dashboard', DASH))).toBe(EDITED_TITLE);
170+
// …and the packaged base does not move.
171+
expect(titleOf(s.protocol.getPackagedDashboardBase(DASH))).toBe(SHIPPED_TITLE);
172+
});
173+
174+
it('is undefined for a dashboard no code package ships, an unknown name, and an empty name', () => {
175+
// "No packaged baseline" is a real answer: the translator reads it as
176+
// "infer nothing" and keeps the catalog, as before this change.
177+
const s = makeSession();
178+
s.registry.registerItem('dashboard', { ...packagedBody(), name: 'tenant_board' }, 'name');
179+
expect(s.protocol.getPackagedDashboardBase('tenant_board')).toBeUndefined();
180+
expect(s.protocol.getPackagedDashboardBase('no_such_board')).toBeUndefined();
181+
expect(s.protocol.getPackagedDashboardBase('')).toBeUndefined();
182+
});
183+
184+
it('is undefined when the host registry cannot answer', () => {
185+
const partial: any = new ObjectStackProtocolImplementation({ registry: {} } as any, undefined, 'env_test');
186+
expect(partial.getPackagedDashboardBase(DASH)).toBeUndefined();
187+
const none: any = new ObjectStackProtocolImplementation({} as any, undefined, 'env_test');
188+
expect(none.getPackagedDashboardBase(DASH)).toBeUndefined();
189+
});
190+
});
191+
192+
describe('#20680 the published org overlay, through the protocol read and the translation', () => {
193+
it('the item read serves the overlay — one identity for the write and the read', async () => {
194+
const s = makeSession();
195+
registerPackaged(s.registry);
196+
seedOrgOverlay(s.rows);
197+
const served = await s.protocol.getMetaItem({ type: 'dashboard', name: DASH, organizationId: ORG });
198+
expect(titleOf(served.item)).toBe(EDITED_TITLE);
199+
// The code artifact's envelope rides on the overlay body; it names the
200+
// registering package, not a second identity the overlay missed.
201+
expect(served.item._packageId).toBe(PKG);
202+
});
203+
204+
it('the translation serves the edit once it is handed the packaged base — en and zh-CN', async () => {
205+
const s = makeSession();
206+
registerPackaged(s.registry);
207+
seedOrgOverlay(s.rows);
208+
const served = (await s.protocol.getMetaItem({ type: 'dashboard', name: DASH, organizationId: ORG })).item;
209+
const packagedBase = s.protocol.getPackagedDashboardBase(DASH);
210+
211+
for (const locale of ['en', 'zh-CN']) {
212+
const out = translateDashboard(served, BUNDLE, { locale, packagedBase });
213+
expect(titleOf(out), `${locale}: the edited widget keeps the edit`).toBe(EDITED_TITLE);
214+
}
215+
// The widget nobody edited is still translated.
216+
const zh = translateDashboard(served, BUNDLE, { locale: 'zh-CN', packagedBase });
217+
expect(titleOf(zh, 'widget_organizations')).toBe('组织');
218+
});
219+
220+
it('CONTROL — without the base, the catalog replaces the edit (the measured defect)', async () => {
221+
const s = makeSession();
222+
registerPackaged(s.registry);
223+
seedOrgOverlay(s.rows);
224+
const served = (await s.protocol.getMetaItem({ type: 'dashboard', name: DASH, organizationId: ORG })).item;
225+
expect(titleOf(translateDashboard(served, BUNDLE, { locale: 'en' }))).toBe(SHIPPED_TITLE);
226+
expect(titleOf(translateDashboard(served, BUNDLE, { locale: 'zh-CN' }))).toBe('用户总数');
227+
});
228+
229+
it('after reset (no overlay row) the shipped body is served and still translated', async () => {
230+
const s = makeSession();
231+
registerPackaged(s.registry);
232+
const served = (await s.protocol.getMetaItem({ type: 'dashboard', name: DASH, organizationId: ORG })).item;
233+
const packagedBase = s.protocol.getPackagedDashboardBase(DASH);
234+
expect(titleOf(served)).toBe(SHIPPED_TITLE);
235+
expect(titleOf(translateDashboard(served, BUNDLE, { locale: 'zh-CN', packagedBase }))).toBe('用户总数');
236+
});
237+
});

0 commit comments

Comments
 (0)