From d0d3bccc5219c78d576c0a3b58eb478554b11910 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 13:47:18 +0000 Subject: [PATCH 1/3] fix(rest): /meta/diagnostics refuses an unknown type, severity or package and keys aliases canonically An unknown ?type=, ?severity= or ?package= used to be answered 200 with zero issues, and ?type=views keyed its stats `views`. The route now refuses all three with 400 INVALID_REQUEST in its one nested envelope: severity in the route, type by the list door's own verdict after the sweep, and package in the sweep (not installed and no item bound to it). The sweep keys a named type by its canonical spelling. Claude-Session: https://claude.ai/code/session_01L6bhgTiSuMwZbnqidCwkCM Co-authored-by: Claude --- .../27514-diagnostics-unknown-params.md | 17 ++ .../src/protocol-meta-diagnostics.ts | 52 +++++- .../protocol.diagnostics-store-outage.test.ts | 4 +- packages/rest/src/meta-collection-routes.ts | 65 ++++++- ...st-meta-diagnostics-unknown-params.test.ts | 171 ++++++++++++++++++ .../rest-server-meta-read-org-scope.test.ts | 4 +- 6 files changed, 308 insertions(+), 5 deletions(-) create mode 100644 .changeset/27514-diagnostics-unknown-params.md create mode 100644 packages/rest/src/rest-meta-diagnostics-unknown-params.test.ts diff --git a/.changeset/27514-diagnostics-unknown-params.md b/.changeset/27514-diagnostics-unknown-params.md new file mode 100644 index 00000000000..23fdff213c2 --- /dev/null +++ b/.changeset/27514-diagnostics-unknown-params.md @@ -0,0 +1,17 @@ +--- +'@objectstack/rest': patch +'@objectstack/metadata-protocol': patch +--- + +fix(rest): `GET /meta/diagnostics` refuses an unknown type, severity or package instead of answering an all-clear, and keys an alias's stats by its canonical type (#27514) + +Clause-②: no (narrowing — owes one contract-review-tier review before the queue) +Metadata-protocol: none + +`/meta/diagnostics` is the door an author, an agent or CI reads to learn whether metadata is healthy, and three query values it could not honour were answered `200` with zero issues. + +- **`?type=`** naming no metadata type (`zzz_unknown`) is refused with `400 INVALID_REQUEST`, by the same verdict `GET /meta/:type` uses: the platform's static type spelling, then the types this deployment carries live. A near-miss spelling of a declared type (`viewes`) keeps its existing refusal, which names the spellings it was reaching for. +- **`?severity=`** other than `error` or `warning` is refused with `400 INVALID_REQUEST`. An empty value still means the default, `error`. +- **`?package=`** naming a package that is not installed, and that no swept item is bound to, is refused with `400 INVALID_REQUEST`. An installed package with no items still answers its zeros, items bound to an uninstalled package are still swept, and `package=all` still sweeps every package. +- **An alias** (`?type=views`) is swept and keyed in `stats` and `entries` under its canonical type (`view`), so it answers exactly what `?type=view` answers. +- **One envelope.** Every refusal of this route now answers the nested `{ error: { code, message } }` the repeated-parameter refusal already used. The near-miss type refusal used to answer `{ error: '…', code }`. diff --git a/packages/metadata-protocol/src/protocol-meta-diagnostics.ts b/packages/metadata-protocol/src/protocol-meta-diagnostics.ts index 6cde52c5fbc..4f60f47c818 100644 --- a/packages/metadata-protocol/src/protocol-meta-diagnostics.ts +++ b/packages/metadata-protocol/src/protocol-meta-diagnostics.ts @@ -36,6 +36,7 @@ import type { ObjectStackProtocolImplementation, ShippingPackages } from './protocol.js'; import { resolveItemLock } from './item-lock.js'; import { DEFAULT_METADATA_TYPE_REGISTRY, getMetadataTypeSchema } from '@objectstack/spec/kernel'; +import { canonicalMetaUrlType } from '@objectstack/spec/shared'; import { computeMetadataDiagnostics } from './metadata-diagnostics.js'; import type { MetadataDiagnostics } from './metadata-diagnostics.js'; @@ -77,8 +78,15 @@ export async function getMetaDiagnostics( stats: Record; }> { const includeWarnings = request.severity === 'warning'; + // [#27514] A named type is swept, and keyed in `stats` and `entries`, under + // its CANONICAL spelling: the fold `canonicalMetaType` applies at every + // `/meta` entry point. `getMetaItems` already listed `views` as `view`, but + // the answer was keyed by the raw alias, so a consumer reading `stats.view` + // missed an alias request's counts. A spelling the fold does not map + // (`viewes`, `fieldz`) comes back unchanged, so the [#8924] refusal below + // still sees what the caller wrote. const targetTypes = request.type - ? [request.type] + ? [canonicalMetaUrlType(request.type)] : DEFAULT_METADATA_TYPE_REGISTRY .filter((e) => getMetadataTypeSchema(e.type)) .map((e) => e.type); @@ -240,6 +248,29 @@ export async function getMetaDiagnostics( stats[t] = { count: items.length, locked: lockedCount, packages: [...pkgSet].sort() }; } + // [#27514] A package that is not installed, and that no swept item is bound + // to, scoped the sweep to nothing: every type answered `count: 0` and the + // whole answer read as a clean package. Refused instead, with the [#8924] + // refusal's status and code. Both halves are read, in this order: an item + // bound to the package is proof enough (rows of an uninstalled package are + // real, and this sweep is how an author finds them), and only an empty + // sweep asks the installed-package listing (`getMetaItems` of type + // `package`, the registry's records). An installed package with no items + // still answers its honest zeros. Fails OPEN: a listing that cannot be read + // keeps the answer, because "no such package" is an existence claim and an + // unreadable authority cannot make it. + if (request.packageId !== undefined && scannedItems === 0 + && !(await packageIsInstalled(this, request.packageId))) { + const err = new Error( + `'${request.packageId}' is not an installed package, and no metadata item is bound to it, so a sweep ` + + `scoped to it would report a clean package that does not exist. GET /api/v1/packages lists the ` + + `installed packages; omit package (or pass package=all) to sweep every package.`, + ); + (err as any).code = 'INVALID_REQUEST'; + (err as any).status = 400; + throw err; + } + return { entries, total: entries.length, @@ -248,3 +279,22 @@ export async function getMetaDiagnostics( stats, }; } + +/** + * [#27514] Does the installed-package listing carry this id? `true` ("cannot + * disprove, so do not refuse") when the listing cannot be read at all. + */ +async function packageIsInstalled(host: Pick, packageId: string): Promise { + let listed: unknown; + try { + listed = await host.getMetaItems({ type: 'package' }); + } catch { + return true; + } + const items = (listed as { items?: unknown } | null | undefined)?.items; + if (!Array.isArray(items)) return true; + return items.some((record) => { + const r = record as { manifest?: { id?: unknown }; id?: unknown } | null | undefined; + return (r?.manifest?.id ?? r?.id) === packageId; + }); +} diff --git a/packages/metadata-protocol/src/protocol.diagnostics-store-outage.test.ts b/packages/metadata-protocol/src/protocol.diagnostics-store-outage.test.ts index 3a32fd92988..ba1373e0a46 100644 --- a/packages/metadata-protocol/src/protocol.diagnostics-store-outage.test.ts +++ b/packages/metadata-protocol/src/protocol.diagnostics-store-outage.test.ts @@ -306,7 +306,9 @@ describe('[#8924] a caller error the producer classified is refused loudly, neve const res: any = await p.getMetaDiagnostics({ type: 'fields' }); expect(res.total).toBe(0); expect(res.scannedTypes).toBe(1); - expect(res.stats.fields).toEqual({ count: 0, locked: 0, packages: [] }); + // [#27514] Keyed by the canonical type the plural folds to, not the plural. + expect(Object.keys(res.stats)).toEqual(['field']); + expect(res.stats.field).toEqual({ count: 0, locked: 0, packages: [] }); }); it('a name that is a plural of NOTHING still answers benignly — the second verdict (#8421) lives on the mint door, not here', async () => { diff --git a/packages/rest/src/meta-collection-routes.ts b/packages/rest/src/meta-collection-routes.ts index 3e38c8ff50b..30dcd1f6ac8 100644 --- a/packages/rest/src/meta-collection-routes.ts +++ b/packages/rest/src/meta-collection-routes.ts @@ -88,6 +88,7 @@ import { rethrowAuthzStoreUnavailable } from '@objectstack/core'; import type { GetMetaItemsRequest } from '@objectstack/spec/api'; +import { truncateClientMessage } from '@objectstack/types'; import { handleRouteError, sendFieldVisibilityFault } from './error-response.js'; import * as metaReadGate from './meta-item-read-gate.js'; import { refuseRepeatedQueryParams } from './query-multiplicity.js'; @@ -189,6 +190,29 @@ export interface MetaCollectionRouteHost { metaTypeSingular: (typeof RestServer)['metaTypeSingular']; } +/** + * [#27514] A `GET {metaPath}/diagnostics` refusal of a query value the sweep + * cannot honour: `400 INVALID_REQUEST`, the status and code of the [#8924] + * near-miss type refusal it extends. Thrown, so it leaves through the route's + * one refusal writer ({@link isDiagnosticsRefusal}) with the refusals the + * list door and the sweep throw. + */ +function diagnosticsRefusal(message: string): Error { + return Object.assign(new Error(message), { code: 'INVALID_REQUEST', status: 400 }); +} + +/** + * [#27514] Is this a classified `400 INVALID_REQUEST` refusal of the + * diagnostics request: {@link diagnosticsRefusal}, the list door's + * unknown-type verdict, or the sweep's near-miss spelling and unknown package? + * Those are the refusals the route answers in its nested envelope; anything + * else keeps `handleRouteError`'s classification. + */ +function isDiagnosticsRefusal(error: unknown): error is Error { + const e = error as { status?: unknown; code?: unknown } | null | undefined; + return error instanceof Error && e?.status === 400 && e?.code === 'INVALID_REQUEST'; +} + /** * Register the `/meta` type list, the whole-store maintenance family and the * per-type list — the first three groups of @@ -291,8 +315,19 @@ export function registerMetaCollectionRoutes(this: MetaCollectionRouteHost, scop // value each; the `as string` casts are exactly the // laundering that kept `tsc` silent about the array arm. if (refuseRepeatedQueryParams(req, res, ['severity', 'type', 'package'])) return; - const severityParam = (req.query?.severity as string | undefined) ?? 'error'; - const severity = severityParam === 'warning' ? 'warning' : 'error'; + // [#27514] The sweep reports one of two severities, and any + // other value used to be read as `error` and answered 200: + // a caller asking for something this sweep does not have + // got the error sweep with nothing saying so. An empty value + // states nothing and keeps the default, as an empty `type` + // keeps the full sweep. + const severity = (req.query?.severity as string | undefined) || 'error'; + if (severity !== 'error' && severity !== 'warning') { + throw diagnosticsRefusal( + `'${severity}' is not a diagnostics severity. Use 'error' (the default: the entries that ` + + `fail validation) or 'warning' (those, and the entries that carry warnings).`, + ); + } const diagnosticsType = (req.query?.type as string | undefined) || undefined; // [ADR-0131 D6] No organization: the sweep reads each // type environment → code, the partition every `/meta` @@ -305,8 +340,34 @@ export function registerMetaCollectionRoutes(this: MetaCollectionRouteHost, scop // each swept type is a list read, so `all` names no package. packageId: metaItemPackageBinding(req.query?.package), }); + // [#27514] A named type that is not a metadata type at all + // was swept as one empty type: `200 {scannedTypes: 1}` with + // zero issues, an all-clear over nothing. Refused by the + // list door's own verdict ({@link refuseUnknownMetaListType}: + // the static spelling contract, then the live type set), so + // the two doors cannot disagree about which names are types. + // Asked AFTER the sweep on purpose: the sweep's [#8924] + // refusal of a near-miss spelling (`viewes`) names the + // spellings the caller was reaching for, and it keeps + // answering first. + if (diagnosticsType !== undefined) await this.refuseUnknownMetaListType(p, diagnosticsType); res.json(result); } catch (error: any) { + // [#27514] ONE envelope for every refusal this route + // answers. The repeated-parameter refusal above writes the + // ADR-0112 nested `{ error: { code, message } }`, and the + // classified 400s that are THROWN (the severity check above, + // the list door's unknown-type verdict, and the sweep's + // [#8924] near-miss spelling and unknown package) used to + // leave through `handleRouteError` as `{ error: '…', code }`, + // so a client reading `body.error.code` found it on one + // refusal and `undefined` on the next. + if (isDiagnosticsRefusal(error)) { + res.status(400).json({ + error: { code: 'INVALID_REQUEST', message: truncateClientMessage(error.message) }, + }); + return; + } handleRouteError(res, error); } }, diff --git a/packages/rest/src/rest-meta-diagnostics-unknown-params.test.ts b/packages/rest/src/rest-meta-diagnostics-unknown-params.test.ts new file mode 100644 index 00000000000..3f63cbf7c2f --- /dev/null +++ b/packages/rest/src/rest-meta-diagnostics-unknown-params.test.ts @@ -0,0 +1,171 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * [#27514] `GET /api/v1/meta/diagnostics` refuses a query value it cannot + * honour, instead of answering a silent all-clear. + * + * The door tells an author, an agent or CI whether metadata is healthy, so a + * `200` with zero issues is a verdict. Measured on `main` before this change, + * through the route below, each of these answered one: + * + * | query | answered | + * | :-------------------------- | :----------------------------------------------- | + * | `?type=zzz_unknown` | `200 {scannedTypes: 1, stats: {zzz_unknown: …}}` | + * | `?severity=bogus` | `200`, read as the error sweep | + * | `?package=com.nope.missing` | `200`, zeros for all 30 swept types | + * | `?type=views` | `200`, stats and entries keyed `views`, not `view` | + * + * and the route's two existing 400s spoke two envelopes: `{ error: '…', code }` + * for the [#8924] near-miss spelling (`?type=viewes`), `{ error: { code, + * message } }` for a repeated parameter (#6877). + * + * The protocol is a REAL `ObjectStackProtocolImplementation` over a REAL + * `SchemaRegistry`, one package installed and one view registered under it, + * and every case drives the REAL route handler. Each refusal asserts the + * ADR-0112 pair (`status` and the NESTED `body.error.code`); the preservation + * cases assert the answer itself, because "still 200" is what the defect + * looked like. + */ + +import { describe, it, expect } from 'vitest'; +import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol'; +import { SchemaRegistry } from '@objectstack/objectql'; +import { RestServer } from './rest-server.js'; + +const PACKAGE_ID = 'com.example.pkg'; + +function createMockServer() { + const noop = () => {}; + return { get: noop, post: noop, put: noop, delete: noop, patch: noop, use: noop, listen: async () => {}, close: async () => {} }; +} + +function makeRes() { + const res: any = { + write: () => true, end: () => {}, + header: () => res, setHeader: () => res, + status: (code: number) => { res._status = code; return res; }, + json: (body: any) => { res._json = body; return res; }, + }; + return res; +} + +function boot(seed: (registry: SchemaRegistry) => void = () => {}) { + const registry = new SchemaRegistry({ multiTenant: false, collisionPolicy: 'error' }); + registry.installPackage({ id: PACKAGE_ID, name: 'pkg', version: '1.0.0', type: 'app' } as never, 'code'); + registry.registerItem('view', { name: 'pkg_view', label: 'View', object: 'account', type: 'grid' }, 'name', PACKAGE_ID); + seed(registry); + const protocol = new ObjectStackProtocolImplementation( + { registry, find: async () => [], findOne: async () => null } as never, () => new Map(), undefined, + ); + const rest = new RestServer(createMockServer() as any, protocol as any, { api: { requireAuth: false } } as any); + (rest as any).resolveExecCtx = async () => ({ userId: 'u_admin', systemPermissions: ['manage_metadata'] }); + rest.registerRoutes(); + const route = rest.getRoutes().find((r: any) => r.method === 'GET' && r.path === '/api/v1/meta/diagnostics'); + if (!route) throw new Error('GET /api/v1/meta/diagnostics is not registered'); + const diagnostics = async (query: Record) => { + const res = makeRes(); + await route.handler({ method: 'GET', params: {}, query, headers: {} } as any, res); + return { status: res._status ?? 200, body: res._json }; + }; + return { diagnostics }; +} + +/** The route's one refusal envelope: ADR-0112 nested, the code a ledgered member. */ +function expectRefusal(answer: { status: number; body: any }, code: string) { + expect(answer.status).toBe(400); + expect(answer.body?.error?.code).toBe(code); + expect(typeof answer.body?.error?.message).toBe('string'); +} + +describe('[#27514] an unknown value is refused, not swept into an all-clear', () => { + it('?type= naming no metadata type answers 400 INVALID_REQUEST', async () => { + const { diagnostics } = boot(); + expectRefusal(await diagnostics({ type: 'zzz_unknown' }), 'INVALID_REQUEST'); + }); + + it('?severity= outside error|warning answers 400 INVALID_REQUEST', async () => { + const { diagnostics } = boot(); + expectRefusal(await diagnostics({ severity: 'bogus' }), 'INVALID_REQUEST'); + }); + + it('?package= naming no installed package, with no item bound to it, answers 400 INVALID_REQUEST', async () => { + const { diagnostics } = boot(); + expectRefusal(await diagnostics({ package: 'com.nope.missing' }), 'INVALID_REQUEST'); + }); +}); + +describe('[#27514] the route answers every refusal in one envelope', () => { + it('the [#8924] near-miss spelling answers nested, still naming the spellings it reached for', async () => { + const { diagnostics } = boot(); + const answer = await diagnostics({ type: 'viewes' }); + expectRefusal(answer, 'INVALID_REQUEST'); + expect(answer.body.error.message).toContain("'view'"); + }); + + it('the repeated-parameter refusal (#6877) keeps the same nested shape', async () => { + const { diagnostics } = boot(); + expectRefusal(await diagnostics({ type: ['view', 'object'] }), 'VALIDATION_ERROR'); + }); +}); + +describe('[#27514] an alias keys the answer canonically', () => { + it('?type=views answers exactly what ?type=view answers', async () => { + const { diagnostics } = boot(); + const alias = await diagnostics({ type: 'views' }); + const canonical = await diagnostics({ type: 'view' }); + expect(alias.status).toBe(200); + expect(Object.keys(alias.body.stats)).toEqual(['view']); + expect(alias.body.stats.view).toEqual({ count: 1, locked: 0, packages: [PACKAGE_ID] }); + expect(alias.body.entries.map((e: any) => e.type)).toEqual(['view']); + expect(alias.body).toEqual(canonical.body); + }); +}); + +describe('[#27514] preservation: what the door could already serve still answers 200', () => { + it('severity=warning, an empty severity, and no severity all answer', async () => { + const { diagnostics } = boot(); + const severities: Array> = [{ severity: 'warning' }, { severity: 'error' }, { severity: '' }, {}]; + for (const query of severities) { + const answer = await diagnostics({ type: 'view', ...query }); + expect(answer.status).toBe(200); + expect(answer.body.stats.view.count).toBe(1); + } + }); + + it('an installed package answers its items, and package=all sweeps every package', async () => { + const { diagnostics } = boot(); + const scoped = await diagnostics({ package: PACKAGE_ID, type: 'view' }); + expect(scoped.status).toBe(200); + expect(scoped.body.stats.view.count).toBe(1); + const all = await diagnostics({ package: 'all' }); + expect(all.status).toBe(200); + expect(all.body.scannedItems).toBe(1); + }); + + it('an installed package with no items answers its honest zeros', async () => { + const { diagnostics } = boot((registry) => { + registry.installPackage({ id: 'com.example.empty', name: 'empty', version: '1.0.0', type: 'app' } as never, 'code'); + }); + const answer = await diagnostics({ package: 'com.example.empty' }); + expect(answer.status).toBe(200); + expect(answer.body.scannedItems).toBe(0); + }); + + it('items bound to a package that is not installed are still swept', async () => { + const { diagnostics } = boot((registry) => { + registry.registerItem('view', { name: 'orphan_view', label: 'Orphan', object: 'account', type: 'grid' }, 'name', 'com.example.gone'); + }); + const answer = await diagnostics({ package: 'com.example.gone', type: 'view' }); + expect(answer.status).toBe(200); + expect(answer.body.stats.view).toEqual({ count: 1, locked: 0, packages: ['com.example.gone'] }); + }); + + it('a type this deployment carries live, outside the static contract, still answers', async () => { + const { diagnostics } = boot((registry) => { + registry.registerItem('zzz_plugin_kind', { name: 'k1' }, 'name'); + }); + const answer = await diagnostics({ type: 'zzz_plugin_kind' }); + expect(answer.status).toBe(200); + expect(answer.body.scannedTypes).toBe(1); + }); +}); diff --git a/packages/rest/src/rest-server-meta-read-org-scope.test.ts b/packages/rest/src/rest-server-meta-read-org-scope.test.ts index 0807bb23ee8..da9db66fce0 100644 --- a/packages/rest/src/rest-server-meta-read-org-scope.test.ts +++ b/packages/rest/src/rest-server-meta-read-org-scope.test.ts @@ -524,7 +524,9 @@ describe('#13753 · ADR-0131 D6 GET /meta/diagnostics sweeps the environment par await b.put(CACHED_ARM, 'authored_at_runtime'); const swept = await b.diagnostics({ type: 'views' }); expect(swept.status).toBe(200); - expect(swept.body?.stats?.views?.count).toBe(1); + // [#27514] …and keys it by the canonical type, not the plural it was asked by. + expect(swept.body?.stats?.view?.count).toBe(1); + expect(swept.body?.stats?.views).toBeUndefined(); }); it('every caller — another organization, none — is swept over it, on both arms', async () => { From 888190b916aeaa6e2e9efcfe1d5eba3950e53a89 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 13:52:58 +0000 Subject: [PATCH 2/3] chore(changeset): record the ADR-0087 disposition for #27514's narrowing Claude-Session: https://claude.ai/code/session_01L6bhgTiSuMwZbnqidCwkCM Co-authored-by: Claude --- .changeset/27514-diagnostics-unknown-params.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.changeset/27514-diagnostics-unknown-params.md b/.changeset/27514-diagnostics-unknown-params.md index 23fdff213c2..cac710d9dd8 100644 --- a/.changeset/27514-diagnostics-unknown-params.md +++ b/.changeset/27514-diagnostics-unknown-params.md @@ -8,6 +8,8 @@ fix(rest): `GET /meta/diagnostics` refuses an unknown type, severity or package Clause-②: no (narrowing — owes one contract-review-tier review before the queue) Metadata-protocol: none + + `/meta/diagnostics` is the door an author, an agent or CI reads to learn whether metadata is healthy, and three query values it could not honour were answered `200` with zero issues. - **`?type=`** naming no metadata type (`zzz_unknown`) is refused with `400 INVALID_REQUEST`, by the same verdict `GET /meta/:type` uses: the platform's static type spelling, then the types this deployment carries live. A near-miss spelling of a declared type (`viewes`) keeps its existing refusal, which names the spellings it was reaching for. From 2659dd6e0bbf974b174f06f148ae2c0c47988852 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 14:08:02 +0000 Subject: [PATCH 3/3] test(rest): the diagnostics rig's findOne routes through assertEngineFindOnePredicate, pinned in the engine-double ledger Claude-Session: https://claude.ai/code/session_01L6bhgTiSuMwZbnqidCwkCM Co-authored-by: Claude --- .../src/rest-meta-diagnostics-unknown-params.test.ts | 12 +++++++++++- scripts/engine-double-contract.pinned.json | 5 +++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/rest/src/rest-meta-diagnostics-unknown-params.test.ts b/packages/rest/src/rest-meta-diagnostics-unknown-params.test.ts index 3f63cbf7c2f..0d111bdba7c 100644 --- a/packages/rest/src/rest-meta-diagnostics-unknown-params.test.ts +++ b/packages/rest/src/rest-meta-diagnostics-unknown-params.test.ts @@ -28,6 +28,7 @@ */ import { describe, it, expect } from 'vitest'; +import { assertEngineFindOnePredicate } from '@objectstack/metadata-core'; import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol'; import { SchemaRegistry } from '@objectstack/objectql'; import { RestServer } from './rest-server.js'; @@ -55,7 +56,16 @@ function boot(seed: (registry: SchemaRegistry) => void = () => {}) { registry.registerItem('view', { name: 'pkg_view', label: 'View', object: 'account', type: 'grid' }, 'name', PACKAGE_ID); seed(registry); const protocol = new ObjectStackProtocolImplementation( - { registry, find: async () => [], findOne: async () => null } as never, () => new Map(), undefined, + { + registry, + find: async () => [], + findOne: async (object: string, query?: unknown) => { + assertEngineFindOnePredicate(object, query as never); + return null; + }, + } as never, + () => new Map(), + undefined, ); const rest = new RestServer(createMockServer() as any, protocol as any, { api: { requireAuth: false } } as any); (rest as any).resolveExecCtx = async () => ({ userId: 'u_admin', systemPermissions: ['manage_metadata'] }); diff --git a/scripts/engine-double-contract.pinned.json b/scripts/engine-double-contract.pinned.json index 9ca77140320..dcb2c6f5229 100644 --- a/scripts/engine-double-contract.pinned.json +++ b/scripts/engine-double-contract.pinned.json @@ -4276,6 +4276,11 @@ "verb": "findOne", "pinned": 1 }, + { + "file": "packages/rest/src/rest-meta-diagnostics-unknown-params.test.ts", + "verb": "findOne", + "pinned": 1 + }, { "file": "packages/rest/src/rest-server-meta-cached-etag-door-scope.test.ts", "verb": "findOne",