@@ -327,6 +327,62 @@ describe('screen headless satisfaction (#15705)', () => {
327327 expect ( res . screen ?. nodeId ) . toBe ( 'screen_1' ) ;
328328 } ) ;
329329
330+ /**
331+ * The row-id VALUE leg reads three candidates, and the `token` pin above is
332+ * satisfied by any of them — so on its own it pins the leg but not its
333+ * parts, and a future edit could delete one candidate and stay green
334+ * (measured: dropping `params.recordId` alone left all 21 green).
335+ *
336+ * These three fixtures give each candidate a case only it can answer, by
337+ * letting a record COLUMN shadow the other seed keys — which is the only
338+ * way a seeded key stops carrying the row id, since `seedFlowActionParams`
339+ * writes a key only `if (seeded[key] === undefined)` and the record spread
340+ * came first.
341+ */
342+ it ( 'only `params.recordId` can refuse this one — the record shadows the alias key' , async ( ) => {
343+ const flow : any = followupFlow ( ) ;
344+ flow . nodes [ 1 ] . config . fields = [ { name : 'sessionToken' , label : 'Session' , type : 'text' , required : true } ] ;
345+ flow . variables = [ { name : 'sessionToken' , type : 'text' , isInput : true , isOutput : true } ] ;
346+ register ( { } , flow ) ;
347+ // recordIdField 'token' -> row id 'tok_9'; the record's own `crmLeadId`
348+ // column survives the alias seed, so that candidate reads 'other_1'.
349+ const record = { id : 'sess_1' , token : 'tok_9' , crmLeadId : 'other_1' } ;
350+ const res = await engine . execute ( 'schedule_followup' , {
351+ record, object : 'crm_lead' ,
352+ params : { ...record , recordId : 'tok_9' , sessionToken : 'tok_9' } ,
353+ } as AutomationContext ) ;
354+ expect ( res . status ) . toBe ( 'paused' ) ;
355+ } ) ;
356+
357+ it ( 'only the alias VALUE can refuse this one — the record shadows `recordId`' , async ( ) => {
358+ const flow : any = followupFlow ( ) ;
359+ flow . nodes [ 1 ] . config . fields = [ { name : 'sessionToken' , label : 'Session' , type : 'text' , required : true } ] ;
360+ flow . variables = [ { name : 'sessionToken' , type : 'text' , isInput : true , isOutput : true } ] ;
361+ register ( { } , flow ) ;
362+ const record = { id : 'sess_1' , token : 'tok_9' , recordId : 'shadow_1' } ;
363+ const res = await engine . execute ( 'schedule_followup' , {
364+ record, object : 'crm_lead' ,
365+ params : { ...record , crmLeadId : 'tok_9' , sessionToken : 'tok_9' } ,
366+ } as AutomationContext ) ;
367+ expect ( res . status ) . toBe ( 'paused' ) ;
368+ } ) ;
369+
370+ it ( 'only `record.id` can refuse this one — object-less action, and the record shadows `recordId`' , async ( ) => {
371+ const flow : any = followupFlow ( ) ;
372+ flow . nodes [ 1 ] . config . fields = [ { name : 'subjectRef' , label : 'Subject' , type : 'text' , required : true } ] ;
373+ flow . variables = [ { name : 'subjectRef' , type : 'text' , isInput : true , isOutput : true } ] ;
374+ register ( { } , flow ) ;
375+ // No `object` on the context (an object-less action), so no alias key
376+ // is derivable at all; the record's own `recordId` column shadows the
377+ // other seed. The default `recordIdField` leaves the row id at `id`.
378+ const record = { id : 'lead_1' , recordId : 'shadow_1' } ;
379+ const res = await engine . execute ( 'schedule_followup' , {
380+ record,
381+ params : { ...record , subjectRef : 'lead_1' } ,
382+ } as AutomationContext ) ;
383+ expect ( res . status ) . toBe ( 'paused' ) ;
384+ } ) ;
385+
330386 it ( 'trigger door: a genuine caller param still satisfies the screen' , async ( ) => {
331387 register ( ) ;
332388 const res = await engine . execute ( 'schedule_followup' , triggerDoorContext ( 'lead_1' , {
0 commit comments