Skip to content

Commit c2603b9

Browse files
authored
test(cli): pin the plugin-carried Setup pages against their translations (#3589) (#3679)
The three Setup pages ship as metadata inside capability plugins (`@objectstack/cloud-connection`, `@objectstack/mcp`) while their translations live in `@objectstack/platform-objects` — two packages, edited independently. `translatePage` applies the bundle for EVERY locale including `en`, so an `en` entry that has drifted from the metadata literal silently overrides the newer authored copy instead of falling back to it, and nothing in the build notices. Asserts: all three pages reachable through the plugins' UI bundles; an `en` entry for each; the `en` bundle byte-identical to the metadata literals; the `pages` key set identical across every shipped locale; and an end-to-end `translatePage` pass putting zh-CN copy into `page:header` while `icon` survives and the plugin's module-level page object is not mutated. The pages run through the CLI's own `collectExpectedEntries` — the extractor behind `os i18n extract` / `coverage` — so this also dogfoods that tooling against the platform's own shipped metadata. Test-only; ships an empty changeset.
1 parent 17749fc commit c2603b9

2 files changed

Lines changed: 141 additions & 0 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
---
3+
4+
test(cli): pin the plugin-carried Setup pages against their translations (#3589)
5+
6+
Test-only; releases nothing. Adds a drift guard asserting the `en` entries in
7+
`@objectstack/platform-objects` stay byte-identical to the `page:header`
8+
literals authored in `@objectstack/cloud-connection` and `@objectstack/mcp`,
9+
that the `pages` key set matches across every shipped locale, and that
10+
`translatePage` localizes the three Setup pages end-to-end.
11+
12+
`translatePage` applies the bundle for every locale including `en`, so a
13+
drifted `en` entry silently overrides newer authored copy rather than falling
14+
back to it — previously nothing in the build caught that.
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* Drift guard for the plugin-carried Setup pages' i18n (#3589).
5+
*
6+
* Three Setup pages ship as metadata inside capability plugins
7+
* (`@objectstack/cloud-connection`, `@objectstack/mcp`) while their
8+
* translations live in `@objectstack/platform-objects` — the two are edited
9+
* in different packages by different people. `translatePage` applies the
10+
* bundle for EVERY locale including `en`, so an `en` entry that has drifted
11+
* from the metadata literal silently *overrides* the newer authored copy
12+
* rather than falling back to it. Nothing else in the build notices.
13+
*
14+
* The pages are fed through the CLI's own `collectExpectedEntries` — the same
15+
* extractor behind `os i18n extract` / `coverage` — so this doubles as a
16+
* dogfood of that tooling against the platform's own shipped metadata.
17+
*
18+
* Placement: `packages/cli` is the only package that already depends on all
19+
* three, and it owns the i18n coverage tooling. `platform-objects` must not
20+
* depend on plugins (wrong direction), and the qa contract packages carry
21+
* none of them.
22+
*/
23+
24+
import { describe, it, expect } from 'vitest';
25+
import {
26+
MARKETPLACE_INSTALLED_UI_BUNDLE,
27+
CLOUD_CONNECTION_UI_BUNDLE,
28+
} from '@objectstack/cloud-connection';
29+
import { CONNECT_AGENT_UI_BUNDLE } from '@objectstack/mcp';
30+
import { SetupAppTranslations } from '@objectstack/platform-objects';
31+
import { translatePage } from '@objectstack/spec/system';
32+
import { collectExpectedEntries } from '../src/utils/i18n-extract';
33+
34+
/** The pages exactly as the plugins register them with the kernel. */
35+
const PAGES = [
36+
MARKETPLACE_INSTALLED_UI_BUNDLE.pages?.[0],
37+
CLOUD_CONNECTION_UI_BUNDLE.pages?.[0],
38+
CONNECT_AGENT_UI_BUNDLE.pages?.[0],
39+
].filter(Boolean) as Array<Record<string, any>>;
40+
41+
const EN = 'en';
42+
const pagesOf = (locale: string): Record<string, any> =>
43+
((SetupAppTranslations as Record<string, any>)[locale]?.pages ?? {});
44+
45+
const read = (node: unknown, path: string): unknown =>
46+
path.split('.').reduce<unknown>(
47+
(acc, seg) => (acc && typeof acc === 'object' ? (acc as Record<string, unknown>)[seg] : undefined),
48+
node,
49+
);
50+
51+
describe('plugin-carried Setup pages — i18n drift guard (#3589)', () => {
52+
it('finds all three pages through the plugins’ UI bundles', () => {
53+
expect(PAGES.map((p) => p.name).sort()).toEqual([
54+
'cloud_connection_settings',
55+
'connect_agent',
56+
'marketplace_installed',
57+
]);
58+
});
59+
60+
it('has an `en` translation entry for every page', () => {
61+
const en = pagesOf(EN);
62+
for (const page of PAGES) {
63+
expect({ page: page.name, hasEntry: Boolean(en[page.name]) })
64+
.toEqual({ page: page.name, hasEntry: true });
65+
}
66+
});
67+
68+
it('keeps the `en` bundle byte-identical to the metadata literals', () => {
69+
// The real drift: someone edits `properties.subtitle` in the plugin and
70+
// leaves the bundle alone. `en` requests then render the STALE bundle
71+
// text, so the edit appears to do nothing.
72+
const en = pagesOf(EN);
73+
const expected = collectExpectedEntries({ pages: PAGES } as any)
74+
.filter((e) => e.path[0] === 'pages');
75+
76+
expect(expected.length).toBeGreaterThan(0);
77+
78+
for (const entry of expected) {
79+
const [, pageName, ...rest] = entry.path;
80+
const key = rest.join('.');
81+
expect({ page: pageName, key, value: read(en[pageName], key) })
82+
.toEqual({ page: pageName, key, value: entry.sourceValue });
83+
}
84+
});
85+
86+
it('translates every page in every shipped locale (no silent English fallback)', () => {
87+
const locales = Object.keys(SetupAppTranslations as Record<string, unknown>);
88+
expect(locales).toContain(EN);
89+
expect(locales.length).toBeGreaterThan(1);
90+
91+
const enKeys = Object.keys(pagesOf(EN)).sort();
92+
for (const locale of locales) {
93+
expect({ locale, pages: Object.keys(pagesOf(locale)).sort() })
94+
.toEqual({ locale, pages: enKeys });
95+
}
96+
});
97+
98+
it('rewrites the page:header copy end-to-end for a non-English locale', () => {
99+
// The whole point of the chain: metadata in, localized header out.
100+
const headerOf = (page: Record<string, any>) => {
101+
for (const region of page.regions ?? []) {
102+
for (const component of region.components ?? []) {
103+
if (component.type === 'page:header') return component.properties ?? {};
104+
}
105+
}
106+
return {};
107+
};
108+
109+
for (const page of PAGES) {
110+
// Snapshot by value up front — comparing two live reads of the same
111+
// object afterwards could not detect a mutation.
112+
const before = { ...headerOf(page) };
113+
const translated = translatePage(page as any, SetupAppTranslations, { locale: 'zh-CN' });
114+
const after = headerOf(translated);
115+
116+
expect({ page: page.name, titleChanged: after.title !== before.title })
117+
.toEqual({ page: page.name, titleChanged: true });
118+
expect({ page: page.name, subtitleChanged: after.subtitle !== before.subtitle })
119+
.toEqual({ page: page.name, subtitleChanged: true });
120+
// Non-translatable props survive the overlay.
121+
expect({ page: page.name, icon: after.icon }).toEqual({ page: page.name, icon: before.icon });
122+
// The shared plugin-owned page object must not be mutated — it is a
123+
// module-level singleton the kernel registers once.
124+
expect({ page: page.name, header: headerOf(page) }).toEqual({ page: page.name, header: before });
125+
}
126+
});
127+
});

0 commit comments

Comments
 (0)