Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

417 lines
14 KiB
Go

package resolver
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zzet/gortex/internal/graph"
"github.com/zzet/gortex/internal/parser/languages"
)
// buildMultiLangGraph extracts each fixture file with the extractor
// matching its extension (.go / .py / .ts / .js) and loads the result
// into a fresh graph. Unlike buildGraphFromSources (TS/JS only) this
// builder spans every ecosystem the external-call synthesis pass
// classifies, so one table can exercise Go modules, pip packages, and
// npm packages through the same real extract → resolve pipeline.
func buildMultiLangGraph(t *testing.T, files map[string]string) graph.Store {
t.Helper()
g := graph.New()
for path, src := range files {
var (
nodes []*graph.Node
edges []*graph.Edge
)
switch {
case strings.HasSuffix(path, ".go"):
r, err := languages.NewGoExtractor().Extract(path, []byte(src))
require.NoError(t, err, "go extract %s", path)
nodes, edges = r.Nodes, r.Edges
case strings.HasSuffix(path, ".py"):
r, err := languages.NewPythonExtractor().Extract(path, []byte(src))
require.NoError(t, err, "py extract %s", path)
nodes, edges = r.Nodes, r.Edges
case strings.HasSuffix(path, ".ts"), strings.HasSuffix(path, ".tsx"):
r, err := languages.NewTypeScriptExtractor().Extract(path, []byte(src))
require.NoError(t, err, "ts extract %s", path)
nodes, edges = r.Nodes, r.Edges
default:
r, err := languages.NewJavaScriptExtractor().Extract(path, []byte(src))
require.NoError(t, err, "js extract %s", path)
nodes, edges = r.Nodes, r.Edges
}
for _, n := range nodes {
g.AddNode(n)
}
for _, e := range edges {
g.AddEdge(e)
}
}
return g
}
// resolveAndSynthesize runs the production resolution pipeline against g
// — the per-edge ResolveAll pass plus the cross-package guard it ends
// with — and then the opt-in external-call synthesis pass. It mirrors
// the indexer settle point: synthesis runs strictly after resolution +
// guard, so the test exercises the same ordering the daemon uses.
func resolveAndSynthesize(g graph.Store, enabled bool) int {
New(g).ResolveAll()
return SynthesizeExternalCalls(g, enabled)
}
// callTargetsFrom collects the To-end of every call/reference edge
// leaving fromID, so a test can assert on the post-resolution shape of
// a caller's outbound calls.
func callTargetsFrom(g graph.Store, fromID string) []string {
var out []string
for _, e := range g.GetOutEdges(fromID) {
if e.Kind == graph.EdgeCalls || e.Kind == graph.EdgeReferences {
out = append(out, e.To)
}
}
return out
}
// TestSynthesizeExternalCalls drives the pass through a real
// extract → resolve → synthesize pipeline for each ecosystem. Every row
// fixes one caller's call into an un-indexed external target and
// asserts, with the option both on and off, what the call edge lands
// on — and that language built-ins / standard-library calls are
// filtered out as noise.
func TestSynthesizeExternalCalls(t *testing.T) {
cases := []struct {
name string
files map[string]string
// callerID identifies the function whose outbound call is
// under test.
callerID string
// wantSyntheticID, when set with the option ON, is the
// synthetic node ID the call edge must retarget onto.
wantSyntheticID string
// wantEcosystem / wantImportPath are asserted on the
// synthetic node's Meta when wantSyntheticID is set.
wantEcosystem string
wantImportPath string
// noiseOnly marks a fixture whose only external call is a
// language built-in / stdlib hop: the filter must synthesize
// nothing even with the option ON.
noiseOnly bool
}{
{
// An un-indexed npm package: `axios` is imported and
// called, but no axios source is in the graph. With the
// option on, the call must terminate on a synthetic node.
name: "un-indexed npm package call",
files: map[string]string{
"web/api.ts": `import axios from "axios";
export function fetchUser(): void {
axios.get("/user");
}`,
},
callerID: "web/api.ts::fetchUser",
wantSyntheticID: externalCallNodeID("stdlib", "axios"),
wantEcosystem: "stdlib",
wantImportPath: "axios",
},
{
// A sibling microservice's client SDK — a scoped package
// that is not part of this repo's index. The call into it
// must be preserved as an explicit external terminal.
name: "sibling-service client SDK call",
files: map[string]string{
"web/orders.ts": `import billing from "@acme/billing-service-client";
export function charge(): void {
billing.createInvoice();
}`,
},
callerID: "web/orders.ts::charge",
wantSyntheticID: externalCallNodeID("stdlib", "@acme/billing-service-client"),
wantEcosystem: "stdlib",
wantImportPath: "@acme/billing-service-client",
},
{
// An un-indexed Go third-party module — a domain-qualified
// import path, so the resolver lands it on a `dep::`
// terminal. The synthetic node must carry it through.
name: "un-indexed go module call",
files: map[string]string{
"svc/main.go": `package main
import "github.com/acme/stripe"
func Pay() {
stripe.New("key")
}`,
},
callerID: "svc/main.go::Pay",
wantSyntheticID: externalCallNodeID("dep", "github.com/acme/stripe"),
wantEcosystem: "dep",
wantImportPath: "github.com/acme/stripe",
},
{
// An un-indexed pip package: `requests` is imported and
// called. Not a stdlib module, so it must be synthesized.
name: "un-indexed pip package call",
files: map[string]string{
"app/client.py": `import requests
def fetch():
requests.get("/health")
`,
},
callerID: "app/client.py::fetch",
wantSyntheticID: externalCallNodeID("stdlib", "requests"),
wantEcosystem: "stdlib",
wantImportPath: "requests",
},
{
// Noise filter — Go standard library. Every Go file calls
// `fmt`; materialising a node for it would bury the real
// cross-system edges. Nothing must be synthesized.
name: "go stdlib call is filtered as noise",
files: map[string]string{
"svc/log.go": `package main
import "fmt"
func Log() {
fmt.Println("hello")
}`,
},
callerID: "svc/log.go::Log",
noiseOnly: true,
},
{
// Noise filter — Python standard library. `os.getenv` is
// an interpreter built-in, not a pip dependency.
name: "python stdlib call is filtered as noise",
files: map[string]string{
"app/env.py": `import os
def home():
os.getenv("HOME")
`,
},
callerID: "app/env.py::home",
noiseOnly: true,
},
{
// Noise filter — a Node.js core module. `node:fs` is part
// of the runtime, not an npm package.
name: "node core module call is filtered as noise",
files: map[string]string{
"web/disk.ts": `import fs from "node:fs";
export function read(): void {
fs.readFileSync("/etc/hosts");
}`,
},
callerID: "web/disk.ts::read",
noiseOnly: true,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
// Option OFF (the default): the synthesis pass is a pure
// no-op — the resolved graph is left exactly as resolution
// produced it, with no synthetic node and no retargeted
// edge. Snapshot the counts after ResolveAll (which itself
// mutates the graph) so the assertion isolates the pass.
gOff := buildMultiLangGraph(t, tc.files)
New(gOff).ResolveAll()
nodesAfterResolve := gOff.NodeCount()
edgesAfterResolve := gOff.EdgeCount()
synthesizedOff := SynthesizeExternalCalls(gOff, false)
assert.Zero(t, synthesizedOff, "option OFF must synthesize nothing")
assert.Equal(t, nodesAfterResolve, gOff.NodeCount(),
"option OFF must not add any node")
assert.Equal(t, edgesAfterResolve, gOff.EdgeCount(),
"option OFF must not add any edge")
for _, to := range callTargetsFrom(gOff, tc.callerID) {
assert.NotContains(t, to, externalCallPrefix,
"option OFF must leave call edges off synthetic nodes")
}
// Option ON: genuine externals get a synthetic terminal;
// noise (builtin / stdlib) is still filtered out.
gOn := buildMultiLangGraph(t, tc.files)
synthesizedOn := resolveAndSynthesize(gOn, true)
if tc.noiseOnly {
assert.Zero(t, synthesizedOn,
"noise filter must exclude builtin/stdlib calls")
for _, to := range callTargetsFrom(gOn, tc.callerID) {
assert.NotContains(t, to, externalCallPrefix,
"a builtin/stdlib call must not gain a synthetic node")
}
return
}
require.Positive(t, synthesizedOn,
"a genuine external call must be synthesized with the option ON")
// The synthetic node exists and is marked unmistakably as
// both synthetic and external so analyzers can filter it.
node := gOn.GetNode(tc.wantSyntheticID)
require.NotNil(t, node, "synthetic external node must be added")
assert.Equal(t, graph.KindModule, node.Kind)
assert.Equal(t, tc.wantImportPath, node.Name)
assert.Equal(t, true, node.Meta["synthetic"])
assert.Equal(t, true, node.Meta["external_call"])
assert.Equal(t, tc.wantEcosystem, node.Meta["ecosystem"])
assert.Equal(t, tc.wantImportPath, node.Meta["import_path"])
// The call edge was retargeted onto the synthetic node and
// the synthetic node sees the inbound call edge — so a
// call-chain walk reaches the external terminal.
targets := callTargetsFrom(gOn, tc.callerID)
assert.Contains(t, targets, tc.wantSyntheticID,
"the call edge must retarget onto the synthetic node")
require.NotEmpty(t, gOn.GetInEdges(tc.wantSyntheticID),
"synthetic node must see the inbound call edge")
// The retargeted edge is marked so a query can pick out the
// cross-system terminals this pass created.
edge := firstOutEdgeByKind(gOn, tc.callerID, graph.EdgeCalls)
require.NotNil(t, edge)
assert.Equal(t, tc.wantSyntheticID, edge.To)
assert.Equal(t, true, edge.Meta["external_call"])
assert.Equal(t, graph.OriginTextMatched, edge.Origin)
})
}
}
// TestSynthesizeExternalCalls_Idempotent pins the full-recompute
// contract: re-running the pass rewrites every edge onto the same
// deterministic synthetic node and accretes no duplicate node or edge.
func TestSynthesizeExternalCalls_Idempotent(t *testing.T) {
files := map[string]string{
"web/api.ts": `import axios from "axios";
export function fetchUser(): void {
axios.get("/user");
}`,
}
g := buildMultiLangGraph(t, files)
New(g).ResolveAll()
first := SynthesizeExternalCalls(g, true)
nodesAfterFirst := g.NodeCount()
second := SynthesizeExternalCalls(g, true)
third := SynthesizeExternalCalls(g, true)
assert.Equal(t, 1, first)
assert.Equal(t, first, second, "re-run must report the same count")
assert.Equal(t, first, third)
assert.Equal(t, nodesAfterFirst, g.NodeCount(),
"re-run must not add a duplicate synthetic node")
syntheticID := externalCallNodeID("stdlib", "axios")
require.Len(t, g.GetInEdges(syntheticID), 1,
"re-run must not accrete a duplicate inbound edge")
}
// TestSynthesizeExternalCalls_DisabledByDefault guards the
// zero-config contract: a graph carrying an un-indexed external call
// is left completely untouched when the option is off — the default.
func TestSynthesizeExternalCalls_DisabledByDefault(t *testing.T) {
files := map[string]string{
"app/client.py": `import requests
def fetch():
requests.get("/health")
`,
}
g := buildMultiLangGraph(t, files)
New(g).ResolveAll()
nodesBefore := g.NodeCount()
edgesBefore := g.EdgeCount()
synthesized := SynthesizeExternalCalls(g, false)
assert.Zero(t, synthesized)
assert.Equal(t, nodesBefore, g.NodeCount(), "default-off must add no node")
assert.Equal(t, edgesBefore, g.EdgeCount(), "default-off must add no edge")
}
// TestParseExternalCallTarget unit-tests the terminal classifier: the
// three external bookkeeping-string shapes yield the right ecosystem +
// import path, while real node IDs, bare placeholders, `builtin::`
// terminals, and already-synthesised nodes are rejected.
func TestParseExternalCallTarget(t *testing.T) {
cases := []struct {
target string
wantOK bool
wantEcosystem string
wantPath string
}{
{"dep::github.com/foo/bar::Baz", true, "dep", "github.com/foo/bar"},
{"stdlib::axios::get", true, "stdlib", "axios"},
{"stdlib::@acme/svc-client::call", true, "stdlib", "@acme/svc-client"},
{"external::lodash", true, "external", "lodash"},
// Rejected: real node IDs and non-external placeholders.
{"pkg/foo.go::Bar", false, "", ""},
{"unresolved::Foo", false, "", ""},
{"unresolved::*.foo", false, "", ""},
{"builtin::js::array::push", false, "", ""},
{externalCallPrefix + "dep::x", false, "", ""},
// `dep::` / `stdlib::` with no `::<symbol>` separator is malformed.
{"dep::", false, "", ""},
{"stdlib::axios", false, "", ""},
{"external::", false, "", ""},
}
for _, tc := range cases {
t.Run(tc.target, func(t *testing.T) {
eco, path, ok := parseExternalCallTarget(tc.target)
assert.Equal(t, tc.wantOK, ok)
assert.Equal(t, tc.wantEcosystem, eco)
assert.Equal(t, tc.wantPath, path)
})
}
}
// TestIsLanguageStdlib unit-tests the language-aware noise filter: the
// same import-path shape is stdlib or third-party depending on the
// caller's language.
func TestIsLanguageStdlib(t *testing.T) {
cases := []struct {
lang string
path string
want bool
}{
// Go: un-dotted first segment is stdlib; domain-led is a module.
{"go", "fmt", true},
{"go", "net/http", true},
{"go", "encoding/json", true},
{"go", "github.com/stripe/stripe-go", false},
{"go", "gopkg.in/yaml.v3", false},
// Python: interpreter modules vs pip packages.
{"python", "os", true},
{"python", "os.path", true},
{"python", "collections", true},
{"python", "requests", false},
{"python", "numpy", false},
// Node: core modules (bare and node:-prefixed) vs npm packages.
{"javascript", "fs", true},
{"typescript", "node:crypto", true},
{"typescript", "stream/promises", true},
{"typescript", "axios", false},
{"typescript", "@acme/billing-client", false},
// Rust: the std distribution vs crates.
{"rust", "std", true},
{"rust", "core::mem", true},
{"rust", "tokio", false},
// Unknown language: treat the path as external so a real edge
// is never dropped by a missing rule.
{"", "anything", false},
}
for _, tc := range cases {
t.Run(tc.lang+":"+tc.path, func(t *testing.T) {
assert.Equal(t, tc.want, isLanguageStdlib(tc.lang, tc.path))
})
}
}