@@ -406,3 +406,197 @@ describe('[#5707] the layered read stops painting an outage as "nothing was cust
406406function p_layered ( engine : any , request : Record < string , unknown > ) : Promise < any > {
407407 return new ObjectStackProtocolImplementation ( engine ) . getMetaItemLayered ( request as any ) ;
408408}
409+
410+ // ---------------------------------------------------------------------------
411+ // [#5840] The OTHER read in these same two methods — the MetadataService one
412+ // ---------------------------------------------------------------------------
413+ // Everything above is about the `sys_metadata` overlay read, whose failure the
414+ // protocol can see because it arrives as a throw. The second source each of
415+ // these methods consults — the `metadata` SERVICE, i.e. the loader chain
416+ // (filesystem, database, attached repository) — failed silently: a loader that
417+ // throws is warn-logged and skipped inside `MetadataManager`, and its `get()`
418+ // dropped the `degraded` verdict `loadDiagnosed` had already computed. So an
419+ // unreachable metadata database arrived here as the ordinary `undefined` of a
420+ // name nobody declared, and the SAME two methods that now refuse to guess on
421+ // their overlay half went on guessing on this one:
422+ //
423+ // getMetaItem → falls through, item stays undefined → 404 "not found"
424+ // getMetaItemLayered → `code: null` → `lockSource = code ?? overlay ?? {}`
425+ // → `editable: true, deletable: true` on an item whose
426+ // code layer may declare `_lock: 'full'`
427+ //
428+ // The second is the sharper one, and it is the shape ADR-0110 D3 names
429+ // outright: an availability failure widening an affordance. `getDiagnosed`
430+ // (#5840) is the seam that makes the failure visible at all; these cases pin
431+ // what each method does with it.
432+ //
433+ // Reverse verification, direction predicted BEFORE running: ordinary red, and
434+ // it must be taken on the CONSUMER, not the producer. These doubles feed the
435+ // return contract directly, so reverting `MetadataManager.getDiagnosed` cannot
436+ // turn them red — only deleting the two `if (… degraded)` branches in
437+ // `protocol.ts` can. Two laps therefore prove two different halves, and
438+ // neither substitutes for the other.
439+ //
440+ // Predicted for the consumer lap: 5 red / 3 green across the two describes
441+ // below — every case that expects a 503, and only those, with all three
442+ // narrowness/back-compat cases green (they assert the branch does NOT fire).
443+ // Measured: exactly that, and the 18 #5532/#5707 cases above stayed green,
444+ // which is what shows this is the third read joining the rule rather than a
445+ // blanket "these methods now throw".
446+
447+ /** A services registry holding one `metadata` service — what the protocol probes. */
448+ function servicesWith ( metadata : unknown ) : ( ) => Map < string , any > {
449+ const registry = new Map < string , any > ( [ [ 'metadata' , metadata ] ] ) ;
450+ return ( ) => registry ;
451+ }
452+
453+ const LOADER_FAILURE = 'database: connect ECONNREFUSED 10.0.0.5:5432' ;
454+
455+ /**
456+ * A `metadata` service whose loader chain is DOWN. `get()` answers exactly what
457+ * it answered before this issue — `undefined`, indistinguishable from a miss —
458+ * and `getDiagnosed()` reports the verdict that was being computed and thrown
459+ * away all along.
460+ */
461+ const metadataServiceInOutage = ( ) => ( {
462+ get : vi . fn ( async ( ) => undefined ) ,
463+ getDiagnosed : vi . fn ( async ( ) => ( { data : undefined , degraded : true , errors : [ LOADER_FAILURE ] } ) ) ,
464+ } ) ;
465+
466+ /** A `metadata` service that answered, and simply does not hold the item. */
467+ const metadataServiceWithMiss = ( ) => ( {
468+ get : vi . fn ( async ( ) => undefined ) ,
469+ getDiagnosed : vi . fn ( async ( ) => ( { data : undefined , degraded : false , errors : [ ] } ) ) ,
470+ } ) ;
471+
472+ /** A `metadata` service that holds `body`. */
473+ const metadataServiceHolding = ( body : unknown ) => ( {
474+ get : vi . fn ( async ( ) => body ) ,
475+ getDiagnosed : vi . fn ( async ( ) => ( { data : body , degraded : false , errors : [ ] } ) ) ,
476+ } ) ;
477+
478+ /** A service that predates #5840: `get` only, no way to report the difference. */
479+ const legacyMetadataService = ( body ?: unknown ) => ( { get : vi . fn ( async ( ) => body ) } ) ;
480+
481+ /** The outage envelope, for a cause built from loader messages rather than a driver error. */
482+ function expectLoaderOutage ( caught : any ) {
483+ expect ( caught ?. status ) . toBe ( 503 ) ;
484+ expect ( caught ?. code ) . toBe ( 'SERVICE_UNAVAILABLE' ) ;
485+ expect ( ErrorCode . safeParse ( caught ?. code ) . success ) . toBe ( true ) ;
486+ expect ( caught . message ) . toContain ( 'unknown' ) ;
487+ expect ( caught . message . toLowerCase ( ) ) . not . toContain ( 'not found' ) ;
488+ // The failing loaders' own words reach the operator on `cause`, which is
489+ // what `logWithheldServerFault` prints (#5437) — the protocol never sees a
490+ // driver error here, because `MetadataManager` already absorbed it.
491+ expect ( String ( ( caught . cause as Error ) ?. message ) ) . toContain ( LOADER_FAILURE ) ;
492+ }
493+
494+ /** A protocol whose overlay store is healthy and empty — only the SERVICE half varies. */
495+ function protocolWithService ( metadata : unknown , registryItems : Record < string , any > = { } ) {
496+ return new ObjectStackProtocolImplementation (
497+ engineWithRows ( [ ] , registryItems ) ,
498+ servicesWith ( metadata ) ,
499+ ) ;
500+ }
501+
502+ describe ( '[#5840] a MetadataService outage stops arriving as "nobody declared this"' , ( ) => {
503+ it ( 'the singular read throws 503 instead of falling through to a 404' , async ( ) => {
504+ const p = protocolWithService ( metadataServiceInOutage ( ) ) ;
505+
506+ const caught = await rejection ( ( ) => p . getMetaItem ( { type : 'object' , name : 'acct' } as any ) ) ;
507+ expectLoaderOutage ( caught ) ;
508+ } ) ;
509+
510+ it ( 'getMetaItemCached stops relabelling that outage "Metadata item object/acct not found"' , async ( ) => {
511+ const p = protocolWithService ( metadataServiceInOutage ( ) ) ;
512+
513+ const caught = await rejection ( ( ) => p . getMetaItemCached ( { type : 'object' , name : 'acct' } as any ) ) ;
514+ expectLoaderOutage ( caught ) ;
515+ // The comment above that 404 claims "reaching here now means a real
516+ // miss". This is the half that used to make it untrue.
517+ expect ( caught . message ) . not . toContain ( 'Metadata item object/acct not found' ) ;
518+ } ) ;
519+
520+ it ( 'a miss and an outage are told apart by code alone — the whole point' , async ( ) => {
521+ const missP = protocolWithService ( metadataServiceWithMiss ( ) ) ;
522+ const outageP = protocolWithService ( metadataServiceInOutage ( ) ) ;
523+
524+ const miss = await rejection ( ( ) => missP . getMetaItemCached ( { type : 'object' , name : 'ghost' } as any ) ) ;
525+ const outage = await rejection ( ( ) => outageP . getMetaItemCached ( { type : 'object' , name : 'ghost' } as any ) ) ;
526+
527+ expect ( [ miss . status , miss . code ] ) . toEqual ( [ 404 , 'RESOURCE_NOT_FOUND' ] ) ;
528+ expect ( [ outage . status , outage . code ] ) . toEqual ( [ 503 , 'SERVICE_UNAVAILABLE' ] ) ;
529+ } ) ;
530+
531+ it ( 'the layered read refuses to publish a `code: null` it never verified' , async ( ) => {
532+ const p = protocolWithService ( metadataServiceInOutage ( ) ) ;
533+
534+ const caught = await rejection (
535+ ( ) => p . getMetaItemLayered ( { type : 'object' , name : 'acct' } as any ) ,
536+ ) ;
537+ expectLoaderOutage ( caught ) ;
538+ } ) ;
539+
540+ it ( 'and that is what stops an outage from unlocking a locked artifact' , async ( ) => {
541+ // The concrete widening. `lockSource = code ?? overlay ?? {}`, so a
542+ // code layer that never arrived resolves the protection envelope from
543+ // `{}` — `editable: true, deletable: true` on an item the packager
544+ // locked. Left column: what the truth looks like. Right column: what
545+ // the outage used to render, and now cannot.
546+ const locked = { name : 'acct' , label : 'Account' , _lock : 'full' } ;
547+
548+ const healthy : any = await protocolWithService (
549+ metadataServiceHolding ( locked ) ,
550+ ) . getMetaItemLayered ( { type : 'object' , name : 'acct' } as any ) ;
551+ expect ( healthy . lock ) . toBe ( 'full' ) ;
552+ expect ( [ healthy . editable , healthy . deletable ] ) . toEqual ( [ false , false ] ) ;
553+
554+ const caught = await rejection (
555+ ( ) => protocolWithService ( metadataServiceInOutage ( ) )
556+ . getMetaItemLayered ( { type : 'object' , name : 'acct' } as any ) ,
557+ ) ;
558+ expectLoaderOutage ( caught ) ;
559+ // Never the silently-permissive envelope.
560+ expect ( caught . editable ) . toBeUndefined ( ) ;
561+ } ) ;
562+ } ) ;
563+
564+ describe ( '[#5840] the narrowness is the design — three things it deliberately does not do' , ( ) => {
565+ it ( 'a registry hit still answers, degraded service or not' , async ( ) => {
566+ // A registry item IS a real declaration, so the answer contains no
567+ // unfounded claim and is served exactly as before. The 503 fires only
568+ // where the alternative would have been "this does not exist".
569+ const p = protocolWithService ( metadataServiceInOutage ( ) , {
570+ acct : { name : 'acct' , label : 'Account (packaged)' } ,
571+ } ) ;
572+
573+ const res : any = await p . getMetaItem ( { type : 'object' , name : 'acct' } as any ) ;
574+ expect ( res . item ?. label ) . toBe ( 'Account (packaged)' ) ;
575+
576+ const layered : any = await p . getMetaItemLayered ( { type : 'object' , name : 'acct' } as any ) ;
577+ expect ( layered . code ) . toMatchObject ( { label : 'Account (packaged)' } ) ;
578+ } ) ;
579+
580+ it ( 'a clean MISS still renders the all-null layered envelope, not a 503' , async ( ) => {
581+ const p = protocolWithService ( metadataServiceWithMiss ( ) ) ;
582+
583+ const res : any = await p . getMetaItemLayered ( { type : 'object' , name : 'ghost' } as any ) ;
584+ expect ( res . code ) . toBeNull ( ) ;
585+ expect ( res . overlay ) . toBeNull ( ) ;
586+ expect ( res . effective ) . toBeNull ( ) ;
587+ } ) ;
588+
589+ it ( 'a service that predates `getDiagnosed` behaves exactly as it did' , async ( ) => {
590+ // It cannot report the distinction, so it is read as "not degraded" —
591+ // precisely what it could express before, unchanged. The alternative
592+ // (treating an un-probeable service as suspect) would 503 every host
593+ // whose `metadata` slot is a shim.
594+ const holding = protocolWithService ( legacyMetadataService ( { name : 'acct' , label : 'From shim' } ) ) ;
595+ const res : any = await holding . getMetaItem ( { type : 'object' , name : 'acct' } as any ) ;
596+ expect ( res . item ?. label ) . toBe ( 'From shim' ) ;
597+
598+ const empty = protocolWithService ( legacyMetadataService ( undefined ) ) ;
599+ const caught = await rejection ( ( ) => empty . getMetaItemCached ( { type : 'object' , name : 'ghost' } as any ) ) ;
600+ expect ( [ caught . status , caught . code ] ) . toEqual ( [ 404 , 'RESOURCE_NOT_FOUND' ] ) ;
601+ } ) ;
602+ } ) ;
0 commit comments