package main import ( "path/filepath" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" "github.com/zzet/gortex/internal/contracts" "github.com/zzet/gortex/internal/graph" ) // TestSnapshotRoundTrip_Contracts is the regression guard for the bug // where per-repo contract registries came back empty after every daemon // restart. The symptom: `contracts` returned only the contracts of the // repo the user was actively editing, because IncrementalReindex skipped // extractContracts when no files were stale — and that was the only // code path writing to idx.contractRegistry. The fix persists each // repo's registry in the snapshot and warmup rehydrates on load. If // either side regresses, cross-repo contract queries silently go back // to returning a fraction of the data. // // Test strategy: two repos' worth of contracts get encoded, the // snapshot is decoded, and every contract must come back with its // RepoPrefix intact. Comparing against the live Contract values // (rather than a hand-rolled expected slice) catches any field drift // between the runtime struct and the wire form. func TestSnapshotRoundTrip_Contracts(t *testing.T) { dir := t.TempDir() t.Setenv("GORTEX_DAEMON_SNAPSHOT", filepath.Join(dir, "snap.gob.gz")) repoA := []contracts.Contract{ { ID: "http::GET::/api/users", Type: contracts.ContractHTTP, Role: contracts.RoleProvider, SymbolID: "repo-a/handler.go::ListUsers", FilePath: "repo-a/handler.go", Line: 42, RepoPrefix: "repo-a", Meta: map[string]any{"framework": "fiber", "method": "GET", "path": "/api/users"}, Confidence: 0.9, }, { ID: "grpc::Users::GetUser", Type: contracts.ContractGRPC, Role: contracts.RoleConsumer, SymbolID: "repo-a/client.go::UserClient.Get", FilePath: "repo-a/client.go", Line: 17, RepoPrefix: "repo-a", Meta: map[string]any{"service": "Users", "method": "GetUser", "lang": "go"}, Confidence: 0.95, }, } repoB := []contracts.Contract{ { ID: "http::POST::/api/users", Type: contracts.ContractHTTP, Role: contracts.RoleConsumer, SymbolID: "repo-b/client.ts::createUser", FilePath: "repo-b/client.ts", Line: 8, RepoPrefix: "repo-b", Meta: map[string]any{"framework": "fetch", "method": "POST", "path": "/api/users"}, Confidence: 0.7, }, } var wire []snapshotContract for _, c := range repoA { wire = append(wire, toSnapshotContract(c)) } for _, c := range repoB { wire = append(wire, toSnapshotContract(c)) } g := graph.New() saveSnapshot(g, nil, wire, snapshotVector{}, version, zap.NewNop()) restored := graph.New() result, err := loadSnapshot(restored, zap.NewNop()) require.NoError(t, err) require.True(t, result.Loaded) require.NotNil(t, result.Contracts) // Every contract must come back with its repo_prefix. Without the // fix, result.Contracts either wouldn't exist (no map) or would be // keyed under "" and conflate providers from different services. assert.Len(t, result.Contracts["repo-a"], len(repoA), "repo-a contracts must round-trip count-for-count") assert.Len(t, result.Contracts["repo-b"], len(repoB), "repo-b contracts must round-trip count-for-count") // Spot-check a field that used to be silently lost under the // "read from graph" fix we considered: confidence. If it's zero, // someone removed it from snapshotContract and the wire form is // no longer round-tripping. got := result.Contracts["repo-a"][0] assert.Equal(t, repoA[0].ID, got.ID) assert.Equal(t, repoA[0].Type, got.Type) assert.Equal(t, repoA[0].Role, got.Role) assert.Equal(t, repoA[0].Confidence, got.Confidence) assert.Equal(t, repoA[0].Meta["framework"], got.Meta["framework"]) assert.Equal(t, repoA[0].RepoPrefix, got.RepoPrefix) } // TestSnapshotRoundTrip_EmptyContractsCompat proves that a snapshot // saved with a nil contracts slice still decodes cleanly — the // `contracts` section is additive, so old-behaviour call sites (tests, // or a daemon that hasn't collected anything yet) must not produce a // corrupt or unreadable file. Without this guard, a nil slice plus a // positive ContractCount from any future refactor would read past the // stream and corrupt a subsequent snapshot load. func TestSnapshotRoundTrip_EmptyContractsCompat(t *testing.T) { dir := t.TempDir() t.Setenv("GORTEX_DAEMON_SNAPSHOT", filepath.Join(dir, "snap.gob.gz")) g := graph.New() g.AddNode(&graph.Node{ID: "a.go::Foo", Name: "Foo", Kind: graph.KindFunction, FilePath: "a.go"}) saveSnapshot(g, nil, nil, snapshotVector{}, version, zap.NewNop()) restored := graph.New() result, err := loadSnapshot(restored, zap.NewNop()) require.NoError(t, err) require.True(t, result.Loaded) require.NotNil(t, result.Contracts, "Contracts map must always be non-nil post-load") assert.Empty(t, result.Contracts) }