The starter grant minted $5 into a wallet from middleware, on first credential contact, with no human in the loop. Credit into an org is an ADMIN decision -- made deliberately, through the admin surface, against an auditable ledger -- so an automatic path that creates money is not a feature to fix but a mechanism to remove. DELETED RATHER THAN SWITCHED OFF. A disabled money-mint is one flag away from an enabled one, and the flag is the kind of thing a later reader flips to "unblock" something. There is no starter code left to re-enable: the middleware, its mount in serve.go, the cross-process plane op (finance_starter / StarterIn / Granted) that let a non-ledger binary ask for it, and their tests are gone. Note this also removes the shared-signup-org exclusion that lived in the gate. It was sound anti-abuse for a grant that no longer exists, and keeping half a mechanism to guard the other half is how dead code survives. The paywall consequence is deliberate and is NOT taken here: SpendGate stays behind its kill switch. With no automatic funding, enforcing it 402s every new account from its first request -- an honest paywall, and a product decision that deserves its own change rather than arriving as a side effect of this one. Co-authored-by: Hanzo Dev <dev@hanzo.ai>
135 lines
5.1 KiB
Go
135 lines
5.1 KiB
Go
// Copyright © 2026 Hanzo AI. MIT License.
|
|
|
|
package cloud_test
|
|
|
|
// plane_encodable_test.go — a plane type that cannot be ENCODED is a door that is
|
|
// shut while every other signal says it is open.
|
|
//
|
|
// ObsErrorIn.Headers was a map[string]string. zapenc carries scalars, strings,
|
|
// byte slices, structs, pointers and slices, and REFUSES anything else at encode
|
|
// so a field can never silently fail to arrive — so every ObsErrorPost call died
|
|
// inside zip.Call, before the socket, in dur_ms=0. The Sentry envelope door
|
|
// answered 503 for 24h+ with the peer up, the socket bound and the op registered,
|
|
// which is why it read as an outage with no failing component: the request never
|
|
// left the caller.
|
|
//
|
|
// Nothing already in the suite could see it. The op's own tests call the handler
|
|
// directly (no encode), and the sibling op that shared the SAME socket — the
|
|
// obs_event_claim wire, two scalar fields — kept working, so POST /v1/event stayed
|
|
// 200 the whole time and pointed every investigation at routing. That op has since
|
|
// been retired with the dead LLM-obs write path it fed, so it is no longer walked
|
|
// below; the property it demonstrated is the reason this test walks KINDS and not a
|
|
// list of types, and it outlives any individual op.
|
|
|
|
import (
|
|
"context"
|
|
"reflect"
|
|
"testing"
|
|
|
|
"github.com/hanzoai/cloud"
|
|
"github.com/hanzoai/cloud/plane"
|
|
"github.com/zap-proto/zip"
|
|
)
|
|
|
|
// The BEHAVIOURAL half: the Sentry envelope's input must survive a real crossing.
|
|
// This fails on the map — zip.Call refuses to encode it — and passes on the list.
|
|
func TestObsErrorInCrossesThePlane(t *testing.T) {
|
|
t.Setenv("ZIP_RUNTIME_DIR", t.TempDir())
|
|
|
|
app := zip.New(zip.Config{AppName: "obsecho"})
|
|
zip.Post[plane.ObsErrorIn, plane.ObsErrorOut](app, "/obs/error/post",
|
|
func(_ context.Context, in *plane.ObsErrorIn) (*plane.ObsErrorOut, error) {
|
|
// Echo one header back as the body so a DROPPED header fails loudly
|
|
// rather than passing as an empty map would.
|
|
var got string
|
|
for _, h := range in.Headers {
|
|
if h.Name == "X-Sentry-Auth" {
|
|
got = h.Value
|
|
}
|
|
}
|
|
return &plane.ObsErrorOut{Status: 401, Body: []byte(got)}, nil
|
|
}, zip.WithOperationID("obs_error_post"))
|
|
go func() { _ = app.Listen(zip.SocketPath("obsecho")) }()
|
|
t.Cleanup(func() { _ = app.Shutdown() })
|
|
waitFor(t, "obsecho")
|
|
|
|
out, err := cloud.Ask[plane.ObsErrorIn, plane.ObsErrorOut](context.Background(),
|
|
"obsecho", "obs_error_post", &plane.ObsErrorIn{
|
|
Path: "/v1/event/6ba7b810-9dad-11d1-80b4-00c04fd430c8/envelope/",
|
|
Headers: []plane.Header{{Name: "X-Sentry-Auth", Value: "Sentry sentry_key=abc"}},
|
|
Body: []byte("{}"),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("the envelope input did not cross the plane: %v", err)
|
|
}
|
|
if out == nil {
|
|
t.Fatal("no answer crossed back")
|
|
}
|
|
// The runtime's status must arrive VERBATIM — a 401 "invalid ingest key" is
|
|
// the SDK's signal to stop retrying, and reshaping it into a 503 is what made
|
|
// every Sentry client retry a door that would never open.
|
|
if out.Status != 401 {
|
|
t.Fatalf("status %d crossed, want 401", out.Status)
|
|
}
|
|
if string(out.Body) != "Sentry sentry_key=abc" {
|
|
t.Fatalf("the DSN header did not survive the crossing: %q", out.Body)
|
|
}
|
|
}
|
|
|
|
// The STRUCTURAL half, and the one that keeps holding: no type on this plane may
|
|
// carry a kind zapenc refuses. Written as a field-kind walk rather than a list of
|
|
// known-bad types, because the failure is a property of the KIND — the next map
|
|
// added to any of these is the same 24h outage.
|
|
func TestNoPlaneTypeCarriesAnUnencodableKind(t *testing.T) {
|
|
types := []any{
|
|
plane.AuthorizeIn{}, plane.RecordIn{}, plane.BalanceIn{},
|
|
plane.SecretIn{}, plane.FilesIn{}, plane.Visibility{}, plane.ReserveIn{},
|
|
plane.ObsErrorIn{}, plane.ObsErrorOut{}, plane.Header{},
|
|
// ScopeRules is walked, not ScopeRule: the walk descends a slice of
|
|
// structs, so the row type is reached through the reply that carries it
|
|
// — which is also the only way it ever crosses.
|
|
plane.ScopeRules{},
|
|
plane.SlackSendIn{},
|
|
plane.StartIn{}, plane.Started{},
|
|
}
|
|
for _, v := range types {
|
|
walkEncodable(t, reflect.TypeOf(v), reflect.TypeOf(v).Name())
|
|
}
|
|
}
|
|
|
|
// walkEncodable asserts every field a plane type reaches is a kind zapenc carries.
|
|
func walkEncodable(t *testing.T, typ reflect.Type, path string) {
|
|
t.Helper()
|
|
for typ.Kind() == reflect.Pointer {
|
|
typ = typ.Elem()
|
|
}
|
|
if typ.Kind() != reflect.Struct {
|
|
return
|
|
}
|
|
for i := 0; i < typ.NumField(); i++ {
|
|
f := typ.Field(i)
|
|
ft := f.Type
|
|
for ft.Kind() == reflect.Pointer {
|
|
ft = ft.Elem()
|
|
}
|
|
where := path + "." + f.Name
|
|
switch ft.Kind() {
|
|
case reflect.Map, reflect.Interface, reflect.Chan, reflect.Func, reflect.Complex64, reflect.Complex128:
|
|
t.Errorf("%s is a %s — zapenc refuses it AT ENCODE, so every call carrying this "+
|
|
"type fails before the socket and the peer looks healthy while the door is shut. "+
|
|
"Carry it as a slice of structs.", where, ft.Kind())
|
|
case reflect.Slice, reflect.Array:
|
|
el := ft.Elem()
|
|
for el.Kind() == reflect.Pointer {
|
|
el = el.Elem()
|
|
}
|
|
if el.Kind() == reflect.Uint8 { // []byte is carried whole
|
|
continue
|
|
}
|
|
walkEncodable(t, el, where+"[]")
|
|
case reflect.Struct:
|
|
walkEncodable(t, ft, where)
|
|
}
|
|
}
|
|
}
|