@@ -1288,16 +1288,15 @@ describe('ApprovalService (node era)', () => {
12881288 expect ( rows [ 0 ] . record_title ) . toBe ( 'Snapshot Title' ) ;
12891289 } ) ;
12901290
1291- // [#23157] The approve action is `type: 'api'` against the decide route,
1292- // which answers decide()'s result verbatim; its success copy interpolates
1293- // `${result.*}` over that answer (the scope `ActionSchema.successMessage`
1294- // declares). So the toast names what was approved only while decide()'s
1295- // answer carries the title the copy reads, finalized or not.
1296- describe ( 'the approve action\'s success copy, filled from decide()\'s answer' , ( ) => {
1297- const approveCopy = ( ) : string => {
1298- const action = ( SysApprovalRequest as any ) . actions . find ( ( a : any ) => a . name === 'approval_approve' ) ;
1299- return action . successMessage ;
1300- } ;
1291+ // [#23157] Every request action is `type: 'api'` against a route that
1292+ // answers its service method's result verbatim, and each of those results
1293+ // carries `request`, the read-back row with its display enrichment. Each
1294+ // action's success copy interpolates `${result.*}` over that answer (the
1295+ // scope `ActionSchema.successMessage` declares), so the toast names the
1296+ // request only while the answer carries the title the copy reads.
1297+ describe ( 'each request action\'s success copy, filled from its route\'s answer' , ( ) => {
1298+ const copyOf = ( name : string ) : string =>
1299+ ( ( SysApprovalRequest as any ) . actions as any [ ] ) . find ( ( a ) => a . name === name ) . successMessage ;
13011300 /** The copy's `${result.<path>}` tokens, each filled from the answer; an unresolved path reads as ''. */
13021301 const fill = ( copy : string , result : unknown ) : { text : string ; unresolved : string [ ] } => {
13031302 const unresolved : string [ ] = [ ] ;
@@ -1308,43 +1307,110 @@ describe('ApprovalService (node era)', () => {
13081307 } ) ;
13091308 return { text, unresolved } ;
13101309 } ;
1310+ const named = ( action : string , answer : unknown ) : string => {
1311+ const { text, unresolved } = fill ( copyOf ( action ) , answer ) ;
1312+ expect ( unresolved , `${ action } : every token resolves on the answer` ) . toEqual ( [ ] ) ;
1313+ return text ;
1314+ } ;
13111315 /** The id of the request an ordinary open answers (never an auto-outcome in these cells). */
13121316 const openRequestId = async ( ...args : Parameters < ApprovalService [ 'openNodeRequest' ] > ) : Promise < string > =>
13131317 ( ( await svc . openNodeRequest ( ...args ) ) as { id : string } ) . id ;
1318+ /** A flow whose approval node declares the `revise` out-edge send-back needs (#3823). */
1319+ const REVISE_FLOW = {
1320+ name : 'deal_approval' ,
1321+ nodes : [
1322+ { id : 'approve_step' , type : 'approval' } ,
1323+ { id : 'wait_revision' , type : APPROVAL_REVISE_NODE_TYPE } ,
1324+ ] ,
1325+ edges : [ { id : 'e_rev' , source : 'approve_step' , target : 'wait_revision' , label : 'revise' } ] ,
1326+ } ;
13141327
1315- it ( 'reads the answer: the copy carries at least one `${result.*}` token' , ( ) => {
1316- expect ( approveCopy ( ) ) . toMatch ( / \$ \{ r e s u l t \. [ \w . ] + \} / ) ;
1328+ beforeEach ( ( ) => {
1329+ engine . _tables [ 'opportunity' ] = [ { id : 'opp1' , name : 'Acme Renewal' , amount : 100 } ] ;
1330+ svc . attachAutomation ( {
1331+ async resume ( ) { /* resumed */ } ,
1332+ async cancelRun ( ) { /* handed back */ } ,
1333+ async getFlow ( ) { return REVISE_FLOW ; } ,
1334+ } as any ) ;
13171335 } ) ;
13181336
1319- it ( 'a finalizing approve: the toast names the record, the way the inbox row\'s inline Approve does' , async ( ) => {
1320- engine . _tables [ 'opportunity' ] = [ { id : 'opp1' , name : 'Acme Renewal' , amount : 100 } ] ;
1337+ it ( 'reads the answer: every action\'s copy carries a `${result.*}` token' , ( ) => {
1338+ for ( const a of ( SysApprovalRequest as any ) . actions as any [ ] ) {
1339+ expect ( a . successMessage , `${ a . name } .successMessage` ) . toMatch ( / \$ \{ r e s u l t \. [ \w . ] + \} / ) ;
1340+ }
1341+ } ) ;
1342+
1343+ it ( 'approve, finalizing: the toast names the record, the way the inbox row\'s inline Approve does' , async ( ) => {
13211344 const id = await openRequestId ( openInput ( [ 'u9' ] ) , CTX ) ;
13221345 const out = await svc . decide ( id , { decision : 'approve' , actorId : 'u9' } , SYS ) ;
13231346 expect ( out . finalized ) . toBe ( true ) ;
1324- const { text, unresolved } = fill ( approveCopy ( ) , out ) ;
1325- expect ( unresolved ) . toEqual ( [ ] ) ;
1326- expect ( text ) . toBe ( 'Approved "Acme Renewal"' ) ;
1347+ expect ( named ( 'approval_approve' , out ) ) . toBe ( 'Approved "Acme Renewal"' ) ;
13271348 } ) ;
13281349
1329- it ( 'an approve that leaves the step waiting on others still names the record' , async ( ) => {
1330- engine . _tables [ 'opportunity' ] = [ { id : 'opp1' , name : 'Acme Renewal' , amount : 100 } ] ;
1350+ it ( 'approve, leaving the step waiting on others: still names the record' , async ( ) => {
13311351 const id = await openRequestId ( openInput ( [ 'u1' , 'u2' ] , { } , { behavior : 'unanimous' } ) , CTX ) ;
13321352 const out = await svc . decide ( id , { decision : 'approve' , actorId : 'u1' } , SYS ) ;
13331353 expect ( out . finalized ) . toBe ( false ) ;
1334- const { text, unresolved } = fill ( approveCopy ( ) , out ) ;
1335- expect ( unresolved ) . toEqual ( [ ] ) ;
1336- expect ( text ) . toContain ( 'Acme Renewal' ) ;
1337- expect ( text ) . not . toBe ( 'Approved.' ) ;
1354+ expect ( named ( 'approval_approve' , out ) ) . toBe ( 'Approved "Acme Renewal"' ) ;
13381355 } ) ;
13391356
1340- it ( 'a record whose row is gone is named from the payload snapshot, as the inbox names it' , async ( ) => {
1341- const id = await openRequestId (
1342- openInput ( [ 'u9' ] , { record : { id : 'opp1' , name : 'Snapshot Title' , amount : 1 } } ) , CTX ,
1343- ) ;
1357+ it ( 'approve, on a record whose row is gone: named from the payload snapshot, as the inbox names it' , async ( ) => {
1358+ engine . _tables [ 'opportunity' ] = [ ] ;
1359+ const id = await openRequestId ( openInput ( [ 'u9' ] , { record : { id : 'opp1' , name : 'Snapshot Title' , amount : 1 } } ) , CTX ) ;
13441360 const out = await svc . decide ( id , { decision : 'approve' , actorId : 'u9' } , SYS ) ;
1345- const { text, unresolved } = fill ( approveCopy ( ) , out ) ;
1346- expect ( unresolved ) . toEqual ( [ ] ) ;
1347- expect ( text ) . toBe ( 'Approved "Snapshot Title"' ) ;
1361+ expect ( named ( 'approval_approve' , out ) ) . toBe ( 'Approved "Snapshot Title"' ) ;
1362+ } ) ;
1363+
1364+ it ( 'reject: names the record, the way the inbox row\'s inline Reject does' , async ( ) => {
1365+ const id = await openRequestId ( openInput ( [ 'u9' ] ) , CTX ) ;
1366+ const out = await svc . decide ( id , { decision : 'reject' , actorId : 'u9' } , SYS ) ;
1367+ expect ( out . request . status ) . toBe ( 'rejected' ) ;
1368+ expect ( named ( 'approval_reject' , out ) ) . toBe ( 'Rejected "Acme Renewal"' ) ;
1369+ } ) ;
1370+
1371+ it ( 'reassign: names the record' , async ( ) => {
1372+ const id = await openRequestId ( openInput ( [ 'u9' ] ) , CTX ) ;
1373+ const out = await svc . reassign ( id , { actorId : 'u9' , to : 'u7' } , asUser ( 'u9' ) ) ;
1374+ expect ( named ( 'approval_reassign' , out ) ) . toBe ( 'Reassigned "Acme Renewal"' ) ;
1375+ } ) ;
1376+
1377+ it ( 'send back for revision: names the record' , async ( ) => {
1378+ const id = await openRequestId ( openInput ( [ 'u9' ] ) , CTX ) ;
1379+ const out = await svc . sendBack ( id , { actorId : 'u9' , comment : 'split the lines' } , asUser ( 'u9' ) ) ;
1380+ expect ( out . request . status ) . toBe ( 'returned' ) ;
1381+ expect ( named ( 'approval_send_back' , out ) ) . toBe ( 'Sent "Acme Renewal" back for revision' ) ;
1382+ } ) ;
1383+
1384+ it ( 'request information: names the record' , async ( ) => {
1385+ const id = await openRequestId ( openInput ( [ 'u9' ] ) , CTX ) ;
1386+ const out = await svc . requestInfo ( id , { actorId : 'u9' , comment : 'which cost centre?' } , asUser ( 'u9' ) ) ;
1387+ expect ( named ( 'approval_request_info' , out ) ) . toBe ( 'Information requested on "Acme Renewal"' ) ;
1388+ } ) ;
1389+
1390+ it ( 'remind: names the record' , async ( ) => {
1391+ const id = await openRequestId ( openInput ( [ 'u9' ] ) , CTX ) ;
1392+ const out = await svc . remind ( id , { actorId : 'u1' } , CTX ) ;
1393+ expect ( named ( 'approval_remind' , out ) ) . toBe ( 'Reminder sent for "Acme Renewal"' ) ;
1394+ } ) ;
1395+
1396+ it ( 'recall: names the record' , async ( ) => {
1397+ const id = await openRequestId ( openInput ( [ 'u9' ] ) , CTX ) ;
1398+ const out = await svc . recall ( id , { actorId : 'u1' } , CTX ) ;
1399+ expect ( out . request . status ) . toBe ( 'recalled' ) ;
1400+ expect ( named ( 'approval_recall' , out ) ) . toBe ( 'Recalled "Acme Renewal"' ) ;
1401+ } ) ;
1402+
1403+ it ( 'resubmit: names the record' , async ( ) => {
1404+ const id = await openRequestId ( openInput ( [ 'u9' ] ) , CTX ) ;
1405+ await svc . sendBack ( id , { actorId : 'u9' , comment : 'split the lines' } , asUser ( 'u9' ) ) ;
1406+ const out = await svc . resubmit ( id , { actorId : 'u1' , comment : 'split' } , CTX ) ;
1407+ expect ( named ( 'approval_resubmit' , out ) ) . toBe ( 'Resubmitted "Acme Renewal"' ) ;
1408+ } ) ;
1409+
1410+ it ( 'reply: names the record' , async ( ) => {
1411+ const id = await openRequestId ( openInput ( [ 'u9' ] ) , CTX ) ;
1412+ const out = await svc . comment ( id , { actorId : 'u1' , comment : 'attached the receipt' } , CTX ) ;
1413+ expect ( named ( 'approval_comment' , out ) ) . toBe ( 'Reply posted on "Acme Renewal"' ) ;
13481414 } ) ;
13491415 } ) ;
13501416
0 commit comments