From 42033c66d66100f8bcbbf10697a53753d73b5acb Mon Sep 17 00:00:00 2001 From: Guy MANDINA Date: Wed, 22 Jul 2026 11:12:28 +0200 Subject: [PATCH 1/2] fix: format tests and resolve clippy lints Restore a green CI pipeline after the test-coverage expansion commit. - Apply `cargo fmt` to test modules across adapters, config, core, event, and state (no behavior change). - Fix `clippy::field_reassign_with_default` in state::persistence tests by using a struct initializer instead of reassigning fields after Default. - Fix `clippy::useless_format` in adapters::codex test by replacing `format!(r#"..."#)` with a `.to_string()` raw string literal. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/adapters/claudecode.rs | 4 ++-- src/adapters/codex.rs | 24 ++++++++++-------------- src/adapters/cursor.rs | 2 +- src/config/loader.rs | 4 ++-- src/config/saver.rs | 4 ++-- src/core/logic/guards.rs | 10 +++++----- src/core/models/git.rs | 19 +++++++------------ src/core/models/state_machine.rs | 6 +++--- src/event/dispatcher.rs | 6 +++--- src/state/persistence.rs | 20 +++++++++++--------- 10 files changed, 46 insertions(+), 53 deletions(-) diff --git a/src/adapters/claudecode.rs b/src/adapters/claudecode.rs index 98f4d02..1085b30 100644 --- a/src/adapters/claudecode.rs +++ b/src/adapters/claudecode.rs @@ -763,8 +763,8 @@ mod tests { let messages = adapter.messages("legacy-session").await.unwrap(); assert_eq!(messages.len(), 2); - assert_eq!(messages[0].role, Role::User); - assert_eq!(messages[1].role, Role::Assistant); + assert_eq!(messages[0].role, Role::User); + assert_eq!(messages[1].role, Role::Assistant); } #[tokio::test] diff --git a/src/adapters/codex.rs b/src/adapters/codex.rs index 2c3dd03..e0c840d 100644 --- a/src/adapters/codex.rs +++ b/src/adapters/codex.rs @@ -799,8 +799,8 @@ mod tests { let messages = adapter.messages("legacy-session").await.unwrap(); assert_eq!(messages.len(), 2); - assert_eq!(messages[0].role, Role::User); - assert_eq!(messages[1].role, Role::Assistant); + assert_eq!(messages[0].role, Role::User); + assert_eq!(messages[1].role, Role::Assistant); } #[tokio::test] @@ -808,12 +808,11 @@ mod tests { let (adapter, _temp) = create_test_adapter(); // Mix valid and corrupt lines — corrupt ones must be skipped - let rollout = format!( - r#"this is not json -{{"timestamp":"2026-02-03T09:57:43.000Z","type":"response_item","payload":{{"type":"message","role":"user","content":[{{"type":"input_text","text":"valid"}}]}}}} -{{"broken": }} -{{"timestamp":"2026-02-03T09:57:44.000Z","type":"response_item","payload":{{"type":"message","role":"assistant","content":[{{"type":"output_text","text":"ok"}}]}}}}"#, - ); + let rollout = r#"this is not json +{"timestamp":"2026-02-03T09:57:43.000Z","type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"valid"}]}} +{"broken": } +{"timestamp":"2026-02-03T09:57:44.000Z","type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"ok"}]}}"# + .to_string(); write_rollout(&adapter, &rollout).await; let messages = adapter.messages(SESSION_ID).await.unwrap(); @@ -878,12 +877,9 @@ mod tests { "updated_at": 1700000100i64, "total_usage": { "prompt_tokens": 500, "completion_tokens": 200 } }); - tokio::fs::write( - adapter.metadata_file("usage-session"), - metadata.to_string(), - ) - .await - .unwrap(); + tokio::fs::write(adapter.metadata_file("usage-session"), metadata.to_string()) + .await + .unwrap(); let usage = adapter.usage("usage-session").await.unwrap(); assert!(usage.is_some()); diff --git a/src/adapters/cursor.rs b/src/adapters/cursor.rs index 9f2ead5..d511355 100644 --- a/src/adapters/cursor.rs +++ b/src/adapters/cursor.rs @@ -527,7 +527,7 @@ mod tests { assert!(tokens.is_none()); assert_eq!(blocks.len(), 2); assert!(matches!(blocks[0], ContentBlock::Text { .. })); - assert!(matches!(blocks[1], ContentBlock::Code { .. })); + assert!(matches!(blocks[1], ContentBlock::Code { .. })); } #[test] diff --git a/src/config/loader.rs b/src/config/loader.rs index 818d34c..8ddd771 100644 --- a/src/config/loader.rs +++ b/src/config/loader.rs @@ -182,9 +182,9 @@ mod tests { let temp_dir = TempDir::new().unwrap(); let path = temp_dir.path().join("config.json"); - fs::write(&path, "invalid json {{{").unwrap(); + fs::write(&path, "invalid json {{{").unwrap(); - assert!(load_from(&path).is_err()); + assert!(load_from(&path).is_err()); } #[test] diff --git a/src/config/saver.rs b/src/config/saver.rs index 11f10ad..b0b8b3b 100644 --- a/src/config/saver.rs +++ b/src/config/saver.rs @@ -112,9 +112,9 @@ pub fn save_config(config: &Config) -> Result<()> { #[cfg(test)] mod tests { use super::*; + use crate::config::loader::load_from; use std::io::Read; use tempfile::TempDir; - use crate::config::loader::load_from; #[test] fn save_creates_file() { @@ -179,7 +179,7 @@ mod tests { save_to(&config, &path).unwrap(); let loaded: Config = serde_json::from_str(&fs::read_to_string(&path).unwrap()).unwrap(); - assert_eq!(config, loaded); + assert_eq!(config, loaded); } #[test] diff --git a/src/core/logic/guards.rs b/src/core/logic/guards.rs index 5972aa4..fd6b854 100644 --- a/src/core/logic/guards.rs +++ b/src/core/logic/guards.rs @@ -382,11 +382,11 @@ mod tests { 0, ); let result = check_guard(&ctx); - assert!(matches!( - result, - GuardResult::Denied(GuardError::InvalidSelection { .. }) - )); - } + assert!(matches!( + result, + GuardResult::Denied(GuardError::InvalidSelection { .. }) + )); + } // --- Diff guard --- diff --git a/src/core/models/git.rs b/src/core/models/git.rs index f2cd2e8..6ea9123 100644 --- a/src/core/models/git.rs +++ b/src/core/models/git.rs @@ -493,12 +493,12 @@ mod tests { } #[test] - fn test_repo_status_default() { - let status = RepoStatus::default(); - assert_eq!(status.branch, "main"); - assert!(!status.is_dirty); - assert_eq!(status.ahead, 0); - assert_eq!(status.behind, 0); + fn test_repo_status_default() { + let status = RepoStatus::default(); + assert_eq!(status.branch, "main"); + assert!(!status.is_dirty); + assert_eq!(status.ahead, 0); + assert_eq!(status.behind, 0); } #[test] @@ -521,12 +521,7 @@ mod tests { #[test] fn test_commit_serde_roundtrip() { - let commit = Commit::new( - "abcdef1234567890", - "feat: add tests", - "Alice", - Utc::now(), - ); + let commit = Commit::new("abcdef1234567890", "feat: add tests", "Alice", Utc::now()); let json = serde_json::to_string(&commit).unwrap(); let back: Commit = serde_json::from_str(&json).unwrap(); assert_eq!(commit, back); diff --git a/src/core/models/state_machine.rs b/src/core/models/state_machine.rs index 8de54c0..0aa823b 100644 --- a/src/core/models/state_machine.rs +++ b/src/core/models/state_machine.rs @@ -452,9 +452,9 @@ mod tests { #[test] fn test_state_machine_can_execute() { - let sm = StateMachine::new(); - assert!(sm.can_execute(ActionId::Refresh)); - assert!(!sm.can_execute(ActionId::Stage)); + let sm = StateMachine::new(); + assert!(sm.can_execute(ActionId::Refresh)); + assert!(!sm.can_execute(ActionId::Stage)); } #[test] diff --git a/src/event/dispatcher.rs b/src/event/dispatcher.rs index bd4b547..ff753ca 100644 --- a/src/event/dispatcher.rs +++ b/src/event/dispatcher.rs @@ -127,9 +127,9 @@ mod tests { dispatcher.publish(Topic::All, Event::RefreshNeeded); // Event should be received - let event = sub.receiver.recv().await; - assert!(matches!(event, Some(Event::RefreshNeeded))); - } + let event = sub.receiver.recv().await; + assert!(matches!(event, Some(Event::RefreshNeeded))); + } #[tokio::test] async fn test_multiple_subscribers_all_receive() { diff --git a/src/state/persistence.rs b/src/state/persistence.rs index 301c07c..626b0db 100644 --- a/src/state/persistence.rs +++ b/src/state/persistence.rs @@ -225,7 +225,7 @@ mod tests { version: 999, ..State::default() }; - assert!(migrate(bad_state).is_err()); + assert!(migrate(bad_state).is_err()); } #[test] @@ -249,10 +249,13 @@ mod tests { let temp_dir = TempDir::new().unwrap(); let path = temp_dir.path().join("state.json"); - let mut state = State::default(); - state.git_graph_enabled = false; - state.line_wrap_enabled = true; - state.active_plugins + let mut state = State { + git_graph_enabled: false, + line_wrap_enabled: true, + ..Default::default() + }; + state + .active_plugins .insert("/proj".to_string(), "gitstatus".to_string()); let json = serde_json::to_string_pretty(&state).unwrap(); @@ -283,10 +286,9 @@ mod tests { let path = temp_dir.path().join("state.json"); fs::write(&path, "not json {{{").unwrap(); - let result: Result = serde_json::from_str::( - &fs::read_to_string(&path).unwrap(), - ) - .map_err(|e| anyhow::anyhow!("{}", e)); + let result: Result = + serde_json::from_str::(&fs::read_to_string(&path).unwrap()) + .map_err(|e| anyhow::anyhow!("{}", e)); assert!(result.is_err()); } } From 2c45a2ba62ac2619abcee9ae7b14e0c63d73b2f4 Mon Sep 17 00:00:00 2001 From: Guy MANDINA Date: Wed, 22 Jul 2026 11:12:38 +0200 Subject: [PATCH 2/2] ci: harden workflow and add macOS build gate - Add concurrency control to cancel stale runs on PRs (saves CI minutes). - Scope token permissions to contents:read (least privilege). - Add workflow_dispatch for manual runs. - Add per-job timeouts, colored cargo output, and RUST_BACKTRACE=1. - Gate on `cargo fetch --locked` to catch Cargo.lock drift early. - Add a macOS build job (`cargo build --locked --all-targets`) to validate cross-platform compilation: lint/test run on Linux, but the project targets macOS (notify macos_fsevent, arboard clipboard). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/ci.yml | 51 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6a47585..73db55f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,22 +4,67 @@ on: push: branches: [main] pull_request: + workflow_dispatch: + +permissions: + contents: read + +# Cancel stale runs on the same PR/branch to free up CI capacity. +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +env: + CARGO_TERM_COLOR: always + CARGO_INCREMENTAL: 0 + RUST_BACKTRACE: 1 jobs: - rust: + # Primary gate: formatting, clippy (-D warnings), and the full test suite. + # Mirrors `bash scripts/dev.sh ci` so local and CI run identical checks. + check: + name: fmt + clippy + test (ubuntu) runs-on: ubuntu-latest + timeout-minutes: 30 steps: - name: Checkout uses: actions/checkout@v4 - - name: Install Rust + - name: Install Rust (pinned to Cargo.toml rust-version) run: | RUST_VERSION="$(bash scripts/dev.sh rust-version)" rustup toolchain install "$RUST_VERSION" --profile minimal --component rustfmt --component clippy rustup default "$RUST_VERSION" + rustc --version - name: Cache cargo uses: Swatinem/rust-cache@v2 - - name: Run checks + - name: Verify Cargo.lock is in sync + run: cargo fetch --locked + + - name: Run checks (fmt, clippy, tests) run: bash scripts/dev.sh ci + + # Cross-platform compile gate. The project targets macOS (notify macos_fsevent, + # arboard clipboard) but CI lint/test runs on Linux; ensure it still builds on macOS. + build-macos: + name: build (macos) + runs-on: macos-latest + timeout-minutes: 30 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Rust (pinned to Cargo.toml rust-version) + run: | + RUST_VERSION="$(bash scripts/dev.sh rust-version)" + rustup toolchain install "$RUST_VERSION" --profile minimal + rustup default "$RUST_VERSION" + rustc --version + + - name: Cache cargo + uses: Swatinem/rust-cache@v2 + + - name: Build all targets + run: cargo build --locked --all-targets