From a23a3b8cc67277560040bb56b1b4738e3238225a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 00:05:01 +0000 Subject: [PATCH 1/8] fix(verify): bootStack composes the artifact-less MetadataPlugin os serve gives a host config The metadata slot was the kernel's in-memory fallback, which has no matchEndpoint, so every declared apis endpoint answered 404 under the harness while os serve served it. A MetadataPlugin in extraPlugins or in the app's own plugins takes the composed one's place. Claude-Session: https://claude.ai/code/session_01VoSxBQujKLZKPwK2u5ehQ6 Co-authored-by: Claude --- packages/verify/package.json | 1 + packages/verify/src/harness.ts | 38 +++++++++++++++++++++++++++++++++- pnpm-lock.yaml | 3 +++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/packages/verify/package.json b/packages/verify/package.json index 73f6776b069..782064d5ff1 100644 --- a/packages/verify/package.json +++ b/packages/verify/package.json @@ -28,6 +28,7 @@ "dependencies": { "@objectstack/core": "workspace:*", "@objectstack/mcp": "workspace:*", + "@objectstack/metadata": "workspace:*", "@objectstack/objectql": "workspace:*", "@objectstack/platform-objects": "workspace:*", "@objectstack/plugin-approvals": "workspace:*", diff --git a/packages/verify/src/harness.ts b/packages/verify/src/harness.ts index 0230cbdecca..70491000325 100644 --- a/packages/verify/src/harness.ts +++ b/packages/verify/src/harness.ts @@ -41,6 +41,7 @@ import { SharingServicePlugin } from '@objectstack/plugin-sharing'; import { SettingsServicePlugin, LocalCryptoProvider } from '@objectstack/service-settings'; import { AnalyticsServicePlugin } from '@objectstack/service-analytics'; import { PlatformObjectsPlugin } from '@objectstack/platform-objects/plugin'; +import { MetadataPlugin } from '@objectstack/metadata'; // Node-only subpath (#4700). Optional packages supplied by the app under // verification — `@objectstack/organizations` above all — must be resolved from // THAT app, not from `packages/verify`'s own realpath inside this workspace. @@ -800,7 +801,11 @@ function claimConfiguration(config: unknown): ConfigurationClaim { * ## What it composes — one composition rule (#22301, ruling A) * * For one configuration, what `objectstack serve` composes from it: the - * harness's service set; the providers `serve` mounts for it — the ones the + * harness's service set, its `metadata` slot held by the artifact-less + * `MetadataPlugin` (`@objectstack/metadata`) `serve` composes for a host + * configuration, so a declared `apis` endpoint is matched as `serve` matches + * it (a `MetadataPlugin` in {@link BootOptions.extraPlugins} or in the app's + * `plugins` takes its place); the providers `serve` mounts for it — the ones the * app's `requires` names and the always-on slate `serve` mounts for every app * (`queue`, `job`, `cache`, `settings`, `email`, `storage`, `sms`, `sharing`, * `messaging`, `analytics`, `package-registry`), each built from the app's @@ -974,6 +979,37 @@ async function bootClaimed( // hit the socket — requests are injected through the Hono app directly. await kernel.use(new HonoServerPlugin({ port: 0 })); + // [#23088] The `metadata` slot, filled the way `objectstack serve` fills it + // for a host configuration (#22989, `hostConfigMetadataDoor` in the CLI's + // `serve.ts`): `@objectstack/metadata`'s `MetadataPlugin` with no artifact, + // the same options, in `serve`'s slot for it — after the HTTP server, before + // the app. Without it the slot is the kernel's in-memory fallback, which has + // no `matchEndpoint`, so every `apis` endpoint a stack declares answered + // `404 ENDPOINT_NOT_FOUND` here and its declared answer under `serve`. + // - It reads none of the stack (no `artifactSource`; `lazy` skips the + // filesystem priming pass), so `AppPlugin` below stays the single + // registrar of the stack's metadata, `apis` and the four security + // collections included. + // - `rootDir` is the app's root, as `serve` hands it, so the repository the + // plugin attaches sits under the app; attaching writes nothing. + // - A `MetadataPlugin` the caller hands in `extraPlugins`, or the app + // declares in its own `plugins`, takes its place by identity — `serve`'s + // "no MetadataPlugin in `plugins`" condition, read on the same two keys. + // No catch: a boot that cannot compose its `metadata` occupant fails, rather + // than falling back to the matcher-less one quietly. + const declaredPlugins = (config as { plugins?: unknown } | null | undefined)?.plugins; + const holdsMetadataPlugin = [ + ...(opts.extraPlugins ?? []), + ...(Array.isArray(declaredPlugins) ? declaredPlugins : []), + ].some( + (plugin) => + registeredName(plugin) === 'com.objectstack.metadata' || + (plugin as { constructor?: { name?: unknown } } | null | undefined)?.constructor?.name === 'MetadataPlugin', + ); + if (!holdsMetadataPlugin) { + await kernel.use(new MetadataPlugin({ watch: false, config: { bootstrap: 'lazy' }, rootDir: hostRoot })); + } + // The app under test (objects, datasets, cubes, flows, seed data): the // configuration, or [#22301] the compiled artifact `BootOptions.artifact` // names, whose hook and action `body`s AppPlugin binds to the sandbox. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3d207cecd40..ca34a384dcc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3054,6 +3054,9 @@ importers: '@objectstack/mcp': specifier: workspace:* version: link:../mcp + '@objectstack/metadata': + specifier: workspace:* + version: link:../metadata '@objectstack/objectql': specifier: workspace:* version: link:../objectql From d072256c135d9cc0ce1cd2b61cece6714764d029 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 00:11:16 +0000 Subject: [PATCH 2/8] test(verify): pin bootStack's metadata slot and a caller's MetadataPlugin precedence Claude-Session: https://claude.ai/code/session_01VoSxBQujKLZKPwK2u5ehQ6 Co-authored-by: Claude --- .../verify/src/harness.metadata-door.test.ts | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 packages/verify/src/harness.metadata-door.test.ts diff --git a/packages/verify/src/harness.metadata-door.test.ts b/packages/verify/src/harness.metadata-door.test.ts new file mode 100644 index 00000000000..87f95f2b105 --- /dev/null +++ b/packages/verify/src/harness.metadata-door.test.ts @@ -0,0 +1,141 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * #23088: `bootStack` fills its `metadata` slot the way `objectstack serve` + * fills it for a host configuration (#22989): `@objectstack/metadata`'s + * `MetadataPlugin` with no artifact. Before, the slot was the kernel's + * in-memory fallback, which has no `matchEndpoint`, so every `apis` endpoint a + * stack declared answered `404 ENDPOINT_NOT_FOUND` under the harness while + * `serve` served it, and the boot logged `AppPlugin`'s no-matcher warning. + * + * Two boots: + * + * - the harness's own composition (nothing in `extraPlugins`): the declared + * path answers 401 without a session and 200 with one, an undeclared path + * under the same mount keeps the bare 404, and the no-matcher warning is + * not logged; + * - a caller's own `MetadataPlugin` in `extraPlugins`: that instance holds + * the slot, and the composed one is never registered (no supersede). + * + * Warnings are identified by their named subjects and structured fields (the + * app id, the remedy plugin, the superseded plugin's name), never by prose. + */ + +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { MetadataPlugin } from '@objectstack/metadata'; +import { defineStack } from '@objectstack/spec'; +import { bootStack, type VerifyStack } from './harness.js'; + +const BOOT_TIMEOUT = 120_000; +const APP_ID = 'com.example.mdoor'; +const FEED = '/apps/mdoor/notes'; + +/** A fresh configuration per boot: the instance rule keys on identity. */ +const stackWithApi = () => + defineStack({ + manifest: { id: APP_ID, name: 'Metadata Door', version: '1.0.0', type: 'app', namespace: 'mdoor' }, + objects: [ + { + name: 'mdoor_note', + label: 'Note', + pluralLabel: 'Notes', + sharingModel: 'public_read_write', + fields: { title: { name: 'title', type: 'text', label: 'Title' } }, + }, + ], + apis: [ + { + name: 'mdoor_note_feed', + path: `/api/v1${FEED}`, + method: 'GET', + summary: 'Note feed', + type: 'object_operation', + objectParams: { object: 'mdoor_note', operation: 'find' }, + authRequired: true, + }, + ], + } as never) as unknown as Record; + +/** Every structured kernel-log line the boot wrote to stdout, ANSI stripped. */ +async function bootCapturing(config: unknown, extraPlugins?: unknown[]): Promise<{ stack: VerifyStack; lines: string[] }> { + const lines: string[] = []; + const spy = vi.spyOn(process.stdout, 'write').mockImplementation(((chunk: unknown) => { + lines.push(...String(chunk).replace(/\u001b\[[0-9;]*m/g, '').split('\n')); + return true; + }) as never); + try { + const stack = await bootStack(config, extraPlugins ? { extraPlugins } : {}); + return { stack, lines }; + } finally { + spy.mockRestore(); + } +} + +/** The structured fields a kernel `WARN` line carries, or undefined for any other line. */ +function warnFields(line: string): Record | undefined { + if (!/ WARN /.test(line)) return undefined; + const at = line.indexOf(' {'); + if (at < 0) return undefined; + try { + return JSON.parse(line.slice(at + 1)) as Record; + } catch { + return undefined; + } +} + +let stack: VerifyStack | undefined; +afterEach(async () => { + await stack?.stop(); + stack = undefined; +}); + +describe('#23088 bootStack serves a declared endpoint, as objectstack serve does', () => { + it('composes a matcher, so the declared path is gated and served, and no matcher warning is logged', async () => { + const booted = await bootCapturing(stackWithApi()); + stack = booted.stack; + + // CONTROL on the composition: the slot carries the endpoint matcher. + const metadata = stack.kernel.getService<{ matchEndpoint?: unknown }>('metadata'); + expect(typeof metadata.matchEndpoint).toBe('function'); + + const anonymous = await stack.api(FEED, { method: 'GET' }); + const refused = (await anonymous.json()) as { error?: { code?: string; status?: number } }; + expect(anonymous.status, JSON.stringify(refused)).toBe(401); + expect(refused.error?.code).toBe('UNAUTHENTICATED'); + + const token = await stack.signIn(); + const served = await stack.apiAs(token, 'GET', FEED); + expect(served.status, await served.clone().text()).toBe(200); + + // CONTROL: an undeclared path under the same mount keeps the bare 404. + const undeclared = await stack.apiAs(token, 'GET', '/apps/mdoor/no-such-endpoint'); + const miss = (await undeclared.json()) as { error?: { code?: string } }; + expect(undeclared.status).toBe(404); + expect(miss.error?.code).toBe('ENDPOINT_NOT_FOUND'); + + const matcherWarnings = booted.lines.filter((line) => { + const fields = warnFields(line); + return fields?.appId === APP_ID && line.includes('MetadataPlugin'); + }); + expect(matcherWarnings).toEqual([]); + }, BOOT_TIMEOUT); + + it("a caller's own MetadataPlugin in extraPlugins holds the slot, and the composed one is never registered", async () => { + const own = new MetadataPlugin({ watch: false, config: { bootstrap: 'lazy' } }); + const booted = await bootCapturing(stackWithApi(), [own]); + stack = booted.stack; + + // The slot is the caller's instance's manager — by identity. + const ownManager = (own as unknown as { manager: unknown }).manager; + expect(stack.kernel.getService('metadata')).toBe(ownManager); + + // Composed and then superseded would log the kernel's supersede warning + // for this name; not composed logs none. + const superseded = booted.lines.filter((line) => warnFields(line)?.plugin === own.name); + expect(superseded).toEqual([]); + + // And the caller's instance serves the declared path. + const anonymous = await stack.api(FEED, { method: 'GET' }); + expect(anonymous.status).toBe(401); + }, BOOT_TIMEOUT); +}); From 4c725eae4cc6587b50389f54e169dc5c4aa405b8 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 00:37:55 +0000 Subject: [PATCH 3/8] test(dogfood): the declared-endpoint acceptance boots the showcase on the harness's own MetadataPlugin The hand-added artifact-backed MetadataPlugin is dropped: bootStack now composes the one objectstack serve gives this host configuration, so the endpoints are the ones a deployment of it serves. The artifact door's apis ingestion keeps its own dogfood proofs. Adds the verify changeset. Claude-Session: https://claude.ai/code/session_01VoSxBQujKLZKPwK2u5ehQ6 Co-authored-by: Claude --- .../23088-verify-bootstack-metadata-plugin.md | 13 +++++ ...case-declarative-endpoints.dogfood.test.ts | 49 +++++++++---------- 2 files changed, 35 insertions(+), 27 deletions(-) create mode 100644 .changeset/23088-verify-bootstack-metadata-plugin.md diff --git a/.changeset/23088-verify-bootstack-metadata-plugin.md b/.changeset/23088-verify-bootstack-metadata-plugin.md new file mode 100644 index 00000000000..7b48d4fdd5f --- /dev/null +++ b/.changeset/23088-verify-bootstack-metadata-plugin.md @@ -0,0 +1,13 @@ +--- +'@objectstack/verify': patch +--- + +fix(verify): `bootStack` composes the artifact-less `MetadataPlugin` that `objectstack serve` gives a host configuration, so a stack's declared `apis` endpoints answer under the harness as they answer under `serve` (#23088) + +Clause-②: no + +`bootStack`'s `metadata` slot was the kernel's in-memory fallback, which has no endpoint matcher (`matchEndpoint`). So every `apis` endpoint a stack declared answered `404 ENDPOINT_NOT_FOUND` in a test boot while `objectstack serve` of the same stack served it, and the boot logged `AppPlugin`'s "no endpoint matcher" warning. The harness now composes `@objectstack/metadata`'s `MetadataPlugin` the way `serve` composes it for a host configuration: no artifact, `bootstrap: 'lazy'`, and `rootDir` set to `BootOptions.hostRoot`. It reads none of the stack, so `AppPlugin` stays the single registrar of the stack's metadata. + +- A declared endpoint answers as it does under `serve`: `401` without a session where it is `authRequired`, and its declared answer with one. An undeclared path under the same mount keeps the bare `404`. +- A `MetadataPlugin` you pass in `BootOptions.extraPlugins`, or one your app declares in its own `plugins`, takes the composed one's place. Pass your own to boot an artifact-backed one. +- Two other answers of a test boot move with the slot: `GET /api/v1/discovery` reports the `metadata` service as `available` instead of `degraded`, and `GET /api/v1/dev/metadata-events` (the development-only metadata reload stream, mounted because `bootStack` boots in the development posture) answers `200` instead of `404`, as under `objectstack dev`. The `GET /api/v1/meta/TYPE` answers are unchanged, measured over 22 types on the showcase and CRM examples. diff --git a/packages/qa/dogfood/test/showcase-declarative-endpoints.dogfood.test.ts b/packages/qa/dogfood/test/showcase-declarative-endpoints.dogfood.test.ts index a76e024c228..5e3a0c3271b 100644 --- a/packages/qa/dogfood/test/showcase-declarative-endpoints.dogfood.test.ts +++ b/packages/qa/dogfood/test/showcase-declarative-endpoints.dogfood.test.ts @@ -13,10 +13,10 @@ // unit-tested by calling `dispatch()` directly, which is why nobody noticed // that no route ever mounted the paths it claimed to serve. // -// So this file stubs NOTHING. It boots the real showcase app, through the real -// artifact ingestion path (`MetadataPlugin` + a `local-file` artifact — the -// same path `objectstack dev` / `serve` take), over the real Hono transport, -// and asks the questions a caller asks: +// So this file stubs NOTHING. It boots the real showcase app as +// `objectstack serve` boots this host configuration — the harness's own +// artifact-less `MetadataPlugin`, with `AppPlugin` registering the declared +// endpoints — over the real Hono transport, and asks the questions a caller asks: // // 1. does a declared endpoint answer at all, and with the SAME body the // built-in route gives for the same operation (#5040 §4's red line); @@ -53,7 +53,6 @@ import { fileURLToPath } from 'node:url'; import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { type VerifyStack } from '@objectstack/verify'; import { bootShowcase } from './showcase-boot.js'; -import { MetadataPlugin } from '@objectstack/metadata'; import { ConnectorRestPlugin } from '@objectstack/connector-rest'; import { ConnectorOpenApiPlugin } from '@objectstack/connector-openapi'; import { ConnectorMcpPlugin } from '@objectstack/connector-mcp'; @@ -82,26 +81,29 @@ let tempDir: string; let prevCwd: string; let stack: VerifyStack; let adminToken: string; -/** Read back off disk — what `MetadataPlugin` actually ingested, not what we meant to write. */ +/** Read back off disk — what the build-shaped writer actually wrote, not what we meant to write. */ let artifactOnDisk: Record; /** * Boot the showcase the way a deployment boots it. * - * `@objectstack/verify`'s lean harness composes no `MetadataPlugin` — its - * proofs are data/authorization ones that read metadata straight off the - * config. A declared endpoint cannot be reached without one: `matchEndpoint` - * is `IMetadataService`'s member, and `NodeMetadataManager` is its only - * implementation. So this file supplies it through the harness's `extraPlugins` - * seam, pointed at an artifact written from the showcase's own stack - * definition — the SAME `artifactSource: { mode: 'local-file' }` ingestion - * `createStandaloneStack` uses, which is what maps `apis:` → `api` metadata - * items (`ARTIFACT_FIELD_TO_TYPE` in `packages/metadata/src/plugin.ts`). + * A declared endpoint cannot be reached without a `MetadataPlugin`: + * `matchEndpoint` is `IMetadataService`'s member, and `NodeMetadataManager` is + * its only implementation. `@objectstack/verify`'s `bootStack` composes the one + * `objectstack serve` composes for a host configuration such as this one — no + * artifact, so `AppPlugin` registers the showcase's `apis:` as `api` items + * (#23088). This file used to hand in an artifact-backed `MetadataPlugin` + * through `extraPlugins`, because the harness composed none; it no longer does, + * so the endpoints below are the ones a deployment of this configuration + * serves, booted with nothing the harness does not compose itself. The + * artifact door's `apis:` ingestion keeps its own proofs + * (`declarative-endpoint-policy.dogfood.test.ts`, + * `declarative-endpoint-anonymous-guest.dogfood.test.ts`). * - * Writing the artifact through `ObjectStackDefinitionSchema` — which the - * ingest performs on load — means this boot ALSO re-proves the E7 publish - * gates accept the restored declarations. A declaration that could not publish - * could not be ingested here either. + * The artifact is still written, through the build's own + * `ObjectStackDefinitionSchema` parse, so this file still re-proves the E7 + * publish gates accept the restored declarations, and the cases in section 0 + * read it back. */ beforeAll(async () => { prevCwd = process.cwd(); @@ -137,13 +139,6 @@ beforeAll(async () => { new ConnectorRestPlugin(), new ConnectorOpenApiPlugin(), new ConnectorMcpPlugin({ declarativeStdio: ['node'] }), - new MetadataPlugin({ - rootDir: tempDir, - watch: false, - artifactWatch: false, - registerSystemObjects: false, - artifactSource: { mode: 'local-file', path: artifactPath }, - }), ], }); adminToken = await stack.signIn(); @@ -156,7 +151,7 @@ afterAll(async () => { }); // ============================================================================ -// 0. The artifact this boot ingests is the shape the build writes (commit c39a911ae) +// 0. The showcase's stand-in artifact is the shape the build writes (commit c39a911ae) // ============================================================================ describe('[#6293] the stand-in artifact carries what a built one carries', () => { From e29e58ce2d9693ab3b0a214121756e3a43f6e20d Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 00:42:41 +0000 Subject: [PATCH 4/8] test(verify): assert the metadata-door observations as one composite Claude-Session: https://claude.ai/code/session_01VoSxBQujKLZKPwK2u5ehQ6 Co-authored-by: Claude --- .../verify/src/harness.metadata-door.test.ts | 45 ++++++++++--------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/packages/verify/src/harness.metadata-door.test.ts b/packages/verify/src/harness.metadata-door.test.ts index 87f95f2b105..f12e00f0582 100644 --- a/packages/verify/src/harness.metadata-door.test.ts +++ b/packages/verify/src/harness.metadata-door.test.ts @@ -93,31 +93,32 @@ describe('#23088 bootStack serves a declared endpoint, as objectstack serve does it('composes a matcher, so the declared path is gated and served, and no matcher warning is logged', async () => { const booted = await bootCapturing(stackWithApi()); stack = booted.stack; - - // CONTROL on the composition: the slot carries the endpoint matcher. - const metadata = stack.kernel.getService<{ matchEndpoint?: unknown }>('metadata'); - expect(typeof metadata.matchEndpoint).toBe('function'); - - const anonymous = await stack.api(FEED, { method: 'GET' }); - const refused = (await anonymous.json()) as { error?: { code?: string; status?: number } }; - expect(anonymous.status, JSON.stringify(refused)).toBe(401); - expect(refused.error?.code).toBe('UNAUTHENTICATED'); - const token = await stack.signIn(); - const served = await stack.apiAs(token, 'GET', FEED); - expect(served.status, await served.clone().text()).toBe(200); - - // CONTROL: an undeclared path under the same mount keeps the bare 404. - const undeclared = await stack.apiAs(token, 'GET', '/apps/mdoor/no-such-endpoint'); - const miss = (await undeclared.json()) as { error?: { code?: string } }; - expect(undeclared.status).toBe(404); - expect(miss.error?.code).toBe('ENDPOINT_NOT_FOUND'); - const matcherWarnings = booted.lines.filter((line) => { - const fields = warnFields(line); - return fields?.appId === APP_ID && line.includes('MetadataPlugin'); + const answer = async (res: Response) => ({ + status: res.status, + code: ((await res.json()) as { error?: { code?: string } }).error?.code, + }); + const observed = { + // The slot carries the endpoint matcher. + matcher: typeof stack.kernel.getService<{ matchEndpoint?: unknown }>('metadata').matchEndpoint, + anonymous: await answer(await stack.api(FEED, { method: 'GET' })), + signedIn: (await stack.apiAs(token, 'GET', FEED)).status, + // CONTROL: an undeclared path under the same mount keeps the bare 404. + undeclared: await answer(await stack.apiAs(token, 'GET', '/apps/mdoor/no-such-endpoint')), + matcherWarnings: booted.lines.filter((line) => { + const fields = warnFields(line); + return fields?.appId === APP_ID && line.includes('MetadataPlugin'); + }), + }; + // One composite, so a failure shows every observation at once. + expect(observed).toEqual({ + matcher: 'function', + anonymous: { status: 401, code: 'UNAUTHENTICATED' }, + signedIn: 200, + undeclared: { status: 404, code: 'ENDPOINT_NOT_FOUND' }, + matcherWarnings: [], }); - expect(matcherWarnings).toEqual([]); }, BOOT_TIMEOUT); it("a caller's own MetadataPlugin in extraPlugins holds the slot, and the composed one is never registered", async () => { From 2ee03b04484dd980c8b011f94bd31bb91b043ecc Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 01:16:07 +0000 Subject: [PATCH 5/8] test(dogfood): keep naming the artifact door's apis mapping beside the proofs that own it Claude-Session: https://claude.ai/code/session_01VoSxBQujKLZKPwK2u5ehQ6 Co-authored-by: Claude --- .../test/showcase-declarative-endpoints.dogfood.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/qa/dogfood/test/showcase-declarative-endpoints.dogfood.test.ts b/packages/qa/dogfood/test/showcase-declarative-endpoints.dogfood.test.ts index 5e3a0c3271b..dba3df7e16f 100644 --- a/packages/qa/dogfood/test/showcase-declarative-endpoints.dogfood.test.ts +++ b/packages/qa/dogfood/test/showcase-declarative-endpoints.dogfood.test.ts @@ -96,7 +96,8 @@ let artifactOnDisk: Record; * through `extraPlugins`, because the harness composed none; it no longer does, * so the endpoints below are the ones a deployment of this configuration * serves, booted with nothing the harness does not compose itself. The - * artifact door's `apis:` ingestion keeps its own proofs + * artifact door's `apis:` → `api` ingestion (`ARTIFACT_FIELD_TO_TYPE` in + * `packages/metadata/src/plugin.ts`) keeps its own proofs * (`declarative-endpoint-policy.dogfood.test.ts`, * `declarative-endpoint-anonymous-guest.dogfood.test.ts`). * From 3867e0a3129ab7dbe5a54c37172daa585e40baef Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 02:16:39 +0000 Subject: [PATCH 6/8] test(verify): resolve @objectstack/metadata to source in this package's tests check:test-source-alias prescribes it: every verify test reaches the metadata package through harness.ts, and the gate's registry is shrink-only. Claude-Session: https://claude.ai/code/session_01VoSxBQujKLZKPwK2u5ehQ6 Co-authored-by: Claude --- packages/verify/vitest.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/verify/vitest.config.ts b/packages/verify/vitest.config.ts index 4efc91612b5..28d95429862 100644 --- a/packages/verify/vitest.config.ts +++ b/packages/verify/vitest.config.ts @@ -57,6 +57,7 @@ export default defineConfig({ resolve: { alias: [ { find: /^@objectstack\/core$/, replacement: path.resolve(HERE, '../core/src/index.ts') }, + { find: /^@objectstack\/metadata$/, replacement: path.resolve(HERE, '../metadata/src/index.ts') }, ], }, test: { From 8b2647eb1916cb9e8facf1c1553ebdc1e3d91228 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 02:18:46 +0000 Subject: [PATCH 7/8] test(dogfood): the federation pin asserts the harness's composed MetadataPlugin, not the fallback The precondition was written to red the day bootStack composes a metadata plugin; it now pins that occupant (registered, a MetadataManager, with the endpoint matcher). The header drops the objectstack start framing and names the seam pin that holds the init-ordering fix. The two federation cases are unchanged. Claude-Session: https://claude.ai/code/session_01VoSxBQujKLZKPwK2u5ehQ6 Co-authored-by: Claude --- ...al-validate-start-ordering.dogfood.test.ts | 51 +++++++++++-------- 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/packages/qa/dogfood/test/external-validate-start-ordering.dogfood.test.ts b/packages/qa/dogfood/test/external-validate-start-ordering.dogfood.test.ts index ef2f01a5bab..9ba00d85b81 100644 --- a/packages/qa/dogfood/test/external-validate-start-ordering.dogfood.test.ts +++ b/packages/qa/dogfood/test/external-validate-start-ordering.dogfood.test.ts @@ -1,29 +1,31 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. // -// [#21876] On a composition with no metadata plugin — the `objectstack start` -// shape — the federation service compares every federated object. +// [#21876] The federation service compares every federated object, on the +// composition the verify harness boots. // // ## What was broken // // `ExternalDatasourceServicePlugin.init()` read the `metadata` service once and -// kept the answer. `objectstack start` composes no metadata plugin: its -// `metadata` service is the kernel's in-memory fallback, registered after every -// plugin's `init()`, just before the start phase. So the federation service -// kept "no metadata service" for the life of the process. Measured on the -// showcase under `objectstack start`: `POST /datasources/:name/external/validate` -// answered `ok: true` with no rows, and the boot gate logged "all federated -// objects match their remote schema" with `objects: 0` — although two -// federated objects exist, and one of them could have drifted. Under -// `objectstack dev` (which composes the metadata plugin before this one) both -// objects were compared. The service now resolves `metadata` when it is used. +// kept the answer. On a composition with no metadata plugin, that service is +// the kernel's in-memory fallback, registered after every plugin's `init()`, +// just before the start phase. So the federation service kept "no metadata +// service" for the life of the process. Measured on the showcase at the time: +// `POST /datasources/:name/external/validate` answered `ok: true` with no rows, +// and the boot gate logged "all federated objects match their remote schema" +// with `objects: 0` — although two federated objects exist, and one of them +// could have drifted. The service now resolves `metadata` when it is used. +// That ordering fix is pinned at its seam, in +// `packages/services/service-datasource/src/__tests__/external-validate-reads-live-registry.test.ts`. // // ## Why this composition // -// The verify harness composes no metadata plugin either, so its `metadata` -// service is the same kernel fallback, registered at the same moment, as on -// `objectstack start`. The first case asserts that, so a harness that one day -// composes a metadata plugin cannot turn this file into a silent pass. The -// harness does not mount the federation service, so this file mounts it, as +// The verify harness composes `@objectstack/metadata`'s `MetadataPlugin`, the +// artifact-less one `objectstack serve` and `start` give a host configuration +// such as the showcase (#23088). The first case asserts that occupant: the +// plugin is registered, and the `metadata` service is its manager, with the +// endpoint matcher. So a change to what the harness composes cannot pass +// through this file unnoticed. The harness does not mount the federation +// service, so this file mounts it, as // `objectstack dev` and `start` do. It does not mount the boot gate either // (`createExternalValidationPlugin`, `@objectstack/runtime`): the gate's input // is the federation service's whole-farm sweep, `validateAll()`, which the @@ -41,6 +43,7 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import showcaseStack, { onEnable } from '@objectstack/example-showcase'; import { ExternalDatasourceServicePlugin, resolveSqliteDriver } from '@objectstack/service-datasource'; +import { MetadataManager } from '@objectstack/metadata'; import { type VerifyStack } from '@objectstack/verify'; import { bootShowcase } from './showcase-boot.js'; import { mkdtempSync, rmSync } from 'node:fs'; @@ -82,7 +85,7 @@ async function driftRemoteCustomers(): Promise { } } -describe('federation on a composition with no metadata plugin (the objectstack start shape)', () => { +describe('federation on the harness composition, which holds the metadata plugin', () => { let stack: VerifyStack; let token: string; let prevCwd: string; @@ -107,10 +110,14 @@ describe('federation on a composition with no metadata plugin (the objectstack s if (dir) rmSync(dir, { recursive: true, force: true }); }); - it('precondition: the metadata service is the kernel\'s in-memory fallback, as on objectstack start', () => { - const metadata = stack.kernel.getService<{ __serviceInfo?: { status?: string; message?: string } }>('metadata'); - expect(metadata.__serviceInfo?.status).toBe('degraded'); - expect(metadata.__serviceInfo?.message).toMatch(/^In-memory metadata registry/); + it('precondition: the metadata service is the composed MetadataPlugin\'s manager, with its endpoint matcher', () => { + // The plugin is registered under its own name, and the `metadata` slot is + // its `MetadataManager`, not the kernel's in-memory fallback (a plain + // object with no matcher). + expect(stack.kernel.hasPlugin('com.objectstack.metadata')).toBe(true); + const metadata = stack.kernel.getService<{ matchEndpoint?: unknown }>('metadata'); + expect(metadata).toBeInstanceOf(MetadataManager); + expect(typeof metadata.matchEndpoint).toBe('function'); }); it('validate compares each federated object, and reports the drifted column', async () => { From cf2065001eeab57c7734b30e71c04b60db5e5c9b Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 02:19:13 +0000 Subject: [PATCH 8/8] test(dogfood): two comments stop saying the harness composes no metadata plugin Comment lines only. Claude-Session: https://claude.ai/code/session_01VoSxBQujKLZKPwK2u5ehQ6 Co-authored-by: Claude --- .../external-validate-sees-runtime-save.dogfood.test.ts | 8 ++++---- .../dogfood/test/multi-package-artifact.dogfood.test.ts | 5 +++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/qa/dogfood/test/external-validate-sees-runtime-save.dogfood.test.ts b/packages/qa/dogfood/test/external-validate-sees-runtime-save.dogfood.test.ts index c7ecabaefae..21c7fa6122c 100644 --- a/packages/qa/dogfood/test/external-validate-sees-runtime-save.dogfood.test.ts +++ b/packages/qa/dogfood/test/external-validate-sees-runtime-save.dogfood.test.ts @@ -21,10 +21,10 @@ // an object saved through `PUT /meta/object/:name` is listed with a real // verdict (it declares a column the remote lacks, and exactly that column is // reported); an object imported through `…/external/tables/:remote/import` is -// listed beside them. The verify harness composes no metadata plugin, so its -// `metadata` service is the kernel's fallback registered after the -// federation service's `init()`, as on `objectstack start`; the service reads -// it when validation runs ([#21876]), so each row here is a real comparison. +// listed beside them. The verify harness composes `@objectstack/metadata`'s +// artifact-less `MetadataPlugin`, as `objectstack serve` does for a host +// configuration (#23088); the service resolves what it reads when validation +// runs ([#21876]), so each row here is a real comparison. // The seam pins sit beside the plugin // (`packages/services/service-datasource/src/__tests__/external-validate-reads-live-registry.test.ts`). // diff --git a/packages/qa/dogfood/test/multi-package-artifact.dogfood.test.ts b/packages/qa/dogfood/test/multi-package-artifact.dogfood.test.ts index fe7651b5b05..81a4a67e002 100644 --- a/packages/qa/dogfood/test/multi-package-artifact.dogfood.test.ts +++ b/packages/qa/dogfood/test/multi-package-artifact.dogfood.test.ts @@ -160,8 +160,9 @@ describe('dogfood: one artifact, two co-owning packages (ADR-0130 D4)', () => { // #14599 — the METADATA door, which the block above does not reach // ─────────────────────────────────────────────────────────────────────────── // -// `bootStack` registers `AppPlugin` and no `MetadataPlugin`, so everything -// above is measured on the ObjectQL registry alone. The defect this block pins +// `bootStack` registers `AppPlugin` and a `MetadataPlugin` with no artifact, +// which loads none of the stack (#23088), so everything above is measured on +// the ObjectQL registry alone. The defect this block pins // lives in the OTHER reader — `MetadataPlugin._parseAndRegisterArtifact`, the // door a real `objectstack dev` / `objectstack serve` boot loads the artifact // through — and the two only disagree once both are present. That is why this