mirror of
https://github.com/zap-proto/zip.git
synced 2026-10-19 15:38:57 +00:00
A typed op could only take one JSON value and answer one, so uploads, signed webhooks, form posts, YAML and merge-patch bodies, PDFs, SSE, WebSockets, OAuth redirects and upstream relays stayed untyped routes with no schema, tool, command or SDK method. Each is now a typed op and its REST wire is unchanged. Requests: a zip.Body field takes the body and Content-Type exactly as sent; an In implementing Parser decodes itself; form: fields and *zip.File parts bind url-encoded and multipart forms; cookie: binds a request cookie as an in: cookie parameter. Consumes names the media. Answers: zip.Body (bytes, filename, its own status and headers, a Reader streams chunked), Verbatim[T] (a relay documented as T), Sse[T] (events with keep-alive), Socket[M] (an upgrade after the contract runs; 426 without one, 501 over ZAP), Redirect, CookieCoder, Produces, and unions: a type whose OneOf method's results are the alternatives, with Or[A,B] as the stock one filing each under the status it states. Projections follow from the manifest: the document publishes binary media, form schemas, x-events, x-socket with 101, 3xx with Location and oneOf; MCP takes bytes as base64 and returns bytes, text or collected events as tool content, listing a socket op in _meta as no tool; the CLI reads --body @file|-, --field name=@file and --out, prints events as they arrive and bridges a socket to stdin and stdout. tools/list is ProjectMCP over the manifest, the projection MCPTools already was. Prose reaches promoted fields (filed under the declaring struct), a type defined over another struct, and the type arguments of zip's kinds and OneOf results; an instantiated generic is published under its name followed by its argument names instead of an illegal component key. The unused reflect-only document and tool builders go.
65 lines
2.0 KiB
Go
65 lines
2.0 KiB
Go
package zip
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
type hiddenIn struct{}
|
|
type hiddenOut struct {
|
|
OK bool `json:"ok"`
|
|
}
|
|
|
|
// Undeclared promises a route serves without appearing "in none of the
|
|
// projections built from [App.Declaration]: the OpenAPI document, the MCP tool
|
|
// list, the CLI commands, the by-name call plane."
|
|
//
|
|
// The MCP half was not held: composeTools reads Registry(), which is every
|
|
// registered op, while the undeclared fact rides the route entry. So an op
|
|
// deliberately kept out of the contract was still offered to an agent — the
|
|
// one projection where being callable matters most.
|
|
func TestAnUndeclaredTypedOpIsNotAnMCPTool(t *testing.T) {
|
|
app := New(Config{AppName: "hide"})
|
|
app.Get("/seen", func(ctx context.Context, in *hiddenIn) (*hiddenOut, error) {
|
|
return &hiddenOut{OK: true}, nil
|
|
})
|
|
app.Undeclared().Get("/unseen", func(ctx context.Context, in *hiddenIn) (*hiddenOut, error) {
|
|
return &hiddenOut{OK: true}, nil
|
|
})
|
|
|
|
tools, _, _ := app.composeTools()
|
|
var got []struct {
|
|
Name string `json:"name"`
|
|
}
|
|
if err := json.Unmarshal(tools, &got); err != nil {
|
|
t.Fatalf("tools: %v", err)
|
|
}
|
|
var names []string
|
|
for _, x := range got {
|
|
names = append(names, x.Name)
|
|
}
|
|
joined := strings.Join(names, " ")
|
|
if !strings.Contains(joined, "get_seen") {
|
|
t.Errorf("the declared op is missing from the tool list: %v", names)
|
|
}
|
|
if strings.Contains(joined, "get_unseen") {
|
|
t.Errorf("an UNDECLARED op was offered as an MCP tool: %v", names)
|
|
}
|
|
}
|
|
|
|
// The route still SERVES — hiding it from the contract must not unmount it.
|
|
func TestAnUndeclaredOpStillAnswers(t *testing.T) {
|
|
app := New(Config{AppName: "hide"})
|
|
app.Undeclared().Get("/unseen", func(ctx context.Context, in *hiddenIn) (*hiddenOut, error) {
|
|
return &hiddenOut{OK: true}, nil
|
|
})
|
|
if app.Declares("GET", "/unseen") {
|
|
t.Error("an undeclared route appeared in the declaration")
|
|
}
|
|
if len(app.Registry()) != 1 {
|
|
t.Errorf("the op left the registry entirely: %d", len(app.Registry()))
|
|
}
|
|
}
|