@@ -847,3 +847,289 @@ describe('defineStack - Map Format Support', () => {
847847 expect ( result . views ! [ 0 ] . list ?. type ) . toBe ( 'grid' ) ;
848848 } ) ;
849849} ) ;
850+
851+ // ============================================================================
852+ // Negative / Inverse Validation Tests — Cross-Reference
853+ // ============================================================================
854+
855+ describe ( 'defineStack - Seed Data Cross-Reference Validation' , ( ) => {
856+ const baseManifest = {
857+ id : 'com.example.test' ,
858+ name : 'test-project' ,
859+ version : '1.0.0' ,
860+ type : 'app' as const ,
861+ } ;
862+
863+ it ( 'should detect seed data referencing undefined object' , ( ) => {
864+ const config = {
865+ manifest : baseManifest ,
866+ objects : [
867+ { name : 'account' , fields : { name : { type : 'text' } } } ,
868+ ] ,
869+ data : [
870+ { object : 'ghost_object' , records : [ { name : 'Test' } ] } ,
871+ ] ,
872+ } ;
873+ expect ( ( ) => defineStack ( config ) ) . toThrow ( 'ghost_object' ) ;
874+ expect ( ( ) => defineStack ( config ) ) . toThrow ( 'cross-reference validation failed' ) ;
875+ } ) ;
876+
877+ it ( 'should pass when seed data references defined object' , ( ) => {
878+ const config = {
879+ manifest : baseManifest ,
880+ objects : [
881+ { name : 'account' , fields : { name : { type : 'text' } } } ,
882+ ] ,
883+ data : [
884+ { object : 'account' , records : [ { name : 'Acme Corp' } ] } ,
885+ ] ,
886+ } ;
887+ expect ( ( ) => defineStack ( config ) ) . not . toThrow ( ) ;
888+ } ) ;
889+ } ) ;
890+
891+ describe ( 'defineStack - Navigation Cross-Reference Validation' , ( ) => {
892+ const baseManifest = {
893+ id : 'com.example.test' ,
894+ name : 'test-project' ,
895+ version : '1.0.0' ,
896+ type : 'app' as const ,
897+ } ;
898+
899+ it ( 'should detect navigation referencing undefined object' , ( ) => {
900+ const config = {
901+ manifest : baseManifest ,
902+ objects : [
903+ { name : 'task' , fields : { title : { type : 'text' } } } ,
904+ ] ,
905+ apps : [
906+ {
907+ name : 'my_app' ,
908+ label : 'My App' ,
909+ navigation : [
910+ { id : 'nav_missing' , type : 'object' as const , label : 'Missing' , objectName : 'nonexistent_object' } ,
911+ ] ,
912+ } ,
913+ ] ,
914+ } ;
915+ expect ( ( ) => defineStack ( config ) ) . toThrow ( 'nonexistent_object' ) ;
916+ } ) ;
917+
918+ it ( 'should detect navigation referencing undefined dashboard' , ( ) => {
919+ const config = {
920+ manifest : baseManifest ,
921+ objects : [
922+ { name : 'task' , fields : { title : { type : 'text' } } } ,
923+ ] ,
924+ dashboards : [
925+ { name : 'sales_dashboard' , label : 'Sales' , widgets : [ ] } ,
926+ ] ,
927+ apps : [
928+ {
929+ name : 'my_app' ,
930+ label : 'My App' ,
931+ navigation : [
932+ { id : 'nav_ghost' , type : 'dashboard' as const , label : 'Missing' , dashboardName : 'ghost_dashboard' } ,
933+ ] ,
934+ } ,
935+ ] ,
936+ } ;
937+ expect ( ( ) => defineStack ( config ) ) . toThrow ( 'ghost_dashboard' ) ;
938+ } ) ;
939+
940+ it ( 'should pass when all navigation references are valid' , ( ) => {
941+ const config = {
942+ manifest : baseManifest ,
943+ objects : [
944+ { name : 'task' , fields : { title : { type : 'text' } } } ,
945+ ] ,
946+ dashboards : [
947+ { name : 'task_overview' , label : 'Overview' , widgets : [ ] } ,
948+ ] ,
949+ apps : [
950+ {
951+ name : 'my_app' ,
952+ label : 'My App' ,
953+ navigation : [
954+ { id : 'nav_tasks' , type : 'object' as const , label : 'Tasks' , objectName : 'task' } ,
955+ { id : 'nav_overview' , type : 'dashboard' as const , label : 'Overview' , dashboardName : 'task_overview' } ,
956+ ] ,
957+ } ,
958+ ] ,
959+ } ;
960+ expect ( ( ) => defineStack ( config ) ) . not . toThrow ( ) ;
961+ } ) ;
962+ } ) ;
963+
964+ // ============================================================================
965+ // Example-Level Strict Validation — mirrors examples/app-todo & examples/app-crm
966+ // ============================================================================
967+
968+ describe ( 'defineStack - Example-Level Strict Validation' , ( ) => {
969+ it ( 'should validate a Todo-style app config (strict mode)' , ( ) => {
970+ const todoConfig = {
971+ manifest : {
972+ id : 'com.example.todo' ,
973+ namespace : 'todo' ,
974+ version : '2.0.0' ,
975+ type : 'app' as const ,
976+ name : 'Todo Manager' ,
977+ description : 'A comprehensive Todo app' ,
978+ } ,
979+ objects : [
980+ {
981+ name : 'task' ,
982+ label : 'Task' ,
983+ fields : {
984+ subject : { type : 'text' , label : 'Subject' , required : true } ,
985+ status : { type : 'select' , label : 'Status' , options : [
986+ { value : 'not_started' , label : 'Not Started' } ,
987+ { value : 'in_progress' , label : 'In Progress' } ,
988+ { value : 'completed' , label : 'Completed' } ,
989+ ] } ,
990+ priority : { type : 'select' , label : 'Priority' , options : [
991+ { value : 'low' , label : 'Low' } ,
992+ { value : 'normal' , label : 'Normal' } ,
993+ { value : 'high' , label : 'High' } ,
994+ ] } ,
995+ category : { type : 'text' , label : 'Category' } ,
996+ due_date : { type : 'date' , label : 'Due Date' } ,
997+ } ,
998+ } ,
999+ ] ,
1000+ data : [
1001+ {
1002+ object : 'task' ,
1003+ mode : 'upsert' as const ,
1004+ externalId : 'subject' ,
1005+ records : [
1006+ { subject : 'Learn ObjectStack' , status : 'completed' , priority : 'high' , category : 'Work' } ,
1007+ { subject : 'Build a cool app' , status : 'in_progress' , priority : 'normal' , category : 'Work' } ,
1008+ ] ,
1009+ } ,
1010+ ] ,
1011+ dashboards : [
1012+ {
1013+ name : 'task_overview' ,
1014+ label : 'Task Overview' ,
1015+ widgets : [
1016+ { title : 'Total Tasks' , type : 'metric' , object : 'task' , aggregate : 'count' , layout : { x : 0 , y : 0 , w : 3 , h : 2 } } ,
1017+ { title : 'By Status' , type : 'pie' , object : 'task' , categoryField : 'status' , aggregate : 'count' , layout : { x : 3 , y : 0 , w : 6 , h : 4 } } ,
1018+ ] ,
1019+ } ,
1020+ ] ,
1021+ apps : [
1022+ {
1023+ name : 'todo_app' ,
1024+ label : 'Todo Manager' ,
1025+ navigation : [
1026+ { id : 'nav_tasks' , type : 'object' as const , label : 'Tasks' , objectName : 'task' } ,
1027+ { id : 'nav_dashboard' , type : 'dashboard' as const , label : 'Overview' , dashboardName : 'task_overview' } ,
1028+ ] ,
1029+ } ,
1030+ ] ,
1031+ } ;
1032+ expect ( ( ) => defineStack ( todoConfig , { strict : true } ) ) . not . toThrow ( ) ;
1033+ } ) ;
1034+
1035+ it ( 'should validate a CRM-style app config with seed data and reports (strict mode)' , ( ) => {
1036+ const crmConfig = {
1037+ manifest : {
1038+ id : 'com.example.crm' ,
1039+ namespace : 'crm' ,
1040+ version : '1.0.0' ,
1041+ type : 'app' as const ,
1042+ name : 'Sales CRM' ,
1043+ description : 'Complete sales management solution' ,
1044+ } ,
1045+ objects : [
1046+ {
1047+ name : 'account' ,
1048+ label : 'Account' ,
1049+ fields : {
1050+ name : { type : 'text' , label : 'Name' , required : true } ,
1051+ industry : { type : 'text' , label : 'Industry' } ,
1052+ annual_revenue : { type : 'number' , label : 'Annual Revenue' } ,
1053+ } ,
1054+ } ,
1055+ {
1056+ name : 'opportunity' ,
1057+ label : 'Opportunity' ,
1058+ fields : {
1059+ name : { type : 'text' , label : 'Name' , required : true } ,
1060+ amount : { type : 'currency' , label : 'Amount' } ,
1061+ stage : { type : 'select' , label : 'Stage' , options : [
1062+ { value : 'prospecting' , label : 'Prospecting' } ,
1063+ { value : 'negotiation' , label : 'Negotiation' } ,
1064+ { value : 'closed_won' , label : 'Closed Won' } ,
1065+ ] } ,
1066+ } ,
1067+ } ,
1068+ ] ,
1069+ data : [
1070+ {
1071+ object : 'account' ,
1072+ mode : 'upsert' as const ,
1073+ externalId : 'name' ,
1074+ records : [
1075+ { name : 'Acme Corp' , industry : 'technology' , annual_revenue : 5000000 } ,
1076+ ] ,
1077+ } ,
1078+ ] ,
1079+ reports : [
1080+ {
1081+ name : 'pipeline_report' ,
1082+ label : 'Pipeline Report' ,
1083+ objectName : 'opportunity' ,
1084+ type : 'summary' as const ,
1085+ columns : [
1086+ { field : 'name' } ,
1087+ { field : 'amount' , aggregate : 'sum' as const } ,
1088+ ] ,
1089+ groupingsDown : [ { field : 'stage' } ] ,
1090+ } ,
1091+ ] ,
1092+ dashboards : [
1093+ {
1094+ name : 'sales_overview' ,
1095+ label : 'Sales Overview' ,
1096+ widgets : [
1097+ { title : 'Pipeline Value' , type : 'metric' , object : 'opportunity' , valueField : 'amount' , aggregate : 'sum' , layout : { x : 0 , y : 0 , w : 4 , h : 2 } } ,
1098+ ] ,
1099+ } ,
1100+ ] ,
1101+ apps : [
1102+ {
1103+ name : 'sales_crm' ,
1104+ label : 'Sales CRM' ,
1105+ icon : 'briefcase' ,
1106+ navigation : [
1107+ { id : 'nav_accounts' , type : 'object' as const , label : 'Accounts' , objectName : 'account' } ,
1108+ { id : 'nav_opportunities' , type : 'object' as const , label : 'Opportunities' , objectName : 'opportunity' } ,
1109+ { id : 'nav_dashboard' , type : 'dashboard' as const , label : 'Sales Overview' , dashboardName : 'sales_overview' } ,
1110+ { id : 'nav_report' , type : 'report' as const , label : 'Pipeline' , reportName : 'pipeline_report' } ,
1111+ ] ,
1112+ } ,
1113+ ] ,
1114+ } ;
1115+ expect ( ( ) => defineStack ( crmConfig , { strict : true } ) ) . not . toThrow ( ) ;
1116+ } ) ;
1117+
1118+ it ( 'should reject CRM config with seed data referencing non-existent object' , ( ) => {
1119+ const badConfig = {
1120+ manifest : {
1121+ id : 'com.example.crm' ,
1122+ name : 'crm' ,
1123+ version : '1.0.0' ,
1124+ type : 'app' as const ,
1125+ } ,
1126+ objects : [
1127+ { name : 'account' , fields : { name : { type : 'text' } } } ,
1128+ ] ,
1129+ data : [
1130+ { object : 'contact' , records : [ { name : 'John' } ] } ,
1131+ ] ,
1132+ } ;
1133+ expect ( ( ) => defineStack ( badConfig , { strict : true } ) ) . toThrow ( 'contact' ) ;
1134+ } ) ;
1135+ } ) ;
0 commit comments