import { readFileSync } from 'node:fs'; import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { describe, expect, test } from 'vitest'; import { RUSTFMT_EXCLUDED_WORKSPACES, RUSTFMT_WORKSPACES, selectRustfmtWorkspaces, } from './lint-staged-rustfmt-workspaces.mjs'; const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); function selectedManifests(stagedFiles: string[], root = repoRoot) { return selectRustfmtWorkspaces({ stagedFiles, repoRoot: root, }).workspaces.map((workspace) => workspace.manifestPath); } describe('lint-staged Rust 格式检查的 workspace 选择', () => { test('只暂存 server-rs 时不再连带检查 src-tauri', () => { // 回归判据:`cargo fmt --all` 会连未暂存的在改文件一起查,全量跑两个 workspace // 会让「只暂存了 server-rs 干净文件」的提交被别人的 src-tauri 改动挡下来。 expect( selectedManifests([ 'server-rs/crates/api-server/src/editor_project.rs', 'server-rs/crates/shared-contracts/src/game_creation_app.rs', ]), ).toEqual(['server-rs/Cargo.toml']); }); test('只暂存 src-tauri 时不再连带检查 server-rs', () => { expect( selectedManifests(['apps/ai-game-creator-shell/src-tauri/src/assets.rs']), ).toEqual(['apps/ai-game-creator-shell/src-tauri/Cargo.toml']); }); test('两个 workspace 都有暂存文件时两个都查', () => { expect( selectedManifests([ 'server-rs/crates/api-server/src/editor_project.rs', 'apps/ai-game-creator-shell/src-tauri/src/assets.rs', ]), ).toEqual([ 'server-rs/Cargo.toml', 'apps/ai-game-creator-shell/src-tauri/Cargo.toml', ]); }); test('手工执行(没有暂存列表)时保持全量检查语义', () => { const selection = selectRustfmtWorkspaces({ stagedFiles: [], repoRoot }); expect(selection.usedStagedList).toBe(false); expect(selection.workspaces).toEqual(RUSTFMT_WORKSPACES); }); test('排除的 workspace 与未登记路径都会被显式报出,不静默通过', () => { const selection = selectRustfmtWorkspaces({ stagedFiles: [ 'apps/desktop-shell/src-tauri/src/host_bridge/mod.rs', 'somewhere-new/src/lib.rs', ], repoRoot, }); // desktop-shell 当前整体未过 `cargo fmt --check`,所以它不参与检查…… expect(selection.workspaces).toEqual([]); // ……但必须被点名,否则「glob 命中了、谁都没查」就是静默通过。 expect(selection.excluded).toEqual([ 'apps/desktop-shell/src-tauri/src/host_bridge/mod.rs', ]); expect(selection.unmanaged).toEqual(['somewhere-new/src/lib.rs']); }); test('Windows 反斜杠与绝对路径都归一化成仓库相对路径', () => { const root = 'C:\\repo\\genarrative'; expect( selectedManifests( [ 'server-rs\\crates\\api-server\\src\\editor_project.rs', `${root}\\apps\\ai-game-creator-shell\\src-tauri\\src\\assets.rs`, ], root, ), ).toEqual([ 'server-rs/Cargo.toml', 'apps/ai-game-creator-shell/src-tauri/Cargo.toml', ]); }); test('检查清单与 package.json 的 check:rustfmt 保持同一份 workspace 表', () => { // 两处各写一份 workspace 列表就会漂移:hook 查的与 `npm run check:rustfmt` 查的必须同集。 const packageJson = JSON.parse( readFileSync(resolve(repoRoot, 'package.json'), 'utf8'), ) as { scripts: Record }; const manifests = Array.from( packageJson.scripts['check:rustfmt']!.matchAll( /--manifest-path\s+(\S+)\s+--\s+--check/gu, ), ).map((match) => match[1]!); expect(manifests).toEqual( RUSTFMT_WORKSPACES.map((workspace) => workspace.manifestPath), ); // 排除项必须是「检查清单之外」的,否则这段注释与实际行为不符。 for (const excluded of RUSTFMT_EXCLUDED_WORKSPACES) { expect( RUSTFMT_WORKSPACES.some((workspace) => workspace.prefix.startsWith(excluded.prefix), ), ).toBe(false); } }); });