Skip to content

Commit 595311b

Browse files
fix(cloud-connection)!: install-local's cloud branch refuses a held namespace or security catalog name (#31392) (#31476)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"} --> _Requested by **Leehom** · [project thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1XTVkGrf2CjQTF79hBERgqiJ)_ domain:server#5 · os-project-manager · session_01FEkQ5ktJb5rQ5n2GUXSQMR Fixes #31392 Clause-②: no (narrowing) — a cloud-branch install that answered success (`hotLoaded: false`) for a held namespace or a held security catalog name is now refused 422; one contract-review-tier review owed before the queue Metadata-protocol: none ## 维护者速读 - 改了什么:`POST /api/v1/marketplace/install-local` 的云端分支(从控制面下载的包)现在拒绝两类注册失败,和内联分支一样回 `422 PLUGIN_REGISTER_FAILED`。一类是包的命名空间已被另一个已装包占用(`NamespaceConflictError`);另一类是包带的岗位、权限集或能力名已被另一个包或平台内置名占用(`SecurityCatalogNameConflictError`,持有者为包或内置)。消息点名两个持有者,不写安装记录,也不注册。 - 为什么改:以前这两类都走宽容路径:写安装记录,回 200 和 `hotLoaded: false`,声称“下次重启加载”。但这两项检查在每次加载时都会重新判定,而且没有封印,所以重启时同一个持有者还在,注册还是会被拒。 - 风险与回滚:原来的两个判定函数合并成一个 `refusesNameHeldElsewhere`,它按错误自身的 code 加 `name` 识别注册表的三种“名字已被占用”拒绝(命名空间、安全目录、裸名),不复制任何持有者或类型规则。其它注册失败照旧走宽容路径。`OS_METADATA_COLLISION=warn` 下命名空间闸门本来就不抛错,行为不变。回滚即还原本 PR。 - 先看:`packages/cloud-connection/src/marketplace-install-local-plugin.ts` 里的 `NAME_HELD_ELSEWHERE_REFUSALS`、`refusesNameHeldElsewhere`,以及 catch 分支里的那一行 `if`。 - 两分钟验证:`pnpm --filter @objectstack/cloud-connection exec vitest run src/marketplace-install-local-catalog-name-collision.test.ts src/marketplace-install-local-bare-name-conflict.test.ts`,预期 28 个用例全绿。 - 需要你做的:无,席位落地。 ## What changes `packages/cloud-connection/src/marketplace-install-local-plugin.ts`, the cloud-fetched branch of `POST /api/v1/marketplace/install-local` (a `packageId` + `versionId` body): 1. **One predicate in place of two.** `refusesNameHeldElsewhere(err)` replaces `refusesEnvironmentHeldCatalogName` (#25366) and `refusesBareNameHeldElsewhere` (#31296). It is #31296's code + name recognition, widened to the registry's three held-name refusals, which share `NAMESPACE_CONFLICT_CODE`: - `NamespaceConflictError` - `SecurityCatalogNameConflictError` - `BareNameConflictError` The names are listed once, in `NAME_HELD_ELSEWHERE_REFUSALS`. The predicate never reads the error's holders or types. #25366's environment-only filter goes away: the environment-held refusal is one case of `SecurityCatalogNameConflictError`, and package and built-in holders are now refused as well. 2. **The catch asks it.** When it answers yes, the cloud branch answers `422 PLUGIN_REGISTER_FAILED` with the registry's sentence. That happens before the ledger write and before anything else this door writes. The registry's package door had already refused before any of its own writes. 3. **Doc comments.** These now say which refusals are refused and why no restart heals any of them: - the module header's install paragraph; - the import comment; - the catch comment; - step 1f's note, which used to read "a namespace held by another code package keeps the lenient branch's existing behaviour, whose fate is a separate decision". What does not change: - Any other register failure on the cloud branch keeps #22695's lenient answer. - The inline branch is unchanged. - Step 1f's `409 RESOURCE_CONFLICT` for a namespace a writable base holds still answers first. - Under `OS_METADATA_COLLISION=warn`, the namespace gate warns and throws nothing. ## Tests Both harnesses use a real `ObjectKernel` + `ObjectQLPlugin`, booted. The holder is installed through the same door, and the cloud leg is a stubbed `fetch`. Cases are added to the two existing harnesses; there is no new test file. - `marketplace-install-local-catalog-name-collision.test.ts`: one new describe on the cloud-fetched branch. - A permission set name another installed package holds. On a fresh install and on an upgrade, the answer is 422 in the declared envelope, naming `package "com.acme.dispatch-roles"`. Nothing is registered or ledgered, the installed version 1 is kept, and the holder's item and ledger entry are unchanged. - The built-in position `everyone`: 422 naming the platform, nothing registered or ledgered. - CONTROL: beside the holder, an upgrade shipping a name nobody holds installs. The answer is `200` with `hotLoaded: true`, and the package is registered and ledgered. - The existing #25366 matrix stays as it is, and it is the CONTROL that an environment-held catalog name keeps its 422. - `marketplace-install-local-bare-name-conflict.test.ts`: one new describe. - The incoming package declares the namespace the holder owns (`dispatch`), with no dependency on it. On a fresh install and on an upgrade, the answer is 422 with the registry's namespace sentence, and the registry and the ledger are the same as before the call. - The harness's `objectql` service now hands the door the engine's `manifests`, read live, as the real service does. Without that, the holder this door installed reads as a writable base, and step 1f answers 409 first. That 409 was measured, and it is not this card's refusal. - The existing CONTROL that a fresh flow name installs stays as it is. <details><summary>Red on main (the new cases run against the unfixed plugin at base 1ff9944, before the fix was written)</summary> ``` × a fresh install is refused: 422 naming the namespace and its owner, nothing registered or ledgered × an upgrade into the held namespace is refused: 422, the installed version still registered and ledgered × a fresh install of a name another package holds is refused: 422 naming the holder, nothing registered or ledgered × an upgrade to a name another package holds is refused: 422, the installed version still registered and ledgered × a fresh install of a built-in name is refused: 422 naming the platform, nothing registered or ledgered AssertionError: {"success":true,"data":{"manifestId":"com.acme.routing",…,"hotLoaded":false,"hotLoadError":"Namespace conflict: namespace \"dispatch\" is already owned by package \"com.acme.dispatch\", so package \"com.acme.routing\" cannot be installed alongside it. …"}}: expected 200 to be 422 AssertionError: {"success":true,"data":{"manifestId":"com.acme.field-service",…,"hotLoaded":false,"hotLoadError":"Security catalog name conflict: … permission set \"acme_dispatcher_lite\" is already held by package \"com.acme.dispatch-roles\". …"}}: expected 200 to be 422 AssertionError: {"success":true,"data":{"manifestId":"com.acme.field-service",…,"hotLoaded":false,"hotLoadError":"Security catalog name conflict: … position \"everyone\" is already held by the platform, as a built-in name no package can declare. …"}}: expected 200 to be 422 Tests 5 failed | 23 passed (28) ``` The namespace half was measured before it was pinned. Its first run against the harness without `manifests` answered `409 RESOURCE_CONFLICT` from step 1f, because the holder read as a writable base. With the engine's `manifests` handed over, it answered `200`, `hotLoaded: false`, and the package was ledgered. </details> <details><summary>Ablations (fix committed at 2f6e606, each restored)</summary> Both were run through `node scripts/ablation-replace.mjs` on `marketplace-install-local-plugin.ts`, against the two suites. The suites import the plugin from `src/` relatively, so no build applies. 1. The `'NamespaceConflictError'` entry was commented out of `NAME_HELD_ELSEWHERE_REFUSALS`: ``` ok mutation landed: anchor 1 -> 0, blob 6d5c60f -> adfacdf83da1 × a fresh install is refused: 422 naming the namespace and its owner, nothing registered or ledgered × an upgrade into the held namespace is refused: 422, the installed version still registered and ledgered Tests 2 failed | 26 passed (28) ok restored: blob == HEAD (6d5c60f) and `git diff HEAD` is empty ``` 2. The `'SecurityCatalogNameConflictError'` entry was commented out. The three new cases red, and so do the six cloud-branch cases of the #25366 matrix plus its CONTROL in the bare-name file, because the environment-held refusal is now recognised by the same entry: ``` ok mutation landed: anchor 1 -> 0, blob 6d5c60f -> 89a012028652 × CONTROL — an environment-held security catalog name keeps its answer: 422 naming the environment catalog × a fresh install is refused: 422, nothing registered, no ledger entry (×3, cloud-fetched · permission / position / capability) × an upgrade is refused: 422, the installed version still registered and ledgered (×3) × a fresh install of a name another package holds is refused: 422 naming the holder, nothing registered or ledgered × an upgrade to a name another package holds is refused: 422, the installed version still registered and ledgered × a fresh install of a built-in name is refused: 422 naming the platform, nothing registered or ledgered Tests 10 failed | 18 passed (28) ok restored: blob == HEAD (6d5c60f) and `git diff HEAD` is empty ``` </details> ## Verification All checks ran on head `2f6e606d`: the fix, the tests and the changeset. `origin/main` was fetched before verification and was still at its base, 1ff9944, so there is no merge. Each exit code was captured before any pipe. - **Full build**, `pnpm turbo run build --concurrency=2`, under the verify lock: VERDICT command-exit 0 (120 of 120 tasks). - **Derived gate union**, `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`: 69 families, every one exit 0 on its first run. `--ran` reports "69 derived famil(ies) accounted for — 69 run, 0 NOT-MEASURED (a DERIVED zero — all 69 recorded an exit code and none of them is 3)". `check:dts-closure` and `check:dual-build-cjs-loads` ran after the full build. `check:type-check-debt` ran after the dogfood run released the lock. - **Facade gate**, `pnpm check:facade-delegation-only`, run by hand until devx #31444 lands: exit 0. This diff touches no facade file. - **Package**: `pnpm --filter @objectstack/cloud-connection test`: Test Files 75 passed, Tests 1000 passed (exit 0). `pnpm --filter @objectstack/cloud-connection typecheck` (src and `tsconfig.test.json`): exit 0. - **Lint**: `eslint --no-inline-config` on the three touched `.ts` files: exit 0. - **Full dogfood battery**, the `(narrowing)` arm, `pnpm --filter @objectstack/dogfood test` after the full build, under the verify lock: VERDICT command-exit 0. Test Files 439 passed / 3 skipped (442), Tests 3628 passed / 41 skipped (3669). - CI-only families (the Test Core shards, the type-check lanes, Build Docs) are read from this PR's check-runs, not re-run here. ## Acceptance notes - **Why one predicate.** The seat asked for #31296's code + name recognition with no copy of any holder rule. With package and built-in holders refused too, #25366's environment-only filter would have been the only holder reading left in this door, and the three classes' names already say everything the door needs to know. - **Namespace and step 1f.** Step 1f, the writable-base check, answers `409` before the register for a namespace a writable base holds. This PR is about the namespace a code package holds, which step 1f's comment had left as a separate decision. That decision is now this card's 422. - **Old ledger entries.** An entry an earlier build ledgered this way is read at boot as before. This PR refuses new ones only. - **Governed surface.** None. The diff is one source file, two test files and one changeset. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01FEkQ5ktJb5rQ5n2GUXSQMR --- _Generated by [Claude Code](https://claude.ai/code/session_01FEkQ5ktJb5rQ5n2GUXSQMR)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent a1d243e commit 595311b

4 files changed

Lines changed: 273 additions & 74 deletions
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@objectstack/cloud-connection': patch
3+
---
4+
5+
fix(cloud-connection)!: install-local's cloud branch refuses a package whose namespace or security catalog name another package or a built-in holds (#31392)
6+
7+
Clause-②: no (narrowing) — `POST /api/v1/marketplace/install-local` with a cloud-fetched package (`packageId` + `versionId`) whose register the registry refuses because another installed package owns its namespace, or because another package or a built-in holds a position, permission set or capability name it ships, answers `422 PLUGIN_REGISTER_FAILED` where it answered `200` with `hotLoaded: false` and wrote the ledger entry. Nothing is widened.
8+
Metadata-protocol: none
9+
10+
<!-- adr-0087: not-required (no-migration-prescription) no key of any schema is added, removed or re-shaped and no stored row is read differently: the narrowing is one runtime refusal at one door, for two registry refusals the inline branch of the same door already answers 422, and its remedy is the rename or uninstall the refusal names, so there is nothing for `objectstack migrate meta` to rewrite. A ledger entry an earlier build wrote this way is read as before at boot. -->
11+
12+
**BREAKING** for a cloud-branch install that relied on the lenient answer for a held namespace or a held security catalog name.
13+
14+
- **Before.** The cloud branch refused two of the registry's held-name refusals: a security catalog name the environment catalog holds (#25366) and a bare name another package holds (#31296). A package whose `manifest.namespace` another installed package owns (with no declared dependency on it), or that ships a position, permission set or capability name another package or a built-in holds, was refused by the registry and then ledgered anyway, answering `200` with `hotLoaded: false`, "loads at the next restart". It does not: both checks run again at every load with no seal, so the next boot refuses the registration again.
15+
- **Now.** Both refusals are answered `422 PLUGIN_REGISTER_FAILED` with the registry's sentence naming both holders, as the inline branch answers them. Nothing is registered or written, and an installed earlier version stays registered and ledgered. The door recognises the registry's three held-name refusals by their own code and `name`, never by a list of types or holders.
16+
- **Unchanged.** Any other register failure on the cloud branch keeps the lenient answer (`200`, `hotLoaded: false`, `hotLoadError`). Under `OS_METADATA_COLLISION=warn` the registry throws nothing for a held namespace, so the install proceeds as before. A namespace held by a writable base is still refused `409 RESOURCE_CONFLICT` before the register.
17+
18+
**The remedy.** Rename the item (or choose another namespace) in the incoming package, declare the holding app in its `manifest.dependencies` when it extends that app, rename the item in the holding package, or uninstall that package, then install again. A built-in name is never available to a package.

‎packages/cloud-connection/src/marketplace-install-local-bare-name-conflict.test.ts‎

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,14 @@
4040
* answered before (#25366; its full matrix lives in
4141
* `marketplace-install-local-catalog-name-collision.test.ts`);
4242
* - CONTROL, the same package declaring a flow name nobody holds: `200`,
43-
* `hotLoaded: true`, registered and ledgered.
43+
* `hotLoaded: true`, registered and ledgered;
44+
* - #31392, the namespace sibling: a fresh install and an upgrade declaring
45+
* the namespace the holder owns, with no dependency on it, answer the same
46+
* `422` with the registry's namespace sentence, nothing registered or
47+
* ledgered. Measured before the fix: `200`, `hotLoaded: false`, ledgered.
48+
* The `objectql` service hands the door the engine's `manifests`, so the
49+
* holder this door installed reads as a code package (step 1f's writable
50+
* base refusal is not this one).
4451
*
4552
* A register failure that is NOT a registry refusal keeps #22695's lenient
4653
* answer; `marketplace-install-local-hot-loaded.test.ts` pins that.
@@ -154,6 +161,10 @@ async function bootPlugin() {
154161
objectql: withInstallerGrants({
155162
convertPackageBodyForward: (body: unknown) => ql.convertPackageBodyForward(body),
156163
registry: ql.registry,
164+
// [#31392] The engine's registered code packages, read live, so a
165+
// package this door installed reads as code and not as a writable
166+
// base (`isWritablePackage`), as on the real `objectql` service.
167+
get manifests() { return (ql as unknown as { manifests: Map<string, unknown> }).manifests; },
157168
syncSchemas: async () => undefined,
158169
}),
159170
};
@@ -298,3 +309,57 @@ describe('install-local cloud branch refuses a package declaring a bare name ano
298309
expectHolderFlowKept(ql);
299310
});
300311
});
312+
313+
describe('install-local cloud branch refuses a package whose namespace another package owns, before any write (#31392)', () => {
314+
const versions = {
315+
// The holder's namespace (`dispatch`), under another id, with no dependency on it.
316+
'v1-ns-held': packageVersion(INCOMING_ID, '1.0.0', [FREE_FLOW], { namespace: 'dispatch' }),
317+
'v1-free': packageVersion(INCOMING_ID, '1.0.0', [FREE_FLOW]),
318+
'v2-ns-held': packageVersion(INCOMING_ID, '2.0.0', [FREE_FLOW], { namespace: 'dispatch' }),
319+
};
320+
321+
function expectNamespaceRefusal(res: any) {
322+
expect(res.status, JSON.stringify(res.payload)).toBe(422);
323+
expect(BaseResponseSchema.safeParse(res.payload).success).toBe(true);
324+
expect(envelopeViolations(res.payload)).toEqual([]);
325+
expect(ApiErrorSchema.safeParse(res.payload.error).success).toBe(true);
326+
expect(res.payload.success).toBe(false);
327+
expect(res.payload.error.code).toBe('PLUGIN_REGISTER_FAILED');
328+
// The registry's own sentence: the namespace, its owner, and the package it stopped.
329+
expect(res.payload.error.message).toContain(`namespace "dispatch" is already owned by package "${HOLDER_ID}"`);
330+
expect(res.payload.error.message).toContain(`package "${INCOMING_ID}" cannot be installed alongside it`);
331+
}
332+
333+
it('a fresh install is refused: 422 naming the namespace and its owner, nothing registered or ledgered', async () => {
334+
serveVersions(versions);
335+
const { ql, installFromCloud, installHolder } = await bootPlugin();
336+
await installHolder();
337+
const before = ledger();
338+
339+
const res = await installFromCloud('v1-ns-held');
340+
341+
expectNamespaceRefusal(res);
342+
expect(res.payload.data).toBeUndefined();
343+
expect(ql.registry.getPackage(INCOMING_ID)).toBeUndefined();
344+
expect(ledger()).toEqual(before);
345+
expect(ledger().incoming).toBeNull();
346+
expectHolderFlowKept(ql);
347+
});
348+
349+
it('an upgrade into the held namespace is refused: 422, the installed version still registered and ledgered', async () => {
350+
serveVersions(versions);
351+
const { ql, installFromCloud, installHolder } = await bootPlugin();
352+
await installHolder();
353+
const first = await installFromCloud('v1-free');
354+
expect(first.status, JSON.stringify(first.payload)).toBe(200);
355+
const before = ledger();
356+
357+
const res = await installFromCloud('v2-ns-held');
358+
359+
expectNamespaceRefusal(res);
360+
expect(ql.registry.getPackage(INCOMING_ID)?.manifest?.version).toBe('1.0.0');
361+
expect(ledger()).toEqual(before);
362+
expect(ledger().incoming?.version).toBe('1.0.0');
363+
expectHolderFlowKept(ql);
364+
});
365+
});

‎packages/cloud-connection/src/marketplace-install-local-catalog-name-collision.test.ts‎

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,13 @@
3636
* the ledger exactly as before the call, and the environment's item still
3737
* the environment's;
3838
* - CONTROL, the same flows shipping a name nobody holds: `200`,
39-
* `hotLoaded: true`, the new version registered and ledgered.
39+
* `hotLoaded: true`, the new version registered and ledgered;
40+
* - #31392, the cloud-fetched branch with the other two holders: a name
41+
* another installed package holds (a fresh install and an upgrade) and a
42+
* built-in name (`everyone`) answer the same `422` naming that holder,
43+
* nothing registered or ledgered, and a name nobody holds beside the
44+
* holding package still installs. Measured before the fix: `200`,
45+
* `hotLoaded: false`, ledgered.
4046
*
4147
* A capability is code-only (`allowRuntimeCreate: false`), so no environment
4248
* save produces one; its case pins that the door judges the refusal, not the
@@ -48,6 +54,7 @@ import { mkdtempSync, rmSync } from 'node:fs';
4854
import { tmpdir } from 'node:os';
4955
import { join } from 'node:path';
5056
import { BaseResponseSchema, ApiErrorSchema, envelopeViolations } from '@objectstack/spec/api';
57+
import { EVERYONE_POSITION } from '@objectstack/spec/identity';
5158
// The plugin reaches `@objectstack/runtime` through a lazy `import()`; its first
5259
// load is paid here, at module top, never inside a clocked `it`.
5360
import '@objectstack/runtime';
@@ -284,3 +291,109 @@ describe('install-local refuses a package whose catalog name an environment-auth
284291
}
285292
}
286293
});
294+
295+
/** [#31392] The package already holding the catalog name, installed first through the same door's inline branch. */
296+
const HOLDER_ID = 'com.acme.dispatch-roles';
297+
298+
/** The registry's sentence for a held catalog name: the name, the incoming package, and `holder` as its holder. */
299+
function expectHeldElsewhereRefusal(res: any, name: string, holder: string) {
300+
expect(res.status, JSON.stringify(res.payload)).toBe(422);
301+
expect(BaseResponseSchema.safeParse(res.payload).success).toBe(true);
302+
expect(envelopeViolations(res.payload)).toEqual([]);
303+
expect(ApiErrorSchema.safeParse(res.payload.error).success).toBe(true);
304+
expect(res.payload.success).toBe(false);
305+
expect(res.payload.error.code).toBe('PLUGIN_REGISTER_FAILED');
306+
expect(res.payload.error.message).toContain(`package "${APP_ID}"`);
307+
expect(res.payload.error.message).toContain(`"${name}" is already held by ${holder}`);
308+
}
309+
310+
describe('install-local cloud branch refuses a package whose catalog name another package or a built-in holds, before any write (#31392)', () => {
311+
const k = CASES[0];
312+
const cloud = BRANCHES[1];
313+
const versions = {
314+
'v1': packageVersion(k, '1.0.0', [k.shipped]),
315+
'v2-collides': packageVersion(k, '2.0.0', [k.shipped, k.cloned]),
316+
'v2-free': packageVersion(k, '2.0.0', [k.shipped, k.free]),
317+
'v1-built-in': {
318+
id: APP_ID,
319+
name: 'acme_field_service',
320+
type: 'app',
321+
version: '1.0.0',
322+
positions: [{ name: EVERYONE_POSITION, label: 'Everyone at Acme' }],
323+
},
324+
};
325+
const holder: Bag = {
326+
id: HOLDER_ID,
327+
name: 'acme_dispatch_roles',
328+
type: 'app',
329+
version: '1.0.0',
330+
[k.collection]: [{ ...item(k, k.cloned), label: 'held by the roles package' }],
331+
};
332+
333+
async function bootWithHolder() {
334+
cloud.serve(versions);
335+
const booted = await bootPlugin(cloud.controlPlaneUrl);
336+
const held = await booted.install({ manifest: holder });
337+
expect(held.status, JSON.stringify(held.payload)).toBe(200);
338+
return booted;
339+
}
340+
const ledgerOf = (id: string) => new LocalManifestSource(dir).read(id).entry;
341+
function expectHolderItemKept(ql: ObjectQL) {
342+
expect(ql.registry.getItem<Bag>(k.type, k.cloned)?.label).toBe('held by the roles package');
343+
expect(ql.registry.getPackage(HOLDER_ID)?.manifest?.version).toBe('1.0.0');
344+
}
345+
346+
it('a fresh install of a name another package holds is refused: 422 naming the holder, nothing registered or ledgered', async () => {
347+
const { ql, install } = await bootWithHolder();
348+
const holderEntry = ledgerOf(HOLDER_ID);
349+
350+
const res = await install(cloud.body(versions, 'v2-collides'));
351+
352+
expectHeldElsewhereRefusal(res, k.cloned, `package "${HOLDER_ID}"`);
353+
expect(res.payload.data).toBeUndefined();
354+
expect(ql.registry.getPackage(APP_ID)).toBeUndefined();
355+
expect(ledgerOf(APP_ID)).toBeNull();
356+
expect(ledgerOf(HOLDER_ID)).toEqual(holderEntry);
357+
expectHolderItemKept(ql);
358+
});
359+
360+
it('an upgrade to a name another package holds is refused: 422, the installed version still registered and ledgered', async () => {
361+
const { ql, install } = await bootWithHolder();
362+
const first = await install(cloud.body(versions, 'v1'));
363+
expect(first.status, JSON.stringify(first.payload)).toBe(200);
364+
expect(first.payload.data.hotLoaded).toBe(true);
365+
const entry = ledgerOf(APP_ID);
366+
367+
const res = await install(cloud.body(versions, 'v2-collides'));
368+
369+
expectHeldElsewhereRefusal(res, k.cloned, `package "${HOLDER_ID}"`);
370+
expect(ql.registry.getPackage(APP_ID)?.manifest?.version).toBe('1.0.0');
371+
expect(ledgerOf(APP_ID)).toEqual(entry);
372+
expectHolderItemKept(ql);
373+
});
374+
375+
it('a fresh install of a built-in name is refused: 422 naming the platform, nothing registered or ledgered', async () => {
376+
cloud.serve(versions);
377+
const { ql, install } = await bootPlugin(cloud.controlPlaneUrl);
378+
379+
const res = await install(cloud.body(versions, 'v1-built-in'));
380+
381+
expectHeldElsewhereRefusal(res, EVERYONE_POSITION, 'the platform');
382+
expect(ql.registry.getPackage(APP_ID)).toBeUndefined();
383+
expect(ledgerOf(APP_ID)).toBeNull();
384+
});
385+
386+
it('CONTROL — beside the holder, an upgrade shipping a name nobody holds installs: 200, hot-loaded, ledgered', async () => {
387+
const { ql, install } = await bootWithHolder();
388+
expect((await install(cloud.body(versions, 'v1'))).status).toBe(200);
389+
390+
const res = await install(cloud.body(versions, 'v2-free'));
391+
392+
expect(res.status, JSON.stringify(res.payload)).toBe(200);
393+
expect(res.payload.data.hotLoaded).toBe(true);
394+
expect(res.payload.data.hotLoadError).toBeUndefined();
395+
expect(ql.registry.getPackage(APP_ID)?.manifest?.version).toBe('2.0.0');
396+
expect(ledgerOf(APP_ID)?.version).toBe('2.0.0');
397+
expectHolderItemKept(ql);
398+
});
399+
});

0 commit comments

Comments
 (0)