11// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
22//
3- // [#21876] On a composition with no metadata plugin — the `objectstack start`
4- // shape — the federation service compares every federated object .
3+ // [#21876] The federation service compares every federated object, on the
4+ // composition the verify harness boots .
55//
66// ## What was broken
77//
88// `ExternalDatasourceServicePlugin.init()` read the `metadata` service once and
9- // kept the answer. `objectstack start` composes no metadata plugin: its
10- // `metadata` service is the kernel's in-memory fallback, registered after every
11- // plugin's `init()`, just before the start phase. So the federation service
12- // kept "no metadata service" for the life of the process. Measured on the
13- // showcase under `objectstack start`: ` POST /datasources/:name/external/validate`
14- // answered `ok: true` with no rows, and the boot gate logged "all federated
15- // objects match their remote schema" with `objects: 0` — although two
16- // federated objects exist, and one of them could have drifted. Under
17- // `objectstack dev` (which composes the metadata plugin before this one) both
18- // objects were compared. The service now resolves `metadata` when it is used .
9+ // kept the answer. On a composition with no metadata plugin, that service is
10+ // the kernel's in-memory fallback, registered after every plugin's `init()`,
11+ // just before the start phase. So the federation service kept "no metadata
12+ // service" for the life of the process. Measured on the showcase at the time:
13+ // ` POST /datasources/:name/external/validate` answered `ok: true` with no rows,
14+ // and the boot gate logged "all federated objects match their remote schema"
15+ // with `objects: 0` — although two federated objects exist, and one of them
16+ // could have drifted. The service now resolves `metadata` when it is used.
17+ // That ordering fix is pinned at its seam, in
18+ // `packages/services/ service-datasource/src/__tests__/external-validate-reads-live-registry.test.ts` .
1919//
2020// ## Why this composition
2121//
22- // The verify harness composes no metadata plugin either, so its `metadata`
23- // service is the same kernel fallback, registered at the same moment, as on
24- // `objectstack start`. The first case asserts that, so a harness that one day
25- // composes a metadata plugin cannot turn this file into a silent pass. The
26- // harness does not mount the federation service, so this file mounts it, as
22+ // The verify harness composes `@objectstack/metadata`'s `MetadataPlugin`, the
23+ // artifact-less one `objectstack serve` and `start` give a host configuration
24+ // such as the showcase (#23088). The first case asserts that occupant: the
25+ // plugin is registered, and the `metadata` service is its manager, with the
26+ // endpoint matcher. So a change to what the harness composes cannot pass
27+ // through this file unnoticed. The harness does not mount the federation
28+ // service, so this file mounts it, as
2729// `objectstack dev` and `start` do. It does not mount the boot gate either
2830// (`createExternalValidationPlugin`, `@objectstack/runtime`): the gate's input
2931// is the federation service's whole-farm sweep, `validateAll()`, which the
4143import { describe , it , expect , beforeAll , afterAll } from 'vitest' ;
4244import showcaseStack , { onEnable } from '@objectstack/example-showcase' ;
4345import { ExternalDatasourceServicePlugin , resolveSqliteDriver } from '@objectstack/service-datasource' ;
46+ import { MetadataManager } from '@objectstack/metadata' ;
4447import { type VerifyStack } from '@objectstack/verify' ;
4548import { bootShowcase } from './showcase-boot.js' ;
4649import { mkdtempSync , rmSync } from 'node:fs' ;
@@ -82,7 +85,7 @@ async function driftRemoteCustomers(): Promise<void> {
8285 }
8386}
8487
85- describe ( 'federation on a composition with no metadata plugin ( the objectstack start shape) ' , ( ) => {
88+ describe ( 'federation on the harness composition, which holds the metadata plugin ' , ( ) => {
8689 let stack : VerifyStack ;
8790 let token : string ;
8891 let prevCwd : string ;
@@ -107,10 +110,14 @@ describe('federation on a composition with no metadata plugin (the objectstack s
107110 if ( dir ) rmSync ( dir , { recursive : true , force : true } ) ;
108111 } ) ;
109112
110- it ( 'precondition: the metadata service is the kernel\'s in-memory fallback, as on objectstack start' , ( ) => {
111- const metadata = stack . kernel . getService < { __serviceInfo ?: { status ?: string ; message ?: string } } > ( 'metadata' ) ;
112- expect ( metadata . __serviceInfo ?. status ) . toBe ( 'degraded' ) ;
113- expect ( metadata . __serviceInfo ?. message ) . toMatch ( / ^ I n - m e m o r y m e t a d a t a r e g i s t r y / ) ;
113+ it ( 'precondition: the metadata service is the composed MetadataPlugin\'s manager, with its endpoint matcher' , ( ) => {
114+ // The plugin is registered under its own name, and the `metadata` slot is
115+ // its `MetadataManager`, not the kernel's in-memory fallback (a plain
116+ // object with no matcher).
117+ expect ( stack . kernel . hasPlugin ( 'com.objectstack.metadata' ) ) . toBe ( true ) ;
118+ const metadata = stack . kernel . getService < { matchEndpoint ?: unknown } > ( 'metadata' ) ;
119+ expect ( metadata ) . toBeInstanceOf ( MetadataManager ) ;
120+ expect ( typeof metadata . matchEndpoint ) . toBe ( 'function' ) ;
114121 } ) ;
115122
116123 it ( 'validate compares each federated object, and reports the drifted column' , async ( ) => {
0 commit comments