Skip to content

Commit 6c52a63

Browse files
committed
test(spec,objectql): join the two retired page entries to their release-naming prescriptions and repin the stored-row save-door reads
The misplaced-isDefault refusal now names the release that first refused the flag (@objectstack/spec 18), as the removal-version pin requires of every freshly retired D2 entry; the save-door test reads a stored row back through the conversion chain. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016MPJ3Y2xUTT42wq9ZiK3MR
1 parent 9fad187 commit 6c52a63

3 files changed

Lines changed: 44 additions & 13 deletions

File tree

‎packages/objectql/src/default-record-page-save-door.test.ts‎

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -297,12 +297,18 @@ describe.each(TOPOLOGIES)('%s kernel — a page saved as a second default record
297297
// is the page parse's: a non-record page's `isDefault: true`, the retired
298298
// `full-width` template and a region the template does not place are each
299299
// refused 422 `INVALID_METADATA` at save. A row ALREADY stored in one of those
300-
// shapes is read as it was (no conversion touches it and the read path does
301-
// not refuse it); saving it again is refused until it is fixed.
302-
const PLACEMENT_SHAPES: ReadonlyArray<[label: string, body: Record<string, unknown>, says: string]> = [
303-
['a home page with `isDefault: true`', { name: 'env_shape', label: 'Shape', type: 'home', isDefault: true, regions: [] }, '`isDefault: true` is refused'],
304-
['the retired `full-width` template', { name: 'env_shape', label: 'Shape', type: 'record', object: 'acme_task', template: 'full-width', regions: [{ name: 'main', components: [] }] }, "`template: 'full-width'` was removed"],
305-
['a `left_sidebar` region on `three-column`', { name: 'env_shape', label: 'Shape', type: 'home', template: 'three-column', regions: [{ name: 'left_sidebar', components: [] }, { name: 'main', components: [] }] }, 'Region `left_sidebar` is refused'],
300+
// shapes is read back through the stored-row conversion chain (the read path
301+
// never refuses it): the two shapes with a lossless rewrite come back
302+
// CONVERTED — the retired D2 entries `page-is-default-off-record-page-removed`
303+
// and `page-template-full-width-to-default` delete the flag and re-spell the
304+
// template — while the unplaced region, which has no lossless rewrite, is
305+
// served as stored. Saving the row as it was stored is refused until it is
306+
// fixed, whichever way it reads back. `served` is what the read answers for
307+
// the keys the shape varies.
308+
const PLACEMENT_SHAPES: ReadonlyArray<[label: string, body: Record<string, unknown>, says: string, served: Record<string, unknown>]> = [
309+
['a home page with `isDefault: true`', { name: 'env_shape', label: 'Shape', type: 'home', isDefault: true, regions: [] }, '`isDefault: true` is refused', { isDefault: undefined, regions: [] }],
310+
['the retired `full-width` template', { name: 'env_shape', label: 'Shape', type: 'record', object: 'acme_task', template: 'full-width', regions: [{ name: 'main', components: [] }] }, "`template: 'full-width'` was removed", { template: 'default', regions: [{ name: 'main', components: [] }] }],
311+
['a `left_sidebar` region on `three-column`', { name: 'env_shape', label: 'Shape', type: 'home', template: 'three-column', regions: [{ name: 'left_sidebar', components: [] }, { name: 'main', components: [] }] }, 'Region `left_sidebar` is refused', { template: 'three-column', regions: [{ name: 'left_sidebar', components: [] }, { name: 'main', components: [] }] }],
306312
];
307313

308314
describe.each(TOPOLOGIES)('%s kernel — the page parse at the save door, and rows stored before it (#23610)', (topology) => {
@@ -316,15 +322,14 @@ describe.each(TOPOLOGIES)('%s kernel — the page parse at the save door, and ro
316322
expect(storedPages()).toEqual([]);
317323
});
318324

319-
it.each(PLACEMENT_SHAPES)('%s, ALREADY stored: the read serves the row as stored, and saving it again is refused', async (_label, body) => {
325+
it.each(PLACEMENT_SHAPES)('%s, ALREADY stored: the read serves the row through the stored-row conversion chain, and saving it as stored is refused', async (_label, body, _says, served) => {
320326
const { protocol, save, storeRow } = await boot(topology);
321327
await storeRow('env_shape', body);
322328
const read: any = await protocol.getMetaItem({ type: 'page', name: 'env_shape' });
323329
expect(read?.item?.name).toBe('env_shape');
324-
for (const key of ['isDefault', 'template'] as const) {
325-
if (key in body) expect(read.item[key]).toEqual(body[key]);
330+
for (const [key, value] of Object.entries(served)) {
331+
expect(read.item[key], key).toEqual(value);
326332
}
327-
expect(read.item.regions).toEqual(body.regions);
328333
const again = await save('env_shape', body);
329334
expect(again.ok, 'the re-save was refused').toBe(false);
330335
expect((again as { error: any }).error?.status).toBe(422);

‎packages/spec/src/shared/retired-key-removal-version.pin.test.ts‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,28 @@ const JOIN: Readonly<Record<string, readonly Renderer[]>> = {
334334
],
335335
// #23650: retired after 17.7.0, so the v18 train's major.
336336
'page-header-show-star-removed': [tombstone(PageHeaderProps, 'page:header', 'showStar')],
337+
// #23904: the two data-at-rest halves of the #23610 page refusals, retired
338+
// after 17.7.0; each prescription is the parse's own refusal at its key — the
339+
// one sentence the registry door prints too — and names the release.
340+
'page-is-default-off-record-page-removed': [
341+
refusal(
342+
"isDefault: true on a type 'home' page",
343+
() => PageSchema.safeParse({ name: 'sales_home_page', label: 'Sales Home', type: 'home', isDefault: true, regions: [] }),
344+
'isDefault',
345+
),
346+
refusal(
347+
'isDefault: true on a record page bound to no object',
348+
() => PageSchema.safeParse({ name: 'draft_record', label: 'Draft', type: 'record', isDefault: true, regions: [] }),
349+
'isDefault',
350+
),
351+
],
352+
'page-template-full-width-to-default': [
353+
refusal(
354+
"page template 'full-width'",
355+
() => PageSchema.safeParse({ name: 'lead_detail_page', label: 'Lead', type: 'record', object: 'crm_lead', template: 'full-width', regions: [{ name: 'main', components: [] }] }),
356+
'template',
357+
),
358+
],
337359
// #23543 F3: a loose `config` key under a sibling-block node type, refused by
338360
// the flow parse; the node's block is complete, so the key is the only issue.
339361
'flow-node-wait-event-config-lift': [

‎packages/spec/src/ui/default-record-page.ts‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,19 +89,23 @@ export function defaultRecordPageObject(page: unknown): string | undefined {
8989
* without a parse.
9090
*
9191
* Reads a parsed page and an unparsed one alike, by the same declared keys
92-
* {@link defaultRecordPageObject} reads.
92+
* {@link defaultRecordPageObject} reads. The sentence names the release that
93+
* first refused the flag (`@objectstack/spec 18`): the retired D2 entry
94+
* `page-is-default-off-record-page-removed` converts data at rest, and the
95+
* removal-version pin holds every such prescription to the release its
96+
* `retiredAfter` derives.
9397
*/
9498
export function misplacedDefaultPageRefusal(page: unknown): string | undefined {
9599
if (!page || typeof page !== 'object' || Array.isArray(page)) return undefined;
96100
const p = page as Record<string, unknown>;
97101
if (p.isDefault !== true || defaultRecordPageObject(page) !== undefined) return undefined;
98102
const type = p.type === undefined ? PAGE_TYPE_DEFAULT : p.type;
99103
if (type === 'record') {
100-
return '`isDefault: true` is refused on a record page bound to no `object`: it makes a record page the '
104+
return '`isDefault: true` is refused on a record page bound to no `object` since @objectstack/spec 18: it makes a record page the '
101105
+ 'default record page of the object it binds, the page that object\'s record view opens. Bind the '
102106
+ 'page (`object: \'<object name>\'`), or delete `isDefault`.';
103107
}
104-
return `\`isDefault: true\` is refused on a \`type: '${String(type)}'\` page: it marks the default record page `
108+
return `\`isDefault: true\` is refused on a \`type: '${String(type)}'\` page since @objectstack/spec 18: it marks the default record page `
105109
+ 'of an object, and nothing reads it on any other page. A page does not choose where an app opens — an '
106110
+ 'app\'s landing page is its first navigation item (by `order`), so to land on this page put its entry '
107111
+ 'first in the app\'s `navigation`; the root landing follows the app\'s own `isDefault`. Delete '

0 commit comments

Comments
 (0)