Skip to content

Commit 0f2416b

Browse files
committed
test(#15705): make each row-id candidate separately load-bearing
The token pin is satisfied by any of the three row-id candidates, so on its own it pinned the leg but not its parts: dropping params.recordId alone left all 21 green. Three fixtures now give each candidate a case only it can answer, by letting a record column shadow the other seed keys. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y
1 parent 6fdffb7 commit 0f2416b

1 file changed

Lines changed: 56 additions & 0 deletions

File tree

packages/services/service-automation/src/builtin/screen-headless-satisfaction.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)