Skip to content

Commit 49c47a4

Browse files
committed
test(lint): pin the objectName-less record trigger entrance and the map child of an open parent; reword S as by design
The record-trigger pin is red until flowCelEntrances gains the open reason in the next commit; the map-child pin holds behaviour already on main. Claude-Session: https://claude.ai/code/session_01S3aAf11JjbW1mSGL1EhfFj Co-authored-by: Claude <noreply@anthropic.com>
1 parent e7e6544 commit 49c47a4

1 file changed

Lines changed: 44 additions & 3 deletions

File tree

‎packages/lint/src/validate-expressions.flow-cel-root.test.ts‎

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,22 @@ describe('the entrances the stack declares (A′)', () => {
294294
expect(userRefusals(flowStack({ edgeCondition: USER, objectName: 'acct' }))).toHaveLength(1);
295295
});
296296

297+
it('a record trigger that names no start config.objectName stands down; the same trigger on a declared object is judged', () => {
298+
// With no `objectName` the record-change trigger registers its hook with no
299+
// object filter (`record-change-trigger.ts`), so it fires on a write to ANY
300+
// object — one this stack does not declare included — and hands that row.
301+
expect(userRefusals(flowStack({ edgeCondition: USER, triggerType: 'record-after-update' }))).toEqual([]);
302+
expect(userRefusals(flowStack({ edgeCondition: USER, triggerType: 'record-after-update', objectName: 'acct' }))).toHaveLength(1);
303+
// And with no `record-*` trigger there is no such entrance: the flow is judged.
304+
expect(userRefusals(flowStack({ edgeCondition: USER }))).toHaveLength(1);
305+
const entrance = flowCelEntrances(
306+
{ flows: [{ name: 'f', type: 'autolaunched', nodes: [{ id: 'start', type: 'start', config: { triggerType: 'record-after-create' } }] }] },
307+
new Map([['acct', ['status']]]),
308+
).get('f');
309+
expect(entrance?.openedBy).toContain("'record-after-create'");
310+
expect(entrance?.bound).toEqual(['record']);
311+
});
312+
297313
it('a time-relative sweep: an undeclared config.timeRelative.object stands down; a declared one is judged', () => {
298314
const sweep = (object: string): AnyRec => {
299315
const stack = flowStack({ edgeCondition: USER });
@@ -361,6 +377,25 @@ describe('the entrances the stack declares (A′)', () => {
361377
expect(userRefusals(withFlows(flowStack({ edgeCondition: USER }), judgedParent))).toHaveLength(1);
362378
});
363379

380+
it('a parent that is itself open: its `map` child stands down, to a fixpoint; a judged parent leaves the child judged', () => {
381+
// `map-node.ts` sets the child's `record` only for an id-bearing item; any
382+
// other item leaves the child holding the parent's record. The declared
383+
// `itemObject` keeps the node from opening `f` itself, so only the parent's
384+
// openness can.
385+
const each = (child: string): AnyRec => ({ id: 'each', type: 'map', config: { collection: '{rows}', flowName: child, itemObject: 'acct' } });
386+
const openParent = parentFlow({ type: 'api', node: each('f') });
387+
expect(userRefusals(withFlows(flowStack({ edgeCondition: USER }), openParent))).toEqual([]);
388+
expect(flowCelEntrances(
389+
{ flows: [{ name: 'f', type: 'autolaunched', nodes: [{ id: 'start', type: 'start' }] }, openParent] },
390+
new Map([['acct', ['status']]]),
391+
).get('f')?.openedBy).toMatch(/^map node 'each' in flow 'g' .*'g' is itself open$/);
392+
// Two levels: g (the inbound hook) → map → h → map → f.
393+
const middle = { ...parentFlow({ node: each('f') }), name: 'h', label: 'H' };
394+
expect(userRefusals(withFlows(flowStack({ edgeCondition: USER }), parentFlow({ type: 'api', node: each('h') }), middle))).toEqual([]);
395+
// RED CONTROL — the same `map` node in a parent no entrance opens leaves the child judged.
396+
expect(userRefusals(withFlows(flowStack({ edgeCondition: USER }), parentFlow({ node: each('f') })))).toHaveLength(1);
397+
});
398+
364399
it('the reader names the entrance', () => {
365400
const entrances = flowCelEntrances(
366401
{
@@ -377,9 +412,14 @@ describe('the entrances the stack declares (A′)', () => {
377412
* S (triage `6094850599`): at the runtime publish gate the per-write snapshot
378413
* holds the written flow alone, so no entrance is visible and this one judgment
379414
* stands down there. Every other expression verdict at that door is unchanged.
380-
* #22636 widens the snapshot; when it lands, this stand-down goes.
415+
* By design, not a hold-out: who launches a flow and what record it is handed
416+
* are read from other items of the stack, a whole-stack judgment that belongs to
417+
* `objectstack validate` (ADR-0109 Decision §3's boundary), and the snapshot
418+
* stays as it is (ruling `5791822697` on #19527, item 3; ruling `6104584601` on
419+
* #22636, letter B). These pins hold the asymmetry: the gate stands down, the
420+
* build door refuses.
381421
*/
382-
describe('the runtime publish gate (S)', () => {
422+
describe('the runtime publish gate stands down by design (S)', () => {
383423
const written = {
384424
name: 'f',
385425
label: 'F',
@@ -415,7 +455,8 @@ describe('the runtime publish gate (S)', () => {
415455
* binds `context.record` and `celScope` binds no `record` of its own. So the
416456
* build door binds it only where an entrance the stack declares hands the flow a
417457
* record, or the flow binds a variable named `record`; otherwise `record.X` is
418-
* refused. The runtime publish gate's stand-down (S) covers it until #22636.
458+
* refused. The runtime publish gate's stand-down (S) covers it, by design (the S
459+
* block above).
419460
*/
420461
describe('`record` — bound only where an entrance hands the flow a record (#22677)', () => {
421462
const READ = 'record.status == "open"';

0 commit comments

Comments
 (0)