@@ -322,6 +322,12 @@ describe('#5588 — built-in routes come from rest, not from the static artifact
322322 it ( 'passes the half of the document `packages/spec` owns through serve untouched' , async ( ) => {
323323 // The artifact's surviving half — `components.schemas`, `securitySchemes`,
324324 // `info` — is the contract, and serve-time enrichment must not touch it.
325+ //
326+ // Until #11646 the `info` third of that claim was narrowed to `title`
327+ // alone, precisely because the serve path overwrote `info.version` with
328+ // the deployment's `api.version`: the invariant was stated and then
329+ // excepted, in this same file. The override is deleted, so the exception
330+ // is gone and the pin below covers the WHOLE block.
325331 const rest = makeRest ( makeProtocol ( { object : [ ] , api : [ ] } ) . protocol ) ;
326332 const artifact = await ( rest as any ) . loadOpenApiSpec ( ) ;
327333 expect ( artifact , 'the bundled artifact must be loadable for this pin to mean anything' ) . toBeTruthy ( ) ;
@@ -336,7 +342,16 @@ describe('#5588 — built-in routes come from rest, not from the static artifact
336342 const { body } = await serveOpenApiFrom ( rest ) ;
337343 expect ( Object . keys ( body . components . schemas ) ) . toEqual ( Object . keys ( artifact . components . schemas ) ) ;
338344 expect ( body . components . securitySchemes ) . toEqual ( artifact . components . securitySchemes ) ;
339- expect ( body . info . title ) . toBe ( artifact . info . title ) ;
345+ // Anti-vacuity: a whole-block `toEqual` over an `info` with no `version`
346+ // would pass without ever reaching the field #11646 closed.
347+ expect (
348+ artifact . info . version ,
349+ 'the artifact must carry a version, or the whole-block pin below never reaches the field #11646 closed' ,
350+ ) . toBeTruthy ( ) ;
351+ expect (
352+ body . info ,
353+ 'serve-time enrichment must not touch `info` — the WHOLE block is the artifact\'s, `version` included (#11646). A serve-time override of any `info` field lands here.' ,
354+ ) . toEqual ( artifact . info ) ;
340355 } ) ;
341356
342357 it ( 'discards a `paths`-carrying artifact instead of merging it' , async ( ) => {
@@ -370,55 +385,87 @@ describe('#5588 — built-in routes come from rest, not from the static artifact
370385 } ) ;
371386} ) ;
372387
373- describe ( 'GET /openapi.json — what `info.version` carries (#11546)' , ( ) => {
388+ describe ( 'GET /openapi.json — what `info.version` carries (#11546, #11646)' , ( ) => {
389+ // History, because this field has now been three different things.
390+ //
374391 // The line under test used to read
375392 // `version: this.config.api.version || enriched.info.version`
376393 // under a comment promising "the runtime version so consumers don't pin to
377394 // the spec package's compile-time version". Both halves were false, and
378395 // nothing pinned either one, so the document could have drifted to any of
379- // three different facts without a test noticing. These four fix what the
380- // field means.
396+ // three different facts without a test noticing. #11546 pinned it to the
397+ // declared API version identifier and deleted the fallback.
398+ //
399+ // #11646 then deleted the serve-time override itself (maintainer ruling,
400+ // 2026-08-25, option B): the served `info.version` is the ARTIFACT's, so
401+ // the served document and `@objectstack/spec`'s published `./openapi.json`
402+ // export state one fact instead of two. The declared identifier still
403+ // exists and is still observable — it builds the mount — it just no longer
404+ // rewrites a producer-owned contract field. The cases below are the
405+ // inversion of the #11546 ones: each now asserts the value the override
406+ // used to suppress.
381407 //
382408 // OpenAPI 3.1, Info Object: `version` is "the version of the OpenAPI
383409 // document (which is distinct from the OpenAPI Specification version or the
384410 // API implementation version)". The runtime version is the implementation
385411 // version, so it is the one value the field's own definition excludes —
386412 // which is why this is NOT the shape #11292 settled for `/discovery`, where
387- // `DiscoverySchema.version` means the serving artifact by #10993.
413+ // `DiscoverySchema.version` means the serving artifact by #10993. That
414+ // exclusion is unchanged by #11646: the artifact version is the document's
415+ // version, not the runtime's.
388416
389- it ( 'serves the declared API version identifier , not the artifact version' , async ( ) => {
417+ it ( 'serves the artifact version, not the declared API version identifier ' , async ( ) => {
390418 const rest = makeRest ( makeProtocol ( { object : [ ] , api : [ ] } ) . protocol ) ;
391419 const artifact = await ( rest as any ) . loadOpenApiSpec ( ) ;
392420 const { body } = await serveOpenApiFrom ( rest ) ;
393421
394- expect ( body . info . version ) . toBe ( 'v1' ) ;
395- // The serve path deliberately overrides the producer here, so the pin is
396- // only meaningful while the two values actually differ — if they ever
397- // converge this assertion says so instead of passing vacuously.
422+ expect ( body . info . version ) . toBe ( artifact . info . version ) ;
423+ // Only meaningful while the two candidate facts are actually different
424+ // values — if they ever converge this says so instead of passing
425+ // vacuously, exactly as the #11546 version of this case did in the other
426+ // direction.
398427 expect (
399428 artifact . info . version ,
400- 'the artifact must carry a DIFFERENT version for the override pin above to mean anything' ,
429+ 'the artifact must carry a version DIFFERENT from the declared `api. version` for the pin above to mean anything' ,
401430 ) . not . toBe ( 'v1' ) ;
402- expect ( body . info . version ) . not . toBe ( artifact . info . version ) ;
431+ expect ( body . info . version ) . not . toBe ( 'v1' ) ;
403432 } ) ;
404433
405- it ( 'tracks a custom `api.version`, which is also the mount segment ' , async ( ) => {
434+ it ( 'does not track a custom `api.version` — that identifier moves the MOUNT, not `info.version` ' , async ( ) => {
406435 const rest = makeRest ( makeProtocol ( { object : [ ] , api : [ ] } ) . protocol , { version : 'v9' } ) ;
436+ const artifact = await ( rest as any ) . loadOpenApiSpec ( ) ;
407437 const { body } = await serveOpenApiFrom ( rest , '/api/v9' ) ;
408- expect ( body . info . version ) . toBe ( 'v9' ) ;
438+
439+ // The identifier is still live and still observable — establish that
440+ // FIRST, so a v9 that quietly stopped taking effect cannot make the
441+ // `info.version` half below pass for the wrong reason.
442+ expect (
443+ Object . keys ( body . paths ) . length ,
444+ 'the v9 mount published no paths — this case would then assert nothing about the identifier' ,
445+ ) . toBeGreaterThan ( 0 ) ;
446+ for ( const path of Object . keys ( body . paths ) ) {
447+ expect ( path . startsWith ( '/api/v9' ) , `'${ path } ' did not follow the v9 mount` ) . toBe ( true ) ;
448+ }
449+
450+ // ...and it does not reach `info`, which is the artifact's.
451+ expect ( body . info . version ) . toBe ( artifact . info . version ) ;
452+ expect ( body . info . version ) . not . toBe ( 'v9' ) ;
409453 } ) ;
410454
411455 it ( 'is not the runtime version — an `OS_RUNTIME_VERSION` stamp does not reach it' , async ( ) => {
412- // The anti-regression pin for the direction this card did NOT take. Were
413- // the field re-pointed at `resolveDiscoveryVersion()`, the sentinel below
414- // would land in the served document and this goes red.
456+ // The anti-regression pin for the direction NEITHER card took. Were the
457+ // field re-pointed at `resolveDiscoveryVersion()`, the sentinel below
458+ // would land in the served document and this goes red. #11646 moved the
459+ // expected value from the declared identifier to the artifact's version;
460+ // the excluded value is unchanged.
415461 const SENTINEL = '9.9.9-openapi-info-version-sentinel' ;
416462 const old = process . env . OS_RUNTIME_VERSION ;
417463 process . env . OS_RUNTIME_VERSION = SENTINEL ;
418464 try {
419465 const rest = makeRest ( makeProtocol ( { object : [ ] , api : [ ] } ) . protocol ) ;
466+ const artifact = await ( rest as any ) . loadOpenApiSpec ( ) ;
420467 const { body } = await serveOpenApiFrom ( rest ) ;
421- expect ( body . info . version ) . toBe ( 'v1' ) ;
468+ expect ( body . info . version ) . toBe ( artifact . info . version ) ;
422469 expect ( JSON . stringify ( body . info ) ) . not . toContain ( SENTINEL ) ;
423470 } finally {
424471 if ( old === undefined ) delete process . env . OS_RUNTIME_VERSION ;
@@ -435,10 +482,15 @@ describe('GET /openapi.json — what `info.version` carries (#11546)', () => {
435482 // instead of cast, so `RestServer` refuses the construction and the
436483 // doubled-slash mount is unreachable.
437484 //
438- // The fact the old pin protected is unchanged and still covered above:
439- // there is still NO `|| enriched.info.version` fallback, so a configured
440- // version is served as itself. What changed is that `''` is no longer a
441- // configurable version.
485+ // What this pin protects is the CONSTRUCTION refusal, and that is
486+ // unchanged: `''` is not a configurable version.
487+ //
488+ // The other half of this comment used to read "there is still NO
489+ // `|| enriched.info.version` fallback, so a configured version is served
490+ // as itself" — #11646 retired that sentence with the override it
491+ // described. `api.version` no longer reaches `info.version` at all, by
492+ // any path, fallback or otherwise; it builds the mount, which is what the
493+ // refusal above keeps well-formed.
442494 expect ( ( ) => makeRest ( makeProtocol ( { object : [ ] , api : [ ] } ) . protocol , { version : '' } ) )
443495 . toThrow ( / a p i \. v e r s i o n / ) ;
444496 } ) ;
0 commit comments