From 61f14f116cb72bb06928089440ad898db061bb95 Mon Sep 17 00:00:00 2001 From: EMRG Evolution Date: Sat, 22 Aug 2026 05:55:00 +0000 Subject: [PATCH] =?UTF-8?q?emrg:=20fix=20GUI=20conn-manager=20test=20flake?= =?UTF-8?q?=20=E2=80=94=20teardown=20leaked=20managers/timers=20between=20?= =?UTF-8?q?tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Agent.md | 4 ++-- emrg/gui/conn-manager.js | 24 ++++++++++++++++++++++++ emrg/gui/test/conn-manager.test.js | 17 +++++++++++++++++ 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/Agent.md b/Agent.md index 31028552..3125fb76 100644 --- a/Agent.md +++ b/Agent.md @@ -85,7 +85,7 @@ Usage: say "tool loop" for the whole process, "round N" for a single LLM request - Streaming chat with delta rendering (16ms batching), markdown on done (marked + DOMPurify + local highlight.js subset), tool call status cards (2000-char truncation + expand) - Session list/switch/new/delete + right-click rename (context menu, #423) synced with daemon; own-stream busy lock (G65); broadcast streams from other clients tagged "来自其他客户端" - Disconnect/reconnect: red status dot, auto daemon respawn (stale-port detection), session resume, input bar restored on disconnect (no 30s fake-timeout) - - Unit tests `npm test` (259: 45 daemon_client + 19 conn-manager + 22 app-commands + 130 renderer smoke + 15 i18n + 8 integration + 3 commands + 8 build-config + 7 gui-state + 2 tool-group); RESPONSE_TYPES mirror daemon protocol verified against `daemon.py` + - Unit tests `npm test` (259: 45 daemon_client + 20 conn-manager + 22 app-commands + 129 renderer smoke + 15 i18n + 8 integration + 3 commands + 8 build-config + 7 gui-state + 2 tool-group); RESPONSE_TYPES mirror daemon protocol verified against `daemon.py` - **Scheduled tasks** — Task generalization + CRUD (rant 2026-08-12T18:23:15, #709/#710/#711) - Task handler generalized: `TaskHandler` (renamed from `EvolutionHandler`), repo-configured self-heal for any project, template lookup builtin → `~/.emrg/task-templates/.md` → fallback - Daemon commands: `task_create/update/delete` + `task_template_create/list/update/delete` (tasks stored in `~/.emrg/tasks.yml`, custom type templates in `~/.emrg/task-templates/`) @@ -119,7 +119,7 @@ pkill -f "emrg.server"; rm -f ~/.emrg/emrgd.token; python -m emrg ``` Python: `uv run pytest tests/ -v` (1003) — import check: `uv run python -c "from emrg.client.app import run_client"` -GUI: `cd emrg/gui && npm test` (259: 45 daemon_client + 19 conn-manager + 22 app-commands + 130 renderer smoke + 15 i18n + 8 integration + 3 commands + 8 build-config + 7 gui-state + 2 tool-group) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js` +GUI: `cd emrg/gui && npm test` (259: 45 daemon_client + 20 conn-manager + 22 app-commands + 129 renderer smoke + 15 i18n + 8 integration + 3 commands + 8 build-config + 7 gui-state + 2 tool-group) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js` CI: `uv run pytest` (ubuntu + **windows-2025 matrix** — Windows pytest 回归在 PR CI 即失败,v0.2.29 教训 #725) + GUI tests + **actionlint workflow lint** (`rhysd/actionlint@v1.7.12` gate, #444 — workflow 解析错误在 PR CI 即失败,如 `if:` secrets 上下文) Re-trigger: `scripts/re-trigger-ci.sh [branch]` (workflow_dispatch, #527 — 替代空 commit 重触发:Actions outage 会整段丢弃 push 事件,dispatch 走 API 路径不受影响) diff --git a/emrg/gui/conn-manager.js b/emrg/gui/conn-manager.js index 4bac0280..d7260dc9 100644 --- a/emrg/gui/conn-manager.js +++ b/emrg/gui/conn-manager.js @@ -26,6 +26,11 @@ const { DaemonClient } = require("./daemon_client.js"); class ConnManager { + // 存活实例登记:destroyAll() 供测试 afterEach / 应用退出时统一清理 + // (清空退避定时器 + 关闭全部连接),防止 unref 的退避定时器跨测试泄漏 + // 干扰后续用例(conn-manager.test.js onRecovered 偶发 cancelledByParent 根因)。 + static _instances = new Set(); + constructor({ logger = console, isPackaged = false, restartWindowMs = 1000, singleRetryDelayMs = 1000 } = {}) { this.logger = logger; this.isPackaged = isPackaged; @@ -40,6 +45,7 @@ class ConnManager { this._recovering = false; // 恢复中守卫(防 close→disconnect→recoverAll 递归) this._singleRetryDelayMs = singleRetryDelayMs; this._singleRetries = new Map(); // sid -> timer(单连接独立退避在途) + ConnManager._instances.add(this); } // 确保 daemon 已运行(connManager = daemon 生命周期唯一 owner)。 @@ -143,6 +149,24 @@ class ConnManager { } } + // destroy():释放本实例全部资源——清空单连接退避定时器(防 unref 定时器 + // 在实例生命周期结束后仍触发重连)、清断连记录、关闭全部连接、清空钩子。 + // 供测试 afterEach / 应用退出(main.js)调用。 + destroy() { + for (const t of this._singleRetries.values()) clearTimeout(t); + this._singleRetries.clear(); + this._disconnects.clear(); + this.closeAll(); + this._openHooks.clear(); + this._recoverHooks.clear(); + ConnManager._instances.delete(this); + } + + // 批量销毁所有存活实例(测试 teardown 用:无论用例如何退出都能兜底清理)。 + static destroyAll() { + for (const m of [...ConnManager._instances]) m.destroy(); + } + // 新会话连接建立钩子(main.js 挂 renderer 事件桥;recoverAll 重开路径同样触发) onOpen(callback) { this._openHooks.add(callback); diff --git a/emrg/gui/test/conn-manager.test.js b/emrg/gui/test/conn-manager.test.js index a53b3b8d..2a89d6bb 100644 --- a/emrg/gui/test/conn-manager.test.js +++ b/emrg/gui/test/conn-manager.test.js @@ -129,6 +129,7 @@ beforeEach(() => { currentMockWs = null; }); afterEach(() => { + ConnManager.destroyAll(); // 清空泄漏实例(退避定时器 + 连接)——onRecovered flake 根因 teardownTempHome(); currentMockWs = null; }); @@ -521,3 +522,19 @@ test("P2 onRecovered 钩子: recoverAll 完成后触发(main.js 刷新 UI 状 assert.strictEqual(reopened, 1); assert.strictEqual(recovered, 1, "onRecovered must fire after recoverAll completes"); }); + +test("P6 destroy: 清空退避定时器 + 关闭全部连接(防 unref 定时器泄漏跨测试干扰)", async () => { + const manager = new ConnManager({ singleRetryDelayMs: 200 }); + const s1 = await driveOpen(manager, "sess-1", "/proj/a"); + const s2 = await driveOpen(manager, "sess-2", "/proj/b"); + // 只断 s1(s2 仍连)→ 非重启 → 单连接退避,200ms 定时器 pending + s1.sessionWs.emit("close"); + await new Promise((r) => setTimeout(r, 30)); + assert.strictEqual(manager._singleRetries.size, 1, "backoff timer scheduled"); + const before = currentMockWs; // s2.sessionWs(最后一个创建的 ws) + manager.destroy(); + assert.strictEqual(manager._singleRetries.size, 0, "destroy clears backoff timers"); + assert.deepStrictEqual(manager.all(), [], "destroy closes all conns"); + await new Promise((r) => setTimeout(r, 250)); + assert.strictEqual(currentMockWs, before, "no stray connection created after destroy"); +});