@@ -9,8 +9,12 @@ import { describe, expect, it } from 'vitest';
99import {
1010 buildDependencyGraph ,
1111 connectedComponents ,
12+ deserializeGraph ,
13+ loadOrBuildGraph ,
1214 pageRank ,
15+ serializeGraph ,
1316 shortestPath ,
17+ updateGraphIncremental ,
1418 type WeightedEdge ,
1519} from '../graph' ;
1620
@@ -333,3 +337,178 @@ describe('shortestPath', () => {
333337 expect ( shortestPath ( new Map ( ) , 'X' , 'Y' ) ) . toBeNull ( ) ;
334338 } ) ;
335339} ) ;
340+
341+ // ============================================================================
342+ // Serialization
343+ // ============================================================================
344+
345+ describe ( 'serializeGraph / deserializeGraph' , ( ) => {
346+ it ( 'should round-trip correctly' , ( ) => {
347+ const graph = new Map < string , WeightedEdge [ ] > ( ) ;
348+ graph . set ( 'src/a.ts' , [ edge ( 'src/b.ts' , 1.414 ) , edge ( 'src/c.ts' , 1 ) ] ) ;
349+ graph . set ( 'src/b.ts' , [ edge ( 'src/c.ts' , 2 ) ] ) ;
350+
351+ const json = serializeGraph ( graph ) ;
352+ const restored = deserializeGraph ( json ) ;
353+
354+ expect ( restored ) . not . toBeNull ( ) ;
355+ expect ( restored ! . size ) . toBe ( 2 ) ;
356+ expect ( restored ! . get ( 'src/a.ts' ) ) . toEqual ( [
357+ { target : 'src/b.ts' , weight : 1.414 } ,
358+ { target : 'src/c.ts' , weight : 1 } ,
359+ ] ) ;
360+ expect ( restored ! . get ( 'src/b.ts' ) ) . toEqual ( [ { target : 'src/c.ts' , weight : 2 } ] ) ;
361+ } ) ;
362+
363+ it ( 'should include metadata in serialized JSON' , ( ) => {
364+ const graph = new Map < string , WeightedEdge [ ] > ( ) ;
365+ graph . set ( 'a' , [ edge ( 'b' ) ] ) ;
366+
367+ const parsed = JSON . parse ( serializeGraph ( graph ) ) ;
368+ expect ( parsed . version ) . toBe ( 1 ) ;
369+ expect ( parsed . nodeCount ) . toBe ( 1 ) ;
370+ expect ( parsed . edgeCount ) . toBe ( 1 ) ;
371+ expect ( parsed . generatedAt ) . toBeTruthy ( ) ;
372+ } ) ;
373+
374+ it ( 'should return null for invalid JSON' , ( ) => {
375+ expect ( deserializeGraph ( 'not json' ) ) . toBeNull ( ) ;
376+ } ) ;
377+
378+ it ( 'should return null for wrong version' , ( ) => {
379+ const json = JSON . stringify ( { version : 99 , graph : { } } ) ;
380+ expect ( deserializeGraph ( json ) ) . toBeNull ( ) ;
381+ } ) ;
382+
383+ it ( 'should return null for missing graph field' , ( ) => {
384+ const json = JSON . stringify ( { version : 1 } ) ;
385+ expect ( deserializeGraph ( json ) ) . toBeNull ( ) ;
386+ } ) ;
387+
388+ it ( 'should handle empty graph' , ( ) => {
389+ const graph = new Map < string , WeightedEdge [ ] > ( ) ;
390+ const json = serializeGraph ( graph ) ;
391+ const restored = deserializeGraph ( json ) ;
392+ expect ( restored ) . not . toBeNull ( ) ;
393+ expect ( restored ! . size ) . toBe ( 0 ) ;
394+ } ) ;
395+ } ) ;
396+
397+ // ============================================================================
398+ // loadOrBuildGraph
399+ // ============================================================================
400+
401+ describe ( 'loadOrBuildGraph' , ( ) => {
402+ it ( 'should call fallback when graphPath is undefined' , async ( ) => {
403+ const fallbackDocs = [
404+ {
405+ id : '1' ,
406+ score : 0 ,
407+ metadata : {
408+ path : 'src/a.ts' ,
409+ callees : [ { name : 'foo' , file : 'src/b.ts' , line : 1 } ] ,
410+ } ,
411+ } ,
412+ ] ;
413+
414+ const graph = await loadOrBuildGraph ( undefined , async ( ) => fallbackDocs ) ;
415+ expect ( graph . get ( 'src/a.ts' ) ) . toBeDefined ( ) ;
416+ } ) ;
417+
418+ it ( 'should call fallback when graphPath file does not exist' , async ( ) => {
419+ const fallbackDocs = [
420+ {
421+ id : '1' ,
422+ score : 0 ,
423+ metadata : {
424+ path : 'src/x.ts' ,
425+ callees : [ { name : 'bar' , file : 'src/y.ts' , line : 1 } ] ,
426+ } ,
427+ } ,
428+ ] ;
429+
430+ const graph = await loadOrBuildGraph ( '/nonexistent/path.json' , async ( ) => fallbackDocs ) ;
431+ expect ( graph . get ( 'src/x.ts' ) ) . toBeDefined ( ) ;
432+ } ) ;
433+ } ) ;
434+
435+ // ============================================================================
436+ // updateGraphIncremental
437+ // ============================================================================
438+
439+ describe ( 'updateGraphIncremental' , ( ) => {
440+ it ( 'should add edges for new files' , ( ) => {
441+ const existing = new Map < string , WeightedEdge [ ] > ( ) ;
442+ existing . set ( 'src/a.ts' , [ edge ( 'src/b.ts' ) ] ) ;
443+
444+ const changedDocs = [
445+ {
446+ id : '1' ,
447+ score : 0 ,
448+ metadata : {
449+ path : 'src/c.ts' ,
450+ callees : [ { name : 'foo' , file : 'src/d.ts' , line : 1 } ] ,
451+ } ,
452+ } ,
453+ ] ;
454+
455+ const updated = updateGraphIncremental ( existing , changedDocs , [ ] ) ;
456+ expect ( updated . get ( 'src/a.ts' ) ) . toBeDefined ( ) ; // Kept
457+ expect ( updated . get ( 'src/c.ts' ) ) . toBeDefined ( ) ; // Added
458+ } ) ;
459+
460+ it ( 'should remove edges for deleted files' , ( ) => {
461+ const existing = new Map < string , WeightedEdge [ ] > ( ) ;
462+ existing . set ( 'src/a.ts' , [ edge ( 'src/b.ts' ) ] ) ;
463+ existing . set ( 'src/b.ts' , [ edge ( 'src/c.ts' ) ] ) ;
464+
465+ const updated = updateGraphIncremental ( existing , [ ] , [ 'src/a.ts' ] ) ;
466+ expect ( updated . has ( 'src/a.ts' ) ) . toBe ( false ) ; // Removed
467+ expect ( updated . get ( 'src/b.ts' ) ) . toBeDefined ( ) ; // Kept
468+ } ) ;
469+
470+ it ( 'should replace edges for changed files' , ( ) => {
471+ const existing = new Map < string , WeightedEdge [ ] > ( ) ;
472+ existing . set ( 'src/a.ts' , [ edge ( 'src/old.ts' ) ] ) ;
473+
474+ const changedDocs = [
475+ {
476+ id : '1' ,
477+ score : 0 ,
478+ metadata : {
479+ path : 'src/a.ts' ,
480+ callees : [ { name : 'foo' , file : 'src/new.ts' , line : 1 } ] ,
481+ } ,
482+ } ,
483+ ] ;
484+
485+ const updated = updateGraphIncremental ( existing , changedDocs , [ ] ) ;
486+ const edges = updated . get ( 'src/a.ts' ) ! ;
487+ expect ( edges . length ) . toBe ( 1 ) ;
488+ expect ( edges [ 0 ] . target ) . toBe ( 'src/new.ts' ) ; // Replaced
489+ } ) ;
490+
491+ it ( 'should not mutate the existing graph' , ( ) => {
492+ const existing = new Map < string , WeightedEdge [ ] > ( ) ;
493+ existing . set ( 'src/a.ts' , [ edge ( 'src/b.ts' ) ] ) ;
494+
495+ updateGraphIncremental ( existing , [ ] , [ 'src/a.ts' ] ) ;
496+ expect ( existing . has ( 'src/a.ts' ) ) . toBe ( true ) ; // Original unchanged
497+ } ) ;
498+
499+ it ( 'should handle empty existing graph' , ( ) => {
500+ const changedDocs = [
501+ {
502+ id : '1' ,
503+ score : 0 ,
504+ metadata : {
505+ path : 'src/a.ts' ,
506+ callees : [ { name : 'foo' , file : 'src/b.ts' , line : 1 } ] ,
507+ } ,
508+ } ,
509+ ] ;
510+
511+ const updated = updateGraphIncremental ( new Map ( ) , changedDocs , [ ] ) ;
512+ expect ( updated . get ( 'src/a.ts' ) ) . toBeDefined ( ) ;
513+ } ) ;
514+ } ) ;
0 commit comments