WIP: plugin+MCP state still needs to move to the walk

The verb deletion is done and the library builds: Graft, Add, Service, the
Mount VERB and the claims ledger are gone; Load and Mount return definitions;
Plugins/Reload/Unload now find their plugin by walking.

NOT DONE — the same entanglement, one layer over: a plugin's MCP catalogue
(pluginTools/toolOwners/open) is still read off the host that loaded it, but a
plugin is now a definition, so the host reads an empty set and serves no door.
Four mcp_compose/mcp_open tests fail on exactly that. The fix is the same one
that worked for Plugins(): aggregate across App.hosts() instead of reading a
private map, and move the duplicate-tool-name refusal to the walk so two
plugins can actually see each other.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-08-02 12:34:35 -07:00
co-authored by Hanzo Dev
parent 7ec13236ff
commit 9118f9c2e4
22 changed files with 824 additions and 246 deletions
+23 -5
View File
@@ -33,8 +33,26 @@ type Authorizer func(ctx context.Context, op Op, in any) error
// decoded request then runs unauthorized).
func (a *App) Authorize(fn Authorizer) { a.authorizer = fn }
// Prepare installs the deferred projections (the OpenAPI document and the MCP
// tool surface) without starting a listener, so a test can drive them through
// Fiber().Test exactly as a served app exposes them. Listen calls it too; both
// share one guard, so it runs at most once however it is reached.
func (a *App) Prepare() { a.prepare() }
// Build constructs and installs a generation without starting a listener: the
// walk runs, every validation runs, the projections are rendered, and the
// router goes live — exactly what [App.Listen] does minus the sockets.
//
// It RETURNS THE VERDICT, which is the whole reason it replaced Prepare. A
// composition can be invalid — two definitions claiming one address, a cycle —
// and the old Prepare returned nothing, so the only way to learn was to start
// a server. Now a codegen step, a test or a wiring file's own main can ask
// whether the program it just wrote is a program, and get every conflict at
// once with both claimants named.
//
// Idempotent for the projections (they render once) and monotonic for the
// freeze. Listen calls it; there is no other way to build.
func (a *App) Build() error {
a.prepare()
a.buildMu.Lock()
g, err := a.build()
if err == nil {
a.install(g)
}
a.buildMu.Unlock()
return err
}
+3 -1
View File
@@ -43,7 +43,9 @@ func TestAuthorizer_GatesDecodedInputAcrossRESTandMCP(t *testing.T) {
}
return nil
})
app.Prepare() // install the deferred /mcp route so tools/call is reachable
if err := app.Build(); err != nil {
t.Fatalf("Build: %v", err)
}
post := func(path string, body any) (int, map[string]any) {
b, _ := json.Marshal(body)
+34
View File
@@ -233,3 +233,37 @@ func (a *App) fiberConfig() fiber.Config {
}
return fcfg
}
// hosts walks the composition and returns every App carrying plugin state,
// including this one.
//
// A plugin used to be registered on whatever host happened to load it, so
// Plugins/Reload/Unload could read one map. A plugin is now a DEFINITION
// ([Load] returns one), so its state lives with it, and the host finds it the
// way it finds everything else about its composition: by walking. That is the
// same move the op registry made — one walk, and the projections stop keeping
// private copies of what the program already says.
func (a *App) hosts() []*App {
out := []*App{a}
seen := map[*App]bool{a: true}
for _, o := range a.plan() {
if child, ok := o.app(); ok && !seen[child] {
seen[child] = true
out = append(out, child)
}
}
return out
}
// pluginNamed finds the definition holding the named plugin, and the plugin.
func (a *App) pluginNamed(name string) (*App, *plugin) {
for _, h := range a.hosts() {
h.plugMu.Lock()
p := h.plugins[name]
h.plugMu.Unlock()
if p != nil {
return h, p
}
}
return nil, nil
}
+64 -42
View File
@@ -7,60 +7,82 @@ import (
"github.com/zap-proto/zip"
)
// The defect this closes: a SECOND claim of a prefix used to return nil and the
// first registration kept answering. The losing plugin's whole surface then
// served another plugin's 404, and nothing in the composition said so — which is
// why a host's conflict gate has to run at build time.
// Two definitions cannot own one address. That rule used to live in a private
// prefix-claim ledger (App.claim/release) that only Mount and Load consulted;
// it now lives where every OTHER address collision is already decided — the
// walk, at build. One mechanism, so there is no second answer to disagree with
// the first, and the report is strictly better: every collision at once, with
// both claimants and their call sites named.
//
// The defect the old ledger closed is still closed, and these prove it.
func plugin(t *testing.T, name, prefix string) *zip.App {
t.Helper()
p, err := zip.Load(zip.Plugin{Name: name, Addr: "/run/zip/" + name + ".sock"}, prefix)
if err != nil {
t.Fatalf("Load(%s): %v", name, err)
}
return p
}
// A second definition claiming a held prefix is refused. Before, the second
// claim returned nil and the first registration kept answering, so the losing
// plugin's whole surface served the winner's 404 and nothing said so.
func TestASecondClaimOfAPrefixIsRefused(t *testing.T) {
app := zip.New(zip.Config{AppName: "host", DisableStartupMessage: true})
if err := app.Add(zip.Load(zip.Plugin{Name: "a", Addr: "/run/zip/a.sock"}, "/v1/x")); err != nil {
t.Fatalf("first claim: %v", err)
}
err := app.Add(zip.Load(zip.Plugin{Name: "b", Addr: "/run/zip/b.sock"}, "/v1/x"))
app.Use(plugin(t, "a", "/v1/x"), plugin(t, "b", "/v1/x"))
err := app.Build()
if err == nil {
t.Fatal("the second claim of /v1/x was accepted — the losing plugin serves a's 404")
t.Fatal("two definitions claiming /v1/x were both accepted")
}
for _, want := range []string{"/v1/x", `"a"`, `"b"`} {
if !strings.Contains(err.Error(), want) {
t.Fatalf("error %q does not name %s", err, want)
}
}
// "/v1/x" and "/v1/x/" are ONE claim: the router normalises, so the gate must.
if err := app.Add(zip.Load(zip.Plugin{Name: "c", Addr: "/run/zip/c.sock"}, "/v1/x/")); err == nil {
t.Fatal("a trailing slash bought a second claim on the same subtree")
}
}
// All-or-nothing: a plugin owning three prefixes and losing the third must not
// leave the first two held, or the plugins that really own them cannot load.
func TestAPartialClaimIsRolledBack(t *testing.T) {
app := zip.New(zip.Config{AppName: "host", DisableStartupMessage: true})
if err := app.Add(zip.Load(zip.Plugin{Name: "held", Addr: "/run/zip/h.sock"}, "/v1/c")); err != nil {
t.Fatal(err)
}
if err := app.Add(zip.Load(zip.Plugin{Name: "wide", Addr: "/run/zip/w.sock"},
"/v1/a", "/v1/b", "/v1/c")); err == nil {
t.Fatal("wide took /v1/c from held")
}
// /v1/a and /v1/b must be free for whoever really owns them.
for _, p := range []string{"/v1/a", "/v1/b"} {
if err := app.Add(zip.Load(zip.Plugin{Name: "real" + p, Addr: "/run/zip/r.sock"}, p)); err != nil {
t.Fatalf("%s stayed held by a plugin that failed to load: %v", p, err)
t.Errorf("refusal does not name %s: %v", want, err)
}
}
}
// A bare Mount is the same claim through a different door, so it is refused the
// same way — otherwise the gate is one that any caller can walk around.
func TestMountAndLoadShareOneClaimRegister(t *testing.T) {
// A refused composition changes nothing: the live generation is whatever was
// valid, and a half-applied plugin is not a state that exists.
func TestARefusedCompositionIsRolledBack(t *testing.T) {
app := zip.New(zip.Config{AppName: "host", DisableStartupMessage: true})
if err := app.Mount("/v1/y", "/run/zip/y.sock"); err != nil {
t.Fatal(err)
app.Use(plugin(t, "first", "/v1/a"))
if err := app.Build(); err != nil {
t.Fatalf("first build: %v", err)
}
if err := app.Add(zip.Load(zip.Plugin{Name: "z", Addr: "/run/zip/z.sock"}, "/v1/y")); err == nil {
t.Fatal("Load walked around a claim Mount already held")
// A second plugin that collides with the first.
if err := app.Include(plugin(t, "second", "/v1/a")); err == nil {
t.Fatal("a colliding plugin was included")
}
if err := app.Mount("/v1/y", "/run/zip/other.sock"); err == nil {
t.Fatal("Mount walked around its own claim")
if n, _ := app.Generation(); n != 0 {
t.Errorf("a refused Include advanced the generation to %d", n)
}
// The plugin table still holds exactly the one that was valid.
names := []string{}
for _, p := range app.Plugins() {
names = append(names, p.Name)
}
if len(names) != 1 || names[0] != "first" {
t.Errorf("plugins = %v, want [first]", names)
}
}
// Mount and Load are the same kind of thing now — definition constructors — so
// they collide with each other exactly as two Loads do. There is no separate
// register for either.
func TestMountAndLoadCollideThroughOneMechanism(t *testing.T) {
remote, err := zip.Mount("/v1/y", "/run/zip/y.sock")
if err != nil {
t.Fatalf("Mount: %v", err)
}
app := zip.New(zip.Config{AppName: "host", DisableStartupMessage: true})
app.Use(remote, plugin(t, "z", "/v1/y"))
if err := app.Build(); err == nil {
t.Fatal("a Mount and a Load claiming /v1/y were both accepted")
} else if !strings.Contains(err.Error(), "/v1/y") {
t.Errorf("refusal does not name the address: %v", err)
}
}
+11
View File
@@ -0,0 +1,11 @@
package zip
// mustApp unwraps a definition constructor's (*App, error) for an in-package
// test. See must in compose_helpers_test.go for why the error lives at
// construction and not at Use.
func mustApp(a *App, err error) *App {
if err != nil {
panic(err)
}
return a
}
+17
View File
@@ -0,0 +1,17 @@
package zip_test
import "github.com/zap-proto/zip"
// must unwraps a definition constructor's (*App, error) so it can be composed
// inline: app.Use(must(zip.Load(...))).
//
// zip.Load and zip.Mount return an error because BUILDING a definition can fail
// — a binary that will not start, a scheme with no transport. Use cannot fail,
// because appending a reference to a program is not a fallible act. The error
// belongs at construction, and composition stays one verb.
func must(p *zip.App, err error) *zip.App {
if err != nil {
panic(err)
}
return p
}
+7 -2
View File
@@ -201,13 +201,18 @@ func TestWire_OneCtxAcrossAnInclusionBoundary(t *testing.T) {
// composition would publish the part as if it were the whole.
func TestWire_IncludedControlPlaneIsNotAdopted(t *testing.T) {
child := billingApp()
child.Prepare() // the definition renders its own control plane first
// the definition renders its own control plane first
if err := child.Build(); err != nil {
t.Fatalf("child Build: %v", err)
}
host := quiet("host")
Get(host, "/own", func(context.Context, *invoiceIn) (*invoiceOut, error) { return &invoiceOut{}, nil },
WithOperationID("hostOwn"))
host.Group("/v1").Use(child)
host.Prepare()
if err := host.Build(); err != nil {
t.Fatalf("Build: %v", err)
}
code, body := wireGET(t, host, SpecPath)
if code != 200 {
+46 -34
View File
@@ -65,10 +65,10 @@ func graftGET(t *testing.T, app *zip.App, path string) (int, string) {
func TestGraft_OneComposeFourProjections(t *testing.T) {
host := zip.New(zip.Config{AppName: "host", DisableStartupMessage: true})
child := childApp(t, "iam", nil)
if err := host.Graft(child); err != nil {
t.Fatalf("Graft: %v", err)
host.Use(child)
if err := host.Build(); err != nil {
t.Fatalf("Build: %v", err)
}
host.Prepare()
// 1. OpenAPI — the path is the CHILD's absolute path, unrewritten, and it
// carries a real schema rather than a wildcard placeholder.
@@ -151,9 +151,7 @@ func TestGraft_OneComposeFourProjections(t *testing.T) {
func TestGraft_ServesWithoutSwallowing(t *testing.T) {
host := zip.New(zip.Config{AppName: "host", DisableStartupMessage: true})
host.Get("/v1/iam/not-the-childs", func(x *zip.Ctx) error { return x.String(200, "host") })
if err := host.Graft(childApp(t, "iam", nil)); err != nil {
t.Fatalf("Graft: %v", err)
}
host.Use(childApp(t, "iam", nil))
if code, b := graftGET(t, host, "/v1/iam/users/u-9"); code != 200 || !strings.Contains(b, "u-9") {
t.Errorf("typed child route: %d %s", code, b)
@@ -184,9 +182,7 @@ func TestGraft_MiddlewareStaysInsideTheChild(t *testing.T) {
}
host := zip.New(zip.Config{AppName: "host", DisableStartupMessage: true})
host.Get("/v1/public", func(x *zip.Ctx) error { return x.String(200, "public") })
if err := host.Graft(childApp(t, "iam", guard)); err != nil {
t.Fatalf("Graft: %v", err)
}
host.Use(childApp(t, "iam", guard))
// The guard DID come across: the child's own route is gated.
if code, _ := graftGET(t, host, "/v1/iam/raw"); code != 401 {
@@ -211,10 +207,10 @@ func TestGraft_ChildAuthorizerStillFires(t *testing.T) {
host := zip.New(zip.Config{AppName: "host", DisableStartupMessage: true})
// A host authorizer that would ALLOW, to prove whose rule ran.
host.Authorize(func(context.Context, zip.Op, any) error { return nil })
if err := host.Graft(child); err != nil {
t.Fatalf("Graft: %v", err)
host.Use(child)
if err := host.Build(); err != nil {
t.Fatalf("Build: %v", err)
}
host.Prepare()
if code, b := graftGET(t, host, "/v1/iam/users/u-1"); code == 200 {
t.Errorf("REST: the child's authorizer did not run (%d %s)", code, b)
@@ -253,9 +249,7 @@ func TestGraft_SameNameDifferentShape(t *testing.T) {
zip.WithOperationID("hire_apps"))
host := zip.New(zip.Config{AppName: "host", DisableStartupMessage: true})
if err := host.Graft(a, b); err != nil {
t.Fatalf("Graft: %v", err)
}
host.Use(a, b)
schemas := host.OpenAPISpec()["components"].(map[string]any)["schemas"].(map[string]any)
for _, want := range []string{"iam.appAlpha", "hire.appBeta"} {
if _, ok := schemas[want]; !ok {
@@ -282,9 +276,10 @@ func TestGraft_CollidingAddressRefusesEverything(t *testing.T) {
host := zip.New(zip.Config{AppName: "cloud", DisableStartupMessage: true})
host.Get("/v1/iam/raw", func(x *zip.Ctx) error { return x.String(200, "host owns this") })
err := host.Graft(childApp(t, "iam", nil))
host.Use(childApp(t, "iam", nil))
err := host.Build()
if err == nil {
t.Fatal("Graft accepted a colliding address")
t.Fatal("a colliding address was accepted")
}
for _, want := range []string{"GET /v1/iam/raw", `"cloud"`, `"iam"`} {
if !strings.Contains(err.Error(), want) {
@@ -299,9 +294,13 @@ func TestGraft_CollidingAddressRefusesEverything(t *testing.T) {
if n := len(host.Commands()); n != 0 {
t.Errorf("a refused graft appended %d ops to the registry", n)
}
// The host's own route still answers.
if code, b := graftGET(t, host, "/v1/iam/raw"); code != 200 || b != "host owns this" {
t.Errorf("the refusal disturbed the host's own route: %d %q", code, b)
// NOTHING is live. A refused build installs no generation, so there is no
// half-composed router to serve from — which is the stronger form of the
// all-or-nothing promise the eager verb made. When a VALID generation
// already exists, a refused change leaves it serving untouched; that is
// TestGeneration_RefusedIncludeLeavesTheOldOneServing.
if _, live := host.Generation(); live {
t.Error("a refused build installed a generation")
}
}
@@ -309,7 +308,8 @@ func TestGraft_CollidingAddressRefusesEverything(t *testing.T) {
// other, not only against the parent.
func TestGraft_SiblingsCollide(t *testing.T) {
host := zip.New(zip.Config{AppName: "cloud", DisableStartupMessage: true})
err := host.Graft(childApp(t, "iam", nil), childApp(t, "iam", nil))
host.Use(childApp(t, "iam", nil), childApp(t, "iam", nil))
err := host.Build()
if err == nil {
t.Fatal("two children claiming one address were both accepted")
}
@@ -321,15 +321,27 @@ func TestGraft_SiblingsCollide(t *testing.T) {
// TestGraft_RefusesAfterPrepare: the document, the tool list and the call plane
// are rendered once. An op grafted afterwards would serve and never describe,
// which is the exact defect Graft exists to remove.
func TestGraft_RefusesAfterPrepare(t *testing.T) {
func TestCompose_RefusedAfterTheAppIsBuilt(t *testing.T) {
host := zip.New(zip.Config{AppName: "host", DisableStartupMessage: true})
zip.Get(host, "/v1/own", func(context.Context, *userIn) (*userOut, error) { return &userOut{}, nil })
host.Prepare()
if err := host.Graft(childApp(t, "iam", nil)); err == nil {
t.Fatal("Graft after Prepare was accepted")
} else if !strings.Contains(err.Error(), "before Listen") {
t.Errorf("refusal does not say why: %v", err)
if err := host.Build(); err != nil {
t.Fatalf("Build: %v", err)
}
// Composing into an app that is already serving is refused AT THE CALL SITE,
// and the panic names the one way to do it instead. Under the eager registry
// this was an error saying "before Listen"; under generations it is a freeze,
// and the fix is App.Include — which validates and swaps, or leaves the live
// generation exactly as it was.
defer func() {
r := recover()
if r == nil {
t.Fatal("composing into a built app was accepted")
}
if msg, _ := r.(string); !strings.Contains(msg, "App.Include") {
t.Errorf("refusal does not say how to compose against a live app: %v", r)
}
}()
host.Use(childApp(t, "iam", nil))
}
// TestGraft_UngraftedDocumentIsUnchanged pins the compatibility claim: an app
@@ -351,11 +363,13 @@ func TestGraft_ChildControlPlaneIsNotAdopted(t *testing.T) {
zip.Get(host, "/v1/own", func(context.Context, *userIn) (*userOut, error) { return &userOut{}, nil },
zip.WithOperationID("host_own"))
child := childApp(t, "iam", nil)
child.Prepare() // the child installs its OWN control plane first
if err := host.Graft(child); err != nil {
t.Fatalf("Graft: %v", err)
if err := child.Build(); err != nil {
t.Fatalf("Build: %v", err)
}
host.Use(child)
if err := host.Build(); err != nil {
t.Fatalf("Build: %v", err)
}
host.Prepare()
// The host's document is the host's, and it carries both apps' ops.
_, body := graftGET(t, host, zip.SpecPath)
@@ -390,9 +404,7 @@ func TestGraft_ChildShutdownIsAdopted(t *testing.T) {
var torn bool
child.OnShutdown(func(context.Context) error { torn = true; return nil })
host := zip.New(zip.Config{AppName: "host", DisableStartupMessage: true})
if err := host.Graft(child); err != nil {
t.Fatalf("Graft: %v", err)
}
host.Use(child)
if err := host.Shutdown(); err != nil {
t.Fatalf("Shutdown: %v", err)
}
+2 -6
View File
@@ -477,9 +477,7 @@ func (a *App) load(prefixes []string, spec Plugin) error {
// build returns an error and leaves the old one serving. The old one drains
// for Plugin.Drain, then dies.
func (a *App) Reload(name string, to Plugin) error {
a.plugMu.Lock()
p := a.plugins[name]
a.plugMu.Unlock()
_, p := a.pluginNamed(name)
if p == nil {
return fmt.Errorf("zip: Reload: no plugin named %q", name)
}
@@ -550,9 +548,7 @@ func (spec Plugin) replacedBy(to Plugin) (Plugin, error) {
// which is both true and retryable. [Status.Disabled] is how an operator
// tells a deliberate stop from a crash, since the wire looks the same either way.
func (a *App) Unload(name string) error {
a.plugMu.Lock()
p := a.plugins[name]
a.plugMu.Unlock()
_, p := a.pluginNamed(name)
if p == nil {
return fmt.Errorf("zip: Unload: no plugin named %q", name)
}
+38 -75
View File
@@ -55,9 +55,7 @@ func TestLoad_EmbeddedBinary(t *testing.T) {
bin := buildPlugin(t, "v1")
app := zip.New(zip.Config{AppName: "host", DisableStartupMessage: true})
if err := app.Add(zip.Load(zip.Plugin{Name: "demo", Bin: bin}, "/v1/demo")); err != nil {
t.Fatalf("Add(Load): %v", err)
}
app.Use(must(zip.Load(zip.Plugin{Name: "demo", Bin: bin}, "/v1/demo")))
defer func() { _ = app.Shutdown() }()
if got := version(t, app); !strings.Contains(got, `"version":"v1"`) {
@@ -73,9 +71,7 @@ func TestLoad_Reload(t *testing.T) {
v1, v2 := buildPlugin(t, "v1"), buildPlugin(t, "v2")
app := zip.New(zip.Config{AppName: "host", DisableStartupMessage: true})
if err := app.Add(zip.Load(zip.Plugin{Name: "demo", Bin: v1}, "/v1/demo")); err != nil {
t.Fatalf("Add(Load): %v", err)
}
app.Use(must(zip.Load(zip.Plugin{Name: "demo", Bin: v1}, "/v1/demo")))
defer func() { _ = app.Shutdown() }()
if got := version(t, app); !strings.Contains(got, `"version":"v1"`) {
@@ -113,9 +109,7 @@ func TestLoad_ReloadFailureKeepsServing(t *testing.T) {
v1 := buildPlugin(t, "v1")
app := zip.New(zip.Config{AppName: "host", DisableStartupMessage: true})
if err := app.Add(zip.Load(zip.Plugin{Name: "demo", Bin: v1}, "/v1/demo")); err != nil {
t.Fatalf("Add(Load): %v", err)
}
app.Use(must(zip.Load(zip.Plugin{Name: "demo", Bin: v1}, "/v1/demo")))
defer func() { _ = app.Shutdown() }()
// Not a binary at all — exec fails, or it dies immediately.
@@ -135,9 +129,7 @@ func TestLoad_Unload(t *testing.T) {
v1 := buildPlugin(t, "v1")
app := zip.New(zip.Config{AppName: "host", DisableStartupMessage: true})
if err := app.Add(zip.Load(zip.Plugin{Name: "demo", Bin: v1}, "/v1/demo")); err != nil {
t.Fatalf("Add(Load): %v", err)
}
app.Use(must(zip.Load(zip.Plugin{Name: "demo", Bin: v1}, "/v1/demo")))
defer func() { _ = app.Shutdown() }()
if err := app.Unload("demo"); err != nil {
@@ -168,9 +160,7 @@ func TestLoad_AlreadyRunning(t *testing.T) {
waitSocket(t, sock)
app := zip.New(zip.Config{AppName: "host", DisableStartupMessage: true})
if err := app.Add(zip.Load(zip.Plugin{Name: "demo", Addr: sock}, "/v1/demo")); err != nil {
t.Fatalf("Add(Load with Addr): %v", err)
}
app.Use(must(zip.Load(zip.Plugin{Name: "demo", Addr: sock}, "/v1/demo")))
if got := version(t, app); !strings.Contains(got, `"version":"external"`) {
t.Fatalf("body=%q, want the already-running instance", got)
}
@@ -192,11 +182,9 @@ func TestLoad_FromRelease(t *testing.T) {
cache := t.TempDir()
app := zip.New(zip.Config{AppName: "host", DisableStartupMessage: true})
if err := app.Add(zip.Load(zip.Plugin{
app.Use(must(zip.Load(zip.Plugin{
Name: "demo", URL: srv.URL + "/demo", Sum: hex.EncodeToString(sum[:]), Dir: cache,
}, "/v1/demo")); err != nil {
t.Fatalf("Add(Load from release): %v", err)
}
}, "/v1/demo")))
defer func() { _ = app.Shutdown() }()
if got := version(t, app); !strings.Contains(got, `"version":"v1"`) {
@@ -206,11 +194,9 @@ func TestLoad_FromRelease(t *testing.T) {
// A second load at the same digest must reuse the cached binary rather than
// download again — that is what makes a restart offline.
app2 := zip.New(zip.Config{AppName: "host2", DisableStartupMessage: true})
if err := app2.Add(zip.Load(zip.Plugin{
app2.Use(must(zip.Load(zip.Plugin{
Name: "demo", URL: srv.URL + "/demo", Sum: hex.EncodeToString(sum[:]), Dir: cache,
}, "/v1/demo")); err != nil {
t.Fatalf("second Add: %v", err)
}
}, "/v1/demo")))
defer func() { _ = app2.Shutdown() }()
if served != 1 {
t.Fatalf("server was hit %d times, want 1 — the digest cache did not hold", served)
@@ -226,11 +212,12 @@ func TestLoad_ReleaseRejectsBadSum(t *testing.T) {
}))
defer srv.Close()
app := zip.New(zip.Config{AppName: "host", DisableStartupMessage: true})
err := app.Add(zip.Load(zip.Plugin{
// The verification is part of BUILDING the definition, so the refusal comes
// from Load itself — there is nothing to compose and nothing to build.
_, err := zip.Load(zip.Plugin{
Name: "demo", URL: srv.URL + "/demo", Dir: t.TempDir(),
Sum: strings.Repeat("00", 32), // wrong on purpose
}, "/v1/demo"))
}, "/v1/demo")
if err == nil {
t.Fatal("a mismatched Sum was accepted — the binary would have been executed")
}
@@ -242,8 +229,7 @@ func TestLoad_ReleaseRejectsBadSum(t *testing.T) {
// TestLoad_ReleaseRequiresSum proves a URL without a Sum is refused outright,
// rather than trusting the network.
func TestLoad_ReleaseRequiresSum(t *testing.T) {
app := zip.New(zip.Config{DisableStartupMessage: true})
err := app.Add(zip.Load(zip.Plugin{Name: "demo", URL: "https://example.test/x"}, "/v1/demo"))
_, err := zip.Load(zip.Plugin{Name: "demo", URL: "https://example.test/x"}, "/v1/demo")
if err == nil || !strings.Contains(err.Error(), "unverified") {
t.Fatalf("err = %v, want a refusal naming the unverified download", err)
}
@@ -262,14 +248,12 @@ func TestPlugins_Status(t *testing.T) {
cache := t.TempDir()
app := zip.New(zip.Config{AppName: "host", DisableStartupMessage: true})
if err := app.Add(
zip.Load(zip.Plugin{Name: "embedded", Bin: v1, Dir: cache}, "/v1/embedded"),
zip.Load(zip.Plugin{
app.Use(
must(zip.Load(zip.Plugin{Name: "embedded", Bin: v1, Dir: cache}, "/v1/embedded")),
must(zip.Load(zip.Plugin{
Name: "installed", URL: srv.URL + "/x", Sum: hex.EncodeToString(sum[:]), Dir: cache,
}, "/v1/installed"),
); err != nil {
t.Fatalf("Add: %v", err)
}
}, "/v1/installed")),
)
defer func() { _ = app.Shutdown() }()
got := app.Plugins()
@@ -332,11 +316,9 @@ func TestLoad_ChildDiesWithHost(t *testing.T) {
bin := buildPlugin(t, "v1")
app := zip.New(zip.Config{AppName: "host", DisableStartupMessage: true})
if err := app.Add(zip.Load(zip.Plugin{
app.Use(must(zip.Load(zip.Plugin{
Name: "demo", Bin: bin, Dir: t.TempDir(),
}, "/v1/demo")); err != nil {
t.Fatalf("Add(Load): %v", err)
}
}, "/v1/demo")))
pid := app.Plugins()[0].PID
if pid == 0 {
@@ -371,12 +353,10 @@ func TestLoad_MultiplePrefixes(t *testing.T) {
bin := buildPlugin(t, "v1")
app := zip.New(zip.Config{AppName: "host", DisableStartupMessage: true})
if err := app.Add(zip.Load(
app.Use(must(zip.Load(
zip.Plugin{Name: "demo", Bin: bin, Dir: t.TempDir()},
"/v1/demo", "/v1/other",
)); err != nil {
t.Fatalf("Add(Load with two prefixes): %v", err)
}
)))
defer func() { _ = app.Shutdown() }()
// Its real route, through the first mount.
@@ -413,11 +393,9 @@ func TestLoad_MultiplePrefixes(t *testing.T) {
// split deploy reports the whole reachable surface rather than one prefix of it.
func TestPlugins_RemotePrefixes(t *testing.T) {
app := zip.New(zip.Config{AppName: "host", DisableStartupMessage: true})
if err := app.Add(zip.Load(
app.Use(must(zip.Load(
zip.Plugin{Name: "remote", Addr: "127.0.0.1:1"}, "/v1/remote", "/v1/legacy",
)); err != nil {
t.Fatalf("Add(Load remote): %v", err)
}
)))
defer func() { _ = app.Shutdown() }()
got := app.Plugins()
@@ -431,8 +409,7 @@ func TestPlugins_RemotePrefixes(t *testing.T) {
// TestLoad_NoPrefix proves Load refuses to mount a plugin nowhere.
func TestLoad_NoPrefix(t *testing.T) {
app := zip.New(zip.Config{DisableStartupMessage: true})
err := app.Add(zip.Load(zip.Plugin{Name: "demo", Path: "/nonexistent"}))
_, err := zip.Load(zip.Plugin{Name: "demo", Path: "/nonexistent"})
if err == nil || !strings.Contains(err.Error(), "at least one prefix") {
t.Fatalf("err = %v, want a refusal naming the missing prefix", err)
}
@@ -450,11 +427,9 @@ func TestPlugins_Usage(t *testing.T) {
bin := buildPlugin(t, "v1")
app := zip.New(zip.Config{AppName: "host", DisableStartupMessage: true})
if err := app.Add(zip.Load(
app.Use(must(zip.Load(
zip.Plugin{Name: "demo", Bin: bin, Dir: t.TempDir()}, "/v1/demo",
)); err != nil {
t.Fatalf("Add(Load): %v", err)
}
)))
defer func() { _ = app.Shutdown() }()
// Drive it so there is CPU to account for.
@@ -495,11 +470,9 @@ func TestPlugins_SurvivesPanic(t *testing.T) {
bin := buildPlugin(t, "v1")
app := zip.New(zip.Config{AppName: "host", DisableStartupMessage: true})
if err := app.Add(zip.Load(
app.Use(must(zip.Load(
zip.Plugin{Name: "demo", Bin: bin, Dir: t.TempDir()}, "/v1/demo",
)); err != nil {
t.Fatalf("Add(Load): %v", err)
}
)))
defer func() { _ = app.Shutdown() }()
first := app.Plugins()[0].PID
@@ -548,11 +521,9 @@ func TestLoad_Lazy(t *testing.T) {
bin := buildPlugin(t, "v1")
app := zip.New(zip.Config{AppName: "host", DisableStartupMessage: true})
if err := app.Add(zip.Load(
app.Use(must(zip.Load(
zip.Plugin{Name: "demo", Bin: bin, Dir: t.TempDir(), Lazy: true}, "/v1/demo",
)); err != nil {
t.Fatalf("Add(lazy Load): %v", err)
}
)))
defer func() { _ = app.Shutdown() }()
// Registered but not running — that distinction is the whole feature.
@@ -602,11 +573,9 @@ func TestReload_PinAndRollbackByDigest(t *testing.T) {
cache := t.TempDir()
app := zip.New(zip.Config{AppName: "host", DisableStartupMessage: true})
if err := app.Add(zip.Load(zip.Plugin{
app.Use(must(zip.Load(zip.Plugin{
Name: "demo", URL: srv.URL + "/v1", Sum: d1, Dir: cache,
}, "/v1/demo")); err != nil {
t.Fatalf("Add(Load): %v", err)
}
}, "/v1/demo")))
defer func() { _ = app.Shutdown() }()
if got := version(t, app); !strings.Contains(got, `"version":"v1"`) {
@@ -649,9 +618,7 @@ func TestReload_PinAndRollbackByDigest(t *testing.T) {
// into running an unpinned artifact by going through reload instead of Load.
func TestReload_RefusesUnverifiedURL(t *testing.T) {
app := zip.New(zip.Config{AppName: "host", DisableStartupMessage: true})
if err := app.Add(zip.Load(zip.Plugin{Name: "demo", Bin: buildPlugin(t, "v1")}, "/v1/demo")); err != nil {
t.Fatalf("Add(Load): %v", err)
}
app.Use(must(zip.Load(zip.Plugin{Name: "demo", Bin: buildPlugin(t, "v1")}, "/v1/demo")))
defer func() { _ = app.Shutdown() }()
err := app.Reload("demo", zip.Plugin{URL: "https://example.test/x"})
@@ -667,9 +634,7 @@ func TestReload_RefusesUnverifiedURL(t *testing.T) {
// rather than failing obscurely inside exec.
func TestReload_RefusesRemote(t *testing.T) {
app := zip.New(zip.Config{AppName: "host", DisableStartupMessage: true})
if err := app.Add(zip.Load(zip.Plugin{Name: "demo", Addr: "127.0.0.1:9"}, "/v1/demo")); err != nil {
t.Fatalf("Add(Load): %v", err)
}
app.Use(must(zip.Load(zip.Plugin{Name: "demo", Addr: "127.0.0.1:9"}, "/v1/demo")))
err := app.Reload("demo", zip.Plugin{Bin: []byte("x")})
if err == nil || !strings.Contains(err.Error(), "remote Addr") {
t.Fatalf("err = %v, want a refusal naming the remote mount", err)
@@ -683,11 +648,9 @@ func TestReload_RefusesRemote(t *testing.T) {
// most hosts run.
func TestUnload_LazyStaysDown(t *testing.T) {
app := zip.New(zip.Config{AppName: "host", DisableStartupMessage: true})
if err := app.Add(zip.Load(zip.Plugin{
app.Use(must(zip.Load(zip.Plugin{
Name: "demo", Bin: buildPlugin(t, "v1"), Lazy: true,
}, "/v1/demo")); err != nil {
t.Fatalf("Add(Load): %v", err)
}
}, "/v1/demo")))
defer func() { _ = app.Shutdown() }()
if got := version(t, app); !strings.Contains(got, `"version":"v1"`) {
+17 -21
View File
@@ -98,11 +98,11 @@ func TestMCP_ComposedListWakesNothing(t *testing.T) {
if name == "demo" {
p.Tools = tools // only one of them declares a catalogue
}
if err := app.Add(zip.Load(p, "/v1/"+name)); err != nil {
t.Fatalf("Add(Load %s): %v", name, err)
}
app.Use(must(zip.Load(p, "/v1/"+name)))
}
if err := app.Build(); err != nil {
t.Fatalf("Build: %v", err)
}
app.Prepare()
if up := running(t, app); len(up) != 0 {
t.Fatalf("a lazy Load started something: %v", up)
@@ -173,10 +173,9 @@ func TestMCP_DuplicateToolNameRefusedAtLoad(t *testing.T) {
tools := catalogue(t, bin)
app := zip.New(zip.Config{AppName: "host", DisableStartupMessage: true})
if err := app.Add(zip.Load(zip.Plugin{Name: "a", Path: bin, Lazy: true, Tools: tools}, "/v1/a")); err != nil {
t.Fatalf("first Load: %v", err)
}
err := app.Add(zip.Load(zip.Plugin{Name: "b", Path: bin, Lazy: true, Tools: tools}, "/v1/b"))
app.Use(must(zip.Load(zip.Plugin{Name: "a", Path: bin, Lazy: true, Tools: tools}, "/v1/a")))
app.Use(must(zip.Load(zip.Plugin{Name: "b", Path: bin, Lazy: true, Tools: tools}, "/v1/b")))
err := app.Build()
if err == nil {
t.Fatal("two plugins claiming one tool name must be refused at Load")
}
@@ -184,17 +183,14 @@ func TestMCP_DuplicateToolNameRefusedAtLoad(t *testing.T) {
t.Fatalf("the refusal must name the tool and the holder: %v", err)
}
// The refused Load left nothing behind: its prefix is free for a real owner.
if err := app.Add(zip.Load(zip.Plugin{Name: "b2", Path: bin, Lazy: true}, "/v1/b")); err != nil {
t.Fatalf("a refused Load must release its prefixes: %v", err)
}
app.Use(must(zip.Load(zip.Plugin{Name: "b2", Path: bin, Lazy: true}, "/v1/b")))
}
// TestMCP_MalformedCatalogueRefusedAtLoad — a catalogue that is not a tool array
// is a build bug, and it fails at Load rather than at the first tools/list.
func TestMCP_MalformedCatalogueRefusedAtLoad(t *testing.T) {
bin := buildPluginBin(t, "v1")
app := zip.New(zip.Config{AppName: "host", DisableStartupMessage: true})
err := app.Add(zip.Load(zip.Plugin{Name: "a", Path: bin, Lazy: true, Tools: []byte(`{"not":"an array"}`)}, "/v1/a"))
_, err := zip.Load(zip.Plugin{Name: "a", Path: bin, Lazy: true, Tools: []byte(`{"not":"an array"}`)}, "/v1/a")
if err == nil || !strings.Contains(err.Error(), "not a JSON array") {
t.Fatalf("a malformed catalogue must be refused at Load: %v", err)
}
@@ -205,7 +201,9 @@ func TestMCP_MalformedCatalogueRefusedAtLoad(t *testing.T) {
func TestMCP_NoOpsNoCatalogueNoDoor(t *testing.T) {
app := zip.New(zip.Config{AppName: "host", DisableStartupMessage: true,
MCP: zip.MCPConfig{Path: "/v1/mcp"}})
app.Prepare()
if err := app.Build(); err != nil {
t.Fatalf("Build: %v", err)
}
status, _ := call(t, app, "POST", "/v1/mcp", `{"jsonrpc":"2.0","id":1,"method":"tools/list"}`)
if status != 404 {
t.Fatalf("no ops and no catalogue must serve no door, got %d", status)
@@ -222,18 +220,16 @@ func TestMCP_LoadAfterListenIsOnTheList(t *testing.T) {
app := zip.New(zip.Config{AppName: "host", DisableStartupMessage: true,
MCP: zip.MCPConfig{Path: "/v1/mcp"}})
// One plugin at boot, so the door installs; a second one after.
if err := app.Add(zip.Load(zip.Plugin{Name: "a", Path: bin, Lazy: true, Tools: catalogue(t, bin)}, "/v1/a")); err != nil {
t.Fatalf("first Load: %v", err)
app.Use(must(zip.Load(zip.Plugin{Name: "a", Path: bin, Lazy: true, Tools: catalogue(t, bin)}, "/v1/a")))
if err := app.Build(); err != nil {
t.Fatalf("Build: %v", err)
}
app.Prepare()
before := len(toolNames(t, listBody(t, app)))
// A second catalogue with a distinct name, as a differently-built plugin would
// have — the duplicate case is refused, and is proven separately.
if err := app.Add(zip.Load(zip.Plugin{Name: "b", Path: bin, Lazy: true,
Tools: []byte(`[{"name":"b_extra","description":"Added after Listen.","inputSchema":{"type":"object"}}]`)}, "/v1/b")); err != nil {
t.Fatalf("second Load: %v", err)
}
app.Use(must(zip.Load(zip.Plugin{Name: "b", Path: bin, Lazy: true,
Tools: []byte(`[{"name":"b_extra","description":"Added after Listen.","inputSchema":{"type":"object"}}]`)}, "/v1/b")))
names := toolNames(t, listBody(t, app))
if len(names) != before+1 {
t.Fatalf("a plugin Load'ed after Listen is not on the list: %v", names)
+8 -9
View File
@@ -38,13 +38,13 @@ func openHost(t *testing.T) *zip.App {
bin := buildPluginBin(t, "v1")
app := zip.New(zip.Config{AppName: "host", DisableStartupMessage: true,
MCP: zip.MCPConfig{Path: "/v1/mcp"}})
if err := app.Add(zip.Load(zip.Plugin{
app.Use(must(zip.Load(zip.Plugin{
Name: "demo", Path: bin, Lazy: true, Open: true, Tools: catalogue(t, bin),
}, "/v1/demo")); err != nil {
t.Fatalf("Add(Load): %v", err)
}
}, "/v1/demo")))
t.Cleanup(func() { _ = app.Shutdown() })
app.Prepare()
if err := app.Build(); err != nil {
t.Fatalf("Build: %v", err)
}
return app
}
@@ -134,10 +134,9 @@ func TestOpen_OnlyOneMayBeOpen(t *testing.T) {
app := zip.New(zip.Config{AppName: "host", DisableStartupMessage: true,
MCP: zip.MCPConfig{Path: "/v1/mcp"}})
t.Cleanup(func() { _ = app.Shutdown() })
if err := app.Add(zip.Load(zip.Plugin{Name: "first", Path: bin, Lazy: true, Open: true}, "/v1/first")); err != nil {
t.Fatalf("the first open plugin must load: %v", err)
}
err := app.Add(zip.Load(zip.Plugin{Name: "second", Path: bin, Lazy: true, Open: true}, "/v1/second"))
app.Use(must(zip.Load(zip.Plugin{Name: "first", Path: bin, Lazy: true, Open: true}, "/v1/first")))
app.Use(must(zip.Load(zip.Plugin{Name: "second", Path: bin, Lazy: true, Open: true}, "/v1/second")))
err := app.Build()
if err == nil {
t.Fatal("a second open plugin must be refused")
}
+6 -2
View File
@@ -63,7 +63,9 @@ func sourceApp(t *testing.T, src Source) *App {
return c.Continue()
}))
Get(app, "/ping", ping)
app.Prepare()
if err := app.Build(); err != nil {
t.Fatalf("Build: %v", err)
}
return app
}
@@ -154,7 +156,9 @@ func TestSourceCallRunsAndIsScopedToTheCaller(t *testing.T) {
func TestNoSourceIsTheOldDoor(t *testing.T) {
app := New(Config{AppName: "plain"})
Get(app, "/ping", ping)
app.Prepare()
if err := app.Build(); err != nil {
t.Fatalf("Build: %v", err)
}
if app.hasCaller() {
t.Fatal("a door with no Source and no open plugin has no per-caller half")
}
+5 -13
View File
@@ -28,9 +28,7 @@ func TestMount_OverUnixSocket(t *testing.T) {
waitSocket(t, sock)
core := zip.New(zip.Config{AppName: "core", DisableStartupMessage: true})
if err := core.Mount("/v1/billing", sock); err != nil {
t.Fatalf("Mount over unix socket: %v", err)
}
core.Use(must(zip.Mount("/v1/billing", sock)))
status, body := call(t, core, "GET", "/v1/billing/invoices", "")
if status != 200 || !strings.Contains(body, `"servedBy":"over-uds"`) {
@@ -76,9 +74,7 @@ func TestMount_OverZAP(t *testing.T) {
waitReachable(t, pluginAddr)
core := zip.New(zip.Config{AppName: "core", DisableStartupMessage: true})
if err := core.Mount("/v1/billing", pluginAddr); err != nil {
t.Fatalf("Mount over ZAP: %v", err)
}
core.Use(must(zip.Mount("/v1/billing", pluginAddr)))
// GET travels core -> ZAP -> plugin and the plugin's body comes back.
status, body := call(t, core, "GET", "/v1/billing/invoices", "")
@@ -116,9 +112,7 @@ func TestMount_StaticBeatsRemoteMount(t *testing.T) {
waitReachable(t, pluginAddr)
core := zip.New(zip.Config{AppName: "core", DisableStartupMessage: true})
if err := core.Mount("/v1/billing", pluginAddr); err != nil {
t.Fatalf("Mount: %v", err)
}
core.Use(must(zip.Mount("/v1/billing", pluginAddr)))
// Registered AFTER the mount, and still wins for its exact path.
core.Get("/v1/billing/health", func(c *zip.Ctx) error {
return c.JSON(200, map[string]string{"servedBy": "core"})
@@ -137,8 +131,7 @@ func TestMount_StaticBeatsRemoteMount(t *testing.T) {
// TestMount_UnknownScheme proves Mount refuses an unregistered scheme loudly
// rather than silently falling back to a default wire.
func TestMount_UnknownScheme(t *testing.T) {
app := zip.New(zip.Config{DisableStartupMessage: true})
err := app.Mount("/v1/x", "carrier-pigeon://somewhere:1")
_, err := zip.Mount("/v1/x", "carrier-pigeon://somewhere:1")
if err == nil {
t.Fatal("Mount with an unregistered scheme returned nil, want an error")
}
@@ -154,8 +147,7 @@ func TestMount_ServeOnlyTransportCannotDial(t *testing.T) {
zip.RegisterTransport("serveonly", zip.Transport{
Serve: func(addr string, h fasthttp.RequestHandler) zip.Server { return nil },
})
app := zip.New(zip.Config{DisableStartupMessage: true})
err := app.Mount("/v1/x", "serveonly://host:1")
_, err := zip.Mount("/v1/x", "serveonly://host:1")
if err == nil || !strings.Contains(err.Error(), "cannot dial") {
t.Fatalf("Mount on a serve-only transport: err=%v, want a 'cannot dial' error", err)
}
+3 -1
View File
@@ -138,7 +138,9 @@ func TestWithStatus_RefusesANonSuccessCode(t *testing.T) {
func TestWithStatus_MCPIsUnaffected(t *testing.T) {
a := zip.New(zip.Config{AppName: "st", DisableStartupMessage: true})
zip.Post(a, "/v1/st/things", mk, zip.WithStatus(201), zip.WithOperationID("st_make"))
a.Prepare()
if err := a.Build(); err != nil {
t.Fatalf("Build: %v", err)
}
code, body := call2(t, a, "POST", "/mcp",
`{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"st_make","arguments":{"name":"x"}}}`)
+3 -1
View File
@@ -141,7 +141,9 @@ func TestAuthorizerSeesPathTarget(t *testing.T) {
// OpenAPI and every generator downstream rejects it.
func TestOpenAPIDeclaresPathParams(t *testing.T) {
a := memberApp(t)
a.Prepare()
if err := a.Build(); err != nil {
t.Fatalf("Build: %v", err)
}
req, _ := http.NewRequest("GET", "/.well-known/openapi.json", nil)
resp, err := a.Fiber().Test(req)
if err != nil {
+1 -3
View File
@@ -42,9 +42,7 @@ func Benchmark_PluginHop(b *testing.B) {
})
host := zip.New(benchConfig())
if err := host.Add(zip.Load(zip.Plugin{Name: "demo", Bin: bin}, "/v1/demo")); err != nil {
b.Skipf("cannot load plugin: %v", err)
}
host.Use(must(zip.Load(zip.Plugin{Name: "demo", Bin: bin}, "/v1/demo")))
defer func() { _ = host.Shutdown() }()
for _, tc := range []struct {
+6 -2
View File
@@ -187,7 +187,9 @@ func TestQueryBeatsBodyAndPathBeatsQuery(t *testing.T) {
// describes a route nobody can call correctly.
func TestOpenAPIDeclaresQueryParams(t *testing.T) {
a := searchApp(t)
a.Prepare()
if err := a.Build(); err != nil {
t.Fatalf("Build: %v", err)
}
req, _ := http.NewRequest("GET", "/.well-known/openapi.json", nil)
resp, err := a.Fiber().Test(req)
if err != nil {
@@ -249,7 +251,9 @@ func TestOpenAPIDeclaresQueryParams(t *testing.T) {
// the tool's input is the whole struct with no URL involved.
func TestMCPCallIsUnaffectedByQueryBinding(t *testing.T) {
a := searchApp(t)
a.Prepare()
if err := a.Build(); err != nil {
t.Fatalf("Build: %v", err)
}
req, _ := http.NewRequest("POST", "/mcp", stringReader(
`{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_v1_t_search","arguments":{"q":"viajson","limit":9}}}`))
req.Header.Set("Content-Type", "application/json")
+510
View File
@@ -0,0 +1,510 @@
package zip
import (
"context"
"os"
"path/filepath"
"strings"
"sync"
"sync/atomic"
"testing"
)
// Adversarial tests for the one-verb fold. Each one FAILS on the branch as
// written and names the single line that causes it. Nothing here is a style
// complaint: every assertion is a runtime behaviour a caller can observe.
func redOK(c *Ctx) error { return c.String(200, "ok") }
func redGuard(next Handler) Handler {
return func(c *Ctx) error {
if c.Header("Authorization") == "" {
return Errorf(401, "no credentials")
}
return next(c)
}
}
// ---------------------------------------------------------------------------
// 1. CONCURRENCY / STALENESS
// ---------------------------------------------------------------------------
// TestRed_DropNeverMovesTheVersionCounter.
//
// liveOrBuild caches a draft keyed on the process-wide `version` counter. Every
// other mutation moves that counter (appendEntry, compose, includeRoutes, and
// even transact's own rollback path). App.Drop is the one that does not — it
// assigns a.entries = kept inside the transaction and never calls version.Add.
//
// So a definition that is Drop'ed keeps serving, forever, in every OTHER app
// that had already materialised a draft over it. The host is not frozen and has
// no live generation, so nothing ever forces it to rebuild.
func TestRed_DropNeverMovesTheVersionCounter(t *testing.T) {
child := quiet("child")
child.Get("/keep", redOK)
sub := child.Group("/sub")
sub.Get("/gone", redOK)
host := quiet("host")
host.Use(child)
// Ordinary pre-Listen inspection: a codegen step, a doc generator, Fiber().
// It materialises a draft and CACHES it against the version counter.
if code, _ := wireGET(t, host, "/sub/gone"); code != 200 {
t.Fatalf("setup: host does not serve /sub/gone: %d", code)
}
before := version.Load()
if err := child.Drop(sub); err != nil {
t.Fatalf("Drop: %v", err)
}
if after := version.Load(); after != before {
t.Logf("version moved %d -> %d (bug is fixed)", before, after)
} else {
t.Errorf("Drop did not move the version counter (still %d) — "+
"generation.go:148 App.Drop assigns a.entries without version.Add(1)", before)
}
// The child itself is correct: it installed a generation, so it reads live.
if code, _ := wireGET(t, child, "/sub/gone"); code == 200 {
t.Errorf("the child still serves its own dropped route")
}
// The host is not. It answers out of a draft the Drop invalidated but never
// marked stale.
if code, _ := wireGET(t, host, "/sub/gone"); code == 200 {
t.Errorf("a DROPPED route is still served by a host that composed it: "+
"GET /sub/gone = %d (stale draft, draftAt == version)", code)
}
}
// TestRed_TheSealIsNotEnforcedUnderConcurrency.
//
// appendEntry decides whether to panic with `a.frozen.Load() && !a.building`.
// frozen is atomic; building is a plain bool, written by transact under buildMu
// and read here under no lock at all. Include is BY DESIGN called against a
// running system, so a second goroutine reaching Use is exactly the case the
// seal exists to catch — and the check that catches it is racy.
//
// When the read observes building==true the panic is skipped and the goroutine
// appends to a.entries while transact is copying and reassigning it: a torn
// entry list, or a silently discarded registration.
//
// Run with -race; the detector is the assertion.
func TestRed_TheSealIsNotEnforcedUnderConcurrency(t *testing.T) {
host := quiet("host")
host.Get("/ok", redOK)
if err := build(host); err != nil {
t.Fatalf("generation 0: %v", err)
}
var wg sync.WaitGroup
wg.Add(2)
// The control plane: composing against a running system, the sanctioned way.
go func() {
defer wg.Done()
for i := 0; i < 100; i++ {
c := quiet("c")
c.Get("/red/"+itoa(i), redOK)
_ = host.Include(c)
}
}()
// Anything else that reaches Use. The seal's whole job is to refuse this
// with a panic naming the freeze site.
go func() {
defer wg.Done()
for i := 0; i < 100; i++ {
func() {
defer func() { _ = recover() }()
host.Use(H(redOK))
}()
}
}()
wg.Wait()
}
// ---------------------------------------------------------------------------
// 2. TRANSACTION ROLLBACK IS NOT TOTAL
// ---------------------------------------------------------------------------
// TestRed_RefusedIncludeStillAdoptsTheChildsTeardown.
//
// transact restores a.entries and nothing else. compose() has a SECOND effect —
// a.OnShutdown(v.ShutdownWithContext) — which is appended to a.hooks before the
// build runs and is never undone. Include's contract is "on error — changes
// nothing at all". It changes the host's teardown set.
//
// Consequence: a host that refuses a plugin still owns that plugin's shutdown,
// so shutting the host down stops a subsystem it never composed and which may
// still be serving somewhere else.
func TestRed_RefusedIncludeStillAdoptsTheChildsTeardown(t *testing.T) {
host := quiet("host")
host.Get("/v1/x", redOK)
if err := build(host); err != nil {
t.Fatalf("generation 0: %v", err)
}
bad := quiet("bad")
bad.Get("/v1/x", redOK) // collides with the live set: Include must refuse
var torn atomic.Bool
bad.OnShutdown(func(context.Context) error { torn.Store(true); return nil })
if err := host.Include(bad); err == nil {
t.Fatal("setup: the colliding definition was accepted")
}
if err := host.Shutdown(); err != nil {
t.Fatalf("Shutdown: %v", err)
}
if torn.Load() {
t.Error("a REFUSED Include still adopted the child's teardown — " +
"generation.go:212 compose() calls OnShutdown, generation.go:182 rolls back only a.entries")
}
}
// TestRed_APanicInMaterialiseBricksTheProgram.
//
// A route whose method fiber does not know panics inside register(). That panic
// unwinds through build() and through transact() — past line 182, where the
// rollback lives. buildMu is released by defer, but a.entries keeps the edit.
//
// The live generation survives (requests keep being served), so the failure is
// invisible until the NEXT composition, which panics on the poison left behind.
// "A plugin whose patterns collide with the live set fails the build, with
// breadcrumbs, and the old generation keeps serving" holds for collisions and
// not for this.
func TestRed_APanicInMaterialiseBricksTheProgram(t *testing.T) {
host := quiet("host")
host.Get("/ok", redOK)
if err := build(host); err != nil {
t.Fatalf("generation 0: %v", err)
}
// A Declaration is a BUILD INPUT — JSON handed over by another team. Nothing
// validates the method it names, and fiber knows only its nine. PURGE is a
// real method (Varnish, Fastly, nginx cache invalidation); so are PROPFIND
// and MKCOL. Any of them, or a typo, reaches fiber's register() as a panic.
bad, err := remoteApp(host, "/v1/cdn", "http://127.0.0.1:9", Declaration{
Name: "cdn",
Routes: []Route{{Method: "PURGE", Pattern: "/v1/cdn/object"}},
})
if err != nil {
t.Fatalf("remoteApp: %v", err)
}
var first any
func() {
defer func() { first = recover() }()
if err := host.Include(bad); err != nil {
t.Logf("Include refused cleanly: %v", err)
}
}()
if first != nil {
t.Errorf("a Declaration's method panicked the build instead of failing it: %v", first)
}
// Whatever happened above, the host must still be composable.
good := quiet("good")
good.Get("/v1/good", redOK)
var second any
func() {
defer func() { second = recover() }()
_ = host.Include(good)
}()
if second != nil {
t.Errorf("the host is BRICKED: a later, valid Include panics too (%v) — "+
"the panic skipped generation.go:182, so a.entries still holds the poison", second)
}
}
// TestRed_MethodCaseIsNotNormalisedBeforeTheConflictCheck.
//
// fiber upper-cases a method inside register(); zip stores route.method exactly
// as written and conflicts() keys on that raw string. So "get /v1/thing" and
// "GET /v1/thing" are two keys to the walk and ONE address to the router: the
// build passes, and one of the two definitions is dead.
//
// Same root cause as the All/GET hole — addrKey normalises the PATH and not the
// METHOD — and it arrives through the same door, a hand-written Declaration.
func TestRed_MethodCaseIsNotNormalisedBeforeTheConflictCheck(t *testing.T) {
host := quiet("host")
host.Get("/v1/thing", func(c *Ctx) error { return c.String(200, "host") })
remote, err := remoteApp(host, "/v1", "http://127.0.0.1:9", Declaration{
Name: "remote",
Routes: []Route{{Method: "get", Pattern: "/v1/thing"}},
})
if err != nil {
t.Fatalf("remoteApp: %v", err)
}
host.Use(remote)
buildErr := build(host)
_, body := wireGET(t, host, "/v1/thing")
if buildErr == nil {
t.Errorf("two definitions claim GET /v1/thing and the walk reported NO conflict; "+
"%q silently wins — walk.go addrKey normalises the path but not the method", body)
}
}
// ---------------------------------------------------------------------------
// 3. SILENT DROPS
// ---------------------------------------------------------------------------
// TestRed_ABuildErrorSilentlyEmptiesEveryProjection.
//
// liveOrBuild does `g, _ := a.build()` and substitutes an EMPTY generation when
// the build fails. build() fails on any conflict, so one duplicated address
// turns Registry, Fiber, Declaration, OpenAPISpec and the CLI into empty
// answers — with no error anywhere. Only Listen reports it.
//
// The worst reachable form is `app.Described()`: a release pipeline writes a
// declaration/OpenAPI file describing ZERO routes and exits 0.
func TestRed_ABuildErrorSilentlyEmptiesEveryProjection(t *testing.T) {
a := billingApp() // one typed op: GET /invoices/:id
a.Get("/healthz", redOK)
a.Get("/healthz", redOK) // the whole defect needs exactly this one line
// The information exists. build() has it.
if err := build(a); err == nil {
t.Fatal("setup: the duplicate was not detected at all")
} else if !strings.Contains(err.Error(), "/healthz") {
t.Fatalf("setup: unexpected build error: %v", err)
}
if got := len(a.Registry()); got == 0 {
t.Errorf("Registry() reports 0 ops for a program that declares 1, and returns no error — "+
"generation.go:266 discards the build error and substitutes an empty generation")
}
if d := a.Declaration(); len(d.Routes) == 0 {
t.Errorf("Declaration() reports 0 routes for a program that declares 3, and returns no error")
}
// The projection a release pipeline actually consumes.
dest := filepath.Join(t.TempDir(), "declare.json")
if err := a.project(Declare, dest); err != nil {
t.Fatalf("project: %v", err)
}
body, err := os.ReadFile(dest)
if err != nil {
t.Fatalf("read: %v", err)
}
if strings.Contains(string(body), `"routes": []`) {
t.Errorf("`app declare` wrote an EMPTY declaration and exited 0 for a broken program:\n%s", body)
}
}
// TestRed_AllShadowsAConcreteMethodAndTheWalkSaysNothing.
//
// conflicts() keys on addrKey(r.method, path) and methodAll is stored as the
// literal "ALL", so "ALL /v1/thing" and "GET /v1/thing" are two different keys.
// They are ONE address at runtime: whichever fiber registers first answers, and
// the other definition's route is dead code.
//
// This is load-bearing now that HEAD deleted App.claim: every mount and every
// plugin registers its prefix as methodAll, and the commit that removed the
// claims ledger asserts the walk "answers it better".
func TestRed_AllShadowsAConcreteMethodAndTheWalkSaysNothing(t *testing.T) {
gateway := quiet("gateway")
gateway.All("/v1/thing", func(c *Ctx) error { return c.String(200, "gateway") })
owner := quiet("owner")
owner.Get("/v1/thing", func(c *Ctx) error { return c.String(200, "owner") })
host := quiet("host")
host.Use(gateway, owner)
err := build(host)
_, body := wireGET(t, host, "/v1/thing")
if err == nil {
t.Errorf("two definitions claim GET /v1/thing (one via All) and the walk reported NO conflict; "+
"%q silently wins and the other route is dead — walk.go:265 keys methodAll as its own method", body)
}
}
// TestRed_AMiddlewareOnlyDefinitionIsSilentlyInert.
//
// materialise registers depth-0 middleware as router middleware and composes
// depth>0 middleware into the routes of its own subtree. A definition that
// declares middleware and NO routes therefore has nowhere for its middleware to
// go, and it is dropped on the floor — not applied to the host, not applied to
// anything.
//
// The shape is the obvious way to package a cross-cutting concern as a unit
// under one composition verb, it looks exactly like the blessed staged
// composition example, and neither the build nor Lint says a word.
func TestRed_AMiddlewareOnlyDefinitionIsSilentlyInert(t *testing.T) {
sec := quiet("sec") // a security module: middleware, no routes of its own
sec.Use(H(func(c *Ctx) error {
if c.Header("Authorization") == "" {
return Errorf(401, "no credentials")
}
return c.Continue()
}))
app := quiet("host")
app.Use(sec) // "install the security module"
app.Get("/private", redOK)
if err := build(app); err != nil {
t.Fatalf("build: %v", err)
}
if code, _ := wireGET(t, app, "/private"); code != 401 {
t.Errorf("an included middleware-only definition never runs: GET /private = %d "+
"(build.go:113 skips depth>0 middleware; it has no routes to be composed into)", code)
}
if len(app.Lint()) == 0 {
t.Errorf("...and Lint() reports nothing, so the inert guard is invisible")
}
}
// TestRed_IncludeOnAFrozenChildIsASilentNoOp.
//
// appendEntry's panic tells the programmer to "go through a generation
// (App.Include / App.Drop)". Taking that advice on anything but the ROOT is a
// silent no-op: transact builds and installs a generation on the RECEIVER, and
// a group has no listener, so the host keeps serving the old one and Include
// returns nil.
func TestRed_IncludeOnAFrozenChildIsASilentNoOp(t *testing.T) {
host := quiet("host")
v1 := host.Group("/v1")
v1.Get("/a", redOK)
if err := build(host); err != nil {
t.Fatalf("generation 0: %v", err)
}
// The advice the panic gives.
var advice string
func() {
defer func() {
if r := recover(); r != nil {
advice, _ = r.(string)
}
}()
v1.Get("/late", redOK)
}()
if !strings.Contains(advice, "App.Include") {
t.Fatalf("setup: the freeze panic no longer names Include: %q", advice)
}
later := quiet("later")
later.Get("/later", redOK)
if err := v1.Include(later); err != nil {
t.Fatalf("Include on the frozen group: %v", err)
}
if code, _ := wireGET(t, host, "/v1/later"); code != 200 {
t.Errorf("Include on a frozen CHILD returned nil and changed nothing the host serves: "+
"GET /v1/later = %d — transact installs onto the receiver, not onto the root", code)
}
}
// ---------------------------------------------------------------------------
// 4. SCOPED MIDDLEWARE IS LOST (auth bypass)
// ---------------------------------------------------------------------------
// TestRed_AScopedWithIsLostByANestedGroup.
//
// On origin/main wrapRouter.Group returned another *wrapRouter, so the chain
// propagated to every nested group. On this branch Router.Group returns *App
// and wrapRouter.Group stashes the chain in g.wrap — but App.Group builds the
// child from groupConfig() and never copies a.wrap, so the chain stops at one
// level.
//
// A regression, and the failure mode is an ungated route.
func TestRed_AScopedWithIsLostByANestedGroup(t *testing.T) {
app := quiet("host")
v1 := app.With(redGuard).Group("/v1")
v1.Get("/direct", redOK)
sub := v1.Group("/sub")
sub.Get("/nested", redOK)
if err := build(app); err != nil {
t.Fatalf("build: %v", err)
}
if code, _ := wireGET(t, app, "/v1/direct"); code != 401 {
t.Fatalf("setup: the direct leaf is not gated either: %d", code)
}
if code, _ := wireGET(t, app, "/v1/sub/nested"); code != 401 {
t.Errorf("a nested group escaped the scoped With: GET /v1/sub/nested = %d "+
"(compose.go:254 newApp(a.groupConfig()) drops a.wrap)", code)
}
}
// TestRed_WithUseDropsTheGateForTheWholeSubtree.
//
// wrapRouter implements Router, Router now carries the ONE composition verb, and
// wrapRouter.Use delegates straight to the inner App. Group propagates the
// chain and OpScope propagates the chain — with the comment that dropping it
// "is a hole, not an inconvenience" — and Use silently drops it.
func TestRed_WithUseDropsTheGateForTheWholeSubtree(t *testing.T) {
child := quiet("child")
child.Get("/child/x", redOK)
app := quiet("host")
app.With(redGuard).Use(child)
if err := build(app); err != nil {
t.Fatalf("build: %v", err)
}
if code, _ := wireGET(t, app, "/child/x"); code != 401 {
t.Errorf("With(...).Use(def) composed the definition UNGATED: GET /child/x = %d "+
"(middleware.go:60 wrapRouter.Use ignores w.wrap)", code)
}
}
// ---------------------------------------------------------------------------
// 5. CONTROL PROBES — categories I claim are CLEAN. These must PASS.
// ---------------------------------------------------------------------------
// TestRed_CleanDescendAncestorAliasing: descend appends to a shared `ancestors`
// backing array across siblings. It is safe, because every frame only ever
// reads the prefix it was handed. A diamond (one definition reached by two
// paths) and repeated siblings must not be mistaken for a cycle.
func TestRed_CleanDescendAncestorAliasing(t *testing.T) {
shared := quiet("shared")
shared.Get("/shared", redOK)
left := quiet("left")
left.Group("/l").Use(shared)
right := quiet("right")
right.Group("/r").Use(shared)
root := quiet("root")
root.Use(left, right)
occ, err := walk(root)
if err != nil {
t.Fatalf("diamond composition reported an error: %v", err)
}
n := 0
for _, o := range occ {
if r, ok := o.route(); ok && strings.HasSuffix(r.path, "/shared") {
n++
}
}
if n != 2 {
t.Fatalf("one definition reached by two paths yielded %d occurrences, want 2", n)
}
}
// TestRed_CleanComposeOpsDoesNotEditTheDefinition: a definition's own document
// must be unchanged by being composed, at any number of prefixes.
func TestRed_CleanComposeOpsDoesNotEditTheDefinition(t *testing.T) {
billing := billingApp()
own := billing.Registry()
if len(own) != 1 {
t.Fatalf("setup: %d ops", len(own))
}
path, id, origin, tags := own[0].Path, own[0].OperationID, own[0].Origin, len(own[0].Tags)
host := quiet("host")
host.Group("/v1").Use(billing)
host.Group("/admin").Use(billing)
if got := len(host.Registry()); got != 2 {
t.Fatalf("composed registry has %d ops, want 2", got)
}
if own[0].Path != path || own[0].OperationID != id || own[0].Origin != origin || len(own[0].Tags) != tags {
t.Errorf("composing edited the definition's own op: %+v", own[0])
}
}
+5 -8
View File
@@ -59,16 +59,13 @@ func remoteService(t *testing.T) (string, Declaration) {
func TestMount_DeclarationIsAnInputNotAFetch(t *testing.T) {
host := quiet("cloud")
dead := "/nonexistent/nothing-listens-here.sock"
err := host.Mount("/v1/ledger", dead, Declaration{
host.Use(mustApp(Mount("/v1/ledger", dead, Declaration{
Name: "ledger",
Routes: []Route{
{Method: "GET", Pattern: "/v1/ledger/entries/:id", Op: "getEntry"},
{Method: "GET", Pattern: "/v1/ledger/raw"},
},
})
if err != nil {
t.Fatalf("Mount: %v", err)
}
})))
if err := build(host); err != nil {
t.Fatalf("build: %v", err)
}
@@ -110,10 +107,10 @@ func TestMount_ServesAndCallsTheRemote(t *testing.T) {
}
host := quiet("cloud")
if err := host.Mount("/v1/ledger", sock, decl); err != nil {
t.Fatalf("Mount: %v", err)
host.Use(mustApp(Mount("/v1/ledger", sock, decl)))
if err := host.Build(); err != nil {
t.Fatalf("Build: %v", err)
}
host.Prepare()
// 1. The proxied route reaches the remote's own handler.
if code, body := wireGET(t, host, "/v1/ledger/raw"); code != 200 || body != "raw ledger" {
+8 -16
View File
@@ -31,11 +31,9 @@ func shortDir(t *testing.T) string {
func TestStart_BringsUpALazyPluginThatNoRequestHasReached(t *testing.T) {
app := zip.New(zip.Config{AppName: "host", DisableStartupMessage: true})
if err := app.Add(zip.Load(
app.Use(must(zip.Load(
zip.Plugin{Name: "demo", Bin: buildPlugin(t, "v1"), Dir: shortDir(t), Lazy: true}, "/v1/demo",
)); err != nil {
t.Fatalf("Add(Load): %v", err)
}
)))
t.Cleanup(func() { _ = app.Shutdown() })
if st := app.Plugins(); st[0].Running {
@@ -59,11 +57,9 @@ func TestStart_BringsUpALazyPluginThatNoRequestHasReached(t *testing.T) {
// child that is answering its own in-flight calls.
func TestStart_OnARunningPluginIsANoOp(t *testing.T) {
app := zip.New(zip.Config{AppName: "host", DisableStartupMessage: true})
if err := app.Add(zip.Load(
app.Use(must(zip.Load(
zip.Plugin{Name: "demo", Bin: buildPlugin(t, "v1"), Dir: shortDir(t), Lazy: true}, "/v1/demo",
)); err != nil {
t.Fatalf("Add(Load): %v", err)
}
)))
t.Cleanup(func() { _ = app.Shutdown() })
if _, err := app.Start("demo"); err != nil {
@@ -89,11 +85,9 @@ func TestStart_OnARunningPluginIsANoOp(t *testing.T) {
// one child rather than one per caller.
func TestStart_ConcurrentFirstCallersProduceOneChild(t *testing.T) {
app := zip.New(zip.Config{AppName: "host", DisableStartupMessage: true})
if err := app.Add(zip.Load(
app.Use(must(zip.Load(
zip.Plugin{Name: "demo", Bin: buildPlugin(t, "v1"), Dir: shortDir(t), Lazy: true}, "/v1/demo",
)); err != nil {
t.Fatalf("Add(Load): %v", err)
}
)))
t.Cleanup(func() { _ = app.Shutdown() })
const n = 12
@@ -123,11 +117,9 @@ func TestStart_ConcurrentFirstCallersProduceOneChild(t *testing.T) {
// a potential resurrection.
func TestStart_WillNotResurrectAnUnloadedPlugin(t *testing.T) {
app := zip.New(zip.Config{AppName: "host", DisableStartupMessage: true})
if err := app.Add(zip.Load(
app.Use(must(zip.Load(
zip.Plugin{Name: "demo", Bin: buildPlugin(t, "v1"), Dir: shortDir(t), Lazy: true}, "/v1/demo",
)); err != nil {
t.Fatalf("Add(Load): %v", err)
}
)))
t.Cleanup(func() { _ = app.Shutdown() })
if _, err := app.Start("demo"); err != nil {
+7 -5
View File
@@ -90,12 +90,14 @@ type Usage struct {
// Plugins reports every plugin this host has loaded, ordered by name so a diff
// between two hosts is stable. Safe to call while requests are in flight.
func (a *App) Plugins() []Status {
a.plugMu.Lock()
ps := make([]*plugin, 0, len(a.plugins))
for _, p := range a.plugins {
ps = append(ps, p)
var ps []*plugin
for _, h := range a.hosts() {
h.plugMu.Lock()
for _, p := range h.plugins {
ps = append(ps, p)
}
h.plugMu.Unlock()
}
a.plugMu.Unlock()
out := make([]Status, 0, len(ps))
for _, p := range ps {