@@ -4,7 +4,9 @@ import { describe, it, expect } from 'vitest';
44import {
55 normalizeMetadataCollection ,
66 normalizeStackInput ,
7+ normalizePluginMetadata ,
78 MAP_SUPPORTED_FIELDS ,
9+ METADATA_ALIASES ,
810} from './metadata-collection.zod' ;
911
1012describe ( 'normalizeMetadataCollection' , ( ) => {
@@ -260,3 +262,144 @@ describe('MAP_SUPPORTED_FIELDS', () => {
260262 expect ( MAP_SUPPORTED_FIELDS ) . not . toContain ( 'devPlugins' ) ;
261263 } ) ;
262264} ) ;
265+
266+ describe ( 'METADATA_ALIASES' , ( ) => {
267+ it ( 'should map triggers to hooks' , ( ) => {
268+ expect ( METADATA_ALIASES . triggers ) . toBe ( 'hooks' ) ;
269+ } ) ;
270+ } ) ;
271+
272+ describe ( 'normalizePluginMetadata' , ( ) => {
273+ describe ( 'map → array conversion' , ( ) => {
274+ it ( 'should convert map-formatted actions to an array' , ( ) => {
275+ const result = normalizePluginMetadata ( {
276+ actions : {
277+ lead_convert : { type : 'custom' , label : 'Convert Lead' } ,
278+ } ,
279+ } ) ;
280+ expect ( result . actions ) . toEqual ( [
281+ { name : 'lead_convert' , type : 'custom' , label : 'Convert Lead' } ,
282+ ] ) ;
283+ } ) ;
284+
285+ it ( 'should convert map-formatted workflows to an array' , ( ) => {
286+ const result = normalizePluginMetadata ( {
287+ workflows : {
288+ auto_assign : { objectName : 'lead' , label : 'Auto Assign' } ,
289+ } ,
290+ } ) ;
291+ expect ( result . workflows ) . toEqual ( [
292+ { name : 'auto_assign' , objectName : 'lead' , label : 'Auto Assign' } ,
293+ ] ) ;
294+ } ) ;
295+
296+ it ( 'should leave array-formatted collections unchanged' , ( ) => {
297+ const actions = [ { name : 'convert' , type : 'custom' } ] ;
298+ const result = normalizePluginMetadata ( { actions } ) ;
299+ expect ( result . actions ) . toBe ( actions ) ;
300+ } ) ;
301+
302+ it ( 'should handle multiple map-formatted collections at once' , ( ) => {
303+ const result = normalizePluginMetadata ( {
304+ actions : { a : { label : 'A' } } ,
305+ flows : { f : { label : 'F' } } ,
306+ hooks : { h : { object : 'lead' } } ,
307+ } ) ;
308+ expect ( result . actions ) . toEqual ( [ { name : 'a' , label : 'A' } ] ) ;
309+ expect ( result . flows ) . toEqual ( [ { name : 'f' , label : 'F' } ] ) ;
310+ expect ( result . hooks ) . toEqual ( [ { name : 'h' , object : 'lead' } ] ) ;
311+ } ) ;
312+ } ) ;
313+
314+ describe ( 'alias resolution (triggers → hooks)' , ( ) => {
315+ it ( 'should rename triggers to hooks' , ( ) => {
316+ const result = normalizePluginMetadata ( {
317+ triggers : {
318+ lead_scoring : { object : 'lead' , event : 'afterInsert' } ,
319+ } ,
320+ } ) ;
321+ expect ( result . hooks ) . toEqual ( [
322+ { name : 'lead_scoring' , object : 'lead' , event : 'afterInsert' } ,
323+ ] ) ;
324+ expect ( result . triggers ) . toBeUndefined ( ) ;
325+ } ) ;
326+
327+ it ( 'should merge triggers into existing hooks (array)' , ( ) => {
328+ const result = normalizePluginMetadata ( {
329+ hooks : [ { name : 'existing_hook' , object : 'account' } ] ,
330+ triggers : {
331+ new_trigger : { object : 'lead' , event : 'afterInsert' } ,
332+ } ,
333+ } ) ;
334+ expect ( result . hooks ) . toEqual ( [
335+ { name : 'existing_hook' , object : 'account' } ,
336+ { name : 'new_trigger' , object : 'lead' , event : 'afterInsert' } ,
337+ ] ) ;
338+ expect ( result . triggers ) . toBeUndefined ( ) ;
339+ } ) ;
340+
341+ it ( 'should merge triggers into existing hooks (map)' , ( ) => {
342+ const result = normalizePluginMetadata ( {
343+ hooks : { existing_hook : { object : 'account' } } ,
344+ triggers : { new_trigger : { object : 'lead' } } ,
345+ } ) ;
346+ expect ( result . hooks ) . toEqual ( [
347+ { name : 'existing_hook' , object : 'account' } ,
348+ { name : 'new_trigger' , object : 'lead' } ,
349+ ] ) ;
350+ expect ( result . triggers ) . toBeUndefined ( ) ;
351+ } ) ;
352+ } ) ;
353+
354+ describe ( 'recursive nested plugin normalization' , ( ) => {
355+ it ( 'should recursively normalize nested plugins' , ( ) => {
356+ const result = normalizePluginMetadata ( {
357+ actions : { a1 : { label : 'Root Action' } } ,
358+ plugins : [
359+ {
360+ actions : { nested_action : { label : 'Nested' } } ,
361+ triggers : { nested_trigger : { object : 'contact' } } ,
362+ } ,
363+ 'string-plugin-ref' , // string refs should pass through
364+ ] ,
365+ } ) ;
366+
367+ expect ( result . actions ) . toEqual ( [ { name : 'a1' , label : 'Root Action' } ] ) ;
368+ expect ( result . plugins ) . toHaveLength ( 2 ) ;
369+
370+ const nestedPlugin = result . plugins [ 0 ] as Record < string , unknown > ;
371+ expect ( nestedPlugin . actions ) . toEqual ( [ { name : 'nested_action' , label : 'Nested' } ] ) ;
372+ expect ( nestedPlugin . hooks ) . toEqual ( [ { name : 'nested_trigger' , object : 'contact' } ] ) ;
373+ expect ( nestedPlugin . triggers ) . toBeUndefined ( ) ;
374+
375+ expect ( result . plugins [ 1 ] ) . toBe ( 'string-plugin-ref' ) ;
376+ } ) ;
377+ } ) ;
378+
379+ describe ( 'pass-through / edge cases' , ( ) => {
380+ it ( 'should not modify non-metadata fields' , ( ) => {
381+ const result = normalizePluginMetadata ( {
382+ manifest : { name : 'test' , version : '1.0.0' } ,
383+ actions : { a : { label : 'A' } } ,
384+ } ) ;
385+ expect ( result . manifest ) . toEqual ( { name : 'test' , version : '1.0.0' } ) ;
386+ } ) ;
387+
388+ it ( 'should handle empty input' , ( ) => {
389+ expect ( normalizePluginMetadata ( { } ) ) . toEqual ( { } ) ;
390+ } ) ;
391+
392+ it ( 'should handle input with no metadata collections' , ( ) => {
393+ const input = { manifest : { name : 'test' } , i18n : { defaultLocale : 'en' } } ;
394+ const result = normalizePluginMetadata ( input ) ;
395+ expect ( result . manifest ) . toBe ( input . manifest ) ;
396+ expect ( result . i18n ) . toBe ( input . i18n ) ;
397+ } ) ;
398+
399+ it ( 'should handle undefined metadata fields' , ( ) => {
400+ const result = normalizePluginMetadata ( { actions : undefined , hooks : undefined } ) ;
401+ expect ( result . actions ) . toBeUndefined ( ) ;
402+ expect ( result . hooks ) . toBeUndefined ( ) ;
403+ } ) ;
404+ } ) ;
405+ } ) ;
0 commit comments