Files
zip/streamop_test.go
zeekay 999e2153b3 Type every wire: bytes, forms, streams, sockets, redirects, unions
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.
2026-10-09 09:42:00 -07:00

89 lines
2.7 KiB
Go

// SPDX-License-Identifier: BSD-3-Clause-Eco
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package zip_test
import (
"encoding/json"
"strings"
"testing"
"github.com/zap-proto/zip"
)
// A streamed answer is published as the stream it is. Without the manifest's
// stream field an op with no Out projected as a 204, which is a document
// promising an empty answer from an op that answers for minutes.
func streamed() zip.Manifest {
return zip.Manifest{
Manifest: zip.ManifestVersion,
App: "chat",
Ops: []zip.ManifestOp{
{Method: "POST", Path: "/v1/chat", In: "Prompt", Stream: "sse"},
{Method: "POST", Path: "/v1/relay", Raw: true, Stream: "bytes"},
},
Types: []zip.TypeDesc{{
ID: "Prompt", Name: "Prompt", Kind: "struct",
Fields: []zip.FieldDesc{{Name: "text", JSON: "text", Type: zip.TypeRef{Prim: "string"}}},
}},
}
}
func TestProjectOpenAPIStreams(t *testing.T) {
m := streamed()
if err := m.Check(); err != nil {
t.Fatal(err)
}
m.Settle()
b, err := json.Marshal(zip.ProjectOpenAPI(m))
if err != nil {
t.Fatal(err)
}
var doc struct {
Paths map[string]map[string]struct {
RequestBody struct {
Content map[string]json.RawMessage `json:"content"`
} `json:"requestBody"`
Responses map[string]struct {
Content map[string]json.RawMessage `json:"content"`
} `json:"responses"`
} `json:"paths"`
}
if err := json.Unmarshal(b, &doc); err != nil {
t.Fatal(err)
}
chat := doc.Paths["/v1/chat"]["post"]
if _, ok := chat.Responses["200"].Content["text/event-stream"]; !ok {
t.Errorf("chat answers %s, want a text/event-stream 200", b)
}
if _, ok := chat.Responses["204"]; ok {
t.Errorf("chat is published as answering nothing")
}
if _, ok := chat.RequestBody.Content["application/json"]; !ok {
t.Errorf("chat's prompt is not a JSON body")
}
relay := doc.Paths["/v1/relay"]["post"]
if got := string(relay.Responses["200"].Content["application/octet-stream"]); !strings.Contains(got, `"binary"`) {
t.Errorf("relay answers %q, want binary", got)
}
if got := string(relay.RequestBody.Content["application/octet-stream"]); !strings.Contains(got, `"binary"`) {
t.Errorf("relay reads %q, want binary", got)
}
}
func TestCheckRefusesACrookedStream(t *testing.T) {
for name, op := range map[string]zip.ManifestOp{
"unknown stream": {Method: "GET", Path: "/a", Stream: "ws"},
"socket and out": {Method: "GET", Path: "/a", Stream: "socket", Out: "Prompt"},
"raw and in": {Method: "POST", Path: "/a", Raw: true, In: "Prompt"},
} {
m := streamed()
m.Ops = []zip.ManifestOp{op}
if err := m.Check(); err == nil {
t.Errorf("%s: checked clean", name)
}
}
}