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
79 changes: 79 additions & 0 deletions .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Code Coverage Workflow
#
# Generates and tracks code coverage using cargo-llvm-cov.
# Coverage reports are uploaded as artifacts and can be viewed in PRs.

name: Coverage

on:
push:
branches: [main]
pull_request:
branches: [main]

env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1

jobs:
coverage:
name: Code Coverage
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- uses: dtolnay/rust-toolchain@1.88
with:
components: llvm-tools-preview

- name: Install cargo-llvm-cov
uses: taiki-e/install-action@cargo-llvm-cov

- name: Install Linux dependencies
run: sudo apt-get update && sudo apt-get install -y libavahi-client-dev

- uses: Swatinem/rust-cache@v2

- name: Generate coverage report
run: |
cargo llvm-cov --all-features --workspace \
--ignore-filename-regex '(tests/|test\.rs|mock\.rs)' \
--lcov --output-path lcov.info

- name: Generate coverage summary
run: |
cargo llvm-cov report --all-features --workspace \
--ignore-filename-regex '(tests/|test\.rs|mock\.rs)' \
> coverage-summary.txt
echo "## Coverage Summary" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
cat coverage-summary.txt >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY

- name: Upload coverage report
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: |
lcov.info
coverage-summary.txt
retention-days: 30

- name: Check coverage threshold
run: |
# Extract total line coverage percentage
COVERAGE=$(cargo llvm-cov report --all-features --workspace \
--ignore-filename-regex '(tests/|test\.rs|mock\.rs)' 2>/dev/null \
| grep -E '^TOTAL' | awk '{print $NF}' | tr -d '%')

echo "Total coverage: ${COVERAGE}%"

# Warn if coverage is below 20% (initial threshold)
if [ -n "$COVERAGE" ]; then
THRESHOLD=20
if (( $(echo "$COVERAGE < $THRESHOLD" | bc -l) )); then
echo "::warning::Coverage ${COVERAGE}% is below threshold ${THRESHOLD}%"
else
echo "Coverage ${COVERAGE}% meets threshold ${THRESHOLD}%"
fi
fi
19 changes: 19 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ members = [
"crates/wonopcode-sandbox",
"crates/wonopcode-protocol",
"crates/wonopcode-discover",
"crates/wonopcode-test-utils",
]

[workspace.package]
Expand Down Expand Up @@ -47,6 +48,7 @@ wonopcode-auth = { path = "crates/wonopcode-auth" }
wonopcode-sandbox = { path = "crates/wonopcode-sandbox" }
wonopcode-protocol = { path = "crates/wonopcode-protocol" }
wonopcode-discover = { path = "crates/wonopcode-discover" }
wonopcode-test-utils = { path = "crates/wonopcode-test-utils" }

# Async runtime
tokio = { version = "1", features = ["full"] }
Expand Down Expand Up @@ -126,6 +128,9 @@ redundant_clone = "warn"
large_enum_variant = "warn"
too_many_arguments = "warn"

# Complexity metrics - warn on overly complex functions
cognitive_complexity = "allow"

# Explicitly allow noisy style lints
needless_borrow = "allow"
uninlined_format_args = "allow"
Expand Down
3 changes: 3 additions & 0 deletions crates/wonopcode-acp/src/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ impl Agent {
}

/// Handle a JSON-RPC request.
#[allow(clippy::cognitive_complexity)]
async fn handle_request(&self, request: JsonRpcRequest) {
let id = match request.id {
Some(id) => id,
Expand Down Expand Up @@ -263,6 +264,7 @@ impl Agent {
}

/// Replay session history to the client.
#[allow(clippy::cognitive_complexity)]
async fn replay_session_history(&self, session_id: &str) {
let processors = self.processors.read().await;
if let Some(processor) = processors.get(session_id) {
Expand Down Expand Up @@ -307,6 +309,7 @@ impl Agent {
}

/// Handle prompt request.
#[allow(clippy::cognitive_complexity)]
async fn handle_prompt(
&self,
params: Option<serde_json::Value>,
Expand Down
1 change: 1 addition & 0 deletions crates/wonopcode-acp/src/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ impl Processor {
}

/// Process a prompt and stream responses via the connection.
#[allow(clippy::cognitive_complexity)]
pub async fn process_prompt(
&self,
session_id: &str,
Expand Down
2 changes: 2 additions & 0 deletions crates/wonopcode-acp/src/transport.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ impl StdioTransport {
}

/// Read from stdin and dispatch messages.
#[allow(clippy::cognitive_complexity)]
async fn stdin_loop(
incoming_tx: mpsc::Sender<IncomingMessage>,
pending: Arc<Mutex<HashMap<JsonRpcId, PendingRequest>>>,
Expand Down Expand Up @@ -147,6 +148,7 @@ impl StdioTransport {
}

/// Write messages to stdout.
#[allow(clippy::cognitive_complexity)]
async fn stdout_loop(mut rx: mpsc::Receiver<String>) {
let mut stdout = tokio::io::stdout();

Expand Down
1 change: 1 addition & 0 deletions crates/wonopcode-core/src/permission.rs
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,7 @@ impl PermissionManager {
}

/// Ask the user for permission.
#[allow(clippy::cognitive_complexity)]
async fn ask_user(&self, session_id: &str, check: PermissionCheck) -> bool {
let (tx, rx) = oneshot::channel();

Expand Down
1 change: 1 addition & 0 deletions crates/wonopcode-core/src/prompt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ impl PromptLoop {
}

/// Execute the prompt loop for a user message.
#[allow(clippy::cognitive_complexity)]
pub async fn run(
&self,
session: &Session,
Expand Down
2 changes: 2 additions & 0 deletions crates/wonopcode-core/src/revert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ impl SessionRevert {
///
/// This marks the revert point in the session. The actual message cleanup
/// happens when the user continues (via `cleanup`).
#[allow(clippy::cognitive_complexity)]
pub async fn revert(&self, project_id: &str, input: RevertInput) -> CoreResult<Session> {
info!(
session_id = %input.session_id,
Expand Down Expand Up @@ -161,6 +162,7 @@ impl SessionRevert {
/// Clean up after a revert when the user continues.
///
/// This removes messages and parts after the revert point.
#[allow(clippy::cognitive_complexity)]
pub async fn cleanup(&self, project_id: &str, session_id: &str) -> CoreResult<()> {
let session = self.session_repo.get(project_id, session_id).await?;

Expand Down
1 change: 1 addition & 0 deletions crates/wonopcode-lsp/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ impl LspClient {
}

/// Get or spawn server for a file, with deduplication and broken tracking.
#[allow(clippy::cognitive_complexity)]
async fn get_servers_for_file(
&self,
file_path: &Path,
Expand Down
1 change: 1 addition & 0 deletions crates/wonopcode-mcp/src/callback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ impl Default for OAuthCallbackServer {
}

/// Handle an incoming HTTP connection.
#[allow(clippy::cognitive_complexity)]
async fn handle_connection(
mut stream: tokio::net::TcpStream,
state: Arc<RwLock<CallbackServerState>>,
Expand Down
2 changes: 2 additions & 0 deletions crates/wonopcode-mcp/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ impl McpClient {
}

/// Add and connect to an MCP server.
#[allow(clippy::cognitive_complexity)]
pub async fn add_server(&self, config: ServerConfig) -> McpResult<()> {
if !config.enabled {
debug!(server = %config.name, "Server is disabled, skipping");
Expand Down Expand Up @@ -286,6 +287,7 @@ impl McpClient {
/// Toggle a server's enabled state.
/// If enabled, disconnect and mark as disabled. If disabled, mark as enabled but don't connect.
/// Returns the new enabled state.
#[allow(clippy::cognitive_complexity)]
pub async fn toggle_server(&self, name: &str) -> McpResult<bool> {
let mut servers = self.servers.write().await;
if let Some(connection) = servers.get_mut(name) {
Expand Down
2 changes: 2 additions & 0 deletions crates/wonopcode-mcp/src/http_serve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ impl McpHttpState {
}

/// Handle a JSON-RPC request.
#[allow(clippy::cognitive_complexity)]
async fn handle_request(&self, request: JsonRpcRequest) -> Option<JsonRpcResponse> {
debug!(method = %request.method, id = ?request.id, "Handling MCP request");

Expand Down Expand Up @@ -231,6 +232,7 @@ impl McpHttpState {
}

/// Handle the tools/call request.
#[allow(clippy::cognitive_complexity)]
async fn handle_call_tool(&self, id: u64, params: Option<Value>) -> JsonRpcResponse {
// Parse parameters
let params: CallToolParams = match params {
Expand Down
1 change: 1 addition & 0 deletions crates/wonopcode-provider/src/claude_cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ impl ClaudeCliProvider {
}

/// Perform the actual authentication check (uncached, async).
#[allow(clippy::cognitive_complexity)]
async fn check_auth_uncached_async() -> bool {
let output = TokioCommand::new("claude")
.args(["-p", "hi", "--output-format", "json"])
Expand Down
2 changes: 2 additions & 0 deletions crates/wonopcode-sandbox/src/runtime/docker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ impl DockerRuntime {
/// Cleanup orphaned wonopcode containers that are stopped.
/// Only removes stopped containers to avoid disrupting other running agents.
/// Running containers from other projects are left alone - they may be in use.
#[allow(clippy::cognitive_complexity)]
pub async fn cleanup_orphaned_containers(&self) -> SandboxResult<()> {
let filters: HashMap<String, Vec<String>> = HashMap::from([
("label".to_string(), vec!["wonopcode=true".to_string()]),
Expand Down Expand Up @@ -153,6 +154,7 @@ impl DockerRuntime {
}

/// Ensure the container image is available.
#[allow(clippy::cognitive_complexity)]
async fn ensure_image(&self) -> SandboxResult<()> {
let image = self.config.image();

Expand Down
2 changes: 2 additions & 0 deletions crates/wonopcode-sandbox/src/runtime/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ impl SandboxManager {
}

/// Create a new runtime instance based on configuration.
#[allow(clippy::cognitive_complexity)]
async fn create_runtime(&self) -> SandboxResult<Arc<dyn SandboxRuntime>> {
let path_mapper = PathMapper::new(
self.project_root.clone(),
Expand Down Expand Up @@ -382,6 +383,7 @@ impl SandboxManager {
/// Detect available sandbox runtimes.
///
/// Checks for Docker, Podman, and Lima in order of preference.
#[allow(clippy::cognitive_complexity)]
pub async fn detect_runtime() -> SandboxRuntimeType {
// Check Docker
if is_docker_available().await {
Expand Down
1 change: 1 addition & 0 deletions crates/wonopcode-server/src/prompt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ impl ServerPromptRunner {
}

/// Run a prompt and stream events.
#[allow(clippy::cognitive_complexity)]
pub async fn run(
&self,
prompt: &str,
Expand Down
1 change: 1 addition & 0 deletions crates/wonopcode-server/src/ws.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ pub async fn ws_handler(ws: WebSocketUpgrade, State(state): State<AppState>) ->
}

/// Handle an individual WebSocket connection.
#[allow(clippy::cognitive_complexity)]
async fn handle_socket(socket: WebSocket, state: AppState) {
let (mut sender, mut receiver) = socket.split();

Expand Down
3 changes: 3 additions & 0 deletions crates/wonopcode-snapshot/src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ impl SnapshotStore {
/// * `session_id` - ID of the current session
/// * `message_id` - ID of the current message
/// * `description` - Description of why the snapshot was taken
#[allow(clippy::cognitive_complexity)]
pub async fn take(
&self,
files: &[PathBuf],
Expand Down Expand Up @@ -172,6 +173,7 @@ impl SnapshotStore {
///
/// # Arguments
/// * `snapshot_id` - ID of the snapshot to restore
#[allow(clippy::cognitive_complexity)]
pub async fn restore(&self, snapshot_id: &SnapshotId) -> SnapshotResult<Snapshot> {
let snapshot = self.get(snapshot_id).await?;
let snapshot_dir = self.snapshot_dir(snapshot_id);
Expand Down Expand Up @@ -314,6 +316,7 @@ impl SnapshotStore {
}

/// Clean up old snapshots based on configuration.
#[allow(clippy::cognitive_complexity)]
pub async fn cleanup(&self) -> SnapshotResult<u32> {
let mut deleted = 0;
let cutoff = Utc::now() - Duration::days(self.config.max_age_days as i64);
Expand Down
44 changes: 44 additions & 0 deletions crates/wonopcode-test-utils/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
[package]
name = "wonopcode-test-utils"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
keywords.workspace = true
categories.workspace = true
description = "Testing utilities, fixtures, and mocks for wonopcode"

[dependencies]
# Workspace crates for types
wonopcode-provider = { workspace = true }
wonopcode-sandbox = { workspace = true }

# Async runtime
tokio.workspace = true
futures.workspace = true
async-trait.workspace = true
async-stream.workspace = true

# Serialization
serde.workspace = true
serde_json.workspace = true

# Error handling
thiserror.workspace = true

# Testing tools
tempfile.workspace = true

# Text diffing for assertions
similar.workspace = true

# UUID generation
uuid.workspace = true

# Time handling
chrono.workspace = true

[lints]
workspace = true
Loading
Loading