Skip to content

Commit c001422

Browse files
os-zhuangclaude
andauthored
feat(spec)!: 声明 routes.mcp,并把 discovery 一致性闸门下沉到 routes 一层 (#5679) (#5743)
* feat(spec)!: declare routes.mcp on ApiRoutesSchema, extend discovery gate to routes keys (#5679) `/discovery` advertises `routes.mcp`, objectui reads it, and `ApiRoutesSchema` never declared it — #4828's defect one level down, with the opposite disposition: `endpoints` was retired for having no reader, `mcp` has two real ones and is in fact the only `routes.*` key anything in objectui reads. `ApiRoutesSchema` is a plain z.object, which strips unknown keys, so any consumer parsing /discovery through the spec dropped `routes.mcp` silently. - spec: declare `mcp: z.string().optional()` as MEASURED off both producers — a path string, always the unscoped base (/mcp is mounted bare), optional rather than nullable (the key is absent, never null, when MCP is off or unserveable). - rest: drop the two `as any` casts at the emit site. Type-only; the emitted body is byte-identical. With the key undeclared, removing them produced two TS2339; with it declared, tsc returns to its ratcheted baseline of 2. - gates: the #4828 conformance tests now check `routes` keys as well as top-level ones in all three producer packages, deriving the allowance from ApiRoutesSchema. Extended one level, not recursed. Corrects the issue's premise on one point: the runtime dispatcher's getDiscoveryInfo() also emits `routes.mcp` (its routes literal always carries the key, holding the path or undefined), so both producers were affected and the new gate went red on both before the fix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018fxLGQdatPbBUvCgiVxg6D * fix(client,spec): 补 client 路由约定表的 mcp 项,并在合并树上重生成 authorable-surface (#5679) CI 上两条不同签名的红,都是合并 origin/main 之后才出现的: 1. `@objectstack/client` DTS build — TS2741: `getRoute()` 的约定回退表 `Record<ApiRouteType, string>` 按设计对 `keyof ApiRoutes` **全键必填**, 所以给 ApiRoutesSchema 新增一个已声明键就必须给出它的约定路径。这是我 消费半径漏掉的包。补 `mcp: '/api/v1/mcp'` —— 不是猜的,正是两个 discovery 生产者实测发出的值,所以回退值与被发现值一致而非互相打架;而且该表本就 是 unscoped 约定表(每行都是 /api/v1/...),恰好符合 /mcp 裸挂载的事实。 全仓复扫 `keyof ApiRoutes` / ApiRoutes 型字面量,确认没有第三处:其余站点 都是 `Partial<ApiRoutes>` 或只填两个必填键,不受影响。 2. `check:authorable-surface` — 合并带进了 #5721 的 `ui/ActionSession` 源, 但 authorable-surface.json 未在合并树上重生成。跑 gen:schema 重出,该文件 现同时含本单的 `api/ApiRoutes:mcp` 与 main 的 ActionSession 三键。 `authorable-surface.base.json` 的 baseRev 重锚照 #5358 再次剔除 —— 那是 main 前移导致的机械重锚,不属本单。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018fxLGQdatPbBUvCgiVxg6D --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 85d95e7 commit c001422

10 files changed

Lines changed: 317 additions & 5 deletions

File tree

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/rest": patch
4+
"@objectstack/client": patch
5+
---
6+
7+
feat(spec): declare `routes.mcp` on `ApiRoutesSchema`, and extend the discovery conformance gate one level down (#5679)
8+
9+
`/discovery` advertises `routes.mcp`, `objectui` reads it, and
10+
`ApiRoutesSchema` never declared it. This is #4828's defect one level down —
11+
with the opposite disposition: `endpoints` was retired because a census found
12+
no reader, while `mcp` has two real ones (`ConnectAgentWidget.tsx` and
13+
`AgentConnectSection.tsx` both gate the Integrations connect card on it), and
14+
it is in fact the only `routes.*` key anything in `objectui` reads. So it is
15+
declared, not removed.
16+
17+
Why it was a defect and not tidiness: `ApiRoutesSchema` is a plain `z.object`,
18+
which **strips** unknown keys. Any consumer parsing `/discovery` through the
19+
spec dropped `routes.mcp` silently — the connect card would blank with no
20+
error. Nothing broke yet only because those two readers happen to read raw
21+
JSON.
22+
23+
- **`ApiRoutesSchema` declares `mcp: z.string().optional()`**, as measured off
24+
both producers rather than guessed: a path string (`/api/v1/mcp`), always the
25+
**unscoped** base — `/mcp` is mounted bare, so a scoped mount advertising
26+
`/api/v1/environments/env_alpha/data` still advertises `/api/v1/mcp` — and
27+
`optional`, not `nullable`: the key is absent (rest-server `delete`s it, the
28+
dispatcher leaves it `undefined`) when MCP is disabled or unserveable.
29+
Neither producer ever emits `null`.
30+
- **`@objectstack/rest` drops the two `as any` casts** at the emit site. That is
31+
type-only — the emitted body is byte-identical — but the cast's disappearance
32+
is the structural proof: with the key undeclared, removing it produced two
33+
`TS2339 Property 'mcp' does not exist`; with it declared, `tsc --noEmit`
34+
returns to its ratcheted baseline.
35+
- **The #4828 conformance gates now cover `routes` keys**, not just top-level
36+
ones, in all three producer packages, deriving the allowance from
37+
`ApiRoutesSchema` the same way the top-level check derives it from the
38+
protocol schema. Extended one level, not recursed — full recursion stays out
39+
of scope, and `capabilities` / `services` are `z.record`s whose keys are open
40+
by design.
41+
42+
- **`@objectstack/client`'s conventional route table gains an `mcp` row.** That
43+
table is `Record<keyof ApiRoutes, string>` — total by design — so a newly
44+
declared route owes a convention, and the public `ApiRouteType` (`keyof
45+
ApiRoutes`) widens by one member. The path is `/api/v1/mcp`, which is what
46+
both producers emit, so the fallback agrees with the discovered value instead
47+
of competing with it. Resolution behaviour is unchanged: `getRoute()` still
48+
prefers the discovered route, and the pre-existing catch-all already produced
49+
the same string.
50+
51+
Corrects one detail of the issue's premise: the runtime dispatcher's
52+
`getDiscoveryInfo()` **does** also emit `routes.mcp` (its routes literal always
53+
carries the key, holding the path or `undefined`), so both producers were
54+
affected, not just REST — and the new gate went red on both before the fix.

content/docs/references/api/discovery.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ const result = ApiRoutesSchema.parse(data);
5757
| **notifications** | `string` | optional | e.g. /api/v1/notifications |
5858
| **ai** | `string` | optional | e.g. /api/v1/ai |
5959
| **i18n** | `string` | optional | e.g. /api/v1/i18n |
60+
| **mcp** | `string` | optional | e.g. /api/v1/mcp — always the unscoped base; absent when MCP is disabled or unserveable |
6061

6162

6263
---

packages/client/src/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4584,6 +4584,16 @@ export class ObjectStackClient {
45844584
notifications: '/api/v1/notifications',
45854585
ai: '/api/v1/ai',
45864586
i18n: '/api/v1/i18n',
4587+
// [#5679] `mcp` became a declared `ApiRoutes` key, and this map is
4588+
// TOTAL over them by design — a new declared route owes a convention.
4589+
// `/api/v1/mcp` is not a guess: it is what both discovery producers
4590+
// actually emit, so the fallback agrees with the discovered value
4591+
// rather than competing with it.
4592+
//
4593+
// Note this table is the UNSCOPED convention (every row is `/api/v1/…`),
4594+
// which suits `mcp` exactly: `/mcp` is mounted bare, so even a
4595+
// project-scoped discovery response advertises the unscoped path.
4596+
mcp: '/api/v1/mcp',
45874597
};
45884598

45894599
return routeMap[type] || `/api/v1/${type}`;

packages/metadata-protocol/src/discovery-schema-conformance.test.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,29 @@
3030
// from becoming a third dialect of the contract.
3131

3232
import { describe, it, expect } from 'vitest';
33-
import { DiscoverySchema, GetDiscoveryResponseSchema } from '@objectstack/spec/api';
33+
import { ApiRoutesSchema, DiscoverySchema, GetDiscoveryResponseSchema } from '@objectstack/spec/api';
3434
import { ObjectStackProtocolImplementation } from './index.js';
3535

3636
/** The keys the protocol declares for a discovery response (canonical + declared alias). */
3737
function declaredResponseKeys(): Set<string> {
3838
return new Set(Object.keys((GetDiscoveryResponseSchema as any).shape));
3939
}
4040

41+
/**
42+
* [#5679] The keys `ApiRoutesSchema` declares INSIDE `routes` — the #4828 gate
43+
* extended one level down, where `routes.mcp` had been living undeclared.
44+
*
45+
* Unlike the REST and dispatcher gates, this one was ALREADY green before
46+
* #5679: this builder's `routes` is annotated `const routes: ApiRoutes`, so
47+
* the compiler kept it inside the declared key set and it never grew an `mcp`.
48+
* It is added anyway so all three producers carry the same gate — this is the
49+
* producer the OTHER two compose over, and the one place where an undeclared
50+
* routes key would today be caught by `tsc` rather than by a test.
51+
*/
52+
function declaredRouteKeys(): Set<string> {
53+
return new Set(Object.keys((ApiRoutesSchema as any).shape));
54+
}
55+
4156
/**
4257
* A protocol impl over a minimal engine. `getDiscovery()` reads
4358
* `engine.registry` (for `sys_comment`), `engine.transaction` (for
@@ -72,6 +87,27 @@ describe('[#4828] getDiscovery() conforms to DiscoverySchema', () => {
7287
expect(undeclared, 'undeclared top-level keys on the getDiscovery() shape').toEqual([]);
7388
});
7489

90+
it('[#5679] emits NO `routes` key the schema does not declare', async () => {
91+
const discovery: any = await makeImpl().getDiscovery();
92+
93+
const declared = declaredRouteKeys();
94+
const undeclared = Object.keys(discovery.routes).filter(k => !declared.has(k));
95+
expect(undeclared, 'undeclared keys inside `routes` on the getDiscovery() shape').toEqual([]);
96+
});
97+
98+
it('[#5679] does NOT advertise `mcp` — this builder knows nothing about the /mcp mount', async () => {
99+
const discovery: any = await makeImpl().getDiscovery();
100+
101+
// The negative half of the same fact: `mcp` is now DECLARED (optional), and
102+
// this producer legitimately leaves it empty — the /mcp route is mounted by
103+
// the host (rest-server) or gated on the kernel's mcp service (dispatcher),
104+
// neither of which this builder can see. Declaring the key does not oblige
105+
// every producer to fill it; it obliges every producer that fills it to
106+
// spell it this way.
107+
expect(Object.prototype.hasOwnProperty.call(discovery.routes, 'mcp')).toBe(false);
108+
expect(declaredRouteKeys().has('mcp')).toBe(true);
109+
});
110+
75111
it('carries the canonical `name`, and keeps `apiName` for its deprecation window', async () => {
76112
const discovery: any = await makeImpl().getDiscovery();
77113

packages/rest/src/discovery-schema-conformance.test.ts

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
// undeclared.
1717

1818
import { describe, it, expect, vi } from 'vitest';
19-
import { DiscoverySchema, GetDiscoveryResponseSchema } from '@objectstack/spec/api';
19+
import { ApiRoutesSchema, DiscoverySchema, GetDiscoveryResponseSchema } from '@objectstack/spec/api';
2020
import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol';
2121
import { RestServer } from './rest-server.js';
2222

@@ -25,6 +25,24 @@ function declaredResponseKeys(): Set<string> {
2525
return new Set(Object.keys((GetDiscoveryResponseSchema as any).shape));
2626
}
2727

28+
/**
29+
* [#5679] The keys `ApiRoutesSchema` declares INSIDE `routes`.
30+
*
31+
* The #4828 gate was deliberately pinned at the top level, and `routes.mcp`
32+
* lived in exactly the blind spot that left: emitted here, read by objectui,
33+
* declared nowhere. Note which assertion catches it — `DiscoverySchema
34+
* .safeParse()` above stays GREEN on an undeclared `routes.mcp`, because
35+
* `ApiRoutesSchema` is a plain `z.object` and zod strips unknown keys. Only a
36+
* key-set check can see it, one level down exactly as at the top.
37+
*
38+
* Extended one level, not recursed: this is the level with a measured
39+
* producer/consumer pair. Full recursion is still out of scope (#4828), and
40+
* `capabilities` / `services` are `z.record`s whose keys are open by design.
41+
*/
42+
function declaredRouteKeys(): Set<string> {
43+
return new Set(Object.keys((ApiRoutesSchema as any).shape));
44+
}
45+
2846
function createMockServer() {
2947
return {
3048
get: vi.fn(), post: vi.fn(), put: vi.fn(), delete: vi.fn(), patch: vi.fn(),
@@ -95,6 +113,52 @@ describe('[#4828] the REST /discovery live shape conforms to DiscoverySchema', (
95113
expect(undeclared, 'undeclared top-level keys on the REST /discovery body').toEqual([]);
96114
});
97115

116+
it('[#5679] emits NO `routes` key the schema does not declare', async () => {
117+
const body = await invoke(discoveryHandler());
118+
119+
const declared = declaredRouteKeys();
120+
const undeclared = Object.keys(body.routes).filter(k => !declared.has(k));
121+
expect(undeclared, 'undeclared keys inside `routes` on the REST /discovery body').toEqual([]);
122+
});
123+
124+
it('[#5679] anti-vacuity: this mount really does advertise `routes.mcp`', async () => {
125+
const body = await invoke(discoveryHandler());
126+
127+
// Without this the gate above would pass for the empty reason. MCP is
128+
// default-on and the probe returns `null` (cannot probe) with no kernel
129+
// manager and no serviceExistsProvider, so the fail-open branch advertises.
130+
expect(body.routes.mcp).toBe('/api/v1/mcp');
131+
expect(declaredRouteKeys().has('mcp')).toBe(true);
132+
});
133+
134+
it('[#5679] advertises the UNSCOPED /mcp even from the scoped mount', async () => {
135+
const body = await invoke(
136+
discoveryHandler({ scoped: true }),
137+
{ environmentId: 'env_alpha' },
138+
);
139+
140+
// The /mcp route is mounted bare, so the advertised path must NOT pick up
141+
// the environment segment the sibling routes carry. Measured, then declared.
142+
expect(body.routes.data).toBe('/api/v1/environments/env_alpha/data');
143+
expect(body.routes.mcp).toBe('/api/v1/mcp');
144+
});
145+
146+
it('[#5679] omits the key entirely when the env opts out of MCP', async () => {
147+
const old = process.env.OS_MCP_SERVER_ENABLED;
148+
process.env.OS_MCP_SERVER_ENABLED = 'false';
149+
try {
150+
const body = await invoke(discoveryHandler());
151+
152+
// `optional`, not `nullable`: the emit site DELETES the key, so a
153+
// consumer sees an absent key rather than an explicit null.
154+
expect(Object.prototype.hasOwnProperty.call(body.routes, 'mcp')).toBe(false);
155+
expect(DiscoverySchema.safeParse(body).success).toBe(true);
156+
} finally {
157+
if (old === undefined) delete process.env.OS_MCP_SERVER_ENABLED;
158+
else process.env.OS_MCP_SERVER_ENABLED = old;
159+
}
160+
});
161+
98162
it('fills the three required identity keys the schema declares', async () => {
99163
const body = await invoke(discoveryHandler());
100164

packages/rest/src/rest-server.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3074,9 +3074,9 @@ export class RestServer {
30743074
const unscopedBase = isScoped
30753075
? basePath.replace(/\/(environments|projects)\/:environmentId$/, '')
30763076
: basePath;
3077-
(discovery.routes as any).mcp = `${unscopedBase}/mcp`;
3077+
discovery.routes.mcp = `${unscopedBase}/mcp`;
30783078
} else {
3079-
delete (discovery.routes as any).mcp;
3079+
delete discovery.routes.mcp;
30803080
}
30813081

30823082
// Align auth route with the versioned base path if present.

packages/runtime/src/discovery-schema-conformance.test.ts

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,29 @@
1515
// contrived fixture needed.
1616

1717
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
18-
import { DiscoverySchema, GetDiscoveryResponseSchema } from '@objectstack/spec/api';
18+
import { ApiRoutesSchema, DiscoverySchema, GetDiscoveryResponseSchema } from '@objectstack/spec/api';
1919
import { HttpDispatcher } from './http-dispatcher.js';
2020

2121
/** The keys the protocol declares for a discovery response (canonical + declared alias). */
2222
function declaredResponseKeys(): Set<string> {
2323
return new Set(Object.keys((GetDiscoveryResponseSchema as any).shape));
2424
}
2525

26+
/**
27+
* [#5679] The keys `ApiRoutesSchema` declares INSIDE `routes` — the same gate
28+
* one level down, where `routes.mcp` had been hiding.
29+
*
30+
* This producer emits `mcp` too, contrary to what #5679's issue body assumed:
31+
* the routes literal below carries `mcp: isMcpServerEnabled() && hasMcp ? … :
32+
* undefined`, so the KEY is always present (value `undefined` when the service
33+
* is absent — `JSON.stringify` drops it on the wire, which is why it read as
34+
* "not emitted"). `Object.keys()` sees it either way, so this gate covers this
35+
* producer as squarely as the REST one.
36+
*/
37+
function declaredRouteKeys(): Set<string> {
38+
return new Set(Object.keys((ApiRoutesSchema as any).shape));
39+
}
40+
2641
describe('[#4828] getDiscoveryInfo() conforms to DiscoverySchema', () => {
2742
let dispatcher: HttpDispatcher;
2843

@@ -64,6 +79,49 @@ describe('[#4828] getDiscoveryInfo() conforms to DiscoverySchema', () => {
6479
expect(undeclared, 'undeclared top-level keys on the getDiscoveryInfo() shape').toEqual([]);
6580
});
6681

82+
it('[#5679] emits NO `routes` key the schema does not declare', async () => {
83+
const info: any = await dispatcher.getDiscoveryInfo('/api/v1');
84+
85+
const declared = declaredRouteKeys();
86+
const undeclared = Object.keys(info.routes).filter(k => !declared.has(k));
87+
expect(undeclared, 'undeclared keys inside `routes` on the getDiscoveryInfo() shape').toEqual([]);
88+
});
89+
90+
it('[#5679] advertises `routes.mcp` when the mcp service is registered and shaped', async () => {
91+
// Anti-vacuity for the gate above, and the fact the issue body got wrong:
92+
// this producer DOES emit `routes.mcp`. Gated on the handler's own
93+
// predicate (`typeof mcp.handleHttpRequest === 'function'`, #4024) — the
94+
// key is present either way, carrying the path or `undefined`.
95+
const kernel = {
96+
context: {
97+
getService: (name: string) => {
98+
if (name === 'objectql') {
99+
return {
100+
registry: {
101+
getObject: vi.fn().mockReturnValue({ name: 'test_obj' }),
102+
getRegisteredTypes: vi.fn().mockReturnValue([]),
103+
getAllPackages: vi.fn().mockReturnValue([]),
104+
},
105+
};
106+
}
107+
if (name === 'mcp') return { handleHttpRequest: () => undefined };
108+
return null;
109+
},
110+
},
111+
} as any;
112+
const info: any = await new HttpDispatcher(kernel).getDiscoveryInfo('/api/v1');
113+
114+
expect(info.routes.mcp).toBe('/api/v1/mcp');
115+
expect(declaredRouteKeys().has('mcp')).toBe(true);
116+
expect(DiscoverySchema.safeParse(info).success).toBe(true);
117+
118+
// …and with no mcp service the key stays present but empty, which is the
119+
// shape `optional` (not `nullable`) declares.
120+
const withoutMcp: any = await dispatcher.getDiscoveryInfo('/api/v1');
121+
expect(Object.prototype.hasOwnProperty.call(withoutMcp.routes, 'mcp')).toBe(true);
122+
expect(withoutMcp.routes.mcp).toBeUndefined();
123+
});
124+
67125
it('has retired `features` and `endpoints` (ADR-0049 enforce-or-remove)', async () => {
68126
const info: any = await dispatcher.getDiscoveryInfo('/api/v1');
69127

packages/spec/authorable-surface.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,7 @@
528528
"api/ApiRoutes:data",
529529
"api/ApiRoutes:discovery",
530530
"api/ApiRoutes:i18n",
531+
"api/ApiRoutes:mcp",
531532
"api/ApiRoutes:metadata",
532533
"api/ApiRoutes:notifications",
533534
"api/ApiRoutes:packages",

packages/spec/src/api/discovery.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -983,6 +983,53 @@ describe('[#4828] DiscoverySchema ↔ GetDiscoveryResponseSchema', () => {
983983
});
984984
});
985985

986+
// ===========================================================================
987+
// [#5679] The same contract, one level down: `routes`
988+
// ===========================================================================
989+
//
990+
// #4828 pinned its gate at the TOP level, deliberately. `routes.mcp` lived in
991+
// exactly the gap that left: emitted by `@objectstack/rest` (behind an `as any`)
992+
// and by the runtime dispatcher, read by objectui's Integrations connect card,
993+
// declared by nothing. `ApiRoutesSchema` is a plain `z.object`, so a
994+
// spec-strict consumer stripped it silently — the connect card would blank with
995+
// no error. The producer-side conformance tests now check `routes` keys the same
996+
// way they check top-level keys, and derive their allowance from
997+
// `ApiRoutesSchema`; these two pin that the allowance is the real one.
998+
describe('[#5679] routes: the declared key set the producer gates check against', () => {
999+
it('is the very schema `DiscoverySchema` nests, not a second copy of it', () => {
1000+
// The three producer gates read `ApiRoutesSchema.shape`. That is only a
1001+
// faithful allowance while it IS what `DiscoverySchema.routes` declares —
1002+
// if the two ever diverge the gates would be policing a schema nothing
1003+
// parses with, which is how #4828's blind spot was built in the first place.
1004+
const nested = (DiscoverySchema as any).shape.routes;
1005+
expect(new Set(Object.keys(nested.shape)))
1006+
.toEqual(new Set(Object.keys((ApiRoutesSchema as any).shape)));
1007+
});
1008+
1009+
it('declares `mcp` — optional, string, and stripped-not-rejected before it was declared', () => {
1010+
const declared = Object.keys((ApiRoutesSchema as any).shape);
1011+
expect(declared).toContain('mcp');
1012+
1013+
const base = { data: '/api/v1/data', metadata: '/api/v1/meta' };
1014+
1015+
// Present: survives the parse. This is the assertion that was impossible
1016+
// before — `mcp` used to be dropped here, which is the whole defect.
1017+
expect(ApiRoutesSchema.parse({ ...base, mcp: '/api/v1/mcp' }).mcp).toBe('/api/v1/mcp');
1018+
1019+
// Absent: optional, so a producer that cannot answer omits it.
1020+
expect(ApiRoutesSchema.parse(base).mcp).toBeUndefined();
1021+
1022+
// A non-string is now rejected rather than silently dropped — the value
1023+
// half of the contract, which a key-set gate alone cannot judge.
1024+
expect(() => ApiRoutesSchema.parse({ ...base, mcp: 123 })).toThrow();
1025+
1026+
// And a still-undeclared key is still stripped, which is why the producer
1027+
// gates need their key-set check in addition to this parse.
1028+
expect(ApiRoutesSchema.parse({ ...base, notAThing: '/x' } as any))
1029+
.not.toHaveProperty('notAThing');
1030+
});
1031+
});
1032+
9861033
describe('[#4828] scoping (decision 3 — declare what REST actually emits)', () => {
9871034
const base = {
9881035
name: 'ObjectStack',

0 commit comments

Comments
 (0)