@@ -277,3 +277,132 @@ describe('[#5532] the benign case and the healthy case are untouched', () => {
277277 expect ( res . item ?. label ) . toBe ( 'Overlaid' ) ;
278278 } ) ;
279279} ) ;
280+
281+ // ---------------------------------------------------------------------------
282+ // [#5707] The same rule on the LAYERED read — kept in this file on purpose
283+ // ---------------------------------------------------------------------------
284+ // `getMetaItemLayered` is a different method and a different read, which is why
285+ // PR #5705 did not reach it (scope = the issue). It is the same DEFECT, so its
286+ // coverage lives next to the four reads above: a future edit that re-widens one
287+ // catch and not the others is then a diff in one file, and the shared
288+ // `expectStoreUnavailable` keeps the envelope from drifting per-method.
289+ //
290+ // What the swallow answered is worse-shaped here than on the singular read. A
291+ // 404 at least says "I have no item for you"; the layered envelope makes three
292+ // POSITIVE claims at once, all of them about what the author declared:
293+ //
294+ // overlay: null → "this item has never been customised"
295+ // overlayScope: null → "no org and no env scope holds a row"
296+ // effective === code → "what runs today is the packaged artifact, verbatim"
297+ //
298+ // and it makes them with HTTP 200, so no client can tell. The Studio diff tab
299+ // exists to answer "what did I change?" — during an outage it answered
300+ // "nothing", which is #5532's error (an availability failure told as an
301+ // authorship fact) landing in the diff view instead of on a 404.
302+ //
303+ // Reverse verification, direction predicted BEFORE running: ordinary red.
304+ // Restoring ` } catch { /* DB unavailable — overlay stays null */ }` turns the
305+ // three outage cases below red (they resolve instead of throwing) and leaves
306+ // the three benign/healthy cases green — that separation is what shows the
307+ // change is the outage split and not a blanket "layered now throws".
308+
309+ describe ( '[#5707] the layered read stops painting an outage as "nothing was customised"' , ( ) => {
310+ /** The registry's artifact baseline — the layer an outage used to promote to `effective`. */
311+ const codeBaseline = { name : 'acct' , label : 'Account (packaged)' } ;
312+
313+ it ( 'throws the same 503 envelope instead of resolving a fabricated 3-layer view' , async ( ) => {
314+ const err = connectionRefused ( ) ;
315+ const p = new ObjectStackProtocolImplementation (
316+ engineThatCannotBeRead ( ( ) => err , { acct : codeBaseline } ) ,
317+ ) ;
318+
319+ const caught = await rejection (
320+ ( ) => p . getMetaItemLayered ( { type : 'object' , name : 'acct' } as any ) ,
321+ ) ;
322+ expectStoreUnavailable ( caught , err ) ;
323+ } ) ;
324+
325+ it ( 'the two failures that used to render IDENTICALLY are now told apart' , async ( ) => {
326+ // Same registry, same request, same all-reads-fail engine — only the
327+ // error TYPE differs, and before the fix both produced byte-identical
328+ // envelopes (`overlay: null`, `overlayScope: null`, `effective = code`,
329+ // HTTP 200). That indistinguishability IS the defect: one of them means
330+ // "nothing was ever customised", the other means "I could not look".
331+ const benign = new ObjectStackProtocolImplementation (
332+ engineThatCannotBeRead ( missingTable , { acct : codeBaseline } ) ,
333+ ) ;
334+ const outage = new ObjectStackProtocolImplementation (
335+ engineThatCannotBeRead ( connectionRefused , { acct : codeBaseline } ) ,
336+ ) ;
337+
338+ const firstBoot : any = await benign . getMetaItemLayered ( { type : 'object' , name : 'acct' } as any ) ;
339+ expect ( [ firstBoot . overlay , firstBoot . overlayScope ] ) . toEqual ( [ null , null ] ) ;
340+ expect ( firstBoot . effective ) . toBe ( firstBoot . code ) ;
341+
342+ const caught = await rejection (
343+ ( ) => outage . getMetaItemLayered ( { type : 'object' , name : 'acct' } as any ) ,
344+ ) ;
345+ expect ( [ caught . status , caught . code ] ) . toEqual ( [ 503 , 'SERVICE_UNAVAILABLE' ] ) ;
346+ } ) ;
347+
348+ it ( 'a failed ORG-scope read does not silently demote to the env row it never got to' , async ( ) => {
349+ // The org lookup runs first and the env lookup is inside the same
350+ // `try`, so the swallow hid BOTH: an env-wide overlay row that was
351+ // perfectly readable was reported as "no overlay" because the org read
352+ // failed ahead of it.
353+ const err = connectionRefused ( ) ;
354+ const engine = engineWithRows ( [ ] ) ;
355+ engine . findOne = vi . fn ( async ( _o : string , opts : any ) => {
356+ if ( opts ?. where ?. organization_id === 'org_acme' ) throw err ;
357+ return { type : 'object' , name : 'acct' , state : 'active' , metadata : JSON . stringify ( { name : 'acct' , label : 'Env overlay' } ) } ;
358+ } ) ;
359+
360+ const caught = await rejection (
361+ ( ) => p_layered ( engine , { type : 'object' , name : 'acct' , organizationId : 'org_acme' } ) ,
362+ ) ;
363+ expectStoreUnavailable ( caught , err ) ;
364+ } ) ;
365+
366+ it ( 'an unprovisioned sys_metadata still renders the code layer (benign, unchanged)' , async ( ) => {
367+ // First boot: no overlay row EXISTS, so `overlay: null`,
368+ // `overlayScope: null` and `effective === code` are the truth — and the
369+ // diff tab must keep rendering rather than 503 on every fresh install.
370+ const p = new ObjectStackProtocolImplementation (
371+ engineThatCannotBeRead ( missingTable , { acct : codeBaseline } ) ,
372+ ) ;
373+
374+ const res : any = await p . getMetaItemLayered ( { type : 'object' , name : 'acct' } as any ) ;
375+ expect ( res . code ) . toMatchObject ( { label : 'Account (packaged)' } ) ;
376+ expect ( res . overlay ) . toBeNull ( ) ;
377+ expect ( res . overlayScope ) . toBeNull ( ) ;
378+ expect ( res . effective ) . toBe ( res . code ) ;
379+ } ) ;
380+
381+ it ( 'an unprovisioned sys_metadata + nothing anywhere is still an all-null envelope, not a 503' , async ( ) => {
382+ const p = new ObjectStackProtocolImplementation ( engineThatCannotBeRead ( missingTable ) ) ;
383+
384+ const res : any = await p . getMetaItemLayered ( { type : 'object' , name : 'ghost' } as any ) ;
385+ expect ( res . code ) . toBeNull ( ) ;
386+ expect ( res . overlay ) . toBeNull ( ) ;
387+ expect ( res . effective ) . toBeNull ( ) ;
388+ } ) ;
389+
390+ it ( 'a healthy store still reports the overlay layer and its scope' , async ( ) => {
391+ const engine = engineWithRows ( [ ] , { acct : codeBaseline } ) ;
392+ engine . findOne = vi . fn ( async ( _o : string , opts : any ) =>
393+ ( opts ?. where ?. organization_id === null
394+ ? { type : 'object' , name : 'acct' , state : 'active' , metadata : JSON . stringify ( { name : 'acct' , label : 'Env overlay' } ) }
395+ : null ) ,
396+ ) ;
397+
398+ const res : any = await p_layered ( engine , { type : 'object' , name : 'acct' } ) ;
399+ expect ( res . overlay ) . toMatchObject ( { label : 'Env overlay' } ) ;
400+ expect ( res . overlayScope ) . toBe ( 'env' ) ;
401+ expect ( res . effective ) . toBe ( res . overlay ) ;
402+ } ) ;
403+ } ) ;
404+
405+ /** `getMetaItemLayered` on a protocol built over `engine` — the call is 3 lines otherwise. */
406+ function p_layered ( engine : any , request : Record < string , unknown > ) : Promise < any > {
407+ return new ObjectStackProtocolImplementation ( engine ) . getMetaItemLayered ( request as any ) ;
408+ }
0 commit comments