Files
zeekayandHanzo Dev bbabddeaa8 examples: declare typed ops, because that is what the examples are teaching
Ten of eleven examples declared their surface with untyped handlers. An untyped
route registers no op, so every one of them modelled the single pattern that
leaves an endpoint out of the OpenAPI document, out of the MCP tool list, out of
the CLI and out of the call plane — while the README two files away calls that
registry the one source of truth. The examples are where the default is learned,
and they were teaching the wrong one.

hello, subsystem-mount, migrate-from-chi, migrate-from-beego, plugin/billing,
plugin/host and internal/testplugin now declare typed ops. The migrate-from-*
examples show the port as the two steps it is: mechanical first (same handler
shape, one edit, and invisible to every projection), typed second, because
stopping after the first leaves exactly the surface you were migrating away
from.

sse-streaming and websocket stay untyped and now say why in the file header: a
typed op is one decode, one call, one serialize, and a stream has no single
value to return while an upgrade has no response body at all. That is the whole
list of reasons — a stream, an upgrade, a proxied byte range, a non-JSON body.

express-in-zip registered two routes on app.Fiber(), BELOW zip entirely: no
*zip.Ctx, no error handler, no middleware, no Authorizer, and no projection —
served by the app but not governed by it, which is a security seam and not a
shortcut. Both move onto the zip router. /runtime/:lang becomes a typed op, so
"run this source in this language" is now in the document and is a tool an agent
can call; the legacy JS wildcard stays a wildcard and says what that costs.

testplugin's catch-all stays untyped for the reason it exists: it answers paths
that do not exist, so a host mounting it at two prefixes can tell its own 404
from the plugin's. A wildcard is deliberately not one named endpoint with one
schema.

Every example was run and probed, not just built: 30 assertions across all
eleven, including the openapi/mcp routes the typed ops now light up.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-28 16:38:21 -07:00

111 lines
3.5 KiB
Go

package main
import (
"io"
"net/http/httptest"
"strings"
"testing"
)
// TestExpressInZip is the proof point: the legacy TS handler in app.ts,
// transpiled by real esbuild, loaded into real goja, mounted on real
// Fiber via zip, exercised over a real HTTP roundtrip.
func TestExpressInZip(t *testing.T) {
app, err := setup()
if err != nil {
t.Fatal(err)
}
t.Run("GET /legacy/foo", func(t *testing.T) {
resp, err := app.Fiber().Test(httptest.NewRequest("GET", "/legacy/foo", nil))
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != 200 {
t.Fatalf("status = %d, want 200", resp.StatusCode)
}
body, _ := io.ReadAll(resp.Body)
assertContainsAll(t, string(body), `"ok":true`, `"path":"/foo"`, `"body":null`)
})
t.Run("POST /legacy/bar echoes body", func(t *testing.T) {
req := httptest.NewRequest("POST", "/legacy/bar", strings.NewReader(`{"x":1}`))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Fiber().Test(req)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != 200 {
t.Fatalf("status = %d, want 200", resp.StatusCode)
}
body, _ := io.ReadAll(resp.Body)
assertContainsAll(t, string(body), `"ok":true`, `"path":"/bar"`, `"x":1`)
})
}
// TestRuntimeRoute drives the unified runner over HTTP: :lang selects the
// backend and the body carries the source. js evaluates in real goja; an
// unregistered language is a 404 with a structured error.
//
// The body is the op's In as JSON, because the route is a typed op — which is
// what puts "run this source" in the document and in the MCP tool list. A raw
// body would have meant no schema, and no schema means no projection.
func TestRuntimeRoute(t *testing.T) {
app, err := setup()
if err != nil {
t.Fatal(err)
}
t.Run("POST /runtime/js evaluates source", func(t *testing.T) {
req := httptest.NewRequest("POST", "/runtime/js", strings.NewReader(`{"source":"40+2"}`))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Fiber().Test(req)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != 200 {
t.Fatalf("status = %d, want 200", resp.StatusCode)
}
body, _ := io.ReadAll(resp.Body)
// goja Export() of an integer arithmetic result is int64; JSON
// encodes it as the bare number 42.
assertContainsAll(t, string(body), `"result":42`)
})
t.Run("POST /runtime/cobol is 404 unknown language", func(t *testing.T) {
req := httptest.NewRequest("POST", "/runtime/cobol", strings.NewReader(`{"source":"DISPLAY 'HI'."}`))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Fiber().Test(req)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != 404 {
t.Fatalf("status = %d, want 404", resp.StatusCode)
}
body, _ := io.ReadAll(resp.Body)
assertContainsAll(t, string(body), `"error":"unknown language"`)
})
// The op is in the document because it IS an op — the assertion that says
// step 5 of setup() achieved something the fiber-level registration did not.
t.Run("the runtime op is projected", func(t *testing.T) {
spec := app.OpenAPISpec()
paths, _ := spec["paths"].(map[string]map[string]any)
if _, ok := paths["/runtime/{lang}"]; !ok {
t.Fatalf("the runtime op is missing from the document; paths = %v", paths)
}
if len(app.MCPTools()) != 1 {
t.Fatalf("tools = %v, want the one op", app.MCPTools())
}
})
}
func assertContainsAll(t *testing.T, body string, wants ...string) {
t.Helper()
for _, w := range wants {
if !strings.Contains(body, w) {
t.Fatalf("body %q missing %q", body, w)
}
}
}