diff --git a/candy/plugin-media/params/cue_types_gen.go b/candy/plugin-media/params/cue_types_gen.go index 967a76b..ac22a4f 100644 --- a/candy/plugin-media/params/cue_types_gen.go +++ b/candy/plugin-media/params/cue_types_gen.go @@ -32,23 +32,23 @@ type TranscodeInput struct { // capture session flushed). The bed-runner evidence phase threads the entry's // primary artifact here; a direct/plan-step dispatch may set it explicitly. // Empty → the provider falls back to the check env's source_artifact. - SourceArtifact string `yaml:"source_artifact,omitempty" json:"source_artifact,omitempty"` + SourceArtifact string `json:"source_artifact,omitempty"` // to — the CONTAINER FORMAT to transcode the source to. Single-purpose verb: // only "mp4" is supported (the default; an empty/omitted value means mp4). The // transcode is H.264 + yuv420p in an MP4 container, so the artifact plays // everywhere (the ffmpeg argv: -c:v libx264 -pix_fmt yuv420p). - To string `yaml:"to,omitempty" json:"to,omitempty"` + To string `json:"to,omitempty"` // artifact — the HOST path the transcoded file is written to. Empty (the // common case) → derived from the source path by swapping its extension to // ".mp4" (the source MJPEG the capture session flushed is already host-side). - Artifact string `yaml:"artifact,omitempty" json:"artifact,omitempty"` + Artifact string `json:"artifact,omitempty"` // artifact_min_bytes — the post-transcode artifact-reality assertion: the // output MP4 must be at least this many bytes (sdk.RunArtifactValidators, // shared implementation). - ArtifactMinBytes int `yaml:"artifact_min_bytes,omitempty" json:"artifact_min_bytes,omitempty"` + ArtifactMinBytes int `json:"artifact_min_bytes,omitempty"` // artifact_not_uniform — the SOURCE-frame motion assertion (the RDD-3 binding // of the nested-capture plan): evaluated on the SOURCE MJPEG frames (the @@ -57,5 +57,5 @@ type TranscodeInput struct { // source MJPEG stream into its constituent JPEGs, decodes a stream-wide // sample, and requires at least two distinct frame signatures: identical // frames FAIL honestly (a real capture shows motion), differing frames PASS. - ArtifactNotUniform bool `yaml:"artifact_not_uniform,omitempty" json:"artifact_not_uniform,omitempty"` + ArtifactNotUniform bool `json:"artifact_not_uniform,omitempty"` } diff --git a/candy/plugin-media/params/decode_test.go b/candy/plugin-media/params/decode_test.go new file mode 100644 index 0000000..8c87bf3 --- /dev/null +++ b/candy/plugin-media/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(TranscodeInput{}) + + 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 TranscodeInput + // 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)) +}