From 15ad256d1ead891a5dad789b02e2e15055bd4ba3 Mon Sep 17 00:00:00 2001 From: lanyuanxiaoyao Date: Thu, 11 Jun 2026 23:38:02 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=9B=BF=E6=8D=A2=E6=89=80=E6=9C=89?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E6=96=87=E4=BB=B6=E7=9A=84=20rm=20=E4=B8=BA?= =?UTF-8?q?=20retryRm=EF=BC=8C=E4=BF=AE=E5=A4=8D=20Windows=20EBUSY?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/adapters/claude-code.test.ts | 5 +- tests/adapters/opencode.test.ts | 11 ++- tests/cli/create.test.ts | 5 +- tests/commands/init.test.ts | 5 +- tests/core/assembler.test.ts | 5 +- tests/core/config.test.ts | 7 +- tests/core/scanner.test.ts | 5 +- tests/helpers/cleanup.test.ts | 145 ++++++++++------------------- tests/helpers/cleanup.ts | 10 +- tests/integration/flow.test.ts | 5 +- 10 files changed, 82 insertions(+), 121 deletions(-) diff --git a/tests/adapters/claude-code.test.ts b/tests/adapters/claude-code.test.ts index d135a2e..626ec44 100644 --- a/tests/adapters/claude-code.test.ts +++ b/tests/adapters/claude-code.test.ts @@ -1,6 +1,7 @@ import { describe, it, expect, beforeEach, afterEach } from "bun:test"; import { existsSync } from "node:fs"; -import { mkdir, rm, readFile, readdir, writeFile } from "node:fs/promises"; +import { mkdir, readFile, readdir, writeFile } from "node:fs/promises"; +import { retryRm } from "../helpers/cleanup.ts"; import { join } from "node:path"; import { injectClaudeCode, updateClaudeCode } from "../../src/adapters/claude-code.ts"; @@ -11,7 +12,7 @@ beforeEach(async () => { }); afterEach(async () => { - await rm(TMP_DIR, { recursive: true, force: true }); + await retryRm(TMP_DIR); }); describe("injectClaudeCode", () => { diff --git a/tests/adapters/opencode.test.ts b/tests/adapters/opencode.test.ts index 43cc57b..4d9e674 100644 --- a/tests/adapters/opencode.test.ts +++ b/tests/adapters/opencode.test.ts @@ -1,6 +1,7 @@ import { describe, it, expect, beforeEach, afterEach } from "bun:test"; import { existsSync } from "node:fs"; -import { mkdir, rm, readFile, readdir, writeFile } from "node:fs/promises"; +import { mkdir, readFile, readdir, writeFile } from "node:fs/promises"; +import { retryRm } from "../helpers/cleanup.ts"; import { join } from "node:path"; import { injectOpenCode, updateOpenCode } from "../../src/adapters/opencode.ts"; @@ -11,7 +12,7 @@ beforeEach(async () => { }); afterEach(async () => { - await rm(TMP_DIR, { recursive: true, force: true }); + await retryRm(TMP_DIR); }); describe("injectOpenCode", () => { @@ -217,7 +218,7 @@ describe("injectOpenCode with command prefix", () => { ); expect(content).toContain("bunx @lanyuanxiaoyao/rune discuss"); } finally { - await rm(tmpDir, { recursive: true, force: true }); + await retryRm(tmpDir); } }); @@ -232,7 +233,7 @@ describe("injectOpenCode with command prefix", () => { ); expect(content).toContain("rune discuss"); } finally { - await rm(tmpDir, { recursive: true, force: true }); + await retryRm(tmpDir); } }); @@ -247,7 +248,7 @@ describe("injectOpenCode with command prefix", () => { ); expect(content).toContain("bunx @lanyuanxiaoyao/rune status"); } finally { - await rm(tmpDir, { recursive: true, force: true }); + await retryRm(tmpDir); } }); }); diff --git a/tests/cli/create.test.ts b/tests/cli/create.test.ts index 14da872..ec87c96 100644 --- a/tests/cli/create.test.ts +++ b/tests/cli/create.test.ts @@ -1,6 +1,7 @@ import { describe, it, expect, beforeEach, afterEach } from "bun:test"; import { existsSync } from "node:fs"; -import { mkdir, rm } from "node:fs/promises"; +import { mkdir } from "node:fs/promises"; +import { retryRm } from "../helpers/cleanup.ts"; import { join } from "node:path"; import { runInit } from "../../src/commands/init.ts"; import { loadConfig, getChangeDir } from "../../src/core/config.ts"; @@ -14,7 +15,7 @@ beforeEach(async () => { }); afterEach(async () => { - await rm(TMP_DIR, { recursive: true, force: true }); + await retryRm(TMP_DIR); }); describe("create 命令(工具命令,非 SDD 阶段)", () => { diff --git a/tests/commands/init.test.ts b/tests/commands/init.test.ts index 3ca42a9..409c5e7 100644 --- a/tests/commands/init.test.ts +++ b/tests/commands/init.test.ts @@ -1,6 +1,7 @@ import { describe, it, expect, beforeEach, afterEach } from "bun:test"; import { existsSync } from "node:fs"; -import { mkdir, rm, readFile, writeFile } from "node:fs/promises"; +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { retryRm } from "../helpers/cleanup.ts"; import { join } from "node:path"; import { runInit } from "../../src/commands/init.ts"; import { CommandError } from "../../src/cli/errors.ts"; @@ -12,7 +13,7 @@ beforeEach(async () => { }); afterEach(async () => { - await rm(TMP_DIR, { recursive: true, force: true }); + await retryRm(TMP_DIR); }); describe("runInit", () => { diff --git a/tests/core/assembler.test.ts b/tests/core/assembler.test.ts index f945379..d60bfd4 100644 --- a/tests/core/assembler.test.ts +++ b/tests/core/assembler.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect, beforeEach, afterEach } from "bun:test"; -import { mkdir, writeFile, rm } from "node:fs/promises"; +import { mkdir, writeFile } from "node:fs/promises"; +import { retryRm } from "../helpers/cleanup.ts"; import { join } from "node:path"; import { assembleDiscussPrompt, @@ -18,7 +19,7 @@ beforeEach(async () => { }); afterEach(async () => { - await rm(TMP_DIR, { recursive: true, force: true }); + await retryRm(TMP_DIR); }); describe("assembleDiscussPrompt", () => { diff --git a/tests/core/config.test.ts b/tests/core/config.test.ts index 6db7ea5..0586b0a 100644 --- a/tests/core/config.test.ts +++ b/tests/core/config.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect, beforeEach, afterEach } from "bun:test"; -import { mkdir, writeFile, rm } from "node:fs/promises"; +import { mkdir, writeFile } from "node:fs/promises"; +import { retryRm } from "../helpers/cleanup.ts"; import { join } from "node:path"; import { loadConfig, findProjectRoot, getRuneDir, validateConfig } from "../../src/core/config.ts"; import { ConfigError } from "../../src/cli/errors.ts"; @@ -12,7 +13,7 @@ beforeEach(async () => { }); afterEach(async () => { - await rm(TMP_DIR, { recursive: true, force: true }); + await retryRm(TMP_DIR); }); describe("findProjectRoot", () => { @@ -188,7 +189,7 @@ describe("mergeConfig 保留 metadata", () => { expect(config.metadata!.command).toBe("bunx @lanyuanxiaoyao/rune"); expect(config.stages.discuss!.prompt).toBe("自定义讨论"); } finally { - await rm(tmpDir, { recursive: true, force: true }); + await retryRm(tmpDir); } }); diff --git a/tests/core/scanner.test.ts b/tests/core/scanner.test.ts index 5b92a83..a5ac916 100644 --- a/tests/core/scanner.test.ts +++ b/tests/core/scanner.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect, beforeEach, afterEach } from "bun:test"; -import { mkdir, writeFile, rm } from "node:fs/promises"; +import { mkdir, writeFile } from "node:fs/promises"; +import { retryRm } from "../helpers/cleanup.ts"; import { join } from "node:path"; import { scanChanges, scanArchives } from "../../src/core/scanner.ts"; import type { RuneConfig } from "../../src/types.ts"; @@ -11,7 +12,7 @@ beforeEach(async () => { }); afterEach(async () => { - await rm(TMP_DIR, { recursive: true, force: true }); + await retryRm(TMP_DIR); }); describe("scanChanges", () => { diff --git a/tests/helpers/cleanup.test.ts b/tests/helpers/cleanup.test.ts index e9c0860..8303595 100644 --- a/tests/helpers/cleanup.test.ts +++ b/tests/helpers/cleanup.test.ts @@ -1,6 +1,7 @@ -import { describe, it, expect, mock, beforeEach, afterEach } from "bun:test"; -import { mkdir, access, rm } from "node:fs/promises"; +import { describe, it, expect } from "bun:test"; +import { mkdir, access } from "node:fs/promises"; import { join } from "node:path"; +import { retryRm } from "./cleanup.ts"; const TMP_DIR = join(import.meta.dir, "__tmp_cleanup_test__"); @@ -14,106 +15,54 @@ async function dirExists(path: string): Promise { } describe("retryRm", () => { - describe("正常删除", () => { - afterEach(async () => { - if (await dirExists(TMP_DIR)) { - await rm(TMP_DIR, { recursive: true, force: true }); + it("删除存在的目录", async () => { + await mkdir(TMP_DIR, { recursive: true }); + await retryRm(TMP_DIR); + expect(await dirExists(TMP_DIR)).toBe(false); + }); + + it("删除不存在的路径不报错(force:true)", async () => { + const nonExist = join(import.meta.dir, "__non_exist__"); + await expect(retryRm(nonExist)).resolves.toBeUndefined(); + }); + + it("EBUSY 后重试成功", async () => { + let callCount = 0; + const fakeRm = async () => { + callCount++; + if (callCount === 1) { + const err: any = new Error("EBUSY"); + err.code = "EBUSY"; + throw err; } - }); - - it("删除存在的目录", async () => { - const { retryRm } = await import("./cleanup.ts"); - await mkdir(TMP_DIR, { recursive: true }); - await retryRm(TMP_DIR); - expect(await dirExists(TMP_DIR)).toBe(false); - }); - - it("删除不存在的路径不报错(force:true)", async () => { - const { retryRm } = await import("./cleanup.ts"); - const nonExist = join(import.meta.dir, "__non_exist__"); - await expect(retryRm(nonExist)).resolves.toBeUndefined(); - }); + }; + await retryRm("/some/path", { maxRetries: 3, baseDelay: 10, _rm: fakeRm as any }); + expect(callCount).toBe(2); }); - describe("EBUSY 重试后成功", () => { - let callCount: number; - - beforeEach(() => { - callCount = 0; - mock.module("node:fs/promises", () => ({ - rm: mock(async () => { - callCount++; - if (callCount === 1) { - const err: any = new Error("EBUSY"); - err.code = "EBUSY"; - throw err; - } - }), - })); - }); - - afterEach(() => { - mock.restore(); - }); - - it("EBUSY 后重试成功", async () => { - const { retryRm } = await import("./cleanup.ts"); - await retryRm("/some/path", { maxRetries: 3, baseDelay: 10 }); - expect(callCount).toBe(2); - }); + it("maxRetries 耗尽后抛出最后一个错误", async () => { + let callCount = 0; + const fakeRm = async () => { + callCount++; + const err: any = new Error("EBUSY"); + err.code = "EBUSY"; + throw err; + }; + await expect( + retryRm("/some/path", { maxRetries: 2, baseDelay: 10, _rm: fakeRm as any }), + ).rejects.toThrow("EBUSY"); + expect(callCount).toBe(3); }); - describe("重试耗尽后抛出错误", () => { - let callCount: number; - - beforeEach(() => { - callCount = 0; - mock.module("node:fs/promises", () => ({ - rm: mock(async () => { - callCount++; - const err: any = new Error("EBUSY"); - err.code = "EBUSY"; - throw err; - }), - })); - }); - - afterEach(() => { - mock.restore(); - }); - - it("maxRetries 耗尽后抛出最后一个错误", async () => { - const { retryRm } = await import("./cleanup.ts"); - await expect(retryRm("/some/path", { maxRetries: 2, baseDelay: 10 })).rejects.toThrow( - "EBUSY", - ); - expect(callCount).toBe(3); - }); - }); - - describe("非 EBUSY/EPERM 错误不重试", () => { - let callCount: number; - - beforeEach(() => { - callCount = 0; - mock.module("node:fs/promises", () => ({ - rm: mock(async () => { - callCount++; - const err: any = new Error("ENOENT: no such file or directory"); - err.code = "ENOENT"; - throw err; - }), - })); - }); - - afterEach(() => { - mock.restore(); - }); - - it("ENOENT 错误直接抛出不重试", async () => { - const { retryRm } = await import("./cleanup.ts"); - await expect(retryRm("/some/path")).rejects.toThrow("ENOENT"); - expect(callCount).toBe(1); - }); + it("ENOENT 错误直接抛出不重试", async () => { + let callCount = 0; + const fakeRm = async () => { + callCount++; + const err: any = new Error("ENOENT: no such file or directory"); + err.code = "ENOENT"; + throw err; + }; + await expect(retryRm("/some/path", { _rm: fakeRm as any })).rejects.toThrow("ENOENT"); + expect(callCount).toBe(1); }); }); diff --git a/tests/helpers/cleanup.ts b/tests/helpers/cleanup.ts index 891e187..c9fc989 100644 --- a/tests/helpers/cleanup.ts +++ b/tests/helpers/cleanup.ts @@ -1,14 +1,18 @@ -import { rm } from "node:fs/promises"; +import { rm as fsRm } from "node:fs/promises"; const RETRY_CODES = new Set(["EBUSY", "EPERM"]); export async function retryRm( path: string, - { maxRetries = 5, baseDelay = 100 }: { maxRetries?: number; baseDelay?: number } = {}, + { + maxRetries = 5, + baseDelay = 100, + _rm = fsRm, + }: { maxRetries?: number; baseDelay?: number; _rm?: typeof fsRm } = {}, ): Promise { for (let attempt = 0; attempt <= maxRetries; attempt++) { try { - await rm(path, { recursive: true, force: true }); + await _rm(path, { recursive: true, force: true }); return; } catch (err: any) { if (!RETRY_CODES.has(err.code) || attempt === maxRetries) { diff --git a/tests/integration/flow.test.ts b/tests/integration/flow.test.ts index 82dac69..16dac87 100644 --- a/tests/integration/flow.test.ts +++ b/tests/integration/flow.test.ts @@ -1,6 +1,7 @@ import { describe, it, expect, beforeEach, afterEach } from "bun:test"; import { existsSync } from "node:fs"; -import { mkdir, writeFile, rm, rename } from "node:fs/promises"; +import { mkdir, writeFile, rename } from "node:fs/promises"; +import { retryRm } from "../helpers/cleanup.ts"; import { join } from "node:path"; import { runInit } from "../../src/commands/init.ts"; import { loadConfig, getChangeDir, getArchiveDir } from "../../src/core/config.ts"; @@ -20,7 +21,7 @@ beforeEach(async () => { }); afterEach(async () => { - await rm(TMP_DIR, { recursive: true, force: true }); + await retryRm(TMP_DIR); }); describe("完整 SDD 流程", () => {