From 9c38591f3e3d2ecff5a96e8d51b2925081d93c95 Mon Sep 17 00:00:00 2001 From: Andreas Trawoeger Date: Sun, 1 Nov 2026 23:11:57 +0100 Subject: [PATCH 01/10] 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 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 with the canonical instrument against main after that change: cue@v0.16.1 gengotypes vs the committed file -> 76 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-cua/params/cue_types_gen.go | 76 ++++++++++++------------ 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/candy/plugin-cua/params/cue_types_gen.go b/candy/plugin-cua/params/cue_types_gen.go index 60bd851..b73e4cf 100644 --- a/candy/plugin-cua/params/cue_types_gen.go +++ b/candy/plugin-cua/params/cue_types_gen.go @@ -26,111 +26,111 @@ package params type CuaInput struct { // method — the cua method to dispatch (also the scalar-sugar primary: // `cua: `). - Method CuaMethod `yaml:"method,omitempty" json:"method"` + Method CuaMethod `json:"method"` // --- connection ------------------------------------------------------- // host — the deployment/venue address hosting the Cua Driver. Omitted means // "the deploy I am running against": the plugin drives it over the host // reverse channel (the venue executor), which is the normal case. - Host string `yaml:"host,omitempty" json:"host,omitempty"` + Host string `json:"host,omitempty"` // driver — the `cua-driver` binary path inside the venue (default // /usr/local/bin/cua-driver). - Driver string `yaml:"driver,omitempty" json:"driver,omitempty"` + Driver string `json:"driver,omitempty"` // device — the name of a `kind: cua` DEVICE entity whose connection/permission // defaults this step inherits. Authored step fields WIN over the entity's. - Device string `yaml:"device,omitempty" json:"device,omitempty"` + Device string `json:"device,omitempty"` // env — extra session environment the driver needs inside the venue (the // desktop-session variables). Merged over the entity's env. - Env map[string]string `yaml:"env,omitempty" json:"env,omitempty"` + Env map[string]string `json:"env,omitempty"` // allow_control — REQUIRED to be true for every MUTATING method (input, // app control, config writes). Read-only methods ignore it. This is the // bed-safety gate: a live desktop is not a throwaway, and an unattended plan // must not type into it by accident. - AllowControl bool `yaml:"allow_control,omitempty" json:"allow_control,omitempty"` + AllowControl bool `json:"allow_control,omitempty"` // --- input (keyboard/pointer) ---------------------------------------- // text — the text `type` inserts. - Text string `yaml:"text,omitempty" json:"text,omitempty"` + Text string `json:"text,omitempty"` // key — the named key `press-key` presses (return, escape, tab, f5, …). - KeyName string `yaml:"key,omitempty" json:"key,omitempty"` + KeyName string `json:"key,omitempty"` // keys — the chord `hotkey` presses, e.g. ["ctrl","c"]. - Keys []string `yaml:"keys,omitempty" json:"keys,omitempty"` + Keys []string `json:"keys,omitempty"` // x / y — desktop-absolute pixel coordinates for click/move/scroll/drag. - X int `yaml:"x,omitempty" json:"x,omitempty"` + X int `json:"x,omitempty"` - Y int `yaml:"y,omitempty" json:"y,omitempty"` + Y int `json:"y,omitempty"` // from_x / from_y — drag start coordinates. - FromX int `yaml:"from_x,omitempty" json:"from_x,omitempty"` + FromX int `json:"from_x,omitempty"` - FromY int `yaml:"from_y,omitempty" json:"from_y,omitempty"` + FromY int `json:"from_y,omitempty"` // button — pointer button (left/right/middle; default left). - Button string `yaml:"button,omitempty" json:"button,omitempty"` + Button string `json:"button,omitempty"` // direction / amount — scroll direction (up/down/left/right) and notches. - Direction string `yaml:"direction,omitempty" json:"direction,omitempty"` + Direction string `json:"direction,omitempty"` - Amount int `yaml:"amount,omitempty" json:"amount,omitempty"` + Amount int `json:"amount,omitempty"` // pid / window_id — target a specific window for element/typed actions. - PID int `yaml:"pid,omitempty" json:"pid,omitempty"` + PID int `json:"pid,omitempty"` - WindowID int `yaml:"window_id,omitempty" json:"window_id,omitempty"` + WindowID int `json:"window_id,omitempty"` // element_index — the AX element index from a prior get-window-state. - ElementIndex int `yaml:"element_index,omitempty" json:"element_index,omitempty"` + ElementIndex int `json:"element_index,omitempty"` // delivery — the Cua delivery mode for an input action: background (default) // or foreground. Foreground briefly fronts the window; it is the documented // escalation when a background action is refused or unverifiable. - Delivery string `yaml:"delivery,omitempty" json:"delivery,omitempty"` + Delivery string `json:"delivery,omitempty"` // --- app / window ----------------------------------------------------- // app — the app name/bundle/launch-path for launch-app. - App string `yaml:"app,omitempty" json:"app,omitempty"` + App string `json:"app,omitempty"` // window_id / value — the window a set-window-frame targets, and its value. - Value string `yaml:"value,omitempty" json:"value,omitempty"` + Value string `json:"value,omitempty"` // width / height — rectangle size for set-window-frame. - Width int `yaml:"width,omitempty" json:"width,omitempty"` + Width int `json:"width,omitempty"` - Height int `yaml:"height,omitempty" json:"height,omitempty"` + Height int `json:"height,omitempty"` // --- raw escape hatch ------------------------------------------------- // tool — the Cua Driver tool `call` invokes (any tool the plugin has not typed // yet). Mutating by nature, so `call` requires allow_control. - Tool string `yaml:"tool,omitempty" json:"tool,omitempty"` + Tool string `json:"tool,omitempty"` // args — the JSON object of arguments for `call`. - Args string `yaml:"args,omitempty" json:"args,omitempty"` + Args string `json:"args,omitempty"` // --- artifact --------------------------------------------------------- // artifact — the host path `screenshot` writes the PNG to. - Artifact string `yaml:"artifact,omitempty" json:"artifact,omitempty"` + Artifact string `json:"artifact,omitempty"` // artifact_dir — the runner-injected generic evidence-artifact dir. - ArtifactDir string `yaml:"artifact_dir,omitempty" json:"artifact_dir,omitempty"` + ArtifactDir string `json:"artifact_dir,omitempty"` // artifact_min_bytes / artifact_min_dimensions / artifact_not_uniform / // artifact_contains_text — the post-run artifact-reality assertions // (sdk.RunArtifactValidators) this verb can produce. artifact_contains_text is // the OCR wait-for-screen primitive in `screenshot` form (tesseract on the HOST). - 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"` - ArtifactContainsText string `yaml:"artifact_contains_text,omitempty" json:"artifact_contains_text,omitempty"` + ArtifactContainsText string `json:"artifact_contains_text,omitempty"` } // #CuaMethod — the method catalog, grouped by intent so the read-only surface is @@ -147,25 +147,25 @@ type CuaMethod string // bed states them once. type CuaDeviceInput struct { // description — human label for the deployment/device. - Description string `yaml:"description,omitempty" json:"description,omitempty"` + Description string `json:"description,omitempty"` // driver — the `cua-driver` binary path inside the venue. - Driver string `yaml:"driver,omitempty" json:"driver,omitempty"` + Driver string `json:"driver,omitempty"` // host — the deployment/venue address (omit for "the deploy I run against"). - Host string `yaml:"host,omitempty" json:"host,omitempty"` + Host string `json:"host,omitempty"` // permission_mode — Cua Driver's own authorization mode. `standard` is the // promptless default; `bounded` admits only a reviewed manifest; `unrestricted` // requires an explicit danger acknowledgement. The plugin passes the mode to the // driver's launch, never overrides a running daemon's policy. - PermissionMode string `yaml:"permission_mode,omitempty" json:"permission_mode,omitempty"` + PermissionMode string `json:"permission_mode,omitempty"` // capability_manifest — the reviewed manifest path for `bounded` mode. - CapabilityManifest string `yaml:"capability_manifest,omitempty" json:"capability_manifest,omitempty"` + CapabilityManifest string `json:"capability_manifest,omitempty"` // env — extra session environment the driver needs inside the venue (the // desktop-session variables: XDG_RUNTIME_DIR, WAYLAND_DISPLAY, // HYPRLAND_INSTANCE_SIGNATURE, DBUS_SESSION_BUS_ADDRESS, YDOTOOL_SOCKET). - Env map[string]string `yaml:"env,omitempty" json:"env,omitempty"` + Env map[string]string `json:"env,omitempty"` } From 488546390b88e28fb3ae2024d42e70b453a2b138 Mon Sep 17 00:00:00 2001 From: Andreas Trawoeger Date: Sun, 1 Nov 2026 23:17:06 +0100 Subject: [PATCH 02/10] 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 a6332b54bee1742272398d84883c37a01f937d49 Mon Sep 17 00:00:00 2001 From: Andreas Trawoeger Date: Sun, 1 Nov 2026 23:21:44 +0100 Subject: [PATCH 03/10] 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 1d10d44457ae2ce88b694618eeff80eb22c9a044 Mon Sep 17 00:00:00 2001 From: Andreas Trawoeger Date: Sun, 1 Nov 2026 23:25:06 +0100 Subject: [PATCH 04/10] 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-cua/params/decode_test.go | 83 ++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 candy/plugin-cua/params/decode_test.go diff --git a/candy/plugin-cua/params/decode_test.go b/candy/plugin-cua/params/decode_test.go new file mode 100644 index 0000000..d397794 --- /dev/null +++ b/candy/plugin-cua/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(CuaInput{}) + + 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 CuaInput + // 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 d8f448f59996f88fb69c4c45137025a0385460d5 Mon Sep 17 00:00:00 2001 From: Andreas Trawoeger Date: Sun, 1 Nov 2026 23:28:09 +0100 Subject: [PATCH 05/10] test(params): anchor the wire keys to the schema, not to the projection's own tags The first version derived each expected key from the struct tag it checked, which made it vacuous: a drifted tag takes its derived expectation with it. Correcting a block that named exactly that. The expected keys now come from candy/plugin-cua/schema/*.cue, hardcoded on purpose. A tag that drifts, empties or disappears leaves a schema key with no field carrying it and the test fails; demonstrated by mutating a tag and restoring it. --- candy/plugin-cua/params/decode_test.go | 116 ++++++++++++++++++------- 1 file changed, 86 insertions(+), 30 deletions(-) diff --git a/candy/plugin-cua/params/decode_test.go b/candy/plugin-cua/params/decode_test.go index d397794..eff7577 100644 --- a/candy/plugin-cua/params/decode_test.go +++ b/candy/plugin-cua/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-cua/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,118 @@ import ( "testing" ) -func TestProjectionJSONDecodePopulatesTaggedFields(t *testing.T) { - rt := reflect.TypeOf(CuaInput{}) +// schemaKeys is the wire-key set declared by candy/plugin-cua/schema/*.cue. +var schemaKeys = map[string]bool{ + "allow_control": true, + "amount": true, + "app": true, + "args": true, + "artifact": true, + "artifact_contains_text": true, + "artifact_dir": true, + "artifact_min_bytes": true, + "artifact_min_dimensions": true, + "artifact_not_uniform": true, + "button": true, + "capability_manifest": true, + "delivery": true, + "description": true, + "device": true, + "direction": true, + "driver": true, + "element_index": true, + "env": true, + "from_x": true, + "from_y": true, + "height": true, + "host": true, + "key": true, + "keys": true, + "method": true, + "permission_mode": true, + "pid": true, + "text": true, + "tool": true, + "value": true, + "width": true, + "window_id": true, + "x": true, + "y": true, +} +func TestProjectionMatchesSchemaWireKeys(t *testing.T) { + rt := reflect.TypeOf(CuaInput{}) + 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-cua/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 CuaInput // 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)) } From 02e1e93ce04acb44348d47c36a121291e7b27d78 Mon Sep 17 00:00:00 2001 From: Andreas Trawoeger Date: Sun, 1 Nov 2026 23:33:13 +0100 Subject: [PATCH 06/10] test(params): check the projection against its schema in BOTH directions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous version iterated the struct's own tags to build the wire map, so its schema-to-struct arm was unreachable: every key in the map necessarily had a field. That left a dropped schema key undetectable, which is exactly the drift the test claims to catch. Both directions now run against the #Input definition in candy//schema/*.cue, hardcoded on purpose: struct -> schema catches a drifted/emptied/removed tag, and schema -> struct catches a key the projection dropped. Two executed negative controls prove it — a mutated tag and a deleted field each turn it red with a precise message, and restoring turns it green. Assisted-by: DeepSeek Harness deepseek-v4.1-flash (analysed on a live system) --- candy/plugin-cua/params/decode_test.go | 134 +++++++++++++------------ 1 file changed, 70 insertions(+), 64 deletions(-) diff --git a/candy/plugin-cua/params/decode_test.go b/candy/plugin-cua/params/decode_test.go index eff7577..be3b4f7 100644 --- a/candy/plugin-cua/params/decode_test.go +++ b/candy/plugin-cua/params/decode_test.go @@ -1,18 +1,17 @@ package params -// decode_test.go — the ONE runtime path these generated structs participate in, pinned against -// the SOURCE OF TRUTH. +// 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 the claim in prose. +// tags are inert, and why this test pins the JSON side rather than asserting that claim in prose. // -// The expected wire keys below are copied from `candy/plugin-cua/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. +// The expected keys below are copied from the `#CuaInput` definition in +// `candy/plugin-cua/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" @@ -22,51 +21,51 @@ import ( "testing" ) -// schemaKeys is the wire-key set declared by candy/plugin-cua/schema/*.cue. -var schemaKeys = map[string]bool{ - "allow_control": true, - "amount": true, - "app": true, - "args": true, - "artifact": true, - "artifact_contains_text": true, - "artifact_dir": true, - "artifact_min_bytes": true, - "artifact_min_dimensions": true, - "artifact_not_uniform": true, - "button": true, - "capability_manifest": true, - "delivery": true, - "description": true, - "device": true, - "direction": true, - "driver": true, - "element_index": true, - "env": true, - "from_x": true, - "from_y": true, - "height": true, - "host": true, - "key": true, - "keys": true, - "method": true, - "permission_mode": true, - "pid": true, - "text": true, - "tool": true, - "value": true, - "width": true, - "window_id": true, - "x": true, - "y": true, +// schemaKeys is the field set of #CuaInput, taken from candy/plugin-cua/schema. +var schemaKeys = []string{ + "allow_control", + "amount", + "app", + "args", + "artifact", + "artifact_contains_text", + "artifact_dir", + "artifact_min_bytes", + "artifact_min_dimensions", + "artifact_not_uniform", + "button", + "delivery", + "device", + "direction", + "driver", + "element_index", + "env", + "from_x", + "from_y", + "height", + "host", + "key", + "keys", + "method", + "pid", + "text", + "tool", + "value", + "width", + "window_id", + "x", + "y", } -func TestProjectionMatchesSchemaWireKeys(t *testing.T) { +func TestProjectionMatchesItsSchema(t *testing.T) { rt := reflect.TypeOf(CuaInput{}) ptr := reflect.New(rt) - wire := map[string]any{} + // 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 != "" { @@ -75,11 +74,12 @@ func TestProjectionMatchesSchemaWireKeys(t *testing.T) { 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) + t.Fatalf("field %s carries no usable json tag (%q) — no schema key can reach it", f.Name, key) } - if !schemaKeys[key] { - t.Fatalf("field %s is tagged json:%q, which the schema candy/plugin-cua/schema does NOT declare — the projection has drifted from its source of truth", f.Name, key) + if !contains(schemaKeys, key) { + t.Fatalf("field %s is tagged json:%q, which #CuaInput in candy/plugin-cua/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" @@ -102,6 +102,15 @@ func TestProjectionMatchesSchemaWireKeys(t *testing.T) { 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 #CuaInput 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) @@ -110,30 +119,27 @@ func TestProjectionMatchesSchemaWireKeys(t *testing.T) { 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() { + 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 !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)) + 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 } From 644b6ab78427ee13585f6b128188b07e5fdff5fa Mon Sep 17 00:00:00 2001 From: Andreas Trawoeger Date: Sun, 1 Nov 2026 23:42:51 +0100 Subject: [PATCH 07/10] chore(pr): re-freeze for the verbatim-evidence round Body corrected: the predecessor number was wrong (self-referential in two PRs), and the three go test captures are now raw output pasted as-is, with the per-repo controls driven by a field each repo actually has. Empty by design. Assisted-by: DeepSeek Harness deepseek-v4.1-flash (documentation reviewed) From 4525593a2d00dc6c5467a95553d45adfa6aa42f0 Mon Sep 17 00:00:00 2001 From: Andreas Trawoeger Date: Mon, 2 Nov 2026 00:02:27 +0100 Subject: [PATCH 08/10] chore(pr): re-freeze for the measurement-reconciliation round The body now pastes git's own diff --stat against origin/main and names the plain-diff count as the same pair aligned differently, instead of quoting one number the PR page contradicts. The round RCA names the missed control for the one-directional test: the negative control exercised only the arm I had already reasoned about, so the arm that could not fire was never asked to fire. Empty by design. Assisted-by: DeepSeek Harness deepseek-v4.1-flash (documentation reviewed) From d1d46eceb9d75c7a4f89b2b5fed7df7caff38cc5 Mon Sep 17 00:00:00 2001 From: Andreas Trawoeger Date: Mon, 2 Nov 2026 00:07:24 +0100 Subject: [PATCH 09/10] chore(pr): re-freeze for the per-repo measurement round The body inherited a sibling repo's alignment note; this repo's two views agree exactly at this file's own numbers, and the round RCA now carries them instead of another PR's. Empty by design. Assisted-by: DeepSeek Harness deepseek-v4.1-flash (documentation reviewed) From 6694570f3f6f4c3bea5f8c4968d55f5281693ef5 Mon Sep 17 00:00:00 2001 From: Andreas Trawoeger Date: Mon, 2 Nov 2026 00:13:42 +0100 Subject: [PATCH 10/10] test(params): cover CuaDeviceInput too, the second struct this diff touches The diff strips yaml tags from two generated structs, but the test reflected only CuaInput, so CuaDeviceInput and its six keys went unexercised. Both now run the same both-directions check against their own schema definition: #CuaInput 32/32, #CuaDeviceInput 6/6. The shared check lives in one helper rather than being copied, so a third projection costs a key list and a one-line test. Also measured for the record: for THIS schema the charly generator's output differs from the canonical one by 38 yaml lines and ZERO comment lines, so the doc-comment-reflow the body previously asserted does not apply here and no longer appears in it. Assisted-by: DeepSeek Harness deepseek-v4.1-flash (analysed on a live system) --- candy/plugin-cua/params/decode_test.go | 102 +++++++++++-------------- 1 file changed, 45 insertions(+), 57 deletions(-) diff --git a/candy/plugin-cua/params/decode_test.go b/candy/plugin-cua/params/decode_test.go index be3b4f7..bed15fc 100644 --- a/candy/plugin-cua/params/decode_test.go +++ b/candy/plugin-cua/params/decode_test.go @@ -1,17 +1,16 @@ package params -// decode_test.go — pins this generated projection against its SOURCE OF TRUTH, in both directions. +// decode_test.go — pins each 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:` +// that JSON into these projections; the sdk's kit.DecodeInput re-marshals to JSON before it +// unmarshals too. No YAML decoder ever sees these types — which is why the projections' `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 `#CuaInput` definition in -// `candy/plugin-cua/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. +// The expected keys are copied from the `#…` definitions in `candy/plugin-cua/schema/*.cue` — the +// schema that GENERATES this file. They are hardcoded on purpose: deriving them from a 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" @@ -21,44 +20,33 @@ import ( "testing" ) -// schemaKeys is the field set of #CuaInput, taken from candy/plugin-cua/schema. -var schemaKeys = []string{ - "allow_control", - "amount", - "app", - "args", - "artifact", - "artifact_contains_text", - "artifact_dir", - "artifact_min_bytes", - "artifact_min_dimensions", - "artifact_not_uniform", - "button", - "delivery", - "device", - "direction", - "driver", - "element_index", - "env", - "from_x", - "from_y", - "height", - "host", - "key", - "keys", - "method", - "pid", - "text", - "tool", - "value", - "width", - "window_id", - "x", - "y", +// cueInputKeys is the field set of #CuaInput in candy/plugin-cua/schema. +var cueInputKeys = []string{ + "allow_control", "amount", "app", "args", "artifact", "artifact_contains_text", + "artifact_dir", "artifact_min_bytes", "artifact_min_dimensions", "artifact_not_uniform", + "button", "delivery", "device", "direction", "driver", "element_index", "env", "from_x", + "from_y", "height", "host", "key", "keys", "method", "pid", "text", "tool", "value", + "width", "window_id", "x", "y", } -func TestProjectionMatchesItsSchema(t *testing.T) { - rt := reflect.TypeOf(CuaInput{}) +// cueDeviceKeys is the field set of #CuaDeviceInput — the OTHER generated struct this diff +// touches, so it carries its own coverage rather than riding on CuaInput's. +var cueDeviceKeys = []string{ + "capability_manifest", "description", "driver", "env", "host", "permission_mode", +} + +func TestCuaInputMatchesItsSchema(t *testing.T) { + checkProjection(t, reflect.TypeOf(CuaInput{}), cueInputKeys, "#CuaInput") +} + +func TestCuaDeviceInputMatchesItsSchema(t *testing.T) { + checkProjection(t, reflect.TypeOf(CuaDeviceInput{}), cueDeviceKeys, "#CuaDeviceInput") +} + +// checkProjection asserts BOTH directions between one generated struct and the schema definition +// that produced it, then decodes a wire map through it. +func checkProjection(t *testing.T, rt reflect.Type, schemaKeys []string, defName string) { + t.Helper() ptr := reflect.New(rt) // Direction 1: STRUCT -> SCHEMA. Every exported field must carry a json key the schema @@ -74,10 +62,10 @@ func TestProjectionMatchesItsSchema(t *testing.T) { 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) + t.Fatalf("%s: field %s carries no usable json tag (%q) — no schema key can reach it", defName, f.Name, key) } - if !contains(schemaKeys, key) { - t.Fatalf("field %s is tagged json:%q, which #CuaInput in candy/plugin-cua/schema does NOT declare — the projection has drifted from its source of truth", f.Name, key) + if !containsKey(schemaKeys, key) { + t.Fatalf("%s: field %s is tagged json:%q, which candy/plugin-cua/schema does NOT declare — the projection has drifted from its source of truth", defName, f.Name, key) } tagOf[key] = f.Name switch f.Type.Kind() { @@ -99,25 +87,25 @@ func TestProjectionMatchesItsSchema(t *testing.T) { } } if exported == 0 { - t.Fatal("the projection has no exported fields — the test would prove nothing") + t.Fatalf("%s: the projection has no exported fields — the test would prove nothing", defName) } - // 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. + // Direction 2: SCHEMA -> STRUCT. Every key the schema declares must be carried by a field; a + // key the projection dropped is caught here, 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 #CuaInput but NO exported field carries it — the projection is missing part of its schema", key) + t.Fatalf("%s: schema key %q is declared in candy/plugin-cua/schema but NO exported field carries it — the projection is missing part of its schema", defName, key) } } b, err := json.Marshal(wire) if err != nil { - t.Fatalf("marshal the wire map: %v", err) + t.Fatalf("%s: marshal the wire map: %v", defName, 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) + t.Fatalf("%s: decode the wire map into the projection: %v", defName, err) } elem := ptr.Elem() missing := []string{} @@ -130,12 +118,12 @@ func TestProjectionMatchesItsSchema(t *testing.T) { } } if len(missing) > 0 { - t.Fatalf("%d wire key(s) were not delivered into the projection: %v", len(missing), missing) + t.Fatalf("%s: %d wire key(s) were not delivered into the projection: %v", defName, len(missing), missing) } - t.Logf("both directions hold: %d exported field(s), %d schema key(s), %d decoded", exported, len(schemaKeys), len(wire)) + t.Logf("%s: both directions hold — %d exported field(s), %d schema key(s), %d decoded", defName, exported, len(schemaKeys), len(wire)) } -func contains(hay []string, needle string) bool { +func containsKey(hay []string, needle string) bool { for _, h := range hay { if h == needle { return true