@@ -688,6 +688,106 @@ describe('sheafify', () => {
688688 ( section as Record < string , unknown > ) [ GET_DESCRIPTION ] ,
689689 ) . toBeUndefined ( ) ;
690690 } ) ;
691+
692+ it ( 'does not drop prototype-named distinguishing metadata keys from stripped candidates' , async ( ) => {
693+ // 'constructor' matches Object.prototype.constructor. Naive `key in constraints`
694+ // returns true on an empty {} because of the prototype chain, causing the key to be
695+ // silently dropped from every stripped candidate even though it was never a constraint.
696+ type Meta = Record < string , unknown > ;
697+ let capturedCandidates : Candidate < Partial < Meta > > [ ] = [ ] ;
698+ let capturedContext : PolicyContext < Meta > | undefined ;
699+
700+ const spy : Policy < Meta > = async function * ( candidates , context ) {
701+ capturedCandidates = candidates ;
702+ capturedContext = context ;
703+ yield candidates [ 0 ] ! ;
704+ } ;
705+
706+ const providers : Provider < Meta > [ ] = [
707+ {
708+ handler : makeHandler (
709+ 'Wallet:0' ,
710+ M . interface ( 'Wallet:0' , {
711+ getBalance : M . call ( M . string ( ) ) . returns ( M . number ( ) ) ,
712+ } ) ,
713+ { getBalance : ( _acct : string ) => 100 } ,
714+ ) ,
715+ metadata : constant ( { constructor : 'typeA' , cost : 100 } ) ,
716+ } ,
717+ {
718+ handler : makeHandler (
719+ 'Wallet:1' ,
720+ M . interface ( 'Wallet:1' , {
721+ getBalance : M . call ( M . string ( ) ) . returns ( M . number ( ) ) ,
722+ } ) ,
723+ { getBalance : ( _acct : string ) => 42 } ,
724+ ) ,
725+ metadata : constant ( { constructor : 'typeB' , cost : 1 } ) ,
726+ } ,
727+ ] ;
728+
729+ const wallet = sheafify ( { name : 'Wallet' , providers } ) . getGlobalSection ( {
730+ lift : spy ,
731+ } ) ;
732+ await E ( wallet ) . getBalance ( 'alice' ) ;
733+
734+ expect ( capturedContext ) . toStrictEqual ( {
735+ method : 'getBalance' ,
736+ args : [ 'alice' ] ,
737+ constraints : { } ,
738+ } ) ;
739+ expect (
740+ capturedCandidates . map ( ( candidate ) => candidate . metadata ) ,
741+ ) . toStrictEqual ( [
742+ { constructor : 'typeA' , cost : 100 } ,
743+ { constructor : 'typeB' , cost : 1 } ,
744+ ] ) ;
745+ } ) ;
746+
747+ it ( 'does not treat prototype-inherited value as shared when key is absent from some candidates' , async ( ) => {
748+ // Provider A has { constructor: Object, cost: 100 }. Provider B has { cost: 1 }.
749+ // Naive `key in meta` finds 'constructor' in B via Object.prototype, and
750+ // Object.is(meta_B['constructor'], Object) is true ({}.constructor === Object),
751+ // so the key is wrongly counted as shared and moved into constraints.
752+ type Meta = Record < string , unknown > ;
753+ let capturedContext : PolicyContext < Meta > | undefined ;
754+
755+ const spy : Policy < Meta > = async function * ( candidates , context ) {
756+ capturedContext = context ;
757+ yield candidates [ 0 ] ! ;
758+ } ;
759+
760+ const providers : Provider < Meta > [ ] = [
761+ {
762+ handler : makeHandler (
763+ 'Wallet:0' ,
764+ M . interface ( 'Wallet:0' , {
765+ getBalance : M . call ( M . string ( ) ) . returns ( M . number ( ) ) ,
766+ } ) ,
767+ { getBalance : ( _acct : string ) => 100 } ,
768+ ) ,
769+ metadata : constant ( { constructor : Object , cost : 100 } ) ,
770+ } ,
771+ {
772+ handler : makeHandler (
773+ 'Wallet:1' ,
774+ M . interface ( 'Wallet:1' , {
775+ getBalance : M . call ( M . string ( ) ) . returns ( M . number ( ) ) ,
776+ } ) ,
777+ { getBalance : ( _acct : string ) => 42 } ,
778+ ) ,
779+ metadata : constant ( { cost : 1 } ) ,
780+ } ,
781+ ] ;
782+
783+ const wallet = sheafify ( { name : 'Wallet' , providers } ) . getGlobalSection ( {
784+ lift : spy ,
785+ } ) ;
786+ await E ( wallet ) . getBalance ( 'alice' ) ;
787+
788+ // 'constructor' is only owned by provider A — must not appear in constraints
789+ expect ( capturedContext ?. constraints ) . not . toHaveProperty ( 'constructor' ) ;
790+ } ) ;
691791} ) ;
692792
693793// ---------------------------------------------------------------------------
0 commit comments