@@ -422,6 +422,26 @@ type recordingSyncCall struct {
422422 existingFiles []string
423423}
424424
425+ type countingIncrementalSyncer struct {
426+ delegate * incremental.Syncer
427+ calls int
428+ }
429+
430+ func (s * countingIncrementalSyncer ) SyncWithExisting (ctx context.Context , files map [string ]ingest.FileInfo , existingFiles []string ) (* ingest.SyncStats , error ) {
431+ s .calls ++
432+ return s .delegate .SyncWithExisting (ctx , files , existingFiles )
433+ }
434+
435+ func (s * countingIncrementalSyncer ) SyncWithExistingStore (ctx context.Context , graphStore ingest.GraphStore , files map [string ]ingest.FileInfo , existingFiles []string ) (* ingest.SyncStats , error ) {
436+ s .calls ++
437+ return s .delegate .SyncWithExistingStore (ctx , graphStore , files , existingFiles )
438+ }
439+
440+ func (s * countingIncrementalSyncer ) SyncBatchesWithExistingStore (ctx context.Context , graphStore ingest.GraphStore , source ingest.FileBatchSource , deletedFiles []string ) (* ingest.SyncStats , error ) {
441+ s .calls ++
442+ return s .delegate .SyncBatchesWithExistingStore (ctx , graphStore , source , deletedFiles )
443+ }
444+
425445func (r * recordingIncrementalSyncer ) Sync (ctx context.Context , files map [string ]incremental.FileInfo ) (* incremental.SyncStats , error ) {
426446 panic ("unexpected Sync call" )
427447}
@@ -1121,14 +1141,18 @@ func TestUpdate_ReconcilesExistingCallerAfterTargetFileAdded(t *testing.T) {
11211141 }
11221142 writeFile ("target.go" , "package sample\n \n func Target() {}\n " )
11231143
1124- syncer := incremental .NewWithRegistry (st , map [string ]incremental.Parser {".go" : walker }, incremental .WithLogger (slog .Default ()))
1144+ delegate := incremental .NewWithRegistry (st , map [string ]incremental.Parser {".go" : walker }, incremental .WithLogger (slog .Default ()))
1145+ syncer := & countingIncrementalSyncer {delegate : delegate }
11251146 stats , err := svc .Update (ctx , UpdateOptions {BuildOptions : BuildOptions {Dir : tmpDir }, Syncer : syncer })
11261147 if err != nil {
11271148 t .Fatalf ("Update after target add: %v" , err )
11281149 }
11291150 if stats .Added != 1 {
11301151 t .Fatalf ("Added = %d, want one newly added target" , stats .Added )
11311152 }
1153+ if syncer .calls != 1 {
1154+ t .Fatalf ("incremental sync calls = %d, want existing-package update path" , syncer .calls )
1155+ }
11321156
11331157 source , err := st .GetNode (ctx , "sample.Source" )
11341158 if err != nil || source == nil {
@@ -1150,6 +1174,78 @@ func TestUpdate_ReconcilesExistingCallerAfterTargetFileAdded(t *testing.T) {
11501174 t .Fatalf ("expected existing caller edge from %d to newly added target %d, got %+v" , source .ID , target .ID , edges )
11511175}
11521176
1177+ func TestUpdate_ReconcilesExistingCrossPackageCallerAfterTargetPackageAdded (t * testing.T ) {
1178+ db , err := gorm .Open (sqlite .Open (":memory:" ), & gorm.Config {Logger : gormlogger .Discard })
1179+ if err != nil {
1180+ t .Fatalf ("open db: %v" , err )
1181+ }
1182+ st := graphgorm .New (db )
1183+ if err := st .AutoMigrate (); err != nil {
1184+ t .Fatalf ("migrate: %v" , err )
1185+ }
1186+ walker := treesitter .NewWalker (treesitter .GoSpec )
1187+ svc := & Service {Store : st , UnitOfWork : newTestUnitOfWork (db , nil ), Walkers : map [string ]Parser {".go" : walker }, Logger : slog .Default ()}
1188+
1189+ tmpDir := t .TempDir ()
1190+ writeFile := func (rel , content string ) {
1191+ full := filepath .Join (tmpDir , rel )
1192+ if err := os .MkdirAll (filepath .Dir (full ), 0o755 ); err != nil {
1193+ t .Fatalf ("mkdir %s: %v" , rel , err )
1194+ }
1195+ if err := os .WriteFile (full , []byte (content ), 0o644 ); err != nil {
1196+ t .Fatalf ("write %s: %v" , rel , err )
1197+ }
1198+ }
1199+ writeFile ("go.mod" , "module github.com/example/project\n \n go 1.25.0\n " )
1200+ writeFile ("app/source.go" , `package app
1201+
1202+ import "github.com/example/project/api"
1203+
1204+ func Source() { api.Target() }
1205+ ` )
1206+
1207+ ctx := context .Background ()
1208+ if _ , err := svc .Build (ctx , BuildOptions {Dir : tmpDir }); err != nil {
1209+ t .Fatalf ("Build before target package exists: %v" , err )
1210+ }
1211+ writeFile ("api/target.go" , "package api\n \n func Target() {}\n " )
1212+
1213+ delegate := incremental .NewWithRegistry (st , map [string ]incremental.Parser {".go" : walker }, incremental .WithLogger (slog .Default ()))
1214+ syncer := & countingIncrementalSyncer {delegate : delegate }
1215+ stats , err := svc .Update (ctx , UpdateOptions {BuildOptions : BuildOptions {Dir : tmpDir }, Syncer : syncer })
1216+ if err != nil {
1217+ t .Fatalf ("Update after target package add: %v" , err )
1218+ }
1219+ if stats .Added != 1 {
1220+ t .Fatalf ("Added = %d, want one newly added target file" , stats .Added )
1221+ }
1222+ if stats .Modified != 0 || stats .Skipped != 1 || stats .Deleted != 0 {
1223+ t .Fatalf ("fallback change counts = %+v, want added=1 modified=0 skipped=1 deleted=0" , stats )
1224+ }
1225+ if syncer .calls != 0 {
1226+ t .Fatalf ("incremental sync calls = %d, want full-build fallback" , syncer .calls )
1227+ }
1228+
1229+ source , err := st .GetNode (ctx , "app.Source" )
1230+ if err != nil || source == nil {
1231+ t .Fatalf ("GetNode source: node=%v err=%v" , source , err )
1232+ }
1233+ target , err := st .GetNode (ctx , "api.Target" )
1234+ if err != nil || target == nil {
1235+ t .Fatalf ("GetNode target: node=%v err=%v" , target , err )
1236+ }
1237+ edges , err := st .GetEdgesFrom (ctx , source .ID )
1238+ if err != nil {
1239+ t .Fatalf ("GetEdgesFrom: %v" , err )
1240+ }
1241+ for _ , edge := range edges {
1242+ if edge .Kind == graph .EdgeKindCalls && edge .ToNodeID == target .ID {
1243+ return
1244+ }
1245+ }
1246+ t .Fatalf ("expected cross-package caller edge from %d to newly added target %d, got %+v" , source .ID , target .ID , edges )
1247+ }
1248+
11531249func TestUpdate_RemovesStaleCrossFileGoStructuralImplements (t * testing.T ) {
11541250 db , err := gorm .Open (sqlite .Open (":memory:" ), & gorm.Config {Logger : gormlogger .Discard })
11551251 if err != nil {
@@ -3393,6 +3489,15 @@ func TestUpdate_IncludePaths_FiltersExistingFilesWhenReplaceFalse(t *testing.T)
33933489 if got , want := deleteCall .existingFiles , []string {filepath .Join ("src" , "api" , "handler.go" )}; ! reflect .DeepEqual (got , want ) {
33943490 t .Fatalf ("delete pass existingFiles mismatch: got=%v want=%v (helper.go outside include path must be scoped out)" , got , want )
33953491 }
3492+ var outsideCount int64
3493+ if err := db .Model (& graph.Node {}).
3494+ Where ("namespace = ? AND file_path = ?" , requestctx .DefaultNamespace , filepath .Join ("src" , "other" , "helper.go" )).
3495+ Count (& outsideCount ).Error ; err != nil {
3496+ t .Fatalf ("count out-of-scope node: %v" , err )
3497+ }
3498+ if outsideCount != 1 {
3499+ t .Fatalf ("out-of-scope node count = %d, want preserved graph state" , outsideCount )
3500+ }
33963501}
33973502
33983503func TestUpdate_ExcludePatterns_LeavesMatchingFilesOutOfSync (t * testing.T ) {
0 commit comments