Skip to content

Commit cb515ff

Browse files
Copilothotlong
andcommitted
Add modal cross-reference validation for action.type="modal" → page existence check
Follows the same pattern as flow validation: when pages are defined, modal-type action targets are validated against the page names collection. When no pages are defined, validation is skipped (plugin may provide them). Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 2497807 commit cb515ff

2 files changed

Lines changed: 78 additions & 4 deletions

File tree

packages/spec/src/stack.test.ts

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1038,9 +1038,69 @@ describe('defineStack - Action Cross-Reference Validation', () => {
10381038
});
10391039

10401040
// ============================================================================
1041-
// Example-Level Strict Validation — mirrors examples/app-todo & examples/app-crm
1041+
// Action → Modal Cross-Reference Validation — ensures modal targets resolve to pages
10421042
// ============================================================================
10431043

1044+
describe('defineStack - Modal Cross-Reference Validation', () => {
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+
const makePage = (name: string) => ({
1053+
name,
1054+
label: name,
1055+
regions: [{ name: 'main', components: [] }],
1056+
});
1057+
1058+
it('should detect modal action referencing undefined page', () => {
1059+
const config = {
1060+
manifest: baseManifest,
1061+
objects: [
1062+
{ name: 'task', label: 'Task', fields: { title: { type: 'text' as const } } },
1063+
],
1064+
pages: [makePage('existing_page')],
1065+
actions: [
1066+
{ name: 'open_modal', label: 'Open Modal', type: 'modal' as const, target: 'nonexistent_page' },
1067+
],
1068+
};
1069+
1070+
expect(() => defineStack(config)).toThrow('cross-reference validation failed');
1071+
expect(() => defineStack(config)).toThrow('nonexistent_page');
1072+
});
1073+
1074+
it('should pass when modal action references a defined page', () => {
1075+
const config = {
1076+
manifest: baseManifest,
1077+
objects: [
1078+
{ name: 'task', label: 'Task', fields: { title: { type: 'text' as const } } },
1079+
],
1080+
pages: [makePage('defer_task_modal')],
1081+
actions: [
1082+
{ name: 'defer_task', label: 'Defer Task', type: 'modal' as const, target: 'defer_task_modal' },
1083+
],
1084+
};
1085+
1086+
expect(() => defineStack(config)).not.toThrow();
1087+
});
1088+
1089+
it('should skip modal validation when no pages are defined', () => {
1090+
const config = {
1091+
manifest: baseManifest,
1092+
objects: [
1093+
{ name: 'task', label: 'Task', fields: { title: { type: 'text' as const } } },
1094+
],
1095+
actions: [
1096+
{ name: 'open_modal', label: 'Open Modal', type: 'modal' as const, target: 'some_modal' },
1097+
],
1098+
};
1099+
1100+
expect(() => defineStack(config)).not.toThrow();
1101+
});
1102+
});
1103+
10441104
describe('defineStack - Example-Level Strict Validation', () => {
10451105
it('should validate a Todo-style app config (strict mode)', () => {
10461106
const todoConfig = {

packages/spec/src/stack.zod.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -400,9 +400,9 @@ function validateCrossReferences(config: ObjectStackDefinition): string[] {
400400
}
401401
}
402402

403-
// Validate action → flow/target cross-references
404-
// Note: When no flows are defined (flowNames.size === 0), flow-type action targets
405-
// are not validated because the referenced flow may be provided by a plugin.
403+
// Validate action → flow/modal cross-references
404+
// Note: When no flows/pages are defined (size === 0), targets are not validated
405+
// because the referenced items may be provided by a plugin.
406406
// This is consistent with dashboard/page/report validation in navigation.
407407
if (config.actions) {
408408
const flowNames = new Set<string>();
@@ -412,13 +412,27 @@ function validateCrossReferences(config: ObjectStackDefinition): string[] {
412412
}
413413
}
414414

415+
const pageNames = new Set<string>();
416+
if (config.pages) {
417+
for (const page of config.pages) {
418+
pageNames.add(page.name);
419+
}
420+
}
421+
415422
for (const action of config.actions) {
416423
// Validate flow-type actions reference a defined flow
417424
if (action.type === 'flow' && action.target && flowNames.size > 0 && !flowNames.has(action.target)) {
418425
errors.push(
419426
`Action '${action.name}' references flow '${action.target}' which is not defined in flows.`,
420427
);
421428
}
429+
430+
// Validate modal-type actions reference a defined page
431+
if (action.type === 'modal' && action.target && pageNames.size > 0 && !pageNames.has(action.target)) {
432+
errors.push(
433+
`Action '${action.name}' references modal '${action.target}' which is not defined in pages.`,
434+
);
435+
}
422436
}
423437
}
424438

0 commit comments

Comments
 (0)