Files
k8s/gate_test.go
zeekayandHanzo Dev 7163867904 initial commit, on zip v1.20.0
The tree was staged but never committed — this is the repo's first commit, so it
carries both the existing content and the zip v1.18.8 -> v1.20.0 migration.

Migration: Prepare() is replaced by Build() error, and both test harnesses assert
the verdict rather than discarding it. main.go:59's Use call needed no change —
the three middleware constructors already return zip.Handler, which satisfies the
closed Component interface.

The committed projections (k8s.plugin.json, openapi.json, ops/zipdoc_gen.go) are
regenerated: v1.20.0 splits an op's prose into summary (first line) + description
and zipdoc now emits per-field docs, so all three artifacts move with the bump.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-02 14:20:05 -07:00

628 lines
22 KiB
Go

package main
import (
"encoding/json"
"go/ast"
"go/parser"
"go/token"
"io/fs"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
"testing"
"github.com/zap-proto/zip"
"github.com/hanzoai/k8s/ops"
"github.com/hanzoai/k8s/registry"
)
// The gates HIP-0106 §8 requires, each named for the failure it catches. Two properties
// hold throughout, because they are the two ways a green tick lies:
//
// - NON-VACUITY. A gate that examined nothing FAILS and says how much it examined.
// Every route to zero examinations is a defect elsewhere that would otherwise arrive
// as a pass.
// - A SOURCE ON ONE SIDE. Never two derived artifacts compared with each other. Here
// the live router is the source: the declaration and the OpenAPI subset are both
// projected from it inside the test, and the committed files are compared against
// that projection rather than against one another.
// app builds the composition this binary serves, exactly as run() does. Nothing is
// stubbed: the registry is the real one over a store that is never reached, because a
// projection is a function of the code and must not need a peer.
func app(t *testing.T) *zip.App {
t.Helper()
a := zip.New(zip.Config{AppName: name})
ops.Ops{Reg: registry.New(registry.Sealed{})}.Mount(a)
if err := a.Build(); err != nil {
t.Fatalf("Build: %v", err)
}
return a
}
// sh executes a tool and returns its combined output, failing the test on error.
func sh(t *testing.T, args ...string) string {
t.Helper()
cmd := exec.Command(args[0], args[1:]...)
cmd.Env = append(os.Environ(), "GOWORK=off", "GOFLAGS=-mod=mod")
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("%s: %v\n%s", strings.Join(args, " "), err, out)
}
return string(out)
}
// §8.1 — the plugin is a LEAF.
func TestPluginIsALeaf(t *testing.T) {
// A grep over source is NOT this gate: the dangerous break is the one that still
// compiles, so the check is over the import graph a build actually produces.
out := sh(t, "go", "list", "-deps", "./...")
deps := strings.Fields(out)
if len(deps) < 100 {
t.Fatalf("this gate proved nothing: the dependency graph came back as %d packages", len(deps))
}
for _, d := range deps {
if strings.HasPrefix(d, "github.com/hanzoai/cloud") || strings.HasPrefix(d, "github.com/hanzoai/base") {
t.Errorf("this plugin links the host package %s — that is the 316-package coupling the contract removes", d)
}
}
t.Logf("%d packages, none of them a host's", len(deps))
}
// §1.2 — the measured floor, reported rather than asserted at a number.
func TestPackageWeightIsReported(t *testing.T) {
binary := strings.Fields(sh(t, "go", "list", "-deps", "."))
leaf := strings.Fields(sh(t, "go", "list", "-deps", "./plane"))
var k8sFamily int
for _, d := range binary {
if strings.HasPrefix(d, "k8s.io/") || strings.HasPrefix(d, "sigs.k8s.io/") {
k8sFamily++
}
}
// The plane leaf is the number that matters to a CONSUMER: it is what an app links
// to ask this service a question, and it must stay at one package. A consumer that
// linked client-go to make an RPC would have gained nothing from the split.
if len(leaf) != 1 {
t.Fatalf("the plane contract is %d packages; a consumer must be able to link it without linking a Kubernetes client:\n%s",
len(leaf), strings.Join(leaf, "\n"))
}
// kustomize is a RENDERING concern and belongs to whoever renders. Worth 74-84
// packages on its own, and it has no business in a cluster client.
for _, d := range binary {
if strings.Contains(d, "kustomize") {
t.Errorf("%s: rendering belongs to the deploy side, not to the cluster client", d)
}
}
t.Logf("binary %d packages (k8s family %d), plane contract %d package", len(binary), k8sFamily, len(leaf))
}
// §8.3 — the committed declaration is current, and the OpenAPI subset is a subset of it.
func TestDeclarationIsCurrent(t *testing.T) {
live := app(t).Declaration()
committed := readJSON[zip.Declaration](t, "k8s.plugin.json")
if diff := jsonDiff(t, live, committed); diff != "" {
t.Fatalf("k8s.plugin.json is not what the router serves; run `make generate`:\n%s", diff)
}
if len(live.Routes) == 0 || len(live.Ops) == 0 {
t.Fatal("this gate proved nothing: the declaration is empty")
}
// openapi ⊆ declaration. The relation is one-way on purpose: a served-but-unpublished
// route must still be DECLARED or a host will not route it, which is the outage class
// where every product beacon in the fleet answered 405 for want of a table row.
spec := readJSON[struct {
Paths map[string]map[string]any `json:"paths"`
}](t, "openapi.json")
declared := map[string]bool{}
for _, r := range live.Routes {
declared[openAPIPath(r.Pattern)] = true
}
for p := range spec.Paths {
if !declared[p] {
t.Errorf("the OpenAPI document publishes %s, which the declaration does not carry — a host will not route it", p)
}
}
t.Logf("%d routes, %d ops, %d published paths", len(live.Routes), len(live.Ops), len(spec.Paths))
}
// §8.4 — the committed OpenAPI subset is current.
func TestOpenAPIIsCurrent(t *testing.T) {
live := app(t).OpenAPISpec()
committed := readJSON[map[string]any](t, "openapi.json")
if diff := jsonDiff(t, live, committed); diff != "" {
t.Fatalf("openapi.json is not what the router serves; run `make generate`:\n%s", diff)
}
}
// §8.5 — zipdoc is wired, current, and every op actually carries prose.
func TestProseReachesBothProjections(t *testing.T) {
// The directive is the ONLY way a doc comment reaches a document: Go drops comments
// at compile time and reflection sees types, never prose. A package without it can
// never get a description into the spec or into an MCP tool, however correct
// everything else is.
for _, dir := range typedOpDirs(t) {
src, err := os.ReadFile(filepath.Join(dir, "zipdoc_gen.go"))
if err != nil {
t.Fatalf("%s registers typed ops but has no committed zipdoc_gen.go: %v", dir, err)
}
if len(src) == 0 {
t.Fatalf("%s/zipdoc_gen.go is empty", dir)
}
if !strings.Contains(readFile(t, filepath.Join(dir, "ops.go")), "//go:generate go run github.com/zap-proto/zip/cmd/zipdoc") {
t.Fatalf("%s is missing the zipdoc directive, so its prose reaches neither projection", dir)
}
sh(t, "go", "run", "github.com/zap-proto/zip/cmd/zipdoc", "-check", "./"+dir)
}
// And the prose is actually THERE, in both projections, for every op. A directive
// that runs over handlers with no comments is a gate that proves nothing.
a := app(t)
spec, err := json.Marshal(a.OpenAPISpec())
if err != nil {
t.Fatal(err)
}
var doc struct {
Paths map[string]map[string]struct {
OperationID string `json:"operationId"`
Description string `json:"description"`
} `json:"paths"`
}
if err := json.Unmarshal(spec, &doc); err != nil {
t.Fatal(err)
}
described := 0
for p, methods := range doc.Paths {
for m, op := range methods {
if op.OperationID == "" {
t.Errorf("%s %s has no operationId, so it is invisible to the plane, the MCP tool list and the CLI", m, p)
}
if strings.TrimSpace(op.Description) == "" {
t.Errorf("%s %s has no description: an agent choosing whether to call it would read nothing", m, p)
continue
}
described++
}
}
tools := a.MCPTools()
for _, tool := range tools {
if d, _ := tool["description"].(string); strings.TrimSpace(d) == "" {
t.Errorf("MCP tool %v has no description", tool["name"])
}
}
if described == 0 || len(tools) == 0 {
t.Fatal("this gate proved nothing: no described operations and no tools")
}
if len(tools) != described {
t.Errorf("%d described operations but %d MCP tools — one projection is losing ops", described, len(tools))
}
t.Logf("%d operations carry prose in both the OpenAPI document and the MCP tool list", described)
}
// §3.4 — the shape a host will accept.
func TestEveryRouteIsRoutable(t *testing.T) {
live := app(t).Declaration()
if live.Name != name {
t.Fatalf("the declaration names %q but the binary is %q — one name, no mapping table", live.Name, name)
}
if live.Eager {
t.Error("this app is request-driven: it owns no listener, consumer or background loop, so a host must be free to start it lazily")
}
for _, r := range live.Routes {
switch {
case !strings.HasPrefix(r.Pattern, "/v1/k8s"):
t.Errorf("%s %s is outside this app's own prefix, so it would claim a path another plugin owns", r.Method, r.Pattern)
case strings.Contains(r.Pattern, ".."):
t.Errorf("%s %s contains a parent reference", r.Method, r.Pattern)
case strings.Contains(r.Pattern, "*"):
t.Errorf("%s %s is a catch-all: it would claim paths other plugins own and paths that do not exist yet", r.Method, r.Pattern)
case strings.Contains(r.Pattern, "/api/"):
t.Errorf("%s %s carries an /api/ prefix", r.Method, r.Pattern)
case strings.Contains(r.Pattern, "/v2"):
t.Errorf("%s %s is a v2", r.Method, r.Pattern)
}
}
if len(live.Routes) < 30 {
t.Fatalf("this gate examined %d routes; the measured demand is 36 ops", len(live.Routes))
}
}
// The op set and the plane contract are one set, read from two sources.
func TestEveryPlaneConstantIsAnOpAndViceVersa(t *testing.T) {
// One side is the LIVE ROUTER, the other is the plane package's own const block. A
// constant nobody registered is an op a consumer will call and get nothing from; an
// op whose token is not declared is a name a consumer cannot spell without copying a
// string. Both fail here rather than at the first call.
registered := app(t).Declaration().Ops
declared := planeConstants(t)
if len(registered) == 0 || len(declared) == 0 {
t.Fatal("this gate proved nothing: one of the two sets is empty")
}
in := func(set []string, v string) bool {
for _, s := range set {
if s == v {
return true
}
}
return false
}
for _, tok := range declared {
if !in(registered, tok) {
t.Errorf("plane declares the op token %q but nothing registers it", tok)
}
}
for _, tok := range registered {
if !in(declared, tok) {
t.Errorf("the router serves the op token %q but plane does not declare it, so a consumer cannot spell it", tok)
}
}
t.Logf("%d op tokens, declared and served", len(declared))
}
// §8.7 — tenancy, read off the published document so every input is covered.
func TestNoOperationTakesATenant(t *testing.T) {
spec, err := json.Marshal(app(t).OpenAPISpec())
if err != nil {
t.Fatal(err)
}
// Walk the WHOLE document rather than a list of types: a parameter name, a schema
// property, a $ref'd component — any of them naming a tenant is the defect, and
// walking everything means a new one cannot slip in beside a check that only looked
// at inputs it knew about.
var doc any
if err := json.Unmarshal(spec, &doc); err != nil {
t.Fatal(err)
}
banned := map[string]bool{"org": true, "orgId": true, "organization": true, "tenant": true, "tenantId": true}
seen := 0
var walk func(path string, v any)
walk = func(path string, v any) {
switch t2 := v.(type) {
case map[string]any:
for k, sub := range t2 {
seen++
// An org on a RESPONSE schema is a property of the thing described. The
// path tells them apart: only request bodies and parameters are inputs.
if banned[k] && (strings.Contains(path, "requestBody") || strings.Contains(path, "parameters")) {
t.Errorf("%s names %q: an org in the argument is an org the caller chose", path, k)
}
walk(path+"."+k, sub)
}
case []any:
for i, sub := range t2 {
if m, ok := sub.(map[string]any); ok {
if n, isStr := m["name"].(string); isStr && banned[n] && strings.Contains(path, "parameters") {
t.Errorf("%s[%d] is a %q parameter: a caller that can name the org can read another tenant", path, i, n)
}
}
walk(path, sub)
}
}
}
walk("", doc)
if seen < 200 {
t.Fatalf("this gate walked only %d nodes of the document; it did not read the spec it is protecting", seen)
}
t.Logf("walked %d document nodes", seen)
}
// Every op that reaches a cluster NAMES it. This is the multi-cluster invariant, gated.
func TestEveryClusterOpNamesItsCluster(t *testing.T) {
spec, err := json.Marshal(app(t).OpenAPISpec())
if err != nil {
t.Fatal(err)
}
var doc struct {
Paths map[string]map[string]struct {
OperationID string `json:"operationId"`
Parameters []struct {
Name string `json:"name"`
} `json:"parameters"`
RequestBody struct {
Content map[string]struct {
Schema struct {
Properties map[string]any `json:"properties"`
Ref string `json:"$ref"`
} `json:"schema"`
} `json:"content"`
} `json:"requestBody"`
} `json:"paths"`
}
if err := json.Unmarshal(spec, &doc); err != nil {
t.Fatal(err)
}
// The two ops that legitimately do not name a cluster: listing the registry, and
// registering a cluster (which is where a name comes to exist, so it takes `name`).
exempt := map[string]bool{"k8s_clusters_list": true, "k8s_clusters_register": true}
checked := 0
for p, methods := range doc.Paths {
for m, op := range methods {
if exempt[op.OperationID] {
continue
}
checked++
named := false
for _, param := range op.Parameters {
if param.Name == "cluster" {
named = true
}
}
for _, c := range op.RequestBody.Content {
if _, ok := c.Schema.Properties["cluster"]; ok {
named = true
}
// A $ref'd body carries the field through its component schema, which the
// plane package's own gate covers; a ref is therefore accepted here.
if c.Schema.Ref != "" {
named = true
}
}
if !named {
t.Errorf("%s %s (%s) does not name a cluster — there is no ambient cluster, so this op would read whichever apiserver the process was started next to",
m, p, op.OperationID)
}
}
}
if checked < 30 {
t.Fatalf("this gate examined %d operations; the measured demand is 36", checked)
}
t.Logf("%d operations, every one naming its cluster", checked)
}
// §8.2 — the binary builds and mounts its whole surface alone.
func TestBuildsAndMountsAlone(t *testing.T) {
bin := filepath.Join(t.TempDir(), name)
sh(t, "go", "build", "-o", bin, ".")
// `declare` exercises construct → register → project without a host, a store or a
// peer, which is exactly what "mounts alone" means for a request-driven app: if any
// of that needed something else to have initialised it first, this exits non-zero.
out := filepath.Join(t.TempDir(), "d.json")
sh(t, bin, "declare", out)
d := readJSON[zip.Declaration](t, out)
if len(d.Routes) == 0 {
t.Fatal("the binary mounted no routes when run with no host")
}
}
// §8.8 — the suite is not empty, and every package contributes.
func TestEveryPackageHasTests(t *testing.T) {
pkgs := strings.Fields(sh(t, "go", "list", "./..."))
if len(pkgs) < 4 {
t.Fatalf("this gate proved nothing: %d packages", len(pkgs))
}
for _, p := range pkgs {
dir := "."
if rel := strings.TrimPrefix(p, "github.com/hanzoai/k8s"); rel != "" {
dir = "." + rel
}
matches, err := filepath.Glob(filepath.Join(dir, "*_test.go"))
if err != nil || len(matches) == 0 {
t.Errorf("%s has no test file: a package with no executed test is a package the suite does not cover", p)
}
}
}
// §8.9 — CI invokes the gate. Without this every gate above protects nobody.
func TestCIInvokesMakeTest(t *testing.T) {
y := readFile(t, "hanzo.yml")
if !strings.Contains(y, "make test") {
t.Fatal("hanzo.yml's test: block does not run `make test`, so every gate in this file is a comment")
}
w := readFile(t, ".github/workflows/cicd.yml")
if !strings.Contains(w, "hanzoai/ci/.github/workflows/build.yml@v1") {
t.Fatal("the workflow does not import the canonical hanzoai/ci reusable, so this repo has its own build logic")
}
mk := readFile(t, "Makefile")
for _, target := range []string{"generate:", "test:"} {
if !strings.Contains(mk, target) {
t.Fatalf("the Makefile has no %s target", target)
}
}
// The regeneration gates above are worth nothing if `make test` does not run them.
if !strings.Contains(mk, "test ./...") {
t.Fatal("`make test` does not run the Go suite")
}
}
// ---- helpers ---------------------------------------------------------------
func readFile(t *testing.T, path string) string {
t.Helper()
raw, err := os.ReadFile(path)
if err != nil {
t.Fatalf("%s: %v", path, err)
}
return string(raw)
}
func readJSON[T any](t *testing.T, path string) T {
t.Helper()
var out T
if err := json.Unmarshal([]byte(readFile(t, path)), &out); err != nil {
t.Fatalf("%s: %v", path, err)
}
return out
}
// jsonDiff compares two values by their CANONICAL JSON.
//
// Both sides go through a marshal → unmarshal → marshal round trip, which is not
// ceremony: the live spec carries authored examples as json.RawMessage, whose field
// order is preserved verbatim, while the committed file re-parses into maps, whose keys
// marshal sorted. Comparing one against the other directly reports a diff on every
// example in the document and would have made this gate permanently red for no reason.
func jsonDiff(t *testing.T, live, committed any) string {
t.Helper()
a, err := canonical(live)
if err != nil {
t.Fatal(err)
}
b, err := canonical(committed)
if err != nil {
t.Fatal(err)
}
if string(a) == string(b) {
return ""
}
al, bl := strings.Split(string(a), "\n"), strings.Split(string(b), "\n")
var out []string
for i := 0; i < len(al) || i < len(bl); i++ {
var x, y string
if i < len(al) {
x = al[i]
}
if i < len(bl) {
y = bl[i]
}
if x != y {
out = append(out, "live: "+x, "committed: "+y)
if len(out) >= 12 {
break
}
}
}
return strings.Join(out, "\n")
}
// canonical renders a value as JSON with every map key sorted and every nested document
// re-parsed, so two spellings of one document compare equal.
func canonical(v any) ([]byte, error) {
raw, err := json.Marshal(v)
if err != nil {
return nil, err
}
var norm any
if err := json.Unmarshal(raw, &norm); err != nil {
return nil, err
}
return json.MarshalIndent(norm, "", " ")
}
// openAPIPath rewrites a router pattern into the document's spelling: ":id" is the
// router's, "{id}" is OpenAPI's, and the two must be compared in one vocabulary.
func openAPIPath(pattern string) string {
parts := strings.Split(pattern, "/")
for i, p := range parts {
if strings.HasPrefix(p, ":") {
parts[i] = "{" + p[1:] + "}"
}
}
return strings.Join(parts, "/")
}
// typedOpDirs returns every directory in this module that registers a typed op, read
// from the AST rather than from a list, so a new package is covered by §8.5 the moment
// it registers its first op.
func typedOpDirs(t *testing.T) []string {
t.Helper()
verbs := map[string]bool{"Get": true, "Post": true, "Put": true, "Patch": true, "Delete": true}
found := map[string]bool{}
err := filepath.WalkDir(".", func(path string, d fs.DirEntry, err error) error {
if err != nil || d.IsDir() || !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
return err
}
f, perr := parser.ParseFile(token.NewFileSet(), path, nil, 0)
if perr != nil {
return perr
}
ast.Inspect(f, func(n ast.Node) bool {
call, ok := n.(*ast.CallExpr)
if !ok {
return true
}
sel, ok := ast.Unparen(call.Fun).(*ast.SelectorExpr)
if !ok {
return true
}
pkg, ok := sel.X.(*ast.Ident)
if ok && pkg.Name == "zip" && verbs[sel.Sel.Name] && len(call.Args) >= 3 {
found[filepath.Dir(path)] = true
}
return true
})
return nil
})
if err != nil {
t.Fatalf("walk: %v", err)
}
if len(found) == 0 {
t.Fatal("this gate proved nothing: no package registers a typed op")
}
out := make([]string, 0, len(found))
for d := range found {
out = append(out, d)
}
sort.Strings(out)
return out
}
// planeConstants reads the op tokens out of the plane package's own const declarations,
// so the comparison against the router is source against source.
func planeConstants(t *testing.T) []string {
t.Helper()
f, err := parser.ParseFile(token.NewFileSet(), "plane/plane.go", nil, 0)
if err != nil {
t.Fatalf("parse plane: %v", err)
}
var out []string
ast.Inspect(f, func(n ast.Node) bool {
vs, ok := n.(*ast.ValueSpec)
if !ok || len(vs.Values) != 1 {
return true
}
lit, ok := vs.Values[0].(*ast.BasicLit)
if !ok || lit.Kind != token.STRING {
return true
}
v := strings.Trim(lit.Value, `"`)
if strings.HasPrefix(v, "k8s_") {
out = append(out, v)
}
return true
})
sort.Strings(out)
return out
}
// The RBAC ceiling and the code are one set, read from two sources.
func TestRBACCoversExactlyTheClosedKindSet(t *testing.T) {
// A kind added to the code with no rule here 403s in production; a rule here with no
// kind in the code is privilege nothing uses. Both are failures, and neither is
// visible without this comparison — which is why the grant lives in the repo that
// performs the verbs rather than in a chart nobody diffs against the code.
rbac := readFile(t, "rbac.yaml")
kinds := ops.Kinds()
if len(kinds) == 0 {
t.Fatal("this gate proved nothing: the kind set is empty")
}
for _, kind := range kinds {
// The rule names the plural RESOURCE, so the kind's own lower-cased plural is
// what has to appear. The two spellings differ only by pluralisation, and every
// kind in this app's table pluralises by adding "s".
resource := strings.ToLower(kind) + "s"
if !strings.Contains(rbac, resource) {
t.Errorf("rbac.yaml grants nothing for %s (%s): the op would 403 in production", kind, resource)
}
}
// The absence of a secrets grant is a design decision, so it is asserted rather than
// left to be quietly re-added by someone who needed one read.
// The comparison is on EXACT resource tokens, not on substrings: "kmssecrets" is the
// CR that exists precisely so no core Secret is ever read, and a substring check
// would flag the solution as the problem.
for _, line := range strings.Split(rbac, "\n") {
trimmed := strings.TrimSpace(line)
if !strings.HasPrefix(trimmed, "resources:") {
continue
}
list := strings.Trim(strings.TrimPrefix(trimmed, "resources:"), " []")
for _, item := range strings.Split(list, ",") {
if strings.TrimSpace(item) == "secrets" {
t.Errorf("rbac.yaml grants core secrets (%q) — material reaches a namespace as a KMSSecret, and nothing here reads a Secret back", trimmed)
}
}
}
t.Logf("%d custom-resource kinds, each with a rule", len(kinds))
}