@@ -2,6 +2,9 @@ import { describe, it, expect } from 'vitest';
22import {
33 ApproverType ,
44 DEPRECATED_APPROVER_TYPES ,
5+ NON_AUTHORABLE_APPROVER_TYPES ,
6+ APPROVER_VALUE_BINDINGS ,
7+ ORG_MEMBERSHIP_LEVELS ,
58 canonicalApproverType ,
69 APPROVAL_NODE_TYPE ,
710 ApprovalDecision ,
@@ -45,15 +48,71 @@ describe('ApproverType', () => {
4548 // Cross-repo contract: the published node configSchema must carry
4649 // `xEnumDeprecated` on the approver type, or the Studio designer (objectui)
4750 // derives its dropdown straight from `enum` and keeps offering `role` — the
48- // exact trap ADR-0090 D3 retires. Renderers read this to omit deprecated
49- // members from pickers while still rendering a stored value.
50- it ( 'publishes xEnumDeprecated on the approver type so pickers can drop `role`' , ( ) => {
51+ // exact trap ADR-0090 D3 retires — and `queue`, which the runtime never
52+ // resolves (#3508). Renderers read this to omit these members from pickers
53+ // while still rendering a stored value.
54+ it ( 'publishes xEnumDeprecated on the approver type so pickers drop `role` and `queue`' , ( ) => {
5155 const schema = getApprovalNodeConfigJsonSchema ( ) as any ;
5256 const typeNode = schema ?. properties ?. approvers ?. items ?. properties ?. type ;
5357 expect ( typeNode ?. enum ) . toContain ( 'role' ) ; // still parses (back-compat)
58+ expect ( typeNode ?. enum ) . toContain ( 'queue' ) ; // still parses (stored rows render)
5459 expect ( typeNode ?. enum ) . toContain ( 'org_membership_level' ) ;
55- expect ( typeNode ?. xEnumDeprecated ) . toEqual ( Object . keys ( DEPRECATED_APPROVER_TYPES ) ) ;
60+ expect ( typeNode ?. xEnumDeprecated ) . toEqual ( [ ... NON_AUTHORABLE_APPROVER_TYPES ] ) ;
5661 expect ( typeNode ?. xEnumDeprecated ) . toContain ( 'role' ) ;
62+ expect ( typeNode ?. xEnumDeprecated ) . toContain ( 'queue' ) ;
63+ } ) ;
64+
65+ // #3508: queue is declared-but-unenforced. It stays parseable (stored flows
66+ // keep loading) but is not authorable, and every deprecated spelling is
67+ // non-authorable too.
68+ it ( 'marks queue non-authorable while keeping it parseable' , ( ) => {
69+ expect ( ( ) => ApproverType . parse ( 'queue' ) ) . not . toThrow ( ) ;
70+ expect ( NON_AUTHORABLE_APPROVER_TYPES ) . toContain ( 'queue' ) ;
71+ for ( const spelling of Object . keys ( DEPRECATED_APPROVER_TYPES ) ) {
72+ expect ( NON_AUTHORABLE_APPROVER_TYPES ) . toContain ( spelling ) ;
73+ }
74+ } ) ;
75+ } ) ;
76+
77+ // #3508: the designer contract for sourcing each approver row's `value`. The
78+ // record-backed kinds MUST match the engine's resolution semantics
79+ // (`plugin-approvals` resolveApproverSpec / expand*Users) — these assertions
80+ // pin the object names and stored fields the engine actually queries.
81+ describe ( 'APPROVER_VALUE_BINDINGS (#3508)' , ( ) => {
82+ it ( 'covers every ApproverType member' , ( ) => {
83+ for ( const t of ApproverType . options ) {
84+ expect ( APPROVER_VALUE_BINDINGS [ t ] ) . toBeDefined ( ) ;
85+ }
86+ } ) ;
87+
88+ it ( 'binds record-backed kinds to the objects and fields the engine queries' , ( ) => {
89+ // applyOooDelegation(String(value)) — a sys_user id.
90+ expect ( APPROVER_VALUE_BINDINGS . user ) . toEqual ( { source : 'record' , object : 'sys_user' , valueField : 'id' } ) ;
91+ // find('sys_team_member', { team_id: value }) — a sys_team id.
92+ expect ( APPROVER_VALUE_BINDINGS . team ) . toEqual ( { source : 'record' , object : 'sys_team' , valueField : 'id' } ) ;
93+ // find('sys_business_unit', { id: value }) — a sys_business_unit id
94+ // (deliberately NOT a `sys_department`).
95+ expect ( APPROVER_VALUE_BINDINGS . department ) . toEqual ( { source : 'record' , object : 'sys_business_unit' , valueField : 'id' } ) ;
96+ // find('sys_user_position', { position: value }) — the position machine
97+ // NAME, not an id (portable across environments).
98+ expect ( APPROVER_VALUE_BINDINGS . position ) . toEqual ( { source : 'record' , object : 'sys_position' , valueField : 'name' } ) ;
99+ } ) ;
100+
101+ it ( 'keeps the non-record kinds off the record-lookup path' , ( ) => {
102+ expect ( APPROVER_VALUE_BINDINGS . org_membership_level ) . toEqual ( { source : 'enum' , values : ORG_MEMBERSHIP_LEVELS } ) ;
103+ // The deprecated alias renders with the same control as its replacement.
104+ expect ( APPROVER_VALUE_BINDINGS . role ) . toEqual ( APPROVER_VALUE_BINDINGS . org_membership_level ) ;
105+ expect ( APPROVER_VALUE_BINDINGS . manager . source ) . toBe ( 'auto' ) ;
106+ expect ( APPROVER_VALUE_BINDINGS . field . source ) . toBe ( 'trigger-field' ) ;
107+ expect ( APPROVER_VALUE_BINDINGS . queue . source ) . toBe ( 'unsupported' ) ;
108+ } ) ;
109+
110+ it ( 'publishes a manager mapping in the value xRef so designers can render auto-resolution' , ( ) => {
111+ const schema = getApprovalNodeConfigJsonSchema ( ) as any ;
112+ const valueNode = schema ?. properties ?. approvers ?. items ?. properties ?. value ;
113+ expect ( valueNode ?. xRef ?. map ?. manager ) . toBe ( 'manager' ) ;
114+ // queue stays mapped so stored rows keep rendering during the window.
115+ expect ( valueNode ?. xRef ?. map ?. queue ) . toBe ( 'queue' ) ;
57116 } ) ;
58117} ) ;
59118
0 commit comments