Files
zip/fiber_containment_test.go
zeekay 7afa7ee42b here: a co-resident caller runs the op, and MCP rides zap-proto/mcp
Two things that are the same thing: a call does not need a wire, only an
address, and a protocol layer belongs in the module that names it.

Ask names an app and reaches it over that app's canonical socket. When the app
is THIS process that socket is a loop -- the value the caller holds is
serialized, handed to the kernel, read back, parsed into a fresh copy, and
dispatched to a function pointer that was in memory the whole time. Nothing
about the call needed a wire; only the ADDRESSING did, and addressing is what a
by-name call already has. So a bound listener records which App answers for it,
Serving reads that back, and Here runs the op through the op's OWN invoke seam --
the same validate -> authorize -> run core a REST request, an MCP tools/call, a
CLI command and a socket call all land on. One fewer transport, not one more.

That is what a co-resident caller uses INSTEAD of building a request for its own
router to take apart again. The pattern it replaces cost hanzoai/cloud a
re-entrancy counter keyed on a goroutine id parsed out of runtime.Stack, because
dispatching a request into the whole app re-ran every edge middleware and a
middleware that read the app it was serving recursed without bound. A direct op
call cannot express that shape: it enters one op, not one router.

MCP now comes from github.com/zap-proto/mcp v1.0.5 rather than being hand-rolled
here, so it sits beside zap-proto/http as a protocol layer over ZAP instead of
inside the framework as a special case.

TestServePathAllocsAreChainDepthInvariant fails at 34 allocs/request against a
budget of 1. It fails identically at v1.26.0 -- verified in a clean worktree at
that tag -- so it is pre-existing and not this change. Named here rather than
left for the next person to rediscover.
2026-08-05 21:29:52 -07:00

213 lines
6.8 KiB
Go

package zip
import (
"go/ast"
"go/parser"
"go/token"
"os"
"path/filepath"
"strings"
"testing"
)
// fiberEscapeHatches are the ONLY exported functions allowed to name a fiber
// type. Both are documented escape hatches, and both hang off a CONCRETE type —
// [App] and [Ctx] — never off [Router].
//
// That distinction is the whole containment rule. Router is the type a decorator
// implements; requiring fiber there made the interface unimplementable outside
// this package, which is what stalled the v1.19 adoption in hanzoai/cloud and
// hanzoai/commerce. Wanting the router now forces you to hold the concrete value,
// which is a visible choice rather than a quiet widening of every Router in the
// estate.
var fiberEscapeHatches = map[string]bool{
"App.Fiber": true,
"Ctx.Fiber": true,
}
// TestFiberDoesNotLeakIntoTheExportedSurface reads this package's own AST and
// fails when an exported function signature names a fiber type outside the two
// sanctioned hatches.
//
// It is a test rather than a review habit because the leaks arrive one
// convenience at a time, and each looks reasonable alone. App.Test carried a
// fiber.TestConfig while its doc comment told callers to use it INSTEAD of
// reaching through App.Fiber — so a test that merely wanted a timeout imported
// the router this package exists to keep out of its callers, and the advice
// pointed back at the thing it was replacing.
func TestFiberDoesNotLeakIntoTheExportedSurface(t *testing.T) {
fset := token.NewFileSet()
entries, err := os.ReadDir(".")
if err != nil {
t.Fatalf("readdir: %v", err)
}
var leaks []string
for _, e := range entries {
name := e.Name()
if e.IsDir() || filepath.Ext(name) != ".go" || strings.HasSuffix(name, "_test.go") {
continue
}
f, err := parser.ParseFile(fset, name, nil, 0)
if err != nil {
t.Fatalf("parse %s: %v", name, err)
}
for _, decl := range f.Decls {
fn, ok := decl.(*ast.FuncDecl)
if !ok || !fn.Name.IsExported() {
continue
}
qualified := fn.Name.Name
if fn.Recv != nil && len(fn.Recv.List) == 1 {
recv := recvTypeName(fn.Recv.List[0].Type)
if !ast.IsExported(recv) {
continue // a method on an unexported type is not public surface
}
qualified = recv + "." + fn.Name.Name
}
if fiberEscapeHatches[qualified] {
continue
}
if sig := renderSignature(fset, fn); strings.Contains(sig, "fiber.") {
leaks = append(leaks, name+": "+qualified+sig)
}
}
}
if len(leaks) > 0 {
t.Errorf("fiber leaked into %d exported signature(s):\n %s\n\n"+
"Give zip its own type for what the caller actually needs (see TestConfig), or, if\n"+
"this genuinely is an escape hatch, hang it off a concrete type and add it to\n"+
"fiberEscapeHatches with the reason.", len(leaks), strings.Join(leaks, "\n "))
}
}
// recvTypeName is the identifier of a receiver type, pointer or not.
func recvTypeName(e ast.Expr) string {
switch t := e.(type) {
case *ast.StarExpr:
return recvTypeName(t.X)
case *ast.Ident:
return t.Name
case *ast.IndexExpr: // generic receiver
return recvTypeName(t.X)
}
return ""
}
// renderSignature flattens params and results to text — enough to spot a
// qualified fiber type without depending on a printer's formatting.
func renderSignature(fset *token.FileSet, fn *ast.FuncDecl) string {
var b strings.Builder
b.WriteString("(")
if fn.Type.Params != nil {
for i, p := range fn.Type.Params.List {
if i > 0 {
b.WriteString(", ")
}
b.WriteString(exprText(fset, p.Type))
}
}
b.WriteString(")")
if fn.Type.Results != nil {
b.WriteString(" ")
for i, r := range fn.Type.Results.List {
if i > 0 {
b.WriteString(", ")
}
b.WriteString(exprText(fset, r.Type))
}
}
return b.String()
}
func exprText(fset *token.FileSet, e ast.Expr) string {
switch t := e.(type) {
case *ast.SelectorExpr:
return exprText(fset, t.X) + "." + t.Sel.Name
case *ast.StarExpr:
return "*" + exprText(fset, t.X)
case *ast.Ellipsis:
return "..." + exprText(fset, t.Elt)
case *ast.ArrayType:
return "[]" + exprText(fset, t.Elt)
case *ast.MapType:
return "map[" + exprText(fset, t.Key) + "]" + exprText(fset, t.Value)
case *ast.Ident:
return t.Name
}
return ""
}
// dispatch is the path a call takes from a protocol to a handler: the MCP door
// and everything it reaches. None of it may name a fiber type — not in a
// signature, not in a body.
//
// This is the rule the inversion exists to make true, so it is the rule worth
// pinning. Before, mcpCall took a fiber.Ctx and read the caller's headers off
// it, which meant a tools/call could not happen unless an HTTP request already
// had: the transport sat UNDERNEATH the protocol. The regression that would
// undo it is not dramatic — it is one convenience, "just read fc.Get here" —
// and it would pass every behavioural test in this package, because the HTTP
// door would keep working. Only the ZAP door would quietly die, and only for
// whatever this one line needed.
var dispatch = map[string]bool{
"App.MCP": true, // the door itself
"App.tool": true, // tools/call
"App.answer": true, // one tool result
"App.list": true, // tools/list
"App.source": true, // the per-caller half
"App.ask": true, // an open plugin's half
"App.relay": true, // the hop to a plugin that owns the tool
}
func TestFiberIsNotInTheDispatchPath(t *testing.T) {
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, "mcp.go", nil, 0)
if err != nil {
t.Fatalf("parse mcp.go: %v", err)
}
seen := map[string]bool{}
var leaks []string
for _, decl := range f.Decls {
fn, ok := decl.(*ast.FuncDecl)
if !ok || fn.Recv == nil || len(fn.Recv.List) != 1 {
continue
}
name := recvTypeName(fn.Recv.List[0].Type) + "." + fn.Name.Name
if !dispatch[name] {
continue
}
seen[name] = true
// The WHOLE function, signature and body: a fiber type reached for
// inside a body is the same braiding as one in a parameter, and it is
// the one that arrives by accident.
ast.Inspect(fn, func(n ast.Node) bool {
sel, ok := n.(*ast.SelectorExpr)
if !ok {
return true
}
if pkg, ok := sel.X.(*ast.Ident); ok && pkg.Name == "fiber" {
leaks = append(leaks, name+" names fiber."+sel.Sel.Name+
" at "+fset.Position(sel.Pos()).String())
}
return true
})
}
for name := range dispatch {
if !seen[name] {
t.Errorf("%s is in the dispatch set but no longer exists in mcp.go — "+
"rename it here, or drop it if the path really did change shape", name)
}
}
if len(leaks) > 0 {
t.Errorf("fiber reached into the dispatch path %d time(s):\n %s\n\n"+
"The door takes a *zapmcp.Frame so it can be served over ZAP with no HTTP\n"+
"listener at all (see TestMCP_ServesOverZapWithNoHTTPListener). Whatever this\n"+
"needed from the request belongs on the ctx, put there by the ADAPTER that\n"+
"has one — see headerOf and callerContext.", len(leaks), strings.Join(leaks, "\n "))
}
}