Skip to content

Commit 934810c

Browse files
feat(app-shell,i18n): the Console draws the DEPENDENCY_MISSING install refusal in the author's terms (#26123)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"} --> _Requested by **Leehom** · [project thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1XJ5XLtgWVaBQg2nkK9T9j9j)_ Part of #25792 Clause-②: no Metadata-protocol: none ## What ADR-0141 D2 refuses an install whose declared `dependencies` the environment does not satisfy: `422 DEPENDENCY_MISSING`, with `details.unmet[]` naming each entry's `packageId`, `dependency`, `range`, what was `found` and the `installedVersion`. Until now the Console showed only the envelope's English sentence, the same in every locale and without the ranges. This is item 3 of #25792 (claim amendment 6415110715). The Console now draws the refusal in the author's terms at every install surface that goes through `install-local`: a heading saying nothing was installed, then one line per unmet entry, built from the refusal's own facts. | Surface | Before | After (zh-CN) | |---|---|---| | Package page → Install | the server's English sentence | `安装被拒绝:…` then `• 请先安装 app.objectstack.hotcrm 6.x。` | | Catalog → Your organization → Install (self-hosted, `installLocal`) | same | same, in the strip above the cards | | Setup → Packages → Import | `<sentence> (DEPENDENCY_MISSING)` | same lines | An out-of-range entry reads `• 请先安装 com.acme.billing ^2.1.0(当前安装的是 1.4.0)。` / `• Install com.acme.billing ^2.1.0 first. Version 1.4.0 is installed.` The `unreadable-range`, `unreadable-version` and `unreadable-declaration` entries each get their own line. ## How - `console/marketplace/dependencyMissing.ts` (new): `readDependencyMissing(details)` reads `details.unmet[]` all or nothing. An unknown `found`, an empty list, or an entry missing its `dependency`/`range` returns `null`, so the Console never shows fewer things to install than the server refused for. `dependencyMissingText(facts, t)` builds the heading plus one bullet per fact. `useDependencyMissingText()` returns the text for a caught error whose code is `DEPENDENCY_MISSING` and whose facts it can read, else `null`, and the caller keeps its own message. - `marketplaceApi.installLocal()` now also carries the envelope's `details` on the error it throws, next to `code` and `status`. - `PackagesPage`'s `apiJson` now carries `code` and `details` on its thrown `Error`. No existing caller reads either one; the message is unchanged. - `MarketplacePackagePage.doInstallLocal`, `MarketplacePage.doOrgInstall` and `PackagesPage.onImportFile` try the refusal text first. The org strip and the import note gain `whitespace-pre-wrap` so the lines stay lines; the package page note already had it. - `@object-ui/i18n`: `marketplace.install.dependencyMissing.{heading,absent,outOfRange,unreadableRange,unreadableVersion,unreadableDeclaration}` in all ten locale packs. Setup → Packages uses the packs through the hook because the `engine.*` table is en/zh only. - `scripts/sdui-manifest.record.json`: the locale packs are registry sources, so the recorded source digest moves. It was regenerated with `node scripts/gen-sdui-manifest-node.mjs`; `sdui.manifest.json` is byte-identical. The dependency shows as its package ID (`app.objectstack.hotcrm`), not a display name: the refusal's facts carry no display name, and this PR adds no server change. ## Tests - `dependencyMissing-25792.test.ts`: the reader (all or nothing, every `found`), the text, and `installLocal` rejecting with `code`, `status` and `details` over a stubbed wire. - `MarketplacePackagePage.dependencyMissing-25792.test.tsx`: the real `marketplaceApi` over a stubbed `fetch`. The note equals the heading plus two lines carrying each entry's id, range and version, and the server sentence is gone. A control case (no `unmet`) keeps the server message. - `MarketplacePage.orgInstallDependencyMissing-25792.test.tsx`: the org install's real `installLocal`, the same lines, the `whitespace-pre-wrap` class, and a `MANIFEST_CONFLICT` control. - `PackagesPage.importDependencyMissing-25792.test.tsx`: the real `I18nProvider`. It checks the exact zh-CN lines (`• 请先安装 app.objectstack.hotcrm 6.x。`) and en lines, plus a control case. Evidence: - **Red first:** with the wiring reverted, 5 failed and 7 passed. The controls and the pure reader/text cases passed. - **Green:** 12/12 pass. - **Ablations:** 5, all red, each restored to HEAD: - drop the hook at the import door - drop the hook at the org install - drop `details` in `installLocal` - drop `details` in `apiJson` - drop `whitespace-pre-wrap` on the org strip - **Wider runs:** `marketplace/`, `views/metadata-admin/` and `packages/i18n` ran 727 files and 8276 tests, all passing. After merging main, the new tests and `packages/i18n` ran 1365 tests, all passing. - **Checks:** app-shell `tsc --noEmit` passed. Scoped eslint shows 0 errors; its first run was red on an unused import in one new test. `check-i18n-call-site-keys`, en-drift, dead-keys and designer-parity all pass. What to do: nothing. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01NndsH9Qm7k6AS2i2fWbjNA _Generated by [Claude Code](https://claude.ai/code/session_01NndsH9Qm7k6AS2i2fWbjNA)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2137155 commit 934810c

21 files changed

Lines changed: 734 additions & 10 deletions
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
'@object-ui/i18n': patch
4+
---
5+
6+
feat(app-shell,i18n): the Console draws the install refusal `DEPENDENCY_MISSING` in the author's terms, one line per unmet dependency
7+
8+
Clause-②: no
9+
Metadata-protocol: none
10+
11+
ADR-0141 D2 refuses to install a package whose declared `dependencies` the environment does not satisfy (`422 DEPENDENCY_MISSING`, `details.unmet[]` naming each entry's `dependency`, `range`, what was `found` and the `installedVersion`). The Console showed the envelope's English sentence, the same in every locale and without the ranges.
12+
13+
- **`@object-ui/app-shell`:** the three install surfaces that go through `install-local` (the package page's Install, the catalog page's organization install on a self-hosted runtime, and Setup → Packages → Import) draw the refusal from its facts: a heading saying nothing was installed, then one line per unmet entry, such as 「请先安装 app.objectstack.hotcrm 6.x。」 or "Install com.acme.billing ^2.1.0 first. Version 1.4.0 is installed." `installLocal()` now carries the envelope's `details` on the error it throws, and the Packages page's request helper carries `code` and `details`. A refusal whose facts the Console cannot read in full (an older runtime, or a `found` value it does not know) keeps the server's message.
14+
- **`@object-ui/i18n`:** `marketplace.install.dependencyMissing.*` (a heading and one line for each `found` value) in all ten locale packs.
15+
16+
What to do: nothing.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"package":"ui/packages/i18n","sources":31,"sha256":"ae5a36bc2327b985cb909eed35e099d1b2d953b7c4cb7dda463e4d8d3ef5cc86","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"}
1+
{"package":"ui/packages/i18n","sources":31,"sha256":"7e7907af7823796538ad5a70e56fb172dc533ec3988a2318c8aeddf7069af14a","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"}

‎ui/packages/app-shell/src/console/marketplace/MarketplacePackagePage.tsx‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ import {
8080
type LocalInstallStrippedLock,
8181
type CloudInstallationInfo,
8282
} from './marketplaceApi.js';
83+
import { useDependencyMissingText } from './dependencyMissing.js';
8384
import { getCloudBase, getRuntimeConfig, isMarketplaceEnabled } from '../../runtime-config.js';
8485
import { emitMetadataRefresh } from '../../assistant/assistantBus.js';
8586
import { useMetadata } from '../../providers/MetadataProvider.js';
@@ -144,6 +145,7 @@ export function MarketplacePackagePage() {
144145
const { packageId, appName } = useParams<{ packageId?: string; appName?: string }>();
145146
const { isAdmin, isResolved } = useWorkspaceAdminStatus();
146147
const { t, language } = useObjectTranslation();
148+
const dependencyMissingText = useDependencyMissingText();
147149
// Dates and numbers on this surface read the display locale; a bare
148150
// `toLocale*()` call used the MACHINE's locale (objectui#9909).
149151
const displayLocale = useDisplayLocale();
@@ -637,7 +639,13 @@ export function MarketplacePackagePage() {
637639
// remedy, and the page has nothing to add to it.
638640
const code = e?.code;
639641
let msg = e?.message ?? String(e);
640-
if (errorCodeIs(e, 'MANIFEST_CONFLICT')) {
642+
// ADR-0141 D2 — an unsatisfied declared dependency, drawn from the
643+
// refusal's facts in the reader's locale; without facts the envelope's
644+
// message stands.
645+
const dependencyMissing = dependencyMissingText(e);
646+
if (dependencyMissing) {
647+
msg = dependencyMissing;
648+
} else if (errorCodeIs(e, 'MANIFEST_CONFLICT')) {
641649
msg = t('marketplace.install.localManifestConflict', { message: msg });
642650
} else if (errorCodeIs(e, 'UNAUTHENTICATED')) {
643651
msg = t('marketplace.install.localUnauthorized');

‎ui/packages/app-shell/src/console/marketplace/MarketplacePage.tsx‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import { emitMetadataRefresh } from '../../assistant/assistantBus.js';
5050
import { appRouteSegment, filterActiveApps, resolveKeyedI18nLabel } from '../../utils/index.js';
5151
import { waitForServedApp, type ServedApp } from './waitForServedApp.js';
5252
import { splitPackageSuffix } from './middleTruncate.js';
53+
import { useDependencyMissingText } from './dependencyMissing.js';
5354
import { packagesHref } from '../../views/metadata-admin/packageSource.js';
5455

5556
/**
@@ -306,6 +307,7 @@ export function MarketplacePage() {
306307
const { appName } = useParams();
307308
const { isAdmin, isResolved } = useWorkspaceAdminStatus();
308309
const { t, language } = useObjectTranslation();
310+
const dependencyMissingText = useDependencyMissingText();
309311
const { appLabel } = useObjectLabel();
310312
// The adapter `MetadataProvider` reads through: the deploy wait asks the
311313
// same `GET /meta/app` the cache would, without the cache persisting it.
@@ -504,7 +506,9 @@ export function MarketplacePage() {
504506
await load();
505507
}
506508
} catch (e: any) {
507-
setOrgMsg({ ok: false, text: e?.message ?? String(e) });
509+
// ADR-0141 D2 — a `DEPENDENCY_MISSING` refusal reads one line per unmet
510+
// dependency, from its own facts; anything else keeps its message.
511+
setOrgMsg({ ok: false, text: dependencyMissingText(e) ?? e?.message ?? String(e) });
508512
} finally {
509513
setOrgInstalling(null);
510514
}
@@ -715,7 +719,7 @@ export function MarketplacePage() {
715719
</div>
716720
{orgDeployNote}
717721
{orgMsg && (
718-
<div className={`rounded-md border p-3 text-sm ${orgMsg.ok ? 'border-green-500/30 bg-green-500/5 text-green-700 dark:text-green-400' : 'border-destructive/30 bg-destructive/5 text-destructive'}`}>
722+
<div className={`whitespace-pre-wrap rounded-md border p-3 text-sm ${orgMsg.ok ? 'border-green-500/30 bg-green-500/5 text-green-700 dark:text-green-400' : 'border-destructive/30 bg-destructive/5 text-destructive'}`}>
719723
{orgMsg.text}
720724
</div>
721725
)}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* The package page draws the `422 DEPENDENCY_MISSING` install refusal
5+
* (ADR-0141 D2) in the author's terms — 「请先安装 HotCRM 6.x」 — one line per
6+
* unmet entry, from the refusal's own facts (objectstack#25792 item 3).
7+
*
8+
* `marketplaceApi` is NOT mocked: one stubbed `fetch` answers by path, so the
9+
* facts the page draws are the ones that crossed the wire. `t` echoes the KEY
10+
* and the interpolated facts: the claim is WHICH line each entry gets and that
11+
* it carries that entry's id, range and version.
12+
*/
13+
14+
import '@testing-library/jest-dom/vitest';
15+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
16+
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react';
17+
18+
vi.mock('react-router-dom', () => ({
19+
useNavigate: () => vi.fn(),
20+
useParams: () => ({ packageId: 'pkg_psa' }),
21+
}));
22+
23+
vi.mock('@object-ui/auth', async (importOriginal) => ({
24+
...(await importOriginal<Record<string, unknown>>()),
25+
useWorkspaceAdminStatus: () => ({ isAdmin: true, isResolved: true }),
26+
}));
27+
28+
vi.mock('@object-ui/i18n', async (importOriginal) => ({
29+
...(await importOriginal<Record<string, unknown>>()),
30+
useObjectTranslation: () => ({
31+
t: (key: string, o?: Record<string, unknown>) => {
32+
const facts = ['dependency', 'range', 'installedVersion'].filter((k) => o?.[k] !== undefined).map((k) => `${k}=${String(o?.[k])}`);
33+
return facts.length > 0 ? `«${key}|${facts.join('|')}»` : `«${key}»`;
34+
},
35+
language: 'en',
36+
}),
37+
}));
38+
39+
vi.mock('../../../assistant/assistantBus', () => ({ emitMetadataRefresh: () => {} }));
40+
vi.mock('../../../providers/MetadataProvider', () => ({ useMetadata: () => ({ refresh: async () => {} }) }));
41+
vi.mock('../../../components/SuggestedBindingsPanel', () => ({
42+
SuggestedBindingsPanel: () => <div data-testid="suggested-bindings" />,
43+
}));
44+
45+
import { initRuntimeConfig, resetRuntimeConfigForTesting } from '../../../runtime-config';
46+
import { MarketplacePackagePage } from '../MarketplacePackagePage';
47+
48+
const INSTALL_PATH = '/api/v1/marketplace/install-local';
49+
const MANIFEST_PATH = '/api/v1/marketplace/packages/pkg_psa/versions/latest/manifest';
50+
const CATALOG_PACKAGE = {
51+
package: { id: 'pkg_psa', manifest_id: 'com.acme.psa', display_name: 'PSA', latest_version: { id: 'ver_1', version: '1.0.0' } },
52+
versions: [{ id: 'ver_1', version: '1.0.0' }],
53+
};
54+
const SERVER_MESSAGE = 'SERVER-ENGLISH sentinel: Install refused: a declared dependency is not satisfied in this environment.';
55+
56+
let installBody: unknown;
57+
58+
const answer = (status: number, body: unknown) => ({
59+
ok: status >= 200 && status < 300,
60+
status,
61+
statusText: '',
62+
headers: new Headers({ 'content-type': 'application/json' }),
63+
json: async () => body,
64+
text: async () => JSON.stringify(body),
65+
});
66+
67+
async function boot() {
68+
resetRuntimeConfigForTesting();
69+
vi.stubGlobal('fetch', vi.fn(async (input: unknown, init?: { method?: string }) => {
70+
const path = new URL(String(input), 'http://console.test').pathname;
71+
const method = (init?.method ?? 'GET').toUpperCase();
72+
if (path === '/api/v1/runtime/config') {
73+
return answer(200, {
74+
cloudUrl: '', singleEnvironment: true,
75+
features: { installLocal: true, marketplace: true, aiStudio: true, autoPublishAiBuilds: true },
76+
branding: { productName: 'ObjectOS', productShortName: 'ObjectOS' },
77+
});
78+
}
79+
if (path === INSTALL_PATH && method === 'GET') return answer(200, { success: true, data: { items: [], total: 0 } });
80+
if (path === INSTALL_PATH && method === 'POST') return answer(422, installBody);
81+
if (path === MANIFEST_PATH) {
82+
return answer(200, { success: true, data: { manifest: { id: 'com.acme.psa', version: '1.0.0', type: 'app' }, version: '1.0.0', version_id: 'ver_1' } });
83+
}
84+
if (path === '/api/v1/marketplace/packages/pkg_psa') return answer(200, { success: true, data: CATALOG_PACKAGE });
85+
return answer(404, { success: false, error: { code: 'NOT_FOUND', message: `no route ${method} ${path}` } });
86+
}));
87+
await initRuntimeConfig();
88+
}
89+
90+
async function install() {
91+
render(<MarketplacePackagePage />);
92+
const button = await screen.findByRole('button', { name: '«marketplace.action.install»' });
93+
await act(async () => {});
94+
fireEvent.click(button);
95+
}
96+
97+
const refusal = (details: unknown) => ({
98+
success: false,
99+
error: { code: 'DEPENDENCY_MISSING', httpStatus: 422, message: SERVER_MESSAGE, details },
100+
});
101+
102+
beforeEach(() => { installBody = undefined; });
103+
afterEach(() => {
104+
cleanup();
105+
vi.unstubAllGlobals();
106+
resetRuntimeConfigForTesting();
107+
});
108+
109+
describe('the package page draws DEPENDENCY_MISSING from its facts (objectstack#25792)', () => {
110+
it('one line per unmet entry under the heading, in place of the server\'s English sentence', async () => {
111+
installBody = refusal({
112+
compatible: false,
113+
issues: [
114+
{ severity: 'error', category: 'dependency_missing', message: 'not installed' },
115+
{ severity: 'error', category: 'dependency_missing', message: 'out of range' },
116+
],
117+
unmet: [
118+
{ packageId: 'com.acme.psa', dependency: 'app.objectstack.hotcrm', range: '6.x', found: 'absent' },
119+
{ packageId: 'com.acme.psa', dependency: 'com.acme.billing', range: '^2.1.0', found: 'out-of-range', installedVersion: '1.4.0' },
120+
],
121+
});
122+
await boot();
123+
await install();
124+
125+
const note = await screen.findByText(/marketplace\.install\.dependencyMissing\.heading/);
126+
expect(note.textContent!.split('\n')).toEqual([
127+
'«marketplace.install.dependencyMissing.heading»',
128+
'• «marketplace.install.dependencyMissing.absent|dependency=app.objectstack.hotcrm|range=6.x|installedVersion=—»',
129+
'• «marketplace.install.dependencyMissing.outOfRange|dependency=com.acme.billing|range=^2.1.0|installedVersion=1.4.0»',
130+
]);
131+
expect(screen.queryByText(SERVER_MESSAGE)).not.toBeInTheDocument();
132+
});
133+
134+
it('CONTROL: a refusal without facts it can read keeps the envelope\'s own message', async () => {
135+
installBody = refusal({ compatible: false, issues: [] });
136+
await boot();
137+
await install();
138+
139+
expect(await screen.findByText(SERVER_MESSAGE)).toBeInTheDocument();
140+
expect(screen.queryByText(/dependencyMissing/)).not.toBeInTheDocument();
141+
});
142+
});
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* The catalog page's org install on a self-hosted runtime (`installLocal`)
5+
* draws the `422 DEPENDENCY_MISSING` refusal (ADR-0141 D2) in the author's
6+
* terms, one line per unmet entry, from the refusal's own facts
7+
* (objectstack#25792 item 3).
8+
*
9+
* The catalog lists are stubbed; `installLocal` is the REAL client, so the
10+
* facts the strip draws are the ones that crossed the wire. `t` echoes the KEY
11+
* and the interpolated facts: the claim is WHICH line each entry gets and that
12+
* it carries that entry's id, range and version.
13+
*/
14+
15+
import '@testing-library/jest-dom/vitest';
16+
import { afterEach, describe, expect, it, vi } from 'vitest';
17+
import { cleanup, fireEvent, render, screen, within } from '@testing-library/react';
18+
19+
vi.mock('react-router-dom', () => ({
20+
useNavigate: () => vi.fn(),
21+
useParams: () => ({}),
22+
}));
23+
24+
vi.mock('@object-ui/auth', async (importOriginal) => ({
25+
...(await importOriginal<Record<string, unknown>>()),
26+
useWorkspaceAdminStatus: () => ({ isAdmin: true, isResolved: true }),
27+
useAuth: () => ({ activeOrganization: { id: 'org_1' } }),
28+
}));
29+
30+
vi.mock('@object-ui/i18n', async (importOriginal) => ({
31+
...(await importOriginal<Record<string, unknown>>()),
32+
useObjectTranslation: () => ({
33+
t: (key: string, o?: Record<string, unknown>) => {
34+
const facts = ['dependency', 'range', 'installedVersion'].filter((k) => o?.[k] !== undefined).map((k) => `${k}=${String(o?.[k])}`);
35+
return facts.length > 0 ? `«${key}|${facts.join('|')}»` : `«${key}»`;
36+
},
37+
language: 'en',
38+
}),
39+
}));
40+
41+
const PKG = 'com.acme.psa';
42+
const ORG_ROW = {
43+
id: 'pkg_psa',
44+
manifest_id: PKG,
45+
display_name: 'Acme PSA',
46+
description: 'A PSA.',
47+
category: null,
48+
icon_url: null,
49+
visibility: 'org',
50+
latest_version: '1.0.0',
51+
};
52+
53+
vi.mock('../marketplaceApi', async (importOriginal) => ({
54+
...(await importOriginal<Record<string, unknown>>()),
55+
listMarketplacePackages: async () => ({ items: [] }),
56+
listLocalInstalls: async () => [],
57+
listOrgPackages: async () => ({ connected: true, items: [ORG_ROW] }),
58+
listInstalledPackages: async () => ({ connected: true, items: [] }),
59+
}));
60+
61+
import { ActiveOrganizationStorage } from '@object-ui/auth';
62+
import { initRuntimeConfig, resetRuntimeConfigForTesting } from '../../../runtime-config';
63+
import { MarketplacePage } from '../MarketplacePage';
64+
65+
const INSTALL_PATH = '/api/v1/marketplace/install-local';
66+
const SERVER_MESSAGE = 'SERVER-ENGLISH sentinel: Install refused: a declared dependency is not satisfied in this environment.';
67+
68+
const answer = (status: number, body: unknown) => ({
69+
ok: status >= 200 && status < 300,
70+
status,
71+
statusText: '',
72+
headers: new Headers({ 'content-type': 'application/json' }),
73+
json: async () => body,
74+
text: async () => JSON.stringify(body),
75+
});
76+
77+
async function boot(installBody: unknown) {
78+
resetRuntimeConfigForTesting();
79+
ActiveOrganizationStorage.set('org_1');
80+
vi.stubGlobal('fetch', vi.fn(async (input: unknown, init?: { method?: string }) => {
81+
const path = new URL(String(input), 'http://console.test').pathname;
82+
const method = (init?.method ?? 'GET').toUpperCase();
83+
if (path === INSTALL_PATH && method === 'POST') return answer(422, installBody);
84+
if (path === '/api/v1/runtime/config') {
85+
return answer(200, {
86+
cloudUrl: 'https://cloud.example', singleEnvironment: true, defaultEnvironmentId: 'env_1',
87+
features: { installLocal: true, marketplace: true, aiStudio: true, autoPublishAiBuilds: true },
88+
branding: { productName: 'ObjectOS', productShortName: 'ObjectOS' },
89+
});
90+
}
91+
return answer(404, { success: false, error: { code: 'NOT_FOUND', message: `no route ${method} ${path}` } });
92+
}));
93+
await initRuntimeConfig();
94+
}
95+
96+
async function installFromOrgCard() {
97+
render(<MarketplacePage />);
98+
const card = await screen.findByTestId(`org-card-${PKG}`);
99+
fireEvent.click(within(card).getByRole('button', { name: '«marketplace.org.install»' }));
100+
}
101+
102+
const refusal = (details: unknown) => ({
103+
success: false,
104+
error: { code: 'DEPENDENCY_MISSING', httpStatus: 422, message: SERVER_MESSAGE, details },
105+
});
106+
107+
afterEach(() => {
108+
cleanup();
109+
vi.unstubAllGlobals();
110+
resetRuntimeConfigForTesting();
111+
});
112+
113+
describe('the catalog page draws DEPENDENCY_MISSING from its facts (objectstack#25792)', () => {
114+
it('one line per unmet entry under the heading, in place of the server\'s English sentence', async () => {
115+
await boot(refusal({
116+
compatible: false,
117+
issues: [
118+
{ severity: 'error', category: 'dependency_missing', message: 'not installed' },
119+
{ severity: 'error', category: 'dependency_missing', message: 'version unreadable' },
120+
],
121+
unmet: [
122+
{ packageId: PKG, dependency: 'app.objectstack.hotcrm', range: '6.x', found: 'absent' },
123+
{ packageId: PKG, dependency: 'com.acme.billing', range: '^2.1.0', found: 'unreadable-version', installedVersion: 'nightly' },
124+
],
125+
}));
126+
await installFromOrgCard();
127+
128+
const strip = await screen.findByText(/marketplace\.install\.dependencyMissing\.heading/);
129+
expect(strip.textContent!.split('\n')).toEqual([
130+
'«marketplace.install.dependencyMissing.heading»',
131+
'• «marketplace.install.dependencyMissing.absent|dependency=app.objectstack.hotcrm|range=6.x|installedVersion=—»',
132+
'• «marketplace.install.dependencyMissing.unreadableVersion|dependency=com.acme.billing|range=^2.1.0|installedVersion=nightly»',
133+
]);
134+
// The lines stay lines on screen.
135+
expect(strip).toHaveClass('whitespace-pre-wrap');
136+
expect(screen.queryByText(SERVER_MESSAGE)).not.toBeInTheDocument();
137+
});
138+
139+
it('CONTROL: another refusal keeps the envelope\'s own message', async () => {
140+
await boot({ success: false, error: { code: 'MANIFEST_CONFLICT', httpStatus: 409, message: SERVER_MESSAGE } });
141+
await installFromOrgCard();
142+
143+
expect(await screen.findByText(SERVER_MESSAGE)).toBeInTheDocument();
144+
expect(screen.queryByText(/dependencyMissing/)).not.toBeInTheDocument();
145+
});
146+
});

0 commit comments

Comments
 (0)