From adb44a14a8ad39b0289aea81db8b59d3427d4924 Mon Sep 17 00:00:00 2001 From: Baptiste Parmantier Date: Wed, 12 Aug 2026 17:31:37 +0200 Subject: [PATCH] fix(validate): stop render blocking on unresolved variables validate allows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The two commands disagreed on the same file: $ rustmotion validate -f var.json Valid scenario: 1 scene(s) in 1 view(s) exit 0 $ rustmotion render -f var.json -o var.mp4 Error: Validation failed: 1 unresolved variable(s). Run `rustmotion validate -f ` to see details. exit 1 The refusal pointed at a command that reports the scenario as valid, so there was no way to see the problem. validate was the one following the documented decision. variables.rs (constat #7) settled that a leftover $word cannot be told apart from legitimate literal $ content — a price tag, a terminal $PATH — and is therefore a loud warning, not a rejection; a hard failure would break any scenario with a $ in its text. is_blocking never got the memo. Drop the unresolved_vars term from is_blocking. It stays in is_clean, so the warning is still printed by both commands — the change is what happens next, not whether it is reported. Nothing that reaches this point is a diagnosable typo: every declared variable is present in defaults ∪ overrides, so substitute cannot leave one behind, and a for-each/use mistake is already named and located by expand.rs. --- .../rustmotion-cli/src/commands/validation.rs | 46 +++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/crates/rustmotion-cli/src/commands/validation.rs b/crates/rustmotion-cli/src/commands/validation.rs index 52d8765e..9a7dafb5 100644 --- a/crates/rustmotion-cli/src/commands/validation.rs +++ b/crates/rustmotion-cli/src/commands/validation.rs @@ -82,13 +82,21 @@ impl ValidationReport { /// every caller blocks on them without extra wiring); the direct /// `attr_warnings` check below is defence in depth for a /// `ValidationReport` assembled some other way. + /// + /// `unresolved_vars` is deliberately **not** blocking. `variables.rs` + /// (constat #7) settled that a leftover `$word` cannot be told apart from + /// legitimate literal `$` content — a price tag, a terminal `$PATH` — so it + /// is reported as a loud warning and the document renders. Blocking here + /// contradicted that: `validate` printed "Valid scenario" and exited 0 on a + /// file `render` then refused, while the refusal told the user to run + /// `validate` for details it would never print. A declared variable can + /// never be left unresolved (every name in `defs` is present in + /// defaults ∪ overrides), and a `for-each`/`use` mistake is caught by name + /// in `expand.rs` — so nothing that reaches here is a diagnosable typo. pub fn is_blocking(&self, lenient: bool) -> bool { if !self.schema_errors.is_empty() { return true; } - if !self.unresolved_vars.is_empty() { - return true; - } if !self.attr_warnings.is_empty() { return true; } @@ -396,6 +404,38 @@ mod html_css_error_tests { assert!(report.is_blocking(false), "must block rendering"); } + /// `validate` and `render` must agree. A leftover `$word` is a warning by + /// design (variables.rs, constat #7: a price tag or a `$PATH` is + /// indistinguishable from a typo), and `validate` treated it that way — + /// but `is_blocking` did not, so the same file passed validation and was + /// refused at render, with the refusal pointing back at validate. + #[test] + fn an_unresolved_variable_warns_without_blocking() { + let json = r##"{ + "version": "1.0", + "video": {"width": 320, "height": 180, "fps": 30, "background": "#000"}, + "scenes": [{"duration": 1.0, "children": [ + {"type": "text", "content": "$9.99 a month", + "style": {"font-size": 20, "color": "#FFF"}}]}] + }"##; + let loaded = load(ValidationSource::Inline(json)).expect("loads"); + let report = run_checks(&loaded, false); + + assert!( + !report.unresolved_vars.is_empty(), + "a literal `$` must still be reported: {:?}", + report.unresolved_vars + ); + assert!( + !report.is_blocking(false), + "a literal `$` in content must not stop the render" + ); + assert!( + !report.is_clean(), + "it is still an advisory — is_clean must stay false so it gets printed" + ); + } + #[test] fn unknown_animation_preset_from_html_is_a_blocking_error() { // Unknown preset names are not validated in the transpiler (no schema