@@ -1547,21 +1547,95 @@ describe('unknown keys are rejected, not stripped (#4001)', () => {
15471547 expect ( issue ! . message ) . toContain ( '`notAKey`' ) ;
15481548 } ) ;
15491549
1550- it ( 'points builder vocabulary (steps/connections/trigger ) at the canonical keys' , ( ) => {
1550+ it ( 'points builder vocabulary (steps/connections) at the canonical keys' , ( ) => {
15511551 expect ( unknownKeyIssue ( FlowSchema , { ...minimalFlow , steps : [ ] } ) ! . message )
15521552 . toContain ( '`steps` → `nodes`' ) ;
15531553 expect ( unknownKeyIssue ( FlowSchema , { ...minimalFlow , connections : [ ] } ) ! . message )
15541554 . toContain ( '`connections` → `edges`' ) ;
1555- expect ( unknownKeyIssue ( FlowSchema , { ...minimalFlow , trigger : 'record_change' } ) ! . message )
1556- . toContain ( '`trigger` → `type`' ) ;
15571555 } ) ;
15581556
1559- it ( 'points a top-level object binding at the START node config' , ( ) => {
1560- for ( const key of [ 'object' , 'objectName' ] ) {
1561- const message = unknownKeyIssue ( FlowSchema , { ...minimalFlow , [ key ] : 'task' } ) ! . message ;
1557+ // `trigger` and `triggerType` were ALIASES pointing at `type` — a rename no
1558+ // author can take: `type` is the flow KIND enum, so following it lands on
1559+ // `Invalid option: expected one of "autolaunched"|…` one round later with the
1560+ // binding still nowhere. They are `guidance` entries now, so the rejection
1561+ // says where the binding really lives instead of prescribing a name. Both
1562+ // directions are pinned: the prescription is present, AND the rename is gone.
1563+ it ( 'sends a top-level `trigger` to the START node config, never to a `type` rename' , ( ) => {
1564+ const message = unknownKeyIssue ( FlowSchema , {
1565+ ...minimalFlow ,
1566+ trigger : { type : 'record_change' , object : 'task' , events : [ 'create' ] } ,
1567+ } ) ! . message ;
1568+ expect ( message ) . toContain ( 'START node' ) ;
1569+ expect ( message ) . toContain ( '`{ objectName, triggerType, condition }`' ) ;
1570+ expect ( message , 'the prescription names a real `record-*` token' ) . toContain ( 'record-after-create' ) ;
1571+ expect ( message , 'the rename an author cannot take is gone' ) . not . toContain ( '`trigger` → `type`' ) ;
1572+ } ) ;
1573+
1574+ it ( 'sends a top-level `triggerType` to the START node config, never to a `type` rename' , ( ) => {
1575+ const message = unknownKeyIssue ( FlowSchema , {
1576+ ...minimalFlow ,
1577+ triggerType : 'record-after-create' ,
1578+ } ) ! . message ;
1579+ expect ( message ) . toContain ( 'START node' ) ;
1580+ expect ( message ) . toContain ( '`{ objectName, triggerType, condition }`' ) ;
1581+ expect ( message , 'the rename an author cannot take is gone' ) . not . toContain ( '`triggerType` → `type`' ) ;
1582+ } ) ;
1583+
1584+ // The alias table is probed case- and separator-insensitively, so removing
1585+ // the `triggertype` row takes every spelling of it with the canonical one.
1586+ // `guidance` is exact-spelling by design (case folding is the rename
1587+ // channel's job — `shared/suggestions.zod.ts`), so a non-canonical spelling
1588+ // now gets the bare rejection: no prescription, and — the point — no
1589+ // confidently wrong one either.
1590+ it ( 'no spelling of the removed alias renames to `type` any more' , ( ) => {
1591+ const message = unknownKeyIssue ( FlowSchema , {
1592+ ...minimalFlow ,
1593+ triggertype : 'record-after-create' ,
1594+ } ) ! . message ;
1595+ expect ( message ) . toContain ( '`triggertype`' ) ;
1596+ expect ( message ) . not . toContain ( '→ `type`' ) ;
1597+ } ) ;
1598+
1599+ // Why both renames were dead ends, pinned so the guidance above cannot
1600+ // quietly turn into correct advice: `type` names the flow KIND and accepts
1601+ // no lifecycle-event token at all.
1602+ it ( '`type` accepts no `record-*` event token — the reason neither key renames to it' , ( ) => {
1603+ const result = FlowSchema . safeParse ( { ...minimalFlow , type : 'record-after-create' } ) ;
1604+ expect ( result . success ) . toBe ( false ) ;
1605+ const issue = result . error ! . issues . find ( ( i : { code : string } ) => i . code === 'invalid_value' ) ;
1606+ expect ( issue ! . message ) . toContain ( 'expected one of' ) ;
1607+ expect ( issue ! . message ) . not . toContain ( 'record-' ) ;
1608+ } ) ;
1609+
1610+ it ( 'points a top-level object/schedule binding at the START node config' , ( ) => {
1611+ const cases : Array < [ string , unknown ] > = [
1612+ [ 'object' , 'task' ] ,
1613+ [ 'objectName' , 'task' ] ,
1614+ [ 'schedule' , '0 8 * * *' ] ,
1615+ ] ;
1616+ for ( const [ key , value ] of cases ) {
1617+ const message = unknownKeyIssue ( FlowSchema , { ...minimalFlow , [ key ] : value } ) ! . message ;
15621618 expect ( message , `\`${ key } \` should point at the start node` ) . toContain ( 'START node' ) ;
15631619 }
15641620 } ) ;
1621+
1622+ // Positive control for the shape every one of those prescriptions points at:
1623+ // the trigger really does bind on the START node's `config`, and a flow that
1624+ // writes it there parses.
1625+ it ( 'accepts the trigger bound on the START node config — the shape the guidance prescribes' , ( ) => {
1626+ const result = FlowSchema . safeParse ( {
1627+ ...minimalFlow ,
1628+ type : 'record_change' ,
1629+ nodes : [
1630+ {
1631+ id : 'start' , type : 'start' , label : 'Start' ,
1632+ config : { objectName : 'task' , triggerType : 'record-after-create' } ,
1633+ } ,
1634+ { id : 'end' , type : 'end' , label : 'End' } ,
1635+ ] ,
1636+ } ) ;
1637+ expect ( result . success ) . toBe ( true ) ;
1638+ } ) ;
15651639 } ) ;
15661640
15671641 describe ( 'FlowNodeSchema' , ( ) => {
0 commit comments