Skip to content

Commit bdfed89

Browse files
fix(metadata-protocol): the customizations read lists a moved edit under the package that serves it (#31668)
<!-- claude-projects-attribution: {"github_login":"os-steve"} --> _Requested by **Steve** · [project thread](https://claude.ai/code/project/chan_01FdL1XeUyvVNxLreh3F4Qi9?thread=cmsg_01FdL1XeUyvVNxLreh3F4Qi9W9WqZzLRELZak23ZMvZcMR)_ Fixes #31656 Clause-②: no — what the customizations list holds for a moved edit; no accept set changes Metadata-protocol: none ## 维护者速读 - 改了什么:套件拆分(或回滚)把条目挪到同一 App 的另一个包后,"包的定制列表"(`GET /packages/:id/customizations`)现在把租户的修改列在实际生效的新包下,且只列在那里。 - 为什么改:#31593 之后修改已在新包下生效,但列表仍按修改时声明的旧包来列:拆包后列在已不再发运该条目的 App 下、新模块下却没有;回滚后两边都列不出。列表改为读取与生效读取同一组候选与判定。 - 风险与回滚:只读,不写任何数据;"同一 App"仍只由安装记录(`appOf`)判定。条目锁本就读取该条目的全部存储行,实测对挪动的修改与在新包下保存的修改给出相同答案,未改,只加了钉。回滚即还原本 PR。 - 先看:`packages/metadata-protocol/src/protocol-served-overrides.ts`(逐条目按候选顺序选行,以及"旧包不再列"的排除)。 - 两分钟验证:`pnpm --filter @objectstack/runtime exec vitest run src/domains/meta-overlay-follows-suite.test.ts`(需先构建 metadata-protocol),预期 12 个全绿。 - 需要你做的:无,席位落地。 ## What changes **As measured on `origin/main` after #31653.** The card read "listed under neither". Measured, it depends on the direction: after a flat → composed split the pair override declaring the App was still listed under the App (which no longer ships the item) and not under the module that serves it; after a composed → flat rollback it was listed under neither. Both are fixed. **The list reads the served reads' own candidates, one reading (seat ruling 1).** `listServedOverrides` groups the pair's ACTIVE rows by item (folded type, name, bundle member) and picks each item's row through `servedOverlayRowCandidates` and `holdsServedOverlayCandidate`, in their order, with the item's shipping packages read the way the by-name read reads them (`shippingPackageIds(shippedArtifactsOf(...))`). So a package lists the pair's declared override of its item first, else the pair's override that `isMovedOverrideRowOf` holds as a moved edit. Same App is read only inside the predicates (`appOf` over installed manifests); no key list and no same-App test of its own. **Listed only where it is served (ruling 2).** A declared override whose package no longer ships the item, which a package shipping it now holds as its moved edit (`isMovedOverrideRowOf`), is that package's and is not also listed under the former owner. A former owner that still ships the item keeps its edit, listed as today. **The list stays the pair's rows.** A row bound to a package of the App itself (a Regime O save from before the ADR-0146 D11 close) is served at the new owner through `isMovedBaseRowOf`'s candidate, but it is not a pair row: it is listed under no package, before or after the move, as such a row bound to its still-shipping owner is not listed today. Named in the report's open questions. Since the list reads no base-bound rows, "two former owners' rows, list neither" has no listed case; the served reads already serve neither. **Item lock (ruling 3): measured, unchanged.** The lock's overlay layer reads every ACTIVE row of the item whichever package it is bound to (`overlayLockLayerAt`, no package filter), so a moved edit was already in its scope. A pin holds that a moved edit carrying a stamped lock answers at the module exactly what the module's own edit answers: the read's `lock`/`editable`/`deletable`/`lockReason` and the save door's `ITEM_LOCKED`. No save door refusal changes. Files: `protocol-served-overrides.ts`, `protocol.ts` (one host member, `shippedArtifactsOf`, passed to the list), the test, a patch changeset. `protocol.ts` is outside the delta's named surface: the host-split pattern needs the member wired there, as #31653 did for the by-name read. No `packages/spec/**`, no `ui/**`, no `docs/adr/**`; `served-override.ts` and `item-lock.ts` are not edited. No forced test edits. ## Tests Six cases added to `packages/runtime/src/domains/meta-overlay-follows-suite.test.ts` (the suite-move harness #31653 added: one SQLite tenant DB, a fresh real kernel per version), as a top-level `describe`: 1. flat → composed: the module lists the edit, the App does not. 2. composed → flat (rollback): the App lists the edit, the uninstalled module does not. 3. CONTROL: a former owner that still ships the view lists its edit as today; the module lists nothing. 4. NEGATIVE: another App's pair declaring HotCRM's App, and HotCRM's pair declaring the partner App: never listed under the module, the App or the partner. 5. CONTROL: a pre-close row bound to the App is served at the module and listed under neither, before or after the move. 6. The item lock answers for a moved edit at the module what it answers for the module's own edit (pinned, holds on main). <details><summary>Red on main first (529e535, fix sources restored from main, tests kept): 2 failed | 10 passed (12)</summary> ``` × flat → composed: the module lists the edit saved under the App, and the App no longer does AssertionError: expected { module: [], app: [ [ …(2) ] ] } to deeply equal { module: [ [ …(2) ] ], app: [] } (received app: [["x3677e_case.default", "6.1.0"]], module: []) × composed → flat (rollback): the App lists the edit saved under the module, though the module is gone AssertionError: expected { app: [], module: [] } to deeply equal { app: [ [ …(2) ] ], module: [] } Tests 2 failed | 10 passed (12) ``` The lock pin passes on main: the lock half of the premise does not hold. </details> <details><summary>Ablations (fix committed first; scripts/ablation-replace.mjs, dist preflight, restore proven blob == HEAD each leg)</summary> The runtime test reads `@objectstack/metadata-protocol` through its `dist/`, so each leg rebuilt it; `ablation-dist-preflight.mjs` found the marker in 2 built files before the run and in none of 24 after the restore build, with the tree clean. The legs ran on a local commit over #31653's branch head `0fad163d` (identical to its squash on main for every file it touched) whose `protocol-served-overrides.ts` blob `0b23b796` is the one at this PR's head. - **A. Moved candidate not read by the list** (blob `0b23b796` → `2f6f269e`): `Tests 2 failed | 10 passed (12)`, cases 1 and 2. Restored `0b23b796` == HEAD. - **B. Former-owner exclusion removed** (→ `a53e1f98`): `Tests 1 failed | 11 passed (12)`, case 1, the App lists the moved edit too. Restored == HEAD. - **C. Moved candidate admitted without its predicate** (→ `7dfc0eba`): `Tests 2 failed | 10 passed (12)`, cases 3 and 4, the module lists the App's still-shipped edit and the pair row declaring the partner App. Restored == HEAD. Case 5 and the lock pin hold on main and guard behaviour this diff does not change, so no ablation is owed for them. </details> ## Acceptance notes - The package upgrade plan's override advice (`listOverrides` in `@objectstack/objectql`, #31627) reads this list, so it follows the same rule; its tests pass unchanged (`package-upgrade-environment.test.ts`). - Lock prose ranking: among several rows declaring the strictest lock, the row bound to the address's own package ranks first. A moved base row is bound to the former owner, so it ranks as another package's row; the lock value is unaffected, and a pair row ranks the same moved or not. Not changed here. ## Verification Head `a135e658` (branch `claude/project-thread-o96evq`): fix `28e7e921` cut from `origin/main` `529e5351` after #31653 merged, then `origin/main` merged in at `a135e658` (merge, no rebase; the two incoming commits touch `plugin-auth` and `metadata`, not this surface). - **Build:** `pnpm install` 0; `pnpm build` 119/119 (on `28e7e921`); `turbo build --filter ./packages/**` 81/81 after the merge. - **Tests:** the suite-move file 12/12, with `packages-customizations`, `dispatcher-plugin.package-customizations-mount` and `meta-legacy-pair-served`: 4 files / 41 tests passed on `a135e658`. `@objectstack/metadata-protocol` 328 files / 22595 tests passed (4 files / 26 tests skipped) on `28e7e921`. `@objectstack/objectql` `package-upgrade-environment` and `protocol-meta` 101 passed. Heavy runs through `os-verify-lock.sh` (VERDICT command-exit 0). - **Typecheck:** metadata-protocol 0, runtime 0 (runtime's `check:test-typecheck` first reported `organization-ownership-boot-guard.turso-remote.kernel.test.ts`, a known prerequisite: the optional peer `driver-turso` had no dist; built, re-run 0). - **Lint:** eslint `--no-inline-config` over the 2 changed sources and the test, exit 0. - **Gates:** `dispatch-gates --commands` derived 72 families; all 72 run on `a135e658`, each exit 0 (also on `28e7e921`, where two first exited 3 on container prerequisites: the teardown self-test's fixture commit, fetched; `dual-build-cjs-loads` needs every dist, after `pnpm build` 0). `dispatch-gates --ran`: 72 derived, 72 run, 0 NOT-MEASURED, 0 UNRUN. `check-platform-checklist --changed-from origin/main`: OK, 0 lost. - **Dogfood:** not owed (no `!` in the title, no narrowing arm). - **`test:repo`:** declared to CI, not run locally. --- _Generated by [Claude Code](https://claude.ai/code/session_01ADHkmPHUhgip3YbMfQh27s)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9bf3286 commit bdfed89

4 files changed

Lines changed: 190 additions & 23 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/metadata-protocol': patch
3+
---
4+
5+
fix(metadata-protocol): the customizations read lists a moved edit under the package that serves it
6+
7+
Clause-②: no — what the customizations list holds for a moved edit; no accept set changes
8+
Metadata-protocol: none
9+
10+
When a later version of an App moves an item from one of its packages to another (an ADR-0130 D4 suite split, or its rollback), the tenant's edit of the item is served at the package that ships it now. `GET /packages/:id/customizations` (`listServedOverrides`) now lists that edit there too, and only there.
11+
12+
- **Before.** The list took a pair override's package from its declaration, the owner at the time of the edit. After a flat → composed split the edit was listed under the App, which no longer ships the item, and not under the module that serves it; after a rollback it was listed under neither.
13+
- **Now.** The list picks each item's row through the served reads' own candidates and predicates (`servedOverlayRowCandidates`, `holdsServedOverlayCandidate`, `isMovedOverrideRowOf`), so a package lists exactly the pair override its address serves. Same App is read only from the install record (`appOf` over the installed manifests), never from an id prefix; a row of another App's pair, or a declaration naming another App, is never listed.
14+
- **Unchanged.** An edit whose owner still ships the item is listed as before. The list stays the pair's rows: a row bound to a package of the App itself (saved before ADR-0146 D11) is listed under no package, before or after a move. The upgrade plan's override advice reads this list, so it follows the same rule. The item lock already reads every stored row of the item whichever package it is bound to, so it answers for a moved edit what it answers for an edit saved at the new owner; that is pinned, not changed.
15+
- **Nothing is written.** The read moves and rewrites no row.

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

Lines changed: 81 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@
1313
*
1414
* ## One predicate, not a second spelling
1515
*
16-
* A row is listed exactly when {@link isServedOverrideRowOf} holds for it and
17-
* X — the predicate the effective read asks before it serves a `.custom` row
18-
* at X's address (`servedOverlayRowCandidates`, N-10). ⛔ Never a body-only
16+
* A row is listed exactly when it holds one of the pair's candidates for X in
17+
* the served reads' order — `isServedOverrideRowOf`, the predicate the
18+
* effective read asks before it serves a `.custom` row at X's address
19+
* (`servedOverlayRowCandidates`, N-10), or [#31656] the moved edit's
20+
* {@link isMovedOverrideRowOf} (below). ⛔ Never a body-only
1921
* test such as `overrides.package === X`: a declaration from a package that is
2022
* not X's pair, or a pair row with no declaration, is not served as X's item,
2123
* so it is not an override of X either. The agreement is pinned in
@@ -40,6 +42,27 @@
4042
*
4143
* ## What it reports, and what it never computes
4244
*
45+
* ## [#31656] A moved edit is listed where it is served now, and only there
46+
*
47+
* A row is admitted through the served reads' own candidates, in their order
48+
* ({@link servedOverlayRowCandidates}, {@link holdsServedOverlayCandidate}):
49+
* the pair's declared override of X first, then the pair's override of the
50+
* item from before a later version of X's App moved it to X
51+
* ({@link isMovedOverrideRowOf}, `suite-moved-overlay.ts`). So after a suite
52+
* split (or its rollback) the tenant's edit is listed under the package that
53+
* ships the item now, the one whose address serves it. ⛔ It is no longer
54+
* listed under the former owner whose name the declaration still carries: a
55+
* pair row that a package shipping the item now holds as a moved edit is that
56+
* package's. Same App is read only through the predicates (`appOf` over the
57+
* installed manifests), never here. Which packages ship the item is the
58+
* registry's answer the served by-name read takes too (`shippedArtifactsOf`).
59+
*
60+
* The list stays the pair's rows. A row bound to a package of the App itself
61+
* (a Regime O save from before the ADR-0146 D11 close) is served at the new
62+
* owner through `isMovedBaseRowOf`'s candidate, last, but it is not a
63+
* pair row, so it is listed under no package, as such a row bound to the
64+
* package that still ships the item is not listed today.
65+
*
4366
* Each entry is the row's own facts: its type (folded to the canonical
4467
* singular), its name, its bundle member when the type's identity is a pair,
4568
* the `basedOn` its declaration records and the row's history `version`.
@@ -55,14 +78,17 @@ import { pairsOf } from '@objectstack/metadata-core';
5578
import { PLURAL_TO_SINGULAR } from '@objectstack/spec/shared';
5679
import { storedRowDocument } from './item-lock.js';
5780
import type { ObjectStackProtocolImplementation } from './protocol.js';
58-
import { isServedOverrideRowOf } from './served-override.js';
81+
import { holdsServedOverlayCandidate, servedOverlayRowCandidates } from './protocol-overlay-merge.js';
82+
import { shippingPackageIds } from './protocol-environment-rows.js';
83+
import { isMovedOverrideRowOf } from './suite-moved-overlay.js';
5984
import { installedManifestsOfEngine } from './installed-manifests.js';
6085

6186
/** The class members {@link listServedOverrides} reads, typed as the class declares them. */
6287
export interface ServedOverridesHost {
6388
readonly engine: ObjectStackProtocolImplementation['engine'];
6489
readonly findEnvironmentRows: ObjectStackProtocolImplementation['findEnvironmentRows'];
6590
readonly declinesStoredRow: ObjectStackProtocolImplementation['declinesStoredRow'];
91+
readonly shippedArtifactsOf: ObjectStackProtocolImplementation['shippedArtifactsOf'];
6692
readonly rethrowUnlessMetadataStoreUnprovisioned: ObjectStackProtocolImplementation['rethrowUnlessMetadataStoreUnprovisioned'];
6793
}
6894

@@ -81,8 +107,10 @@ export interface ServedOverrideEntry {
81107
}
82108

83109
/**
84-
* The served overrides of `request.packageId`'s items: the ACTIVE rows of its
85-
* pair that {@link isServedOverrideRowOf} admits and no read declines, ordered
110+
* The served overrides of `request.packageId`'s items: per item, the ACTIVE
111+
* row of its pair that the served reads' candidates pick (the declared
112+
* override, else [#31656] the moved edit, module header) and no read
113+
* declines, ordered
86114
* by type, name and member. A base with no pair package, or a package id that
87115
* has no pair (a `.custom` id, an invalid id), answers `[]`; this read never
88116
* creates the pair.
@@ -93,7 +121,7 @@ export interface ServedOverrideEntry {
93121
* An unprovisioned table answers `[]`.
94122
*/
95123
export async function listServedOverrides(
96-
this: Pick<ServedOverridesHost, 'engine' | 'findEnvironmentRows' | 'declinesStoredRow' | 'rethrowUnlessMetadataStoreUnprovisioned'>,
124+
this: Pick<ServedOverridesHost, 'engine' | 'findEnvironmentRows' | 'declinesStoredRow' | 'shippedArtifactsOf' | 'rethrowUnlessMetadataStoreUnprovisioned'>,
97125
request: { packageId: string },
98126
): Promise<ServedOverrideEntry[]> {
99127
const base = request.packageId;
@@ -114,31 +142,61 @@ export async function listServedOverrides(
114142
// overridden".
115143
this.rethrowUnlessMetadataStoreUnprovisioned(error, 'sys_metadata');
116144
}
117-
const entries = new Map<string, ServedOverrideEntry>();
145+
// [#31656] The pair's rows of one item (type folded, name, member) are
146+
// judged together: the served reads pick one of them by candidate order.
147+
const items = new Map<string, { type: string; name: string; member: string | undefined; rows: any[] }>();
118148
for (const row of rows ?? []) {
119149
if (typeof row?.name !== 'string' || row.name === '') continue;
120-
if (!isServedOverrideRowOf(row, base, installed)) continue;
121150
const type = PLURAL_TO_SINGULAR[row.type] ?? row.type;
122-
if (this.declinesStoredRow(type, row.name, row)) continue;
151+
if (typeof type !== 'string' || type === '') continue;
123152
const member = typeof row.bundle_member === 'string' && row.bundle_member !== '' ? row.bundle_member : undefined;
124-
// `isServedOverrideRowOf` parsed this body already; a body it admitted parses.
125-
const declaration = (storedRowDocument(row) as { overrides?: { basedOn?: unknown } }).overrides;
126-
const version = typeof row.version === 'number' && Number.isInteger(row.version) && row.version >= 0
127-
? row.version
153+
const key = `${type}\u0000${row.name}\u0000${member ?? ''}`;
154+
const item = items.get(key) ?? { type, name: row.name as string, member, rows: [] as any[] };
155+
item.rows.push(row);
156+
items.set(key, item);
157+
}
158+
const entries: ServedOverrideEntry[] = [];
159+
for (const { type, name, member, rows: itemRows } of items.values()) {
160+
// The code packages that ship the item, read once per item and only
161+
// when a moved edit is in question (the by-name read's own reading).
162+
let shippers: ReadonlySet<string> | undefined;
163+
const shippersOf = (): ReadonlySet<string> =>
164+
(shippers ??= shippingPackageIds(this.shippedArtifactsOf(type, name, undefined, member)));
165+
const ships = (packageId: string): boolean => shippersOf().has(packageId);
166+
let served: any;
167+
for (const candidate of servedOverlayRowCandidates({ packageId: base, servedOverride: true, installed, ships })) {
168+
// Only the pair's candidates: the base's own row, the
169+
// package-less row and a row bound to a former owner are not
170+
// the pair's, and this list reads the pair's rows alone.
171+
if (candidate.overrideOf === undefined || candidate.moved === 'base') continue;
172+
served = itemRows.find((row) => (row.type === type ? 'canonical' : 'other') === candidate.spelling
173+
&& row.package_id === candidate.packageId
174+
&& holdsServedOverlayCandidate(candidate, row, installed, ships));
175+
if (served === undefined) continue;
176+
// A declared override the base no longer ships, which a package
177+
// shipping the item now holds as its moved edit, is that
178+
// package's: listed there, never also here.
179+
if (candidate.moved === undefined && !ships(base)
180+
&& [...shippersOf()].some((now) => now !== base && isMovedOverrideRowOf(served, now, installed, ships))) {
181+
served = undefined;
182+
}
183+
break;
184+
}
185+
if (served === undefined) continue;
186+
if (this.declinesStoredRow(type, name, served)) continue;
187+
// The candidate's predicate parsed this body already; a body it admitted parses.
188+
const declaration = (storedRowDocument(served) as { overrides?: { basedOn?: unknown } }).overrides;
189+
const version = typeof served.version === 'number' && Number.isInteger(served.version) && served.version >= 0
190+
? served.version
128191
: undefined;
129-
const entry: ServedOverrideEntry = {
192+
entries.push({
130193
type,
131-
name: row.name,
194+
name,
132195
...(member !== undefined ? { member } : {}),
133196
...(typeof declaration?.basedOn === 'string' ? { basedOn: declaration.basedOn } : {}),
134197
...(version !== undefined ? { version } : {}),
135-
};
136-
// A row stored under the type's other spelling (pre-#4432 residue)
137-
// and one under the canonical spelling are one item: listed once,
138-
// the canonical row's facts kept.
139-
const key = `${type}\u0000${row.name}\u0000${member ?? ''}`;
140-
if (!entries.has(key) || row.type === type) entries.set(key, entry);
198+
});
141199
}
142-
return [...entries.values()].sort((a, b) =>
200+
return entries.sort((a, b) =>
143201
a.type.localeCompare(b.type) || a.name.localeCompare(b.name) || (a.member ?? '').localeCompare(b.member ?? ''));
144202
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3443,6 +3443,7 @@ export class ObjectStackProtocolImplementation implements
34433443
get engine() { return readEngine(); },
34443444
findEnvironmentRows: (...a) => this.findEnvironmentRows(...a),
34453445
declinesStoredRow: (...a) => this.declinesStoredRow(...a),
3446+
shippedArtifactsOf: (...a) => this.shippedArtifactsOf(...a),
34463447
rethrowUnlessMetadataStoreUnprovisioned: (...a) => this.rethrowUnlessMetadataStoreUnprovisioned(...a),
34473448
};
34483449
return listServedOverrides.call(host, request);

‎packages/runtime/src/domains/meta-overlay-follows-suite.test.ts‎

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,3 +239,96 @@ describe('[#31593] a tenant\'s edit follows its view across the packages of one
239239
expect((r?.item ?? r)?.label).toBe('Case');
240240
}, 60_000);
241241
});
242+
243+
/** The views `packageId`'s customizations read lists as overridden, by name and recorded `basedOn`. */
244+
async function listedOverrides(s: Session, packageId: string) {
245+
const entries: Array<{ type: string; name: string; basedOn?: string }> = await s.protocol.listServedOverrides({ packageId });
246+
return entries.filter((e) => e.type === 'view').map((e) => [e.name, e.basedOn ?? null]);
247+
}
248+
/** What the item lock answers for the view at `packageId`'s address: the read's envelope, and the save door. */
249+
async function lockAt(s: Session, packageId: string) {
250+
const r: any = await s.protocol.getMetaItem({ type: 'view', name: VIEW, packageId });
251+
const save = await s.protocol.saveMetaItem({
252+
type: 'view', name: VIEW, packageId: PAIR, item: { ...(await editedView(s, packageId)), overrides: { package: packageId, basedOn: '6.2.0' } },
253+
}).then(() => 'saved', (e: any) => e?.code ?? String(e));
254+
return { lock: r?.lock, editable: r?.editable, deletable: r?.deletable, lockReason: r?.lockReason, save };
255+
}
256+
257+
describe('[#31656] the customizations read lists a moved edit under the package that serves it, and the item lock answers for it as for an edit there', () => {
258+
it('flat → composed: the module lists the edit saved under the App, and the App no longer does', async () => {
259+
const db = tenantDb();
260+
const flat = await boot(db, 'flat');
261+
await editInPair(flat, APP, '6.1.0');
262+
// CONTROL: listed under its own package, as before the move.
263+
expect(await listedOverrides(flat, APP)).toEqual([[VIEW, '6.1.0']]);
264+
265+
const composed = await boot(db, 'composed');
266+
expect(await composed.label(MODULE)).toBe(EDITED);
267+
expect({ module: await listedOverrides(composed, MODULE), app: await listedOverrides(composed, APP) })
268+
.toEqual({ module: [[VIEW, '6.1.0']], app: [] });
269+
}, 60_000);
270+
271+
it('composed → flat (rollback): the App lists the edit saved under the module, though the module is gone', async () => {
272+
const db = tenantDb();
273+
const composed = await boot(db, 'composed');
274+
await editInPair(composed, MODULE, '6.2.0');
275+
expect(await listedOverrides(composed, MODULE)).toEqual([[VIEW, '6.2.0']]);
276+
277+
const flat = await boot(db, 'flat');
278+
expect(await flat.label(APP)).toBe(EDITED);
279+
expect({ app: await listedOverrides(flat, APP), module: await listedOverrides(flat, MODULE) })
280+
.toEqual({ app: [[VIEW, '6.2.0']], module: [] });
281+
}, 60_000);
282+
283+
it('CONTROL: a former owner that still ships the view lists its edit as today; the module lists nothing', async () => {
284+
const db = tenantDb();
285+
const flat = await boot(db, 'flat');
286+
await editInPair(flat, APP, '6.1.0');
287+
const both = await boot(db, 'both');
288+
expect({ app: await listedOverrides(both, APP), module: await listedOverrides(both, MODULE) })
289+
.toEqual({ app: [[VIEW, '6.1.0']], module: [] });
290+
}, 60_000);
291+
292+
it('NEGATIVE: a row of another App\'s pair, or a declaration naming another App, is never listed under the module', async () => {
293+
const db = tenantDb();
294+
const flat = await boot(db, 'flat', { partner: true });
295+
const body = await editedView(flat, APP);
296+
const composed = await boot(db, 'composed', { partner: true });
297+
expect(((await composed.protocol.ensureCustomizationPackage({ basePackageId: PARTNER })) as any).packageId).toBe(`${PARTNER}.custom`);
298+
await composed.layRow('view', VIEW, `${PARTNER}.custom`, { ...body, overrides: { package: APP, basedOn: '6.1.0' } });
299+
expect(await listedOverrides(composed, MODULE)).toEqual([]);
300+
await composed.layRow('view', VIEW, PAIR, { ...body, overrides: { package: PARTNER, basedOn: '2.0.0' } });
301+
expect({ module: await listedOverrides(composed, MODULE), app: await listedOverrides(composed, APP), partner: await listedOverrides(composed, PARTNER) })
302+
.toEqual({ module: [], app: [], partner: [] });
303+
}, 60_000);
304+
305+
it('CONTROL: a pre-close row bound to the App is served at the module and listed under neither, as such a row bound to its owner is not listed today', async () => {
306+
const db = tenantDb();
307+
const flat = await boot(db, 'flat');
308+
await flat.layRow('view', VIEW, APP, await editedView(flat, APP));
309+
expect(await listedOverrides(flat, APP)).toEqual([]);
310+
const composed = await boot(db, 'composed');
311+
expect(await composed.label(MODULE)).toBe(EDITED);
312+
expect({ module: await listedOverrides(composed, MODULE), app: await listedOverrides(composed, APP) }).toEqual({ module: [], app: [] });
313+
}, 60_000);
314+
315+
it('the item lock answers for a moved edit at the module exactly what it answers for an edit saved at the module', async () => {
316+
// The lock a stamped row carries (a loader's or an install's stamp;
317+
// no save door writes one), on the module's own edit and on a moved one.
318+
const stamped = { _lock: 'no-overlay', _lockReason: 'Stamped by the install.' };
319+
const ownDb = tenantDb();
320+
const own = await boot(ownDb, 'composed');
321+
await own.layRow('view', VIEW, PAIR, { ...(await editedView(own, MODULE)), overrides: { package: MODULE, basedOn: '6.2.0' }, ...stamped });
322+
const atOwner = await lockAt(own, MODULE);
323+
324+
const movedDb = tenantDb();
325+
const flat = await boot(movedDb, 'flat');
326+
await flat.layRow('view', VIEW, PAIR, { ...(await editedView(flat, APP)), overrides: { package: APP, basedOn: '6.1.0' }, ...stamped });
327+
const composed = await boot(movedDb, 'composed');
328+
expect(await composed.label(MODULE)).toBe(EDITED);
329+
const moved = await lockAt(composed, MODULE);
330+
331+
expect(atOwner).toEqual({ lock: 'no-overlay', editable: false, deletable: true, lockReason: 'Stamped by the install.', save: 'ITEM_LOCKED' });
332+
expect(moved).toEqual(atOwner);
333+
}, 60_000);
334+
});

0 commit comments

Comments
 (0)