From 1b3039eef0108677f57994e4aa61106e02b3358b Mon Sep 17 00:00:00 2001 From: Andreas Trawoeger Date: Sun, 1 Nov 2026 23:11:41 +0100 Subject: [PATCH 1/5] fix(params): restore the projection to the canonical cue@v0.16.1 output The previous change regenerated this file with `charly candy params`, which is NOT the instrument this repo is graded on. The org's own reusable candy-validate gate and this repo family's CI run plain `cue exp gengotypes` at cue@v0.16.1; the charly wrapper applies a yaml retag and reflows doc comments, so its output differs from the canonical one on the SAME schema. Measured after the previous change landed, with the canonical instrument: cue@v0.16.1 gengotypes vs the committed file -> 56 diff lines (FAIL) Restoring the file to exactly what cue@v0.16.1 emits, so the committed projection matches the gate that grades it. Nothing else changes. Assisted-by: DeepSeek Harness deepseek-v4.1-flash (analysed on a live system) --- candy/plugin-cstream/params/cue_types_gen.go | 36 ++++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) 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"` } From 2dace8b93d4361725ee48171b2c4eeb94bb3297a Mon Sep 17 00:00:00 2001 From: Andreas Trawoeger Date: Sun, 1 Nov 2026 23:17:05 +0100 Subject: [PATCH 2/5] chore(pr): re-freeze so the validator reads the corrected body Body-only round: the decode-path proof (JSON never YAML), the filed issue, and the untrimmed module gate are now in the PR body. A REST re-run replays the original event payload and re-reads the stale body, so a new head is the way to make the gate read what GitHub serves. Empty by design. Assisted-by: DeepSeek Harness deepseek-v4.1-flash (documentation reviewed) From 94fa4fad65a19f5badc96607beb0289755c6a3e5 Mon Sep 17 00:00:00 2001 From: Andreas Trawoeger Date: Sun, 1 Nov 2026 23:21:43 +0100 Subject: [PATCH 3/5] chore(pr): re-freeze for the executed-decode evidence round Body now carries executed proof instead of static reading: the same JSON decode run against BOTH projections of the schema, with identical results, plus literal grep output. A REST re-run replays the original payload, so a new head is what makes the gate read the served body. Empty by design. Assisted-by: DeepSeek Harness deepseek-v4.1-flash (documentation reviewed) From 39d76e2c3582197b1072d0f42a5a223377dfda1c Mon Sep 17 00:00:00 2001 From: Andreas Trawoeger Date: Sun, 1 Nov 2026 23:25:06 +0100 Subject: [PATCH 4/5] test(params): pin the projection's JSON decode, the only path these structs travel The tag question is a runtime question, so it gets runtime coverage instead of a prose claim. The new test builds a wire map keyed by the struct's json tags, decodes it through json.Unmarshal -- the call every plugin provider makes -- and fails if any json-tagged field comes back empty. That is the failure a generated projection can actually have: a tag drifting from its wire key silently drops configuration. Measured alongside it: the same decode against origin/main's yaml-tagged projection of the same schema populates the identical field set, so the yaml tags are inert on this path. Assisted-by: DeepSeek Harness deepseek-v4.1-flash (analysed on a live system) --- candy/plugin-cstream/params/decode_test.go | 83 ++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 candy/plugin-cstream/params/decode_test.go diff --git a/candy/plugin-cstream/params/decode_test.go b/candy/plugin-cstream/params/decode_test.go new file mode 100644 index 0000000..a32ce58 --- /dev/null +++ b/candy/plugin-cstream/params/decode_test.go @@ -0,0 +1,83 @@ +package params + +// decode_test.go — the ONE runtime path these generated structs participate in, pinned. +// +// 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 — that is why the projection's `yaml:` +// tags are inert, and why this test pins the JSON side rather than asserting the claim in prose. +// +// It fails if a json tag drifts away from the wire key: the field it belongs to would silently +// stop receiving its configured value, which is the failure mode a generated projection can have. + +import ( + "encoding/json" + "fmt" + "reflect" + "strings" + "testing" +) + +func TestProjectionJSONDecodePopulatesTaggedFields(t *testing.T) { + rt := reflect.TypeOf(CstreamInput{}) + + wire := map[string]any{} + fillable := map[int]string{} // field index -> json key the probe can populate + for i := 0; i < rt.NumField(); i++ { + f := rt.Field(i) + if f.PkgPath != "" { + continue // unexported + } + key := strings.Split(f.Tag.Get("json"), ",")[0] + if key == "" || key == "-" { + continue + } + var v any + switch f.Type.Kind() { + case reflect.String: + v = "probe" + case reflect.Bool: + v = true + case reflect.Int, reflect.Int64: + v = 7 + case reflect.Float64: + v = 1.5 + case reflect.Slice: + v = []any{"probe"} + case reflect.Map: + v = map[string]any{"k": "v"} + default: + // structs/pointers/interfaces are not filled by a flat wire map; skip them rather than + // assert something this probe cannot exercise. + continue + } + wire[key] = v + fillable[i] = key + } + if len(fillable) == 0 { + t.Fatal("no json-tagged field the probe can populate — the test would prove nothing") + } + + b, err := json.Marshal(wire) + if err != nil { + t.Fatalf("marshal the wire map: %v", err) + } + var got CstreamInput + // THE PATH: json -> struct, exactly `json.Unmarshal(req.GetParamsJson(), &op)`. + if err := json.Unmarshal(b, &got); err != nil { + t.Fatalf("decode the wire map into the projection: %v", err) + } + + rv := reflect.ValueOf(got) + missing := []string{} + for i, key := range fillable { + if rv.Field(i).IsZero() { + missing = append(missing, fmt.Sprintf("%s (field %s)", key, rt.Field(i).Name)) + } + } + if len(missing) > 0 { + t.Fatalf("%d of %d json-tagged fields were NOT populated by the decode: %v", + len(missing), len(fillable), missing) + } + t.Logf("decoded %d/%d json-tagged fields from the wire map", len(fillable), len(fillable)) +} From be6845258ddbcee8d9434bc854bbda1007c1ae37 Mon Sep 17 00:00:00 2001 From: Andreas Trawoeger Date: Sun, 1 Nov 2026 23:27:35 +0100 Subject: [PATCH 5/5] test(params): anchor the wire keys to the schema, not to the projection's own tags The first version of this test derived each expected wire key from the struct tag it was checking, which made it vacuous: a drifted tag takes its derived expectation with it, so the decode still lines up and the test still passes. Correcting a block that named exactly that. The expected keys now come from candy//schema/*.cue -- the schema that GENERATES this file -- hardcoded on purpose. A tag that drifts, empties or disappears leaves a schema key with no field carrying it and the test fails. Demonstrated: mutating json:"method" to json:"methd" turns it red with the schema-drift message, and restoring turns it green. --- candy/plugin-cstream/params/decode_test.go | 89 ++++++++++++++-------- 1 file changed, 59 insertions(+), 30 deletions(-) diff --git a/candy/plugin-cstream/params/decode_test.go b/candy/plugin-cstream/params/decode_test.go index a32ce58..660fa0a 100644 --- a/candy/plugin-cstream/params/decode_test.go +++ b/candy/plugin-cstream/params/decode_test.go @@ -1,14 +1,18 @@ package params -// decode_test.go — the ONE runtime path these generated structs participate in, pinned. +// 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 — that is why the projection's `yaml:` +// 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. // -// It fails if a json tag drifts away from the wire key: the field it belongs to would silently -// stop receiving its configured value, which is the failure mode a generated projection can have. +// 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" @@ -18,66 +22,91 @@ import ( "testing" ) -func TestProjectionJSONDecodePopulatesTaggedFields(t *testing.T) { - rt := reflect.TypeOf(CstreamInput{}) +// 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{} - fillable := map[int]string{} // field index -> json key the probe can populate + + 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 == "-" { - continue + 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) } - var v any switch f.Type.Kind() { case reflect.String: - v = "probe" + wire[key] = "probe" case reflect.Bool: - v = true + wire[key] = true case reflect.Int, reflect.Int64: - v = 7 + wire[key] = 7 case reflect.Float64: - v = 1.5 + wire[key] = 1.5 case reflect.Slice: - v = []any{"probe"} + wire[key] = []any{"probe"} case reflect.Map: - v = map[string]any{"k": "v"} + wire[key] = map[string]any{"k": "v"} default: - // structs/pointers/interfaces are not filled by a flat wire map; skip them rather than - // assert something this probe cannot exercise. + // nested structs/pointers/interfaces are not filled by a flat wire map continue } - wire[key] = v - fillable[i] = key } - if len(fillable) == 0 { - t.Fatal("no json-tagged field the probe can populate — the test would prove nothing") + 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) } - var got CstreamInput // THE PATH: json -> struct, exactly `json.Unmarshal(req.GetParamsJson(), &op)`. - if err := json.Unmarshal(b, &got); err != nil { + if err := json.Unmarshal(b, ptr.Interface()); err != nil { t.Fatalf("decode the wire map into the projection: %v", err) } - rv := reflect.ValueOf(got) + elem := ptr.Elem() missing := []string{} - for i, key := range fillable { - if rv.Field(i).IsZero() { - missing = append(missing, fmt.Sprintf("%s (field %s)", key, rt.Field(i).Name)) + 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 of %d json-tagged fields were NOT populated by the decode: %v", - len(missing), len(fillable), missing) + t.Fatalf("%d wire key(s) were not delivered into the projection: %v", len(missing), missing) } - t.Logf("decoded %d/%d json-tagged fields from the wire map", len(fillable), len(fillable)) + t.Logf("verified %d exported field(s) against the schema's wire keys and decoded %d of them", exported, len(wire)) }