mirror of
https://github.com/zap-proto/zip.git
synced 2026-10-19 15:38:57 +00:00
Two defects, one theme: a declaration the programmer WROTE is not something the
shape around it gets to overrule.
occurrenceID qualified every operation id with its occurrence's prefix,
including one the author had written with WithOperationID. Including
github.com/hanzoai/o11y under /v1/o11y therefore renamed its published ops to
v1.o11y.<id> — every MCP tool an agent had cached, every operationId, every CLI
command, every generated SDK method — as a silent side effect of one wiring
line. LLM.md's own Graft table has said `operationIds | untouched` since v1.18;
the walk had stopped obeying it. Measured on o11y: 217 of its 353 ops carry a
declared id, and all 217 were renamed by a host prefix. They are not now.
The rule asks one question first. A DECLARED id survives verbatim, because it is
a published name and a service embedded at a prefix publishes the same
operations it publishes standalone. An UNDECLARED one is still qualified by its
occurrence's prefix, deterministically and from the composition's shape, because
there no promise exists and a real ambiguity does: one definition included twice
is two operations and a document cannot hold two under one id. Where the two
meet — declared AND twice — is now a refused program that names both occurrences
and both fixes, instead of a silent rename that resolved an ambiguity the author
never saw.
Two registrations at one address was a refused program, and that refusal is
right. It also refused two shapes the estate writes on purpose: a service
installing its own published TABLE of ops onto the router its implementation
already answers on (o11y: 353 ops, publish() after the service's own routes —
"order is the contract"), and a second handler at an address declared to pin
what the router does with two (cloud's openapi generator test). Nothing in the
program said which it was, so the check could not tell them from an accident.
App.Shadow() says it. Scoped like With, and for the same reason — a group is an
App, so app.Shadow().Group(p) carries the property onto the group and every leaf
beneath it inherits it, including leaves registered later. Nothing about the
wire, the registry or any projection changes: the route installs exactly as
before and fiber chains it behind whatever came first. Only the conflict check
changes, and into two NARROWER rules rather than none —
- exactly one claim at an address may decline to yield, and it must be the
FIRST, because the router answers with the first registration. A shadow
ahead of what it yields to would stand in front of it, so "order is the
contract" is now checked rather than stated. Two claims that both mean to
answer is still the accidental collision, refused in the same words.
- at most one claim may carry an op. A document keys an operation on method
and path; there is one slot, and two ops asking for it would publish one and
drop the other. Shadow yields an address to another HANDLER, never a
document slot to another op.
A shadow that is the only claim simply answers, which is what lets ONE table be
mounted into a host that has an implementation and into one that does not.
Shadow is on the concrete *App and deliberately NOT on the Router interface:
Router is implemented from outside this package method by method (cloud's scope,
commerce's mintRouter), and widening it is what stalled v1.19 adoption once.
Tests: declaredid_test.go and shadow_test.go, twelve cases, each failing before
the change — the id one with the literal v1.o11y.CreateRole, the address ones
with the refusal origin/main emits. compose_test.go gains unnamedApp, the same
definition with no declared id, because a definition that names its ops cannot
be the one included twice: the diamond is a test about the UNDECLARED half of
the rule and now says so.
Co-authored-by: Hanzo Dev <[email protected]>
235 lines
8.6 KiB
Go
235 lines
8.6 KiB
Go
package zip
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// The walk says what a program MEANS. These say what it DOES — the same
|
|
// programs, driven over the wire, because a projection that is right on paper
|
|
// and wrong at the socket is wrong.
|
|
|
|
func wireGET(t *testing.T, a *App, path string, headers ...string) (int, string) {
|
|
t.Helper()
|
|
req := httptest.NewRequest("GET", path, nil)
|
|
for i := 0; i+1 < len(headers); i += 2 {
|
|
req.Header.Set(headers[i], headers[i+1])
|
|
}
|
|
resp, err := a.Fiber().Test(req)
|
|
if err != nil {
|
|
t.Fatalf("GET %s: %v", path, err)
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
body, _ := io.ReadAll(resp.Body)
|
|
return resp.StatusCode, string(body)
|
|
}
|
|
|
|
// guarded is a definition whose entire authentication seam is a pathless
|
|
// app.Use — the shape that made route-copying unsafe and that [App.Graft]'s
|
|
// delegation existed to protect.
|
|
func guarded(name string) *App {
|
|
a := quiet(name)
|
|
a.Use(H(func(c *Ctx) error {
|
|
if c.Header("Authorization") == "" {
|
|
return Errorf(401, "no credentials")
|
|
}
|
|
return c.Continue()
|
|
}))
|
|
a.Get("/thing", func(c *Ctx) error { return c.String(200, name+" thing") })
|
|
return a
|
|
}
|
|
|
|
// TestWire_IncludedMiddlewareStaysInsideItsDefinition is the property the whole
|
|
// composition model is judged on, and inclusion by reference has to earn it the
|
|
// hard way: there is no delegation boundary at run time to hide behind, so the
|
|
// definition's middleware has to be composed onto the definition's OWN routes
|
|
// and nothing else.
|
|
//
|
|
// The failure this prevents is not hypothetical: a child's pathless Use, placed
|
|
// on the host's router, gates every route in the host binary.
|
|
func TestWire_IncludedMiddlewareStaysInsideItsDefinition(t *testing.T) {
|
|
root := quiet("host")
|
|
root.Get("/before", func(c *Ctx) error { return c.String(200, "before") })
|
|
root.Use(guarded("iam"))
|
|
root.Get("/after", func(c *Ctx) error { return c.String(200, "after") })
|
|
|
|
// The guard DID come across: the definition's own route is gated.
|
|
if code, _ := wireGET(t, root, "/thing"); code != 401 {
|
|
t.Errorf("the definition's route answered %d without credentials — its guard was dropped", code)
|
|
}
|
|
if code, b := wireGET(t, root, "/thing", "Authorization", "yes"); code != 200 || b != "iam thing" {
|
|
t.Errorf("the definition's route with credentials: %d %q", code, b)
|
|
}
|
|
// And it did NOT bleed — in EITHER direction. Routes written before the
|
|
// inclusion and routes written after it are both untouched.
|
|
if code, b := wireGET(t, root, "/before"); code != 200 || b != "before" {
|
|
t.Errorf("host route written BEFORE the inclusion answered %d %q — the guard bled backwards", code, b)
|
|
}
|
|
if code, b := wireGET(t, root, "/after"); code != 200 || b != "after" {
|
|
t.Errorf("host route written AFTER the inclusion answered %d %q — the guard bled forwards", code, b)
|
|
}
|
|
}
|
|
|
|
// TestWire_GroupIsLexicalScope: middleware declared in a group applies to that
|
|
// group and to nothing else, including nothing that comes after it.
|
|
func TestWire_GroupIsLexicalScope(t *testing.T) {
|
|
root := quiet("host")
|
|
v1 := root.Group("/v1")
|
|
v1.Use(H(func(c *Ctx) error {
|
|
if c.Header("X-Key") == "" {
|
|
return Errorf(401, "no key")
|
|
}
|
|
return c.Continue()
|
|
}))
|
|
v1.Get("/x", func(c *Ctx) error { return c.String(200, "v1 x") })
|
|
root.Get("/open", func(c *Ctx) error { return c.String(200, "open") })
|
|
|
|
if code, _ := wireGET(t, root, "/v1/x"); code != 401 {
|
|
t.Errorf("group route answered %d without the key — the group's Use did not apply", code)
|
|
}
|
|
if code, b := wireGET(t, root, "/v1/x", "X-Key", "k"); code != 200 || b != "v1 x" {
|
|
t.Errorf("group route with the key: %d %q", code, b)
|
|
}
|
|
if code, b := wireGET(t, root, "/open"); code != 200 || b != "open" {
|
|
t.Errorf("a route outside the group answered %d %q — the group's Use escaped its scope", code, b)
|
|
}
|
|
}
|
|
|
|
// TestWire_DiamondServesBothOccurrences: one definition, two prefixes, and BOTH
|
|
// answer. The router keys on the occurrence, so two inclusions are two route
|
|
// sets, not one that the second inclusion silently lost.
|
|
func TestWire_DiamondServesBothOccurrences(t *testing.T) {
|
|
billing := unnamedApp() // no declared id, so it may occur twice
|
|
root := quiet("cloud")
|
|
root.Group("/v1").Use(billing)
|
|
root.Group("/admin").Use(billing)
|
|
|
|
for _, p := range []string{"/v1/invoices/i-1", "/admin/invoices/i-1"} {
|
|
code, body := wireGET(t, root, p)
|
|
if code != 200 || !strings.Contains(body, "i-1") {
|
|
t.Errorf("%s answered %d %q", p, code, body)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestWire_DifferentGuardsPerOccurrence. The same definition legitimately runs
|
|
// under one guard in one place and another guard elsewhere; that is the
|
|
// semantics of an occurrence, not an approximation of it.
|
|
func TestWire_DifferentGuardsPerOccurrence(t *testing.T) {
|
|
shared := quiet("shared")
|
|
shared.Get("/who", func(c *Ctx) error { return c.String(200, "shared") })
|
|
|
|
need := func(h string) Handler {
|
|
return func(c *Ctx) error {
|
|
if c.Header(h) == "" {
|
|
return Errorf(401, "want %s", h)
|
|
}
|
|
return c.Continue()
|
|
}
|
|
}
|
|
root := quiet("host")
|
|
pub := root.Group("/pub")
|
|
pub.Use(need("X-User"))
|
|
pub.Use(shared)
|
|
adm := root.Group("/adm")
|
|
adm.Use(need("X-Admin"))
|
|
adm.Use(shared)
|
|
|
|
if code, _ := wireGET(t, root, "/pub/who", "X-User", "u"); code != 200 {
|
|
t.Errorf("/pub/who with a user credential answered %d", code)
|
|
}
|
|
if code, _ := wireGET(t, root, "/pub/who", "X-Admin", "a"); code != 401 {
|
|
t.Errorf("/pub/who accepted the ADMIN credential (%d) — the occurrences share a guard", code)
|
|
}
|
|
if code, _ := wireGET(t, root, "/adm/who", "X-Admin", "a"); code != 200 {
|
|
t.Errorf("/adm/who with an admin credential answered %d", code)
|
|
}
|
|
if code, _ := wireGET(t, root, "/adm/who", "X-User", "u"); code != 401 {
|
|
t.Errorf("/adm/who accepted the USER credential (%d) — the occurrences share a guard", code)
|
|
}
|
|
}
|
|
|
|
// TestWire_UndeclaredPathIsNotSwallowed. Inclusion registers exactly the
|
|
// addresses the definition declares, so a path nobody declares is a 404 from
|
|
// the host rather than being swallowed by a wildcard.
|
|
func TestWire_UndeclaredPathIsNotSwallowed(t *testing.T) {
|
|
root := quiet("host")
|
|
root.Group("/v1").Use(billingApp())
|
|
if code, _ := wireGET(t, root, "/v1/nobody-declares-this"); code != 404 {
|
|
t.Errorf("undeclared path answered %d, want 404 — the inclusion is swallowing", code)
|
|
}
|
|
}
|
|
|
|
// TestWire_OneCtxAcrossAnInclusionBoundary. A definition's middleware and the
|
|
// host's middleware run in one request, so they must be handed ONE *Ctx —
|
|
// enrichment written by either has to be visible to the other. Binding a
|
|
// handler to the definition that declared it instead of the app that serves it
|
|
// breaks exactly this.
|
|
func TestWire_OneCtxAcrossAnInclusionBoundary(t *testing.T) {
|
|
var seen []*Ctx
|
|
inner := quiet("inner")
|
|
inner.Use(H(func(c *Ctx) error { seen = append(seen, c); return c.Continue() }))
|
|
inner.Get("/deep", func(c *Ctx) error {
|
|
seen = append(seen, c)
|
|
return c.NoContent(204)
|
|
})
|
|
|
|
root := quiet("host")
|
|
root.Use(H(func(c *Ctx) error { seen = append(seen, c); return c.Continue() }))
|
|
root.Group("/v1").Use(inner)
|
|
|
|
if code, _ := wireGET(t, root, "/v1/deep"); code != 204 {
|
|
t.Fatalf("/v1/deep answered %d", code)
|
|
}
|
|
if len(seen) != 3 {
|
|
t.Fatalf("ran %d handlers, want 3", len(seen))
|
|
}
|
|
for i, c := range seen {
|
|
if c != seen[0] {
|
|
t.Fatalf("handler %d got a different *Ctx — handlers were bound to their definition, not to the server", i)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestWire_IncludedControlPlaneIsNotAdopted. zip's own projections belong to
|
|
// the process serving them. Inclusion reads a definition's whole program, so
|
|
// without an explicit rule a definition that had ever rendered its own document
|
|
// would hand the host ITS document at the host's well-known path — and the
|
|
// composition would publish the part as if it were the whole.
|
|
func TestWire_IncludedControlPlaneIsNotAdopted(t *testing.T) {
|
|
child := billingApp()
|
|
// 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)
|
|
if err := host.Build(); err != nil {
|
|
t.Fatalf("Build: %v", err)
|
|
}
|
|
|
|
code, body := wireGET(t, host, SpecPath)
|
|
if code != 200 {
|
|
t.Fatalf("%s answered %d", SpecPath, code)
|
|
}
|
|
if !strings.Contains(body, `"title":"host"`) {
|
|
t.Errorf("the included definition took the host's document: %s", firstN(body, 200))
|
|
}
|
|
if !strings.Contains(body, "/v1/invoices/{id}") {
|
|
t.Errorf("the host's served document is missing the included op: %s", firstN(body, 400))
|
|
}
|
|
}
|
|
|
|
func firstN(s string, n int) string {
|
|
if len(s) <= n {
|
|
return s
|
|
}
|
|
return s[:n]
|
|
}
|