|
| 1 | +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. |
| 2 | + |
| 3 | +/** |
| 4 | + * [#24067] No schedule flow this example ships stores `objectName` on its |
| 5 | + * start node. |
| 6 | + * |
| 7 | + * A schedule binding carries no object. `AutomationEngine.deriveTriggerBinding` |
| 8 | + * lifts the start node's `config.objectName` into the record-change binding |
| 9 | + * (and, as a fallback behind `timeRelative.object`, into the time-relative |
| 10 | + * one); its `schedule` branch hands the trigger `schedule` and `organization` |
| 11 | + * only, and `ScheduleTrigger` never reads `objectName`. On a schedule start |
| 12 | + * node the key is stored and never acted on, and the console's flow designer |
| 13 | + * labels it "kept, not in effect". An example is what an AI copies, so the |
| 14 | + * example must not model it: a sweep names its object where it reads, on its |
| 15 | + * `get_record` step, as both daily sweeps here do. |
| 16 | + * |
| 17 | + * The flows are read off the stack the example ships (its default |
| 18 | + * `defineStack` export), never a hand list, so a schedule flow added later is |
| 19 | + * covered on the day it lands. "Schedule" is the spec's |
| 20 | + * `resolveFlowTriggerKind` answer, the one predicate the engine binds by: a |
| 21 | + * time-relative sweep, whose binding DOES fall back to `objectName`, is |
| 22 | + * correctly left out, and every other `type: 'schedule'` flow is in. |
| 23 | + */ |
| 24 | + |
| 25 | +import { describe, it, expect } from 'vitest'; |
| 26 | +import { resolveFlowTriggerKind } from '@objectstack/spec/automation'; |
| 27 | + |
| 28 | +import TodoApp from '../objectstack.config.js'; |
| 29 | + |
| 30 | +type NodeLike = { id?: string; type?: string; config?: Record<string, unknown> }; |
| 31 | +type FlowLike = { name?: string; type?: string; nodes?: NodeLike[] }; |
| 32 | + |
| 33 | +const shippedFlows = ((TodoApp as unknown as { flows?: FlowLike[] }).flows ?? []); |
| 34 | +const scheduleFlows = shippedFlows.filter((flow) => resolveFlowTriggerKind(flow) === 'schedule'); |
| 35 | + |
| 36 | +function startConfig(flow: FlowLike): Record<string, unknown> { |
| 37 | + const start = (flow.nodes ?? []).find((node) => node.type === 'start'); |
| 38 | + if (!start) throw new Error(`harness: flow '${flow.name}' has no start node`); |
| 39 | + return start.config ?? {}; |
| 40 | +} |
| 41 | + |
| 42 | +describe('#24067: a schedule start node carries no objectName', () => { |
| 43 | + it('reads real schedule flows off the shipped stack (anti-vacuity)', () => { |
| 44 | + expect(scheduleFlows.length).toBeGreaterThan(0); |
| 45 | + // Each start node read here is the one the schedule binding reads: it |
| 46 | + // carries the cadence that binding DOES act on. |
| 47 | + for (const flow of scheduleFlows) { |
| 48 | + expect(startConfig(flow), `flow '${flow.name}' start node`).toHaveProperty('schedule'); |
| 49 | + } |
| 50 | + }); |
| 51 | + |
| 52 | + it('no schedule flow stores objectName on its start node', () => { |
| 53 | + const offenders = scheduleFlows |
| 54 | + .filter((flow) => Object.hasOwn(startConfig(flow), 'objectName')) |
| 55 | + .map((flow) => `flow '${flow.name}' · start node config.objectName`); |
| 56 | + expect(offenders).toEqual([]); |
| 57 | + }); |
| 58 | +}); |
0 commit comments