@@ -337,3 +337,90 @@ describe("a page action row's `onSuccess` stays open beside `api`, `script` and
337337 expect ( issues . map ( ( i ) => [ i . code , i . path ] ) ) . toEqual ( [ [ 'custom' , [ ...row . at , 'onSuccess' ] ] ] ) ;
338338 } ) ;
339339} ) ;
340+
341+ // ===========================================================================
342+ // 4. operation — ActionSchema's own enum, and its `onSuccess` rule
343+ // ===========================================================================
344+
345+ /** A declarative update row: `operation: 'update'` with a static `patch`. */
346+ const UPDATE = { operation : 'update' , patch : { status : 'done' } } as const ;
347+
348+ /** `ActionSchema`'s refusal of `onSuccess` beside `operation: 'update'`, and the subject it opens with. */
349+ const ACTION_UPDATE_SUBJECT = "An `operation: 'update'` action's `onSuccess`" ;
350+ const actionUpdateOnSuccessMessage = ( ) : string => {
351+ const issues = issuesOf ( ActionSchema . safeParse ( {
352+ name : 'close_task' , label : 'Close' , ...UPDATE , onSuccess : HOP ,
353+ } ) ) ;
354+ expect ( issues . map ( ( i ) => [ i . code , i . path ] ) ) . toEqual ( [ [ 'custom' , [ 'onSuccess' ] ] ] ) ;
355+ expect ( issues [ 0 ] . message . startsWith ( ACTION_UPDATE_SUBJECT ) ) . toBe ( true ) ;
356+ return issues [ 0 ] . message ;
357+ } ;
358+
359+ describe ( "a page action row's `operation` is `ActionSchema`'s own enum" , ( ) => {
360+ it ( 'NO SECOND VOCABULARY: every row holds the very enum instance `ActionSchema.operation` declares' , ( ) => {
361+ const operation = ActionSchema . in . shape . operation . unwrap ( ) ;
362+ type Shape = { shape : Record < string , z . ZodOptional < z . ZodType > > } ;
363+ const memberOf = ( props : unknown ) =>
364+ ( ( props as Shape ) . shape . actions . unwrap ( ) as unknown as z . ZodArray < z . ZodType > ) . element as unknown as Shape ;
365+ expect ( ( ActionButtonPropsSchema as unknown as Shape ) . shape . operation . unwrap ( ) ) . toBe ( operation ) ;
366+ expect ( ( ActionIconPropsSchema as unknown as Shape ) . shape . operation . unwrap ( ) ) . toBe ( operation ) ;
367+ expect ( memberOf ( GROUP ) . shape . operation . unwrap ( ) ) . toBe ( operation ) ;
368+ expect ( memberOf ( MENU ) . shape . operation . unwrap ( ) ) . toBe ( operation ) ;
369+ } ) ;
370+
371+ it . each ( ROWS ) ( "REFUSAL: $name with `operation: 'delete'` is refused at `operation` with the action's own text" , ( row ) => {
372+ const issues = issuesOf ( row . parse ( { operation : 'delete' , patch : { status : 'done' } } ) ) ;
373+ expect ( issues . map ( ( i ) => i . path ) ) . toEqual ( [ [ ...row . at , 'operation' ] ] ) ;
374+ const action = issuesOf ( ActionSchema . safeParse ( { name : 'x' , label : 'X' , operation : 'delete' , patch : { status : 'done' } } ) )
375+ . find ( ( i ) => i . path . join ( '.' ) === 'operation' ) ;
376+ expect ( issues [ 0 ] . message ) . toBe ( action ?. message ) ;
377+ expect ( issues [ 0 ] . message . startsWith ( "`operation: 'delete'` is a bulk-def operation with no row-level form" ) ) . toBe ( true ) ;
378+ } ) ;
379+
380+ it . each ( ROWS ) ( "REFUSAL: $name with `operation: 'create'` (no member) is refused at `operation`" , ( row ) => {
381+ const issues = issuesOf ( row . parse ( { operation : 'create' , patch : { status : 'done' } } ) ) ;
382+ expect ( issues . map ( ( i ) => [ i . code , i . path ] ) ) . toEqual ( [ [ 'invalid_value' , [ ...row . at , 'operation' ] ] ] ) ;
383+ } ) ;
384+
385+ it . each ( ROWS ) ( "ACCEPTED CONTROL: $name with `operation: 'update'` and a `patch` parses unchanged" , ( row ) => {
386+ const r = row . parse ( { ...UPDATE } ) ;
387+ expect ( r . success , JSON . stringify ( issuesOf ( r ) ) ) . toBe ( true ) ;
388+ expect ( row . parsed ( r . data ) . operation ) . toBe ( 'update' ) ;
389+ } ) ;
390+ } ) ;
391+
392+ describe ( "a page action row's `onSuccess` is refused beside `operation: 'update'`, as `ActionSchema` refuses it" , ( ) => {
393+ /** No executor, the two the hop is declared beside, and a host handler: the write runs first on each. */
394+ const BESIDE = [ undefined , 'api' , 'script' , 'run' ] as const ;
395+ const updateRow = ( row : Row , executor : string | undefined , extra : Record < string , unknown > = { } ) =>
396+ row . parse ( { ...( executor === undefined ? { } : { [ row . executorKey ] : executor } ) , ...UPDATE , ...extra } ) ;
397+ const updateSubject = ( row : Row ) => onSuccessSubject ( row , "`operation: 'update'`" ) ;
398+
399+ it . each ( ROWS . flatMap ( ( row ) => BESIDE . map ( ( executor ) => [ row . name , String ( executor ) , row , executor ] as const ) ) ) (
400+ 'REFUSAL: %s with executor %s, `operation: \'update\'` and `onSuccess` is refused at `onSuccess`, ONE issue' ,
401+ ( _name , _label , row , executor ) => {
402+ const issues = issuesOf ( updateRow ( row , executor , { onSuccess : HOP } ) ) ;
403+ expect ( issues . map ( ( i ) => [ i . code , i . path ] ) ) . toEqual ( [ [ 'custom' , [ ...row . at , 'onSuccess' ] ] ] ) ;
404+ expect ( firstSentence ( issues [ 0 ] . message ) ) . toBe (
405+ `${ updateSubject ( row ) } would follow a result the contract does not declare: the console's action runner reads `
406+ + "`operation` before any executor, so an `operation: 'update'` action runs no handler and no executor of its "
407+ + 'own (the platform action route writes `patch` to the current record as the caller), and the runner then '
408+ + 'performs the post-success hop after that write succeeds.' ,
409+ ) ;
410+ } ,
411+ ) ;
412+
413+ it . each ( ROWS . flatMap ( ( row ) => BESIDE . map ( ( executor ) => [ row . name , String ( executor ) , row , executor ] as const ) ) ) (
414+ 'NEGATIVE CONTROL: %s with executor %s and `operation: \'update\'` and no `onSuccess` parses' ,
415+ ( _name , _label , row , executor ) => {
416+ const r = updateRow ( row , executor ) ;
417+ expect ( r . success , JSON . stringify ( issuesOf ( r ) ) ) . toBe ( true ) ;
418+ } ,
419+ ) ;
420+
421+ it . each ( ROWS ) ( "ONE TEXT: $name gives `ActionSchema`'s own refusal of the pairing, after the subject" , ( row ) => {
422+ const rowMessage = issuesOf ( updateRow ( row , 'api' , { onSuccess : HOP } ) ) [ 0 ] . message ;
423+ expect ( rowMessage . slice ( updateSubject ( row ) . length ) )
424+ . toBe ( actionUpdateOnSuccessMessage ( ) . slice ( ACTION_UPDATE_SUBJECT . length ) ) ;
425+ } ) ;
426+ } ) ;
0 commit comments