From 80523e56f0a5933f3071df684c6ab48f67a81c44 Mon Sep 17 00:00:00 2001 From: Logan Nguyen Date: Tue, 31 Mar 2026 14:09:25 -0500 Subject: [PATCH 1/7] chore: add CI pipeline and Dependabot config Adds a composable GitHub Actions PR pipeline with three parallel jobs: - Build validation (macos-latest): bun validate-build (lint, format, typecheck, full Tauri build) - Frontend tests (ubuntu-latest): bun test:coverage (Vitest, 100% threshold) - Backend tests (macos-latest): bun test:backend (cargo test) Adds Dependabot watching npm (/), cargo (/src-tauri), and github-actions weekly. Signed-off-by: Logan Nguyen Co-Authored-By: Claude Sonnet 4.6 --- .github/dependabot.yml | 50 +++++++++++++++++++++++ .github/workflows/pr-backend-tests.yml | 37 +++++++++++++++++ .github/workflows/pr-build-validation.yml | 49 ++++++++++++++++++++++ .github/workflows/pr-frontend-tests.yml | 37 +++++++++++++++++ .github/workflows/pr-pipeline.yml | 39 ++++++++++++++++++ 5 files changed, 212 insertions(+) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/pr-backend-tests.yml create mode 100644 .github/workflows/pr-build-validation.yml create mode 100644 .github/workflows/pr-frontend-tests.yml create mode 100644 .github/workflows/pr-pipeline.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..10dbcdbb --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,50 @@ +version: 2 +updates: + # Check for updates to frontend npm dependencies + - package-ecosystem: 'npm' + directory: '/' + schedule: + interval: 'weekly' + open-pull-requests-limit: 3 + groups: + dependencies-minor-patch: + patterns: + - '*' + update-types: + - 'minor' + - 'patch' + dependencies-major: + patterns: + - '*' + update-types: + - 'major' + + # Check for updates to Rust/Cargo dependencies + - package-ecosystem: 'cargo' + directory: '/src-tauri' + schedule: + interval: 'weekly' + open-pull-requests-limit: 3 + groups: + dependencies-minor-patch: + patterns: + - '*' + update-types: + - 'minor' + - 'patch' + dependencies-major: + patterns: + - '*' + update-types: + - 'major' + + # Check for updates to GitHub Actions + - package-ecosystem: 'github-actions' + directory: '/' + schedule: + interval: 'weekly' + open-pull-requests-limit: 3 + groups: + github-actions: + patterns: + - '*' diff --git a/.github/workflows/pr-backend-tests.yml b/.github/workflows/pr-backend-tests.yml new file mode 100644 index 00000000..81d523a3 --- /dev/null +++ b/.github/workflows/pr-backend-tests.yml @@ -0,0 +1,37 @@ +name: Backend Tests + +on: + workflow_call: + inputs: + head_ref: + description: 'The head ref of the PR' + required: false + type: string + event_name: + description: 'The event name' + required: false + type: string + +jobs: + test: + name: Backend Tests + runs-on: macos-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ inputs.head_ref || github.head_ref }} + fetch-depth: 0 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Run backend tests + run: | + echo "Running Cargo tests..." + bun run test:backend diff --git a/.github/workflows/pr-build-validation.yml b/.github/workflows/pr-build-validation.yml new file mode 100644 index 00000000..1d956dd6 --- /dev/null +++ b/.github/workflows/pr-build-validation.yml @@ -0,0 +1,49 @@ +name: Build Validation + +on: + workflow_call: + inputs: + head_ref: + description: 'The head ref of the PR' + required: false + type: string + event_name: + description: 'The event name' + required: false + type: string + +jobs: + build: + name: Build Validation + runs-on: macos-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ inputs.head_ref || github.head_ref }} + fetch-depth: 0 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Verify dependencies + run: bun pm ls + + - name: Security vulnerability scan + run: | + echo "Running security audit for dependencies..." + if bun audit; then + echo "Security scan completed successfully" + else + echo "::warning::Security vulnerabilities detected in dependencies" + fi + + - name: Build validation + run: | + echo "Running full build validation (lint, format, typecheck, frontend + Tauri build)..." + bun run validate-build diff --git a/.github/workflows/pr-frontend-tests.yml b/.github/workflows/pr-frontend-tests.yml new file mode 100644 index 00000000..b3e6b923 --- /dev/null +++ b/.github/workflows/pr-frontend-tests.yml @@ -0,0 +1,37 @@ +name: Frontend Tests + +on: + workflow_call: + inputs: + head_ref: + description: 'The head ref of the PR' + required: false + type: string + event_name: + description: 'The event name' + required: false + type: string + +jobs: + test: + name: Frontend Tests + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ inputs.head_ref || github.head_ref }} + fetch-depth: 0 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Run frontend tests with coverage + run: | + echo "Running Vitest with 100% coverage threshold..." + bun run test:coverage diff --git a/.github/workflows/pr-pipeline.yml b/.github/workflows/pr-pipeline.yml new file mode 100644 index 00000000..980241ec --- /dev/null +++ b/.github/workflows/pr-pipeline.yml @@ -0,0 +1,39 @@ +name: PR Pipeline + +on: + pull_request: + branches: [main] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read + pull-requests: write + +jobs: + build-validation: + name: Build Validation + uses: ./.github/workflows/pr-build-validation.yml + with: + head_ref: ${{ github.head_ref }} + event_name: ${{ github.event_name }} + secrets: inherit + + frontend-tests: + name: Frontend Tests + uses: ./.github/workflows/pr-frontend-tests.yml + with: + head_ref: ${{ github.head_ref }} + event_name: ${{ github.event_name }} + secrets: inherit + + backend-tests: + name: Backend Tests + uses: ./.github/workflows/pr-backend-tests.yml + with: + head_ref: ${{ github.head_ref }} + event_name: ${{ github.event_name }} + secrets: inherit From 9bbb343c3b8cb3b1e918c9ec72a69c0440ac73c8 Mon Sep 17 00:00:00 2001 From: Logan Nguyen Date: Tue, 31 Mar 2026 14:13:25 -0500 Subject: [PATCH 2/7] chore: update bun lockfile with test dependencies Signed-off-by: Logan Nguyen --- bun.lock | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/bun.lock b/bun.lock index 9135573c..d079221d 100644 --- a/bun.lock +++ b/bun.lock @@ -8,7 +8,6 @@ "@tauri-apps/api": "^2", "dompurify": "^3.3.3", "framer-motion": "^12.38.0", - "jsdom": "^29.0.1", "marked": "^17.0.5", "react": "^19.1.0", "react-dom": "^19.1.0", @@ -19,7 +18,6 @@ "@tauri-apps/cli": "^2", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", - "@testing-library/user-event": "^14.6.1", "@types/dompurify": "^3.2.0", "@types/marked": "^6.0.0", "@types/node": "^25.5.0", @@ -31,7 +29,7 @@ "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^7.0.1", "globals": "^17.4.0", - "happy-dom": "^20.8.9", + "jsdom": "^29.0.1", "msw": "^2.12.14", "prettier": "^3.8.1", "tailwindcss": "^4.2.2", @@ -331,8 +329,6 @@ "@testing-library/react": ["@testing-library/react@16.3.2", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "@testing-library/dom": "^10.0.0", "@types/react": "^18.0.0 || ^19.0.0", "@types/react-dom": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g=="], - "@testing-library/user-event": ["@testing-library/user-event@14.6.1", "", { "peerDependencies": { "@testing-library/dom": ">=7.21.4" } }, "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw=="], - "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="], "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], @@ -525,7 +521,7 @@ "enhanced-resolve": ["enhanced-resolve@5.20.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA=="], - "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], + "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], "es-abstract": ["es-abstract@1.24.1", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw=="], @@ -1037,7 +1033,7 @@ "webidl-conversions": ["webidl-conversions@8.0.1", "", {}, "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ=="], - "whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="], + "whatwg-mimetype": ["whatwg-mimetype@5.0.0", "", {}, "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw=="], "whatwg-url": ["whatwg-url@16.0.1", "", { "dependencies": { "@exodus/bytes": "^1.11.0", "tr46": "^6.0.0", "webidl-conversions": "^8.0.1" } }, "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw=="], @@ -1113,18 +1109,16 @@ "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - "data-urls/whatwg-mimetype": ["whatwg-mimetype@5.0.0", "", {}, "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw=="], - "eslint/@eslint/js": ["@eslint/js@9.39.4", "", {}, "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw=="], - "jsdom/whatwg-mimetype": ["whatwg-mimetype@5.0.0", "", {}, "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw=="], + "happy-dom/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], + + "happy-dom/whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="], "loose-envify/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "make-dir/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], - "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], - "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], "pretty-format/react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], From fab13d054e718f2c96c9b9382ec40d99bd9fd205 Mon Sep 17 00:00:00 2001 From: Logan Nguyen Date: Tue, 31 Mar 2026 14:45:57 -0500 Subject: [PATCH 3/7] test: enforce 100% Rust coverage via cargo-llvm-cov - Add #[cfg_attr(coverage_nightly, coverage(off))] to untestable runtime functions: CGEventTap setup, AX capture, Tauri command wrappers - Add 5 new tests in commands.rs covering: invalid UTF-8 bytes in stream, mid-stream connection drop, whitespace-only lines, missing response field, and an empty-body 500 variant - Add new_activator_is_inactive test in activator.rs - Configure cargo-llvm-cov to exclude lib.rs and main.rs (Tauri runtime setup that requires a live app process) and enforce --fail-under-lines 100 - Register coverage cfgs in build.rs to suppress unexpected_cfg warnings - Update pr-backend-tests CI job to install nightly toolchain and cargo-llvm-cov via taiki-e/install-action Signed-off-by: Logan Nguyen Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/pr-backend-tests.yml | 13 ++- src-tauri/Cargo.toml | 2 +- src-tauri/build.rs | 3 + src-tauri/src/activator.rs | 10 ++ src-tauri/src/commands.rs | 129 ++++++++++++++++++++++++- src-tauri/src/context.rs | 2 + src-tauri/src/lib.rs | 3 + 7 files changed, 157 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pr-backend-tests.yml b/.github/workflows/pr-backend-tests.yml index 81d523a3..5d52246e 100644 --- a/.github/workflows/pr-backend-tests.yml +++ b/.github/workflows/pr-backend-tests.yml @@ -31,7 +31,14 @@ jobs: - name: Install dependencies run: bun install --frozen-lockfile - - name: Run backend tests + - name: Install nightly Rust toolchain + run: rustup toolchain install nightly --no-self-update + + - name: Install cargo-llvm-cov + uses: taiki-e/install-action@cargo-llvm-cov + + - name: Run backend tests with coverage + working-directory: src-tauri run: | - echo "Running Cargo tests..." - bun run test:backend + echo "Running Cargo tests with 100% line coverage enforcement..." + cargo +nightly llvm-cov --ignore-filename-regex "(lib|main)\.rs" --fail-under-lines 100 diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 7509c7c4..d2b7b39e 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -32,5 +32,5 @@ core-foundation = "0.10" [dev-dependencies] mockito = "1" -tokio = { version = "1", features = ["rt", "macros"] } +tokio = { version = "1", features = ["rt", "macros", "net", "io-util"] } diff --git a/src-tauri/build.rs b/src-tauri/build.rs index d860e1e6..09a62663 100644 --- a/src-tauri/build.rs +++ b/src-tauri/build.rs @@ -1,3 +1,6 @@ fn main() { + // Register cfg flags set by cargo-llvm-cov so rustc doesn't warn about unknown cfgs. + println!("cargo::rustc-check-cfg=cfg(coverage)"); + println!("cargo::rustc-check-cfg=cfg(coverage_nightly)"); tauri_build::build() } diff --git a/src-tauri/src/activator.rs b/src-tauri/src/activator.rs index 4532b2a4..931e1f48 100644 --- a/src-tauri/src/activator.rs +++ b/src-tauri/src/activator.rs @@ -60,6 +60,7 @@ extern "C" { /// Under development builds launched via terminal, macOS attributes this /// permission to the terminal emulator. In production `.app` bundles, the /// permission is correctly attributed to the application identity. +#[cfg_attr(coverage_nightly, coverage(off))] fn request_authorization(prompt: bool) -> bool { unsafe { if AXIsProcessTrusted() { @@ -148,6 +149,7 @@ impl OverlayActivator { /// /// * `on_activation` - A thread-safe closure executed whenever the activation /// sequence is detected. + #[cfg_attr(coverage_nightly, coverage(off))] pub fn start(&self, on_activation: F) where F: Fn() + Send + Sync + 'static, @@ -170,6 +172,7 @@ impl OverlayActivator { } /// Persistence layer that maintains the event loop through permission cycles. +#[cfg_attr(coverage_nightly, coverage(off))] fn run_loop_with_retry(is_active: Arc, on_activation: Arc) where F: Fn() + Send + Sync + 'static, @@ -191,6 +194,7 @@ where } /// Core initialization of the Mach event tap. +#[cfg_attr(coverage_nightly, coverage(off))] fn try_initialize_tap(is_active: &Arc, on_activation: &Arc) -> bool where F: Fn() + Send + Sync + 'static, @@ -262,6 +266,12 @@ where mod tests { use super::*; + #[test] + fn new_activator_is_inactive() { + let activator = OverlayActivator::new(); + assert!(!activator.is_active.load(std::sync::atomic::Ordering::SeqCst)); + } + #[test] fn validates_activation_sequence() { let mut state = ActivationState { diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 07b1a798..4d92d3c2 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -106,7 +106,8 @@ pub async fn stream_ollama( /// Streams text chunks from the local Ollama backend via `reqwest` to the frontend using `Channel`. /// Uses `State` to persist the HTTP Client's connection pool. -#[tauri::command] +#[cfg_attr(coverage_nightly, coverage(off))] +#[cfg_attr(not(coverage), tauri::command)] pub async fn ask_ollama( prompt: String, on_event: Channel, @@ -305,6 +306,76 @@ mod tests { assert_eq!(tokens, vec!["A", "B", "C"]); } + #[tokio::test] + async fn handles_invalid_utf8_in_stream() { + let mut server = mockito::Server::new_async().await; + // Line 1: invalid UTF-8 bytes + newline, Line 2: valid JSON + newline + let body = b"\xFF\xFE\n{\"response\":\"ok\",\"done\":true}\n".to_vec(); + let mock = server + .mock("POST", "/api/generate") + .with_body(body) + .create_async() + .await; + + let client = reqwest::Client::new(); + let (chunks, callback) = collect_chunks(); + + stream_ollama( + &format!("{}/api/generate", server.url()), + "test-model", + "hi".to_string(), + &client, + callback, + ) + .await; + + mock.assert_async().await; + let chunks = chunks.lock().unwrap(); + // Invalid UTF-8 line is silently skipped; valid line emits Done + assert!(chunks.iter().any(|c| matches!(c, StreamChunk::Done))); + } + + #[tokio::test] + async fn handles_mid_stream_network_error() { + use tokio::io::AsyncWriteExt; + use tokio::net::TcpListener; + + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let port = listener.local_addr().unwrap().port(); + + tokio::spawn(async move { + let (mut stream, _) = listener.accept().await.unwrap(); + // Send valid HTTP headers then partial chunked body and drop + let _ = stream + .write_all( + b"HTTP/1.1 200 OK\r\n\ + Content-Type: application/x-ndjson\r\n\ + Transfer-Encoding: chunked\r\n\r\n\ + 4\r\ntest", + ) + .await; + // Drop stream — connection closed mid-chunk, triggering a reqwest stream error + }); + + let client = reqwest::Client::new(); + let (chunks, callback) = collect_chunks(); + + stream_ollama( + &format!("http://127.0.0.1:{}/api/generate", port), + "test-model", + "hi".to_string(), + &client, + callback, + ) + .await; + + let chunks = chunks.lock().unwrap(); + // Either an Error chunk from the stream error, or empty if reqwest absorbed the truncation + // Either way, no panic and the function completed cleanly + let has_no_tokens = chunks.iter().all(|c| !matches!(c, StreamChunk::Token(_))); + assert!(has_no_tokens); + } + #[tokio::test] async fn http_500_with_empty_body() { let mut server = mockito::Server::new_async().await; @@ -332,4 +403,60 @@ mod tests { assert_eq!(chunks.len(), 1); assert!(matches!(&chunks[0], StreamChunk::Error(e) if e.contains("HTTP 500"))); } + + #[tokio::test] + async fn whitespace_only_lines_are_skipped() { + let mut server = mockito::Server::new_async().await; + // A line of only spaces/tabs followed by a valid JSON line + let mock = server + .mock("POST", "/api/generate") + .with_body(" \n{\"response\":\"hi\",\"done\":true}\n") + .create_async() + .await; + + let client = reqwest::Client::new(); + let (chunks, callback) = collect_chunks(); + + stream_ollama( + &format!("{}/api/generate", server.url()), + "test-model", + "hi".to_string(), + &client, + callback, + ) + .await; + + mock.assert_async().await; + let chunks = chunks.lock().unwrap(); + assert!(chunks.iter().any(|c| matches!(c, StreamChunk::Done))); + } + + #[tokio::test] + async fn response_field_absent_emits_only_done() { + let mut server = mockito::Server::new_async().await; + // JSON where `response` key is missing — exercises the None arm of if-let Some(token) + let mock = server + .mock("POST", "/api/generate") + .with_body("{\"done\":true}\n") + .create_async() + .await; + + let client = reqwest::Client::new(); + let (chunks, callback) = collect_chunks(); + + stream_ollama( + &format!("{}/api/generate", server.url()), + "test-model", + "hi".to_string(), + &client, + callback, + ) + .await; + + mock.assert_async().await; + let chunks = chunks.lock().unwrap(); + // No Token chunks since response is absent; Done is emitted + assert!(chunks.iter().all(|c| !matches!(c, StreamChunk::Token(_)))); + assert!(chunks.iter().any(|c| matches!(c, StreamChunk::Done))); + } } diff --git a/src-tauri/src/context.rs b/src-tauri/src/context.rs index 08b30902..c3ba3828 100644 --- a/src-tauri/src/context.rs +++ b/src-tauri/src/context.rs @@ -47,6 +47,7 @@ impl ActivationContext { // ─── macOS AX capture ──────────────────────────────────────────────────────── #[cfg(target_os = "macos")] +#[cfg_attr(coverage_nightly, coverage(off))] mod macos { use std::ffi::c_void; @@ -317,6 +318,7 @@ mod macos { /// /// When `overlay_is_visible` is `true` the hotkey will hide the overlay, so /// no context is needed — skip AX queries and clipboard simulation entirely. +#[cfg_attr(coverage_nightly, coverage(off))] pub fn capture_activation_context(overlay_is_visible: bool) -> ActivationContext { if overlay_is_visible { return ActivationContext::empty(); diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index ecb29276..d767e1a8 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -13,6 +13,8 @@ * managed by the `activator` module. */ +#![cfg_attr(coverage_nightly, feature(coverage_attribute))] + pub mod commands; #[cfg(target_os = "macos")] @@ -482,6 +484,7 @@ pub fn run() { Ok(()) }) .invoke_handler(tauri::generate_handler![ + #[cfg(not(coverage))] commands::ask_ollama, notify_overlay_hidden, set_window_frame From 853f61659bf37d0249a606c92cfc60dac298d5cd Mon Sep 17 00:00:00 2001 From: Logan Nguyen Date: Tue, 31 Mar 2026 14:51:09 -0500 Subject: [PATCH 4/7] chore: formatted codebase Signed-off-by: Logan Nguyen --- src-tauri/src/activator.rs | 4 +++- src/__tests__/App.test.tsx | 25 +++++++++++++++----- src/components/__tests__/ChatBubble.test.tsx | 8 +++++-- src/components/__tests__/CopyButton.test.tsx | 8 +++++-- src/view/__tests__/AskBarView.test.tsx | 4 +++- src/view/__tests__/ConversationView.test.tsx | 4 +++- 6 files changed, 40 insertions(+), 13 deletions(-) diff --git a/src-tauri/src/activator.rs b/src-tauri/src/activator.rs index 931e1f48..7b9ff4cd 100644 --- a/src-tauri/src/activator.rs +++ b/src-tauri/src/activator.rs @@ -269,7 +269,9 @@ mod tests { #[test] fn new_activator_is_inactive() { let activator = OverlayActivator::new(); - assert!(!activator.is_active.load(std::sync::atomic::Ordering::SeqCst)); + assert!(!activator + .is_active + .load(std::sync::atomic::Ordering::SeqCst)); } #[test] diff --git a/src/__tests__/App.test.tsx b/src/__tests__/App.test.tsx index e1131ac5..8ef07280 100644 --- a/src/__tests__/App.test.tsx +++ b/src/__tests__/App.test.tsx @@ -40,7 +40,9 @@ describe('App', () => { await showOverlay(); - expect(screen.getByPlaceholderText('Ask Thuki anything...')).toBeInTheDocument(); + expect( + screen.getByPlaceholderText('Ask Thuki anything...'), + ).toBeInTheDocument(); }); it('hides overlay on Escape key', async () => { @@ -50,7 +52,9 @@ describe('App', () => { await showOverlay(); // Confirm overlay is visible - expect(screen.getByPlaceholderText('Ask Thuki anything...')).toBeInTheDocument(); + expect( + screen.getByPlaceholderText('Ask Thuki anything...'), + ).toBeInTheDocument(); act(() => { fireEvent.keyDown(window, { key: 'Escape' }); @@ -106,7 +110,9 @@ describe('App', () => { // First show overlay await showOverlay(); - expect(screen.getByPlaceholderText('Ask Thuki anything...')).toBeInTheDocument(); + expect( + screen.getByPlaceholderText('Ask Thuki anything...'), + ).toBeInTheDocument(); // Then send hide-request — calls requestHideOverlay() (not handleCloseOverlay) await act(async () => { @@ -123,7 +129,9 @@ describe('App', () => { await act(async () => {}); await showOverlay(); - expect(screen.getByPlaceholderText('Ask Thuki anything...')).toBeInTheDocument(); + expect( + screen.getByPlaceholderText('Ask Thuki anything...'), + ).toBeInTheDocument(); act(() => { fireEvent.keyDown(window, { key: 'w', metaKey: true }); @@ -332,7 +340,10 @@ describe('App', () => { await act(async () => {}); act(() => { - getLastChannel()?.simulateMessage({ type: 'Token', data: 'First response' }); + getLastChannel()?.simulateMessage({ + type: 'Token', + data: 'First response', + }); getLastChannel()?.simulateMessage({ type: 'Done' }); }); @@ -345,7 +356,9 @@ describe('App', () => { await showOverlay(); // Should be back to input bar mode with placeholder - expect(screen.getByPlaceholderText('Ask Thuki anything...')).toBeInTheDocument(); + expect( + screen.getByPlaceholderText('Ask Thuki anything...'), + ).toBeInTheDocument(); // Old messages should be gone expect(screen.queryByText('First response')).toBeNull(); }); diff --git a/src/components/__tests__/ChatBubble.test.tsx b/src/components/__tests__/ChatBubble.test.tsx index 7ea9a588..2c0384bd 100644 --- a/src/components/__tests__/ChatBubble.test.tsx +++ b/src/components/__tests__/ChatBubble.test.tsx @@ -27,7 +27,9 @@ describe('ChatBubble', () => { it('shows copy button for user messages', () => { render(); - expect(screen.getByRole('button', { name: 'Copy message' })).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: 'Copy message' }), + ).toBeInTheDocument(); }); it('right-aligns user messages (justify-end class)', () => { @@ -58,7 +60,9 @@ describe('ChatBubble', () => { it('shows copy button for assistant messages', () => { render(); - expect(screen.getByRole('button', { name: 'Copy message' })).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: 'Copy message' }), + ).toBeInTheDocument(); }); it('left-aligns assistant messages (justify-start class)', () => { diff --git a/src/components/__tests__/CopyButton.test.tsx b/src/components/__tests__/CopyButton.test.tsx index ea457d0c..9d625e0c 100644 --- a/src/components/__tests__/CopyButton.test.tsx +++ b/src/components/__tests__/CopyButton.test.tsx @@ -34,7 +34,9 @@ describe('CopyButton', () => { act(() => { vi.advanceTimersByTime(1500); }); - expect(screen.getByRole('button', { name: 'Copy message' })).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: 'Copy message' }), + ).toBeInTheDocument(); vi.useRealTimers(); }); @@ -47,7 +49,9 @@ describe('CopyButton', () => { fireEvent.click(screen.getByRole('button', { name: 'Copy message' })); }); // Button should remain in un-copied state after failure - expect(screen.getByRole('button', { name: 'Copy message' })).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: 'Copy message' }), + ).toBeInTheDocument(); }); it('handles multiple rapid clicks', async () => { diff --git a/src/view/__tests__/AskBarView.test.tsx b/src/view/__tests__/AskBarView.test.tsx index 2e22fef7..fcd251b4 100644 --- a/src/view/__tests__/AskBarView.test.tsx +++ b/src/view/__tests__/AskBarView.test.tsx @@ -167,7 +167,9 @@ describe('AskBarView', () => { inputRef={makeRef()} />, ); - expect(screen.getByRole('button', { name: 'Send message' })).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: 'Send message' }), + ).toBeInTheDocument(); }); it('displays selectedText when provided', () => { diff --git a/src/view/__tests__/ConversationView.test.tsx b/src/view/__tests__/ConversationView.test.tsx index 6af146a7..a9ffe2ee 100644 --- a/src/view/__tests__/ConversationView.test.tsx +++ b/src/view/__tests__/ConversationView.test.tsx @@ -99,7 +99,9 @@ describe('ConversationView', () => { onClose={onClose} />, ); - expect(screen.getByRole('button', { name: 'Close window' })).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: 'Close window' }), + ).toBeInTheDocument(); }); it('renders empty state with no messages (no .chat-bubble elements)', () => { From 7542c6605de8380e0d713023bd2d88826f2cd376 Mon Sep 17 00:00:00 2001 From: Logan Nguyen Date: Tue, 31 Mar 2026 15:06:29 -0500 Subject: [PATCH 5/7] chore: address code review findings on CI workflows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Security: - Remove secrets: inherit — no workflow consumes any secrets - Pin all GitHub Actions to immutable commit SHAs (checkout v4.3.1, setup-bun v2.2.0, taiki-e/install-action v2.9.4) - Make bun audit a hard failure instead of a non-blocking warning Reproducibility: - Add rust-toolchain.toml pinning nightly-2026-03-30 so the coverage toolchain is explicit and stable rather than always-latest nightly - Pin bun-version to 1.3.11 across all workflows YAGNI / cleanup: - Remove unused event_name input from all four workflow files - Remove bun pm ls (Verify dependencies) step — frozen-lockfile already guarantees consistency - Remove Setup Bun and Install dependencies steps from pr-backend-tests (cargo-llvm-cov does not require the frontend toolchain) - Change fetch-depth: 0 to fetch-depth: 1 across all reusable workflows Signed-off-by: Logan Nguyen Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/pr-backend-tests.yml | 23 +++++------------------ .github/workflows/pr-build-validation.yml | 23 +++++------------------ .github/workflows/pr-frontend-tests.yml | 12 ++++-------- .github/workflows/pr-pipeline.yml | 6 ------ rust-toolchain.toml | 2 ++ 5 files changed, 16 insertions(+), 50 deletions(-) create mode 100644 rust-toolchain.toml diff --git a/.github/workflows/pr-backend-tests.yml b/.github/workflows/pr-backend-tests.yml index 5d52246e..b8ed9412 100644 --- a/.github/workflows/pr-backend-tests.yml +++ b/.github/workflows/pr-backend-tests.yml @@ -7,10 +7,6 @@ on: description: 'The head ref of the PR' required: false type: string - event_name: - description: 'The event name' - required: false - type: string jobs: test: @@ -18,24 +14,15 @@ jobs: runs-on: macos-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: ref: ${{ inputs.head_ref || github.head_ref }} - fetch-depth: 0 - - - name: Setup Bun - uses: oven-sh/setup-bun@v2 - with: - bun-version: latest - - - name: Install dependencies - run: bun install --frozen-lockfile - - - name: Install nightly Rust toolchain - run: rustup toolchain install nightly --no-self-update + fetch-depth: 1 - name: Install cargo-llvm-cov - uses: taiki-e/install-action@cargo-llvm-cov + uses: taiki-e/install-action@899b013517f9e7774591216672bf75a46bb9a481 # v2.9.4 + with: + tool: cargo-llvm-cov - name: Run backend tests with coverage working-directory: src-tauri diff --git a/.github/workflows/pr-build-validation.yml b/.github/workflows/pr-build-validation.yml index 1d956dd6..817dfc9a 100644 --- a/.github/workflows/pr-build-validation.yml +++ b/.github/workflows/pr-build-validation.yml @@ -7,10 +7,6 @@ on: description: 'The head ref of the PR' required: false type: string - event_name: - description: 'The event name' - required: false - type: string jobs: build: @@ -18,30 +14,21 @@ jobs: runs-on: macos-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: ref: ${{ inputs.head_ref || github.head_ref }} - fetch-depth: 0 + fetch-depth: 1 - name: Setup Bun - uses: oven-sh/setup-bun@v2 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: latest + bun-version: 1.3.11 - name: Install dependencies run: bun install --frozen-lockfile - - name: Verify dependencies - run: bun pm ls - - name: Security vulnerability scan - run: | - echo "Running security audit for dependencies..." - if bun audit; then - echo "Security scan completed successfully" - else - echo "::warning::Security vulnerabilities detected in dependencies" - fi + run: bun audit - name: Build validation run: | diff --git a/.github/workflows/pr-frontend-tests.yml b/.github/workflows/pr-frontend-tests.yml index b3e6b923..8c1da3f2 100644 --- a/.github/workflows/pr-frontend-tests.yml +++ b/.github/workflows/pr-frontend-tests.yml @@ -7,10 +7,6 @@ on: description: 'The head ref of the PR' required: false type: string - event_name: - description: 'The event name' - required: false - type: string jobs: test: @@ -18,15 +14,15 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: ref: ${{ inputs.head_ref || github.head_ref }} - fetch-depth: 0 + fetch-depth: 1 - name: Setup Bun - uses: oven-sh/setup-bun@v2 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: latest + bun-version: 1.3.11 - name: Install dependencies run: bun install --frozen-lockfile diff --git a/.github/workflows/pr-pipeline.yml b/.github/workflows/pr-pipeline.yml index 980241ec..e4f2b186 100644 --- a/.github/workflows/pr-pipeline.yml +++ b/.github/workflows/pr-pipeline.yml @@ -19,21 +19,15 @@ jobs: uses: ./.github/workflows/pr-build-validation.yml with: head_ref: ${{ github.head_ref }} - event_name: ${{ github.event_name }} - secrets: inherit frontend-tests: name: Frontend Tests uses: ./.github/workflows/pr-frontend-tests.yml with: head_ref: ${{ github.head_ref }} - event_name: ${{ github.event_name }} - secrets: inherit backend-tests: name: Backend Tests uses: ./.github/workflows/pr-backend-tests.yml with: head_ref: ${{ github.head_ref }} - event_name: ${{ github.event_name }} - secrets: inherit diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 00000000..6ec48051 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "nightly-2026-03-30" From a6073b2b16927b4169afef35b84b56dc010416ec Mon Sep 17 00:00:00 2001 From: Logan Nguyen Date: Tue, 31 Mar 2026 15:14:36 -0500 Subject: [PATCH 6/7] fix: resolve security vulnerabilities and harden audit gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add overrides.picomatch >=4.0.4 to force the patched version; tinyglobby (used by vite, vitest, typescript-eslint) was locked to the vulnerable picomatch range and bun update alone did not resolve it - brace-expansion is now resolved to 1.1.13 (minimatch@3 chain) and 5.0.5 (minimatch@10 chain) — both patched — after bun update pulled in compatible versions for each slot - Change audit step to fail on high/critical only; moderate dev-only transitive vulnerabilities that cannot be upgraded without breaking API changes (e.g. brace-expansion 1.x vs 5.x) should not permanently block CI, but high and critical always must Signed-off-by: Logan Nguyen Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/pr-build-validation.yml | 8 +++- bun.lock | 51 ++++++++++++----------- package.json | 23 +++++----- 3 files changed, 47 insertions(+), 35 deletions(-) diff --git a/.github/workflows/pr-build-validation.yml b/.github/workflows/pr-build-validation.yml index 817dfc9a..4ddc8546 100644 --- a/.github/workflows/pr-build-validation.yml +++ b/.github/workflows/pr-build-validation.yml @@ -28,7 +28,13 @@ jobs: run: bun install --frozen-lockfile - name: Security vulnerability scan - run: bun audit + run: | + AUDIT=$(bun audit 2>&1 || true) + echo "$AUDIT" + if echo "$AUDIT" | grep -qE '\b(critical|high)\b'; then + echo "::error::High or critical severity vulnerabilities found. Fix before merging." + exit 1 + fi - name: Build validation run: | diff --git a/bun.lock b/bun.lock index d079221d..024ef27b 100644 --- a/bun.lock +++ b/bun.lock @@ -5,27 +5,27 @@ "": { "name": "thuki", "dependencies": { - "@tauri-apps/api": "^2", + "@tauri-apps/api": "^2.10.1", "dompurify": "^3.3.3", "framer-motion": "^12.38.0", "marked": "^17.0.5", - "react": "^19.1.0", - "react-dom": "^19.1.0", + "react": "^19.2.4", + "react-dom": "^19.2.4", }, "devDependencies": { "@eslint/js": "^10.0.1", "@tailwindcss/vite": "^4.2.2", - "@tauri-apps/cli": "^2", + "@tauri-apps/cli": "^2.10.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@types/dompurify": "^3.2.0", "@types/marked": "^6.0.0", "@types/node": "^25.5.0", - "@types/react": "^19.1.8", - "@types/react-dom": "^19.1.6", - "@vitejs/plugin-react": "^4.6.0", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^4.7.0", "@vitest/coverage-v8": "^4.1.2", - "eslint": "^9", + "eslint": "^9.39.4", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^7.0.1", "globals": "^17.4.0", @@ -34,12 +34,15 @@ "prettier": "^3.8.1", "tailwindcss": "^4.2.2", "typescript": "~5.8.3", - "typescript-eslint": "^8.57.1", - "vite": "^7.0.4", + "typescript-eslint": "^8.58.0", + "vite": "^7.3.1", "vitest": "^4.1.2", }, }, }, + "overrides": { + "picomatch": ">=4.0.4", + }, "packages": { "@adobe/css-tools": ["@adobe/css-tools@4.4.4", "", {}, "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg=="], @@ -365,25 +368,25 @@ "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.57.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.57.1", "@typescript-eslint/type-utils": "8.57.1", "@typescript-eslint/utils": "8.57.1", "@typescript-eslint/visitor-keys": "8.57.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.57.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Gn3aqnvNl4NGc6x3/Bqk1AOn0thyTU9bqDRhiRnUWezgvr2OnhYCWCgC8zXXRVqBsIL1pSDt7T9nJUe0oM0kDQ=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.58.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/type-utils": "8.58.0", "@typescript-eslint/utils": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.58.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg=="], - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.57.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.57.1", "@typescript-eslint/types": "8.57.1", "@typescript-eslint/typescript-estree": "8.57.1", "@typescript-eslint/visitor-keys": "8.57.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-k4eNDan0EIMTT/dUKc/g+rsJ6wcHYhNPdY19VoX/EOtaAG8DLtKCykhrUnuHPYvinn5jhAPgD2Qw9hXBwrahsw=="], + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.58.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/types": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA=="], - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.57.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.57.1", "@typescript-eslint/types": "^8.57.1", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-vx1F37BRO1OftsYlmG9xay1TqnjNVlqALymwWVuYTdo18XuKxtBpCj1QlzNIEHlvlB27osvXFWptYiEWsVdYsg=="], + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.58.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.58.0", "@typescript-eslint/types": "^8.58.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg=="], - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.57.1", "", { "dependencies": { "@typescript-eslint/types": "8.57.1", "@typescript-eslint/visitor-keys": "8.57.1" } }, "sha512-hs/QcpCwlwT2L5S+3fT6gp0PabyGk4Q0Rv2doJXA0435/OpnSR3VRgvrp8Xdoc3UAYSg9cyUjTeFXZEPg/3OKg=="], + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0" } }, "sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ=="], - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.57.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-0lgOZB8cl19fHO4eI46YUx2EceQqhgkPSuCGLlGi79L2jwYY1cxeYc1Nae8Aw1xjgW3PKVDLlr3YJ6Bxx8HkWg=="], + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.58.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A=="], - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.57.1", "", { "dependencies": { "@typescript-eslint/types": "8.57.1", "@typescript-eslint/typescript-estree": "8.57.1", "@typescript-eslint/utils": "8.57.1", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-+Bwwm0ScukFdyoJsh2u6pp4S9ktegF98pYUU0hkphOOqdMB+1sNQhIz8y5E9+4pOioZijrkfNO/HUJVAFFfPKA=="], + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0", "@typescript-eslint/utils": "8.58.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-aGsCQImkDIqMyx1u4PrVlbi/krmDsQUs4zAcCV6M7yPcPev+RqVlndsJy9kJ8TLihW9TZ0kbDAzctpLn5o+lOg=="], - "@typescript-eslint/types": ["@typescript-eslint/types@8.57.1", "", {}, "sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ=="], + "@typescript-eslint/types": ["@typescript-eslint/types@8.58.0", "", {}, "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww=="], - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.57.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.57.1", "@typescript-eslint/tsconfig-utils": "8.57.1", "@typescript-eslint/types": "8.57.1", "@typescript-eslint/visitor-keys": "8.57.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ybe2hS9G6pXpqGtPli9Gx9quNV0TWLOmh58ADlmZe9DguLq0tiAKVjirSbtM1szG6+QH6rVXyU6GTLQbWnMY+g=="], + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.58.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.58.0", "@typescript-eslint/tsconfig-utils": "8.58.0", "@typescript-eslint/types": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA=="], - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.57.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.57.1", "@typescript-eslint/types": "8.57.1", "@typescript-eslint/typescript-estree": "8.57.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-XUNSJ/lEVFttPMMoDVA2r2bwrl8/oPx8cURtczkSEswY5T3AeLmCy+EKWQNdL4u0MmAHOjcWrqJp2cdvgjn8dQ=="], + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.58.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/types": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA=="], - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.57.1", "", { "dependencies": { "@typescript-eslint/types": "8.57.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-YWnmJkXbofiz9KbnbbwuA2rpGkFPLbAIetcCNO6mJ8gdhdZ/v7WDXsoGFAJuM6ikUFKTlSQnjWnVO4ux+UzS6A=="], + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ=="], "@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="], @@ -445,7 +448,7 @@ "bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="], - "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + "brace-expansion": ["brace-expansion@1.1.13", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w=="], "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], @@ -859,7 +862,7 @@ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], @@ -1011,7 +1014,7 @@ "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], - "typescript-eslint": ["typescript-eslint@8.57.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.57.1", "@typescript-eslint/parser": "8.57.1", "@typescript-eslint/typescript-estree": "8.57.1", "@typescript-eslint/utils": "8.57.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-fLvZWf+cAGw3tqMCYzGIU6yR8K+Y9NT2z23RwOjlNFF2HwSB3KhdEFI5lSBv8tNmFkkBShSjsCjzx1vahZfISA=="], + "typescript-eslint": ["typescript-eslint@8.58.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.58.0", "@typescript-eslint/parser": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0", "@typescript-eslint/utils": "8.58.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-e2TQzKfaI85fO+F3QywtX+tCTsu/D3WW5LVU6nz8hTFKFZ8yBJ6mSYRpXqdR3mFjPWmO0eWsTa5f+UpAOe/FMA=="], "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], @@ -1123,7 +1126,7 @@ "pretty-format/react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], - "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="], + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], } diff --git a/package.json b/package.json index c8dab214..e4388016 100644 --- a/package.json +++ b/package.json @@ -27,27 +27,30 @@ "validate-build": "bun run lint && bun run format:check && bun run typecheck && bun run build:all" }, "dependencies": { - "@tauri-apps/api": "^2", + "@tauri-apps/api": "^2.10.1", "dompurify": "^3.3.3", "framer-motion": "^12.38.0", "marked": "^17.0.5", - "react": "^19.1.0", - "react-dom": "^19.1.0" + "react": "^19.2.4", + "react-dom": "^19.2.4" + }, + "overrides": { + "picomatch": ">=4.0.4" }, "devDependencies": { "@eslint/js": "^10.0.1", "@tailwindcss/vite": "^4.2.2", - "@tauri-apps/cli": "^2", + "@tauri-apps/cli": "^2.10.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@types/dompurify": "^3.2.0", "@types/marked": "^6.0.0", "@types/node": "^25.5.0", - "@types/react": "^19.1.8", - "@types/react-dom": "^19.1.6", - "@vitejs/plugin-react": "^4.6.0", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^4.7.0", "@vitest/coverage-v8": "^4.1.2", - "eslint": "^9", + "eslint": "^9.39.4", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^7.0.1", "globals": "^17.4.0", @@ -56,8 +59,8 @@ "prettier": "^3.8.1", "tailwindcss": "^4.2.2", "typescript": "~5.8.3", - "typescript-eslint": "^8.57.1", - "vite": "^7.0.4", + "typescript-eslint": "^8.58.0", + "vite": "^7.3.1", "vitest": "^4.1.2" } } From a02dbda83b54ad936a457aa07554929ca3dc0737 Mon Sep 17 00:00:00 2001 From: Logan Nguyen Date: Tue, 31 Mar 2026 15:18:03 -0500 Subject: [PATCH 7/7] fix: scope nightly Rust to backend-tests only, remove root rust-toolchain.toml rust-toolchain.toml at the repo root overrode ALL cargo commands project-wide, causing the build validation job to pick up nightly instead of the runner's stable Rust. Clippy is not installed for the bare nightly toolchain, so lint:backend failed. Remove rust-toolchain.toml and instead install nightly-2026-03-30 explicitly in the backend-tests workflow with the llvm-tools component. Build validation now uses the pre-installed stable toolchain as intended. Signed-off-by: Logan Nguyen Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/pr-backend-tests.yml | 5 ++++- rust-toolchain.toml | 2 -- 2 files changed, 4 insertions(+), 3 deletions(-) delete mode 100644 rust-toolchain.toml diff --git a/.github/workflows/pr-backend-tests.yml b/.github/workflows/pr-backend-tests.yml index b8ed9412..9b3cb4dc 100644 --- a/.github/workflows/pr-backend-tests.yml +++ b/.github/workflows/pr-backend-tests.yml @@ -19,6 +19,9 @@ jobs: ref: ${{ inputs.head_ref || github.head_ref }} fetch-depth: 1 + - name: Install nightly Rust toolchain + run: rustup toolchain install nightly-2026-03-30 --component llvm-tools --no-self-update + - name: Install cargo-llvm-cov uses: taiki-e/install-action@899b013517f9e7774591216672bf75a46bb9a481 # v2.9.4 with: @@ -28,4 +31,4 @@ jobs: working-directory: src-tauri run: | echo "Running Cargo tests with 100% line coverage enforcement..." - cargo +nightly llvm-cov --ignore-filename-regex "(lib|main)\.rs" --fail-under-lines 100 + cargo +nightly-2026-03-30 llvm-cov --ignore-filename-regex "(lib|main)\.rs" --fail-under-lines 100 diff --git a/rust-toolchain.toml b/rust-toolchain.toml deleted file mode 100644 index 6ec48051..00000000 --- a/rust-toolchain.toml +++ /dev/null @@ -1,2 +0,0 @@ -[toolchain] -channel = "nightly-2026-03-30"