Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion crates/tauri-pilot-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down
76 changes: 76 additions & 0 deletions crates/tauri-pilot-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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(());
Comment thread
mpiton marked this conversation as resolved.
}

Comment thread
mpiton marked this conversation as resolved.
format_result(output_kind, &result, args.json)?;
Comment thread
mpiton marked this conversation as resolved.
Ok(())
}
Comment thread
mpiton marked this conversation as resolved.
Expand Down Expand Up @@ -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<String>) {
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)."
)),
),
}
}
Comment thread
mpiton marked this conversation as resolved.

async fn follow_logs(
client: &mut Client,
emit_json: bool,
Expand Down Expand Up @@ -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!(
Expand Down
16 changes: 16 additions & 0 deletions crates/tauri-pilot-cli/tests/version.rs
Original file line number Diff line number Diff line change
@@ -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_eq!(
stdout.trim_end(),
format!("tauri-pilot {}", env!("CARGO_PKG_VERSION")),
"expected `--version` to print `tauri-pilot <version>`, got: {stdout}",
);
}
Comment thread
mpiton marked this conversation as resolved.
83 changes: 79 additions & 4 deletions crates/tauri-plugin-pilot/src/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -105,7 +118,11 @@ pub(crate) async fn dispatch(
let win = window.as_deref();

let result = match method {
"ping" => Ok(serde_json::json!({"status": "ok"})),
"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())
Expand Down Expand Up @@ -135,10 +152,19 @@ 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?;
inject_plugin_version(&mut result);
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).
Expand Down Expand Up @@ -584,8 +610,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")]
Expand Down Expand Up @@ -1484,4 +1525,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")));
}
}
9 changes: 7 additions & 2 deletions crates/tauri-plugin-pilot/src/server/unix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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((
Expand Down Expand Up @@ -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);
Expand Down
9 changes: 7 additions & 2 deletions crates/tauri-plugin-pilot/src/server/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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;
Expand Down
27 changes: 25 additions & 2 deletions docs/src/content/docs/guides/plugin-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Loading