Files
Genarrative/scripts/lint-staged-rustfmt.test.ts
T
suzmii 3b385ef575 修复 PR316 review:pre-commit Rust 格式检查按暂存文件筛 workspace
- scripts/lint-staged-rustfmt.mjs:12 [maintainability medium] 改为读取 lint-staged 追加的暂存 .rs 路径,只对真正命中暂存文件的 workspace 跑 cargo fmt --all -- --check:此前永远全量跑两个 workspace,只暂存 server-rs 干净文件的提交会被别人未暂存的 src-tauri 改动挡下来;没收到暂存列表(手工执行)时回退全量检查并在 stderr 说明,避免假装检查过。
- scripts/lint-staged-rustfmt.mjs:7 [maintainability low] apps/desktop-shell/src-tauri 显式记为「有意排除」而不是静默通过:实测该 workspace 当前整体不过 cargo fmt --check(host_bridge/mod.rs 等),直接纳入会让任何 Rust 提交都失败;命中该前缀的暂存文件会打印跳过提示,纳入步骤写在模块注释里(同时要补 package.json 的 check:rustfmt / format:rust,package.json 不在本批次改动范围)。
- 新增 scripts/lint-staged-rustfmt-workspaces.mjs:把「暂存路径 → 待检查 workspace」的纯映射逻辑独立出来,附 Windows 反斜杠/绝对路径归一化。
- 新增 scripts/lint-staged-rustfmt.test.ts(vitest,随 npm test 运行):覆盖只暂存单侧 workspace 的回归判据、手工执行回退全量、排除项与未登记路径必须被点名、路径归一化,以及与 package.json check:rustfmt 的 workspace 表同集交叉校验。
2026-09-12 19:44:18 +08:00

114 lines
4.1 KiB
TypeScript

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<string, string> };
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);
}
});
});