diff --git a/src/envelope.rs b/src/envelope.rs index 05bc841..dfa5b06 100644 --- a/src/envelope.rs +++ b/src/envelope.rs @@ -107,6 +107,60 @@ mod tests { assert_eq!(typed["results"], serde_json::json!(["x", "y"])); } + #[test] + fn resource_is_byte_identical_to_hand_rolled_json() { + // Same migration guarantee the action/versioned dialects assert: the + // resource dialect must serialize identically to the hand-rolled form + // it replaced, so adopting the helper is never a wire change. + let built = resource( + 1, + "plugins", + vec![serde_json::json!({ "name": "github", "version": "0.4.0" })], + ); + let hand = serde_json::json!({ + "schema_version": 1, + "plugins": [{ "name": "github", "version": "0.4.0" }], + }); + assert_eq!( + serde_json::to_string_pretty(&built).unwrap(), + serde_json::to_string_pretty(&hand).unwrap() + ); + } + + #[test] + fn versioned_flattens_a_serialized_struct_alongside_schema_version() { + // The shape `plugins validate` / `lanes validate` emit: a report struct + // is serialized to a Value, then wrapped. The report's own fields must + // stay at the top level next to `schema_version`, not nested under a + // key, and the result must match the hand-rolled equivalent. + #[derive(Serialize)] + struct Report { + path: String, + lane_count: usize, + errors: Vec, + warnings: Vec, + } + let report = Report { + path: "fledge.toml".to_string(), + lane_count: 3, + errors: vec![], + warnings: vec!["unpinned step".to_string()], + }; + + let built = versioned(1, serde_json::to_value(&report).unwrap()); + let hand = serde_json::json!({ + "schema_version": 1, + "path": "fledge.toml", + "lane_count": 3, + "errors": [], + "warnings": ["unpinned step"], + }); + assert_eq!( + serde_json::to_string_pretty(&built).unwrap(), + serde_json::to_string_pretty(&hand).unwrap() + ); + } + #[test] fn action_leads_with_schema_version_and_action_then_merges_fields() { let out = action( diff --git a/src/lanes/mod.rs b/src/lanes/mod.rs index 0de4771..f0df252 100644 --- a/src/lanes/mod.rs +++ b/src/lanes/mod.rs @@ -40,6 +40,7 @@ pub(super) const LANES_SEARCH_SCHEMA: u32 = 1; pub(super) const LANES_IMPORT_SCHEMA: u32 = 1; pub(super) const LANES_CREATE_SCHEMA: u32 = 1; pub(super) const LANES_PUBLISH_SCHEMA: u32 = 1; +pub(super) const LANES_VALIDATE_SCHEMA: u32 = 1; #[derive(Debug, Deserialize)] pub(super) struct FledgeFileWithLanes { diff --git a/src/lanes/validate.rs b/src/lanes/validate.rs index 575382f..c597528 100644 --- a/src/lanes/validate.rs +++ b/src/lanes/validate.rs @@ -174,7 +174,11 @@ pub(crate) fn print_lane_report( ) -> Result<()> { if json { // Wrap with schema_version envelope (matches lanes list/run/search shape). - let value = crate::envelope::versioned(1, serde_json::to_value(report)?); + // The version comes from the named per-command constant rather than a + // literal so a future shape change bumps it in the same place as every + // other `lanes` envelope. + let value = + crate::envelope::versioned(super::LANES_VALIDATE_SCHEMA, serde_json::to_value(report)?); println!("{}", serde_json::to_string_pretty(&value)?); } else if report.errors.is_empty() && report.warnings.is_empty() { println!( diff --git a/src/plugin/mod.rs b/src/plugin/mod.rs index beb066b..75afef7 100644 --- a/src/plugin/mod.rs +++ b/src/plugin/mod.rs @@ -71,6 +71,7 @@ const PLUGINS_SEARCH_SCHEMA: u32 = 1; const PLUGINS_RECOMMEND_SCHEMA: u32 = 1; const PLUGINS_CREATE_SCHEMA: u32 = 1; const PLUGINS_PUBLISH_SCHEMA: u32 = 1; +const PLUGINS_VALIDATE_SCHEMA: u32 = 1; // ─── Types ─────────────────────────────────────────────────────────────────── diff --git a/src/plugin/validate.rs b/src/plugin/validate.rs index 8845506..eb5ddcf 100644 --- a/src/plugin/validate.rs +++ b/src/plugin/validate.rs @@ -166,7 +166,13 @@ pub(crate) fn print_plugin_report( // schema_version (matches plugins list/audit/search shape). // The full report is flattened so existing fields (path, // plugin_name, errors, warnings) sit at the same level. - let value = crate::envelope::versioned(1, serde_json::to_value(report)?); + // The version comes from the named per-command constant rather than a + // literal so a future shape change bumps it in the same place as every + // other `plugins` envelope. + let value = crate::envelope::versioned( + super::PLUGINS_VALIDATE_SCHEMA, + serde_json::to_value(report)?, + ); println!("{}", serde_json::to_string_pretty(&value)?); } else if report.errors.is_empty() && report.warnings.is_empty() { let name = if report.plugin_name.is_empty() {