From 1a88a78e376822b56f070c04a0d4c1dadf022833 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 00:19:18 +0000 Subject: [PATCH 1/3] fix(runtime): the base export serves the base's own document while a .custom override of it is live On a showcase-shaped boot the per-package layer lists only the override copy under the base's name (the metadata service holds the base's code documents too), so the override filter dropped the item from the base's export. Serve the base's code artifact in its place, and pin it on that boot shape. Claude-Session: https://claude.ai/code/session_01JQwxbac7S1AsbGi5a46PrB Co-authored-by: Claude --- ...9649-base-export-keeps-overridden-items.md | 8 + ...ort-base-showcase-boot.integration.test.ts | 269 ++++++++++++++++++ packages/runtime/src/domains/packages.ts | 52 +++- 3 files changed, 328 insertions(+), 1 deletion(-) create mode 100644 .changeset/29649-base-export-keeps-overridden-items.md create mode 100644 packages/runtime/src/domains/packages-export-base-showcase-boot.integration.test.ts diff --git a/.changeset/29649-base-export-keeps-overridden-items.md b/.changeset/29649-base-export-keeps-overridden-items.md new file mode 100644 index 00000000000..4a7659babc9 --- /dev/null +++ b/.changeset/29649-base-export-keeps-overridden-items.md @@ -0,0 +1,8 @@ +--- +'@objectstack/runtime': patch +--- + +Clause-②: no +Metadata-protocol: none + +`GET /packages//export` carries the base's own document again while its `.custom` pair's override of a dashboard or report is live (ADR-0146 D8 item 3). On a boot that also holds the base's code documents in the metadata service, as every `defineStack` host does, the per-package layer listed only the override copy under the base's name, and the export's override filter (#29563) left the item out of the base's export. The export now serves the base's code artifact in that item's place, so the base's export is byte-identical with or without a published override, and still carries no `overrides` block. diff --git a/packages/runtime/src/domains/packages-export-base-showcase-boot.integration.test.ts b/packages/runtime/src/domains/packages-export-base-showcase-boot.integration.test.ts new file mode 100644 index 00000000000..83dd2803bc4 --- /dev/null +++ b/packages/runtime/src/domains/packages-export-base-showcase-boot.integration.test.ts @@ -0,0 +1,269 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * [#29649, ADR-0146 D8 item 3] "`GET /packages/hotcrm/export` carries the base + * unchanged" while its `.custom` pair's override of an item is live — on a base + * booted as the showcase boots, where the base's code documents are held twice: + * by the registry (`ObjectQL.registerApp`) AND by the metadata service + * (`MetadataPlugin`'s artifact door, which stamps each body with the base's + * envelope before it stores it). + * + * That second holder is what the #29563 pin + * (`packages-export-base-unchanged-per-type.integration.test.ts`) does not + * register, and why it stayed green while a real showcase boot lost the item: + * the per-package layer merges the registry's items over the metadata + * service's, one per `(name, package)` slot, and the published override is + * registered in the registry's bare slot wearing the base's envelope (#29600). + * So the base's slot of the name holds the override alone, and the export's + * "never an override of THIS package" filter (#29563) left nothing in it. + * Measured on a real showcase boot at b391feb1: dashboards 3 → 2, reports + * 4 → 3, views 36 → 36. + * + * Per type (view, dashboard, report), over the real door + * (`HttpDispatcher.handlePackages`), the real protocol, the real `ObjectQL` + * engine on a SQLite file and the real `MetadataManager`: + * + * - the base's export after the override is published is byte-identical to + * the base's export before it, so the item is present and equal, and no + * item in it carries an `overrides` block; + * - the same holds on a restart over the same database; + * - the pair's export carries the overriding item with its `overrides` block; + * - CONTROL: the effective read of the base's name serves the override, so + * the base export's answer is the export's own. + */ +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; +import { mkdtempSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { ObjectQL } from '@objectstack/objectql'; +import { SqlDriver } from '@objectstack/driver-sql'; +import { MetadataManager } from '@objectstack/metadata'; +import { ObjectStackProtocolImplementation, resetEnvWritableMetadataTypes } from '@objectstack/metadata-protocol'; +import { + SysMetadataAuditObject, + SysMetadataCommitObject, + SysMetadataHistoryObject, + SysMetadataObject, +} from '@objectstack/metadata-core'; +import { applyProtection } from '@objectstack/spec/shared'; +import { expandViewContainer } from '@objectstack/spec/ui'; +import { HttpDispatcher } from '../http-dispatcher.js'; + +const BASE = 'com.example.showcase'; +const BASE_VERSION = '0.1.0'; +const PAIR = 'com.example.showcase.custom'; +const PLATFORM_PKG = 'com.objectstack.platform'; +const DECLARATION = { package: BASE, basedOn: BASE_VERSION } as const; + +const CALLER: any = { + request: {}, + environmentId: 'platform', + executionContext: { userId: 'u_admin', isSystem: false, systemPermissions: ['manage_metadata', 'studio.access'] }, +}; + +const OBJECT = 'showcase_task'; +const VIEW = `${OBJECT}.in_progress`; +const DASHBOARD = 'showcase_ops_dashboard'; +const REPORT = 'showcase_hours_by_status'; +const OVERRIDE_LABEL = 'Edited label'; + +const COLUMNS = [{ field: 'title' }, { field: 'status' }]; +const viewContainer = () => ({ + object: OBJECT, + list: { label: 'All Tasks', type: 'grid', data: { provider: 'object', object: OBJECT }, columns: COLUMNS }, + listViews: { + in_progress: { label: 'In Progress', type: 'grid', data: { provider: 'object', object: OBJECT }, columns: COLUMNS }, + }, +}); +const dashboards = () => [ + { name: 'showcase_chart_gallery', label: 'Chart Gallery', columns: 12, widgets: [] }, + { name: DASHBOARD, label: 'Delivery Operations', columns: 12, widgets: [] }, + { name: 'showcase_revenue_pulse', label: 'Revenue Pulse', columns: 12, widgets: [] }, +]; +const report = (name: string, label: string) => ({ + name, + label, + type: 'summary', + dataset: 'showcase_task_metrics', + rows: ['status'], + values: ['task_count'], +}); +const reports = () => [ + report(REPORT, 'Hours by Status'), + report('showcase_task_overview', 'Task Overview'), +]; + +const TYPES = [ + { type: 'view', plural: 'viewItems', name: VIEW }, + { type: 'dashboard', plural: 'dashboards', name: DASHBOARD }, + { type: 'report', plural: 'reports', name: REPORT }, +] as const; + +/** The base's body, as the showcase's `defineStack` carries it: one fresh copy per reader. */ +function baseBody(): Record { + return { + id: BASE, + name: 'showcase', + version: BASE_VERSION, + namespace: 'showcase', + objects: [{ + name: OBJECT, + label: 'Task', + sharingModel: 'private', + fields: { + title: { name: 'title', label: 'Title', type: 'text' }, + status: { name: 'status', label: 'Status', type: 'text' }, + }, + }], + datasets: [{ + name: 'showcase_task_metrics', + label: 'Task Metrics', + object: OBJECT, + dimensions: [{ name: 'status', label: 'Status', field: 'status', type: 'string' }], + measures: [{ name: 'task_count', label: 'Tasks', aggregate: 'count' }], + }], + views: [viewContainer()], + dashboards: dashboards(), + reports: reports(), + }; +} + +/** + * The metadata service's copy of the base's documents, stored as + * `MetadataPlugin`'s artifact door stores them: each body stamped with the + * base's envelope (`applyProtection`), a view container under its object and + * each view it expands to under its own name. + */ +async function metadataServiceOf(): Promise { + const manager = new MetadataManager({ formats: ['json'], loaders: [] }); + const body = baseBody(); + const stamp = >(item: T): T => + applyProtection(item as any, { packageId: BASE, packageVersion: BASE_VERSION }) as T; + for (const container of body.views) { + await manager.register('view', OBJECT, stamp(container), { notify: false }); + for (const view of expandViewContainer(OBJECT, container)) { + await manager.register('view', view.name, stamp(view as any), { notify: false }); + } + } + for (const dashboard of body.dashboards) await manager.register('dashboard', dashboard.name, stamp(dashboard), { notify: false }); + for (const item of body.reports) await manager.register('report', item.name, stamp(item), { notify: false }); + return manager; +} + +let cleanup: Array<() => void | Promise> = []; +afterEach(async () => { + for (const c of cleanup.reverse()) await c(); + cleanup = []; +}); + +const envSnapshot = { OS_HOME: process.env.OS_HOME, OS_METADATA_WRITABLE: process.env.OS_METADATA_WRITABLE }; +let home: string; +beforeAll(() => { + home = mkdtempSync(join(tmpdir(), 'os-29649-home-')); + process.env.OS_HOME = home; +}); +afterAll(() => { + for (const [k, v] of Object.entries(envSnapshot)) { + if (v === undefined) delete process.env[k]; + else process.env[k] = v; + } + rmSync(home, { recursive: true, force: true }); +}); +beforeEach(() => { + delete process.env.OS_METADATA_WRITABLE; + ObjectStackProtocolImplementation.resetEnvWritableCache(); + resetEnvWritableMetadataTypes(); + vi.spyOn(console, 'warn').mockImplementation(() => {}); +}); +afterEach(() => { vi.restoreAllMocks(); }); + +/** One boot over the SQLite file in `dir`; the pair is installed on the first boot only. */ +async function boot(dir: string, { installPair }: { installPair: boolean }) { + const driver = new SqlDriver({ + client: 'better-sqlite3', + connection: { filename: join(dir, 'data.sqlite') }, + useNullAsDefault: true, + }); + const objects = [SysMetadataObject, SysMetadataHistoryObject, SysMetadataAuditObject, SysMetadataCommitObject] as any[]; + await driver.initObjects(objects); + + const engine = new ObjectQL(); + engine.registerDriver(driver as any, true); + await engine.init(); + for (const o of objects) engine.registry.registerObject(o, PLATFORM_PKG); + cleanup.push(() => engine.destroy()); + engine.registerApp(baseBody()); + + const services = new Map([['objectql', engine], ['metadata', await metadataServiceOf()]]); + const protocol: any = new ObjectStackProtocolImplementation(engine as any, () => services as any); + services.set('protocol', protocol); + const get = (name: string) => services.get(name) ?? null; + const dispatcher = new HttpDispatcher({ context: { getService: get }, getService: get, getServiceAsync: async (n: string) => get(n) } as any); + + if (installPair) { + await protocol.installPackage({ + manifest: { + id: PAIR, name: 'showcase-custom', version: '0.1.0', type: 'app', + namespace: 'showcase_custom', dependencies: { [BASE]: '^0' }, + }, + }); + } + + return { + protocol, + async exported(id: string): Promise> { + const res = await dispatcher.handlePackages(`/${encodeURIComponent(id)}/export`, 'GET', undefined, {}, CALLER); + expect(res.response?.status, JSON.stringify(res.response?.body)).toBe(200); + const body: any = JSON.parse(JSON.stringify(res.response?.body ?? null)); + return body?.data ?? body; + }, + async override(type: string, name: string, item: Record) { + const saved = await protocol.saveMetaItem({ type, name, item, mode: 'draft', packageId: PAIR }); + expect(saved?.success, `the ${type} draft save`).toBe(true); + await protocol.publishMetaItem({ type, name, packageId: PAIR }); + }, + }; +} + +const itemsOf = (manifest: Record, plural: string, name: string): any[] => + (manifest[plural] ?? []).filter((item: any) => item?.name === name); + +/** Every exported item, across the collections, that carries an `overrides` block. */ +const overridingItems = (manifest: Record): string[] => Object.entries(manifest) + .filter(([, value]) => Array.isArray(value)) + .flatMap(([plural, value]) => (value as any[]) + .filter((item) => item && typeof item === 'object' && 'overrides' in item) + .map((item) => `${plural}/${item.name}`)); + +describe('[#29649] GET /packages//export carries the base unchanged on a showcase-shaped boot', () => { + for (const { type, plural, name } of TYPES) { + it(`${type}: the base's export is byte-identical with the override published, and after a restart`, async () => { + const dir = mkdtempSync(join(tmpdir(), 'os-29649-db-')); + cleanup.push(() => rmSync(dir, { recursive: true, force: true })); + + const door = await boot(dir, { installPair: true }); + const before = await door.exported(BASE); + const shipped = itemsOf(before, plural, name); + expect(shipped, `the base's ${type} before the override`).toHaveLength(1); + + await door.override(type, name, { ...shipped[0], label: OVERRIDE_LABEL, overrides: { ...DECLARATION } }); + + // CONTROL: the effective read of the base's name serves the published override. + const effective: any = await door.protocol.getMetaItem({ type, name, packageId: BASE }); + expect(effective?.item?.label, `the effective ${type} read`).toBe(OVERRIDE_LABEL); + + const after = await door.exported(BASE); + expect(itemsOf(after, plural, name), `the base's ${type} with the override live`).toEqual(shipped); + expect(overridingItems(after), `the base's export carries no override`).toEqual([]); + expect(JSON.stringify(after), `the base's whole export with the ${type} override live`).toBe(JSON.stringify(before)); + + const pair = itemsOf(await door.exported(PAIR), plural, name); + expect(pair.map((item) => item.label), `the pair's ${type} export`).toEqual([OVERRIDE_LABEL]); + expect(pair[0].overrides).toEqual(DECLARATION); + + // A restart over the same database: the published row is hydrated from storage. + const restarted = await boot(dir, { installPair: false }); + expect(JSON.stringify(await restarted.exported(BASE)), `the base's whole export after a restart`).toBe(JSON.stringify(before)); + }); + } +}); diff --git a/packages/runtime/src/domains/packages.ts b/packages/runtime/src/domains/packages.ts index fac0bdc3bad..32cda091e6e 100644 --- a/packages/runtime/src/domains/packages.ts +++ b/packages/runtime/src/domains/packages.ts @@ -3342,6 +3342,42 @@ function declaresOverrideOf(item: unknown, packageId: string): boolean { && (overrides as { package?: unknown }).package === packageId; } +/** + * [#29563, #29649, ADR-0146 D8 item 3] A package's own documents of one type, + * out of its per-package layer `items`: an item that declares it overrides + * `packageId` ({@link declaresOverrideOf}) is the package's `.custom` pair's + * override, never one of the package's documents, so it is replaced, in its + * place, by the package's own code artifact of the name when the layer lists + * none beside it, and dropped otherwise. The artifact is the registry's + * prefer-local answer for the package (`getArtifactItem`), kept only when it is + * the package's own (`_packageId`) and declares no override itself. + */ +function ownDocumentsOf(items: any[], type: string, packageId: string, registry: any): any[] { + const own = new Set(items + .filter((it) => !declaresOverrideOf(it, packageId)) + .map((it) => (it as { name?: unknown } | null)?.name)); + const out: any[] = []; + for (const it of items) { + if (!declaresOverrideOf(it, packageId)) { + out.push(it); + continue; + } + const name = (it as { name?: unknown } | null)?.name; + if (typeof name !== 'string' || own.has(name)) continue; + let artifact: unknown; + try { + artifact = registry?.getArtifactItem?.(type, name, packageId); + } catch { + artifact = undefined; + } + if ((artifact as { _packageId?: unknown } | null | undefined)?._packageId !== packageId) continue; + if (declaresOverrideOf(artifact, packageId)) continue; + own.add(name); + out.push(artifact); + } + return out; +} + async function assemblePackageManifest( deps: DomainHandlerDeps, packageId: string, @@ -3420,7 +3456,21 @@ context: HttpProtocolContext, // the base's export carried both, the second with the edited label and // the `overrides` block. A protocol with no per-package layer is read // as before. - if (perPackageLayer) items = items.filter((it) => !declaresOverrideOf(it, packageId)); + // + // [#29649] …and the package's own document is served in its place: the + // override copy is not always listed BESIDE the base's item. On a boot + // that also holds the base's code documents in the metadata service + // (`MetadataPlugin`'s artifact door, as every `defineStack` host does), + // the layer merges the registry's items over the service's, one per + // `(name, package)` slot, so the copy wearing the base's envelope takes + // the base's slot and the base's own item is not listed at all. + // Measured on a real showcase boot: a published dashboard or report + // override left the base's export without the item. The package's own + // document is its code artifact, read prefer-local from the registry + // (`getArtifactItem` with the package's id: the composite slot + // `registerApp` filled, which the bare-slot copy never reaches), the + // same body the layer serves for the name while no override is live. + if (perPackageLayer) items = ownDocumentsOf(items, singular, packageId, registry); if (items.length === 0) continue; // [#5320] `views` is partitioned rather than dumped: the registry's // ADR-0017 dual-read returns the container AND its expanded per-view From e373cc163e96f0cdb459b1a201671818753a0583 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 00:26:51 +0000 Subject: [PATCH 2/3] test(runtime): pin the base export for the five types the registry still holds in its bare slot On a showcase-shaped boot a published page, app, dataset, doc or book override still drops the item from the base export after the dashboard and report copies left the bare slot; the export-read fix covers them too. Claude-Session: https://claude.ai/code/session_01JQwxbac7S1AsbGi5a46PrB Co-authored-by: Claude --- ...9649-base-export-keeps-overridden-items.md | 2 +- ...ort-base-showcase-boot.integration.test.ts | 88 +++++++++++++++---- packages/runtime/src/domains/packages.ts | 4 +- 3 files changed, 75 insertions(+), 19 deletions(-) diff --git a/.changeset/29649-base-export-keeps-overridden-items.md b/.changeset/29649-base-export-keeps-overridden-items.md index 4a7659babc9..af877efbbd5 100644 --- a/.changeset/29649-base-export-keeps-overridden-items.md +++ b/.changeset/29649-base-export-keeps-overridden-items.md @@ -5,4 +5,4 @@ Clause-②: no Metadata-protocol: none -`GET /packages//export` carries the base's own document again while its `.custom` pair's override of a dashboard or report is live (ADR-0146 D8 item 3). On a boot that also holds the base's code documents in the metadata service, as every `defineStack` host does, the per-package layer listed only the override copy under the base's name, and the export's override filter (#29563) left the item out of the base's export. The export now serves the base's code artifact in that item's place, so the base's export is byte-identical with or without a published override, and still carries no `overrides` block. +`GET /packages//export` carries the base's own document again while its `.custom` pair's override of an item is live (measured for dashboard, report, page, app, dataset, doc and book) (ADR-0146 D8 item 3). On a boot that also holds the base's code documents in the metadata service, as every `defineStack` host does, the per-package layer listed only the override copy under the base's name, and the export's override filter (#29563) left the item out of the base's export. The export now serves the base's code artifact in that item's place, so the base's export is byte-identical with or without a published override, and still carries no `overrides` block. diff --git a/packages/runtime/src/domains/packages-export-base-showcase-boot.integration.test.ts b/packages/runtime/src/domains/packages-export-base-showcase-boot.integration.test.ts index 83dd2803bc4..56dc25679e6 100644 --- a/packages/runtime/src/domains/packages-export-base-showcase-boot.integration.test.ts +++ b/packages/runtime/src/domains/packages-export-base-showcase-boot.integration.test.ts @@ -17,9 +17,13 @@ * So the base's slot of the name holds the override alone, and the export's * "never an override of THIS package" filter (#29563) left nothing in it. * Measured on a real showcase boot at b391feb1: dashboards 3 → 2, reports - * 4 → 3, views 36 → 36. + * 4 → 3, views 36 → 36. #29640 (7dae3b3c) took the dashboard and report copy + * out of the bare slot; on a real showcase boot at 7dae3b3c a published page, + * app, dataset, doc or book override still dropped the item (pages 29 → 28, + * apps 1 → 0, datasets 4 → 3, docs 8 → 7, books 1 → 0). * - * Per type (view, dashboard, report), over the real door + * Per type (view, dashboard, report, and the five types #29640 left in the + * registry's bare slot: page, app, dataset, doc, book), over the real door * (`HttpDispatcher.handlePackages`), the real protocol, the real `ObjectQL` * engine on a SQLite file and the real `MetadataManager`: * @@ -27,9 +31,11 @@ * the base's export before it, so the item is present and equal, and no * item in it carries an `overrides` block; * - the same holds on a restart over the same database; - * - the pair's export carries the overriding item with its `overrides` block; - * - CONTROL: the effective read of the base's name serves the override, so - * the base export's answer is the export's own. + * - the pair's export carries the overriding item with its `overrides` block + * (view, dashboard, report; see `pairServed`); + * - CONTROL: the pair's override row is published, and for view, dashboard + * and report the effective read of the base's name serves it, so the base + * export's answer is the export's own. */ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; import { mkdtempSync, rmSync } from 'node:fs'; @@ -93,10 +99,37 @@ const reports = () => [ report('showcase_task_overview', 'Task Overview'), ]; +/** + * The base's collections the metadata service holds besides its views, as the + * artifact door files them: plural collection → metadata type. Every one is a + * type a `.custom` pair can publish an override of. + */ +const SERVICE_COLLECTIONS = { + apps: 'app', + pages: 'page', + datasets: 'dataset', + dashboards: 'dashboard', + reports: 'report', + docs: 'doc', + books: 'book', +} as const; + +/** + * `pairServed`: whether the pair's export and the effective read serve the + * override. For the five types #29640 left in the bare slot neither does on + * this boot shape (the pair's per-package layer lists none of them, and the + * effective read serves the base's document; #29613), so only the base's half + * is pinned for them. + */ const TYPES = [ - { type: 'view', plural: 'viewItems', name: VIEW }, - { type: 'dashboard', plural: 'dashboards', name: DASHBOARD }, - { type: 'report', plural: 'reports', name: REPORT }, + { type: 'view', plural: 'viewItems', name: VIEW, pairServed: true }, + { type: 'dashboard', plural: 'dashboards', name: DASHBOARD, pairServed: true }, + { type: 'report', plural: 'reports', name: REPORT, pairServed: true }, + { type: 'page', plural: 'pages', name: 'showcase_start_here', pairServed: false }, + { type: 'app', plural: 'apps', name: 'showcase_app', pairServed: false }, + { type: 'dataset', plural: 'datasets', name: 'showcase_task_metrics', pairServed: false }, + { type: 'doc', plural: 'docs', name: 'showcase_index', pairServed: false }, + { type: 'book', plural: 'books', name: 'showcase_manual', pairServed: false }, ] as const; /** The base's body, as the showcase's `defineStack` carries it: one fresh copy per reader. */ @@ -122,9 +155,19 @@ function baseBody(): Record { dimensions: [{ name: 'status', label: 'Status', field: 'status', type: 'string' }], measures: [{ name: 'task_count', label: 'Tasks', aggregate: 'count' }], }], + apps: [{ name: 'showcase_app', label: 'Showcase', navigation: [] }], + pages: [ + { name: 'showcase_capability_map', label: 'Capability Map', type: 'app', regions: [] }, + { name: 'showcase_start_here', label: 'Start Here', type: 'app', regions: [] }, + ], views: [viewContainer()], dashboards: dashboards(), reports: reports(), + docs: [ + { name: 'showcase_docs_guide', label: 'Docs Guide', content: '# Docs guide' }, + { name: 'showcase_index', label: 'Showcase', content: '# Showcase' }, + ], + books: [{ name: 'showcase_manual', label: 'Showcase Manual', groups: [] }], }; } @@ -145,8 +188,9 @@ async function metadataServiceOf(): Promise { await manager.register('view', view.name, stamp(view as any), { notify: false }); } } - for (const dashboard of body.dashboards) await manager.register('dashboard', dashboard.name, stamp(dashboard), { notify: false }); - for (const item of body.reports) await manager.register('report', item.name, stamp(item), { notify: false }); + for (const [plural, type] of Object.entries(SERVICE_COLLECTIONS)) { + for (const item of body[plural]) await manager.register(type, item.name, stamp(item), { notify: false }); + } return manager; } @@ -211,6 +255,10 @@ async function boot(dir: string, { installPair }: { installPair: boolean }) { return { protocol, + /** The pair's active (published) rows of `(type, name)`, read off the store. */ + async publishedRows(type: string, name: string): Promise { + return await driver.find('sys_metadata', { where: { type, name, package_id: PAIR, state: 'active' } } as any) as unknown[]; + }, async exported(id: string): Promise> { const res = await dispatcher.handlePackages(`/${encodeURIComponent(id)}/export`, 'GET', undefined, {}, CALLER); expect(res.response?.status, JSON.stringify(res.response?.body)).toBe(200); @@ -236,7 +284,7 @@ const overridingItems = (manifest: Record): string[] => Object.entr .map((item) => `${plural}/${item.name}`)); describe('[#29649] GET /packages//export carries the base unchanged on a showcase-shaped boot', () => { - for (const { type, plural, name } of TYPES) { + for (const { type, plural, name, pairServed } of TYPES) { it(`${type}: the base's export is byte-identical with the override published, and after a restart`, async () => { const dir = mkdtempSync(join(tmpdir(), 'os-29649-db-')); cleanup.push(() => rmSync(dir, { recursive: true, force: true })); @@ -248,18 +296,24 @@ describe('[#29649] GET /packages//export carries the base unchanged on a s await door.override(type, name, { ...shipped[0], label: OVERRIDE_LABEL, overrides: { ...DECLARATION } }); - // CONTROL: the effective read of the base's name serves the published override. - const effective: any = await door.protocol.getMetaItem({ type, name, packageId: BASE }); - expect(effective?.item?.label, `the effective ${type} read`).toBe(OVERRIDE_LABEL); + // CONTROL: the override is published, and, where the protocol serves + // it, the effective read of the base's name serves it. + expect(await door.publishedRows(type, name), `the pair's published ${type} row`).toHaveLength(1); + if (pairServed) { + const effective: any = await door.protocol.getMetaItem({ type, name, packageId: BASE }); + expect(effective?.item?.label, `the effective ${type} read`).toBe(OVERRIDE_LABEL); + } const after = await door.exported(BASE); expect(itemsOf(after, plural, name), `the base's ${type} with the override live`).toEqual(shipped); expect(overridingItems(after), `the base's export carries no override`).toEqual([]); expect(JSON.stringify(after), `the base's whole export with the ${type} override live`).toBe(JSON.stringify(before)); - const pair = itemsOf(await door.exported(PAIR), plural, name); - expect(pair.map((item) => item.label), `the pair's ${type} export`).toEqual([OVERRIDE_LABEL]); - expect(pair[0].overrides).toEqual(DECLARATION); + if (pairServed) { + const pair = itemsOf(await door.exported(PAIR), plural, name); + expect(pair.map((item) => item.label), `the pair's ${type} export`).toEqual([OVERRIDE_LABEL]); + expect(pair[0].overrides).toEqual(DECLARATION); + } // A restart over the same database: the published row is hydrated from storage. const restarted = await boot(dir, { installPair: false }); diff --git a/packages/runtime/src/domains/packages.ts b/packages/runtime/src/domains/packages.ts index 32cda091e6e..1dad8d5010a 100644 --- a/packages/runtime/src/domains/packages.ts +++ b/packages/runtime/src/domains/packages.ts @@ -3465,7 +3465,9 @@ context: HttpProtocolContext, // `(name, package)` slot, so the copy wearing the base's envelope takes // the base's slot and the base's own item is not listed at all. // Measured on a real showcase boot: a published dashboard or report - // override left the base's export without the item. The package's own + // override left the base's export without the item, and after #29640 + // took those two out of the bare slot, a page, app, dataset, doc or + // book override still did (#29613 holds them). The package's own // document is its code artifact, read prefer-local from the registry // (`getArtifactItem` with the package's id: the composite slot // `registerApp` filled, which the bare-slot copy never reaches), the From 51c4d1ee1a2c63e76a61ddf1b6176ad6166a112c Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 01:06:21 +0000 Subject: [PATCH 3/3] test(runtime): read the pair's published row with a typed driver query Claude-Session: https://claude.ai/code/session_01JQwxbac7S1AsbGi5a46PrB Co-authored-by: Claude --- .../packages-export-base-showcase-boot.integration.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/runtime/src/domains/packages-export-base-showcase-boot.integration.test.ts b/packages/runtime/src/domains/packages-export-base-showcase-boot.integration.test.ts index 56dc25679e6..2abb61fa6e5 100644 --- a/packages/runtime/src/domains/packages-export-base-showcase-boot.integration.test.ts +++ b/packages/runtime/src/domains/packages-export-base-showcase-boot.integration.test.ts @@ -257,7 +257,7 @@ async function boot(dir: string, { installPair }: { installPair: boolean }) { protocol, /** The pair's active (published) rows of `(type, name)`, read off the store. */ async publishedRows(type: string, name: string): Promise { - return await driver.find('sys_metadata', { where: { type, name, package_id: PAIR, state: 'active' } } as any) as unknown[]; + return await driver.find('sys_metadata', { where: { type, name, package_id: PAIR, state: 'active' } }); }, async exported(id: string): Promise> { const res = await dispatcher.handlePackages(`/${encodeURIComponent(id)}/export`, 'GET', undefined, {}, CALLER);