From 4a98c82ec7c38d1a9e2a55a5310c37f98da8200e Mon Sep 17 00:00:00 2001 From: Mathieu Piton <27002047+mpiton@users.noreply.github.com> Date: Wed, 10 Jun 2026 10:00:48 +0200 Subject: [PATCH 1/4] feat: expose plugin and CLI versions to catch eval version drift (#135) The macOS "native WebKit eval callback returned an error" came from a plugin <= 0.7.0 compiled into the app while the CLI was already 0.7.1. Nothing reported either version, so the drift was invisible and the same eval problem kept resurfacing per platform. - plugin: `ping` returns `plugin_version`; `state` includes it too - plugin: log the version when the socket/named pipe starts listening - cli: add `--version`; `ping` prints plugin+CLI versions and warns on drift - docs: plugin-setup troubleshooting for the drift plus `cargo update` --- CHANGELOG.md | 13 ++++ crates/tauri-pilot-cli/src/cli.rs | 6 +- crates/tauri-pilot-cli/src/main.rs | 76 +++++++++++++++++++ crates/tauri-pilot-cli/tests/version.rs | 16 ++++ crates/tauri-plugin-pilot/src/handler.rs | 74 +++++++++++++++++- crates/tauri-plugin-pilot/src/server/unix.rs | 9 ++- .../tauri-plugin-pilot/src/server/windows.rs | 2 +- docs/src/content/docs/guides/plugin-setup.md | 27 ++++++- 8 files changed, 213 insertions(+), 10 deletions(-) create mode 100644 crates/tauri-pilot-cli/tests/version.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index cb16270..61955a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- `tauri-pilot --version` prints the CLI version. The flag was missing, so a + caller had no way to confirm which build they were running. [#135] +- The plugin reports its own version in the `ping` response, and + `tauri-pilot ping` now shows the plugin and CLI versions together and warns + when they drift. A response without a `plugin_version` field comes from a + plugin <= 0.7.0, which predates the macOS native-eval fix, so the CLI points + at an upgrade. [#135] +- `state` output includes the plugin's `plugin_version`. [#135] +- The plugin logs its version when the socket or named pipe starts listening, + so the running plugin version shows up in the app's logs. [#135] + ## [0.7.1] - 2026-06-05 ### Fixed diff --git a/crates/tauri-pilot-cli/src/cli.rs b/crates/tauri-pilot-cli/src/cli.rs index e32a035..064f193 100644 --- a/crates/tauri-pilot-cli/src/cli.rs +++ b/crates/tauri-pilot-cli/src/cli.rs @@ -2,7 +2,11 @@ use clap::{Parser, Subcommand}; use std::path::PathBuf; #[derive(Parser)] -#[command(name = "tauri-pilot", about = "Interactive testing CLI for Tauri apps")] +#[command( + name = "tauri-pilot", + version, + about = "Interactive testing CLI for Tauri apps" +)] pub(crate) struct Cli { /// Socket path (auto-detected if omitted). #[arg(long, env = "TAURI_PILOT_SOCKET")] diff --git a/crates/tauri-pilot-cli/src/main.rs b/crates/tauri-pilot-cli/src/main.rs index f3d90cb..5aab43b 100644 --- a/crates/tauri-pilot-cli/src/main.rs +++ b/crates/tauri-pilot-cli/src/main.rs @@ -103,6 +103,7 @@ async fn main() -> Result<()> { None }; let is_screenshot = matches!(args.command, Command::Screenshot { .. }); + let is_ping = matches!(args.command, Command::Ping); // Capture output kind before command is consumed by run_command let output_kind = OutputKind::from(&args.command); let result = if is_screenshot && !args.json && std::io::stdout().is_terminal() { @@ -130,6 +131,15 @@ async fn main() -> Result<()> { return Ok(()); } + // `ping` doubles as a version handshake: surface the plugin version baked + // into the running app alongside the CLI version and warn on drift, the + // signal that was missing when issue #135 was diagnosed. JSON output keeps + // the raw `plugin_version` field instead. + if is_ping && !args.json { + report_ping(&result); + return Ok(()); + } + format_result(output_kind, &result, args.json)?; Ok(()) } @@ -213,6 +223,47 @@ fn format_result(kind: OutputKind, result: &serde_json::Value, emit_json: bool) Ok(()) } +/// Print the `ping` version handshake: the plugin version embedded in the +/// running app, the CLI version, and a drift warning when they disagree. +fn report_ping(result: &Value) { + let (summary, warning) = diagnose_versions( + env!("CARGO_PKG_VERSION"), + result.get("plugin_version").and_then(Value::as_str), + ); + println!("{}", crate::style::success(&summary)); + if let Some(warning) = warning { + eprintln!("{}", crate::style::warn(&warning)); + } +} + +/// Build the human-facing `ping` summary line and an optional version-drift +/// warning. +/// +/// Since 0.7.1 the plugin reports its own compile-time version in the ping +/// response. A response without that field comes from a plugin <= 0.7.0, which +/// predates the macOS native-eval fix (issue #135), so the CLI points the user +/// at an upgrade. +fn diagnose_versions(cli: &str, plugin: Option<&str>) -> (String, Option) { + match plugin { + Some(p) if p == cli => (format!("Connected. Plugin and CLI both {p}."), None), + Some(p) => ( + format!("Connected. Plugin {p}, CLI {cli}."), + Some(format!( + "Plugin {p} and CLI {cli} differ. Rebuild your app against \ + tauri-plugin-pilot {cli} (or match the CLI) so they stay in sync." + )), + ), + None => ( + format!("Connected. Plugin <= 0.7.0, CLI {cli}."), + Some(format!( + "This plugin predates version reporting (<= 0.7.0). If eval fails \ + on macOS, bump tauri-plugin-pilot to >= {cli} and rebuild the app \ + (issue #135)." + )), + ), + } +} + async fn follow_logs( client: &mut Client, emit_json: bool, @@ -1528,6 +1579,31 @@ mod tests { use super::*; use serial_test::serial; + #[test] + fn test_diagnose_versions_match_has_no_warning() { + let (line, warning) = diagnose_versions("0.7.1", Some("0.7.1")); + assert!(line.contains("0.7.1")); + assert!(warning.is_none()); + } + + #[test] + fn test_diagnose_versions_mismatch_warns() { + let (line, warning) = diagnose_versions("0.7.1", Some("0.7.0")); + assert!(line.contains("0.7.0")); + assert!(line.contains("0.7.1")); + let warning = warning.expect("a version mismatch must warn"); + assert!(warning.contains("0.7.0")); + assert!(warning.contains("0.7.1")); + } + + #[test] + fn test_diagnose_versions_absent_plugin_warns_pre_introspection() { + let (line, warning) = diagnose_versions("0.7.1", None); + assert!(line.contains("0.7.1")); + let warning = warning.expect("an absent plugin version must warn"); + assert!(warning.contains("135")); + } + #[test] fn test_mime_from_ext_png() { assert_eq!( diff --git a/crates/tauri-pilot-cli/tests/version.rs b/crates/tauri-pilot-cli/tests/version.rs new file mode 100644 index 0000000..7b868bc --- /dev/null +++ b/crates/tauri-pilot-cli/tests/version.rs @@ -0,0 +1,16 @@ +use assert_cmd::Command; + +/// `tauri-pilot --version` must print the CLI version. The flag was missing +/// entirely until issue #135 surfaced it (a caller couldn't tell which CLI +/// build they were running). +#[test] +fn test_version_flag_prints_cli_version() { + let mut cmd = Command::cargo_bin("tauri-pilot").expect("tauri-pilot binary builds"); + let assert = cmd.arg("--version").assert().success(); + let stdout = String::from_utf8_lossy(&assert.get_output().stdout).into_owned(); + assert!( + stdout.contains(env!("CARGO_PKG_VERSION")), + "expected --version output to contain {}, got: {stdout}", + env!("CARGO_PKG_VERSION"), + ); +} diff --git a/crates/tauri-plugin-pilot/src/handler.rs b/crates/tauri-plugin-pilot/src/handler.rs index d82517c..1fbfa01 100644 --- a/crates/tauri-plugin-pilot/src/handler.rs +++ b/crates/tauri-plugin-pilot/src/handler.rs @@ -105,7 +105,10 @@ pub(crate) async fn dispatch( let win = window.as_deref(); let result = match method { - "ping" => Ok(serde_json::json!({"status": "ok"})), + "ping" => Ok(serde_json::json!({ + "status": "ok", + "plugin_version": env!("CARGO_PKG_VERSION"), + })), "windows.list" => { if let Some(f) = list_fn { Ok(f()) @@ -135,10 +138,24 @@ pub(crate) async fn dispatch( data: None, }), "click" | "fill" | "type" | "select" | "check" | "scroll" | "drag" | "drop" | "text" - | "html" | "value" | "attrs" | "eval" | "ipc" | "navigate" | "url" | "title" | "state" + | "html" | "value" | "attrs" | "eval" | "ipc" | "navigate" | "url" | "title" | "visible" | "count" | "checked" => { handle_eval_method(method, params, engine, eval_fn, win, DEFAULT_TIMEOUT).await } + // `state` is a bridge-derived method (url/title/ready), but the dispatch + // merges the plugin's compile-time version into the result so a single + // `state` call also surfaces plugin/CLI version drift (issue #135). + "state" => { + let mut result = + handle_eval_method("state", params, engine, eval_fn, win, DEFAULT_TIMEOUT).await?; + if let Some(obj) = result.as_object_mut() { + obj.insert( + "plugin_version".to_owned(), + serde_json::json!(env!("CARGO_PKG_VERSION")), + ); + } + Ok(result) + } // `wait` and `watch` both honor a JS-side `options.timeout`; the Rust // channel timeout must outlive that so the bridge can surface its own // rejection (issue #91). @@ -584,8 +601,23 @@ mod tests { #[tokio::test] async fn test_dispatch_ping_returns_ok() { let engine = EvalEngine::new(); - let result = dispatch("ping", None, &engine, None, None, None, &Recorder::new()).await; - assert_eq!(result.expect("dispatch succeeds"), json!({"status": "ok"})); + let result = dispatch("ping", None, &engine, None, None, None, &Recorder::new()) + .await + .expect("dispatch succeeds"); + assert_eq!(result["status"], json!("ok")); + } + + #[tokio::test] + async fn test_dispatch_ping_reports_plugin_version() { + // The ping response carries the plugin's own compile-time version so a + // caller can detect when the plugin baked into the app has drifted from + // the CLI (issue #135). Older plugins (<= 0.7.0) omit the field, which + // the CLI reads as "pre-introspection, upgrade recommended". + let engine = EvalEngine::new(); + let result = dispatch("ping", None, &engine, None, None, None, &Recorder::new()) + .await + .expect("dispatch succeeds"); + assert_eq!(result["plugin_version"], json!(env!("CARGO_PKG_VERSION"))); } #[cfg(feature = "press")] @@ -1484,4 +1516,38 @@ mod tests { .expect("dispatch should resolve via callback, not time out"); assert_eq!(result, json!({"found": true})); } + + #[tokio::test] + async fn test_dispatch_state_injects_plugin_version() { + // `state` returns url/title/ready from the bridge, then the dispatch + // merges in the plugin's own version so callers can spot version drift + // from a single `state` call (issue #135). + let engine = EvalEngine::new(); + let engine_clone = engine.clone(); + let eval_fn: crate::server::EvalFn = + std::sync::Arc::new(move |_w: Option<&str>, _script: String| { + // First register on a fresh engine has id == 1 (see eval.rs). + engine_clone.resolve( + 1, + Ok(json!({"url": "http://localhost/", "title": "App", "ready": true})), + ); + Ok(()) + }); + + let result = dispatch( + "state", + None, + &engine, + Some(&eval_fn), + None, + None, + &Recorder::new(), + ) + .await + .expect("dispatch succeeds"); + + assert_eq!(result["url"], json!("http://localhost/")); + assert_eq!(result["ready"], json!(true)); + assert_eq!(result["plugin_version"], json!(env!("CARGO_PKG_VERSION"))); + } } diff --git a/crates/tauri-plugin-pilot/src/server/unix.rs b/crates/tauri-plugin-pilot/src/server/unix.rs index a992876..738970b 100644 --- a/crates/tauri-plugin-pilot/src/server/unix.rs +++ b/crates/tauri-plugin-pilot/src/server/unix.rs @@ -137,7 +137,7 @@ pub fn bind( // Must be non-blocking for tokio conversion listener.set_nonblocking(true)?; - tracing::info!(path = %socket_path.display(), "tauri-pilot socket listening"); + tracing::info!(version = env!("CARGO_PKG_VERSION"), path = %socket_path.display(), "tauri-pilot socket listening"); let inode = inode_from_raw_fd(listener.as_raw_fd()); Ok(( @@ -281,7 +281,12 @@ mod tests { assert_eq!(resp.id, serde_json::json!(1)); assert!(resp.error.is_none()); - assert_eq!(resp.result, Some(serde_json::json!({"status": "ok"}))); + let result = resp.result.expect("ping returns a result"); + assert_eq!(result["status"], serde_json::json!("ok")); + assert_eq!( + result["plugin_version"], + serde_json::json!(env!("CARGO_PKG_VERSION")) + ); handle.abort(); let _ = std::fs::remove_file(&socket); diff --git a/crates/tauri-plugin-pilot/src/server/windows.rs b/crates/tauri-plugin-pilot/src/server/windows.rs index bc145a7..5ab5b88 100644 --- a/crates/tauri-plugin-pilot/src/server/windows.rs +++ b/crates/tauri-plugin-pilot/src/server/windows.rs @@ -406,7 +406,7 @@ pub fn bind(pipe_path: &Path) -> Result<(NamedPipeServer, RegistryGuard), Error> } .map_err(Error::from)?; - tracing::info!(path = %pipe_path.display(), "tauri-pilot named pipe listening"); + tracing::info!(version = env!("CARGO_PKG_VERSION"), path = %pipe_path.display(), "tauri-pilot named pipe listening"); let identifier = pipe_path .file_name() diff --git a/docs/src/content/docs/guides/plugin-setup.md b/docs/src/content/docs/guides/plugin-setup.md index 2b666cd..62ed6a6 100644 --- a/docs/src/content/docs/guides/plugin-setup.md +++ b/docs/src/content/docs/guides/plugin-setup.md @@ -82,7 +82,30 @@ cargo tauri dev # Terminal 2 — verify the plugin is reachable tauri-pilot ping -# Expected output: pong +# Connected. Plugin and CLI both 0.7.1. ``` -If you see `pong`, the plugin is running and the CLI can reach it. You are ready to start using the snapshot/action workflow. +`ping` reports the plugin version compiled into your app alongside the CLI version. When they match, you're ready to start using the snapshot/action workflow. + +## 7. Keep the plugin and CLI in sync + +The plugin is a Rust dependency compiled into your app. The CLI is a separate binary. They're versioned independently, so they drift apart if you update one and not the other. `ping` surfaces a drift: + +```bash +tauri-pilot ping +# Connected. Plugin 0.7.0, CLI 0.7.1. +# Plugin 0.7.0 and CLI 0.7.1 differ. Rebuild your app against tauri-plugin-pilot 0.7.1 ... +``` + +If `ping` reports `Plugin <= 0.7.0`, or eval commands fail on macOS with `native WebKit eval callback returned an error`, the plugin baked into your app predates the unified eval path (removed in 0.7.1). Update it and rebuild: + +```bash +# git dependency (the setup above): pull the latest commit +cargo update -p tauri-plugin-pilot + +# or pin a released version in src-tauri/Cargo.toml: +# tauri-plugin-pilot = "0.7.1" + +# then rebuild +cargo tauri dev +``` From db8f11f7c711f876dbccf3c356a3dc74100c1128 Mon Sep 17 00:00:00 2001 From: Mathieu Piton <27002047+mpiton@users.noreply.github.com> Date: Wed, 10 Jun 2026 12:17:44 +0200 Subject: [PATCH 2/4] test(plugin): assert plugin_version in the Windows ping round-trip (#135) The unix server test was updated for the new ping field, but its windows.rs twin still asserted the old {"status":"ok"} shape, so cargo test failed on windows-latest only. --- crates/tauri-plugin-pilot/src/server/windows.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/tauri-plugin-pilot/src/server/windows.rs b/crates/tauri-plugin-pilot/src/server/windows.rs index 5ab5b88..f94fa5d 100644 --- a/crates/tauri-plugin-pilot/src/server/windows.rs +++ b/crates/tauri-plugin-pilot/src/server/windows.rs @@ -589,7 +589,12 @@ mod tests { assert_eq!(resp.id, serde_json::json!(1)); assert!(resp.error.is_none()); - assert_eq!(resp.result, Some(serde_json::json!({"status": "ok"}))); + let result = resp.result.expect("ping returns a result"); + assert_eq!(result["status"], serde_json::json!("ok")); + assert_eq!( + result["plugin_version"], + serde_json::json!(env!("CARGO_PKG_VERSION")) + ); handle.abort(); let _ = handle.await; From c2504122a94820c80d5cf64f05051779e2e3de4b Mon Sep 17 00:00:00 2001 From: Mathieu Piton <27002047+mpiton@users.noreply.github.com> Date: Wed, 10 Jun 2026 12:27:27 +0200 Subject: [PATCH 3/4] refactor(plugin): share plugin_version injection between ping and state Addresses a PR review nit: extract inject_plugin_version so the `plugin_version` field and its env! source live in one place instead of being repeated in the ping and state arms. --- crates/tauri-plugin-pilot/src/handler.rs | 29 ++++++++++++++++-------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/crates/tauri-plugin-pilot/src/handler.rs b/crates/tauri-plugin-pilot/src/handler.rs index 1fbfa01..be5e246 100644 --- a/crates/tauri-plugin-pilot/src/handler.rs +++ b/crates/tauri-plugin-pilot/src/handler.rs @@ -82,6 +82,19 @@ fn extract_window( } } +/// Merge the plugin's compile-time version into a JSON object response. +/// +/// No-op when the value is not an object. Single source for the +/// `plugin_version` field so `ping` and `state` report the same value (#135). +fn inject_plugin_version(result: &mut serde_json::Value) { + if let Some(obj) = result.as_object_mut() { + obj.insert( + "plugin_version".to_owned(), + serde_json::json!(env!("CARGO_PKG_VERSION")), + ); + } +} + /// Dispatch a JSON-RPC method call to the appropriate handler. #[allow(clippy::too_many_lines, clippy::too_many_arguments)] pub(crate) async fn dispatch( @@ -105,10 +118,11 @@ pub(crate) async fn dispatch( let win = window.as_deref(); let result = match method { - "ping" => Ok(serde_json::json!({ - "status": "ok", - "plugin_version": env!("CARGO_PKG_VERSION"), - })), + "ping" => { + let mut result = serde_json::json!({"status": "ok"}); + inject_plugin_version(&mut result); + Ok(result) + } "windows.list" => { if let Some(f) = list_fn { Ok(f()) @@ -148,12 +162,7 @@ pub(crate) async fn dispatch( "state" => { let mut result = handle_eval_method("state", params, engine, eval_fn, win, DEFAULT_TIMEOUT).await?; - if let Some(obj) = result.as_object_mut() { - obj.insert( - "plugin_version".to_owned(), - serde_json::json!(env!("CARGO_PKG_VERSION")), - ); - } + inject_plugin_version(&mut result); Ok(result) } // `wait` and `watch` both honor a JS-side `options.timeout`; the Rust From 49d779e32c07b1e984f9a5b61278da465aab13ed Mon Sep 17 00:00:00 2001 From: Mathieu Piton <27002047+mpiton@users.noreply.github.com> Date: Wed, 10 Jun 2026 12:30:29 +0200 Subject: [PATCH 4/4] test(cli): assert exact --version output format Per review: pin the assertion to `tauri-pilot ` so a change to the binary name or clap's version layout is caught, not just the presence of the version string. --- crates/tauri-pilot-cli/tests/version.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/tauri-pilot-cli/tests/version.rs b/crates/tauri-pilot-cli/tests/version.rs index 7b868bc..d7c5510 100644 --- a/crates/tauri-pilot-cli/tests/version.rs +++ b/crates/tauri-pilot-cli/tests/version.rs @@ -8,9 +8,9 @@ fn test_version_flag_prints_cli_version() { let mut cmd = Command::cargo_bin("tauri-pilot").expect("tauri-pilot binary builds"); let assert = cmd.arg("--version").assert().success(); let stdout = String::from_utf8_lossy(&assert.get_output().stdout).into_owned(); - assert!( - stdout.contains(env!("CARGO_PKG_VERSION")), - "expected --version output to contain {}, got: {stdout}", - env!("CARGO_PKG_VERSION"), + assert_eq!( + stdout.trim_end(), + format!("tauri-pilot {}", env!("CARGO_PKG_VERSION")), + "expected `--version` to print `tauri-pilot `, got: {stdout}", ); }