Skip to content

Commit dcafe58

Browse files
test(rest): restore the #5821 ghost-path pin, whose input set had silently become empty (#6894) (#6986)
`rest-openapi-route.test.ts` pinned "no static-artifact path leaks into the served document" by looping over the artifact's own `paths`. #5744 removed the spec-side route-section emission (correctly, ADR-0076), so that input became the empty set and the loop stopped executing — the case kept reporting green while asserting nothing. Feed the pin a constructed `paths` section instead of waiting for the producer to re-emit a removed defect, and guard the input set itself so the layer cannot go hollow again without failing. The three surviving assertions (`components.schemas` key set, `securitySchemes`, `info.title`) move into their own case unchanged — they still bite. Claude-Session: https://claude.ai/code/session_01USNUyHEr7uaU6MoEWXitei Co-authored-by: Claude <noreply@anthropic.com>
1 parent 59c544d commit dcafe58

1 file changed

Lines changed: 76 additions & 10 deletions

File tree

packages/rest/src/rest-openapi-route.test.ts

Lines changed: 76 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,42 @@ const PHANTOM_PATHS = [
170170
'/api/.well-known/objectstack',
171171
];
172172

173+
/**
174+
* The `paths` section the static artifact carried until #5744 — rebuilt here
175+
* as a FIXTURE, because the artifact no longer supplies one.
176+
*
177+
* #5821 pinned "no artifact path leaks into the served document" by reading
178+
* the artifact's OWN `paths` and looping over them. That was the right input
179+
* while spec still emitted a route section. #5744 then removed the emission —
180+
* correctly, ADR-0076: a section can only be produced by the package that
181+
* mounts the routes — and the pin's input silently became the empty set.
182+
* Re-measured on this branch through the same `loadOpenApiSpec` seam the pin
183+
* uses (#6894):
184+
*
185+
* #5821 pin input set (stale paths): [] -> size 0
186+
* loop body executions: 0
187+
*
188+
* A `for` over nothing asserts nothing, and the case stayed green saying so.
189+
* The other direction — waiting for the artifact to carry a route section
190+
* again — would mean asking the producer to keep a removed defect alive in
191+
* order to feed a test, so the input is constructed here instead.
192+
*/
193+
function staleArtifactPaths(): Record<string, any> {
194+
const operation = (operationId: string) => ({
195+
operationId,
196+
responses: { '200': { description: 'OK' } },
197+
});
198+
const section: Record<string, any> = {};
199+
for (const path of PHANTOM_PATHS) {
200+
section[path] = { get: operation(`stale ${path}`) };
201+
}
202+
// The historical section's other defect, kept so the fixture is the shape
203+
// that really shipped rather than a uniform one: it documented PUT on a
204+
// record, a verb this server answers 405 to.
205+
section['/api/{object}/{id}'].put = operation('stale put record');
206+
return section;
207+
}
208+
173209
describe('#5588 — built-in routes come from rest, not from the static artifact', () => {
174210
it('publishes no path the server does not mount', async () => {
175211
// The converse of the bug, asserted as a set relation rather than by
@@ -273,23 +309,53 @@ describe('#5588 — built-in routes come from rest, not from the static artifact
273309
expect(names).toEqual(['environmentId', 'object']);
274310
});
275311

276-
it('discards the static artifact section even though spec still emits it', async () => {
277-
// Leg 2 (#5744) removes the spec-side generation. Until it lands, the
278-
// bundled artifact really does carry `/api/{object}` & co — so the serve
279-
// path has to DISCARD, not merge. Proven against the artifact this
280-
// runtime actually loads.
312+
it('passes the half of the document `packages/spec` owns through serve untouched', async () => {
313+
// The artifact's surviving half — `components.schemas`, `securitySchemes`,
314+
// `info` — is the contract, and serve-time enrichment must not touch it.
281315
const rest = makeRest(makeProtocol({ object: [], api: [] }).protocol);
282316
const artifact = await (rest as any).loadOpenApiSpec();
283317
expect(artifact, 'the bundled artifact must be loadable for this pin to mean anything').toBeTruthy();
284-
const stale = Object.keys(artifact.paths ?? {});
318+
// Recorded rather than assumed, because the pin below depends on it: since
319+
// #5744 the artifact is the contract half ONLY and describes no routes, so
320+
// the discard case has to plant its own section to have anything to check.
321+
expect(
322+
artifact.paths,
323+
'since #5744 the artifact emits no route section — if this ever comes back, feed it to the discard pin below instead of a fixture',
324+
).toBeUndefined();
285325

286326
const { body } = await serveOpenApiFrom(rest);
287-
for (const path of stale) {
288-
expect(body.paths[path], `stale artifact path '${path}' leaked into the served document`).toBeUndefined();
289-
}
290-
// What spec genuinely owns survives untouched.
291327
expect(Object.keys(body.components.schemas)).toEqual(Object.keys(artifact.components.schemas));
292328
expect(body.components.securitySchemes).toEqual(artifact.components.securitySchemes);
293329
expect(body.info.title).toBe(artifact.info.title);
294330
});
331+
332+
it('discards a `paths`-carrying artifact instead of merging it', async () => {
333+
// #5588 ruling C: the serve path DISCARDS whatever route section the
334+
// static artifact carries rather than merging it, because a merge with a
335+
// wrong section republishes the wrong section. Today's artifact carries
336+
// none (#5744), so the only honest way to keep asserting the discard is to
337+
// hand the handler one that does — see `staleArtifactPaths` for why the
338+
// input is a fixture and not the artifact's own key any more (#6894).
339+
const rest = makeRest(makeProtocol({ object: [], api: [] }).protocol);
340+
const artifact = await (rest as any).loadOpenApiSpec();
341+
expect(artifact, 'the bundled artifact must be loadable for this pin to mean anything').toBeTruthy();
342+
// `loadOpenApiSpec` caches per instance and hands back the cached object
343+
// itself, so planting the section on it is what the handler will read.
344+
artifact.paths = staleArtifactPaths();
345+
346+
// Read the input back THROUGH the loader, and require it to be non-empty.
347+
// This assertion is the one that would have caught #6894: it turns "the
348+
// loop found nothing to check" from a silent pass into a failure, so the
349+
// layer cannot go hollow again without saying so.
350+
const stale = Object.keys((await (rest as any).loadOpenApiSpec()).paths ?? {});
351+
expect(
352+
stale.length,
353+
'the pin input set is empty — the loop below would assert nothing (#6894)',
354+
).toBeGreaterThan(0);
355+
356+
const { body } = await serveOpenApiFrom(rest);
357+
for (const path of stale) {
358+
expect(body.paths[path], `stale artifact path '${path}' leaked into the served document`).toBeUndefined();
359+
}
360+
});
295361
});

0 commit comments

Comments
 (0)