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..660fa0a --- /dev/null +++ b/candy/plugin-cstream/params/decode_test.go @@ -0,0 +1,112 @@ +package params + +// decode_test.go — the ONE runtime path these generated structs participate in, pinned against +// the SOURCE OF TRUTH. +// +// 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 the claim in prose. +// +// The expected wire keys below are copied from `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 to do) makes the test vacuous, because a tag that drifts takes its +// derived expectation with it and the decode still lines up. Anchored to the schema instead, a +// drifted, emptied or removed tag leaves the field unpopulated and fails here. + +import ( + "encoding/json" + "fmt" + "reflect" + "strings" + "testing" +) + +// schemaKeys is the wire-key set declared by candy/plugin-cstream/schema/*.cue. +var schemaKeys = map[string]bool{ + "artifact": true, + "artifact_min_bytes": true, + "artifact_min_dimensions": true, + "artifact_not_uniform": true, + "expect": true, + "method": true, + "password": true, + "user": true, +} + +func TestProjectionMatchesSchemaWireKeys(t *testing.T) { + rt := reflect.TypeOf(CstreamInput{}) + ptr := reflect.New(rt) + wire := map[string]any{} + + exported := 0 + 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) — the schema's key cannot reach it", f.Name, key) + } + if !schemaKeys[key] { + t.Fatalf("field %s is tagged json:%q, which the schema candy/plugin-cstream/schema does NOT declare — the projection has drifted from its source of truth", f.Name, key) + } + 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") + } + + 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 { + found := false + for i := 0; i < rt.NumField(); i++ { + f := rt.Field(i) + if f.PkgPath != "" { + continue + } + if strings.Split(f.Tag.Get("json"), ",")[0] != key { + continue + } + found = true + if elem.Field(i).IsZero() { + missing = append(missing, fmt.Sprintf("%s (field %s)", key, f.Name)) + } + } + if !found { + missing = append(missing, key+" (no field carries it)") + } + } + if len(missing) > 0 { + t.Fatalf("%d wire key(s) were not delivered into the projection: %v", len(missing), missing) + } + t.Logf("verified %d exported field(s) against the schema's wire keys and decoded %d of them", exported, len(wire)) +}