From 87d946a44159abf0d2649bcc663a4416af751c92 Mon Sep 17 00:00:00 2001 From: lanyuanxiaoyao Date: Tue, 12 May 2026 22:11:34 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20Windows=20=E5=B9=B3?= =?UTF-8?q?=E5=8F=B0=E6=B5=8B=E8=AF=95=E5=85=BC=E5=AE=B9=E6=80=A7=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 tests/helpers.ts 的 rmRetry 工具函数,解决 SQLite 文件句柄未及时释放导致 afterAll 清理时 EBUSY 错误 - 修改通配符测试用例,使用 bun -e 替代 echo 命令,确保跨平台行为一致 --- openspec/config.yaml | 4 +-- openspec/specs/windows-test-compat/spec.md | 25 +++++++++++++++++++ tests/helpers.ts | 13 ++++++++++ tests/server/app.test.ts | 5 ++-- .../checker/runner/command/runner.test.ts | 2 +- tests/server/checker/store.test.ts | 5 ++-- 6 files changed, 47 insertions(+), 7 deletions(-) create mode 100644 openspec/specs/windows-test-compat/spec.md create mode 100644 tests/helpers.ts diff --git a/openspec/config.yaml b/openspec/config.yaml index eb6c3fc..e269907 100644 --- a/openspec/config.yaml +++ b/openspec/config.yaml @@ -9,9 +9,9 @@ context: | - 新增的逻辑必须编写完善的测试,并保证测试的正确性,不允许跳过任何测试 - 这是基于bun实现的前端后一体化项目,使用bun作为唯一包管理器,严禁使用pnpm、npm,使用bunx运行工具,严禁使用npx、pnpx - src/server目录下是基于bun实现的后端代码 - - src/web目录下是基于vite、react、TDesign实现的前端代码 - 后端库使用优先级:Bun 内置 API > es-toolkit > 主流三方库 > 项目公共工具 > 自行实现 - - 前端样式开发优先级:TDesign组件 > 组件props > TDesign CSS tokens(--td-*) > styles.css CSS类 > 自行开发组件 + - src/web目录下是基于vite、react、TDesign实现的前端代码 + - 前端样式开发优先级:TDesign组件 > 组件props > TDesign CSS tokens(--td-*) > styles.css CSS类 > 自行开发组件 - 前端严禁:组件内联style属性、CSS覆盖TD内部类名、使用!important、硬编码色值 - Git提交: 仅中文; 格式"类型: 简短描述", 类型: feat/fix/refactor/docs/style/test/chore; 多行描述空行后写详细说明 - 禁止创建git操作task diff --git a/openspec/specs/windows-test-compat/spec.md b/openspec/specs/windows-test-compat/spec.md new file mode 100644 index 0000000..fef4176 --- /dev/null +++ b/openspec/specs/windows-test-compat/spec.md @@ -0,0 +1,25 @@ +# Capability: windows-test-compat + +## Purpose + +确保测试在 Windows 平台上的兼容性,包括文件句柄释放后的目录清理重试机制和跨平台命令测试约定。 + +## Requirements + +### Requirement: 测试临时目录清理 SHALL 支持重试 + +使用 SQLite 数据库的测试 SHALL 在 `afterAll` 中使用带重试的目录删除机制,确保在 Windows 上文件句柄未及时释放时不会导致测试失败。 + +#### Scenario: Windows 上 SQLite 文件句柄延迟释放 + +- **WHEN** 测试在 Windows 上运行,`store.close()` 后立即尝试删除临时目录 +- **THEN** 删除操作 SHALL 自动重试(最多 3 次,间隔 200ms),直到成功或耗尽重试次数 + +### Requirement: 命令检测器测试 SHALL 使用跨平台命令 + +命令检测器的测试 SHALL 使用 `bun -e` 脚本替代系统 `echo` 命令,确保测试断言在所有平台上行为一致。 + +#### Scenario: 验证非 shell 模式下特殊字符不被展开 + +- **WHEN** 通过 `Bun.spawn` 执行 `bun -e "console.log('*')"` 并检查 stdout 包含 `*` +- **THEN** 测试 SHALL 在 Windows 和 Linux 上均返回 `matched: true` diff --git a/tests/helpers.ts b/tests/helpers.ts new file mode 100644 index 0000000..7cbb863 --- /dev/null +++ b/tests/helpers.ts @@ -0,0 +1,13 @@ +import { rm } from "node:fs/promises"; + +export async function rmRetry(dir: string, retries = 10, delayMs = 500) { + for (let i = 0; i < retries; i++) { + try { + await rm(dir, { force: true, recursive: true }); + return; + } catch (e) { + if (i === retries - 1) throw e; + await new Promise((r) => setTimeout(r, delayMs)); + } + } +} diff --git a/tests/server/app.test.ts b/tests/server/app.test.ts index d1fb1a9..e9f5288 100644 --- a/tests/server/app.test.ts +++ b/tests/server/app.test.ts @@ -1,5 +1,5 @@ import { afterAll, beforeAll, describe, expect, test } from "bun:test"; -import { mkdir, rm } from "node:fs/promises"; +import { mkdir } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -10,6 +10,7 @@ import { checkerRegistry } from "../../src/server/checker/runner"; import { CommandChecker } from "../../src/server/checker/runner/command/runner"; import { HttpChecker } from "../../src/server/checker/runner/http/runner"; import { ProbeStore } from "../../src/server/checker/store"; +import { rmRetry } from "../helpers"; function ensureRegistered() { if (!checkerRegistry.supportedTypes.includes("http")) { @@ -100,7 +101,7 @@ describe("API 路由", () => { afterAll(async () => { store.close(); - await rm(tempDir, { force: true, recursive: true }); + await rmRetry(tempDir); }); test("/health 返回健康检查", async () => { diff --git a/tests/server/checker/runner/command/runner.test.ts b/tests/server/checker/runner/command/runner.test.ts index 5d442f0..d75bf4b 100644 --- a/tests/server/checker/runner/command/runner.test.ts +++ b/tests/server/checker/runner/command/runner.test.ts @@ -119,7 +119,7 @@ describe("CommandChecker", () => { test("不使用 shell,通配符不被展开", async () => { const result = await checker.execute( - makeTarget({ args: ["*"], exec: "echo" }, { expect: { stdout: [{ contains: "*" }] } }), + makeTarget({ args: ["-e", "console.log('*')"], exec: "bun" }, { expect: { stdout: [{ contains: "*" }] } }), makeCtx(), ); expect(result.matched).toBe(true); diff --git a/tests/server/checker/store.test.ts b/tests/server/checker/store.test.ts index 0a472eb..0fa249f 100644 --- a/tests/server/checker/store.test.ts +++ b/tests/server/checker/store.test.ts @@ -1,5 +1,5 @@ import { afterAll, beforeAll, describe, expect, test } from "bun:test"; -import { mkdir, rm } from "node:fs/promises"; +import { mkdir } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -9,6 +9,7 @@ import { checkerRegistry } from "../../../src/server/checker/runner"; import { CommandChecker } from "../../../src/server/checker/runner/command/runner"; import { HttpChecker } from "../../../src/server/checker/runner/http/runner"; import { ProbeStore } from "../../../src/server/checker/store"; +import { rmRetry } from "../../helpers"; function ensureRegistered() { if (!checkerRegistry.supportedTypes.includes("http")) { @@ -63,7 +64,7 @@ describe("ProbeStore", () => { afterAll(async () => { store.close(); - await rm(tempDir, { force: true, recursive: true }); + await rmRetry(tempDir); }); test("初始化后无 targets", () => {