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
235 lines
7.6 KiB
Go
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")
|
|
}
|
|
}
|