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

228 lines
7.6 KiB
Go

package contracts
import "testing"
// Topic-pairing tests covering the matcher behaviour the indexer's
// ReconcileContractEdges pass relies on: same-(broker,topic) pairs
// match, different-broker pairs don't, multi-consumer fanout produces
// one CrossLink per consumer, and the workspace boundary keeps
// unrelated repos from pairing. The actual EdgeProducesTopic /
// EdgeConsumesTopic emission is covered by indexer-level tests
// (TestReconcileContractEdges_TopicEdges*), but those depend on
// matcher correctness — these tests pin the contract behaviour the
// indexer assumes.
// TestTopicMatch_BasicPair verifies that a single Kafka producer +
// single Kafka consumer for the same topic pair into one CrossLink
// with no orphans. WorkspaceID and ProjectID are shared so neither
// the workspace nor the project boundary intervenes — pairing is
// purely on the broker-tagged Contract.ID.
func TestTopicMatch_BasicPair(t *testing.T) {
id := "topic::kafka::user.created"
reg := NewRegistry()
reg.Add(Contract{
ID: id,
Type: ContractTopic,
Role: RoleProvider,
SymbolID: "producer/pub.go::publish",
FilePath: "pub.go",
RepoPrefix: "producer",
WorkspaceID: "shared",
ProjectID: "shared",
Meta: map[string]any{"broker": "kafka", "topic": "user.created"},
})
reg.Add(Contract{
ID: id,
Type: ContractTopic,
Role: RoleConsumer,
SymbolID: "consumer/sub.go::consume",
FilePath: "sub.go",
RepoPrefix: "consumer",
WorkspaceID: "shared",
ProjectID: "shared",
Meta: map[string]any{"broker": "kafka", "topic": "user.created"},
})
res := Match(reg)
if len(res.Matched) != 1 {
t.Fatalf("expected 1 matched pair, got %d", len(res.Matched))
}
if len(res.OrphanProviders) != 0 || len(res.OrphanConsumers) != 0 {
t.Errorf("expected zero orphans, got providers=%d consumers=%d", len(res.OrphanProviders), len(res.OrphanConsumers))
}
m := res.Matched[0]
if !m.CrossRepo {
t.Errorf("expected CrossRepo=true (different repos in same workspace)")
}
if m.Provider.SymbolID != "producer/pub.go::publish" {
t.Errorf("unexpected provider symbol: %q", m.Provider.SymbolID)
}
if m.Consumer.SymbolID != "consumer/sub.go::consume" {
t.Errorf("unexpected consumer symbol: %q", m.Consumer.SymbolID)
}
}
// TestTopicMatch_CrossBrokerIsolation verifies that a topic named
// "foo" on Kafka and a topic named "foo" on NATS do not pair: their
// Contract.IDs differ in the broker segment so the matcher's bucket
// keys keep them apart.
func TestTopicMatch_CrossBrokerIsolation(t *testing.T) {
reg := NewRegistry()
reg.Add(Contract{
ID: "topic::kafka::foo",
Type: ContractTopic,
Role: RoleProvider,
SymbolID: "svc-a/pub.go::publish",
RepoPrefix: "svc-a",
WorkspaceID: "shared",
ProjectID: "shared",
Meta: map[string]any{"broker": "kafka", "topic": "foo"},
})
reg.Add(Contract{
ID: "topic::nats::foo",
Type: ContractTopic,
Role: RoleConsumer,
SymbolID: "svc-b/sub.go::consume",
RepoPrefix: "svc-b",
WorkspaceID: "shared",
ProjectID: "shared",
Meta: map[string]any{"broker": "nats", "topic": "foo"},
})
res := Match(reg)
if len(res.Matched) != 0 {
t.Errorf("expected zero matched pairs (cross-broker), got %d", len(res.Matched))
}
if len(res.OrphanProviders) != 1 || len(res.OrphanConsumers) != 1 {
t.Errorf("expected 1 orphan provider + 1 orphan consumer, got %d / %d",
len(res.OrphanProviders), len(res.OrphanConsumers))
}
}
// TestTopicMatch_MultiConsumerFanout verifies that one producer with
// three consumers on the same topic yields three CrossLinks (one
// per consumer).
func TestTopicMatch_MultiConsumerFanout(t *testing.T) {
reg := NewRegistry()
id := "topic::kafka::orders.created"
reg.Add(Contract{
ID: id,
Type: ContractTopic,
Role: RoleProvider,
SymbolID: "producer/pub.go::publish",
RepoPrefix: "producer",
WorkspaceID: "shared",
ProjectID: "shared",
Meta: map[string]any{"broker": "kafka", "topic": "orders.created"},
})
for i, name := range []string{"a", "b", "c"} {
_ = i
reg.Add(Contract{
ID: id,
Type: ContractTopic,
Role: RoleConsumer,
SymbolID: "consumer-" + name + "/sub.go::consume",
RepoPrefix: "consumer-" + name,
WorkspaceID: "shared",
ProjectID: "shared",
Meta: map[string]any{"broker": "kafka", "topic": "orders.created"},
})
}
res := Match(reg)
if len(res.Matched) != 3 {
t.Fatalf("expected 3 matched pairs (fanout), got %d", len(res.Matched))
}
if len(res.OrphanProviders) != 0 || len(res.OrphanConsumers) != 0 {
t.Errorf("expected zero orphans, got providers=%d consumers=%d",
len(res.OrphanProviders), len(res.OrphanConsumers))
}
gotConsumers := make(map[string]bool)
for _, m := range res.Matched {
gotConsumers[m.Consumer.SymbolID] = true
if m.Provider.SymbolID != "producer/pub.go::publish" {
t.Errorf("matched pair has wrong provider: %q", m.Provider.SymbolID)
}
}
for _, name := range []string{"a", "b", "c"} {
want := "consumer-" + name + "/sub.go::consume"
if !gotConsumers[want] {
t.Errorf("expected matched consumer %q to appear", want)
}
}
}
// TestTopicMatch_CrossWorkspaceIsolation verifies that a producer in
// workspace A and a consumer in workspace B for the same topic do
// not pair. The matcher's bucket key includes EffectiveWorkspace,
// so different workspaces yield different buckets and each side
// becomes an orphan.
func TestTopicMatch_CrossWorkspaceIsolation(t *testing.T) {
reg := NewRegistry()
id := "topic::kafka::user.created"
reg.Add(Contract{
ID: id,
Type: ContractTopic,
Role: RoleProvider,
SymbolID: "ws-a/pub.go::publish",
RepoPrefix: "ws-a",
WorkspaceID: "workspace-a",
ProjectID: "shared",
Meta: map[string]any{"broker": "kafka", "topic": "user.created"},
})
reg.Add(Contract{
ID: id,
Type: ContractTopic,
Role: RoleConsumer,
SymbolID: "ws-b/sub.go::consume",
RepoPrefix: "ws-b",
WorkspaceID: "workspace-b",
ProjectID: "shared",
Meta: map[string]any{"broker": "kafka", "topic": "user.created"},
})
res := Match(reg)
if len(res.Matched) != 0 {
t.Errorf("expected zero matched pairs (cross-workspace), got %d", len(res.Matched))
}
if len(res.OrphanProviders) != 1 || len(res.OrphanConsumers) != 1 {
t.Errorf("expected 1 orphan provider + 1 orphan consumer, got %d / %d",
len(res.OrphanProviders), len(res.OrphanConsumers))
}
}
// TestTopicMatch_DynamicTopicNoEmission verifies the
// upstream-extractor behaviour the matcher depends on: a callsite
// whose topic argument is a variable produces no contract, so the
// matcher never sees one. This is the contract-pairer side of the
// dynamic-topic cases tested per-broker in topics_brokers_test.go.
func TestTopicMatch_DynamicTopicNoEmission(t *testing.T) {
ext := &TopicExtractor{}
src := []byte(`
package main
func publish(p *kafka.Producer, name string) {
p.Produce(name, nil)
}
func consume(p *kafka.Producer, name string) {
p.Subscribe(name, nil)
}
`)
got := ext.Extract("dyn.go", src, nil, nil)
if len(got) != 0 {
t.Fatalf("expected 0 contracts from dynamic-topic source, got %d (%+v)",
len(got), got)
}
// And confirm the matcher pipeline behaves correctly when fed
// nothing: an empty registry yields zero matches and zero
// orphans.
reg := NewRegistry()
res := Match(reg)
if len(res.Matched) != 0 || len(res.OrphanProviders) != 0 || len(res.OrphanConsumers) != 0 {
t.Errorf("expected an empty result, got matched=%d providers=%d consumers=%d",
len(res.Matched), len(res.OrphanProviders), len(res.OrphanConsumers))
}
}