fix: 修复 Windows 平台测试兼容性问题
- 新增 tests/helpers.ts 的 rmRetry 工具函数,解决 SQLite 文件句柄未及时释放导致 afterAll 清理时 EBUSY 错误 - 修改通配符测试用例,使用 bun -e 替代 echo 命令,确保跨平台行为一致
This commit is contained in:
@@ -9,9 +9,9 @@ context: |
|
|||||||
- 新增的逻辑必须编写完善的测试,并保证测试的正确性,不允许跳过任何测试
|
- 新增的逻辑必须编写完善的测试,并保证测试的正确性,不允许跳过任何测试
|
||||||
- 这是基于bun实现的前端后一体化项目,使用bun作为唯一包管理器,严禁使用pnpm、npm,使用bunx运行工具,严禁使用npx、pnpx
|
- 这是基于bun实现的前端后一体化项目,使用bun作为唯一包管理器,严禁使用pnpm、npm,使用bunx运行工具,严禁使用npx、pnpx
|
||||||
- src/server目录下是基于bun实现的后端代码
|
- src/server目录下是基于bun实现的后端代码
|
||||||
- src/web目录下是基于vite、react、TDesign实现的前端代码
|
|
||||||
- 后端库使用优先级:Bun 内置 API > es-toolkit > 主流三方库 > 项目公共工具 > 自行实现
|
- 后端库使用优先级: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、硬编码色值
|
- 前端严禁:组件内联style属性、CSS覆盖TD内部类名、使用!important、硬编码色值
|
||||||
- Git提交: 仅中文; 格式"类型: 简短描述", 类型: feat/fix/refactor/docs/style/test/chore; 多行描述空行后写详细说明
|
- Git提交: 仅中文; 格式"类型: 简短描述", 类型: feat/fix/refactor/docs/style/test/chore; 多行描述空行后写详细说明
|
||||||
- 禁止创建git操作task
|
- 禁止创建git操作task
|
||||||
|
|||||||
25
openspec/specs/windows-test-compat/spec.md
Normal file
25
openspec/specs/windows-test-compat/spec.md
Normal file
@@ -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`
|
||||||
13
tests/helpers.ts
Normal file
13
tests/helpers.ts
Normal file
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
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 { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
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 { CommandChecker } from "../../src/server/checker/runner/command/runner";
|
||||||
import { HttpChecker } from "../../src/server/checker/runner/http/runner";
|
import { HttpChecker } from "../../src/server/checker/runner/http/runner";
|
||||||
import { ProbeStore } from "../../src/server/checker/store";
|
import { ProbeStore } from "../../src/server/checker/store";
|
||||||
|
import { rmRetry } from "../helpers";
|
||||||
|
|
||||||
function ensureRegistered() {
|
function ensureRegistered() {
|
||||||
if (!checkerRegistry.supportedTypes.includes("http")) {
|
if (!checkerRegistry.supportedTypes.includes("http")) {
|
||||||
@@ -100,7 +101,7 @@ describe("API 路由", () => {
|
|||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
store.close();
|
store.close();
|
||||||
await rm(tempDir, { force: true, recursive: true });
|
await rmRetry(tempDir);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("/health 返回健康检查", async () => {
|
test("/health 返回健康检查", async () => {
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ describe("CommandChecker", () => {
|
|||||||
|
|
||||||
test("不使用 shell,通配符不被展开", async () => {
|
test("不使用 shell,通配符不被展开", async () => {
|
||||||
const result = await checker.execute(
|
const result = await checker.execute(
|
||||||
makeTarget({ args: ["*"], exec: "echo" }, { expect: { stdout: [{ contains: "*" }] } }),
|
makeTarget({ args: ["-e", "console.log('*')"], exec: "bun" }, { expect: { stdout: [{ contains: "*" }] } }),
|
||||||
makeCtx(),
|
makeCtx(),
|
||||||
);
|
);
|
||||||
expect(result.matched).toBe(true);
|
expect(result.matched).toBe(true);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
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 { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
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 { CommandChecker } from "../../../src/server/checker/runner/command/runner";
|
||||||
import { HttpChecker } from "../../../src/server/checker/runner/http/runner";
|
import { HttpChecker } from "../../../src/server/checker/runner/http/runner";
|
||||||
import { ProbeStore } from "../../../src/server/checker/store";
|
import { ProbeStore } from "../../../src/server/checker/store";
|
||||||
|
import { rmRetry } from "../../helpers";
|
||||||
|
|
||||||
function ensureRegistered() {
|
function ensureRegistered() {
|
||||||
if (!checkerRegistry.supportedTypes.includes("http")) {
|
if (!checkerRegistry.supportedTypes.includes("http")) {
|
||||||
@@ -63,7 +64,7 @@ describe("ProbeStore", () => {
|
|||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
store.close();
|
store.close();
|
||||||
await rm(tempDir, { force: true, recursive: true });
|
await rmRetry(tempDir);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("初始化后无 targets", () => {
|
test("初始化后无 targets", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user