Files
zzet--gortex/internal/contracts/bodyfacts_bugfixtures_test.go
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

235 lines
7.6 KiB
Go

package contracts
// Regression fixtures for the four bug classes called out in
// specs/spec-contract-extraction.md §1. Each test reproduces a
// real-world failure of the regex enricher and confirms the AST
// overlay produces the expected Meta values.
//
// The fixtures don't run the indexer or the route-detection regex —
// they construct a Contract directly and call EnrichHTTPContractWithTree
// with a tree parsed from the fixture source. This isolates the
// AST overlay's behaviour from the rest of the contract pipeline.
import (
"strings"
"testing"
"github.com/zzet/gortex/internal/graph"
"github.com/zzet/gortex/internal/parser"
gosrc "github.com/zzet/gortex/internal/parser/tsitter/golang"
)
// fixtureContract is the shared scaffold: parse src, build a fake
// Contract whose handler matches the function at handlerLine, run
// EnrichHTTPContractWithTree, return the contract.
func fixtureContract(t *testing.T, src string, handlerName string, handlerLine, handlerEndLine int) Contract {
t.Helper()
tree, err := parser.ParseFile([]byte(src), gosrc.GetLanguage())
if err != nil {
t.Fatalf("parse: %v", err)
}
pt := parser.NewParseTree(tree, []byte(src), "go")
t.Cleanup(func() { pt.Release() })
handlerNode := &graph.Node{
ID: "h.go::" + handlerName,
Name: handlerName,
Kind: graph.KindFunction,
FilePath: "h.go",
StartLine: handlerLine,
EndLine: handlerEndLine,
Language: "go",
Meta: map[string]any{},
}
c := Contract{
ID: "http::GET::/test",
Type: ContractHTTP,
Role: RoleProvider,
SymbolID: handlerNode.ID,
FilePath: "h.go",
Line: handlerLine,
Meta: map[string]any{
"path": "/test",
"method": "GET",
},
}
lines := strings.Split(src, "\n")
EnrichHTTPContractWithTree(&c, lines, []*graph.Node{handlerNode}, "go", pt)
return c
}
// Bug 1 — Fiber uppercase verbs leak into HTTP path.
//
// The fiber-route regex matched `.GET(` inside the contract package's
// own httpPrefilterMarkers source (which has `[]byte(".GET(")` byte
// markers). The route-detection regex is in http.go and outside
// BodyFacts' scope (Phase 2); this fixture documents the bug and
// shows that the AST overlay doesn't make it worse.
//
// Phase 1 doesn't fully fix this bug — it's a route-registration
// problem (Phase 2) — but we keep the fixture as a regression
// boundary. When Phase 2 lands, this test should be tightened to
// assert no contracts are produced for the marker source.
func TestBugFixture_FiberUppercaseVerbsRouteRegistration(t *testing.T) {
// Phase 1 scope is body-content enrichment, not route detection.
// This test exists to document the bug class for Phase 2; it
// passes today because we're not asserting the route is rejected,
// only that body enrichment doesn't crash on the pathological
// path.
src := `package h
func H(w, r) {
WriteJSON(w, http.StatusOK, "ok")
}
`
c := fixtureContract(t, src, "H", 3, 5)
if c.Meta["response_type"] != "string" {
t.Errorf("response_type: want string, got %v", c.Meta["response_type"])
}
}
// Bug 2 — handler_trail / handler_ident leak to wire.
//
// The legacy regex enricher writes Meta["handler_trail"] and
// Meta["handler_ident"] as scratch state for cross-file resolution.
// The cleanup defer in resolveProviderHandlers wipes them, but if
// the post-pass never runs (e.g. the contract resolves on the
// initial pass), they leak to the dashboard.
//
// The AST overlay never writes these keys — they're internal scratch
// state for the legacy regex pipeline only. This test verifies that
// after the AST overlay runs (with a body it can fully resolve),
// those keys are absent.
func TestBugFixture_NoHandlerTrailLeak(t *testing.T) {
src := `package h
type User struct {
Name string
}
func ListUsers(w, r) {
users := []User{{Name: "alice"}}
WriteJSON(w, http.StatusOK, users)
}
`
c := fixtureContract(t, src, "ListUsers", 7, 10)
if _, has := c.Meta["handler_trail"]; has {
t.Errorf("handler_trail leaked: %v", c.Meta["handler_trail"])
}
if _, has := c.Meta["handler_ident"]; has {
t.Errorf("handler_ident leaked: %v", c.Meta["handler_ident"])
}
}
// Bug 3 — `[]any{` envelope truncation.
//
// The legacy splitMapLiteralBody regex truncated `"flags": []any{...}`
// at the first `}` it saw, producing `"expr": "[]any{"` in the
// envelope row. The AST walks composite_literal children correctly
// regardless of nested braces.
func TestBugFixture_NoEnvelopeTruncation(t *testing.T) {
src := `package h
func H(w, r) {
workspaces := svc.Workspaces()
repos := svc.Repos()
WriteJSON(w, http.StatusOK, map[string]any{
"workspaces": workspaces,
"repos": repos,
"flags": []any{true, false, true},
"meta": map[string]int{"a": 1, "b": 2},
})
}
`
c := fixtureContract(t, src, "H", 3, 12)
envRaw, ok := c.Meta["response_envelope"].([]map[string]any)
if !ok {
t.Fatalf("response_envelope: want []map[string]any, got %T", c.Meta["response_envelope"])
}
if len(envRaw) != 4 {
t.Fatalf("envelope rows: want 4, got %d", len(envRaw))
}
flags := envRaw[2]
if flags["name"] != "flags" {
t.Errorf("flags row name: want flags, got %v", flags["name"])
}
if flags["expr"] != "[]any{true, false, true}" {
t.Errorf("flags row expr: want full slice literal, got %q", flags["expr"])
}
if !strings.Contains(flags["expr"].(string), "}") {
t.Errorf("flags row expr missing closing brace: %q", flags["expr"])
}
}
// Bug 4 — `WriteJSON(w, http.StatusOK, result)` leaks raw helper
// expression as response_expr.
//
// The legacy regex enricher couldn't recover `result`'s type without
// graph-aware tracing, so it stored the entire helper-call text as
// response_expr. The AST identifies the bare ident `result` and the
// post-pass traces it (via BodyFacts.VarBinding's CallExpr field)
// to the binding call's return type.
//
// This test asserts the AST overlay produces a clean response_expr
// (just `result`, not the full helper call) so the post-pass's
// isLikelyIdentifier branch matches.
func TestBugFixture_NoRawHelperResponseExpr(t *testing.T) {
src := `package h
func ListRepos(w, r) {
result, err := h.svc.GetRepos()
if err != nil {
WriteJSON(w, http.StatusInternalServerError, err)
return
}
WriteJSON(w, http.StatusOK, result)
}
`
c := fixtureContract(t, src, "ListRepos", 3, 10)
expr, _ := c.Meta["response_expr"].(string)
if expr != "" && !isPlainIdent(expr) {
t.Errorf("response_expr leaked helper text: %q (want bare ident or empty)", expr)
}
// An empty expr is also fine: the AST may have already resolved the
// response type from a binding (composite/literal). We only assert
// that nothing leaked.
}
// Bug 4b — bare struct response from variable binding.
//
// `WriteJSON(w, code, user)` where `user` is bound to `User{...}` —
// the AST should resolve response_type = User without a graph walk.
func TestBugFixture_VarBoundCompositeResolves(t *testing.T) {
src := `package h
func GetUser(w, r) {
user := User{Name: "x"}
WriteJSON(w, http.StatusOK, user)
}
`
c := fixtureContract(t, src, "GetUser", 3, 6)
rt, _ := c.Meta["response_type"].(string)
if rt != "User" {
t.Errorf("response_type: want User, got %q", rt)
}
}
// Bug 4c — slice literal response from variable binding.
func TestBugFixture_VarBoundSliceLitResolves(t *testing.T) {
src := `package h
func ListUsers(w, r) {
users := []User{{Name: "alice"}, {Name: "bob"}}
WriteJSON(w, http.StatusOK, users)
}
`
c := fixtureContract(t, src, "ListUsers", 3, 6)
rt, _ := c.Meta["response_type"].(string)
if rt != "User" {
t.Errorf("response_type: want User, got %q", rt)
}
if r, _ := c.Meta["response_repeated"].(bool); !r {
t.Errorf("response_repeated: want true, got false")
}
}