From 4a043c6904cd26b559afabb61680b5424700e4e9 Mon Sep 17 00:00:00 2001 From: qnbs <155236708+qnbs@users.noreply.github.com> Date: Wed, 9 Sep 2026 16:06:20 +0200 Subject: [PATCH 01/10] fix(desktop): align Tauri plugin Rust/npm versions, add version-parity CI guard v1.28.5's tag-triggered Tauri release build failed on every platform: tauri-plugin-http (Rust 2.6.0) and tauri-plugin-notification (Rust 2.4.0) had drifted ahead of their npm counterparts (@tauri-apps/plugin-http 2.5.9, @tauri-apps/plugin-notification 2.3.3) after #661 bumped only the Rust side. Tauri CLI hard-rejects a Rust/npm major.minor mismatch at build time. Tag v1.28.5 itself is untouched; its GitHub Release was correctly skipped since the bundle jobs never produced assets. - Bump @tauri-apps/plugin-http to ^2.6.0 and @tauri-apps/plugin-notification to ^2.4.0 (both published, verified against the npm registry), matching the already-resolved Rust crate versions. Lockfile reconciled. - Add scripts/check-tauri-plugin-versions.mjs: a cheap, deterministic check mirroring Tauri's own build-time version-parity rule for all 7 coupled plugin pairs, without needing the slow cross-platform build. Wired into the fast CI quality-gate job and pnpm run ci:prepush, so a one-sided future bump (Dependabot or manual) is caught before the next release tag instead of at tag-triggered release time. --- .github/workflows/ci.yml | 3 + CHANGELOG.md | 9 +++ README.md | 8 +-- package.json | 7 +- pnpm-lock.yaml | 21 ++++-- scripts/check-tauri-plugin-versions.d.mts | 4 ++ scripts/check-tauri-plugin-versions.mjs | 71 +++++++++++++++++++++ scripts/ci-prepush-lowend.mjs | 3 + tests/unit/checkTauriPluginVersions.test.ts | 58 +++++++++++++++++ 9 files changed, 170 insertions(+), 14 deletions(-) create mode 100644 scripts/check-tauri-plugin-versions.d.mts create mode 100644 scripts/check-tauri-plugin-versions.mjs create mode 100644 tests/unit/checkTauriPluginVersions.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2cd778df9..d34d28c3d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -337,6 +337,9 @@ jobs: - name: Native-readiness roadmap policy gate run: pnpm run native-readiness:check + - name: Tauri plugin Rust/npm version-parity gate + run: pnpm run tauri-plugins:check + - name: Feature parity audit run: pnpm run parity:check diff --git a/CHANGELOG.md b/CHANGELOG.md index 0114ce434..b350f0ef7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- **Tauri plugin Rust/npm version parity restored:** `tauri-plugin-http` and + `tauri-plugin-notification` had drifted ahead of their npm counterparts + (`@tauri-apps/plugin-http`, `@tauri-apps/plugin-notification`) after #661 bumped only the Rust + side, failing every platform's Tauri release build. Bumped the npm packages to match; added + `check-tauri-plugin-versions.mjs`, a cheap CI guard catching this class of mismatch before the + next release tag instead of at tag-triggered release time. + ## [1.28.5] — 2026-09-09 diff --git a/README.md b/README.md index 97ddc9775..1f8ba1062 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ IndexedDB v8 PWA v3.0 i18n 19 locales — 2942 keys - 7635+ tests / 603 files + 7641+ tests / 604 files Codecov Coverage License MIT CI Status @@ -512,7 +512,7 @@ The Settings → AI panel shows a live GPU status badge with adapter details and | **Document Export** | docx + jszip | Word-compatible `.docx` generation (lazy-loaded) | | **PWA** | Service Worker + Web App Manifest v3 | Offline support, installability, Workbox chunking | | **i18n** | Custom React Context (`I18nContext.tsx`) | 2942 keys × 19 locales (de/en/es/fr/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu/ru/ko Beta); EN fallback; `localStorage` persistence | -| **Testing** | Vitest 4.x (7635+ tests / 603 files) + Playwright E2E | Unit/integration + cross-browser E2E; Stryker mutation (manual workflow) | +| **Testing** | Vitest 4.x (7641+ tests / 604 files) + Playwright E2E | Unit/integration + cross-browser E2E; Stryker mutation (manual workflow) | | **Code Quality** | Biome (lint + format) + TypeScript 7 (tsgo) strict | `--error-on-warnings` in CI; zero `any` policy | | **Visualization** | Force-directed graph | Interactive character relationship network | | **Desktop** | Tauri v2 | Cross-platform installer; auto-updater via `latest.json` | @@ -550,7 +550,7 @@ WorldScript-Studio/ │ ├── sw.js # PWA Service Worker │ └── manifest.json # PWA Web App Manifest v3 ├── tests/ -│ ├── unit/ # Vitest unit tests (7635+ tests, 603 files) — count spans tests/, components/, packages/*/tests/, not just this folder +│ ├── unit/ # Vitest unit tests (7641+ tests, 604 files) — count spans tests/, components/, packages/*/tests/, not just this folder │ │ ├── ai/ # aiSmallModules, aiCoreFallbackPaths │ │ └── settings/ # WebLlmPanel, AiSections │ └── e2e/ # Playwright specs + helpers.ts @@ -715,7 +715,7 @@ The main pipeline is [`.github/workflows/ci.yml`](.github/workflows/ci.yml). Opt Raw bundle-budget ceilings (KB per uncompressed asset): entry **2500 KB**, vendor **6200 KB**, other JavaScript **2500 KB**, and WASM **30000 KB**. **Current test metrics (2026-09-07, source-synchronized; CI remains authoritative for pass/fail):** -- **7635+ unit tests** across **603 test files** — CI is authoritative for pass/fail +- **7641+ unit tests** across **604 test files** — CI is authoritative for pass/fail - Coverage thresholds: lines ≥ 80 · branches ≥ 66 · functions ≥ 72 · statements ≥ 78 — enforced in CI (see Codecov badge for live metrics) - i18n: **2942 keys × 19 locales** (en/de/fr/es/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu/ru/ko Beta) diff --git a/package.json b/package.json index b92f2caee..dcfbcb844 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "deps:verify": "node scripts/dependency-state.mjs verify", "deps:reconcile": "node scripts/dependency-state.mjs reconcile", "ci:prepush": "node scripts/ci-prepush-lowend.mjs", - "ci:local:full": "pnpm run lint && pnpm run typecheck && pnpm run i18n:check && pnpm run guardrail:desktop-imports && pnpm run native-readiness:check", + "ci:local:full": "pnpm run lint && pnpm run typecheck && pnpm run i18n:check && pnpm run guardrail:desktop-imports && pnpm run native-readiness:check && pnpm run tauri-plugins:check", "predev": "node scripts/sync-csp.mjs && node scripts/sync-sw-version.mjs && node scripts/sync-tauri-version.mjs && node scripts/sync-readme-metrics.mjs && node scripts/build-i18n.mjs && node scripts/copy-duckdb-assets.mjs", "dev": "vite", "dev:turbo": "turbo run dev --parallel", @@ -46,6 +46,7 @@ "csp:check": "node scripts/check-csp-policy.mjs", "csp:verify": "node scripts/sync-csp.mjs && git diff --exit-code -- index.html nginx.conf public/_headers vercel.json src-tauri/tauri.conf.json && node scripts/check-csp-policy.mjs", "native-readiness:check": "node scripts/check-native-readiness.mjs", + "tauri-plugins:check": "node scripts/check-tauri-plugin-versions.mjs", "workflow-policy:check": "node scripts/workflow-policy-check.mjs", "pr-size:check": "node scripts/check-pr-size.mjs", "suppressions:check": "node scripts/check-suppressions.mjs", @@ -142,8 +143,8 @@ "@tauri-apps/api": "^2.11.1", "@tauri-apps/plugin-dialog": "^2.7.1", "@tauri-apps/plugin-fs": "^2.5.1", - "@tauri-apps/plugin-http": "^2.5.9", - "@tauri-apps/plugin-notification": "^2.3.3", + "@tauri-apps/plugin-http": "^2.6.0", + "@tauri-apps/plugin-notification": "^2.4.0", "@tauri-apps/plugin-process": "^2.3.1", "@tauri-apps/plugin-shell": "^2.3.5", "@tauri-apps/plugin-updater": "^2.11.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 226c4f915..fbc8a0bb3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -125,11 +125,11 @@ importers: specifier: ^2.5.1 version: 2.5.1 '@tauri-apps/plugin-http': - specifier: ^2.5.9 - version: 2.5.9 + specifier: ^2.6.0 + version: 2.6.0 '@tauri-apps/plugin-notification': - specifier: ^2.3.3 - version: 2.3.3 + specifier: ^2.4.0 + version: 2.4.0 '@tauri-apps/plugin-process': specifier: ^2.3.1 version: 2.3.1 @@ -3133,12 +3133,15 @@ packages: '@tauri-apps/plugin-fs@2.5.1': resolution: {integrity: sha512-9Lz+Jopp6QyeEWhlpkMx4R/+P9HgR+AVAI4vOZhlT8Xaymtz8iVI/Ov984/XTqgJz/5gz5NretqPB/XEMS3NhQ==} - '@tauri-apps/plugin-http@2.5.9': - resolution: {integrity: sha512-lCiY0+vs4HvIUSvZrBs8TC3TiCB0MOPRmiUjTq4prW7SlcJE2jdLeT6KBsJrT9Tlplufl7W1pY6SFAO3gCWxDA==} + '@tauri-apps/plugin-http@2.6.0': + resolution: {integrity: sha512-QYXwbGb4hQ9/8Riv/ejU/kPFFnbBIrBcWwV1LIXv2xBKfoj8lkWfGkd9pkCSsBI/pljPtz+IPqfrE3t3bVl3mg==} '@tauri-apps/plugin-notification@2.3.3': resolution: {integrity: sha512-Zw+ZH18RJb41G4NrfHgIuofJiymusqN+q8fGUIIV7vyCH+5sSn5coqRv/MWB9qETsUs97vmU045q7OyseCV3Qg==} + '@tauri-apps/plugin-notification@2.4.0': + resolution: {integrity: sha512-xlJXMcUoKOjNupzDue5wrEsa1wytf+l/2gCAPhafHyP683Y3N7J/8clUWLZ3vpnwkpT2C1zcLMMQFjjecIG2xg==} + '@tauri-apps/plugin-process@2.3.1': resolution: {integrity: sha512-nCa4fGVaDL/B9ai03VyPOjfAHRHSBz5v6F/ObsB73r/dA3MHHhZtldaDMIc0V/pnUw9ehzr2iEG+XkSEyC0JJA==} @@ -10613,7 +10616,7 @@ snapshots: dependencies: '@tauri-apps/api': 2.11.1 - '@tauri-apps/plugin-http@2.5.9': + '@tauri-apps/plugin-http@2.6.0': dependencies: '@tauri-apps/api': 2.11.1 @@ -10621,6 +10624,10 @@ snapshots: dependencies: '@tauri-apps/api': 2.11.1 + '@tauri-apps/plugin-notification@2.4.0': + dependencies: + '@tauri-apps/api': 2.11.1 + '@tauri-apps/plugin-process@2.3.1': dependencies: '@tauri-apps/api': 2.11.1 diff --git a/scripts/check-tauri-plugin-versions.d.mts b/scripts/check-tauri-plugin-versions.d.mts new file mode 100644 index 000000000..a96b902a4 --- /dev/null +++ b/scripts/check-tauri-plugin-versions.d.mts @@ -0,0 +1,4 @@ +export function findTauriPluginVersionMismatches( + cargoLock: string, + pkg: { dependencies?: Record }, +): string[]; diff --git a/scripts/check-tauri-plugin-versions.mjs b/scripts/check-tauri-plugin-versions.mjs new file mode 100644 index 000000000..4d0ff178a --- /dev/null +++ b/scripts/check-tauri-plugin-versions.mjs @@ -0,0 +1,71 @@ +#!/usr/bin/env node +/** + * Verify each Tauri plugin's Rust crate (src-tauri/Cargo.lock) and coupled npm package + * (package.json) share the same major.minor line. + * Run via ci:prepush or manually: + * node scripts/check-tauri-plugin-versions.mjs + */ +// QNBS-v3: tauri build rejects this mismatch too, but only inside the slow, tag-triggered, cross-platform release workflow — mirrors that check cheaply for regular CI (found live: v1.28.5's Rust-only plugin bump via #661 broke every platform's release build). +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const root = path.join(__dirname, '..'); + +const cargoLockPath = path.join(root, 'src-tauri', 'Cargo.lock'); +const pkgPath = path.join(root, 'package.json'); + +// QNBS-v3: only Tauri plugins with a corresponding @tauri-apps/plugin-* npm package are coupled — tauri-plugin-log/window-state/deep-link/single-instance have no JS-side counterpart to drift against. +const PLUGIN_PAIRS = [ + ['tauri-plugin-dialog', '@tauri-apps/plugin-dialog'], + ['tauri-plugin-fs', '@tauri-apps/plugin-fs'], + ['tauri-plugin-http', '@tauri-apps/plugin-http'], + ['tauri-plugin-notification', '@tauri-apps/plugin-notification'], + ['tauri-plugin-process', '@tauri-apps/plugin-process'], + ['tauri-plugin-shell', '@tauri-apps/plugin-shell'], + ['tauri-plugin-updater', '@tauri-apps/plugin-updater'], +]; + +function resolvedCargoVersion(cargoLock, crateName) { + const match = cargoLock.match(new RegExp(`name = "${crateName}"\\nversion = "([^"]+)"`)); + return match ? match[1] : null; +} + +function majorMinor(version) { + const match = version.match(/^(\d+)\.(\d+)/); + return match ? `${match[1]}.${match[2]}` : null; +} + +export function findTauriPluginVersionMismatches(cargoLock, pkg) { + const findings = []; + for (const [crateName, npmName] of PLUGIN_PAIRS) { + const rustVersion = resolvedCargoVersion(cargoLock, crateName); + const npmRange = pkg.dependencies?.[npmName]; + if (!rustVersion || !npmRange) continue; + const npmVersion = npmRange.replace(/^[\^~]/, ''); + if (majorMinor(rustVersion) !== majorMinor(npmVersion)) { + findings.push( + `${crateName} (Rust ${rustVersion}) vs ${npmName} (npm ${npmVersion}) — major/minor mismatch, "pnpm exec tauri build" rejects this`, + ); + } + } + return findings; +} + +function main() { + const cargoLock = fs.readFileSync(cargoLockPath, 'utf8'); + const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); + const findings = findTauriPluginVersionMismatches(cargoLock, pkg); + if (findings.length > 0) { + process.stderr.write( + `[tauri-plugin-versions] MISMATCH — ${findings.length} finding(s):\n${findings.map((f) => ` - ${f}`).join('\n')}\n`, + ); + process.exit(1); + } + process.stdout.write( + `[tauri-plugin-versions] OK — ${PLUGIN_PAIRS.length} plugin pairs checked, all major.minor-aligned.\n`, + ); +} + +if (path.resolve(process.argv[1] ?? '') === fileURLToPath(import.meta.url)) main(); diff --git a/scripts/ci-prepush-lowend.mjs b/scripts/ci-prepush-lowend.mjs index a65fe344f..a26db12c9 100644 --- a/scripts/ci-prepush-lowend.mjs +++ b/scripts/ci-prepush-lowend.mjs @@ -84,6 +84,9 @@ async function main() { runNodeScript('scripts/check-tauri-import-boundary.mjs'), ); await runCheck('Native readiness', () => runNodeScript('scripts/check-native-readiness.mjs')); + await runCheck('Tauri plugin version parity', () => + runNodeScript('scripts/check-tauri-plugin-versions.mjs'), + ); if (shouldRunAdmissionCheck('i18n', classification.files) || full) { await runCheck('i18n key parity', () => runNodeScript('scripts/check-i18n-keys.mjs')); diff --git a/tests/unit/checkTauriPluginVersions.test.ts b/tests/unit/checkTauriPluginVersions.test.ts new file mode 100644 index 000000000..a4a47240c --- /dev/null +++ b/tests/unit/checkTauriPluginVersions.test.ts @@ -0,0 +1,58 @@ +// @vitest-environment node +import { describe, expect, it } from 'vitest'; +import { findTauriPluginVersionMismatches } from '../../scripts/check-tauri-plugin-versions.mjs'; + +function cargoLockEntry(crateName: string, version: string): string { + return `[[package]]\nname = "${crateName}"\nversion = "${version}"\nsource = "registry+https://github.com/rust-lang/crates.io-index"\n`; +} + +describe('findTauriPluginVersionMismatches', () => { + it('reports nothing when every plugin pair shares the same major.minor', () => { + const cargoLock = cargoLockEntry('tauri-plugin-http', '2.6.0'); + const pkg = { dependencies: { '@tauri-apps/plugin-http': '^2.6.3' } }; + expect(findTauriPluginVersionMismatches(cargoLock, pkg)).toEqual([]); + }); + + // QNBS-v3: reproduces the live v1.28.5 release-build failure — Rust bumped via a Dependabot PR without a matching npm bump. + it('flags a Rust-ahead-of-npm minor-version mismatch, reproducing the v1.28.5 release failure', () => { + const cargoLock = + cargoLockEntry('tauri-plugin-http', '2.6.0') + + cargoLockEntry('tauri-plugin-notification', '2.4.0'); + const pkg = { + dependencies: { + '@tauri-apps/plugin-http': '^2.5.9', + '@tauri-apps/plugin-notification': '^2.3.3', + }, + }; + const findings = findTauriPluginVersionMismatches(cargoLock, pkg); + expect(findings).toHaveLength(2); + expect(findings[0]).toContain('tauri-plugin-http'); + expect(findings[1]).toContain('tauri-plugin-notification'); + }); + + it('flags an npm-ahead-of-Rust minor-version mismatch too', () => { + const cargoLock = cargoLockEntry('tauri-plugin-updater', '2.9.0'); + const pkg = { dependencies: { '@tauri-apps/plugin-updater': '^2.11.0' } }; + const findings = findTauriPluginVersionMismatches(cargoLock, pkg); + expect(findings).toHaveLength(1); + expect(findings[0]).toContain('tauri-plugin-updater'); + }); + + it('ignores a plugin with no npm counterpart declared', () => { + const cargoLock = cargoLockEntry('tauri-plugin-log', '2.7.0'); + const pkg = { dependencies: {} }; + expect(findTauriPluginVersionMismatches(cargoLock, pkg)).toEqual([]); + }); + + it('ignores an npm package with no resolved Rust crate in Cargo.lock', () => { + const cargoLock = ''; + const pkg = { dependencies: { '@tauri-apps/plugin-http': '^2.6.0' } }; + expect(findTauriPluginVersionMismatches(cargoLock, pkg)).toEqual([]); + }); + + it('tolerates a patch-version difference within the same major.minor', () => { + const cargoLock = cargoLockEntry('tauri-plugin-dialog', '2.7.1'); + const pkg = { dependencies: { '@tauri-apps/plugin-dialog': '^2.7.0' } }; + expect(findTauriPluginVersionMismatches(cargoLock, pkg)).toEqual([]); + }); +}); From cfa032a16c3f4f43e61f808475028968f7b798df Mon Sep 17 00:00:00 2001 From: qnbs <155236708+qnbs@users.noreply.github.com> Date: Wed, 9 Sep 2026 16:39:39 +0200 Subject: [PATCH 02/10] fix(desktop): compare resolved lockfile versions across the whole workspace, fix closed-over checks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five independent reviewers converged on the same underlying design flaw in the version-parity guard added by the previous commit: - it only read package.json's declared range (e.g. "^2.4.0"), not the actually resolved version — a caret range's minimum can be well behind what pnpm actually installed, and complex range syntax (">=2.6.0 <3", "2.6.x", OR-ranges) silently failed to parse into a comparable major.minor, which made the mismatch check pass vacuously instead of failing; - it only checked the root package.json, missing that packages/desktop-contracts declares its own @tauri-apps/plugin-* set and had independently drifted (still on plugin-notification 2.3.3 while Rust and root npm were both on 2.4.0); - Cargo.lock parsing used a literal \n, silently stopping on a CRLF-line-ended file. Also fixed a second bug found while rewriting: crateToNpmName stripped the full "tauri-plugin-" prefix instead of just "tauri-", producing "@tauri-apps/http" instead of "@tauri-apps/plugin-http" — this made the "is this plugin declared" lookup fail for every pair, so the entire guard (both the original version and its first replacement) never actually matched anything and always reported OK regardless of real state, undetected because the local reproduction case happened not to depend on that lookup succeeding. - packages/desktop-contracts: bump @tauri-apps/plugin-notification to ^2.4.0 to match Rust; lockfile reconciled once. - check-tauri-plugin-versions.mjs rewritten to read the resolved version from pnpm-lock.yaml's importers block for every workspace package with a package.json (root + packages/*), not the declared range, and to fail closed (report a finding, not silently skip) when a declared plugin has no resolved Cargo.lock or pnpm-lock.yaml entry to compare. Cargo.lock parsing normalizes CRLF first. - findTauriPluginVersionMismatches now takes fully-loaded importer/pkg data as a parameter instead of reading package.json from disk itself, keeping it a pure, deterministically testable function; file I/O is isolated to main()'s own discovery step. - Regression tests added for: workspace-member drift independent of root, resolved-version-ahead-of-specifier (would have false-flagged under the old range-parsing logic), CRLF Cargo.lock, fail-closed on a missing Cargo.lock entry, fail-closed on a missing pnpm-lock.yaml entry, and a plugin genuinely not applicable to an importer. --- README.md | 8 +- packages/desktop-contracts/package.json | 2 +- pnpm-lock.yaml | 11 +- scripts/check-tauri-plugin-versions.d.mts | 5 +- scripts/check-tauri-plugin-versions.mjs | 141 +++++++++++++---- tests/unit/checkTauriPluginVersions.test.ts | 166 ++++++++++++++++---- 6 files changed, 263 insertions(+), 70 deletions(-) diff --git a/README.md b/README.md index 1f8ba1062..e5c00b43a 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ IndexedDB v8 PWA v3.0 i18n 19 locales — 2942 keys - 7641+ tests / 604 files + 7646+ tests / 604 files Codecov Coverage License MIT CI Status @@ -512,7 +512,7 @@ The Settings → AI panel shows a live GPU status badge with adapter details and | **Document Export** | docx + jszip | Word-compatible `.docx` generation (lazy-loaded) | | **PWA** | Service Worker + Web App Manifest v3 | Offline support, installability, Workbox chunking | | **i18n** | Custom React Context (`I18nContext.tsx`) | 2942 keys × 19 locales (de/en/es/fr/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu/ru/ko Beta); EN fallback; `localStorage` persistence | -| **Testing** | Vitest 4.x (7641+ tests / 604 files) + Playwright E2E | Unit/integration + cross-browser E2E; Stryker mutation (manual workflow) | +| **Testing** | Vitest 4.x (7646+ tests / 604 files) + Playwright E2E | Unit/integration + cross-browser E2E; Stryker mutation (manual workflow) | | **Code Quality** | Biome (lint + format) + TypeScript 7 (tsgo) strict | `--error-on-warnings` in CI; zero `any` policy | | **Visualization** | Force-directed graph | Interactive character relationship network | | **Desktop** | Tauri v2 | Cross-platform installer; auto-updater via `latest.json` | @@ -550,7 +550,7 @@ WorldScript-Studio/ │ ├── sw.js # PWA Service Worker │ └── manifest.json # PWA Web App Manifest v3 ├── tests/ -│ ├── unit/ # Vitest unit tests (7641+ tests, 604 files) — count spans tests/, components/, packages/*/tests/, not just this folder +│ ├── unit/ # Vitest unit tests (7646+ tests, 604 files) — count spans tests/, components/, packages/*/tests/, not just this folder │ │ ├── ai/ # aiSmallModules, aiCoreFallbackPaths │ │ └── settings/ # WebLlmPanel, AiSections │ └── e2e/ # Playwright specs + helpers.ts @@ -715,7 +715,7 @@ The main pipeline is [`.github/workflows/ci.yml`](.github/workflows/ci.yml). Opt Raw bundle-budget ceilings (KB per uncompressed asset): entry **2500 KB**, vendor **6200 KB**, other JavaScript **2500 KB**, and WASM **30000 KB**. **Current test metrics (2026-09-07, source-synchronized; CI remains authoritative for pass/fail):** -- **7641+ unit tests** across **604 test files** — CI is authoritative for pass/fail +- **7646+ unit tests** across **604 test files** — CI is authoritative for pass/fail - Coverage thresholds: lines ≥ 80 · branches ≥ 66 · functions ≥ 72 · statements ≥ 78 — enforced in CI (see Codecov badge for live metrics) - i18n: **2942 keys × 19 locales** (en/de/fr/es/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu/ru/ko Beta) diff --git a/packages/desktop-contracts/package.json b/packages/desktop-contracts/package.json index f670943fa..dc5db2439 100644 --- a/packages/desktop-contracts/package.json +++ b/packages/desktop-contracts/package.json @@ -12,7 +12,7 @@ "@tauri-apps/api": "^2.11.1", "@tauri-apps/plugin-dialog": "^2.7.1", "@tauri-apps/plugin-fs": "^2.5.1", - "@tauri-apps/plugin-notification": "^2.3.3", + "@tauri-apps/plugin-notification": "^2.4.0", "@tauri-apps/plugin-process": "^2.3.1", "@tauri-apps/plugin-shell": "^2.3.5", "@tauri-apps/plugin-updater": "^2.11.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fbc8a0bb3..db2d41f4c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -375,8 +375,8 @@ importers: specifier: ^2.5.1 version: 2.5.1 '@tauri-apps/plugin-notification': - specifier: ^2.3.3 - version: 2.3.3 + specifier: ^2.4.0 + version: 2.4.0 '@tauri-apps/plugin-process': specifier: ^2.3.1 version: 2.3.1 @@ -3136,9 +3136,6 @@ packages: '@tauri-apps/plugin-http@2.6.0': resolution: {integrity: sha512-QYXwbGb4hQ9/8Riv/ejU/kPFFnbBIrBcWwV1LIXv2xBKfoj8lkWfGkd9pkCSsBI/pljPtz+IPqfrE3t3bVl3mg==} - '@tauri-apps/plugin-notification@2.3.3': - resolution: {integrity: sha512-Zw+ZH18RJb41G4NrfHgIuofJiymusqN+q8fGUIIV7vyCH+5sSn5coqRv/MWB9qETsUs97vmU045q7OyseCV3Qg==} - '@tauri-apps/plugin-notification@2.4.0': resolution: {integrity: sha512-xlJXMcUoKOjNupzDue5wrEsa1wytf+l/2gCAPhafHyP683Y3N7J/8clUWLZ3vpnwkpT2C1zcLMMQFjjecIG2xg==} @@ -10620,10 +10617,6 @@ snapshots: dependencies: '@tauri-apps/api': 2.11.1 - '@tauri-apps/plugin-notification@2.3.3': - dependencies: - '@tauri-apps/api': 2.11.1 - '@tauri-apps/plugin-notification@2.4.0': dependencies: '@tauri-apps/api': 2.11.1 diff --git a/scripts/check-tauri-plugin-versions.d.mts b/scripts/check-tauri-plugin-versions.d.mts index a96b902a4..b9d58466a 100644 --- a/scripts/check-tauri-plugin-versions.d.mts +++ b/scripts/check-tauri-plugin-versions.d.mts @@ -1,4 +1,7 @@ +export function resolvedCargoPluginVersions(cargoLock: string): Map; +export function resolvedPnpmImporterVersions(pnpmLock: string): Map>; export function findTauriPluginVersionMismatches( cargoLock: string, - pkg: { dependencies?: Record }, + pnpmLock: string, + importerPackages: Array<{ importer: string; pkg: { dependencies?: Record } }>, ): string[]; diff --git a/scripts/check-tauri-plugin-versions.mjs b/scripts/check-tauri-plugin-versions.mjs index 4d0ff178a..bdfa950f6 100644 --- a/scripts/check-tauri-plugin-versions.mjs +++ b/scripts/check-tauri-plugin-versions.mjs @@ -1,7 +1,8 @@ #!/usr/bin/env node /** - * Verify each Tauri plugin's Rust crate (src-tauri/Cargo.lock) and coupled npm package - * (package.json) share the same major.minor line. + * Verify each Tauri plugin's Rust crate (src-tauri/Cargo.lock) and every workspace importer's + * coupled npm package (pnpm-lock.yaml resolved version, not the package.json declared range) + * share the same major.minor line. * Run via ci:prepush or manually: * node scripts/check-tauri-plugin-versions.mjs */ @@ -14,40 +15,123 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)); const root = path.join(__dirname, '..'); const cargoLockPath = path.join(root, 'src-tauri', 'Cargo.lock'); -const pkgPath = path.join(root, 'package.json'); +const pnpmLockPath = path.join(root, 'pnpm-lock.yaml'); +const rootPkgPath = path.join(root, 'package.json'); +const packagesDir = path.join(root, 'packages'); // QNBS-v3: only Tauri plugins with a corresponding @tauri-apps/plugin-* npm package are coupled — tauri-plugin-log/window-state/deep-link/single-instance have no JS-side counterpart to drift against. -const PLUGIN_PAIRS = [ - ['tauri-plugin-dialog', '@tauri-apps/plugin-dialog'], - ['tauri-plugin-fs', '@tauri-apps/plugin-fs'], - ['tauri-plugin-http', '@tauri-apps/plugin-http'], - ['tauri-plugin-notification', '@tauri-apps/plugin-notification'], - ['tauri-plugin-process', '@tauri-apps/plugin-process'], - ['tauri-plugin-shell', '@tauri-apps/plugin-shell'], - ['tauri-plugin-updater', '@tauri-apps/plugin-updater'], +const PLUGIN_CRATE_NAMES = [ + 'tauri-plugin-dialog', + 'tauri-plugin-fs', + 'tauri-plugin-http', + 'tauri-plugin-notification', + 'tauri-plugin-process', + 'tauri-plugin-shell', + 'tauri-plugin-updater', ]; +const crateToNpmName = (crateName) => `@tauri-apps/${crateName.replace(/^tauri-/, '')}`; -function resolvedCargoVersion(cargoLock, crateName) { - const match = cargoLock.match(new RegExp(`name = "${crateName}"\\nversion = "([^"]+)"`)); - return match ? match[1] : null; +function normalizeLineEndings(text) { + return text.replace(/\r\n/g, '\n'); } +// QNBS-v3: only the exact leading semver (2.6.0) is kept — a resolved lockfile version can carry a trailing "(patch_hash=...)" or peer-suffix annotation that must not corrupt the comparison. function majorMinor(version) { - const match = version.match(/^(\d+)\.(\d+)/); + const match = version.match(/^(\d+)\.(\d+)\.\d+/); return match ? `${match[1]}.${match[2]}` : null; } -export function findTauriPluginVersionMismatches(cargoLock, pkg) { +export function resolvedCargoPluginVersions(cargoLock) { + const normalized = normalizeLineEndings(cargoLock); + const versions = new Map(); + for (const crateName of PLUGIN_CRATE_NAMES) { + const match = normalized.match(new RegExp(`name = "${crateName}"\\nversion = "([^"]+)"`)); + if (match) versions.set(crateName, match[1]); + } + return versions; +} + +// QNBS-v3: line-based state-machine parse (importer header at 2-space indent, package name at 6-space, specifier/version at 8-space) — pnpm-lock.yaml's importers block is regular enough that this avoids adding a YAML-parsing dependency. +export function resolvedPnpmImporterVersions(pnpmLock) { + const normalized = normalizeLineEndings(pnpmLock); + const lines = normalized.split('\n'); + const byImporter = new Map(); + let currentImporter = null; + let currentPackage = null; + for (const line of lines) { + const importerMatch = line.match(/^ {2}(\S.*):$/); + if (importerMatch && !line.startsWith(' ')) { + currentImporter = importerMatch[1]; + currentPackage = null; + if (!byImporter.has(currentImporter)) byImporter.set(currentImporter, new Map()); + continue; + } + const packageMatch = line.match(/^ {6}'?(@[\w.-]+\/[\w.-]+|[\w.-]+)'?:$/); + if (packageMatch) { + currentPackage = packageMatch[1]; + continue; + } + const versionMatch = line.match(/^ {8}version: (.+)$/); + if (versionMatch && currentImporter && currentPackage) { + byImporter.get(currentImporter).set(currentPackage, versionMatch[1].trim()); + } + } + return byImporter; +} + +// QNBS-v3: reads package.json for every workspace member from disk — kept separate from the pure comparison function below so that one stays fully unit-testable without touching the filesystem. +function discoverWorkspaceImporterPackages() { + const importerPackages = [ + { importer: '.', pkg: JSON.parse(fs.readFileSync(rootPkgPath, 'utf8')) }, + ]; + if (!fs.existsSync(packagesDir)) return importerPackages; + for (const entry of fs.readdirSync(packagesDir, { withFileTypes: true })) { + if (!entry.isDirectory()) continue; + const pkgJsonPath = path.join(packagesDir, entry.name, 'package.json'); + if (!fs.existsSync(pkgJsonPath)) continue; + importerPackages.push({ + importer: `packages/${entry.name}`, + pkg: JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8')), + }); + } + return importerPackages; +} + +export function findTauriPluginVersionMismatches(cargoLock, pnpmLock, importerPackages) { const findings = []; - for (const [crateName, npmName] of PLUGIN_PAIRS) { - const rustVersion = resolvedCargoVersion(cargoLock, crateName); - const npmRange = pkg.dependencies?.[npmName]; - if (!rustVersion || !npmRange) continue; - const npmVersion = npmRange.replace(/^[\^~]/, ''); - if (majorMinor(rustVersion) !== majorMinor(npmVersion)) { - findings.push( - `${crateName} (Rust ${rustVersion}) vs ${npmName} (npm ${npmVersion}) — major/minor mismatch, "pnpm exec tauri build" rejects this`, - ); + const cargoVersions = resolvedCargoPluginVersions(cargoLock); + const pnpmVersions = resolvedPnpmImporterVersions(pnpmLock); + + for (const { importer, pkg } of importerPackages) { + const importerVersions = pnpmVersions.get(importer); + for (const crateName of PLUGIN_CRATE_NAMES) { + const npmName = crateToNpmName(crateName); + const isDeclared = Boolean(pkg.dependencies?.[npmName]); + if (!isDeclared) continue; // not applicable to this importer + + const rustVersion = cargoVersions.get(crateName); + if (!rustVersion) { + findings.push( + `${importer}: ${npmName} is declared but ${crateName} has no resolved version in Cargo.lock — fix Cargo.lock before this check can validate parity`, + ); + continue; + } + + const npmVersion = importerVersions?.get(npmName); + if (!npmVersion) { + findings.push( + `${importer}: ${npmName} is declared but has no resolved version in pnpm-lock.yaml for this importer — reconcile the lockfile before this check can validate parity`, + ); + continue; + } + + const rustMM = majorMinor(rustVersion); + const npmMM = majorMinor(npmVersion); + if (!rustMM || !npmMM || rustMM !== npmMM) { + findings.push( + `${importer}: ${crateName} (Rust ${rustVersion}) vs ${npmName} (npm ${npmVersion}, resolved) — major.minor mismatch, "pnpm exec tauri build" rejects this`, + ); + } } } return findings; @@ -55,8 +139,9 @@ export function findTauriPluginVersionMismatches(cargoLock, pkg) { function main() { const cargoLock = fs.readFileSync(cargoLockPath, 'utf8'); - const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); - const findings = findTauriPluginVersionMismatches(cargoLock, pkg); + const pnpmLock = fs.readFileSync(pnpmLockPath, 'utf8'); + const importerPackages = discoverWorkspaceImporterPackages(); + const findings = findTauriPluginVersionMismatches(cargoLock, pnpmLock, importerPackages); if (findings.length > 0) { process.stderr.write( `[tauri-plugin-versions] MISMATCH — ${findings.length} finding(s):\n${findings.map((f) => ` - ${f}`).join('\n')}\n`, @@ -64,7 +149,7 @@ function main() { process.exit(1); } process.stdout.write( - `[tauri-plugin-versions] OK — ${PLUGIN_PAIRS.length} plugin pairs checked, all major.minor-aligned.\n`, + `[tauri-plugin-versions] OK — ${importerPackages.length} workspace importer(s) checked against resolved lockfile versions, all major.minor-aligned.\n`, ); } diff --git a/tests/unit/checkTauriPluginVersions.test.ts b/tests/unit/checkTauriPluginVersions.test.ts index a4a47240c..fca87ee62 100644 --- a/tests/unit/checkTauriPluginVersions.test.ts +++ b/tests/unit/checkTauriPluginVersions.test.ts @@ -1,58 +1,170 @@ // @vitest-environment node import { describe, expect, it } from 'vitest'; -import { findTauriPluginVersionMismatches } from '../../scripts/check-tauri-plugin-versions.mjs'; +import { + findTauriPluginVersionMismatches, + resolvedCargoPluginVersions, + resolvedPnpmImporterVersions, +} from '../../scripts/check-tauri-plugin-versions.mjs'; function cargoLockEntry(crateName: string, version: string): string { return `[[package]]\nname = "${crateName}"\nversion = "${version}"\nsource = "registry+https://github.com/rust-lang/crates.io-index"\n`; } +function pnpmImporterBlock( + importer: string, + pkgs: Record, +): string { + const lines = [` ${importer}:`, ' dependencies:']; + for (const [name, { specifier, version }] of Object.entries(pkgs)) { + lines.push( + ` '${name}':`, + ` specifier: ${specifier}`, + ` version: ${version}`, + ); + } + return `${lines.join('\n')}\n`; +} + +function importerPkg(importer: string, dependencies: Record) { + return { importer, pkg: { dependencies } }; +} + +describe('resolvedCargoPluginVersions', () => { + it('resolves a plugin crate version from Cargo.lock', () => { + const versions = resolvedCargoPluginVersions(cargoLockEntry('tauri-plugin-http', '2.6.0')); + expect(versions.get('tauri-plugin-http')).toBe('2.6.0'); + }); + + // QNBS-v3: a Windows checkout without a pinned EOL for Cargo.lock can convert it to CRLF; the literal \n in the matcher must not silently stop matching. + it('resolves a plugin crate version from a CRLF-line-ended Cargo.lock', () => { + const crlf = cargoLockEntry('tauri-plugin-http', '2.6.0').replace(/\n/g, '\r\n'); + const versions = resolvedCargoPluginVersions(crlf); + expect(versions.get('tauri-plugin-http')).toBe('2.6.0'); + }); +}); + +describe('resolvedPnpmImporterVersions', () => { + it('resolves the actual locked version, not the declared specifier range', () => { + const lock = pnpmImporterBlock('.', { + '@tauri-apps/plugin-http': { specifier: '^2.6.0', version: '2.6.3' }, + }); + const byImporter = resolvedPnpmImporterVersions(lock); + expect(byImporter.get('.')?.get('@tauri-apps/plugin-http')).toBe('2.6.3'); + }); + + it('keeps each importer’s resolved versions independent', () => { + const lock = + pnpmImporterBlock('.', { + '@tauri-apps/plugin-notification': { specifier: '^2.4.0', version: '2.4.0' }, + }) + + pnpmImporterBlock('packages/desktop-contracts', { + '@tauri-apps/plugin-notification': { specifier: '^2.3.3', version: '2.3.3' }, + }); + const byImporter = resolvedPnpmImporterVersions(lock); + expect(byImporter.get('.')?.get('@tauri-apps/plugin-notification')).toBe('2.4.0'); + expect( + byImporter.get('packages/desktop-contracts')?.get('@tauri-apps/plugin-notification'), + ).toBe('2.3.3'); + }); +}); + describe('findTauriPluginVersionMismatches', () => { - it('reports nothing when every plugin pair shares the same major.minor', () => { + it('reports nothing when every declared, resolved pair is major.minor-aligned', () => { const cargoLock = cargoLockEntry('tauri-plugin-http', '2.6.0'); - const pkg = { dependencies: { '@tauri-apps/plugin-http': '^2.6.3' } }; - expect(findTauriPluginVersionMismatches(cargoLock, pkg)).toEqual([]); + const pnpmLock = pnpmImporterBlock('.', { + '@tauri-apps/plugin-http': { specifier: '^2.6.0', version: '2.6.3' }, + }); + const findings = findTauriPluginVersionMismatches(cargoLock, pnpmLock, [ + importerPkg('.', { '@tauri-apps/plugin-http': '^2.6.0' }), + ]); + expect(findings).toEqual([]); }); - // QNBS-v3: reproduces the live v1.28.5 release-build failure — Rust bumped via a Dependabot PR without a matching npm bump. + // QNBS-v3: reproduces the live v1.28.5 release-build failure — Rust bumped via a Dependabot PR without a matching npm bump, resolved lockfile versions included. it('flags a Rust-ahead-of-npm minor-version mismatch, reproducing the v1.28.5 release failure', () => { const cargoLock = cargoLockEntry('tauri-plugin-http', '2.6.0') + cargoLockEntry('tauri-plugin-notification', '2.4.0'); - const pkg = { - dependencies: { + const pnpmLock = pnpmImporterBlock('.', { + '@tauri-apps/plugin-http': { specifier: '^2.5.9', version: '2.5.9' }, + '@tauri-apps/plugin-notification': { specifier: '^2.3.3', version: '2.3.3' }, + }); + const findings = findTauriPluginVersionMismatches(cargoLock, pnpmLock, [ + importerPkg('.', { '@tauri-apps/plugin-http': '^2.5.9', '@tauri-apps/plugin-notification': '^2.3.3', - }, - }; - const findings = findTauriPluginVersionMismatches(cargoLock, pkg); + }), + ]); expect(findings).toHaveLength(2); expect(findings[0]).toContain('tauri-plugin-http'); expect(findings[1]).toContain('tauri-plugin-notification'); }); - it('flags an npm-ahead-of-Rust minor-version mismatch too', () => { - const cargoLock = cargoLockEntry('tauri-plugin-updater', '2.9.0'); - const pkg = { dependencies: { '@tauri-apps/plugin-updater': '^2.11.0' } }; - const findings = findTauriPluginVersionMismatches(cargoLock, pkg); + // QNBS-v3: reproduces the exact reviewer-found gap — root package.json/lockfile were fixed, but a workspace member's own manifest still resolved the stale minor. + it('flags a workspace-member importer drifting independently of the root', () => { + const cargoLock = cargoLockEntry('tauri-plugin-notification', '2.4.0'); + const pnpmLock = + pnpmImporterBlock('.', { + '@tauri-apps/plugin-notification': { specifier: '^2.4.0', version: '2.4.0' }, + }) + + pnpmImporterBlock('packages/desktop-contracts', { + '@tauri-apps/plugin-notification': { specifier: '^2.3.3', version: '2.3.3' }, + }); + const findings = findTauriPluginVersionMismatches(cargoLock, pnpmLock, [ + importerPkg('.', { '@tauri-apps/plugin-notification': '^2.4.0' }), + importerPkg('packages/desktop-contracts', { '@tauri-apps/plugin-notification': '^2.3.3' }), + ]); expect(findings).toHaveLength(1); - expect(findings[0]).toContain('tauri-plugin-updater'); + expect(findings[0]).toContain('packages/desktop-contracts'); + expect(findings[0]).toContain('@tauri-apps/plugin-notification'); }); - it('ignores a plugin with no npm counterpart declared', () => { - const cargoLock = cargoLockEntry('tauri-plugin-log', '2.7.0'); - const pkg = { dependencies: {} }; - expect(findTauriPluginVersionMismatches(cargoLock, pkg)).toEqual([]); + // QNBS-v3: a caret range's declared minimum can be well behind what pnpm actually resolved — only the resolved lockfile version is trustworthy. + it('does not flag a resolved version that has moved ahead of the declared specifier, when still aligned', () => { + const cargoLock = cargoLockEntry('tauri-plugin-updater', '2.11.0'); + const pnpmLock = pnpmImporterBlock('.', { + '@tauri-apps/plugin-updater': { specifier: '^2.9.0', version: '2.11.4' }, + }); + const findings = findTauriPluginVersionMismatches(cargoLock, pnpmLock, [ + importerPkg('.', { '@tauri-apps/plugin-updater': '^2.9.0' }), + ]); + expect(findings).toEqual([]); }); - it('ignores an npm package with no resolved Rust crate in Cargo.lock', () => { - const cargoLock = ''; - const pkg = { dependencies: { '@tauri-apps/plugin-http': '^2.6.0' } }; - expect(findTauriPluginVersionMismatches(cargoLock, pkg)).toEqual([]); + // QNBS-v3: fail closed — a plugin declared in package.json with no resolved Cargo.lock entry means the lockfile is out of sync and parity cannot be verified, which must surface as a finding, not a silent pass. + it('fails closed when a declared plugin has no resolved Rust crate version', () => { + const cargoLock = ''; // no tauri-plugin-http entry at all + const pnpmLock = pnpmImporterBlock('.', { + '@tauri-apps/plugin-http': { specifier: '^2.6.0', version: '2.6.0' }, + }); + const findings = findTauriPluginVersionMismatches(cargoLock, pnpmLock, [ + importerPkg('.', { '@tauri-apps/plugin-http': '^2.6.0' }), + ]); + expect(findings).toHaveLength(1); + expect(findings[0]).toContain('no resolved version in Cargo.lock'); + }); + + it('fails closed when a declared plugin has no resolved npm version in the lockfile', () => { + const cargoLock = cargoLockEntry('tauri-plugin-http', '2.6.0'); + const pnpmLock = pnpmImporterBlock('.', {}); // http declared in package.json but absent from the lockfile importer + const findings = findTauriPluginVersionMismatches(cargoLock, pnpmLock, [ + importerPkg('.', { '@tauri-apps/plugin-http': '^2.6.0' }), + ]); + expect(findings).toHaveLength(1); + expect(findings[0]).toContain('no resolved version in pnpm-lock.yaml'); }); - it('tolerates a patch-version difference within the same major.minor', () => { - const cargoLock = cargoLockEntry('tauri-plugin-dialog', '2.7.1'); - const pkg = { dependencies: { '@tauri-apps/plugin-dialog': '^2.7.0' } }; - expect(findTauriPluginVersionMismatches(cargoLock, pkg)).toEqual([]); + it('skips a plugin the importer does not declare at all (not applicable, not a failure)', () => { + // QNBS-v3: http has no Cargo.lock entry at all here, proving it was never even considered for an importer that doesn't declare it — only notification (declared, and given a matching Cargo entry) is checked. + const cargoLock = cargoLockEntry('tauri-plugin-notification', '2.4.0'); + const pnpmLock = pnpmImporterBlock('packages/desktop-contracts', { + '@tauri-apps/plugin-notification': { specifier: '^2.4.0', version: '2.4.0' }, + }); + const findings = findTauriPluginVersionMismatches(cargoLock, pnpmLock, [ + importerPkg('packages/desktop-contracts', { + '@tauri-apps/plugin-notification': '^2.4.0', + }), + ]); + expect(findings).toEqual([]); }); }); From ea6aa67aeb09c2e23e3e1b9af2a30220c9e199c3 Mon Sep 17 00:00:00 2001 From: qnbs <155236708+qnbs@users.noreply.github.com> Date: Wed, 9 Sep 2026 17:00:52 +0200 Subject: [PATCH 03/10] fix(desktop): reduce complexity/duplication flagged by CodeScene on the parity guard - findTauriPluginVersionMismatches: extracted the per-pair check into checkPluginPairParity so the outer function is a flat flatMap/filter instead of a nested loop with three early-continue branches (Complex Method, code health 8.55). - checkTauriPluginVersions.test.ts: consolidated the two structurally identical "fails closed" tests into one it.each (Code Duplication, code health 9.39). Behavior unchanged; 11/11 tests still pass. --- README.md | 8 +-- scripts/check-tauri-plugin-versions.mjs | 65 ++++++++++----------- tests/unit/checkTauriPluginVersions.test.ts | 48 ++++++++------- 3 files changed, 62 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index e5c00b43a..23a620752 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ IndexedDB v8 PWA v3.0 i18n 19 locales — 2942 keys - 7646+ tests / 604 files + 7644+ tests / 604 files Codecov Coverage License MIT CI Status @@ -512,7 +512,7 @@ The Settings → AI panel shows a live GPU status badge with adapter details and | **Document Export** | docx + jszip | Word-compatible `.docx` generation (lazy-loaded) | | **PWA** | Service Worker + Web App Manifest v3 | Offline support, installability, Workbox chunking | | **i18n** | Custom React Context (`I18nContext.tsx`) | 2942 keys × 19 locales (de/en/es/fr/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu/ru/ko Beta); EN fallback; `localStorage` persistence | -| **Testing** | Vitest 4.x (7646+ tests / 604 files) + Playwright E2E | Unit/integration + cross-browser E2E; Stryker mutation (manual workflow) | +| **Testing** | Vitest 4.x (7644+ tests / 604 files) + Playwright E2E | Unit/integration + cross-browser E2E; Stryker mutation (manual workflow) | | **Code Quality** | Biome (lint + format) + TypeScript 7 (tsgo) strict | `--error-on-warnings` in CI; zero `any` policy | | **Visualization** | Force-directed graph | Interactive character relationship network | | **Desktop** | Tauri v2 | Cross-platform installer; auto-updater via `latest.json` | @@ -550,7 +550,7 @@ WorldScript-Studio/ │ ├── sw.js # PWA Service Worker │ └── manifest.json # PWA Web App Manifest v3 ├── tests/ -│ ├── unit/ # Vitest unit tests (7646+ tests, 604 files) — count spans tests/, components/, packages/*/tests/, not just this folder +│ ├── unit/ # Vitest unit tests (7644+ tests, 604 files) — count spans tests/, components/, packages/*/tests/, not just this folder │ │ ├── ai/ # aiSmallModules, aiCoreFallbackPaths │ │ └── settings/ # WebLlmPanel, AiSections │ └── e2e/ # Playwright specs + helpers.ts @@ -715,7 +715,7 @@ The main pipeline is [`.github/workflows/ci.yml`](.github/workflows/ci.yml). Opt Raw bundle-budget ceilings (KB per uncompressed asset): entry **2500 KB**, vendor **6200 KB**, other JavaScript **2500 KB**, and WASM **30000 KB**. **Current test metrics (2026-09-07, source-synchronized; CI remains authoritative for pass/fail):** -- **7646+ unit tests** across **604 test files** — CI is authoritative for pass/fail +- **7644+ unit tests** across **604 test files** — CI is authoritative for pass/fail - Coverage thresholds: lines ≥ 80 · branches ≥ 66 · functions ≥ 72 · statements ≥ 78 — enforced in CI (see Codecov badge for live metrics) - i18n: **2942 keys × 19 locales** (en/de/fr/es/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu/ru/ko Beta) diff --git a/scripts/check-tauri-plugin-versions.mjs b/scripts/check-tauri-plugin-versions.mjs index bdfa950f6..a0b2bd32d 100644 --- a/scripts/check-tauri-plugin-versions.mjs +++ b/scripts/check-tauri-plugin-versions.mjs @@ -97,44 +97,43 @@ function discoverWorkspaceImporterPackages() { return importerPackages; } +// QNBS-v3 (CodeScene): extracted so findTauriPluginVersionMismatches stays a flat loop — returns a finding string for one declared pair, or null when it's aligned. +function checkPluginPairParity(importer, crateName, cargoVersions, importerVersions) { + const npmName = crateToNpmName(crateName); + const rustVersion = cargoVersions.get(crateName); + if (!rustVersion) { + return `${importer}: ${npmName} is declared but ${crateName} has no resolved version in Cargo.lock — fix Cargo.lock before this check can validate parity`; + } + const npmVersion = importerVersions?.get(npmName); + if (!npmVersion) { + return `${importer}: ${npmName} is declared but has no resolved version in pnpm-lock.yaml for this importer — reconcile the lockfile before this check can validate parity`; + } + const rustMM = majorMinor(rustVersion); + const npmMM = majorMinor(npmVersion); + if (!rustMM || !npmMM || rustMM !== npmMM) { + return `${importer}: ${crateName} (Rust ${rustVersion}) vs ${npmName} (npm ${npmVersion}, resolved) — major.minor mismatch, "pnpm exec tauri build" rejects this`; + } + return null; +} + +function declaredPluginCrateNames(pkg) { + return PLUGIN_CRATE_NAMES.filter((crateName) => + Boolean(pkg.dependencies?.[crateToNpmName(crateName)]), + ); +} + export function findTauriPluginVersionMismatches(cargoLock, pnpmLock, importerPackages) { - const findings = []; const cargoVersions = resolvedCargoPluginVersions(cargoLock); const pnpmVersions = resolvedPnpmImporterVersions(pnpmLock); - for (const { importer, pkg } of importerPackages) { + return importerPackages.flatMap(({ importer, pkg }) => { const importerVersions = pnpmVersions.get(importer); - for (const crateName of PLUGIN_CRATE_NAMES) { - const npmName = crateToNpmName(crateName); - const isDeclared = Boolean(pkg.dependencies?.[npmName]); - if (!isDeclared) continue; // not applicable to this importer - - const rustVersion = cargoVersions.get(crateName); - if (!rustVersion) { - findings.push( - `${importer}: ${npmName} is declared but ${crateName} has no resolved version in Cargo.lock — fix Cargo.lock before this check can validate parity`, - ); - continue; - } - - const npmVersion = importerVersions?.get(npmName); - if (!npmVersion) { - findings.push( - `${importer}: ${npmName} is declared but has no resolved version in pnpm-lock.yaml for this importer — reconcile the lockfile before this check can validate parity`, - ); - continue; - } - - const rustMM = majorMinor(rustVersion); - const npmMM = majorMinor(npmVersion); - if (!rustMM || !npmMM || rustMM !== npmMM) { - findings.push( - `${importer}: ${crateName} (Rust ${rustVersion}) vs ${npmName} (npm ${npmVersion}, resolved) — major.minor mismatch, "pnpm exec tauri build" rejects this`, - ); - } - } - } - return findings; + return declaredPluginCrateNames(pkg) + .map((crateName) => + checkPluginPairParity(importer, crateName, cargoVersions, importerVersions), + ) + .filter((finding) => finding !== null); + }); } function main() { diff --git a/tests/unit/checkTauriPluginVersions.test.ts b/tests/unit/checkTauriPluginVersions.test.ts index fca87ee62..1b395955c 100644 --- a/tests/unit/checkTauriPluginVersions.test.ts +++ b/tests/unit/checkTauriPluginVersions.test.ts @@ -131,28 +131,32 @@ describe('findTauriPluginVersionMismatches', () => { expect(findings).toEqual([]); }); - // QNBS-v3: fail closed — a plugin declared in package.json with no resolved Cargo.lock entry means the lockfile is out of sync and parity cannot be verified, which must surface as a finding, not a silent pass. - it('fails closed when a declared plugin has no resolved Rust crate version', () => { - const cargoLock = ''; // no tauri-plugin-http entry at all - const pnpmLock = pnpmImporterBlock('.', { - '@tauri-apps/plugin-http': { specifier: '^2.6.0', version: '2.6.0' }, - }); - const findings = findTauriPluginVersionMismatches(cargoLock, pnpmLock, [ - importerPkg('.', { '@tauri-apps/plugin-http': '^2.6.0' }), - ]); - expect(findings).toHaveLength(1); - expect(findings[0]).toContain('no resolved version in Cargo.lock'); - }); - - it('fails closed when a declared plugin has no resolved npm version in the lockfile', () => { - const cargoLock = cargoLockEntry('tauri-plugin-http', '2.6.0'); - const pnpmLock = pnpmImporterBlock('.', {}); // http declared in package.json but absent from the lockfile importer - const findings = findTauriPluginVersionMismatches(cargoLock, pnpmLock, [ - importerPkg('.', { '@tauri-apps/plugin-http': '^2.6.0' }), - ]); - expect(findings).toHaveLength(1); - expect(findings[0]).toContain('no resolved version in pnpm-lock.yaml'); - }); + // QNBS-v3: fail closed — a plugin declared in package.json with no resolved Cargo.lock or pnpm-lock.yaml entry means a lockfile is out of sync and parity cannot be verified, which must surface as a finding, not a silent pass. + it.each([ + { + name: 'no resolved Rust crate version', + cargoLock: '', // no tauri-plugin-http entry at all + pnpmLock: pnpmImporterBlock('.', { + '@tauri-apps/plugin-http': { specifier: '^2.6.0', version: '2.6.0' }, + }), + expectedSubstring: 'no resolved version in Cargo.lock', + }, + { + name: 'no resolved npm version in the lockfile', + cargoLock: cargoLockEntry('tauri-plugin-http', '2.6.0'), + pnpmLock: pnpmImporterBlock('.', {}), // http declared in package.json but absent from the lockfile importer + expectedSubstring: 'no resolved version in pnpm-lock.yaml', + }, + ])( + 'fails closed when a declared plugin has $name', + ({ cargoLock, pnpmLock, expectedSubstring }) => { + const findings = findTauriPluginVersionMismatches(cargoLock, pnpmLock, [ + importerPkg('.', { '@tauri-apps/plugin-http': '^2.6.0' }), + ]); + expect(findings).toHaveLength(1); + expect(findings[0]).toContain(expectedSubstring); + }, + ); it('skips a plugin the importer does not declare at all (not applicable, not a failure)', () => { // QNBS-v3: http has no Cargo.lock entry at all here, proving it was never even considered for an importer that doesn't declare it — only notification (declared, and given a matching Cargo entry) is checked. From 92b20452206e9ecc33122a92adb5be81297290eb Mon Sep 17 00:00:00 2001 From: qnbs <155236708+qnbs@users.noreply.github.com> Date: Wed, 9 Sep 2026 17:25:46 +0200 Subject: [PATCH 04/10] fix(desktop): resolve duplicate Cargo.lock crate versions via direct-dependency reference, finish CodeScene cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - resolvedCargoPluginVersions no longer picks whichever [[package]] entry for a crate name appears first in Cargo.lock. It now reads the worldscript-studio package's own dependencies list, which Cargo.lock itself qualifies as "name version" whenever more than one resolved version of that crate name exists — the authoritative signal for which one is the direct app dependency, not a transitive occurrence. A crate that still resolves to more than one version with no such disambiguating reference maps to null and fails closed (a finding, not a silent first-match guess) rather than requiring heavier tooling to resolve unambiguously. - checkPluginPairParity: replaced the compound "!rustMM || !npmMM || rustMM !== npmMM" condition with three sequential single-condition checks, each with its own specific message (unparseable Rust version / unparseable npm version / real mismatch) instead of one generic branch. - resolvedPnpmImporterVersions: removed a dead, redundant "&& !line.startsWith(' ')" guard — the 2-space-anchored importer regex already can't match a 4-plus-space-indented line, so the extra condition never changed the result. Extracted the three per-line regex checks into small named helpers. - Consolidated two more structurally-duplicate tests ("aligned pair" / "resolved version ahead of specifier") into an it.each table. - Added regression tests: a duplicate-crate Cargo.lock resolves to the direct dependency's version rather than the textually-first (transitive-shaped) one, both directly and through the full findTauriPluginVersionMismatches pipeline; and fails closed when a duplicate has no disambiguating reference at all. 15/15 tests pass. --- README.md | 8 +- scripts/check-tauri-plugin-versions.d.mts | 2 +- scripts/check-tauri-plugin-versions.mjs | 90 ++++++++++++++++----- tests/unit/checkTauriPluginVersions.test.ts | 77 +++++++++++++++--- 4 files changed, 142 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 23a620752..e5c00b43a 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ IndexedDB v8 PWA v3.0 i18n 19 locales — 2942 keys - 7644+ tests / 604 files + 7646+ tests / 604 files Codecov Coverage License MIT CI Status @@ -512,7 +512,7 @@ The Settings → AI panel shows a live GPU status badge with adapter details and | **Document Export** | docx + jszip | Word-compatible `.docx` generation (lazy-loaded) | | **PWA** | Service Worker + Web App Manifest v3 | Offline support, installability, Workbox chunking | | **i18n** | Custom React Context (`I18nContext.tsx`) | 2942 keys × 19 locales (de/en/es/fr/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu/ru/ko Beta); EN fallback; `localStorage` persistence | -| **Testing** | Vitest 4.x (7644+ tests / 604 files) + Playwright E2E | Unit/integration + cross-browser E2E; Stryker mutation (manual workflow) | +| **Testing** | Vitest 4.x (7646+ tests / 604 files) + Playwright E2E | Unit/integration + cross-browser E2E; Stryker mutation (manual workflow) | | **Code Quality** | Biome (lint + format) + TypeScript 7 (tsgo) strict | `--error-on-warnings` in CI; zero `any` policy | | **Visualization** | Force-directed graph | Interactive character relationship network | | **Desktop** | Tauri v2 | Cross-platform installer; auto-updater via `latest.json` | @@ -550,7 +550,7 @@ WorldScript-Studio/ │ ├── sw.js # PWA Service Worker │ └── manifest.json # PWA Web App Manifest v3 ├── tests/ -│ ├── unit/ # Vitest unit tests (7644+ tests, 604 files) — count spans tests/, components/, packages/*/tests/, not just this folder +│ ├── unit/ # Vitest unit tests (7646+ tests, 604 files) — count spans tests/, components/, packages/*/tests/, not just this folder │ │ ├── ai/ # aiSmallModules, aiCoreFallbackPaths │ │ └── settings/ # WebLlmPanel, AiSections │ └── e2e/ # Playwright specs + helpers.ts @@ -715,7 +715,7 @@ The main pipeline is [`.github/workflows/ci.yml`](.github/workflows/ci.yml). Opt Raw bundle-budget ceilings (KB per uncompressed asset): entry **2500 KB**, vendor **6200 KB**, other JavaScript **2500 KB**, and WASM **30000 KB**. **Current test metrics (2026-09-07, source-synchronized; CI remains authoritative for pass/fail):** -- **7644+ unit tests** across **604 test files** — CI is authoritative for pass/fail +- **7646+ unit tests** across **604 test files** — CI is authoritative for pass/fail - Coverage thresholds: lines ≥ 80 · branches ≥ 66 · functions ≥ 72 · statements ≥ 78 — enforced in CI (see Codecov badge for live metrics) - i18n: **2942 keys × 19 locales** (en/de/fr/es/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu/ru/ko Beta) diff --git a/scripts/check-tauri-plugin-versions.d.mts b/scripts/check-tauri-plugin-versions.d.mts index b9d58466a..f0de7ac02 100644 --- a/scripts/check-tauri-plugin-versions.d.mts +++ b/scripts/check-tauri-plugin-versions.d.mts @@ -1,4 +1,4 @@ -export function resolvedCargoPluginVersions(cargoLock: string): Map; +export function resolvedCargoPluginVersions(cargoLock: string): Map; export function resolvedPnpmImporterVersions(pnpmLock: string): Map>; export function findTauriPluginVersionMismatches( cargoLock: string, diff --git a/scripts/check-tauri-plugin-versions.mjs b/scripts/check-tauri-plugin-versions.mjs index a0b2bd32d..14bd41fa1 100644 --- a/scripts/check-tauri-plugin-versions.mjs +++ b/scripts/check-tauri-plugin-versions.mjs @@ -41,39 +41,84 @@ function majorMinor(version) { return match ? `${match[1]}.${match[2]}` : null; } +// QNBS-v3 (cubic): Cargo.lock qualifies a same-named dependency as "name version" inside its owning package's own dependencies list only when more than one resolved version of that crate exists — reading this authoritative signal avoids ever mistaking a transitive occurrence for the direct app dependency. +function directDependencyReferences(cargoLock) { + const ownPackageMatch = cargoLock.match( + /name = "worldscript-studio"\n(?:[^\n]*\n)*?dependencies = \[\n([^\]]*)\]/, + ); + const references = new Map(); + if (!ownPackageMatch) return references; + for (const line of ownPackageMatch[1].split('\n')) { + const depMatch = line.match(/"([^"]+)"/); + if (!depMatch) continue; + const [name, version] = depMatch[1].split(' '); + references.set(name, version ?? null); + } + return references; +} + +function allResolvedVersionsOf(cargoLock, crateName) { + const regex = new RegExp(`name = "${crateName}"\\nversion = "([^"]+)"`, 'g'); + const versions = []; + for (const match of cargoLock.matchAll(regex)) versions.push(match[1]); + return versions; +} + +// QNBS-v3: a crate absent from the returned map has no resolved version at all; a crate mapped to null was found more than once in Cargo.lock with no disambiguating reference — both are distinct fail-closed states, never guessed. export function resolvedCargoPluginVersions(cargoLock) { const normalized = normalizeLineEndings(cargoLock); - const versions = new Map(); + const references = directDependencyReferences(normalized); + const resolved = new Map(); for (const crateName of PLUGIN_CRATE_NAMES) { - const match = normalized.match(new RegExp(`name = "${crateName}"\\nversion = "([^"]+)"`)); - if (match) versions.set(crateName, match[1]); + const qualifiedVersion = references.get(crateName); + if (qualifiedVersion) { + resolved.set(crateName, qualifiedVersion); + continue; + } + const versions = allResolvedVersionsOf(normalized, crateName); + if (versions.length === 1) resolved.set(crateName, versions[0]); + else if (versions.length > 1) resolved.set(crateName, null); } - return versions; + return resolved; +} + +function importerHeaderName(line) { + const match = line.match(/^ {2}(\S.*):$/); + return match ? match[1] : null; +} + +function importerPackageName(line) { + const match = line.match(/^ {6}'?(@[\w.-]+\/[\w.-]+|[\w.-]+)'?:$/); + return match ? match[1] : null; +} + +function importerPackageVersion(line) { + const match = line.match(/^ {8}version: (.+)$/); + return match ? match[1].trim() : null; } // QNBS-v3: line-based state-machine parse (importer header at 2-space indent, package name at 6-space, specifier/version at 8-space) — pnpm-lock.yaml's importers block is regular enough that this avoids adding a YAML-parsing dependency. export function resolvedPnpmImporterVersions(pnpmLock) { - const normalized = normalizeLineEndings(pnpmLock); - const lines = normalized.split('\n'); + const lines = normalizeLineEndings(pnpmLock).split('\n'); const byImporter = new Map(); let currentImporter = null; let currentPackage = null; for (const line of lines) { - const importerMatch = line.match(/^ {2}(\S.*):$/); - if (importerMatch && !line.startsWith(' ')) { - currentImporter = importerMatch[1]; + const importer = importerHeaderName(line); + if (importer) { + currentImporter = importer; currentPackage = null; - if (!byImporter.has(currentImporter)) byImporter.set(currentImporter, new Map()); + if (!byImporter.has(importer)) byImporter.set(importer, new Map()); continue; } - const packageMatch = line.match(/^ {6}'?(@[\w.-]+\/[\w.-]+|[\w.-]+)'?:$/); - if (packageMatch) { - currentPackage = packageMatch[1]; + const packageName = importerPackageName(line); + if (packageName) { + currentPackage = packageName; continue; } - const versionMatch = line.match(/^ {8}version: (.+)$/); - if (versionMatch && currentImporter && currentPackage) { - byImporter.get(currentImporter).set(currentPackage, versionMatch[1].trim()); + const version = importerPackageVersion(line); + if (version && currentImporter && currentPackage) { + byImporter.get(currentImporter).set(currentPackage, version); } } return byImporter; @@ -100,17 +145,24 @@ function discoverWorkspaceImporterPackages() { // QNBS-v3 (CodeScene): extracted so findTauriPluginVersionMismatches stays a flat loop — returns a finding string for one declared pair, or null when it's aligned. function checkPluginPairParity(importer, crateName, cargoVersions, importerVersions) { const npmName = crateToNpmName(crateName); - const rustVersion = cargoVersions.get(crateName); - if (!rustVersion) { + if (!cargoVersions.has(crateName)) { return `${importer}: ${npmName} is declared but ${crateName} has no resolved version in Cargo.lock — fix Cargo.lock before this check can validate parity`; } + const rustVersion = cargoVersions.get(crateName); + if (rustVersion === null) { + return `${importer}: ${npmName} is declared but ${crateName} resolves to more than one version in Cargo.lock with no disambiguating direct-dependency reference — fix Cargo.lock before this check can validate parity`; + } const npmVersion = importerVersions?.get(npmName); if (!npmVersion) { return `${importer}: ${npmName} is declared but has no resolved version in pnpm-lock.yaml for this importer — reconcile the lockfile before this check can validate parity`; } const rustMM = majorMinor(rustVersion); + if (!rustMM) + return `${importer}: ${crateName}'s resolved Rust version "${rustVersion}" is not a parseable semver`; const npmMM = majorMinor(npmVersion); - if (!rustMM || !npmMM || rustMM !== npmMM) { + if (!npmMM) + return `${importer}: ${npmName}'s resolved npm version "${npmVersion}" is not a parseable semver`; + if (rustMM !== npmMM) { return `${importer}: ${crateName} (Rust ${rustVersion}) vs ${npmName} (npm ${npmVersion}, resolved) — major.minor mismatch, "pnpm exec tauri build" rejects this`; } return null; diff --git a/tests/unit/checkTauriPluginVersions.test.ts b/tests/unit/checkTauriPluginVersions.test.ts index 1b395955c..1a619ff1d 100644 --- a/tests/unit/checkTauriPluginVersions.test.ts +++ b/tests/unit/checkTauriPluginVersions.test.ts @@ -10,6 +10,12 @@ function cargoLockEntry(crateName: string, version: string): string { return `[[package]]\nname = "${crateName}"\nversion = "${version}"\nsource = "registry+https://github.com/rust-lang/crates.io-index"\n`; } +// QNBS-v3: mirrors how Cargo.lock disambiguates a same-named dependency as "name version" inside the owning package's own dependencies list only when more than one resolved version exists. +function ownPackageBlock(dependencyRefs: string[]): string { + const refLines = dependencyRefs.map((ref) => ` "${ref}",`).join('\n'); + return `[[package]]\nname = "worldscript-studio"\nversion = "1.28.5"\ndependencies = [\n${refLines}\n]\n`; +} + function pnpmImporterBlock( importer: string, pkgs: Record, @@ -41,6 +47,23 @@ describe('resolvedCargoPluginVersions', () => { const versions = resolvedCargoPluginVersions(crlf); expect(versions.get('tauri-plugin-http')).toBe('2.6.0'); }); + + // QNBS-v3 (cubic): reproduces the reviewer-found gap — a first-match lookup over duplicate package entries could silently pick a transitive occurrence instead of the direct app dependency Cargo.lock itself disambiguates. + it('resolves the direct-dependency version, not the textually-first one, when Cargo.lock has duplicate entries', () => { + const cargoLock = + ownPackageBlock(['tauri-plugin-http 2.6.0']) + + cargoLockEntry('tauri-plugin-http', '2.5.0') + // transitive, textually first + cargoLockEntry('tauri-plugin-http', '2.6.0'); // direct, referenced by name + const versions = resolvedCargoPluginVersions(cargoLock); + expect(versions.get('tauri-plugin-http')).toBe('2.6.0'); + }); + + it('fails closed (null) when a crate resolves to more than one version with no disambiguating reference', () => { + const cargoLock = + cargoLockEntry('tauri-plugin-http', '2.5.0') + cargoLockEntry('tauri-plugin-http', '2.6.0'); + const versions = resolvedCargoPluginVersions(cargoLock); + expect(versions.get('tauri-plugin-http')).toBeNull(); + }); }); describe('resolvedPnpmImporterVersions', () => { @@ -69,13 +92,29 @@ describe('resolvedPnpmImporterVersions', () => { }); describe('findTauriPluginVersionMismatches', () => { - it('reports nothing when every declared, resolved pair is major.minor-aligned', () => { - const cargoLock = cargoLockEntry('tauri-plugin-http', '2.6.0'); - const pnpmLock = pnpmImporterBlock('.', { - '@tauri-apps/plugin-http': { specifier: '^2.6.0', version: '2.6.3' }, - }); + // QNBS-v3: consolidates two previously-separate "no finding" cases (a straightforwardly aligned pair, and a resolved version that has moved past its declared specifier) into one table. + it.each([ + { + name: 'an aligned pair', + crate: 'tauri-plugin-http', + rust: '2.6.0', + npm: '@tauri-apps/plugin-http', + specifier: '^2.6.0', + resolved: '2.6.3', + }, + { + name: 'a resolved version ahead of its specifier', + crate: 'tauri-plugin-updater', + rust: '2.11.0', + npm: '@tauri-apps/plugin-updater', + specifier: '^2.9.0', + resolved: '2.11.4', + }, + ])('reports nothing for $name', ({ crate, rust, npm, specifier, resolved }) => { + const cargoLock = cargoLockEntry(crate, rust); + const pnpmLock = pnpmImporterBlock('.', { [npm]: { specifier, version: resolved } }); const findings = findTauriPluginVersionMismatches(cargoLock, pnpmLock, [ - importerPkg('.', { '@tauri-apps/plugin-http': '^2.6.0' }), + importerPkg('.', { [npm]: specifier }), ]); expect(findings).toEqual([]); }); @@ -119,18 +158,34 @@ describe('findTauriPluginVersionMismatches', () => { expect(findings[0]).toContain('@tauri-apps/plugin-notification'); }); - // QNBS-v3: a caret range's declared minimum can be well behind what pnpm actually resolved — only the resolved lockfile version is trustworthy. - it('does not flag a resolved version that has moved ahead of the declared specifier, when still aligned', () => { - const cargoLock = cargoLockEntry('tauri-plugin-updater', '2.11.0'); + // QNBS-v3 (cubic): a duplicate Cargo.lock entry must resolve to the direct app dependency's version (2.6.0), never the textually-first transitive occurrence (2.5.0) — proven by the fact that using the wrong one would falsely flag this as a mismatch. + it('does not mistake a transitive crate version for the direct app dependency', () => { + const cargoLock = + ownPackageBlock(['tauri-plugin-http 2.6.0']) + + cargoLockEntry('tauri-plugin-http', '2.5.0') + + cargoLockEntry('tauri-plugin-http', '2.6.0'); const pnpmLock = pnpmImporterBlock('.', { - '@tauri-apps/plugin-updater': { specifier: '^2.9.0', version: '2.11.4' }, + '@tauri-apps/plugin-http': { specifier: '^2.6.0', version: '2.6.3' }, }); const findings = findTauriPluginVersionMismatches(cargoLock, pnpmLock, [ - importerPkg('.', { '@tauri-apps/plugin-updater': '^2.9.0' }), + importerPkg('.', { '@tauri-apps/plugin-http': '^2.6.0' }), ]); expect(findings).toEqual([]); }); + it('fails closed when Cargo.lock has an undisambiguated duplicate, rather than guessing', () => { + const cargoLock = + cargoLockEntry('tauri-plugin-http', '2.5.0') + cargoLockEntry('tauri-plugin-http', '2.6.0'); + const pnpmLock = pnpmImporterBlock('.', { + '@tauri-apps/plugin-http': { specifier: '^2.6.0', version: '2.6.3' }, + }); + const findings = findTauriPluginVersionMismatches(cargoLock, pnpmLock, [ + importerPkg('.', { '@tauri-apps/plugin-http': '^2.6.0' }), + ]); + expect(findings).toHaveLength(1); + expect(findings[0]).toContain('more than one version'); + }); + // QNBS-v3: fail closed — a plugin declared in package.json with no resolved Cargo.lock or pnpm-lock.yaml entry means a lockfile is out of sync and parity cannot be verified, which must surface as a finding, not a silent pass. it.each([ { From 1a435abdf7ead1f0ad469099635c583f7534fd79 Mon Sep 17 00:00:00 2001 From: qnbs <155236708+qnbs@users.noreply.github.com> Date: Wed, 9 Sep 2026 17:49:46 +0200 Subject: [PATCH 05/10] fix(desktop): validate qualified Cargo.lock references, cover all real plugin/npm pairs, finish CodeScene cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - resolvedCargoPluginVersions now verifies a qualified direct-dependency reference against the actual resolved [[package]] entries before trusting it; an inconsistent lockfile (reference points to a version with no matching entry) fails closed instead of being accepted at face value. - PLUGIN_CRATE_NAMES now includes tauri-plugin-log, -window-state, and -deep-link: all three have real @tauri-apps/plugin-* npm packages (verified against the npm registry), so the previous exclusion comment was factually wrong for 3 of its 4 claimed exclusions. Only tauri-plugin-single-instance genuinely has no npm counterpart (confirmed 404). None of the three added crates are currently declared in this repo's package.json, so this is a no-op today and only starts mattering if one is added later — exactly the guard's purpose. - resolvedPnpmImporterVersions: replaced the remaining compound condition ("version && currentImporter && currentPackage") with three sequential single-condition early-continues (CodeScene Complex Conditional, code health 9.69). - Added a regression test for the qualified-reference-without-matching- entry case. 16/16 tests pass; the real checker still reports OK against this repo's actual lockfiles (the three newly-covered crates aren't declared in package.json yet, so they're correctly skipped as not applicable, not silently mismatched). --- README.md | 8 ++++---- scripts/check-tauri-plugin-versions.mjs | 18 ++++++++++++------ tests/unit/checkTauriPluginVersions.test.ts | 10 ++++++++++ 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index e5c00b43a..fc5f726c5 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ IndexedDB v8 PWA v3.0 i18n 19 locales — 2942 keys - 7646+ tests / 604 files + 7647+ tests / 604 files Codecov Coverage License MIT CI Status @@ -512,7 +512,7 @@ The Settings → AI panel shows a live GPU status badge with adapter details and | **Document Export** | docx + jszip | Word-compatible `.docx` generation (lazy-loaded) | | **PWA** | Service Worker + Web App Manifest v3 | Offline support, installability, Workbox chunking | | **i18n** | Custom React Context (`I18nContext.tsx`) | 2942 keys × 19 locales (de/en/es/fr/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu/ru/ko Beta); EN fallback; `localStorage` persistence | -| **Testing** | Vitest 4.x (7646+ tests / 604 files) + Playwright E2E | Unit/integration + cross-browser E2E; Stryker mutation (manual workflow) | +| **Testing** | Vitest 4.x (7647+ tests / 604 files) + Playwright E2E | Unit/integration + cross-browser E2E; Stryker mutation (manual workflow) | | **Code Quality** | Biome (lint + format) + TypeScript 7 (tsgo) strict | `--error-on-warnings` in CI; zero `any` policy | | **Visualization** | Force-directed graph | Interactive character relationship network | | **Desktop** | Tauri v2 | Cross-platform installer; auto-updater via `latest.json` | @@ -550,7 +550,7 @@ WorldScript-Studio/ │ ├── sw.js # PWA Service Worker │ └── manifest.json # PWA Web App Manifest v3 ├── tests/ -│ ├── unit/ # Vitest unit tests (7646+ tests, 604 files) — count spans tests/, components/, packages/*/tests/, not just this folder +│ ├── unit/ # Vitest unit tests (7647+ tests, 604 files) — count spans tests/, components/, packages/*/tests/, not just this folder │ │ ├── ai/ # aiSmallModules, aiCoreFallbackPaths │ │ └── settings/ # WebLlmPanel, AiSections │ └── e2e/ # Playwright specs + helpers.ts @@ -715,7 +715,7 @@ The main pipeline is [`.github/workflows/ci.yml`](.github/workflows/ci.yml). Opt Raw bundle-budget ceilings (KB per uncompressed asset): entry **2500 KB**, vendor **6200 KB**, other JavaScript **2500 KB**, and WASM **30000 KB**. **Current test metrics (2026-09-07, source-synchronized; CI remains authoritative for pass/fail):** -- **7646+ unit tests** across **604 test files** — CI is authoritative for pass/fail +- **7647+ unit tests** across **604 test files** — CI is authoritative for pass/fail - Coverage thresholds: lines ≥ 80 · branches ≥ 66 · functions ≥ 72 · statements ≥ 78 — enforced in CI (see Codecov badge for live metrics) - i18n: **2942 keys × 19 locales** (en/de/fr/es/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu/ru/ko Beta) diff --git a/scripts/check-tauri-plugin-versions.mjs b/scripts/check-tauri-plugin-versions.mjs index 14bd41fa1..8fabe4490 100644 --- a/scripts/check-tauri-plugin-versions.mjs +++ b/scripts/check-tauri-plugin-versions.mjs @@ -19,15 +19,18 @@ const pnpmLockPath = path.join(root, 'pnpm-lock.yaml'); const rootPkgPath = path.join(root, 'package.json'); const packagesDir = path.join(root, 'packages'); -// QNBS-v3: only Tauri plugins with a corresponding @tauri-apps/plugin-* npm package are coupled — tauri-plugin-log/window-state/deep-link/single-instance have no JS-side counterpart to drift against. +// QNBS-v3 (codex): tauri-plugin-log, -window-state, and -deep-link all have real @tauri-apps/plugin-* npm packages too (verified against the npm registry) even though this repo doesn't declare them in package.json yet — listed here so the guard covers them immediately if that ever changes. Only tauri-plugin-single-instance genuinely has no npm counterpart (confirmed 404). const PLUGIN_CRATE_NAMES = [ + 'tauri-plugin-deep-link', 'tauri-plugin-dialog', 'tauri-plugin-fs', 'tauri-plugin-http', + 'tauri-plugin-log', 'tauri-plugin-notification', 'tauri-plugin-process', 'tauri-plugin-shell', 'tauri-plugin-updater', + 'tauri-plugin-window-state', ]; const crateToNpmName = (crateName) => `@tauri-apps/${crateName.replace(/^tauri-/, '')}`; @@ -70,12 +73,14 @@ export function resolvedCargoPluginVersions(cargoLock) { const references = directDependencyReferences(normalized); const resolved = new Map(); for (const crateName of PLUGIN_CRATE_NAMES) { + const versions = allResolvedVersionsOf(normalized, crateName); const qualifiedVersion = references.get(crateName); if (qualifiedVersion) { - resolved.set(crateName, qualifiedVersion); + // QNBS-v3 (cubic): a qualified reference not matched by any actual [[package]] entry means the lockfile itself is inconsistent — trust only a reference that a real resolved entry confirms. + if (versions.includes(qualifiedVersion)) resolved.set(crateName, qualifiedVersion); + else if (versions.length > 0) resolved.set(crateName, null); continue; } - const versions = allResolvedVersionsOf(normalized, crateName); if (versions.length === 1) resolved.set(crateName, versions[0]); else if (versions.length > 1) resolved.set(crateName, null); } @@ -117,9 +122,10 @@ export function resolvedPnpmImporterVersions(pnpmLock) { continue; } const version = importerPackageVersion(line); - if (version && currentImporter && currentPackage) { - byImporter.get(currentImporter).set(currentPackage, version); - } + if (!version) continue; + if (!currentImporter) continue; + if (!currentPackage) continue; + byImporter.get(currentImporter).set(currentPackage, version); } return byImporter; } diff --git a/tests/unit/checkTauriPluginVersions.test.ts b/tests/unit/checkTauriPluginVersions.test.ts index 1a619ff1d..2aba81002 100644 --- a/tests/unit/checkTauriPluginVersions.test.ts +++ b/tests/unit/checkTauriPluginVersions.test.ts @@ -64,6 +64,16 @@ describe('resolvedCargoPluginVersions', () => { const versions = resolvedCargoPluginVersions(cargoLock); expect(versions.get('tauri-plugin-http')).toBeNull(); }); + + // QNBS-v3 (cubic): a qualified direct-dependency reference must be checked against real [[package]] entries — an inconsistent lockfile referencing a version with no matching entry must not be trusted at face value. + it('fails closed when the direct-dependency reference points to a version with no matching package entry', () => { + const cargoLock = + ownPackageBlock(['tauri-plugin-http 2.7.0']) + // references a version that doesn't exist below + cargoLockEntry('tauri-plugin-http', '2.5.0') + + cargoLockEntry('tauri-plugin-http', '2.6.0'); + const versions = resolvedCargoPluginVersions(cargoLock); + expect(versions.get('tauri-plugin-http')).toBeNull(); + }); }); describe('resolvedPnpmImporterVersions', () => { From ba0855a444420a878cf9f4e0650ea7e1c35788f1 Mon Sep 17 00:00:00 2001 From: qnbs <155236708+qnbs@users.noreply.github.com> Date: Wed, 9 Sep 2026 18:10:47 +0200 Subject: [PATCH 06/10] fix(desktop): require direct-dependency membership, check every dependency section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - resolvedCargoPluginVersions now requires a crate to appear in worldscript-studio's own direct-dependencies list at all (qualified or not) before treating any resolved version as valid. A crate with only one lockfile entry is not necessarily a direct dependency — it can be pulled in solely as a transitive of another plugin (concretely possible here: tauri-plugin-deep-link via tauri-plugin-single-instance's "deep-link" feature). Absent from the direct-dependencies list, it now falls through the existing "no resolved version" fail-closed path instead of being silently compared as if it were the app's own dependency. - declaredPluginCrateNames now checks optionalDependencies, peerDependencies, and devDependencies in addition to dependencies — a plugin declared under any of those still resolves into the lockfile and can still ship, so checking only "dependencies" could silently skip it. No current package.json uses any of these sections for a Tauri plugin, so this is a no-op today. - Added regression tests for both, at both the resolvedCargoPluginVersions level and the full findTauriPluginVersionMismatches pipeline. 19/19 tests pass; the real checker still reports OK against this repo's actual lockfiles. --- README.md | 8 +-- scripts/check-tauri-plugin-versions.mjs | 16 ++++- tests/unit/checkTauriPluginVersions.test.ts | 75 ++++++++++++++++++--- 3 files changed, 85 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index fc5f726c5..55255588a 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ IndexedDB v8 PWA v3.0 i18n 19 locales — 2942 keys - 7647+ tests / 604 files + 7650+ tests / 604 files Codecov Coverage License MIT CI Status @@ -512,7 +512,7 @@ The Settings → AI panel shows a live GPU status badge with adapter details and | **Document Export** | docx + jszip | Word-compatible `.docx` generation (lazy-loaded) | | **PWA** | Service Worker + Web App Manifest v3 | Offline support, installability, Workbox chunking | | **i18n** | Custom React Context (`I18nContext.tsx`) | 2942 keys × 19 locales (de/en/es/fr/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu/ru/ko Beta); EN fallback; `localStorage` persistence | -| **Testing** | Vitest 4.x (7647+ tests / 604 files) + Playwright E2E | Unit/integration + cross-browser E2E; Stryker mutation (manual workflow) | +| **Testing** | Vitest 4.x (7650+ tests / 604 files) + Playwright E2E | Unit/integration + cross-browser E2E; Stryker mutation (manual workflow) | | **Code Quality** | Biome (lint + format) + TypeScript 7 (tsgo) strict | `--error-on-warnings` in CI; zero `any` policy | | **Visualization** | Force-directed graph | Interactive character relationship network | | **Desktop** | Tauri v2 | Cross-platform installer; auto-updater via `latest.json` | @@ -550,7 +550,7 @@ WorldScript-Studio/ │ ├── sw.js # PWA Service Worker │ └── manifest.json # PWA Web App Manifest v3 ├── tests/ -│ ├── unit/ # Vitest unit tests (7647+ tests, 604 files) — count spans tests/, components/, packages/*/tests/, not just this folder +│ ├── unit/ # Vitest unit tests (7650+ tests, 604 files) — count spans tests/, components/, packages/*/tests/, not just this folder │ │ ├── ai/ # aiSmallModules, aiCoreFallbackPaths │ │ └── settings/ # WebLlmPanel, AiSections │ └── e2e/ # Playwright specs + helpers.ts @@ -715,7 +715,7 @@ The main pipeline is [`.github/workflows/ci.yml`](.github/workflows/ci.yml). Opt Raw bundle-budget ceilings (KB per uncompressed asset): entry **2500 KB**, vendor **6200 KB**, other JavaScript **2500 KB**, and WASM **30000 KB**. **Current test metrics (2026-09-07, source-synchronized; CI remains authoritative for pass/fail):** -- **7647+ unit tests** across **604 test files** — CI is authoritative for pass/fail +- **7650+ unit tests** across **604 test files** — CI is authoritative for pass/fail - Coverage thresholds: lines ≥ 80 · branches ≥ 66 · functions ≥ 72 · statements ≥ 78 — enforced in CI (see Codecov badge for live metrics) - i18n: **2942 keys × 19 locales** (en/de/fr/es/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu/ru/ko Beta) diff --git a/scripts/check-tauri-plugin-versions.mjs b/scripts/check-tauri-plugin-versions.mjs index 8fabe4490..b64941a96 100644 --- a/scripts/check-tauri-plugin-versions.mjs +++ b/scripts/check-tauri-plugin-versions.mjs @@ -73,6 +73,8 @@ export function resolvedCargoPluginVersions(cargoLock) { const references = directDependencyReferences(normalized); const resolved = new Map(); for (const crateName of PLUGIN_CRATE_NAMES) { + // QNBS-v3 (codex): a crate absent from worldscript-studio's own dependencies list is at most a transitive occurrence, even with only one lockfile entry — leaving it unset here routes it through the existing "no resolved version" fail-closed path instead of comparing an unrelated transitive version. + if (!references.has(crateName)) continue; const versions = allResolvedVersionsOf(normalized, crateName); const qualifiedVersion = references.get(crateName); if (qualifiedVersion) { @@ -174,9 +176,21 @@ function checkPluginPairParity(importer, crateName, cargoVersions, importerVersi return null; } +// QNBS-v3 (codex): a plugin declared under optionalDependencies/peerDependencies/devDependencies still resolves into the lockfile and can still be bundled — checking only "dependencies" silently skipped it. +const DEPENDENCY_SECTIONS = [ + 'dependencies', + 'optionalDependencies', + 'peerDependencies', + 'devDependencies', +]; + +function isDeclaredInAnySection(pkg, npmName) { + return DEPENDENCY_SECTIONS.some((section) => Boolean(pkg[section]?.[npmName])); +} + function declaredPluginCrateNames(pkg) { return PLUGIN_CRATE_NAMES.filter((crateName) => - Boolean(pkg.dependencies?.[crateToNpmName(crateName)]), + isDeclaredInAnySection(pkg, crateToNpmName(crateName)), ); } diff --git a/tests/unit/checkTauriPluginVersions.test.ts b/tests/unit/checkTauriPluginVersions.test.ts index 2aba81002..8858f20d5 100644 --- a/tests/unit/checkTauriPluginVersions.test.ts +++ b/tests/unit/checkTauriPluginVersions.test.ts @@ -35,16 +35,27 @@ function importerPkg(importer: string, dependencies: Record) { return { importer, pkg: { dependencies } }; } +function importerPkgWithSection( + importer: string, + section: 'optionalDependencies' | 'peerDependencies' | 'devDependencies', + deps: Record, +) { + return { importer, pkg: { [section]: deps } }; +} + describe('resolvedCargoPluginVersions', () => { it('resolves a plugin crate version from Cargo.lock', () => { - const versions = resolvedCargoPluginVersions(cargoLockEntry('tauri-plugin-http', '2.6.0')); + const cargoLock = + ownPackageBlock(['tauri-plugin-http']) + cargoLockEntry('tauri-plugin-http', '2.6.0'); + const versions = resolvedCargoPluginVersions(cargoLock); expect(versions.get('tauri-plugin-http')).toBe('2.6.0'); }); // QNBS-v3: a Windows checkout without a pinned EOL for Cargo.lock can convert it to CRLF; the literal \n in the matcher must not silently stop matching. it('resolves a plugin crate version from a CRLF-line-ended Cargo.lock', () => { - const crlf = cargoLockEntry('tauri-plugin-http', '2.6.0').replace(/\n/g, '\r\n'); - const versions = resolvedCargoPluginVersions(crlf); + const cargoLock = + ownPackageBlock(['tauri-plugin-http']) + cargoLockEntry('tauri-plugin-http', '2.6.0'); + const versions = resolvedCargoPluginVersions(cargoLock.replace(/\n/g, '\r\n')); expect(versions.get('tauri-plugin-http')).toBe('2.6.0'); }); @@ -60,7 +71,9 @@ describe('resolvedCargoPluginVersions', () => { it('fails closed (null) when a crate resolves to more than one version with no disambiguating reference', () => { const cargoLock = - cargoLockEntry('tauri-plugin-http', '2.5.0') + cargoLockEntry('tauri-plugin-http', '2.6.0'); + ownPackageBlock(['tauri-plugin-http']) + + cargoLockEntry('tauri-plugin-http', '2.5.0') + + cargoLockEntry('tauri-plugin-http', '2.6.0'); const versions = resolvedCargoPluginVersions(cargoLock); expect(versions.get('tauri-plugin-http')).toBeNull(); }); @@ -74,6 +87,15 @@ describe('resolvedCargoPluginVersions', () => { const versions = resolvedCargoPluginVersions(cargoLock); expect(versions.get('tauri-plugin-http')).toBeNull(); }); + + // QNBS-v3 (codex): a crate with only one lockfile entry is not necessarily a direct app dependency — it can be pulled in solely as a transitive of another plugin (e.g. tauri-plugin-deep-link via single-instance's "deep-link" feature). Absent from the direct-dependencies list, it must not be treated as resolved at all. + it('does not resolve a crate that has only one lockfile entry but is absent from the direct-dependencies list', () => { + const cargoLock = + ownPackageBlock(['tauri-plugin-dialog']) + // http is NOT listed as a direct dependency here + cargoLockEntry('tauri-plugin-http', '2.6.0'); // present only as a transitive occurrence + const versions = resolvedCargoPluginVersions(cargoLock); + expect(versions.has('tauri-plugin-http')).toBe(false); + }); }); describe('resolvedPnpmImporterVersions', () => { @@ -121,7 +143,7 @@ describe('findTauriPluginVersionMismatches', () => { resolved: '2.11.4', }, ])('reports nothing for $name', ({ crate, rust, npm, specifier, resolved }) => { - const cargoLock = cargoLockEntry(crate, rust); + const cargoLock = ownPackageBlock([crate]) + cargoLockEntry(crate, rust); const pnpmLock = pnpmImporterBlock('.', { [npm]: { specifier, version: resolved } }); const findings = findTauriPluginVersionMismatches(cargoLock, pnpmLock, [ importerPkg('.', { [npm]: specifier }), @@ -151,7 +173,9 @@ describe('findTauriPluginVersionMismatches', () => { // QNBS-v3: reproduces the exact reviewer-found gap — root package.json/lockfile were fixed, but a workspace member's own manifest still resolved the stale minor. it('flags a workspace-member importer drifting independently of the root', () => { - const cargoLock = cargoLockEntry('tauri-plugin-notification', '2.4.0'); + const cargoLock = + ownPackageBlock(['tauri-plugin-notification']) + + cargoLockEntry('tauri-plugin-notification', '2.4.0'); const pnpmLock = pnpmImporterBlock('.', { '@tauri-apps/plugin-notification': { specifier: '^2.4.0', version: '2.4.0' }, @@ -168,6 +192,34 @@ describe('findTauriPluginVersionMismatches', () => { expect(findings[0]).toContain('@tauri-apps/plugin-notification'); }); + // QNBS-v3 (codex): a crate resolved only transitively (absent from the direct-dependencies list) must fail closed through the full pipeline too, not just at the resolvedCargoPluginVersions level. + it('fails closed end-to-end when the declared npm plugin has no direct Rust dependency, only a transitive one', () => { + const cargoLock = + ownPackageBlock(['tauri-plugin-dialog']) + cargoLockEntry('tauri-plugin-http', '2.6.0'); + const pnpmLock = pnpmImporterBlock('.', { + '@tauri-apps/plugin-http': { specifier: '^2.6.0', version: '2.6.0' }, + }); + const findings = findTauriPluginVersionMismatches(cargoLock, pnpmLock, [ + importerPkg('.', { '@tauri-apps/plugin-http': '^2.6.0' }), + ]); + expect(findings).toHaveLength(1); + expect(findings[0]).toContain('no resolved version in Cargo.lock'); + }); + + // QNBS-v3 (codex): a plugin declared under optionalDependencies/peerDependencies/devDependencies still resolves into the lockfile and can still be bundled — checking only "dependencies" silently skipped it. + it('checks a plugin declared under a non-"dependencies" section too', () => { + const cargoLock = + ownPackageBlock(['tauri-plugin-http']) + cargoLockEntry('tauri-plugin-http', '2.5.0'); + const pnpmLock = pnpmImporterBlock('.', { + '@tauri-apps/plugin-http': { specifier: '^2.6.0', version: '2.6.0' }, + }); + const findings = findTauriPluginVersionMismatches(cargoLock, pnpmLock, [ + importerPkgWithSection('.', 'optionalDependencies', { '@tauri-apps/plugin-http': '^2.6.0' }), + ]); + expect(findings).toHaveLength(1); + expect(findings[0]).toContain('major.minor mismatch'); + }); + // QNBS-v3 (cubic): a duplicate Cargo.lock entry must resolve to the direct app dependency's version (2.6.0), never the textually-first transitive occurrence (2.5.0) — proven by the fact that using the wrong one would falsely flag this as a mismatch. it('does not mistake a transitive crate version for the direct app dependency', () => { const cargoLock = @@ -185,7 +237,9 @@ describe('findTauriPluginVersionMismatches', () => { it('fails closed when Cargo.lock has an undisambiguated duplicate, rather than guessing', () => { const cargoLock = - cargoLockEntry('tauri-plugin-http', '2.5.0') + cargoLockEntry('tauri-plugin-http', '2.6.0'); + ownPackageBlock(['tauri-plugin-http']) + + cargoLockEntry('tauri-plugin-http', '2.5.0') + + cargoLockEntry('tauri-plugin-http', '2.6.0'); const pnpmLock = pnpmImporterBlock('.', { '@tauri-apps/plugin-http': { specifier: '^2.6.0', version: '2.6.3' }, }); @@ -208,7 +262,8 @@ describe('findTauriPluginVersionMismatches', () => { }, { name: 'no resolved npm version in the lockfile', - cargoLock: cargoLockEntry('tauri-plugin-http', '2.6.0'), + cargoLock: + ownPackageBlock(['tauri-plugin-http']) + cargoLockEntry('tauri-plugin-http', '2.6.0'), pnpmLock: pnpmImporterBlock('.', {}), // http declared in package.json but absent from the lockfile importer expectedSubstring: 'no resolved version in pnpm-lock.yaml', }, @@ -225,7 +280,9 @@ describe('findTauriPluginVersionMismatches', () => { it('skips a plugin the importer does not declare at all (not applicable, not a failure)', () => { // QNBS-v3: http has no Cargo.lock entry at all here, proving it was never even considered for an importer that doesn't declare it — only notification (declared, and given a matching Cargo entry) is checked. - const cargoLock = cargoLockEntry('tauri-plugin-notification', '2.4.0'); + const cargoLock = + ownPackageBlock(['tauri-plugin-notification']) + + cargoLockEntry('tauri-plugin-notification', '2.4.0'); const pnpmLock = pnpmImporterBlock('packages/desktop-contracts', { '@tauri-apps/plugin-notification': { specifier: '^2.4.0', version: '2.4.0' }, }); From 85357785c63637166794caaa5c31056c8851ad74 Mon Sep 17 00:00:00 2001 From: qnbs <155236708+qnbs@users.noreply.github.com> Date: Wed, 9 Sep 2026 18:18:19 +0200 Subject: [PATCH 07/10] fix(desktop): flatten resolvedCargoPluginVersions, consolidate remaining test duplication - Extracted resolveCrateVersion so resolvedCargoPluginVersions is a flat loop of early returns instead of nested if/else-if branches (CodeScene Bumpy Road Ahead, flagged as a critical gate). - Consolidated the three duplicate-Cargo.lock-entry tests (qualified match / no reference / qualified-but-nonexistent) into one it.each table (CodeScene Code Duplication). 19/19 tests pass; the real checker still reports OK against this repo's actual lockfiles. --- README.md | 8 ++--- scripts/check-tauri-plugin-versions.mjs | 30 +++++++++------- tests/unit/checkTauriPluginVersions.test.ts | 39 +++++++++------------ 3 files changed, 38 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 55255588a..fc5f726c5 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ IndexedDB v8 PWA v3.0 i18n 19 locales — 2942 keys - 7650+ tests / 604 files + 7647+ tests / 604 files Codecov Coverage License MIT CI Status @@ -512,7 +512,7 @@ The Settings → AI panel shows a live GPU status badge with adapter details and | **Document Export** | docx + jszip | Word-compatible `.docx` generation (lazy-loaded) | | **PWA** | Service Worker + Web App Manifest v3 | Offline support, installability, Workbox chunking | | **i18n** | Custom React Context (`I18nContext.tsx`) | 2942 keys × 19 locales (de/en/es/fr/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu/ru/ko Beta); EN fallback; `localStorage` persistence | -| **Testing** | Vitest 4.x (7650+ tests / 604 files) + Playwright E2E | Unit/integration + cross-browser E2E; Stryker mutation (manual workflow) | +| **Testing** | Vitest 4.x (7647+ tests / 604 files) + Playwright E2E | Unit/integration + cross-browser E2E; Stryker mutation (manual workflow) | | **Code Quality** | Biome (lint + format) + TypeScript 7 (tsgo) strict | `--error-on-warnings` in CI; zero `any` policy | | **Visualization** | Force-directed graph | Interactive character relationship network | | **Desktop** | Tauri v2 | Cross-platform installer; auto-updater via `latest.json` | @@ -550,7 +550,7 @@ WorldScript-Studio/ │ ├── sw.js # PWA Service Worker │ └── manifest.json # PWA Web App Manifest v3 ├── tests/ -│ ├── unit/ # Vitest unit tests (7650+ tests, 604 files) — count spans tests/, components/, packages/*/tests/, not just this folder +│ ├── unit/ # Vitest unit tests (7647+ tests, 604 files) — count spans tests/, components/, packages/*/tests/, not just this folder │ │ ├── ai/ # aiSmallModules, aiCoreFallbackPaths │ │ └── settings/ # WebLlmPanel, AiSections │ └── e2e/ # Playwright specs + helpers.ts @@ -715,7 +715,7 @@ The main pipeline is [`.github/workflows/ci.yml`](.github/workflows/ci.yml). Opt Raw bundle-budget ceilings (KB per uncompressed asset): entry **2500 KB**, vendor **6200 KB**, other JavaScript **2500 KB**, and WASM **30000 KB**. **Current test metrics (2026-09-07, source-synchronized; CI remains authoritative for pass/fail):** -- **7650+ unit tests** across **604 test files** — CI is authoritative for pass/fail +- **7647+ unit tests** across **604 test files** — CI is authoritative for pass/fail - Coverage thresholds: lines ≥ 80 · branches ≥ 66 · functions ≥ 72 · statements ≥ 78 — enforced in CI (see Codecov badge for live metrics) - i18n: **2942 keys × 19 locales** (en/de/fr/es/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu/ru/ko Beta) diff --git a/scripts/check-tauri-plugin-versions.mjs b/scripts/check-tauri-plugin-versions.mjs index b64941a96..e24b80e38 100644 --- a/scripts/check-tauri-plugin-versions.mjs +++ b/scripts/check-tauri-plugin-versions.mjs @@ -67,24 +67,30 @@ function allResolvedVersionsOf(cargoLock, crateName) { return versions; } +// QNBS-v3 (CodeScene): extracted so resolvedCargoPluginVersions stays a flat loop of early returns instead of nested branches — undefined means "not a direct dependency at all", null means "ambiguous/inconsistent", a string is the confirmed resolved version. +function resolveCrateVersion(crateName, references, normalized) { + // QNBS-v3 (codex): a crate absent from worldscript-studio's own dependencies list is at most a transitive occurrence, even with only one lockfile entry — routing it through the existing "no resolved version" fail-closed path instead of comparing an unrelated transitive version. + if (!references.has(crateName)) return undefined; + const versions = allResolvedVersionsOf(normalized, crateName); + const qualifiedVersion = references.get(crateName); + if (!qualifiedVersion) { + if (versions.length === 1) return versions[0]; + if (versions.length > 1) return null; + return undefined; + } + // QNBS-v3 (cubic): a qualified reference not matched by any actual [[package]] entry means the lockfile itself is inconsistent — trust only a reference that a real resolved entry confirms. + if (versions.includes(qualifiedVersion)) return qualifiedVersion; + return versions.length > 0 ? null : undefined; +} + // QNBS-v3: a crate absent from the returned map has no resolved version at all; a crate mapped to null was found more than once in Cargo.lock with no disambiguating reference — both are distinct fail-closed states, never guessed. export function resolvedCargoPluginVersions(cargoLock) { const normalized = normalizeLineEndings(cargoLock); const references = directDependencyReferences(normalized); const resolved = new Map(); for (const crateName of PLUGIN_CRATE_NAMES) { - // QNBS-v3 (codex): a crate absent from worldscript-studio's own dependencies list is at most a transitive occurrence, even with only one lockfile entry — leaving it unset here routes it through the existing "no resolved version" fail-closed path instead of comparing an unrelated transitive version. - if (!references.has(crateName)) continue; - const versions = allResolvedVersionsOf(normalized, crateName); - const qualifiedVersion = references.get(crateName); - if (qualifiedVersion) { - // QNBS-v3 (cubic): a qualified reference not matched by any actual [[package]] entry means the lockfile itself is inconsistent — trust only a reference that a real resolved entry confirms. - if (versions.includes(qualifiedVersion)) resolved.set(crateName, qualifiedVersion); - else if (versions.length > 0) resolved.set(crateName, null); - continue; - } - if (versions.length === 1) resolved.set(crateName, versions[0]); - else if (versions.length > 1) resolved.set(crateName, null); + const version = resolveCrateVersion(crateName, references, normalized); + if (version !== undefined) resolved.set(crateName, version); } return resolved; } diff --git a/tests/unit/checkTauriPluginVersions.test.ts b/tests/unit/checkTauriPluginVersions.test.ts index 8858f20d5..e41460106 100644 --- a/tests/unit/checkTauriPluginVersions.test.ts +++ b/tests/unit/checkTauriPluginVersions.test.ts @@ -59,33 +59,26 @@ describe('resolvedCargoPluginVersions', () => { expect(versions.get('tauri-plugin-http')).toBe('2.6.0'); }); - // QNBS-v3 (cubic): reproduces the reviewer-found gap — a first-match lookup over duplicate package entries could silently pick a transitive occurrence instead of the direct app dependency Cargo.lock itself disambiguates. - it('resolves the direct-dependency version, not the textually-first one, when Cargo.lock has duplicate entries', () => { + // QNBS-v3 (cubic): three ways the same two-entry duplicate Cargo.lock can be resolved, table-driven — a matching qualified reference disambiguates it, no reference at all is genuinely ambiguous, and a reference pointing nowhere real is an inconsistent lockfile. All three must fail closed except the first. + it.each([ + { + name: 'a qualified reference matching an existing entry', + ref: 'tauri-plugin-http 2.6.0', + expected: '2.6.0', + }, + { name: 'no disambiguating reference at all', ref: 'tauri-plugin-http', expected: null }, + { + name: 'a qualified reference matching no entry', + ref: 'tauri-plugin-http 2.7.0', + expected: null, + }, + ])('resolves a duplicate-entry Cargo.lock correctly for $name', ({ ref, expected }) => { const cargoLock = - ownPackageBlock(['tauri-plugin-http 2.6.0']) + + ownPackageBlock([ref]) + cargoLockEntry('tauri-plugin-http', '2.5.0') + // transitive, textually first cargoLockEntry('tauri-plugin-http', '2.6.0'); // direct, referenced by name const versions = resolvedCargoPluginVersions(cargoLock); - expect(versions.get('tauri-plugin-http')).toBe('2.6.0'); - }); - - it('fails closed (null) when a crate resolves to more than one version with no disambiguating reference', () => { - const cargoLock = - ownPackageBlock(['tauri-plugin-http']) + - cargoLockEntry('tauri-plugin-http', '2.5.0') + - cargoLockEntry('tauri-plugin-http', '2.6.0'); - const versions = resolvedCargoPluginVersions(cargoLock); - expect(versions.get('tauri-plugin-http')).toBeNull(); - }); - - // QNBS-v3 (cubic): a qualified direct-dependency reference must be checked against real [[package]] entries — an inconsistent lockfile referencing a version with no matching entry must not be trusted at face value. - it('fails closed when the direct-dependency reference points to a version with no matching package entry', () => { - const cargoLock = - ownPackageBlock(['tauri-plugin-http 2.7.0']) + // references a version that doesn't exist below - cargoLockEntry('tauri-plugin-http', '2.5.0') + - cargoLockEntry('tauri-plugin-http', '2.6.0'); - const versions = resolvedCargoPluginVersions(cargoLock); - expect(versions.get('tauri-plugin-http')).toBeNull(); + expect(versions.get('tauri-plugin-http') ?? null).toEqual(expected); }); // QNBS-v3 (codex): a crate with only one lockfile entry is not necessarily a direct app dependency — it can be pulled in solely as a transitive of another plugin (e.g. tauri-plugin-deep-link via single-instance's "deep-link" feature). Absent from the direct-dependencies list, it must not be treated as resolved at all. From f7eeda17a532f8a5774850dd0f62f8cb300259f2 Mon Sep 17 00:00:00 2001 From: qnbs <155236708+qnbs@users.noreply.github.com> Date: Wed, 9 Sep 2026 18:23:10 +0200 Subject: [PATCH 08/10] test(desktop): fix a test-fidelity gap the direct-dependency requirement introduced "flags a Rust-ahead-of-npm minor-version mismatch, reproducing the v1.28.5 release failure" was missing the ownPackageBlock() added by the direct-dependency-membership fix, so after that fix landed, both crates fell through the "not a direct dependency" fail-closed path instead of the "major.minor mismatch" path the test's name and comment claim to reproduce. It stayed green only because its assertions checked for the crate name as a substring, which both finding messages contain. Fixed the fixture and tightened this assertion (and the sibling workspace-member-drift test's) to also require the literal "major.minor mismatch" wording, so a future regression back into the wrong code path fails loudly instead of passing for the wrong reason. 19/19 tests still pass. --- tests/unit/checkTauriPluginVersions.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/unit/checkTauriPluginVersions.test.ts b/tests/unit/checkTauriPluginVersions.test.ts index e41460106..580d47a65 100644 --- a/tests/unit/checkTauriPluginVersions.test.ts +++ b/tests/unit/checkTauriPluginVersions.test.ts @@ -147,6 +147,7 @@ describe('findTauriPluginVersionMismatches', () => { // QNBS-v3: reproduces the live v1.28.5 release-build failure — Rust bumped via a Dependabot PR without a matching npm bump, resolved lockfile versions included. it('flags a Rust-ahead-of-npm minor-version mismatch, reproducing the v1.28.5 release failure', () => { const cargoLock = + ownPackageBlock(['tauri-plugin-http', 'tauri-plugin-notification']) + cargoLockEntry('tauri-plugin-http', '2.6.0') + cargoLockEntry('tauri-plugin-notification', '2.4.0'); const pnpmLock = pnpmImporterBlock('.', { @@ -160,7 +161,10 @@ describe('findTauriPluginVersionMismatches', () => { }), ]); expect(findings).toHaveLength(2); + // QNBS-v3: asserts the specific "major.minor mismatch" wording, not just the crate name — a weaker substring check on the crate name alone would also pass for the wrong reason (e.g. a "no resolved version" fail-closed finding also contains the crate name). + expect(findings[0]).toContain('major.minor mismatch'); expect(findings[0]).toContain('tauri-plugin-http'); + expect(findings[1]).toContain('major.minor mismatch'); expect(findings[1]).toContain('tauri-plugin-notification'); }); @@ -181,6 +185,7 @@ describe('findTauriPluginVersionMismatches', () => { importerPkg('packages/desktop-contracts', { '@tauri-apps/plugin-notification': '^2.3.3' }), ]); expect(findings).toHaveLength(1); + expect(findings[0]).toContain('major.minor mismatch'); expect(findings[0]).toContain('packages/desktop-contracts'); expect(findings[0]).toContain('@tauri-apps/plugin-notification'); }); From e882aeef5faad0859e7add071cbc63de140e3d9a Mon Sep 17 00:00:00 2001 From: qnbs <155236708+qnbs@users.noreply.github.com> Date: Wed, 9 Sep 2026 18:58:28 +0200 Subject: [PATCH 09/10] test(desktop): consolidate Tauri plugin-parity single-importer cases Collapses six near-identical findTauriPluginVersionMismatches tests (each asserting one single-'.'-importer / single-http-crate scenario) into one it.each table, resolving CodeScene's repeated Code Duplication finding on this file without dropping any asserted code path. --- README.md | 8 +- tests/unit/checkTauriPluginVersions.test.ts | 138 +++++++++----------- 2 files changed, 69 insertions(+), 77 deletions(-) diff --git a/README.md b/README.md index fc5f726c5..ac00f4ae3 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ IndexedDB v8 PWA v3.0 i18n 19 locales — 2942 keys - 7647+ tests / 604 files + 7643+ tests / 604 files Codecov Coverage License MIT CI Status @@ -512,7 +512,7 @@ The Settings → AI panel shows a live GPU status badge with adapter details and | **Document Export** | docx + jszip | Word-compatible `.docx` generation (lazy-loaded) | | **PWA** | Service Worker + Web App Manifest v3 | Offline support, installability, Workbox chunking | | **i18n** | Custom React Context (`I18nContext.tsx`) | 2942 keys × 19 locales (de/en/es/fr/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu/ru/ko Beta); EN fallback; `localStorage` persistence | -| **Testing** | Vitest 4.x (7647+ tests / 604 files) + Playwright E2E | Unit/integration + cross-browser E2E; Stryker mutation (manual workflow) | +| **Testing** | Vitest 4.x (7643+ tests / 604 files) + Playwright E2E | Unit/integration + cross-browser E2E; Stryker mutation (manual workflow) | | **Code Quality** | Biome (lint + format) + TypeScript 7 (tsgo) strict | `--error-on-warnings` in CI; zero `any` policy | | **Visualization** | Force-directed graph | Interactive character relationship network | | **Desktop** | Tauri v2 | Cross-platform installer; auto-updater via `latest.json` | @@ -550,7 +550,7 @@ WorldScript-Studio/ │ ├── sw.js # PWA Service Worker │ └── manifest.json # PWA Web App Manifest v3 ├── tests/ -│ ├── unit/ # Vitest unit tests (7647+ tests, 604 files) — count spans tests/, components/, packages/*/tests/, not just this folder +│ ├── unit/ # Vitest unit tests (7643+ tests, 604 files) — count spans tests/, components/, packages/*/tests/, not just this folder │ │ ├── ai/ # aiSmallModules, aiCoreFallbackPaths │ │ └── settings/ # WebLlmPanel, AiSections │ └── e2e/ # Playwright specs + helpers.ts @@ -715,7 +715,7 @@ The main pipeline is [`.github/workflows/ci.yml`](.github/workflows/ci.yml). Opt Raw bundle-budget ceilings (KB per uncompressed asset): entry **2500 KB**, vendor **6200 KB**, other JavaScript **2500 KB**, and WASM **30000 KB**. **Current test metrics (2026-09-07, source-synchronized; CI remains authoritative for pass/fail):** -- **7647+ unit tests** across **604 test files** — CI is authoritative for pass/fail +- **7643+ unit tests** across **604 test files** — CI is authoritative for pass/fail - Coverage thresholds: lines ≥ 80 · branches ≥ 66 · functions ≥ 72 · statements ≥ 78 — enforced in CI (see Codecov badge for live metrics) - i18n: **2942 keys × 19 locales** (en/de/fr/es/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu/ru/ko Beta) diff --git a/tests/unit/checkTauriPluginVersions.test.ts b/tests/unit/checkTauriPluginVersions.test.ts index 580d47a65..03cde13a7 100644 --- a/tests/unit/checkTauriPluginVersions.test.ts +++ b/tests/unit/checkTauriPluginVersions.test.ts @@ -190,91 +190,83 @@ describe('findTauriPluginVersionMismatches', () => { expect(findings[0]).toContain('@tauri-apps/plugin-notification'); }); - // QNBS-v3 (codex): a crate resolved only transitively (absent from the direct-dependencies list) must fail closed through the full pipeline too, not just at the resolvedCargoPluginVersions level. - it('fails closed end-to-end when the declared npm plugin has no direct Rust dependency, only a transitive one', () => { - const cargoLock = - ownPackageBlock(['tauri-plugin-dialog']) + cargoLockEntry('tauri-plugin-http', '2.6.0'); - const pnpmLock = pnpmImporterBlock('.', { - '@tauri-apps/plugin-http': { specifier: '^2.6.0', version: '2.6.0' }, - }); - const findings = findTauriPluginVersionMismatches(cargoLock, pnpmLock, [ - importerPkg('.', { '@tauri-apps/plugin-http': '^2.6.0' }), - ]); - expect(findings).toHaveLength(1); - expect(findings[0]).toContain('no resolved version in Cargo.lock'); - }); - - // QNBS-v3 (codex): a plugin declared under optionalDependencies/peerDependencies/devDependencies still resolves into the lockfile and can still be bundled — checking only "dependencies" silently skipped it. - it('checks a plugin declared under a non-"dependencies" section too', () => { - const cargoLock = - ownPackageBlock(['tauri-plugin-http']) + cargoLockEntry('tauri-plugin-http', '2.5.0'); - const pnpmLock = pnpmImporterBlock('.', { - '@tauri-apps/plugin-http': { specifier: '^2.6.0', version: '2.6.0' }, - }); - const findings = findTauriPluginVersionMismatches(cargoLock, pnpmLock, [ - importerPkgWithSection('.', 'optionalDependencies', { '@tauri-apps/plugin-http': '^2.6.0' }), - ]); - expect(findings).toHaveLength(1); - expect(findings[0]).toContain('major.minor mismatch'); - }); - - // QNBS-v3 (cubic): a duplicate Cargo.lock entry must resolve to the direct app dependency's version (2.6.0), never the textually-first transitive occurrence (2.5.0) — proven by the fact that using the wrong one would falsely flag this as a mismatch. - it('does not mistake a transitive crate version for the direct app dependency', () => { - const cargoLock = - ownPackageBlock(['tauri-plugin-http 2.6.0']) + - cargoLockEntry('tauri-plugin-http', '2.5.0') + - cargoLockEntry('tauri-plugin-http', '2.6.0'); - const pnpmLock = pnpmImporterBlock('.', { - '@tauri-apps/plugin-http': { specifier: '^2.6.0', version: '2.6.3' }, - }); - const findings = findTauriPluginVersionMismatches(cargoLock, pnpmLock, [ - importerPkg('.', { '@tauri-apps/plugin-http': '^2.6.0' }), - ]); - expect(findings).toEqual([]); - }); - - it('fails closed when Cargo.lock has an undisambiguated duplicate, rather than guessing', () => { - const cargoLock = - ownPackageBlock(['tauri-plugin-http']) + - cargoLockEntry('tauri-plugin-http', '2.5.0') + - cargoLockEntry('tauri-plugin-http', '2.6.0'); - const pnpmLock = pnpmImporterBlock('.', { - '@tauri-apps/plugin-http': { specifier: '^2.6.0', version: '2.6.3' }, - }); - const findings = findTauriPluginVersionMismatches(cargoLock, pnpmLock, [ - importerPkg('.', { '@tauri-apps/plugin-http': '^2.6.0' }), - ]); - expect(findings).toHaveLength(1); - expect(findings[0]).toContain('more than one version'); - }); - - // QNBS-v3: fail closed — a plugin declared in package.json with no resolved Cargo.lock or pnpm-lock.yaml entry means a lockfile is out of sync and parity cannot be verified, which must surface as a finding, not a silent pass. + // QNBS-v3 (CodeScene): unifies six single-'.'-importer, single-http-crate scenarios (each previously its own near-identical test) into one table — every distinct code path (no direct Rust dependency, non-"dependencies" section, undisambiguated duplicate, correctly-disambiguated duplicate, missing Cargo.lock entry, missing pnpm-lock.yaml entry) stays independently named and asserted, just without repeating the same four-line "build lockfiles, call, assert" shape six times. it.each([ { - name: 'no resolved Rust crate version', - cargoLock: '', // no tauri-plugin-http entry at all + name: 'no direct Rust dependency, only a transitive one', + cargoLock: + ownPackageBlock(['tauri-plugin-dialog']) + cargoLockEntry('tauri-plugin-http', '2.6.0'), + pnpmLock: pnpmImporterBlock('.', { + '@tauri-apps/plugin-http': { specifier: '^2.6.0', version: '2.6.0' }, + }), + importerPackages: [importerPkg('.', { '@tauri-apps/plugin-http': '^2.6.0' })], + expectedSubstrings: ['no resolved version in Cargo.lock'], + }, + { + // QNBS-v3 (codex): a plugin declared under optionalDependencies/peerDependencies/devDependencies still resolves into the lockfile and can still be bundled — checking only "dependencies" silently skipped it. + name: 'a plugin declared under a non-"dependencies" section', + cargoLock: + ownPackageBlock(['tauri-plugin-http']) + cargoLockEntry('tauri-plugin-http', '2.5.0'), + pnpmLock: pnpmImporterBlock('.', { + '@tauri-apps/plugin-http': { specifier: '^2.6.0', version: '2.6.0' }, + }), + importerPackages: [ + importerPkgWithSection('.', 'optionalDependencies', { + '@tauri-apps/plugin-http': '^2.6.0', + }), + ], + expectedSubstrings: ['major.minor mismatch'], + }, + { + name: 'an undisambiguated Cargo.lock duplicate, rather than guessing', + cargoLock: + ownPackageBlock(['tauri-plugin-http']) + + cargoLockEntry('tauri-plugin-http', '2.5.0') + + cargoLockEntry('tauri-plugin-http', '2.6.0'), + pnpmLock: pnpmImporterBlock('.', { + '@tauri-apps/plugin-http': { specifier: '^2.6.0', version: '2.6.3' }, + }), + importerPackages: [importerPkg('.', { '@tauri-apps/plugin-http': '^2.6.0' })], + expectedSubstrings: ['more than one version'], + }, + { + // QNBS-v3 (cubic): a duplicate Cargo.lock entry must resolve to the direct app dependency's version (2.6.0), never the textually-first transitive occurrence (2.5.0) — proven by zero findings, since using the wrong one would falsely flag this as a mismatch. + name: 'nothing, correctly disambiguating a duplicate Cargo.lock entry via the direct reference', + cargoLock: + ownPackageBlock(['tauri-plugin-http 2.6.0']) + + cargoLockEntry('tauri-plugin-http', '2.5.0') + + cargoLockEntry('tauri-plugin-http', '2.6.0'), + pnpmLock: pnpmImporterBlock('.', { + '@tauri-apps/plugin-http': { specifier: '^2.6.0', version: '2.6.3' }, + }), + importerPackages: [importerPkg('.', { '@tauri-apps/plugin-http': '^2.6.0' })], + expectedSubstrings: [], + }, + { + // QNBS-v3: fail closed — a plugin declared in package.json with no resolved Cargo.lock entry means the lockfile is out of sync and parity cannot be verified, which must surface as a finding, not a silent pass. + name: 'no resolved Rust crate version at all', + cargoLock: '', pnpmLock: pnpmImporterBlock('.', { '@tauri-apps/plugin-http': { specifier: '^2.6.0', version: '2.6.0' }, }), - expectedSubstring: 'no resolved version in Cargo.lock', + importerPackages: [importerPkg('.', { '@tauri-apps/plugin-http': '^2.6.0' })], + expectedSubstrings: ['no resolved version in Cargo.lock'], }, { name: 'no resolved npm version in the lockfile', cargoLock: ownPackageBlock(['tauri-plugin-http']) + cargoLockEntry('tauri-plugin-http', '2.6.0'), pnpmLock: pnpmImporterBlock('.', {}), // http declared in package.json but absent from the lockfile importer - expectedSubstring: 'no resolved version in pnpm-lock.yaml', + importerPackages: [importerPkg('.', { '@tauri-apps/plugin-http': '^2.6.0' })], + expectedSubstrings: ['no resolved version in pnpm-lock.yaml'], }, - ])( - 'fails closed when a declared plugin has $name', - ({ cargoLock, pnpmLock, expectedSubstring }) => { - const findings = findTauriPluginVersionMismatches(cargoLock, pnpmLock, [ - importerPkg('.', { '@tauri-apps/plugin-http': '^2.6.0' }), - ]); - expect(findings).toHaveLength(1); - expect(findings[0]).toContain(expectedSubstring); - }, - ); + ])('flags $name', ({ cargoLock, pnpmLock, importerPackages, expectedSubstrings }) => { + const findings = findTauriPluginVersionMismatches(cargoLock, pnpmLock, importerPackages); + expect(findings).toHaveLength(expectedSubstrings.length); + expectedSubstrings.forEach((substring, index) => { + expect(findings[index]).toContain(substring); + }); + }); it('skips a plugin the importer does not declare at all (not applicable, not a failure)', () => { // QNBS-v3: http has no Cargo.lock entry at all here, proving it was never even considered for an importer that doesn't declare it — only notification (declared, and given a matching Cargo entry) is checked. From 52f14d15aebede5a0482225ba6327221ef81d9c8 Mon Sep 17 00:00:00 2001 From: qnbs <155236708+qnbs@users.noreply.github.com> Date: Wed, 9 Sep 2026 20:52:36 +0200 Subject: [PATCH 10/10] docs(ci): document the Tauri plugin parity gate; trim redundant test comment docs/CI.md and docs/PR-CI-MERGE-WORKFLOW.md both enumerated ci:prepush's unconditional checks without the new Tauri plugin version-parity gate, leaving maintainers with an inaccurate gate inventory. Also removes two QNBS-v3 comments that only restated what the test name and adjacent assertions already made explicit, per AGENTS.md's obvious-test exemption. --- docs/CI.md | 2 +- docs/PR-CI-MERGE-WORKFLOW.md | 4 ++-- tests/unit/checkTauriPluginVersions.test.ts | 2 -- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/CI.md b/docs/CI.md index fd3f09d0b..b54e79a6c 100644 --- a/docs/CI.md +++ b/docs/CI.md @@ -375,7 +375,7 @@ the local block above; GitHub Actions owns those heavy checks on this hardware. `pnpm run ci:prepush` always resolves a change classification from the outgoing evidence first (`scripts/ci-prepush-classifier.mjs`), then runs docs/release-truth, CSP, desktop-import boundary, -native-readiness, and dependency-state checks unconditionally on every invocation. It does **not** +native-readiness, Tauri plugin version-parity, and dependency-state checks unconditionally on every invocation. It does **not** run Biome lint — full-repository lint stays CI-owned (`quality` job); only staged files are linted locally, by the separate pre-commit hook (`lint-staged`). Two check groups are conditional on the change classification instead of always running: diff --git a/docs/PR-CI-MERGE-WORKFLOW.md b/docs/PR-CI-MERGE-WORKFLOW.md index e5d810934..9a7ea1a33 100644 --- a/docs/PR-CI-MERGE-WORKFLOW.md +++ b/docs/PR-CI-MERGE-WORKFLOW.md @@ -2,11 +2,11 @@ Full pre-push gate mechanics, worktree bootstrap, CI pipeline order, the PR review-comment correction loop (bot roster, three-channel comment check, quiescence loop), and known GitHub merge-gate quirks with recovery steps. The short list of non-negotiable rules distilled from this doc lives in the root `AGENTS.md`. -**Mandatory pre-push gate:** Run `pnpm run ci:prepush` before every push and again after every local correction before re-pushing. It always resolves a change-aware classification (`scripts/ci-prepush-classifier.mjs`) from the outgoing evidence first, then runs docs/release-truth, CSP, desktop-import-boundary, native-readiness, and dependency-state checks unconditionally — it does **not** run Biome lint; that stays the pre-commit hook's job on staged files only (`lint-staged`), and full-repository lint is CI-owned. The single-checker (`--checkers 1`) local typecheck and the i18n/content-guard checks run only when the classification requires them — `DOCS_ONLY`, `WORKFLOW_ONLY`, `NON_CODE_ONLY`, `RUST_TAURI`, `TOOLING`, and non-TypeScript `TEST_ONLY` changes report typecheck as `DEFERRED_TO_REQUIRED_CI` instead of running it locally, and i18n/content-guard checks run only for changes matching their own governed paths or implementation files (see `scripts/ci-prepush-check-registry.mjs`). It is the same `tsgo --noEmit` check as CI, not literally identical to it — CI uses `--checkers 4`. Whenever outgoing path evidence is incomplete, unresolved, or the manual committed-range diff fails, the gate fails closed into full local admission (every conditional check runs) rather than deferring anything. A targeted test or changed-file lint run alone is insufficient. If pnpm reports a dependency verification failure after a branch or lockfile change, run `node scripts/dependency-state.mjs reconcile` (or `pnpm run deps:reconcile`) first, then rerun the gate. The pre-commit hook does not replace this gate. Required GitHub CI remains the unconditional authority for the complete lint, TypeScript, and i18n validation regardless of what the local gate deferred. +**Mandatory pre-push gate:** Run `pnpm run ci:prepush` before every push and again after every local correction before re-pushing. It always resolves a change-aware classification (`scripts/ci-prepush-classifier.mjs`) from the outgoing evidence first, then runs docs/release-truth, CSP, desktop-import-boundary, native-readiness, Tauri plugin version-parity, and dependency-state checks unconditionally — it does **not** run Biome lint; that stays the pre-commit hook's job on staged files only (`lint-staged`), and full-repository lint is CI-owned. The single-checker (`--checkers 1`) local typecheck and the i18n/content-guard checks run only when the classification requires them — `DOCS_ONLY`, `WORKFLOW_ONLY`, `NON_CODE_ONLY`, `RUST_TAURI`, `TOOLING`, and non-TypeScript `TEST_ONLY` changes report typecheck as `DEFERRED_TO_REQUIRED_CI` instead of running it locally, and i18n/content-guard checks run only for changes matching their own governed paths or implementation files (see `scripts/ci-prepush-check-registry.mjs`). It is the same `tsgo --noEmit` check as CI, not literally identical to it — CI uses `--checkers 4`. Whenever outgoing path evidence is incomplete, unresolved, or the manual committed-range diff fails, the gate fails closed into full local admission (every conditional check runs) rather than deferring anything. A targeted test or changed-file lint run alone is insufficient. If pnpm reports a dependency verification failure after a branch or lockfile change, run `node scripts/dependency-state.mjs reconcile` (or `pnpm run deps:reconcile`) first, then rerun the gate. The pre-commit hook does not replace this gate. Required GitHub CI remains the unconditional authority for the complete lint, TypeScript, and i18n validation regardless of what the local gate deferred. **New-worktree / first-time dependency bootstrap — never a bare `pnpm install`:** When materializing `node_modules` for the first time on a new git worktree (or any branch checkout that hasn't been installed yet), always run `node scripts/dependency-state.mjs reconcile` (or `pnpm run deps:reconcile`) — never a bare `pnpm install`. The bare form omits `--frozen-lockfile`, so on any manifest/lockfile drift it silently *rewrites* `pnpm-lock.yaml` instead of failing loudly, and it never writes the repo's own dependency fingerprint (`node_modules/.worldscript-deps-fingerprint`) — that fingerprint is checked only by `pnpm run deps:verify`, `ci:prepush`, and the pre-commit hook, not by arbitrary `pnpm run