Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/rest/src/analytics-dataset-dimension-gate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ function mockServer() {
}
function mockProtocol() {
return {
getDiscovery: vi.fn().mockResolvedValue({ version: 'v0', endpoints: {} }),
getDiscovery: vi.fn().mockResolvedValue({ version: 'v0', routes: { data: '', metadata: '' } }),
getMetaTypes: vi.fn().mockResolvedValue([]),
getMetaItems: vi.fn().mockResolvedValue([]),
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ function mockServer() {
}
function mockProtocol() {
return {
getDiscovery: vi.fn().mockResolvedValue({ version: 'v0', endpoints: {} }),
getDiscovery: vi.fn().mockResolvedValue({ version: 'v0', routes: { data: '', metadata: '' } }),
getMetaTypes: vi.fn().mockResolvedValue([]),
getMetaItems: vi.fn().mockResolvedValue([]),
};
Expand Down
2 changes: 1 addition & 1 deletion packages/rest/src/analytics-dataset-where-gate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ function mockServer() {
}
function mockProtocol() {
return {
getDiscovery: vi.fn().mockResolvedValue({ version: 'v0', endpoints: {} }),
getDiscovery: vi.fn().mockResolvedValue({ version: 'v0', routes: { data: '', metadata: '' } }),
getMetaTypes: vi.fn().mockResolvedValue([]),
getMetaItems: vi.fn().mockResolvedValue([]),
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ function mockServer() {
}
function mockProtocol() {
return {
getDiscovery: vi.fn().mockResolvedValue({ version: 'v0', endpoints: {} }),
getDiscovery: vi.fn().mockResolvedValue({ version: 'v0', routes: { data: '', metadata: '' } }),
getMetaTypes: vi.fn().mockResolvedValue([]),
getMetaItems: vi.fn().mockResolvedValue([]),
};
Expand Down
2 changes: 1 addition & 1 deletion packages/rest/src/analytics-routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ function mockServer() {
};
}
function mockProtocol() {
return { getDiscovery: vi.fn().mockResolvedValue({ version: 'v0', endpoints: {} }), getMetaTypes: vi.fn().mockResolvedValue([]), getMetaItems: vi.fn().mockResolvedValue([]) };
return { getDiscovery: vi.fn().mockResolvedValue({ version: 'v0', routes: { data: '', metadata: '' } }), getMetaTypes: vi.fn().mockResolvedValue([]), getMetaItems: vi.fn().mockResolvedValue([]) };
}
function mockRes() {
const res: any = { statusCode: 200, body: undefined };
Expand Down
137 changes: 137 additions & 0 deletions packages/rest/src/discovery-double-retired-key.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* [#5674] No `getDiscovery` test double in this package may spell `endpoints`.
*
* ## What was wrong
*
* 25 test files in `packages/rest/src` mocked the protocol like this:
*
* ```ts
* getDiscovery: vi.fn().mockResolvedValue({
* version: 'v0',
* endpoints: { data: '', metadata: '', ui: '', auth: '/auth' },
* })
* ```
*
* The real producer (`ObjectStackProtocolImplementation.getDiscovery()` in
* `packages/metadata-protocol/src/protocol.ts`) emits `routes` — declared as
* `ApiRoutesSchema` and REQUIRED by `DiscoverySchema` — and has never emitted
* `endpoints`. That key existed only on the dispatcher path, as a verbatim
* copy of `routes`, and #4828 removed it under ADR-0049. The producer side is
* already pinned next door: `discovery-schema-conformance.test.ts` asserts the
* live `/discovery` body has no `endpoints` property.
*
* Those doubles were inert — `rest-server`'s discovery handler reads
* `discovery.routes`, so `if (discovery.routes)` was simply false and the
* route-augmentation block was skipped — which is exactly why they survived
* the retirement: nothing they asserted depended on the key at all.
*
* ## Why a pin, and why THIS pin
*
* The harm is authoring-time, not runtime: these were the last places in the
* repo still spelling a retired key, and a test double is the most-copied
* artifact there is. Copy one and the key is back in the fixture layer, still
* inert, still teaching a producer shape that never existed. A conformance
* test over the live producer cannot see that — no producer is involved.
*
* Scope, stated honestly:
*
* - It scans the `mockResolvedValue({ … })` form only, which is the form all
* 26 doubles in this package use and therefore the form that gets copied. A
* double written some other way (`vi.fn(async () => ({ … }))`) is not read.
* - It asserts only the NEGATIVE — no `endpoints` key. It deliberately does
* not require `routes`, because `rest-server` guards with
* `if (discovery.routes)` and a future test is entitled to drive that guard's
* falsy branch with a double that omits it.
* - The floor below is anti-vacuity, in the sense of #4642: a scanner that
* silently stops matching would otherwise pass by finding nothing.
*/

import { describe, it, expect } from 'vitest';
import { readdirSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';

const SRC_DIR = fileURLToPath(new URL('.', import.meta.url));

/**
* This file is excluded from its own scan, EXPLICITLY.
*
* The docblock above quotes the defective double verbatim — that is the point
* of it — so the scanner does match a "double" here. It happens to escape the
* key check only because a docblock line prefixes the key with `* `, which the
* `[{,]\s*` lead-in does not accept. Depending on that is depending on comment
* formatting: re-wrap the docblock and this pin fails on its own prose. Name
* the exclusion instead of inheriting the luck.
*/
const SELF = 'discovery-double-retired-key.test.ts';

/** Matches an `endpoints` key at the start of a line or after `{` / `,`. */
const ENDPOINTS_KEY = /(^|[{,])\s*endpoints\s*:/;

interface Double {
file: string;
/** The source text of the object the double resolves to, braces included. */
literal: string;
}

/**
* Capture the balanced `{ … }` that follows `getDiscovery: …mockResolvedValue(`.
*
* Brace counting is enough here: the doubles are plain data literals, and a
* `{` inside a string would only ever make this capture MORE text, i.e. fail
* loud rather than pass quiet.
*/
function collectDoubles(file: string, source: string): Double[] {
const found: Double[] = [];
const opener = /getDiscovery\s*:[\s\S]{0,80}?mockResolvedValue\(\s*\{/g;
let m: RegExpExecArray | null;
while ((m = opener.exec(source)) !== null) {
const start = m.index + m[0].length - 1; // index of the `{`
let depth = 0;
let end = -1;
for (let i = start; i < source.length; i++) {
const ch = source[i];
if (ch === '{') depth++;
else if (ch === '}') {
depth--;
if (depth === 0) { end = i; break; }
}
}
if (end === -1) throw new Error(`${file}: unbalanced getDiscovery double literal`);
found.push({ file, literal: source.slice(start, end + 1) });
}
return found;
}

const DOUBLES: Double[] = readdirSync(SRC_DIR)
.filter((f) => f.endsWith('.test.ts') && f !== SELF)
.flatMap((f) => collectDoubles(f, readFileSync(join(SRC_DIR, f), 'utf8')));

describe('[#5674] getDiscovery doubles carry the producer key, not the retired one', () => {
it('finds the doubles it claims to police (anti-vacuity)', () => {
// 27 doubles across 26 files when this pin was written (the 26 #5674
// corrected, plus `rest-route-ledger.conformance.test.ts`, which
// resolves to a bare `{}` and never spelled the retired key). The floor
// is deliberately slack — it exists so a scanner that matches NOTHING
// fails instead of reporting a clean sweep of an empty set.
expect(
DOUBLES.length,
'the scanner found (almost) no getDiscovery doubles — it has drifted from how this package writes them, so its verdict below means nothing',
).toBeGreaterThanOrEqual(20);
});

it('no double resolves to an object carrying `endpoints` (retired in #4828)', () => {
const offenders = DOUBLES
.filter((d) => ENDPOINTS_KEY.test(d.literal))
.map((d) => d.file);

expect(
Array.from(new Set(offenders)),
'the discovery producer emits `routes` (ApiRoutesSchema) and never emitted `endpoints`; '
+ '`endpoints` was the dispatcher-only copy retired by #4828 (ADR-0049). A double that spells '
+ 'it teaches a shape no producer ever had, and is the seed the next copy-paste grows from (#5674).',
).toEqual([]);
});
});
2 changes: 1 addition & 1 deletion packages/rest/src/meta-app-area-nav-gate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ function makeRes() {
*/
function setup(perms: string[], services: string[] = ['org-scoping']) {
const protocol: any = {
getDiscovery: vi.fn().mockResolvedValue({ version: 'v0', endpoints: { data: '', metadata: '', ui: '', auth: '/auth' } }),
getDiscovery: vi.fn().mockResolvedValue({ version: 'v0', routes: { data: '', metadata: '', ui: '', auth: '/auth' } }),
getMetaTypes: vi.fn().mockResolvedValue([]),
// Deep-clone per call: the filter must never mutate stored metadata, and
// a shared object would hide that by carrying a prior call's damage.
Expand Down
2 changes: 1 addition & 1 deletion packages/rest/src/meta-audience-plural.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ function makeRes() {

function setup() {
const protocol: any = {
getDiscovery: vi.fn().mockResolvedValue({ version: 'v0', endpoints: { data: '', metadata: '', ui: '', auth: '/auth' } }),
getDiscovery: vi.fn().mockResolvedValue({ version: 'v0', routes: { data: '', metadata: '', ui: '', auth: '/auth' } }),
getMetaTypes: vi.fn().mockResolvedValue([]),
// The real implementation normalizes singular↔plural, so BOTH spellings
// resolve to the same items.
Expand Down
2 changes: 1 addition & 1 deletion packages/rest/src/meta-public-book-grant.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ function makeRes() {
/** Secure-by-default: `requireAuth` is ON for every case below. */
function setup() {
const protocol: any = {
getDiscovery: vi.fn().mockResolvedValue({ version: 'v0', endpoints: { data: '', metadata: '', ui: '', auth: '/auth' } }),
getDiscovery: vi.fn().mockResolvedValue({ version: 'v0', routes: { data: '', metadata: '', ui: '', auth: '/auth' } }),
getMetaTypes: vi.fn().mockResolvedValue([]),
getMetaItems: vi.fn(async ({ type }: any) => {
const t = RestServerTypes.singular(String(type ?? ''));
Expand Down
2 changes: 1 addition & 1 deletion packages/rest/src/public-form-routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ const ticketObject = {
function buildServer(sections: any[]) {
const createData = vi.fn().mockResolvedValue({ object: 'ticket', id: 'rec_1', record: {} });
const protocol: any = {
getDiscovery: vi.fn().mockResolvedValue({ version: 'v0', endpoints: {} }),
getDiscovery: vi.fn().mockResolvedValue({ version: 'v0', routes: { data: '', metadata: '' } }),
getMetaTypes: vi.fn().mockResolvedValue([]),
getMetaItems: vi.fn(async ({ type }: { type: string }) => {
if (type === 'view') return [formView(sections)];
Expand Down
2 changes: 1 addition & 1 deletion packages/rest/src/request-schema-gate.conformance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ function setup() {
deleteManyData: vi.fn().mockResolvedValue({ success: true, total: 0, succeeded: 0, failed: 0, results: [] }),
};
const protocol: any = {
getDiscovery: vi.fn().mockResolvedValue({ version: 'v0', endpoints: { data: '', metadata: '', ui: '', auth: '/auth' } }),
getDiscovery: vi.fn().mockResolvedValue({ version: 'v0', routes: { data: '', metadata: '', ui: '', auth: '/auth' } }),
getMetaTypes: vi.fn().mockResolvedValue([]),
getMetaItems: vi.fn().mockResolvedValue([{ name: 'invoice' }]),
getMetaItem: vi.fn().mockResolvedValue({}),
Expand Down
2 changes: 1 addition & 1 deletion packages/rest/src/rest-4xx-message-truncation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ function makeRes() {
function setup(protocolOverrides: Record<string, unknown> = {}) {
const protocol: any = {
getDiscovery: vi.fn().mockResolvedValue({
version: 'v0', endpoints: { data: '', metadata: '', ui: '', auth: '/auth' },
version: 'v0', routes: { data: '', metadata: '', ui: '', auth: '/auth' },
}),
getMetaTypes: vi.fn().mockResolvedValue([]),
getMetaItems: vi.fn().mockResolvedValue([]),
Expand Down
2 changes: 1 addition & 1 deletion packages/rest/src/rest-5xx-message-sanitization.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ function mountRest(protocol: any) {
function setup(protocolOverrides: Record<string, unknown> = {}) {
const protocol: any = {
getDiscovery: vi.fn().mockResolvedValue({
version: 'v0', endpoints: { data: '', metadata: '', ui: '', auth: '/auth' },
version: 'v0', routes: { data: '', metadata: '', ui: '', auth: '/auth' },
}),
getMetaTypes: vi.fn().mockResolvedValue([]),
getMetaItems: vi.fn().mockResolvedValue([]),
Expand Down
2 changes: 1 addition & 1 deletion packages/rest/src/rest-batch-endpoint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ function makeCreateData(ql: any, opts: { readonlyFields?: string[] } = {}) {
function buildServer(opts: { ql?: any; objects?: any[]; readonlyFields?: string[] } = {}) {
const server = mockServer();
const protocol: any = {
getDiscovery: vi.fn().mockResolvedValue({ version: 'v0', endpoints: {} }),
getDiscovery: vi.fn().mockResolvedValue({ version: 'v0', routes: { data: '', metadata: '' } }),
getMetaTypes: vi.fn().mockResolvedValue([]),
getMetaItems: vi.fn().mockResolvedValue(opts.objects ?? []),
createData: opts.ql
Expand Down
2 changes: 1 addition & 1 deletion packages/rest/src/rest-batch-size-cap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ function setup(maxBatchSize?: number) {
batchData: vi.fn().mockResolvedValue({ success: true, total: 0, succeeded: 0, failed: 0, results: [] }),
};
const protocol: any = {
getDiscovery: vi.fn().mockResolvedValue({ version: 'v0', endpoints: { data: '', metadata: '', ui: '', auth: '/auth' } }),
getDiscovery: vi.fn().mockResolvedValue({ version: 'v0', routes: { data: '', metadata: '', ui: '', auth: '/auth' } }),
getMetaTypes: vi.fn().mockResolvedValue([]),
getMetaItems: vi.fn().mockResolvedValue([{ name: 'invoice' }]),
getMetaItem: vi.fn().mockResolvedValue({}),
Expand Down
2 changes: 1 addition & 1 deletion packages/rest/src/rest-bulk-path-object.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ function setup() {
success: true, operation: 'delete', total: 1, succeeded: 1, failed: 0, results: [],
});
const protocol: any = {
getDiscovery: vi.fn().mockResolvedValue({ version: 'v0', endpoints: { data: '', metadata: '', ui: '', auth: '/auth' } }),
getDiscovery: vi.fn().mockResolvedValue({ version: 'v0', routes: { data: '', metadata: '', ui: '', auth: '/auth' } }),
getMetaTypes: vi.fn().mockResolvedValue([]),
getMetaItems: vi.fn().mockResolvedValue([
{ name: 'open' },
Expand Down
2 changes: 1 addition & 1 deletion packages/rest/src/rest-delete-many-ingress.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ function setup() {
success: true, operation: 'delete', total: 1, succeeded: 1, failed: 0, results: [],
});
const protocol: any = {
getDiscovery: vi.fn().mockResolvedValue({ version: 'v0', endpoints: { data: '', metadata: '', ui: '', auth: '/auth' } }),
getDiscovery: vi.fn().mockResolvedValue({ version: 'v0', routes: { data: '', metadata: '', ui: '', auth: '/auth' } }),
getMetaTypes: vi.fn().mockResolvedValue([]),
getMetaItems: vi.fn().mockResolvedValue([{ name: 'invoice' }]),
getMetaItem: vi.fn().mockResolvedValue({}),
Expand Down
2 changes: 1 addition & 1 deletion packages/rest/src/rest-dropped-fields.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ function mockRes() {

function buildServer(protocolOverrides: Record<string, any>) {
const protocol: any = {
getDiscovery: vi.fn().mockResolvedValue({ version: 'v0', endpoints: {} }),
getDiscovery: vi.fn().mockResolvedValue({ version: 'v0', routes: { data: '', metadata: '' } }),
getMetaTypes: vi.fn().mockResolvedValue([]),
// No object registered → enforceApiAccess default-allows and the handler
// proceeds straight to the (mocked) write method.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ async function managerHolding(items: Array<Record<string, unknown>>): Promise<Me
function mountRest(enumerated: Array<Record<string, unknown>>, authority: unknown | undefined) {
const protocol: any = {
getDiscovery: vi.fn().mockResolvedValue({
version: 'v0', endpoints: { data: '', metadata: '', ui: '', auth: '/auth' },
version: 'v0', routes: { data: '', metadata: '', ui: '', auth: '/auth' },
}),
getMetaTypes: vi.fn().mockResolvedValue([]),
getMetaItems: vi.fn(async ({ type }: any) => {
Expand Down
2 changes: 1 addition & 1 deletion packages/rest/src/rest-env-resolution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ function createMockServer() {

function createMockProtocol() {
return {
getDiscovery: vi.fn().mockResolvedValue({ version: 'v0', endpoints: {} }),
getDiscovery: vi.fn().mockResolvedValue({ version: 'v0', routes: { data: '', metadata: '' } }),
getMetaTypes: vi.fn().mockResolvedValue([]),
getMetaItems: vi.fn().mockResolvedValue([]),
getMetaItem: vi.fn().mockResolvedValue({}),
Expand Down
2 changes: 1 addition & 1 deletion packages/rest/src/rest-expected-error-logging.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ function noDraftError(target: string) {
function setup(protocolOverrides: Record<string, unknown> = {}) {
const protocol: any = {
getDiscovery: vi.fn().mockResolvedValue({
version: 'v0', endpoints: { data: '', metadata: '', ui: '', auth: '/auth' },
version: 'v0', routes: { data: '', metadata: '', ui: '', auth: '/auth' },
}),
getMetaTypes: vi.fn().mockResolvedValue([]),
getMetaItems: vi.fn().mockResolvedValue([{ name: 'showcase_account' }]),
Expand Down
2 changes: 1 addition & 1 deletion packages/rest/src/rest-meta-outage-vs-miss.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ function makeRes() {
function setup(protocolOverrides: Record<string, unknown> = {}) {
const protocol: any = {
getDiscovery: vi.fn().mockResolvedValue({
version: 'v0', endpoints: { data: '', metadata: '', ui: '', auth: '/auth' },
version: 'v0', routes: { data: '', metadata: '', ui: '', auth: '/auth' },
}),
getMetaTypes: vi.fn().mockResolvedValue([]),
getMetaItems: vi.fn().mockResolvedValue([]),
Expand Down
2 changes: 1 addition & 1 deletion packages/rest/src/rest-unclassified-fault-status.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ function makeRes() {
function setup(protocolOverrides: Record<string, unknown> = {}) {
const protocol: any = {
getDiscovery: vi.fn().mockResolvedValue({
version: 'v0', endpoints: { data: '', metadata: '', ui: '', auth: '/auth' },
version: 'v0', routes: { data: '', metadata: '', ui: '', auth: '/auth' },
}),
getMetaTypes: vi.fn().mockResolvedValue([]),
getMetaItems: vi.fn().mockResolvedValue([]),
Expand Down
4 changes: 2 additions & 2 deletions packages/rest/src/rest-unknown-object-heuristic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@ describe('[#5462] a real unknown object is still 404 OBJECT_NOT_FOUND, still sil
// trace per request (#4886).
const rest = mountRest({
getDiscovery: vi.fn().mockResolvedValue({
version: 'v0', endpoints: { data: '', metadata: '', ui: '', auth: '/auth' },
version: 'v0', routes: { data: '', metadata: '', ui: '', auth: '/auth' },
}),
getMetaTypes: vi.fn().mockResolvedValue([]),
findData: vi.fn().mockRejectedValue(driverError('no such table: ghost')),
Expand Down Expand Up @@ -416,7 +416,7 @@ describe('[#5462] the declared-status band is untouched', () => {
);
const rest = mountRest({
getDiscovery: vi.fn().mockResolvedValue({
version: 'v0', endpoints: { data: '', metadata: '', ui: '', auth: '/auth' },
version: 'v0', routes: { data: '', metadata: '', ui: '', auth: '/auth' },
}),
getMetaTypes: vi.fn().mockResolvedValue([]),
saveMetaItem: vi.fn().mockRejectedValue(err),
Expand Down
6 changes: 5 additions & 1 deletion packages/rest/src/rest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ function createMockProtocol() {
return {
getDiscovery: vi.fn().mockResolvedValue({
version: 'v0',
endpoints: { data: '', metadata: '', ui: '', auth: '/auth' },
// The producer's key is `routes` (`ApiRoutesSchema`, required by
// `DiscoverySchema`). `endpoints` was a dispatcher-only verbatim copy of
// it, retired in #4828 — a double that spells it is teaching a shape no
// producer ever emitted (#5674). Copy this mock, not that one.
routes: { data: '', metadata: '', ui: '', auth: '/auth' },
}),
getMetaTypes: vi.fn().mockResolvedValue([]),
getMetaItems: vi.fn().mockResolvedValue([]),
Expand Down
2 changes: 1 addition & 1 deletion packages/rest/src/security-routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ function mockServer() {
};
}
function mockProtocol() {
return { getDiscovery: vi.fn().mockResolvedValue({ version: 'v0', endpoints: {} }), getMetaTypes: vi.fn().mockResolvedValue([]), getMetaItems: vi.fn().mockResolvedValue([]) };
return { getDiscovery: vi.fn().mockResolvedValue({ version: 'v0', routes: { data: '', metadata: '' } }), getMetaTypes: vi.fn().mockResolvedValue([]), getMetaItems: vi.fn().mockResolvedValue([]) };
}
function mockRes() {
const res: any = { statusCode: 200, body: undefined };
Expand Down
Loading