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
51 changes: 48 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 2 additions & 2 deletions src/adapters/claudecode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
24 changes: 10 additions & 14 deletions src/adapters/codex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -799,21 +799,20 @@ 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]
async fn test_messages_corrupt_lines_are_skipped() {
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();
Expand Down Expand Up @@ -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());
Expand Down
2 changes: 1 addition & 1 deletion src/adapters/cursor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
4 changes: 2 additions & 2 deletions src/config/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
4 changes: 2 additions & 2 deletions src/config/saver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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]
Expand Down
10 changes: 5 additions & 5 deletions src/core/logic/guards.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 ---

Expand Down
19 changes: 7 additions & 12 deletions src/core/models/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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);
Expand Down
6 changes: 3 additions & 3 deletions src/core/models/state_machine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
6 changes: 3 additions & 3 deletions src/event/dispatcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
20 changes: 11 additions & 9 deletions src/state/persistence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ mod tests {
version: 999,
..State::default()
};
assert!(migrate(bad_state).is_err());
assert!(migrate(bad_state).is_err());
}

#[test]
Expand All @@ -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();
Expand Down Expand Up @@ -283,10 +286,9 @@ mod tests {
let path = temp_dir.path().join("state.json");
fs::write(&path, "not json {{{").unwrap();

let result: Result<State> = serde_json::from_str::<State>(
&fs::read_to_string(&path).unwrap(),
)
.map_err(|e| anyhow::anyhow!("{}", e));
let result: Result<State> =
serde_json::from_str::<State>(&fs::read_to_string(&path).unwrap())
.map_err(|e| anyhow::anyhow!("{}", e));
assert!(result.is_err());
}
}
Loading