Skip to content

Commit 883fc0d

Browse files
committed
test(config): set-equality guard for ActionType vs schema actionType.enum
Adding or renaming an ActionType variant in Rust without the matching change to $defs.actionType.enum in config.v2.schema.json (or vice versa) silently breaks saving/exporting any profile using it - the same class of bug that once hit repairClipboard. action_type_set_matches_schema_enum asserts the two list the same set; an always-on exhaustive match forces a new variant to be handled here and in the schema. Mirrors control_id_as_str_matches_serde_rename; runs under ci.yml cargo test and is clean under clippy -D warnings.
1 parent ea7f912 commit 883fc0d

1 file changed

Lines changed: 59 additions & 0 deletions

File tree

src-tauri/src/config.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -528,8 +528,44 @@ impl ActionType {
528528
ActionType::RepairClipboard => "repairClipboard",
529529
}
530530
}
531+
532+
/// Every variant, in declaration order. Test-only: the set-equality guard
533+
/// `action_type_set_matches_schema_enum` iterates this and asserts it equals
534+
/// `$defs.actionType.enum` in `schemas/config.v2.schema.json`. The always-on
535+
/// `match` below forces a new variant to be added here and to the schema
536+
/// (a missed schema entry is a save-breaker).
537+
#[cfg(test)]
538+
const ALL: [ActionType; 10] = [
539+
ActionType::Shortcut,
540+
ActionType::TextSnippet,
541+
ActionType::Sequence,
542+
ActionType::Launch,
543+
ActionType::Menu,
544+
ActionType::MouseAction,
545+
ActionType::MediaKey,
546+
ActionType::ProfileSwitch,
547+
ActionType::Disabled,
548+
ActionType::RepairClipboard,
549+
];
531550
}
532551

552+
// Compile-time exhaustiveness: adding an ActionType variant breaks this match
553+
// (no `_` arm), forcing you to also update `ActionType::ALL` above and
554+
// `$defs.actionType.enum` in schemas/config.v2.schema.json — the JSON contract a
555+
// saved config is validated against, where a missed entry is a save-breaker.
556+
const _: fn(ActionType) = |a| match a {
557+
ActionType::Shortcut
558+
| ActionType::TextSnippet
559+
| ActionType::Sequence
560+
| ActionType::Launch
561+
| ActionType::Menu
562+
| ActionType::MouseAction
563+
| ActionType::MediaKey
564+
| ActionType::ProfileSwitch
565+
| ActionType::Disabled
566+
| ActionType::RepairClipboard => {}
567+
};
568+
533569
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
534570
#[serde(tag = "type", rename_all = "camelCase")]
535571
pub enum ActionCondition {
@@ -4228,6 +4264,29 @@ mod edge_proptests {
42284264
}
42294265
}
42304266

4267+
/// ActionType (Rust) and `$defs.actionType.enum` in config.v2.schema.json must
4268+
/// list the SAME set. The schema is the contract a saved config is validated
4269+
/// against; a variant in one but not the other is a save-breaker (this is how
4270+
/// `repairClipboard` once broke saving). The per-variant tests above only cover
4271+
/// known cases — this closes the whole class against a future added/renamed type.
4272+
#[test]
4273+
fn action_type_set_matches_schema_enum() {
4274+
let schema: Value =
4275+
serde_json::from_str(CONFIG_SCHEMA_JSON).expect("schema is valid JSON");
4276+
let schema_enum: std::collections::BTreeSet<&str> = schema["$defs"]["actionType"]["enum"]
4277+
.as_array()
4278+
.expect("$defs.actionType.enum is an array")
4279+
.iter()
4280+
.map(|v| v.as_str().expect("actionType.enum values are strings"))
4281+
.collect();
4282+
let rust: std::collections::BTreeSet<&str> =
4283+
ActionType::ALL.iter().map(|a| a.as_str()).collect();
4284+
assert_eq!(
4285+
rust, schema_enum,
4286+
"ActionType (Rust) and config.v2.schema.json $defs.actionType.enum drifted apart"
4287+
);
4288+
}
4289+
42314290
/// Layer::as_str() must match its serde JSON representation.
42324291
#[test]
42334292
fn layer_as_str_matches_serde_rename() {

0 commit comments

Comments
 (0)