From beff558028902ce540d4b2f75edcf4e8ce7031dc Mon Sep 17 00:00:00 2001 From: ficowang Date: Sun, 11 Oct 2026 09:04:50 +0800 Subject: [PATCH 1/3] =?UTF-8?q?test(remote-agent):=20groupRelay=20?= =?UTF-8?q?=E7=94=A8=E4=BE=8B=E6=B8=85=E7=90=86=E4=B8=B4=E6=97=B6=E7=9B=AE?= =?UTF-8?q?=E5=BD=95=E6=97=B6=E9=87=8D=E8=AF=95=EF=BC=8C=E9=81=BF=E5=85=8D?= =?UTF-8?q?=E4=B8=8E=E5=90=8E=E5=8F=B0=E5=86=99=E5=85=A5=E7=AB=9E=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 供应商组中转测试的 host 会异步写入 runsRoot(运行目录与 guest-sessions.json 的 tmp+rename),用例结束时仍在落盘的写入会让 afterEach 的 rmSync 报 ENOTEMPTY, 使本已通过的用例失败(CI 中已在两个无关 PR 上复现)。清理改用 rmSync 自带的 重试(maxRetries/retryDelay),不改任何断言。 Signed-off-by: ficowang --- .../src/main/remote-agent/__tests__/groupRelay.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/main/remote-agent/__tests__/groupRelay.test.ts b/apps/desktop/src/main/remote-agent/__tests__/groupRelay.test.ts index 38f7e65db38..2c1b0c9615e 100644 --- a/apps/desktop/src/main/remote-agent/__tests__/groupRelay.test.ts +++ b/apps/desktop/src/main/remote-agent/__tests__/groupRelay.test.ts @@ -206,7 +206,10 @@ beforeEach(() => { }); afterEach(() => { - fs.rmSync(root, { recursive: true, force: true }); + // The hosts persist run directories and guest-session state asynchronously (tmp + rename); + // a write that is still landing when the test ends makes rmdir see ENOTEMPTY. Retry the + // cleanup instead of failing an otherwise passing test (Node retries ENOTEMPTY/EBUSY/EPERM). + fs.rmSync(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 }); }); let runSeq = 0; From c6f9aaaec0cec96208c83d274ee0883bff58111b Mon Sep 17 00:00:00 2001 From: ficowang Date: Sun, 11 Oct 2026 10:02:50 +0800 Subject: [PATCH 2/3] =?UTF-8?q?test(remote-agent):=20groupRelay=20?= =?UTF-8?q?=E7=94=A8=E4=BE=8B=E5=BC=82=E6=AD=A5=E6=B8=85=E7=90=86=E5=B9=B6?= =?UTF-8?q?=E7=AD=89=E5=BE=85=E7=9B=AE=E5=BD=95=E4=B8=8D=E5=86=8D=E8=A2=AB?= =?UTF-8?q?=E9=87=8D=E5=BB=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit rmSync 带重试只能避开 ENOTEMPTY,却会阻塞事件循环;删除若抢在后台 写入之前完成,写入落地后又会重建临时目录并遗留在 tmpdir。改为异步 删除、让在途写入完成,确认目录在静默窗口内不再出现才结束;反复被 重建则直接报错,不再悄悄遗留。 Signed-off-by: ficowang --- .../remote-agent/__tests__/groupRelay.test.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src/main/remote-agent/__tests__/groupRelay.test.ts b/apps/desktop/src/main/remote-agent/__tests__/groupRelay.test.ts index 2c1b0c9615e..3cd1fe114e9 100644 --- a/apps/desktop/src/main/remote-agent/__tests__/groupRelay.test.ts +++ b/apps/desktop/src/main/remote-agent/__tests__/groupRelay.test.ts @@ -9,6 +9,7 @@ import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; +import { setTimeout as delay } from 'node:timers/promises'; import { REMOTE_AGENT_MAX_RUNS_PER_CONTROLLER } from '@cindy/device-link'; import type { AgentEvent, AgentSessionHandle } from '@cindy/maker-core'; @@ -205,11 +206,18 @@ beforeEach(() => { root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'cindy-group-relay-'))); }); -afterEach(() => { - // The hosts persist run directories and guest-session state asynchronously (tmp + rename); - // a write that is still landing when the test ends makes rmdir see ENOTEMPTY. Retry the - // cleanup instead of failing an otherwise passing test (Node retries ENOTEMPTY/EBUSY/EPERM). - fs.rmSync(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 }); +afterEach(async () => { + // The hosts persist run directories and guest-session state in fire-and-forget promises + // (mkdir + tmp + rename), so a write can still be landing when a test ends: a one-shot rmdir + // sees ENOTEMPTY, and a removal that wins the race is undone when the write recreates `root`. + // Remove asynchronously so those writes can finish, and only stop once `root` stays gone for a + // quiet window; if something keeps recreating it, fail instead of leaking the directory. + for (let attempt = 0; attempt < 20; attempt += 1) { + await fs.promises.rm(root, { recursive: true, force: true, maxRetries: 3, retryDelay: 20 }); + await delay(50); + if (!fs.existsSync(root)) return; + } + throw new Error(`test directory ${root} kept being recreated after cleanup`); }); let runSeq = 0; From edf31311af0309981d6b320d365602a84548100b Mon Sep 17 00:00:00 2001 From: ficowang Date: Sun, 11 Oct 2026 11:02:04 +0800 Subject: [PATCH 3/3] =?UTF-8?q?test(remote-agent):=20groupRelay=20?= =?UTF-8?q?=E6=B8=85=E7=90=86=E5=9C=A8=E5=88=A0=E9=99=A4=E5=A4=B1=E8=B4=A5?= =?UTF-8?q?=E6=97=B6=E7=BB=A7=E7=BB=AD=E9=87=8D=E8=AF=95=EF=BC=8C=E5=B9=B6?= =?UTF-8?q?=E8=A6=81=E6=B1=82=E8=BF=9E=E7=BB=AD=E4=B8=A4=E4=B8=AA=E9=9D=99?= =?UTF-8?q?=E9=BB=98=E7=AA=97=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit rm 用尽内部重试后 reject 会直接让 hook 失败、外层循环不再继续;改为 吞掉单次失败交给外层循环重试。退出条件从一次检查改为连续两个 50ms 窗口内目录都不存在,降低慢速写入在判定之后才重建目录的概率。 Signed-off-by: ficowang --- .../main/remote-agent/__tests__/groupRelay.test.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/main/remote-agent/__tests__/groupRelay.test.ts b/apps/desktop/src/main/remote-agent/__tests__/groupRelay.test.ts index 3cd1fe114e9..7cfb37fc404 100644 --- a/apps/desktop/src/main/remote-agent/__tests__/groupRelay.test.ts +++ b/apps/desktop/src/main/remote-agent/__tests__/groupRelay.test.ts @@ -210,14 +210,16 @@ afterEach(async () => { // The hosts persist run directories and guest-session state in fire-and-forget promises // (mkdir + tmp + rename), so a write can still be landing when a test ends: a one-shot rmdir // sees ENOTEMPTY, and a removal that wins the race is undone when the write recreates `root`. - // Remove asynchronously so those writes can finish, and only stop once `root` stays gone for a - // quiet window; if something keeps recreating it, fail instead of leaking the directory. - for (let attempt = 0; attempt < 20; attempt += 1) { - await fs.promises.rm(root, { recursive: true, force: true, maxRetries: 3, retryDelay: 20 }); + // Remove asynchronously so those writes can finish, and only stop once `root` has stayed gone + // across two consecutive quiet windows; a removal that still hits ENOTEMPTY is retried by the + // loop rather than failing the hook. If something keeps recreating it, fail instead of leaking. + let absentChecks = 0; + for (let attempt = 0; attempt < 40 && absentChecks < 2; attempt += 1) { + await fs.promises.rm(root, { recursive: true, force: true, maxRetries: 3, retryDelay: 20 }).catch(() => undefined); await delay(50); - if (!fs.existsSync(root)) return; + absentChecks = fs.existsSync(root) ? 0 : absentChecks + 1; } - throw new Error(`test directory ${root} kept being recreated after cleanup`); + if (absentChecks < 2) throw new Error(`test directory ${root} kept being recreated after cleanup`); }); let runSeq = 0;