Skip to content

Commit c90b5b1

Browse files
fix(metadata-protocol)!: refuse an object whose datasource names no datasource at save and publish (#26912)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"} --> _Requested by **Leehom** · [project thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1X3b8PhkSKHUoQZv2MEwGvNf)_ Fixes #26724 Clause-②: no (narrowing) Metadata-protocol: narrowed Skills-docs: none — the refusal's finding names the datasource and the three remedies, and `skills/objectstack-data/rules/datasources.md` already teaches that an object binds a datasource by its declared name; no skill or doc states the old acceptance. ## 维护者速读 - **改了什么**:对象的 `datasource` 写了一个不存在的数据源名时,保存(草稿与直接发布)、发布(单项发布与整包 publish-drafts)以及回滚/还原都会被拒绝,返回 422 `INVALID_METADATA`,错误定位在 `datasource` 并点名那个数据源。 - **为什么改**:以前这种对象能保存、能发布(回执 "published"),之后却永远注册不上,所有数据接口都回 404 "not registered",作者和 AI 都不知道原因。 - **风险与回滚**:只收紧一个以前就用不了的写法;运行时注册的驱动、包里声明或环境里保存的数据源、同包同批的数据源草稿、对象本来就绑定的值都照常通过。回滚即还原本 PR,无数据迁移。 - **先看**:`packages/metadata-protocol/src/protocol-save-door-refusals.ts` 的 `unknownDatasourceSaveRefusal`("什么算存在"的判定);`protocol.ts` 中 `saveMetaItem` 与 `promoteDraftForPublish` 的两处调用;`packages/qa/dogfood/test/object-unknown-datasource-refusal.dogfood.test.ts`。 - **两分钟验证**:`pnpm --filter @objectstack/metadata-protocol exec vitest run src/protocol.unknown-datasource-refusal.test.ts`,预期 15 个用例全绿,其中拒绝用例在 main 上为红。 - **需要你做的**:无,席位落地。 ## What changes - **The refusal.** A new save-door refusal, `unknownDatasourceSaveRefusal` (body in `protocol-save-door-refusals.ts`, method on `ObjectStackProtocolImplementation`), refuses an `object` whose `datasource` names a datasource that does not exist here: `422 INVALID_METADATA`, one finding at `datasource`, "Object 'NAME' names datasource 'DS', and no datasource of that name exists here: none is declared (a `*.datasource.ts` or a saved `datasource`) and no driver is registered under it. …", with the remedies. The envelope is the one the external-binding refusal (#25211) already answers at the same doors, so no error code is added. - **The seam, and why it covers save and publish.** It sits beside `externalBindingSaveRefusal` at its three call sites. `saveMetaItem` asks it after the schema gate in draft and live mode, which is every `PUT /api/v1/meta/object/:name`. `promoteDraftForPublish` asks it on the draft body before `repo.promoteDraft` writes; that is the one promotion both `publishMetaItem` (`POST /meta/object/:name/publish`) and `publishPackageDrafts` (`POST /packages/:id/publish-drafts`) route through, so a refusal there aborts the batch like the other per-item refusals do. The restore doors (`rollbackMetaItem`, `revertCommit`) ask it through `refuseRestoredBodyTheSaveDoorRefuses`, which keeps that family complete. The 404 itself comes from registration (`resolveDatasourceBinding` step 1 routes an explicit `datasource` to the driver of that name only), but registration runs after the write has committed, so the refusal belongs at the door. - **What "exists" means, read from the code.** Not refused: no `datasource`, `''` or `'default'`; a driver the engine has registered under the name (`IDataEngine.getDriverByName`, which covers a `driver.*` service and an `onEnable` `drivers.register`, neither of which declares a datasource item); a datasource item the by-name read resolves (`readMetaItem`: the environment's stored row, else the code layer's `*.datasource.ts`), connected or not; on a draft save, any datasource draft of that name; on a package publish, a datasource draft of the same batch (`publishPackageDrafts` passes the batch's datasource draft names as `batchDatasources`, because `listDrafts` gives no type order and the object can be promoted first); and the binding the object already holds. An engine without `getDriverByName` cannot say a name is unknown, so nothing is refused on it. - **The late-registration path, and why it does not refuse a valid app.** A declaration boot (`os migrate`, `skipOnEnable`) never runs `onEnable`, so a driver an app registers there is absent from that boot, and `os migrate meta --stored` and the organization-ownership ceremony re-save stored objects through `saveMetaItem`. They keep the binding the stored object already holds, and a binding the object already holds is not judged, so they keep working. Every HTTP save runs after the kernel has started, when every `onEnable` and `driver.*` registration has happened. I found no other path that registers a datasource after an object that names it is saved. - **Overlap with #26708.** It refuses a save naming an uninstalled package (`uninstalledNamedBaseRefusal`, `WRITABLE_PACKAGE_REQUIRED`). This check stays local to the datasource seam and shares no helper with it. The only common part is "a name the save carries must resolve", and the two answer different envelopes (a 422 naming the package vs a located `issues[]` finding), so there is nothing to share yet. `git merge-tree` against #26708's head `0da44906`: `protocol.ts` (the one shared file) auto-merges; the only conflict is `packages/runtime/src/domains/packages.ts`, between #26708 and `main` itself, which this diff does not touch. ## Why An author, or an AI writing metadata, who mistypes a datasource name got "published" for an object that can never serve a row, and no pointer to the cause. The door now says so at the save, naming the datasource. ## Tests - `packages/metadata-protocol/src/protocol.unknown-datasource-refusal.test.ts` (new, 15 cases), through `saveMetaItem`, `publishMetaItem` and `publishPackageDrafts` over a stateful `sys_metadata` double. Refusals: draft save into a package, draft save naming none, live save, live save under `force`; a draft whose datasource is still only a draft, published on its own; a draft stored before the rule, at `publishMetaItem` and at `publishPackageDrafts` (`outcome: 'refused'`, `published: []`). Controls: no `datasource`, `'default'`, a run-time-registered driver, a code-layer datasource, a stored datasource, a same-package datasource draft promoted AFTER the object in one batch (the order is asserted), a re-save keeping the held binding (and moving it to another unknown name is refused), and an engine with no `getDriverByName`. - `packages/metadata-protocol/src/protocol.restore-doors-save-refusal-family.test.ts`: one more `pinBothDoors` case, an object version bound to a driver that has since gone, refused at `rollbackMetaItem` and at `revertCommit`, with the family's controls. - `packages/qa/dogfood/test/object-unknown-datasource-refusal.dogfood.test.ts` (new, 5 cases) over the booted showcase and the REST doors: the card's draft save and a live save are refused 422 naming the datasource, nothing is listed in `/meta/_drafts` and `/data` answers 404; controls each saved as a draft, published with `publish-drafts` and served through `POST`/`GET /api/v1/data`: no `datasource`; a driver a plugin registers as a `driver.*` service under a name no datasource item declares; and a datasource drafted in the same package and published in the same batch. <details><summary>Red on main, green on the fix</summary> - Protocol doors, with `protocol.ts`, `protocol-save-door-refusals.ts` and `protocol-authoring-gate.ts` checked out at `96e7a433` and the new tests kept: `Tests 10 failed | 37 passed (47)`. Every refusal case is red (both restore doors, the four saves, both publish doors, the draft-only datasource publish, and the second half of the held-binding control); every other control is green. Restored with `git checkout HEAD --`, blob hashes equal to `HEAD`, `git diff HEAD` empty. - Real stack, the same three files at `96e7a433` with `@objectstack/metadata-protocol` rebuilt (`ablation-dist-preflight` confirmed the marker absent from `dist/`): `Tests 2 failed | 3 passed (5)`. The draft save answered `200 {"success":true,…,"state":"draft","message":"Saved object 'dsprobe_badds' (env-wide, package=app.dsprobe26724, state=draft) [seq=1]"}` and the live save `200 …"state":"active"…`; the three controls were green. Restored and rebuilt, preflight confirmed the marker present. - On the fix: protocol doors `Tests 15 passed (15)` and the restore family `32 passed (32)`; real stack `Tests 5 passed (5)`. </details> ## Acceptance notes - **The other half of the pair is not judged.** Deleting or renaming a datasource that objects still bind (`DELETE /meta/datasource/:name`) would leave those objects in the same 404 state. The card asks about the object's save, and I did not measure that door, so this is noted, not filed. - **Residue stays re-saveable.** An object already stored with an unknown datasource keeps re-saving while its `datasource` is unchanged (that exemption is what keeps `os migrate` re-saves working). `os migrate meta --stored` does not list such rows. - **`Metadata-protocol: narrowed`, `minor`.** Ruled by the narrowing contract review (comment 6453465645): a datasource name the door stored before is now refused, the same declaration as #25211's external-binding refusal. - **Test-double limit.** The protocol double's `transaction` does not roll back, so the batch-refusal case reads `publishedCount: 0, published: []` and the causal draft, not the sibling's row. The real engine's Phase-1 transaction is what rolls the sibling back. ## Verification All on head `79ec0859` unless noted. - `pnpm --filter @objectstack/metadata-protocol test`: `Test Files 271 passed | 4 skipped (275)`, `Tests 21685 passed | 26 skipped (21711)`, exit 0; `typecheck` exit 0 (run before the fixture-only commit `79ec0859`; that commit touches one test double and one ledger file). - `vitest run --project repo` for core, objectql, rest, runtime and types: 6/6, 1/1, 5/5, 3/3 and 1/1 files, each exit 0. - `eslint packages/metadata-protocol packages/qa/dogfood/test/object-unknown-datasource-refusal.dogfood.test.ts --no-inline-config`: exit 0. - Derived gates (`dispatch-gates.mjs --commands`), each run with its exit captured before any pipe: 73 of 73, all exit 0. `dispatch-gates --ran`: "73 derived famil(ies) accounted for — 73 run, 0 NOT-MEASURED". The first pass had four non-zero exits, each resolved: `check-plugin-teardown-shape --self-test` exit 3 on the shallow clone (green after `git fetch --depth=1` of its fixture commit); `check:dual-build-cjs-loads` exit 3, PREREQUISITE NOT MET (green after the full build); `check:engine-double-contract` (the ledger learnt the new file's doubles); and `check:where-matcher` (the history double now refuses an operator it does not match). - Dogfood battery: on `79ec0859`, `pnpm --filter @objectstack/dogfood test` under the lock after the `@objectstack/dogfood^...` build: VERDICT command-exit 1 · `Test Files 1 failed | 327 passed | 3 skipped (331)` · `Tests 2 failed | 2686 passed | 20 skipped (2708)`. The two failures are both in `packaged-action-disabled-dispatch-live.dogfood.test.ts` (#26572's file): `acceptance[0]` read `after.done` true at line 153, and `acceptance[5] / [2]` read a different action row id at line 244. That file passes alone on the same head twice (`Tests 8 passed (8)`, exit 0 both times), and the full battery on `1735274f` (the same product tree; `79ec0859` adds only a test double and a ledger row) was VERDICT command-exit 0 · `Test Files 328 passed | 3 skipped (331)` · `Tests 2688 passed | 20 skipped (2708)`. This diff refuses nothing that is not an `object` save, so I read it as a load-dependent flake in that file, not a consequence of this change. It is reported here, not skipped or narrowed. --- _Generated by [Claude Code](https://claude.ai/code/session_01FEkQ5nuvdPiLSVSMGPCsuK)_ --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent e1ab32e commit c90b5b1

8 files changed

Lines changed: 703 additions & 2 deletions
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@objectstack/metadata-protocol': minor
3+
---
4+
5+
fix(metadata-protocol)!: the metadata save doors refuse an object whose `datasource` names a datasource that does not exist, instead of storing and publishing an object that never registers (#26724)
6+
7+
Clause-②: no (narrowing) — `saveMetaItem` (draft and live), the draft promotion behind `publishMetaItem` and `publishPackageDrafts`, and the restore doors (`rollbackMetaItem`, `revertCommit`) refuse an `object` whose `datasource` names no registered driver, no declared or stored datasource and no pending datasource draft, which they stored before. Nothing is widened.
8+
Metadata-protocol: narrowed
9+
10+
<!-- adr-0087: not-required (no-migration-prescription) No key of any schema is added, removed, renamed or re-shaped, and no export or type changes: `ObjectSchema.datasource` already names a datasource ID, and the door now refuses an ID that names none, which never worked. A stored object that already holds such a binding keeps re-saving; there is nothing for a migration to rewrite. -->
11+
12+
**BREAKING** accept-set narrowing at the metadata save and publish doors (`@objectstack/metadata-protocol`), served by `PUT /api/v1/meta/object/:name`, `POST /api/v1/meta/object/:name/publish` and `POST /api/v1/packages/:id/publish-drafts`.
13+
14+
- **Before.** An object saved with a misspelled `datasource` (`gapdata_nosuch`) was stored, and `publish-drafts` answered `outcome: 'published'`. The engine routes an explicit `datasource` to the driver of that name only, so the object never registered: every `/api/v1/data/:object` call answered 404 "not registered", with no pointer to the cause.
15+
- **Now.** The save answers `422 INVALID_METADATA` with one finding at `datasource`: "Object 'NAME' names datasource 'DS', and no datasource of that name exists here: …". Nothing is stored. A draft stored before this is refused at its publish the same way, and a package publish then promotes nothing.
16+
- **What exists.** No `datasource`, or `'default'`; a driver the engine has registered under the name (a `driver.*` service or an `onEnable` `drivers.register`, with no datasource item); a datasource item the code layer declares (`*.datasource.ts`) or this environment stores, connected or not; on a draft save, any datasource draft of that name; on a package publish, a datasource draft of the same batch.
17+
- **Not judged.** A save, publish or restore that keeps the `datasource` the object already holds: the binding is not that write's. So `os migrate`'s re-saves, which never run `onEnable`, keep working for an object bound to a driver registered there.
18+
19+
**The remedy.** Correct the name, declare the datasource first (or draft it in the same package and publish both together), or remove `datasource` to use the primary datasource.

‎packages/metadata-protocol/src/protocol-authoring-gate.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ export interface AuthoringGateHost {
8484
rlsMembershipResolverKeys: ObjectStackProtocolImplementation['rlsMembershipResolverKeys'];
8585
runAuthoringGate: ObjectStackProtocolImplementation['runAuthoringGate'];
8686
tenancyPostureInForce: ObjectStackProtocolImplementation['tenancyPostureInForce'];
87+
unknownDatasourceSaveRefusal: ObjectStackProtocolImplementation['unknownDatasourceSaveRefusal'];
8788
}
8889

8990
/** The body of `ObjectStackProtocolImplementation#restoredBodyDerivation`; the method's docblock there is its contract. */
@@ -111,7 +112,7 @@ export function restoredBodyWriter(
111112

112113
/** The body of `ObjectStackProtocolImplementation#refuseRestoredBodyTheSaveDoorRefuses`; the method's docblock there is its contract. */
113114
export async function refuseRestoredBodyTheSaveDoorRefuses(
114-
this: Pick<AuthoringGateHost, 'anonymousElevatedFlowRefusal' | 'assertRuntimeAuthoringRules' | 'authoringChannel' | 'credentialChannelFor' | 'defaultRecordPageSaveRefusal' | 'externalBindingSaveRefusal' | 'runAuthoringGate'>,
115+
this: Pick<AuthoringGateHost, 'anonymousElevatedFlowRefusal' | 'assertRuntimeAuthoringRules' | 'authoringChannel' | 'credentialChannelFor' | 'defaultRecordPageSaveRefusal' | 'externalBindingSaveRefusal' | 'runAuthoringGate' | 'unknownDatasourceSaveRefusal'>,
115116
type: string,
116117
name: string,
117118
body: unknown,
@@ -124,6 +125,8 @@ export async function refuseRestoredBodyTheSaveDoorRefuses(
124125
if (defaultPageRefusal) throw defaultPageRefusal;
125126
const bindingRefusal = await this.externalBindingSaveRefusal(type, body, name);
126127
if (bindingRefusal) throw bindingRefusal;
128+
const datasourceRefusal = await this.unknownDatasourceSaveRefusal(type, body, name);
129+
if (datasourceRefusal) throw datasourceRefusal;
127130
const elevatedRefusal = await this.anonymousElevatedFlowRefusal(type, body, name);
128131
if (elevatedRefusal) throw elevatedRefusal;
129132
const channel = this.credentialChannelFor(type);

‎packages/metadata-protocol/src/protocol-save-door-refusals.ts‎

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ export interface SaveDoorRefusalHost {
169169
externalBindingDatasourceDefinition: ObjectStackProtocolImplementation['externalBindingDatasourceDefinition'];
170170
getOverlayRepo: ObjectStackProtocolImplementation['getOverlayRepo'];
171171
readonly getServicesRegistry: ObjectStackProtocolImplementation['getServicesRegistry'];
172+
listDrafts: ObjectStackProtocolImplementation['listDrafts'];
172173
objectsBoundExternallyTo: ObjectStackProtocolImplementation['objectsBoundExternallyTo'];
173174
overlayLockLayerAt: ObjectStackProtocolImplementation['overlayLockLayerAt'];
174175
readActiveOverlayRows: ObjectStackProtocolImplementation['readActiveOverlayRows'];
@@ -312,6 +313,65 @@ export async function externalBindingDatasourceDefinition(
312313
return definition && typeof definition === 'object' ? definition as { schemaMode?: unknown } : undefined;
313314
}
314315

316+
/**
317+
* The body of `ObjectStackProtocolImplementation#unknownDatasourceSaveRefusal`;
318+
* the method's docblock there is its contract. [#26724] Written here after the
319+
* cut-6 move, beside the external-binding refusal it sits next to at every
320+
* door, rather than moved: its reads are the same by-name read
321+
* (`readMetaItem`) plus the engine's registered drivers.
322+
*/
323+
export async function unknownDatasourceSaveRefusal(
324+
this: Pick<SaveDoorRefusalHost, 'engine' | 'listDrafts' | 'readMetaItem'>,
325+
singularType: string,
326+
item: unknown,
327+
name: string,
328+
pending?: 'drafts' | ReadonlySet<string>,
329+
writeFace?: MetadataWriteFace,
330+
): Promise<(Error & { code: 'INVALID_METADATA'; status: 422; issues: MetadataIssueEntry[] }) | undefined> {
331+
if (singularType !== 'object') return undefined;
332+
if (!item || typeof item !== 'object' || Array.isArray(item)) return undefined;
333+
const raw = (item as { datasource?: unknown }).datasource;
334+
if (typeof raw !== 'string' || raw === '' || raw === 'default') return undefined;
335+
const datasource = raw;
336+
// An engine that cannot name its registered drivers cannot say a name is
337+
// unknown: a driver registered at run time (a `driver.*` service, an
338+
// `onEnable` `drivers.register`) declares no datasource item.
339+
const getDriverByName = (this.engine as { getDriverByName?: unknown } | undefined)?.getDriverByName;
340+
if (typeof getDriverByName !== 'function') return undefined;
341+
if (getDriverByName.call(this.engine, datasource)) return undefined;
342+
// A declared datasource: the environment's stored row, else the code
343+
// layer's definition. Its driver may not be connected (yet, or ever);
344+
// that is reported where it is used, not refused here.
345+
if ((await this.readMetaItem({ type: 'datasource', name: datasource }))?.item) return undefined;
346+
// A datasource still pending: any draft, for a draft save; the batch's
347+
// own drafts, for a package publish.
348+
const pendingDraft = pending instanceof Set
349+
? pending.has(datasource)
350+
: pending === 'drafts' && (await this.listDrafts({ type: 'datasource' })).drafts.some((d) => d.name === datasource);
351+
if (pendingDraft) return undefined;
352+
// The binding the object already holds is not this save's: a re-save, a
353+
// stored-row migration or a restore that keeps it is judged at the save
354+
// that introduced it. (A declaration boot never runs `onEnable`, so a
355+
// driver registered there is absent from it.)
356+
const current = (await this.readMetaItem({ type: 'object', name }))?.item as { datasource?: unknown } | undefined;
357+
if (current?.datasource === datasource) return undefined;
358+
const issues: MetadataIssueEntry[] = [{
359+
path: 'datasource',
360+
message: `Object '${name}' names datasource '${datasource}', and no datasource of that name exists here: `
361+
+ `none is declared (a \`*.datasource.ts\` or a saved \`datasource\`) and no driver is registered under it. `
362+
+ 'An object bound to it never registers, so every data call on it would answer 404. Correct the name, '
363+
+ `declare datasource '${datasource}' first, or remove \`datasource\` to use the primary datasource ('default').`,
364+
code: 'custom',
365+
}];
366+
const err = new Error(
367+
`${singularType}/${name} failed spec validation: ${specValidationFindings(writeFace, issues)}`,
368+
) as Error & { code: 'INVALID_METADATA'; status: 422; issues: MetadataIssueEntry[] };
369+
err.code = 'INVALID_METADATA';
370+
err.status = 422;
371+
err.issues = issues;
372+
return err;
373+
}
374+
315375
/** The body of `ObjectStackProtocolImplementation#objectsBoundExternallyTo`; the method's docblock there is its contract. */
316376
export async function objectsBoundExternallyTo(
317377
this: Pick<SaveDoorRefusalHost, 'engine' | 'readActiveOverlayRows' | 'rethrowUnlessMetadataStoreUnprovisioned' | 'storedOverlayEntries'>,

‎packages/metadata-protocol/src/protocol.restore-doors-save-refusal-family.test.ts‎

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
* body back as the active row (`repo.restoreVersion`). `saveMetaItem` refuses a
88
* family of shapes after its schema gate: a hook with no `body`, a second
99
* default record page of an object, an `external` object binding on a managed
10-
* datasource (both halves of the pair), a `runAs: 'system'` flow an anonymous
10+
* datasource (both halves of the pair), an object `datasource` that names no
11+
* datasource (#26724), a `runAs: 'system'` flow an anonymous
1112
* endpoint names, the shared runtime authoring rules, and the authoring gate a
1213
* domain plugin registers. Measured on `origin/main` `5801a4f2c` before this
1314
* file: each restore door wrote every one of those shapes back, so a version
@@ -92,6 +93,8 @@ function makeStubEngine() {
9293
const rows = new Map<string, Row>();
9394
const historyRows: HistoryRow[] = [];
9495
const registered = new Map<string, Map<string, unknown>>();
96+
// [#26724] The drivers the engine has registered by name, with no datasource item.
97+
const drivers = new Set<string>();
9598
let nextId = 0;
9699
let commit: unknown = null;
97100

@@ -169,6 +172,7 @@ function makeStubEngine() {
169172
return { deleted: 1 };
170173
},
171174
async count() { return 0; },
175+
getDriverByName: (name: string) => (drivers.has(name) ? { name } : undefined),
172176
async syncObjectSchema() { return true; },
173177
async dropObjectSchema() { return true; },
174178
registry: {
@@ -195,6 +199,7 @@ function makeStubEngine() {
195199
rows,
196200
historyRows,
197201
registered,
202+
drivers,
198203
serveCommit: (c: unknown) => { commit = c; },
199204
};
200205
}
@@ -476,6 +481,27 @@ pinBothDoors({
476481
expectDetail: (error) => expect(error.issues).toEqual([expect.objectContaining({ path: 'runAs', code: 'custom' })]),
477482
});
478483

484+
pinBothDoors({
485+
title: 'an object restored with a `datasource` that names nothing now (unknownDatasourceSaveRefusal)',
486+
type: 'object',
487+
name: 'wh_order',
488+
code: 'INVALID_METADATA',
489+
status: 422,
490+
async stage(h) {
491+
h.drivers.add('rt_store');
492+
await h.protocol.saveMetaItem({ type: 'object', name: 'wh_order', item: order({ datasource: 'rt_store' }) });
493+
await h.protocol.saveMetaItem({ type: 'object', name: 'wh_order', item: order({ datasource: undefined }) });
494+
await h.protocol.saveMetaItem({ type: 'object', name: 'wh_order', item: order({ datasource: undefined, label: 'Order v3' }) });
495+
// The driver v1 was bound to is gone.
496+
h.drivers.delete('rt_store');
497+
return { refused: 1, accepted: 2 };
498+
},
499+
expectDetail: (error) => {
500+
expect(error.issues).toEqual([expect.objectContaining({ path: 'datasource', code: 'custom' })]);
501+
expect(error.issues[0].message.startsWith("Object 'wh_order' names datasource 'rt_store', and no datasource")).toBe(true);
502+
},
503+
});
504+
479505
pinBothDoors({
480506
title: 'a body the runtime authoring rules refuse (assertRuntimeAuthoringRules)',
481507
type: 'object',

0 commit comments

Comments
 (0)