@@ -542,6 +542,146 @@ describe('EmbeddingsGrouper', () => {
542542 expect ( totalNodesInClusters ) . toBe ( nodes . length ) ;
543543 } ) ;
544544 } ) ;
545+
546+ describe ( 'defensive handling and robustness' , ( ) => {
547+ describe ( 'invalid embeddings handling' , ( ) => {
548+ it ( 'should handle null and undefined embeddings' , ( ) => {
549+ // This test verifies the fix for runtime crashes from corrupted cache data
550+ const nodes = [
551+ createNode ( 'validTool' , 'cat1' , [ 1 , 0.8 , 0.6 ] ) ,
552+ // Simulate corrupted cache entries - these should be filtered out early
553+ { value : { name : 'corruptedTool1' , category : 'cat1' } , embedding : null as any } ,
554+ { value : { name : 'corruptedTool2' , category : 'cat1' } , embedding : undefined as any } ,
555+ createNode ( 'validTool2' , 'cat1' , [ 0.9 , 0.7 , 0.5 ] )
556+ ] ;
557+
558+ expect ( ( ) => {
559+ nodes . forEach ( node => grouper . addNode ( node ) ) ;
560+ grouper . recluster ( ) ;
561+ } ) . not . toThrow ( ) ;
562+
563+ const clusters = grouper . getClusters ( ) ;
564+ expect ( clusters . length ) . toBeGreaterThan ( 0 ) ;
565+
566+ // Should only include valid nodes in clusters (corrupted ones filtered out early)
567+ const totalValidNodes = clusters . reduce ( ( sum , cluster ) => sum + cluster . nodes . length , 0 ) ;
568+ expect ( totalValidNodes ) . toBe ( 2 ) ; // Only the two valid tools
569+ } ) ;
570+
571+ it ( 'should handle embeddings with null/undefined value arrays' , ( ) => {
572+ const nodes = [
573+ createNode ( 'validTool' , 'cat1' , [ 1 , 0.8 , 0.6 ] ) ,
574+ {
575+ value : { name : 'nullValuesTool' , category : 'cat1' } ,
576+ embedding : { type : EmbeddingType . text3small_512 , value : null as any }
577+ } ,
578+ {
579+ value : { name : 'undefinedValuesTool' , category : 'cat1' } ,
580+ embedding : { type : EmbeddingType . text3small_512 , value : undefined as any }
581+ } ,
582+ createNode ( 'validTool2' , 'cat1' , [ 0.9 , 0.7 , 0.5 ] )
583+ ] ;
584+
585+ expect ( ( ) => {
586+ nodes . forEach ( node => grouper . addNode ( node ) ) ;
587+ grouper . recluster ( ) ;
588+ } ) . not . toThrow ( ) ;
589+
590+ const clusters = grouper . getClusters ( ) ;
591+ expect ( clusters . length ) . toBeGreaterThan ( 0 ) ;
592+ } ) ;
593+
594+ it ( 'should handle empty embedding arrays' , ( ) => {
595+ const nodes = [
596+ createNode ( 'validTool' , 'cat1' , [ 1 , 0.8 , 0.6 ] ) ,
597+ {
598+ value : { name : 'emptyEmbedding' , category : 'cat1' } ,
599+ embedding : createEmbedding ( [ ] )
600+ } ,
601+ createNode ( 'validTool2' , 'cat1' , [ 0.9 , 0.7 , 0.5 ] )
602+ ] ;
603+
604+ expect ( ( ) => {
605+ nodes . forEach ( node => grouper . addNode ( node ) ) ;
606+ grouper . recluster ( ) ;
607+ } ) . not . toThrow ( ) ;
608+
609+ const clusters = grouper . getClusters ( ) ;
610+ expect ( clusters . length ) . toBeGreaterThan ( 0 ) ;
611+ } ) ;
612+ } ) ;
613+
614+ describe ( 'NaN values handling' , ( ) => {
615+ it ( 'should handle embeddings containing NaN values' , ( ) => {
616+ const nodes = [
617+ createNode ( 'validTool' , 'cat1' , [ 1 , 0.8 , 0.6 ] ) ,
618+ createNode ( 'nanTool' , 'cat1' , [ NaN , 0.5 , NaN ] ) ,
619+ createNode ( 'mixedNanTool' , 'cat1' , [ 0.7 , NaN , 0.4 ] ) ,
620+ createNode ( 'validTool2' , 'cat1' , [ 0.9 , 0.7 , 0.5 ] )
621+ ] ;
622+
623+ // The main goal: should not crash when processing NaN values
624+ expect ( ( ) => {
625+ nodes . forEach ( node => grouper . addNode ( node ) ) ;
626+ grouper . recluster ( ) ;
627+ } ) . not . toThrow ( ) ;
628+
629+ const clusters = grouper . getClusters ( ) ;
630+ expect ( clusters . length ) . toBeGreaterThan ( 0 ) ;
631+
632+ // Verify that centroids exist (NaN handling may vary)
633+ clusters . forEach ( cluster => {
634+ expect ( cluster . centroid ) . toBeDefined ( ) ;
635+ expect ( Array . isArray ( cluster . centroid ) ) . toBe ( true ) ;
636+ } ) ;
637+ } ) ;
638+
639+ it ( 'should handle zero vectors without division by zero' , ( ) => {
640+ const nodes = [
641+ createNode ( 'validTool' , 'cat1' , [ 1 , 0.8 , 0.6 ] ) ,
642+ createNode ( 'zeroVector' , 'cat1' , [ 0 , 0 , 0 ] ) ,
643+ createNode ( 'validTool2' , 'cat1' , [ 0.9 , 0.7 , 0.5 ] )
644+ ] ;
645+
646+ expect ( ( ) => {
647+ nodes . forEach ( node => grouper . addNode ( node ) ) ;
648+ grouper . recluster ( ) ;
649+ } ) . not . toThrow ( ) ;
650+
651+ const clusters = grouper . getClusters ( ) ;
652+ expect ( clusters . length ) . toBeGreaterThan ( 0 ) ;
653+ } ) ;
654+ } ) ;
655+
656+ describe ( 'dimension mismatches handling' , ( ) => {
657+ it ( 'should handle embeddings with different dimensions' , ( ) => {
658+ const nodes = [
659+ createNode ( 'tool3d' , 'cat1' , [ 1 , 0.8 , 0.6 ] ) ,
660+ createNode ( 'tool2d' , 'cat1' , [ 0.9 , 0.7 ] ) , // Different dimension
661+ createNode ( 'tool4d' , 'cat1' , [ 0.8 , 0.6 , 0.5 , 0.4 ] ) , // Different dimension
662+ createNode ( 'tool3d2' , 'cat1' , [ 0.7 , 0.5 , 0.3 ] )
663+ ] ;
664+
665+ // The main goal: should not crash when processing dimension mismatches
666+ expect ( ( ) => {
667+ nodes . forEach ( node => grouper . addNode ( node ) ) ;
668+ grouper . recluster ( ) ;
669+ } ) . not . toThrow ( ) ;
670+
671+ const clusters = grouper . getClusters ( ) ;
672+ expect ( clusters . length ) . toBeGreaterThan ( 0 ) ;
673+
674+ // Verify that centroids exist
675+ clusters . forEach ( cluster => {
676+ expect ( cluster . centroid ) . toBeDefined ( ) ;
677+ expect ( cluster . centroid . length ) . toBeGreaterThan ( 0 ) ;
678+
679+ // Just ensure we don't have empty clusters
680+ expect ( cluster . nodes . length ) . toBeGreaterThan ( 0 ) ;
681+ } ) ;
682+ } ) ;
683+ } ) ;
684+ } ) ;
545685} ) ;
546686
547687
0 commit comments