a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
133 lines
4.9 KiB
Go
133 lines
4.9 KiB
Go
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)
|
|
}
|