@@ -1037,6 +1037,170 @@ describe('defineStack - Action Cross-Reference Validation', () => {
10371037 } ) ;
10381038} ) ;
10391039
1040+ // ============================================================================
1041+ // Action → Object Auto-Merge — actions with objectName are merged into objects
1042+ // ============================================================================
1043+
1044+ describe ( 'defineStack - Action Auto-Merge into Objects' , ( ) => {
1045+ const baseManifest = {
1046+ id : 'com.example.test' ,
1047+ name : 'test-project' ,
1048+ version : '1.0.0' ,
1049+ type : 'app' as const ,
1050+ } ;
1051+
1052+ it ( 'should merge actions with objectName into corresponding objects' , ( ) => {
1053+ const config = {
1054+ manifest : baseManifest ,
1055+ objects : [
1056+ { name : 'task' , fields : { title : { type : 'text' as const } } } ,
1057+ ] ,
1058+ actions : [
1059+ { name : 'approve_task' , label : 'Approve' , objectName : 'task' } ,
1060+ ] ,
1061+ } ;
1062+
1063+ const result = defineStack ( config ) ;
1064+ expect ( result . objects ! [ 0 ] . actions ) . toHaveLength ( 1 ) ;
1065+ expect ( result . objects ! [ 0 ] . actions ! [ 0 ] . name ) . toBe ( 'approve_task' ) ;
1066+ } ) ;
1067+
1068+ it ( 'should merge multiple actions into the same object' , ( ) => {
1069+ const config = {
1070+ manifest : baseManifest ,
1071+ objects : [
1072+ { name : 'deal' , fields : { amount : { type : 'number' as const } } } ,
1073+ ] ,
1074+ actions : [
1075+ { name : 'close_deal' , label : 'Close Deal' , objectName : 'deal' } ,
1076+ { name : 'reopen_deal' , label : 'Reopen Deal' , objectName : 'deal' } ,
1077+ ] ,
1078+ } ;
1079+
1080+ const result = defineStack ( config ) ;
1081+ expect ( result . objects ! [ 0 ] . actions ) . toHaveLength ( 2 ) ;
1082+ expect ( result . objects ! [ 0 ] . actions ! . map ( a => a . name ) ) . toEqual ( [ 'close_deal' , 'reopen_deal' ] ) ;
1083+ } ) ;
1084+
1085+ it ( 'should merge actions into different objects' , ( ) => {
1086+ const config = {
1087+ manifest : baseManifest ,
1088+ objects : [
1089+ { name : 'task' , fields : { title : { type : 'text' as const } } } ,
1090+ { name : 'project' , fields : { name : { type : 'text' as const } } } ,
1091+ ] ,
1092+ actions : [
1093+ { name : 'complete_task' , label : 'Complete' , objectName : 'task' } ,
1094+ { name : 'archive_project' , label : 'Archive' , objectName : 'project' } ,
1095+ ] ,
1096+ } ;
1097+
1098+ const result = defineStack ( config ) ;
1099+ expect ( result . objects ! [ 0 ] . actions ) . toHaveLength ( 1 ) ;
1100+ expect ( result . objects ! [ 0 ] . actions ! [ 0 ] . name ) . toBe ( 'complete_task' ) ;
1101+ expect ( result . objects ! [ 1 ] . actions ) . toHaveLength ( 1 ) ;
1102+ expect ( result . objects ! [ 1 ] . actions ! [ 0 ] . name ) . toBe ( 'archive_project' ) ;
1103+ } ) ;
1104+
1105+ it ( 'should not modify objects when no actions have objectName' , ( ) => {
1106+ const config = {
1107+ manifest : baseManifest ,
1108+ objects : [
1109+ { name : 'task' , fields : { title : { type : 'text' as const } } } ,
1110+ ] ,
1111+ actions : [
1112+ { name : 'global_action' , label : 'Global' } ,
1113+ ] ,
1114+ } ;
1115+
1116+ const result = defineStack ( config ) ;
1117+ expect ( result . objects ! [ 0 ] . actions ) . toBeUndefined ( ) ;
1118+ } ) ;
1119+
1120+ it ( 'should preserve top-level actions array after merge' , ( ) => {
1121+ const config = {
1122+ manifest : baseManifest ,
1123+ objects : [
1124+ { name : 'task' , fields : { title : { type : 'text' as const } } } ,
1125+ ] ,
1126+ actions : [
1127+ { name : 'approve_task' , label : 'Approve' , objectName : 'task' } ,
1128+ { name : 'global_search' , label : 'Search' } ,
1129+ ] ,
1130+ } ;
1131+
1132+ const result = defineStack ( config ) ;
1133+ // Top-level actions are preserved
1134+ expect ( result . actions ) . toHaveLength ( 2 ) ;
1135+ // Object has the merged action
1136+ expect ( result . objects ! [ 0 ] . actions ) . toHaveLength ( 1 ) ;
1137+ } ) ;
1138+
1139+ it ( 'should append merged actions to existing object.actions' , ( ) => {
1140+ const config = {
1141+ manifest : baseManifest ,
1142+ objects : [
1143+ {
1144+ name : 'task' ,
1145+ fields : { title : { type : 'text' as const } } ,
1146+ actions : [ { name : 'inline_action' , label : 'Inline' } ] ,
1147+ } ,
1148+ ] ,
1149+ actions : [
1150+ { name : 'merged_action' , label : 'Merged' , objectName : 'task' } ,
1151+ ] ,
1152+ } ;
1153+
1154+ const result = defineStack ( config ) ;
1155+ expect ( result . objects ! [ 0 ] . actions ) . toHaveLength ( 2 ) ;
1156+ expect ( result . objects ! [ 0 ] . actions ! [ 0 ] . name ) . toBe ( 'inline_action' ) ;
1157+ expect ( result . objects ! [ 0 ] . actions ! [ 1 ] . name ) . toBe ( 'merged_action' ) ;
1158+ } ) ;
1159+
1160+ it ( 'should work in non-strict mode' , ( ) => {
1161+ const config = {
1162+ manifest : baseManifest ,
1163+ objects : [
1164+ { name : 'task' , fields : { title : { type : 'text' as const } } } ,
1165+ ] ,
1166+ actions : [
1167+ { name : 'approve_task' , label : 'Approve' , objectName : 'task' } ,
1168+ ] ,
1169+ } ;
1170+
1171+ const result = defineStack ( config , { strict : false } ) ;
1172+ expect ( result . objects ! [ 0 ] . actions ) . toHaveLength ( 1 ) ;
1173+ expect ( result . objects ! [ 0 ] . actions ! [ 0 ] . name ) . toBe ( 'approve_task' ) ;
1174+ } ) ;
1175+
1176+ it ( 'should detect action objectName referencing undefined object' , ( ) => {
1177+ const config = {
1178+ manifest : baseManifest ,
1179+ objects : [
1180+ { name : 'task' , fields : { title : { type : 'text' as const } } } ,
1181+ ] ,
1182+ actions : [
1183+ { name : 'approve_deal' , label : 'Approve' , objectName : 'nonexistent_object' } ,
1184+ ] ,
1185+ } ;
1186+
1187+ expect ( ( ) => defineStack ( config ) ) . toThrow ( 'cross-reference validation failed' ) ;
1188+ expect ( ( ) => defineStack ( config ) ) . toThrow ( 'nonexistent_object' ) ;
1189+ } ) ;
1190+
1191+ it ( 'should skip objectName validation when no objects are defined' , ( ) => {
1192+ const config = {
1193+ manifest : baseManifest ,
1194+ actions : [
1195+ { name : 'approve_deal' , label : 'Approve' , objectName : 'deal' } ,
1196+ ] ,
1197+ } ;
1198+
1199+ // No objects defined, cross-ref validation is skipped
1200+ expect ( ( ) => defineStack ( config ) ) . not . toThrow ( ) ;
1201+ } ) ;
1202+ } ) ;
1203+
10401204// ============================================================================
10411205// Action → Modal Cross-Reference Validation — ensures modal targets resolve to pages
10421206// ============================================================================
0 commit comments