diff --git a/candy/plugin-cstream/params/cue_types_gen.go b/candy/plugin-cstream/params/cue_types_gen.go index 31d24d7..ef74c60 100644 --- a/candy/plugin-cstream/params/cue_types_gen.go +++ b/candy/plugin-cstream/params/cue_types_gen.go @@ -18,17 +18,17 @@ type CstreamInput struct { // The set is deliberately small. Each method is here because it can FAIL on a // defect nothing else catches: // - // status the gateway's own readiness view -- it reports ready only when a - // DRM render node exists AND the compositor has published a socket, - // which is strictly more than "the port answers" - // login one PAM authentication through the real session leader + // status the gateway's own readiness view -- it reports ready only when a + // DRM render node exists AND the compositor has published a socket, + // which is strictly more than "the port answers" + // login one PAM authentication through the real session leader // - // frame a frame pulled back THROUGH the negotiated WebRTC track. This is - // the method the verb ultimately exists for: `wl: screenshot` reads - // the COMPOSITOR and `stream-probe` reads the producer's own tap, so - // neither can fail on an encoding or transport defect. This one - // decodes what a real consumer receives, so it fails on a broken - // encoder, a broken negotiation, or a broken transport. + // frame a frame pulled back THROUGH the negotiated WebRTC track. This is + // the method the verb ultimately exists for: `wl: screenshot` reads + // the COMPOSITOR and `stream-probe` reads the producer's own tap, so + // neither can fail on an encoding or transport defect. This one + // decodes what a real consumer receives, so it fails on a broken + // encoder, a broken negotiation, or a broken transport. // // `frame` was deliberately withheld until it could be gated. The defect that // kept it unshippable is on record and fixed: gst-plugins-bad was missing, so @@ -40,7 +40,7 @@ type CstreamInput struct { // idle, input, logout) are NOT served yet: the control surface they read does // not exist in the streamer, and a method that cannot be exercised reads as // coverage while providing none. - Method string `yaml:"method,omitempty" json:"method"` + Method string `json:"method"` // user / password — `login` only. // @@ -57,15 +57,15 @@ type CstreamInput struct { // broker (root) execs the leader over a socketpair and the leader reads the // credential from STDIN, never argv. That discipline is deliberate and is // tested; this verb simply cannot reach it through the executor it is given. - User string `yaml:"user,omitempty" json:"user,omitempty"` + User string `json:"user,omitempty"` - Password string `yaml:"password,omitempty" json:"password,omitempty"` + Password string `json:"password,omitempty"` // expect — `login` only: whether this credential SHOULD be accepted. // A login probe that only ever checks the correct password passes just as // happily against a stack that accepts anything, so the rejecting case has to // be expressible. - Expect string `yaml:"expect,omitempty" json:"expect,omitempty"` + Expect string `json:"expect,omitempty"` // artifact — `frame` only: the HOST path the decoded frame is written to. // @@ -77,7 +77,7 @@ type CstreamInput struct { // Without a gate on the CONTENT this method would be theatre: a stream that // negotiates and sends nothing, or sends a uniform placeholder, is exactly the // failure mode it exists to catch. - Artifact string `yaml:"artifact,omitempty" json:"artifact,omitempty"` + Artifact string `json:"artifact,omitempty"` // The artifact VALIDATORS. These are not general #Op modifiers -- they left // core #Op in the schema-compaction cutover, so every verb that writes an @@ -89,9 +89,9 @@ type CstreamInput struct { // artifact_not_uniform is the one that matters: a stream that negotiates and // then sends a black or placeholder frame is precisely the failure this method // exists to catch, and a byte-count alone would pass it. - ArtifactMinBytes int `yaml:"artifact_min_bytes,omitempty" json:"artifact_min_bytes,omitempty"` + ArtifactMinBytes int `json:"artifact_min_bytes,omitempty"` - ArtifactMinDimensions string `yaml:"artifact_min_dimensions,omitempty" json:"artifact_min_dimensions,omitempty"` + ArtifactMinDimensions string `json:"artifact_min_dimensions,omitempty"` - ArtifactNotUniform bool `yaml:"artifact_not_uniform,omitempty" json:"artifact_not_uniform,omitempty"` + ArtifactNotUniform bool `json:"artifact_not_uniform,omitempty"` } diff --git a/candy/plugin-cstream/params/decode_test.go b/candy/plugin-cstream/params/decode_test.go new file mode 100644 index 0000000..42ea840 --- /dev/null +++ b/candy/plugin-cstream/params/decode_test.go @@ -0,0 +1,121 @@ +package params + +// decode_test.go — pins this generated projection against its SOURCE OF TRUTH, in both directions. +// +// charly hands a plugin its parameters as JSON (`spec.Op`'s params_json) and the plugin decodes +// that JSON into this projection; the sdk's kit.DecodeInput re-marshals to JSON before it +// unmarshals too. No YAML decoder ever sees these types — which is why the projection's `yaml:` +// tags are inert, and why this test pins the JSON side rather than asserting that claim in prose. +// +// The expected keys below are copied from the `#CstreamInput` definition in +// `candy/plugin-cstream/schema/*.cue` — the schema that GENERATES this file. They are hardcoded on +// purpose: deriving them from the struct's own `json` tags (the obvious thing) makes the test +// vacuous, because a tag that drifts takes its derived expectation with it and the decode still +// lines up. + +import ( + "encoding/json" + "fmt" + "reflect" + "strings" + "testing" +) + +// schemaKeys is the field set of #CstreamInput, taken from candy/plugin-cstream/schema. +var schemaKeys = []string{ + "artifact", + "artifact_min_bytes", + "artifact_min_dimensions", + "artifact_not_uniform", + "expect", + "method", + "password", + "user", +} + +func TestProjectionMatchesItsSchema(t *testing.T) { + rt := reflect.TypeOf(CstreamInput{}) + ptr := reflect.New(rt) + + // Direction 1: STRUCT -> SCHEMA. Every exported field must carry a json key the schema + // declares; a drifted, emptied or removed tag is caught here. + wire := map[string]any{} + exported := 0 + tagOf := map[string]string{} // json key -> Go field name + for i := 0; i < rt.NumField(); i++ { + f := rt.Field(i) + if f.PkgPath != "" { + continue // unexported + } + exported++ + key := strings.Split(f.Tag.Get("json"), ",")[0] + if key == "" || key == "-" { + t.Fatalf("field %s carries no usable json tag (%q) — no schema key can reach it", f.Name, key) + } + if !contains(schemaKeys, key) { + t.Fatalf("field %s is tagged json:%q, which #CstreamInput in candy/plugin-cstream/schema does NOT declare — the projection has drifted from its source of truth", f.Name, key) + } + tagOf[key] = f.Name + switch f.Type.Kind() { + case reflect.String: + wire[key] = "probe" + case reflect.Bool: + wire[key] = true + case reflect.Int, reflect.Int64: + wire[key] = 7 + case reflect.Float64: + wire[key] = 1.5 + case reflect.Slice: + wire[key] = []any{"probe"} + case reflect.Map: + wire[key] = map[string]any{"k": "v"} + default: + // nested structs/pointers/interfaces are not filled by a flat wire map + continue + } + } + if exported == 0 { + t.Fatal("the projection has no exported fields — the test would prove nothing") + } + + // Direction 2: SCHEMA -> STRUCT. Every key the schema declares must be carried by a field; + // a key the projection dropped is caught here, and it is checked against schemaKeys directly + // rather than against anything derived from the tags. + for _, key := range schemaKeys { + if _, ok := tagOf[key]; !ok { + t.Fatalf("schema key %q is declared by #CstreamInput but NO exported field carries it — the projection is missing part of its schema", key) + } + } + + b, err := json.Marshal(wire) + if err != nil { + t.Fatalf("marshal the wire map: %v", err) + } + // THE PATH: json -> struct, exactly `json.Unmarshal(req.GetParamsJson(), &op)`. + if err := json.Unmarshal(b, ptr.Interface()); err != nil { + t.Fatalf("decode the wire map into the projection: %v", err) + } + elem := ptr.Elem() + missing := []string{} + for key := range wire { + for i := 0; i < rt.NumField(); i++ { + f := rt.Field(i) + if f.PkgPath == "" && strings.Split(f.Tag.Get("json"), ",")[0] == key && elem.Field(i).IsZero() { + missing = append(missing, fmt.Sprintf("%s (field %s)", key, f.Name)) + } + } + } + if len(missing) > 0 { + t.Fatalf("%d wire key(s) were not delivered into the projection: %v", len(missing), missing) + } + t.Logf("both directions hold: %d exported field(s), %d schema key(s), %d decoded", exported, len(schemaKeys), len(wire)) +} + +func contains(hay []string, needle string) bool { + for _, h := range hay { + if h == needle { + return true + } + } + return false +}