Skip to content

Commit ab5ba9e

Browse files
committed
wip: close tests, five-types re-judged
Claude-Session: https://claude.ai/code/session_01FEkQ5ni5yjiTswP6crgAkX Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 8c98b51 commit ab5ba9e

3 files changed

Lines changed: 165 additions & 49 deletions

File tree

‎packages/metadata-protocol/src/protocol-packaged-base-refusal.ts‎

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -250,18 +250,17 @@ export function packagelessOverlayWriteRefusal(
250250
const item = `'${type}/${request.name}'`;
251251
const owner = this.packagedArtifactOwner(request);
252252
const pairId = typeof owner === 'string' ? customizationPackageId(owner) : undefined;
253-
const pair = pairId?.ok ? `'${pairId.value}'` : 'the paired .custom package of the package that ships it';
253+
const pair = pairId?.ok ? `'${pairId.value}'` : "the base's paired .custom package";
254254
const query = pairId?.ok ? ` (?package=${pairId.value})` : '';
255255
const shipped = typeof owner === 'string' ? `ships in '${owner}'` : 'ships in an installed package';
256256
const declared = typeof owner === 'string' ? `overrides: { package: '${owner}', basedOn }` : 'overrides: { package, basedOn }';
257-
const ceremony = `A package-less customization stored before this release is still served: run \`os migrate packageless-overlays\` to move it into ${pair}`;
257+
const ceremony = 'A package-less customization stored before this release is still served; run '
258+
+ `\`os migrate packageless-overlays\` first to move it into ${pair}.`;
258259
const fix = operation === 'delete'
259-
? `Remove it from ${pair}${query}. ${ceremony} first.`
260+
? `Remove it from ${pair}${query}. ${ceremony}`
260261
: operation === 'rollback'
261-
? `${ceremony}, then roll it back there.`
262-
: operation === 'publish'
263-
? `Publish it into ${pair}${query}, declaring ${declared}. ${ceremony} first.`
264-
: `Save it into ${pair}${query}, declaring ${declared}. ${ceremony} first.`;
262+
? `Its customization belongs in ${pair}: run \`os migrate packageless-overlays\` to move the row there, then roll it back.`
263+
: `${operation === 'publish' ? 'Publish' : 'Save'} it into ${pair}${query}, declaring ${declared}. ${ceremony}`;
265264
const what = operation === 'rollback' ? 'a rollback of its package-less row' : 'a write naming no package';
266265
const err = new Error(
267266
`${item} ${shipped}, and ${what} onto a packaged item is closed: a customization of an installed package `

‎packages/objectql/src/environment-overlay-five-types.test.ts‎

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
11
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
22

33
/**
4-
* The five overlay-enabled metadata types keep their ENVIRONMENT overlay: a
5-
* packaged item of each can be overlaid environment-wide, the overlay is stored
6-
* with no organization, it is what the item's read then serves, and
7-
* `GET /meta/types` tells Studio the type is writable.
4+
* The five overlay-enabled metadata types keep their ENVIRONMENT customization:
5+
* a packaged item of each is customized environment-wide, the customization is
6+
* stored with no organization, it is what the item's read then serves, and
7+
* `GET /meta/types` still carries the type's flag.
8+
*
9+
* ## Re-judged for the migrated state (#28576, ADR-0146 D11)
10+
*
11+
* Since D11 closed the package-less write, the flag no longer opens a write
12+
* door: a save naming no package over the packaged item is refused
13+
* `403 NOT_OVERRIDABLE`, naming the base's paired `.custom` and the
14+
* `os migrate packageless-overlays` ceremony, and stores nothing. The
15+
* customization is the same save naming the pair (`?package=<base>.custom`),
16+
* declaring the override. The flag stays the predicate the migration reads
17+
* and the read of a row not migrated yet honours, so leg 2 below (a stored
18+
* package-less row is served) is unchanged.
819
*
920
* ## Why this file exists (#22340, stage S1)
1021
*
@@ -21,15 +32,17 @@
2132
*
2233
* ## What is pinned, per type and on both topologies
2334
*
24-
* 1. the write — `saveMetaItem` over the packaged item is accepted, stores
25-
* exactly one row, the row names no organization, and the item's read
35+
* 1. the write — `saveMetaItem` naming no package over the packaged item is
36+
* refused naming the pair, and stores nothing; the same save into the
37+
* pair, declaring the override, is accepted, stores exactly one row,
38+
* bound to the pair, the row names no organization, and the item's read
2639
* serves the body that was written;
2740
* 2. the read of a stored overlay — a stored environment-wide row is served
2841
* by `getMetaItem`. The row is inserted through the engine, never a write
2942
* door, so this leg asks the read alone (the engine, not the driver, so
3043
* the served read's cache sees the write as it sees every production one): a row stored before a
3144
* flag change keeps being served whatever the flag then says;
32-
* 3. the wire — `getMetaTypes` advertises the flag `true` for the type, from
45+
* 3. the wire — `getMetaTypes` carries the flag `true` for the type, from
3346
* the registry and not from the `OS_METADATA_WRITABLE` hatch.
3447
*
3548
* And the identity: the registry types whose flag is `true` are exactly these
@@ -85,7 +98,10 @@ import { ObjectStackProtocolImplementation, resetEnvWritableMetadataTypes } from
8598
import { SysMetadataAuditObject, SysMetadataCommitObject, SysMetadataHistoryObject } from '@objectstack/metadata-core';
8699
import { ObjectQL } from './engine.js';
87100

88-
const PKG = 'com.acme.overlay_catalog';
101+
const PKG = 'com.acme.overlaycatalog';
102+
/** The base's paired customization package (ADR-0146 D3), where its customization lives. */
103+
const PAIR = 'com.acme.overlaycatalog.custom';
104+
const PKG_VERSION = '1.0.0';
89105
const PACKAGED_LABEL = 'Packaged';
90106
const OVERLAY_LABEL = 'Environment overlay';
91107

@@ -135,6 +151,7 @@ function overlayCatalogManifest(): Record<string, unknown> {
135151
return {
136152
id: PKG,
137153
name: 'overlay_catalog',
154+
version: PKG_VERSION,
138155
// [#24668] The object the view and the dataset bind to, shipped by the
139156
// package: the overlay save door refuses a list view bound to an object
140157
// that exists nowhere, and `probe_obj` was declared by nothing.
@@ -333,7 +350,7 @@ afterEach(() => {
333350
resetEnvWritableMetadataTypes();
334351
});
335352

336-
describe('the five overlay-enabled types keep their environment overlay of a packaged item', () => {
353+
describe('the five overlay-enabled types keep their environment customization of a packaged item, in its pair (ADR-0146 D11)', () => {
337354
it('the registry opens an overlay on exactly these five types', () => {
338355
const flagged = DEFAULT_METADATA_TYPE_REGISTRY
339356
.filter((entry) => entry.allowEnvironmentOverlay === true)
@@ -343,19 +360,32 @@ describe('the five overlay-enabled types keep their environment overlay of a pac
343360
});
344361

345362
for (const topology of TOPOLOGIES) {
346-
it.each(OVERLAY_ENABLED)(`${topology}: a save over the packaged %s is accepted, stored with no organization, and served back`, async (type) => {
363+
it.each(OVERLAY_ENABLED)(`${topology}: a package-less save over the packaged %s is refused naming its pair; the same save into the pair is accepted, stored with no organization, and served back`, async (type) => {
347364
const { engine, protocol, rowsOf } = await boot(topology);
348365
const { name, body } = ITEMS[type];
349-
// Only an overlay if a package ships the item it lands on.
366+
// Only a customization if a package ships the item it lands on.
350367
expect(engine.registry.getArtifactItem<any>(type, name)?._packageId).toBe(PKG);
351368

352-
const result: any = await protocol
369+
const refused: any = await protocol
353370
.saveMetaItem({ type, name, item: body(OVERLAY_LABEL) })
354-
.catch((e: any) => ({ refused: { code: e?.code, status: e?.status } }));
371+
.then(() => undefined, (e: any) => ({ code: e?.code, status: e?.status, message: String(e?.message ?? '') }));
372+
expect(refused).toMatchObject({ code: 'NOT_OVERRIDABLE', status: 403 });
373+
expect(refused.message).toContain(`'${PAIR}'`);
374+
expect(refused.message).toContain('os migrate packageless-overlays');
375+
expect(rowsOf(type, name)).toHaveLength(0);
376+
377+
await (protocol as any).ensureCustomizationPackage({ basePackageId: PKG });
378+
const result: any = await protocol
379+
.saveMetaItem({
380+
type, name, packageId: PAIR,
381+
item: { ...body(OVERLAY_LABEL), overrides: { package: PKG, basedOn: PKG_VERSION } },
382+
})
383+
.catch((e: any) => ({ refused: { code: e?.code, status: e?.status, message: e?.message } }));
355384

356385
expect(result).toMatchObject({ success: true });
357386
const rows = rowsOf(type, name);
358387
expect(rows).toHaveLength(1);
388+
expect(rows[0].package_id).toBe(PAIR);
359389
// NULL is how the repository writes "no organization" today; a
360390
// tenant-less row with no such column reads the same here.
361391
expect(rows[0].organization_id ?? null).toBeNull();
@@ -378,7 +408,7 @@ describe('the five overlay-enabled types keep their environment overlay of a pac
378408
});
379409
}
380410

381-
it.each(OVERLAY_ENABLED)('GET /meta/types advertises %s as overlay-writable, from the registry', async (type) => {
411+
it.each(OVERLAY_ENABLED)('GET /meta/types carries the overlay flag on %s, from the registry (the migration\'s and the unmigrated read\'s predicate)', async (type) => {
382412
const { protocol } = await boot('environment-scoped');
383413

384414
const result: any = await protocol.getMetaTypes();

0 commit comments

Comments
 (0)