Files
zeekayandHanzo Dev c53d10fe4b A README to build a service from, and examples/local-service
The README gave no version to install and no service to run, never
mentioned zipdoc, Described, ErrorHandler, the call plane's field-order
rule, zipgen, CommandsFromSpec, Authorize or the MCP door's limits, and
named APIs that do not exist (app.Add, app.Mount, a zip.Load that takes a
prefix first). It now takes one service through REST, MCP, OpenAPI and the
CLI with output from real runs, then covers each projection, the error
format and the release that introduced it, and the security model.

examples/local-service is that service: one SQLite-backed operation, doc
comments run through zipdoc, a .mcp.json for Claude Code, and a test that
checks every operation against every projection. It is a module of its own
so the SQLite driver stays out of zip's graph. CI runs zipdoc -check and
its tests.

LLM.md is now a map of the code, the test commands, the release steps and
the invariants that hold today.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-11 10:36:14 -07:00

164 lines
4.5 KiB
Go

package main
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http/httptest"
"path/filepath"
"strings"
"testing"
"github.com/zap-proto/zip"
)
// newService is the service over a fresh SQLite file.
func newService(t *testing.T) (*zip.App, *Store) {
t.Helper()
s := &Store{}
if err := s.Open(filepath.Join(t.TempDir(), "notes.db")); err != nil {
t.Fatal(err)
}
t.Cleanup(func() { s.db.Close() })
return New(s), s
}
// call sends one HTTP request to the app without opening a socket.
func call(t *testing.T, app *zip.App, method, path, body string) (int, string, string) {
t.Helper()
req := httptest.NewRequest(method, path, strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
b, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
return resp.StatusCode, resp.Header.Get("Content-Type"), string(b)
}
// TestEveryOperationIsInEveryProjection reads the router and checks that each
// typed operation is also an OpenAPI operation, an MCP tool and a CLI command,
// under one id. An operation added to New is covered with no change here.
func TestEveryOperationIsInEveryProjection(t *testing.T) {
app, _ := newService(t)
_, _, body := call(t, app, "GET", zip.SpecPath, "")
var spec struct {
Paths map[string]map[string]struct {
OperationID string `json:"operationId"`
Description string `json:"description"`
} `json:"paths"`
}
if err := json.Unmarshal([]byte(body), &spec); err != nil {
t.Fatalf("OpenAPI document: %v", err)
}
_, _, body = call(t, app, "POST", "/mcp", `{"jsonrpc":"2.0","id":1,"method":"tools/list"}`)
var list struct {
Result struct {
Tools []struct {
Name string `json:"name"`
} `json:"tools"`
} `json:"result"`
}
if err := json.Unmarshal([]byte(body), &list); err != nil {
t.Fatalf("tools/list: %v", err)
}
tools := map[string]bool{}
for _, tool := range list.Result.Tools {
tools[tool.Name] = true
}
commands := map[string]bool{}
for _, c := range app.Commands() {
commands[c.OperationID] = true
}
ops := 0
for _, r := range app.Routes() {
if r.Op == "" {
continue // an untyped route has no projections
}
ops++
op := spec.Paths[zip.Template(r.Pattern)][strings.ToLower(r.Method)]
if op.OperationID != r.Op {
t.Errorf("%s %s: OpenAPI operationId %q, want %q", r.Method, r.Pattern, op.OperationID, r.Op)
}
if op.Description == "" {
t.Errorf("%s %s: no description; run go generate", r.Method, r.Pattern)
}
if !tools[r.Op] {
t.Errorf("%s %s: no MCP tool %q", r.Method, r.Pattern, r.Op)
}
if !commands[r.Op] {
t.Errorf("%s %s: no CLI command %q", r.Method, r.Pattern, r.Op)
}
}
if ops == 0 {
t.Fatal("no typed operations registered")
}
}
// TestOneHandlerBehindEveryDoor calls the operation over REST, MCP and the CLI,
// then counts the rows: three doors, one handler, one table.
func TestOneHandlerBehindEveryDoor(t *testing.T) {
app, s := newService(t)
code, _, body := call(t, app, "POST", "/v1/notes", `{"text":"over REST"}`)
if code != 200 || body != `{"id":1,"text":"over REST"}` {
t.Errorf("REST: %d %s", code, body)
}
_, _, body = call(t, app, "POST", "/mcp",
`{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"post_notes","arguments":{"text":"over MCP"}}}`)
if !strings.Contains(body, `{\"id\":2,\"text\":\"over MCP\"}`) {
t.Errorf("MCP: %s", body)
}
var out bytes.Buffer
cli := app.CLI()
cli.Out = &out
if err := cli.Run(context.Background(), []string{"notes", "create", "--text", "over the CLI"}); err != nil {
t.Fatalf("CLI: %v", err)
}
if !strings.Contains(out.String(), `"id": 3`) {
t.Errorf("CLI: %s", out.String())
}
var n int
if err := s.db.QueryRow(`SELECT count(*) FROM notes`).Scan(&n); err != nil {
t.Fatal(err)
}
if n != 3 {
t.Errorf("notes table has %d rows, want 3", n)
}
}
// TestARefusalIsAProblemDocument checks the error body: RFC 9457, not a shape of
// zip's own.
func TestARefusalIsAProblemDocument(t *testing.T) {
app, _ := newService(t)
code, typ, body := call(t, app, "POST", "/v1/notes", `{}`)
if code != 400 || typ != "application/problem+json" {
t.Fatalf("empty note: %d %s %s", code, typ, body)
}
var problem struct {
Type string `json:"type"`
Title string `json:"title"`
Status int `json:"status"`
Detail string `json:"detail"`
}
if err := json.Unmarshal([]byte(body), &problem); err != nil {
t.Fatal(err)
}
if problem.Type != "about:blank" || problem.Title != "Bad Request" || problem.Status != 400 || problem.Detail == "" {
t.Errorf("problem document: %s", body)
}
}