From a19c24868f5e4b6ca676a7c4fa3eadd3d4205050 Mon Sep 17 00:00:00 2001 From: juan <2930882+juacker@users.noreply.github.com> Date: Sat, 27 Jun 2026 23:28:08 +0200 Subject: [PATCH 1/3] ci: add a windows-latest job to compile-check Windows code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI ran only on ubuntu-latest, so Windows-only Rust paths (`#[cfg(windows)]` and `cfg!(windows)` branches) were never compiled, clippy-checked, or tested on a PR — they were first built only at release time. The recent Windows work (#80 system_apps, #81 bash_exec shell + taskkill tree-kill) shipped with no Windows compile check anywhere. Add a `windows-check` job that builds the frontend (so the `generate_context!` macro in lib.rs can embed `../dist` at compile time, mirroring the Linux job's ordering) and then runs `cargo check`, `cargo clippy -- -D warnings`, and `cargo test` on windows-latest. Scope is deliberately lean: the frontend quality checks (lint/typecheck/vitest/bindings) and `cargo fmt --check` are platform-independent and already covered by the Linux job, so they are not duplicated here. This job exists solely to catch Windows-only Rust regressions before merge. --- .github/workflows/ci.yml | 46 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1a61c410..227d2a23 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -75,3 +75,49 @@ jobs: - name: Build Tauri app (debug, no bundle) run: npm run tauri -- build --debug --no-bundle + + windows-check: + name: Windows check + runs-on: windows-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Rust cache + uses: swatinem/rust-cache@v2 + with: + workspaces: './src-tauri -> target' + + - name: Install npm dependencies + run: npm ci + + # generate_context! (src-tauri/src/lib.rs) embeds frontendDist (../dist) + # into the binary at compile time, so the Rust steps below need dist/ to + # exist. Build the frontend first (mirrors the Linux job's ordering). The + # frontend quality checks (lint/typecheck/tests/bindings) and fmt are + # platform-independent and already covered by the Linux `check` job, so we + # don't repeat them here — this job exists to compile-check the + # Windows-only (`#[cfg(windows)]` / `cfg!(windows)`) Rust paths. + - name: Build frontend + run: npm run build + + - name: Rust check + working-directory: src-tauri + run: cargo check + + - name: Rust clippy + working-directory: src-tauri + run: cargo clippy -- -D warnings + + - name: Rust tests + working-directory: src-tauri + run: cargo test From 262d223eecb559fa8ccffa3e27cf8d1700601617 Mon Sep 17 00:00:00 2001 From: juan <2930882+juacker@users.noreply.github.com> Date: Sat, 27 Jun 2026 23:42:14 +0200 Subject: [PATCH 2/3] fix: silence Windows-only dead-code/size clippy lints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The new windows-latest CI job surfaced 6 clippy errors (`-D warnings`) that never appear on Linux — `cargo check` passes, so the code compiles and the app runs; these are lint-only and pre-existing, exposed for the first time now that Windows is compiled in CI. Five are dead_code on Windows for code that is genuinely used on Unix: - `providers::USER_BIN_PATHS` and `command_exists_at_path` are referenced only from the `#[cfg(not(target_os = "windows"))]` fallback in `command_exists` (Windows uses `where`), so cfg-gate them off Windows — they don't exist there. - `SandboxProfile` fields, `SandboxEnv::{iter,home}`, and `SandboxCommand.profile` are read only by the Linux (bwrap) / macOS (seatbelt) sandbox backends. On platforms without a sandbox the profile is built but never read, so `#[allow(dead_code)]` with a comment (the types are constructed on all platforms, so they can't be cfg-gated out). One is `clippy::large_enum_variant` on `ConnectedMcpServer`, which only trips on Windows' type sizes; allowed (few, cold connections) rather than boxing. Linux: fmt/clippy -D warnings/sandbox tests green; allows are inert where the lints don't fire. --- src-tauri/src/assistant/sandbox/profile.rs | 8 ++++++++ src-tauri/src/assistant/sandbox/runner.rs | 3 +++ src-tauri/src/mcp/client.rs | 4 ++++ src-tauri/src/providers/mod.rs | 8 ++++++-- 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/assistant/sandbox/profile.rs b/src-tauri/src/assistant/sandbox/profile.rs index 0d5a5164..858c1f69 100644 --- a/src-tauri/src/assistant/sandbox/profile.rs +++ b/src-tauri/src/assistant/sandbox/profile.rs @@ -24,6 +24,10 @@ const ENV_DENY_EXACT: &[&str] = &[ "SUDO_ASKPASS", ]; +// Fields are consumed by the Linux (bwrap) and macOS (seatbelt) sandbox +// backends; on platforms without a sandbox (e.g. Windows -> `unsupported.rs`) +// the profile is built but never read, so allow the resulting dead_code there. +#[allow(dead_code)] #[derive(Debug, Clone)] pub struct SandboxProfile { pub workspace_root: PathBuf, @@ -111,12 +115,16 @@ impl SandboxEnv { Self { vars: filtered } } + // Read only by the Unix sandbox backends when applying the env to the + // sandboxed command; unused on platforms without a sandbox. + #[allow(dead_code)] pub fn iter(&self) -> impl Iterator { self.vars .iter() .map(|(key, value)| (key.as_str(), value.as_str())) } + #[allow(dead_code)] pub fn home(&self) -> Option<&str> { self.vars.get("HOME").map(String::as_str) } diff --git a/src-tauri/src/assistant/sandbox/runner.rs b/src-tauri/src/assistant/sandbox/runner.rs index e7896900..a9e760c7 100644 --- a/src-tauri/src/assistant/sandbox/runner.rs +++ b/src-tauri/src/assistant/sandbox/runner.rs @@ -26,6 +26,9 @@ pub struct SandboxCommand { pub cwd: PathBuf, pub timeout_ms: u64, pub max_output_chars: usize, + // Consumed by the Unix sandbox backends; unused on platforms without a + // sandbox (Windows runs the command unsandboxed via `unsupported.rs`). + #[allow(dead_code)] pub profile: SandboxProfile, } diff --git a/src-tauri/src/mcp/client.rs b/src-tauri/src/mcp/client.rs index 960902ad..d69fb01c 100644 --- a/src-tauri/src/mcp/client.rs +++ b/src-tauri/src/mcp/client.rs @@ -102,6 +102,10 @@ struct StdioMcpServerConnection { child: Child, } +// The variants' sizes differ enough to trip clippy::large_enum_variant only on +// Windows (platform-dependent type sizes); there are few of these per process +// and they are not hot, so allow it rather than box the larger variant. +#[allow(clippy::large_enum_variant)] enum ConnectedMcpServer { Http(RunningService), Stdio(StdioMcpServerConnection), diff --git a/src-tauri/src/providers/mod.rs b/src-tauri/src/providers/mod.rs index a5660b08..09edfff9 100644 --- a/src-tauri/src/providers/mod.rs +++ b/src-tauri/src/providers/mod.rs @@ -97,7 +97,9 @@ pub(crate) fn get_host_command(cmd: &str) -> Command { } /// Common user-local binary paths to search when command isn't in PATH. -/// These are relative to the user's home directory. +/// These are relative to the user's home directory. Only used by the +/// non-Windows `command_exists` fallback (Windows relies on `where`). +#[cfg(not(target_os = "windows"))] const USER_BIN_PATHS: &[&str] = &[ ".local/bin", // Standard XDG user binaries ".bun/bin", // Bun global installs @@ -133,7 +135,9 @@ pub fn get_home_dir() -> Option { std::env::var("HOME").ok() } -/// Checks if a command exists at a specific path. +/// Checks if a command exists at a specific path. Only used by the non-Windows +/// `command_exists` fallback that probes user-local bin paths. +#[cfg(not(target_os = "windows"))] fn command_exists_at_path(path: &str) -> bool { if is_flatpak() { // Use flatpak-spawn to check file existence on host From 95dfe34bf06524bbbd1cdc2f7fefdf559687f61b Mon Sep 17 00:00:00 2001 From: juan <2930882+juacker@users.noreply.github.com> Date: Sat, 27 Jun 2026 23:59:05 +0200 Subject: [PATCH 3/3] test: make path assertions cross-platform for the Windows CI job The new windows-latest job surfaced 5 unit tests that hardcoded Unix path assumptions; cargo check + clippy pass, so these are test-only, not product bugs. Fixes: - canonicalize_expands_tilde_with_real_home: compare structurally (is_absolute + starts_with(temp) + component-based ends_with) instead of against std::fs::canonicalize, which adds a Windows `\\?\` verbatim prefix and resolves the macOS /var symlink. - fs_list / fs_glob / resolve_session_image_files: normalize `\` to `/` before asserting (no-op on Unix) so the separator the OS produces doesn't break the comparison. - fs_glob_rejects_absolute_patterns_outside_allowed_grants: use a platform-absolute pattern (C:/... on Windows) since `/etc/...` is not absolute on Windows and skipped the grant check. Product code is unchanged; it continues to emit OS-native paths. --- src-tauri/src/assistant/tools/local.rs | 29 +++++++++++++++++++------- src-tauri/src/commands/assistant.rs | 7 ++++++- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/src-tauri/src/assistant/tools/local.rs b/src-tauri/src/assistant/tools/local.rs index daa1fba2..ea22cf0b 100644 --- a/src-tauri/src/assistant/tools/local.rs +++ b/src-tauri/src/assistant/tools/local.rs @@ -2342,11 +2342,14 @@ mod tests { } let resolved = canonicalize_requested_path("~/some/subpath").unwrap(); - // Use canonicalize on the temp dir for comparison since macOS - // symlinks /var → /private/var which would otherwise mismatch. - let expected_parent = std::fs::canonicalize(temp.path()).unwrap(); - assert!(resolved.starts_with(&expected_parent)); - assert!(resolved.ends_with("some/subpath")); + // The "~/some/subpath" target does not exist, so the product returns + // the normalized (non-canonicalized) form. Compare structurally: + // `canonicalize` would add a Windows `\\?\` verbatim prefix and resolve + // the macOS `/var`->`/private/var` symlink, both of which break a naive + // prefix match. Component-based checks are separator-agnostic. + assert!(resolved.is_absolute()); + assert!(resolved.starts_with(temp.path())); + assert!(resolved.ends_with(std::path::Path::new("some").join("subpath"))); unsafe { match prev { @@ -2390,7 +2393,9 @@ mod tests { .strip_prefix(root) .unwrap() .to_string_lossy() - .into_owned() + // Normalize Windows `\` to `/` so the assertion is + // separator-agnostic (no-op on Unix). + .replace('\\', "/") }) .collect(); @@ -2427,7 +2432,8 @@ mod tests { .strip_prefix(root) .unwrap() .to_string_lossy() - .into_owned() + // Normalize Windows `\` to `/` (no-op on Unix). + .replace('\\', "/") }) .collect(); @@ -2446,7 +2452,14 @@ mod tests { let temp = tempdir().unwrap(); let root = temp.path(); - let error = glob_allowed_paths("/etc/**/*.conf", &[grant_for(root)], 10).unwrap_err(); + // `/etc/...` is not absolute on Windows (no drive), so use a + // platform-absolute pattern that cannot intersect the temp grant. + let outside_pattern = if cfg!(windows) { + "C:/Windows/System32/**/*.conf" + } else { + "/etc/**/*.conf" + }; + let error = glob_allowed_paths(outside_pattern, &[grant_for(root)], 10).unwrap_err(); assert!(error.contains("outside the agent's allowed filesystem grants")); } diff --git a/src-tauri/src/commands/assistant.rs b/src-tauri/src/commands/assistant.rs index 5f03616e..b40e2355 100644 --- a/src-tauri/src/commands/assistant.rs +++ b/src-tauri/src/commands/assistant.rs @@ -1115,7 +1115,12 @@ mod tests { paths.insert(".clai/images/uuid-a.png".to_string()); paths.insert(".clai/images/uuid-b.jpg".to_string()); let files = resolve_session_image_files(root, &paths); - let mut display: Vec = files.iter().map(|p| p.display().to_string()).collect(); + // Normalize Windows `\` to `/` so the assertion is separator-agnostic + // (no-op on Unix). `root.join(p)` yields `\` joins on Windows. + let mut display: Vec = files + .iter() + .map(|p| p.display().to_string().replace('\\', "/")) + .collect(); display.sort(); assert_eq!( display,