Skip to content

Commit 0444cc3

Browse files
Copilothotlong
andcommitted
feat: comprehensive protocol improvements across all layers
- Field: add `group` for UI organization and `conditionalRequired` for dynamic rules - Object: add `displayNameField` and `recordName` for record name generation - Validation: add `priority` for deterministic execution ordering - Hook: add `condition` for declarative filtering without handler code - ListView: add `quickFilters` for one-click filter chips - FormView: add `defaultSort` for related list sorting - Dashboard: add `dateRange` for global time range filtering - Action: add `variant` for button styling (primary/secondary/danger/ghost/link) - Flow: add `errorHandling` strategy (retry/fail/continue) at flow level - Workflow: add `executionOrder` for deterministic multi-workflow ordering - Approval: add `escalation` config for SLA-based auto-escalation - Translation: add `validationMessages` for translatable validation errors - Datasource: add `ssl` config for secure database connections - Permission: add `tabPermissions` for app tab visibility control All 4912 tests pass (52 new tests added, 0 failures) Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent ae47a03 commit 0444cc3

26 files changed

Lines changed: 807 additions & 1 deletion

packages/spec/src/automation/approval.test.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,3 +216,85 @@ describe('ApprovalProcess.create', () => {
216216
expect(result).toEqual(config);
217217
});
218218
});
219+
220+
// ============================================================================
221+
// Protocol Improvement Tests: Approval escalation
222+
// ============================================================================
223+
224+
describe('ApprovalProcessSchema - escalation', () => {
225+
it('should accept approval process with escalation config', () => {
226+
const result = ApprovalProcessSchema.parse({
227+
name: 'escalated_approval',
228+
label: 'Escalated Approval',
229+
object: 'purchase_order',
230+
steps: [{
231+
name: 'manager_review',
232+
label: 'Manager Review',
233+
approvers: [{ type: 'manager', value: 'direct_manager' }],
234+
}],
235+
escalation: {
236+
enabled: true,
237+
timeoutHours: 48,
238+
action: 'reassign',
239+
escalateTo: 'vp_operations',
240+
notifySubmitter: true,
241+
},
242+
});
243+
expect(result.escalation?.enabled).toBe(true);
244+
expect(result.escalation?.timeoutHours).toBe(48);
245+
expect(result.escalation?.action).toBe('reassign');
246+
expect(result.escalation?.escalateTo).toBe('vp_operations');
247+
});
248+
249+
it('should accept escalation with auto_approve action', () => {
250+
const result = ApprovalProcessSchema.parse({
251+
name: 'auto_escalate',
252+
label: 'Auto Escalate',
253+
object: 'expense_report',
254+
steps: [{
255+
name: 'review',
256+
label: 'Review',
257+
approvers: [{ type: 'user', value: 'finance_team' }],
258+
}],
259+
escalation: {
260+
enabled: true,
261+
timeoutHours: 72,
262+
action: 'auto_approve',
263+
},
264+
});
265+
expect(result.escalation?.action).toBe('auto_approve');
266+
});
267+
268+
it('should default escalation action to notify', () => {
269+
const result = ApprovalProcessSchema.parse({
270+
name: 'default_escalation',
271+
label: 'Default',
272+
object: 'request',
273+
steps: [{
274+
name: 'step_one',
275+
label: 'Step 1',
276+
approvers: [{ type: 'role', value: 'approver' }],
277+
}],
278+
escalation: {
279+
enabled: true,
280+
timeoutHours: 24,
281+
},
282+
});
283+
expect(result.escalation?.action).toBe('notify');
284+
expect(result.escalation?.notifySubmitter).toBe(true);
285+
});
286+
287+
it('should accept approval process without escalation (optional)', () => {
288+
const result = ApprovalProcessSchema.parse({
289+
name: 'no_escalation',
290+
label: 'No Escalation',
291+
object: 'task',
292+
steps: [{
293+
name: 'approve',
294+
label: 'Approve',
295+
approvers: [{ type: 'user', value: 'admin' }],
296+
}],
297+
});
298+
expect(result.escalation).toBeUndefined();
299+
});
300+
});

packages/spec/src/automation/approval.zod.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,15 @@ export const ApprovalProcessSchema = z.object({
9191

9292
/** Steps */
9393
steps: z.array(ApprovalStepSchema).min(1).describe('Sequence of approval steps'),
94+
95+
/** Escalation Configuration (SLA-based auto-escalation) */
96+
escalation: z.object({
97+
enabled: z.boolean().default(false).describe('Enable SLA-based escalation'),
98+
timeoutHours: z.number().min(1).describe('Hours before escalation triggers'),
99+
action: z.enum(['reassign', 'auto_approve', 'auto_reject', 'notify']).default('notify').describe('Action to take on escalation timeout'),
100+
escalateTo: z.string().optional().describe('User ID, role, or manager level to escalate to'),
101+
notifySubmitter: z.boolean().default(true).describe('Notify the original submitter on escalation'),
102+
}).optional().describe('SLA escalation configuration for pending approval steps'),
94103

95104
/** Global Actions */
96105
onSubmit: z.array(ApprovalActionSchema).optional().describe('Actions on initial submission'),

packages/spec/src/automation/flow.test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -525,3 +525,75 @@ describe('FlowSchema', () => {
525525
});
526526
});
527527
});
528+
529+
// ============================================================================
530+
// Protocol Improvement Tests: Flow errorHandling
531+
// ============================================================================
532+
533+
describe('FlowSchema - errorHandling', () => {
534+
it('should accept flow with errorHandling config', () => {
535+
const result = FlowSchema.parse({
536+
name: 'resilient_flow',
537+
label: 'Resilient Flow',
538+
type: 'autolaunched',
539+
nodes: [
540+
{ id: 'start', type: 'start', label: 'Start' },
541+
{ id: 'end', type: 'end', label: 'End' },
542+
],
543+
edges: [{ id: 'e1', source: 'start', target: 'end' }],
544+
errorHandling: {
545+
strategy: 'retry',
546+
maxRetries: 3,
547+
retryDelayMs: 2000,
548+
},
549+
});
550+
expect(result.errorHandling?.strategy).toBe('retry');
551+
expect(result.errorHandling?.maxRetries).toBe(3);
552+
expect(result.errorHandling?.retryDelayMs).toBe(2000);
553+
});
554+
555+
it('should default errorHandling strategy to fail', () => {
556+
const result = FlowSchema.parse({
557+
name: 'default_flow',
558+
label: 'Default',
559+
type: 'autolaunched',
560+
nodes: [
561+
{ id: 'start', type: 'start', label: 'Start' },
562+
],
563+
edges: [],
564+
errorHandling: {},
565+
});
566+
expect(result.errorHandling?.strategy).toBe('fail');
567+
expect(result.errorHandling?.maxRetries).toBe(0);
568+
});
569+
570+
it('should accept continue strategy with fallback node', () => {
571+
const result = FlowSchema.parse({
572+
name: 'fallback_flow',
573+
label: 'Fallback',
574+
type: 'autolaunched',
575+
nodes: [
576+
{ id: 'start', type: 'start', label: 'Start' },
577+
{ id: 'fallback', type: 'end', label: 'Fallback' },
578+
],
579+
edges: [],
580+
errorHandling: {
581+
strategy: 'continue',
582+
fallbackNodeId: 'fallback',
583+
},
584+
});
585+
expect(result.errorHandling?.strategy).toBe('continue');
586+
expect(result.errorHandling?.fallbackNodeId).toBe('fallback');
587+
});
588+
589+
it('should accept flow without errorHandling (optional)', () => {
590+
const result = FlowSchema.parse({
591+
name: 'simple_flow',
592+
label: 'Simple',
593+
type: 'autolaunched',
594+
nodes: [{ id: 'start', type: 'start', label: 'Start' }],
595+
edges: [],
596+
});
597+
expect(result.errorHandling).toBeUndefined();
598+
});
599+
});

packages/spec/src/automation/flow.zod.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,14 @@ export const FlowSchema = z.object({
137137
/** Execution Config */
138138
active: z.boolean().default(false).describe('Is active (Deprecated: use status)'),
139139
runAs: z.enum(['system', 'user']).default('user').describe('Execution context'),
140+
141+
/** Error Handling Strategy */
142+
errorHandling: z.object({
143+
strategy: z.enum(['fail', 'retry', 'continue']).default('fail').describe('How to handle node execution errors'),
144+
maxRetries: z.number().int().min(0).max(10).default(0).describe('Number of retry attempts (only for retry strategy)'),
145+
retryDelayMs: z.number().int().min(0).default(1000).describe('Delay between retries in milliseconds'),
146+
fallbackNodeId: z.string().optional().describe('Node ID to jump to on unrecoverable error'),
147+
}).optional().describe('Flow-level error handling configuration'),
140148
});
141149

142150
export type Flow = z.input<typeof FlowSchema>;

packages/spec/src/automation/workflow.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,3 +445,38 @@ describe('WorkflowRuleSchema', () => {
445445
expect(() => WorkflowRuleSchema.parse(rule)).toThrow();
446446
});
447447
});
448+
449+
// ============================================================================
450+
// Protocol Improvement Tests: Workflow executionOrder
451+
// ============================================================================
452+
453+
describe('WorkflowRuleSchema - executionOrder', () => {
454+
it('should accept workflow with custom executionOrder', () => {
455+
const result = WorkflowRuleSchema.parse({
456+
name: 'first_workflow',
457+
objectName: 'order',
458+
triggerType: 'on_create',
459+
executionOrder: 10,
460+
});
461+
expect(result.executionOrder).toBe(10);
462+
});
463+
464+
it('should default executionOrder to 100', () => {
465+
const result = WorkflowRuleSchema.parse({
466+
name: 'default_order',
467+
objectName: 'order',
468+
triggerType: 'on_create',
469+
});
470+
expect(result.executionOrder).toBe(100);
471+
});
472+
473+
it('should accept executionOrder of 0', () => {
474+
const result = WorkflowRuleSchema.parse({
475+
name: 'first_ever',
476+
objectName: 'lead',
477+
triggerType: 'on_update',
478+
executionOrder: 0,
479+
});
480+
expect(result.executionOrder).toBe(0);
481+
});
482+
});

packages/spec/src/automation/workflow.zod.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,9 @@ export const WorkflowRuleSchema = z.object({
267267

268268
/** Active status */
269269
active: z.boolean().default(true).describe('Whether this workflow is active'),
270+
271+
/** Execution Order */
272+
executionOrder: z.number().int().min(0).default(100).describe('Deterministic execution order when multiple workflows match (lower runs first)'),
270273

271274
/** Recursion Control */
272275
reevaluateOnChange: z.boolean().default(false).describe('Re-evaluate rule if field updates change the record validity'),

packages/spec/src/data/datasource.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,3 +417,60 @@ describe('DatasourceSchema', () => {
417417
})).toThrow();
418418
});
419419
});
420+
421+
// ============================================================================
422+
// Protocol Improvement Tests: Datasource SSL
423+
// ============================================================================
424+
425+
describe('DatasourceSchema - ssl', () => {
426+
it('should accept datasource with SSL config', () => {
427+
const result = DatasourceSchema.parse({
428+
name: 'secure_db',
429+
driver: 'postgres',
430+
config: { host: 'db.example.com', port: 5432 },
431+
ssl: {
432+
enabled: true,
433+
rejectUnauthorized: true,
434+
ca: '/path/to/ca.pem',
435+
},
436+
});
437+
expect(result.ssl?.enabled).toBe(true);
438+
expect(result.ssl?.rejectUnauthorized).toBe(true);
439+
expect(result.ssl?.ca).toBe('/path/to/ca.pem');
440+
});
441+
442+
it('should accept SSL with client certificates', () => {
443+
const result = DatasourceSchema.parse({
444+
name: 'mtls_db',
445+
driver: 'postgres',
446+
config: { host: 'db.secure.com' },
447+
ssl: {
448+
enabled: true,
449+
ca: '/certs/ca.pem',
450+
cert: '/certs/client.pem',
451+
key: '/certs/client-key.pem',
452+
},
453+
});
454+
expect(result.ssl?.cert).toBe('/certs/client.pem');
455+
expect(result.ssl?.key).toBe('/certs/client-key.pem');
456+
});
457+
458+
it('should default rejectUnauthorized to true', () => {
459+
const result = DatasourceSchema.parse({
460+
name: 'ssl_db',
461+
driver: 'mysql',
462+
config: {},
463+
ssl: { enabled: true },
464+
});
465+
expect(result.ssl?.rejectUnauthorized).toBe(true);
466+
});
467+
468+
it('should accept datasource without SSL (optional)', () => {
469+
const result = DatasourceSchema.parse({
470+
name: 'local_db',
471+
driver: 'sqlite',
472+
config: { path: './data.db' },
473+
});
474+
expect(result.ssl).toBeUndefined();
475+
});
476+
});

packages/spec/src/data/datasource.zod.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,15 @@ export const DatasourceSchema = z.object({
139139
timeoutMs: z.number().default(5000).describe('Health check timeout in milliseconds'),
140140
}).optional().describe('Datasource health check configuration'),
141141

142+
/** SSL/TLS Configuration */
143+
ssl: z.object({
144+
enabled: z.boolean().default(false).describe('Enable SSL/TLS for database connection'),
145+
rejectUnauthorized: z.boolean().default(true).describe('Reject connections with invalid/self-signed certificates'),
146+
ca: z.string().optional().describe('CA certificate (PEM format or path to file)'),
147+
cert: z.string().optional().describe('Client certificate (PEM format or path to file)'),
148+
key: z.string().optional().describe('Client private key (PEM format or path to file)'),
149+
}).optional().describe('SSL/TLS configuration for secure database connections'),
150+
142151
/** Retry Policy */
143152
retryPolicy: z.object({
144153
maxRetries: z.number().default(3).describe('Maximum number of retry attempts'),

packages/spec/src/data/field.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1385,3 +1385,51 @@ describe('Field Factory Helpers', () => {
13851385
});
13861386
});
13871387
});
1388+
1389+
// ============================================================================
1390+
// Protocol Improvement Tests: Field group and conditionalRequired
1391+
// ============================================================================
1392+
1393+
describe('FieldSchema - group property', () => {
1394+
it('should accept a field with group', () => {
1395+
const result = FieldSchema.parse({
1396+
type: 'text',
1397+
group: 'contact_info',
1398+
});
1399+
expect(result.group).toBe('contact_info');
1400+
});
1401+
1402+
it('should accept a field without group (optional)', () => {
1403+
const result = FieldSchema.parse({
1404+
type: 'text',
1405+
});
1406+
expect(result.group).toBeUndefined();
1407+
});
1408+
});
1409+
1410+
describe('FieldSchema - conditionalRequired property', () => {
1411+
it('should accept a field with conditionalRequired formula', () => {
1412+
const result = FieldSchema.parse({
1413+
type: 'text',
1414+
conditionalRequired: "status = 'closed_won'",
1415+
});
1416+
expect(result.conditionalRequired).toBe("status = 'closed_won'");
1417+
});
1418+
1419+
it('should accept a field without conditionalRequired (optional)', () => {
1420+
const result = FieldSchema.parse({
1421+
type: 'text',
1422+
});
1423+
expect(result.conditionalRequired).toBeUndefined();
1424+
});
1425+
1426+
it('should allow combining required and conditionalRequired', () => {
1427+
const result = FieldSchema.parse({
1428+
type: 'text',
1429+
required: false,
1430+
conditionalRequired: 'amount > 1000',
1431+
});
1432+
expect(result.required).toBe(false);
1433+
expect(result.conditionalRequired).toBe('amount > 1000');
1434+
});
1435+
});

packages/spec/src/data/field.zod.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,12 @@ export const FieldSchema = z.object({
449449
// Data quality rules
450450
dataQuality: DataQualityRulesSchema.optional().describe('Data quality validation and monitoring rules'),
451451

452+
/** Layout & Grouping */
453+
group: z.string().optional().describe('Field group name for organizing fields in forms and layouts (e.g., "contact_info", "billing", "system")'),
454+
455+
/** Conditional Requirements */
456+
conditionalRequired: z.string().optional().describe('Formula expression that makes this field required when TRUE (e.g., "status = \'closed_won\'")'),
457+
452458
/** Security & Visibility */
453459
hidden: z.boolean().default(false).describe('Hidden from default UI'),
454460
readonly: z.boolean().default(false).describe('Read-only in UI'),

0 commit comments

Comments
 (0)