Skip to content

feat(nix): add Nix package and NixOS integration test for cua-driver - #1746

Merged
r33drichards merged 8 commits into
mainfrom
nix/cua-driver-integration-test
May 28, 2026
Merged

feat(nix): add Nix package and NixOS integration test for cua-driver#1746
r33drichards merged 8 commits into
mainfrom
nix/cua-driver-integration-test

Conversation

@r33drichards

@r33drichards r33drichards commented May 28, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Adds a Nix flake (flake.nix) to the cua repo with packages.{x86_64,aarch64}-linux.cua-driver and checks.x86_64-linux.cua-driver-integration
  • nix/cua-driver/package.nix: Builds the Rust cua-driver binary via rustPlatform.buildRustPackage (pure Rust deps, no system libraries needed)
  • nix/cua-driver/module.nix: NixOS module (services.cua-driver.enable) that installs the binary + runtime deps (ImageMagick, AT-SPI, D-Bus)
  • nix/cua-driver/tests/integration.nix: NixOS VM integration test that:
    • Verifies CLI subcommands (list-tools, describe, doctor)
    • Starts Xvfb and runs full MCP protocol handshake (initialize -> tools/list -> tools/call)
    • Validates 34 registered tools including click, type_text, get_screen_size, get_window_state

Build verified locally

$ nix build .#cua-driver
$ ./result/bin/cua-driver list-tools | wc -l
34
$ nix flake show
checks.aarch64-linux.cua-driver-build
checks.x86_64-linux.cua-driver-build
checks.x86_64-linux.cua-driver-integration
nixosModules.cua-driver
packages.{aarch64,x86_64}-linux.{cua-driver,default}

Note

The .github/workflows/nix-build.yml CI workflow was prepared but could not be pushed due to OAuth scope restrictions. It needs to be added separately (requires workflow scope). The workflow depends on trycua/cloud#TBD being merged first (OIDC trust for nix cache).

Test plan

  • nix build .#cua-driver produces working binary
  • ./result/bin/cua-driver list-tools lists 34 tools
  • nix flake show shows correct outputs
  • nix build .#checks.x86_64-linux.cua-driver-integration passes on x86_64-linux

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • CUA driver is now available as a NixOS package and configurable service.
    • Multi-architecture support for x86_64-linux and aarch64-linux systems.
  • Tests

    • Added integration tests validating CLI and MCP server functionality.

Review Change Stack

Adds Nix infrastructure to build and test the cua-driver Rust binary on Linux:

- flake.nix: Top-level flake with packages, nixosModules, and checks
- nix/cua-driver/package.nix: Rust build via rustPlatform.buildRustPackage
- nix/cua-driver/module.nix: NixOS module (services.cua-driver.enable)
- nix/cua-driver/tests/integration.nix: VM integration test covering
  CLI subcommands, MCP protocol handshake, and tool invocation with Xvfb

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Ignored Ignored Preview May 28, 2026 5:28pm

Request Review

@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9f088fea-d265-479d-8e76-c686857f6939

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR introduces a complete Nix flake infrastructure for building and deploying cua-driver, a Rust-based MCP server. It defines package derivation, NixOS service options, flake exports, and end-to-end integration tests covering CLI behavior, X11 display interaction, and JSON-RPC protocol handshake.

Changes

Nix Flake Build and Service Setup

Layer / File(s) Summary
Flake structure and system support
flake.nix
Declares nixpkgs and flake-utils inputs, establishes per-system outputs for x86_64-linux and aarch64-linux, and sets up the source tree import.
Rust package derivation
nix/cua-driver/package.nix
Defines buildRustPackage for cua-driver v0.3.2 using cargoHash for vendor dependencies, disables tests (X11/AT-SPI requirements), restricts build to main crate, and limits platforms to Linux.
Package export in flake
flake.nix
Imports and exports the built package as packages.cua-driver and packages.default for both systems.
NixOS service module definition
nix/cua-driver/module.nix
Defines services.cua-driver options (enable, package) and configures the system to install the package, runtime dependencies (imagemagick, at-spi2-core), enable D-Bus, and set the CUA_DRIVER_BIN environment variable.
Module export and flake checks
flake.nix
Exports the NixOS module, defines cua-driver-build check for all systems, and conditionally adds cua-driver-integration check for x86_64-linux only, wiring it to the integration test and module.
End-to-end integration testing
nix/cua-driver/tests/integration.nix
Provisions a NixOS test VM with cua-driver enabled, validates CLI behavior (--help, list-tools, describe), runs cua-driver doctor against a virtual X11 display (:99), and executes a Python MCP client script that sends initialize, tool-list, and get_screen_size requests with response assertions.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A driver builds on Linux ground,
Via Nix, its home is found—
Package, module, flake so bright,
Tests confirm it runs just right!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: adding Nix package configuration and NixOS integration test for cua-driver across the four new files.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch nix/cua-driver-integration-test

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
flake.nix (1)

57-60: ⚡ Quick win

Export a ready-to-use module with package prewired.

Line 57 currently exports the raw module, while services.cua-driver.package has no default in nix/cua-driver/module.nix (Lines 24-31). Consider exporting a wrapped module that sets a default package from self.packages.${pkgs.system}.cua-driver to avoid easy consumer misconfiguration.

♻️ Suggested flake export tweak
-      # NixOS module — consumers must set services.cua-driver.package
-      # (or use the per-system package from self.packages)
-      nixosModules.cua-driver = ./nix/cua-driver/module.nix;
+      # NixOS module with default package wiring
+      nixosModules.cua-driver = { pkgs, lib, ... }: {
+        imports = [ ./nix/cua-driver/module.nix ];
+        services.cua-driver.package = lib.mkDefault self.packages.${pkgs.system}.cua-driver;
+      };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@flake.nix` around lines 57 - 60, Export a wrapped NixOS module instead of the
raw one so services.cua-driver.package gets a sensible default; specifically,
change the export at nixosModules.cua-driver to call/overlay the module (from
./nix/cua-driver/module.nix) with a default for services.cua-driver.package
pointing to self.packages.${pkgs.system}.cua-driver (use a wrapper or
mkForce/defaults mechanism in the flake evaluation to inject that default while
preserving the original module API).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@nix/cua-driver/tests/integration.nix`:
- Around line 156-161: The package list in environment.systemPackages only
includes xorg.xorgserver but the test calls xdpyinfo and will fail if its
package isn't present; update the environment.systemPackages array (where
xorg.xorgserver is listed) to also include xorg.xdpyinfo so xdpyinfo is
available during the integration test run.
- Around line 113-121: The current check treats any response with a "result" key
as a pass even if that result contains an error payload; modify the logic so
that when "result" in resp you also verify the result is not an error and
contains valid content: check that resp["result"] does not include an "error"
key (e.g., if resp["result"].get("error") is truthy, treat it like the "error"
branch) and require non-empty content before printing the PASS message for
get_screen_size; otherwise raise an AssertionError or route to the existing
error handling branch. Ensure you update the block that reads resp, content, and
text to implement these extra checks.

---

Nitpick comments:
In `@flake.nix`:
- Around line 57-60: Export a wrapped NixOS module instead of the raw one so
services.cua-driver.package gets a sensible default; specifically, change the
export at nixosModules.cua-driver to call/overlay the module (from
./nix/cua-driver/module.nix) with a default for services.cua-driver.package
pointing to self.packages.${pkgs.system}.cua-driver (use a wrapper or
mkForce/defaults mechanism in the flake evaluation to inject that default while
preserving the original module API).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ee0af4bc-e9bd-44d6-bc4c-e75c0d33d249

📥 Commits

Reviewing files that changed from the base of the PR and between 61cbd03 and 28a4990.

⛔ Files ignored due to path filters (1)
  • flake.lock is excluded by !**/*.lock
📒 Files selected for processing (4)
  • flake.nix
  • nix/cua-driver/module.nix
  • nix/cua-driver/package.nix
  • nix/cua-driver/tests/integration.nix

Comment on lines +113 to +121
if "result" in resp:
content = resp["result"].get("content", [])
text = content[0].get("text", "") if content else ""
print(f"PASS: get_screen_size returned: {text[:200]}", flush=True)
elif "error" in resp:
err_msg = resp.get("error", {}).get("message", "unknown")
print(f"PASS: get_screen_size returned error (acceptable in headless): {err_msg}", flush=True)
else:
raise AssertionError(f"Unexpected response: {resp}")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Strengthen get_screen_size assertions to prevent false-positive passes.

Any response containing "result" currently passes, even if the tool reports an error payload. This weakens the integration contract for tools/call.

Suggested patch
             # get_screen_size should return display dimensions from Xvfb
             if "result" in resp:
-                content = resp["result"].get("content", [])
-                text = content[0].get("text", "") if content else ""
-                print(f"PASS: get_screen_size returned: {text[:200]}", flush=True)
+                is_error = resp["result"].get("isError", False)
+                assert not is_error, f"get_screen_size returned isError=true: {resp}"
+                sc = resp["result"].get("structuredContent", {})
+                assert sc.get("width", 0) > 0, f"Invalid width: {resp}"
+                assert sc.get("height", 0) > 0, f"Invalid height: {resp}"
+                print(f"PASS: get_screen_size returned {sc}", flush=True)
             elif "error" in resp:
                 err_msg = resp.get("error", {}).get("message", "unknown")
                 print(f"PASS: get_screen_size returned error (acceptable in headless): {err_msg}", flush=True)
             else:
                 raise AssertionError(f"Unexpected response: {resp}")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@nix/cua-driver/tests/integration.nix` around lines 113 - 121, The current
check treats any response with a "result" key as a pass even if that result
contains an error payload; modify the logic so that when "result" in resp you
also verify the result is not an error and contains valid content: check that
resp["result"] does not include an "error" key (e.g., if
resp["result"].get("error") is truthy, treat it like the "error" branch) and
require non-empty content before printing the PASS message for get_screen_size;
otherwise raise an AssertionError or route to the existing error handling
branch. Ensure you update the block that reads resp, content, and text to
implement these extra checks.

Comment on lines +156 to +161
environment.systemPackages = with pkgs; [
xorg.xorgserver # Xvfb for headless X11
python3 # MCP client test script
jq
procps # pgrep/pkill
];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add the package that provides xdpyinfo to avoid command-not-found failures.

The test executes xdpyinfo (Line 186), but the package list only adds xorg.xorgserver. Please add xorg.xdpyinfo explicitly.

Suggested patch
       environment.systemPackages = with pkgs; [
         xorg.xorgserver # Xvfb for headless X11
+        xorg.xdpyinfo  # xdpyinfo used to verify DISPLAY
         python3 # MCP client test script
         jq
         procps # pgrep/pkill
       ];

Also applies to: 186-186

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@nix/cua-driver/tests/integration.nix` around lines 156 - 161, The package
list in environment.systemPackages only includes xorg.xorgserver but the test
calls xdpyinfo and will fail if its package isn't present; update the
environment.systemPackages array (where xorg.xorgserver is listed) to also
include xorg.xdpyinfo so xdpyinfo is available during the integration test run.

enchanted-koala and others added 7 commits May 28, 2026 16:20
Runs on PRs and pushes to main when nix/**, flake.nix, flake.lock,
or libs/cua-driver/rust/** change. Uses OIDC to access the shared S3
nix binary cache. Signs and pushes artifacts on main branch merges.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The integration test already builds the package as a dependency,
making the separate build and smoke test steps redundant.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove unused `import json` from testScript (ruff F401 lint failure)
- Always sign+upload to S3 cache, even on failed runs, so partial
  build artifacts are cached for faster retries
- Add want-mass-query=true to skip uploading paths that already exist

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
doctor's AT-SPI probe can hang without a proper D-Bus session bus.
Wrap doctor with timeout 15s and the MCP test with timeout 60s.
Remove xdpyinfo check that may not be available.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
machine.succeed blocks even with &. Use machine.execute which
fires-and-forgets, then wait_until_succeeds for the X11 socket.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants