From d98394b10fba3919ef835b07d2a6f7b5ce44186b Mon Sep 17 00:00:00 2001 From: EMRG Evolution Date: Thu, 20 Aug 2026 14:46:13 +0800 Subject: [PATCH] =?UTF-8?q?emrg:=20daemon=20auth=20credentials=20file=20em?= =?UTF-8?q?rgd.port=20=E2=86=92=20emrgd.token=20=E2=80=94=20single-line=20?= =?UTF-8?q?token=20only,=20port=20constant=20everywhere=20(rant=202026-08-?= =?UTF-8?q?20T14:32:52)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Agent.md | 4 +- DEVELOPMENT.md | 2 +- emrg/_stop_all.py | 16 ++--- emrg/connect.py | 34 +++++----- emrg/gui/daemon_client.js | 102 +++++++++++++++------------- emrg/gui/test/conn-manager.test.js | 6 +- emrg/gui/test/daemon_client.test.js | 94 ++++++++++++------------- emrg/gui/test/integration.test.js | 10 +-- emrg/server/atomic.py | 2 +- emrg/server/daemon.py | 36 +++++----- emrg/server/scheduler.py | 10 +-- packaging/smoke-test.sh | 19 +++--- tests/conftest.py | 2 +- tests/test_atomic.py | 26 +++---- tests/test_connect.py | 32 ++++----- tests/test_daemon.py | 69 +++++++++---------- tests/test_daemon_manager.py | 48 ++++++------- tests/test_daemon_manager_e2e.py | 2 +- tests/test_installer_stop.py | 2 +- tests/test_scheduler.py | 6 +- tests/test_ws_e2e.py | 6 +- 21 files changed, 268 insertions(+), 260 deletions(-) diff --git a/Agent.md b/Agent.md index 6df5a7b5..18c56578 100644 --- a/Agent.md +++ b/Agent.md @@ -13,7 +13,7 @@ EMRG is a self-evolving AI agent architecture experiment. Python implementation, - `__main__.py` — CLI entry (`emrg`, `emrg server`, `emrg rant`, `emrg update`) - `protocol.py` — Communication protocol (TaskRequest, TaskResponse, ToolStart, ToolEnd, ServerPong, EvolutionLog, InstanceIdentity) - `config.py` — Config loading (`~/.emrg/config.toml`, Python 3.11+ tomllib) - - `connect.py` — IPC connection (WebSocket over TCP loopback, token auth via `emrgd.port`) + - `connect.py` — IPC connection (WebSocket over TCP loopback, token auth via `emrgd.token`) - `memory.py` — Memory system (ProjectMemoryStore, SessionMemoryStore, MemoryFile, MemoryIndex) - `session.py` — Session management (Session CRUD, history persistence, compact/clear) - `emrg/server/` — Server (WebSocket daemon, EMRG's living core) @@ -115,7 +115,7 @@ Community needs voiced in HN agent-UI discussions map directly to EMRG's design: ## Test Commands ```bash -pkill -f "emrg.server"; rm -f ~/.emrg/emrgd.port; python -m emrg +pkill -f "emrg.server"; rm -f ~/.emrg/emrgd.token; python -m emrg ``` Python: `uv run pytest tests/ -v` (988) — import check: `uv run python -c "from emrg.client.app import run_client"` diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 1aa2493f..c8427e67 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -103,7 +103,7 @@ vision = true ┌─────────────┐ WebSocket (ws://) ┌──────────────┐ │ emrg TUI │ ◄─────────────────────► │ emrgd │ │ (client) │ TCP loopback + auth │ (daemon) │ -│ │ token (emrgd.port) │ │ +│ │ token (emrgd.token) │ │ │ • Chat │ │ • LLM loop │ │ • Markdown │ │ • Tools │ │ • ToolCards│ │ • Evolution │ diff --git a/emrg/_stop_all.py b/emrg/_stop_all.py index 34c24a9c..0b7a4518 100644 --- a/emrg/_stop_all.py +++ b/emrg/_stop_all.py @@ -329,17 +329,17 @@ def ws_graceful_shutdown(port: int, token: str, timeout: float = 3.0) -> bool: def stop_daemon() -> None: """Stop the daemon: ws shutdown → pid file → SIGTERM/taskkill /F → poll. - Also removes ``~/.emrg/emrgd.port`` once the daemon pid is confirmed dead + Also removes ``~/.emrg/emrgd.token`` once the daemon pid is confirmed dead (the daemon itself removes it on graceful shutdown; a force-killed daemon cannot, so we clean it up — the next daemon start re-asserts both files). """ - port_path = config_dir() / "emrgd.port" + token_path = config_dir() / "emrgd.token" # Fixed-port shutdown (rant 2026-08-19T08:05:21): the daemon always - # listens on _EMRGD_PORT; the port file only supplies the auth token. If - # the file is missing/stale, fall through to the pid + cmdline paths. + # listens on _EMRGD_PORT; the token file only supplies the auth token + # (single line, rant 2026-08-20T14:32:52). If the file is missing/stale, + # fall through to the pid + cmdline paths. try: - text = port_path.read_text(encoding="utf-8").split() - token = text[1] if len(text) == 2 else "" + token = token_path.read_text(encoding="utf-8").strip() except (OSError, ValueError): token = "" if token and ws_graceful_shutdown(_EMRGD_PORT, token): @@ -371,13 +371,13 @@ def stop_daemon() -> None: for pid in _scan_windows_python_emrg(os.getpid()): _kill_pid_windows(pid) - # Port file cleanup: the daemon removes it on graceful shutdown; a + # Token file cleanup: the daemon removes it on graceful shutdown; a # force-killed daemon cannot, so remove it once the pid is confirmed gone # (the next daemon start re-asserts both files). daemon_gone = pid is None or not _pid_alive(pid) if daemon_gone: try: - port_path.unlink() + token_path.unlink() except OSError: pass diff --git a/emrg/connect.py b/emrg/connect.py index 4d74e9c8..4b6a52db 100644 --- a/emrg/connect.py +++ b/emrg/connect.py @@ -8,8 +8,8 @@ The daemon listens on the fixed port ``127.0.0.1:EMRGD_PORT`` (56031, rant 2026-08-19T08:05:21 — fixed-port bind exclusivity is the single-instance -admission) and writes its auth token to ``~/.emrg/emrgd.port`` -(``port\\n token``, mode 0o600). Clients read that file for the token, and +admission) and writes its auth token to ``~/.emrg/emrgd.token`` +(single-line token, mode 0o600). Clients read that file for the token, and connect to the fixed port. Clients then send a first-frame auth message; the daemon confirms with ``auth_ok`` before the @@ -33,13 +33,13 @@ logger = logging.getLogger(__name__) # ── Connection identifier ─────────────────────────────────────── -# Port/token file lives at ~/.emrg/emrgd.port (port\n token, mode 0o600) +# Auth token file lives at ~/.emrg/emrgd.token (single-line token, 0o600) CONNECT_ID = "emrgd" # Fixed daemon port (host rant 2026-08-19T08:05:21): the daemon binds a FIXED # loopback port so kernel-level bind exclusivity (EADDRINUSE) is the single- -# instance admission — no PID file to forge/delete, no race window. The port -# file still carries the auth token; the port itself is now a constant. +# instance admission — no PID file to forge/delete, no race window. The +# token file only carries the auth token; the port itself is a constant. # Keep in sync with emrg._stop_all._EMRGD_PORT (that module is pure stdlib). EMRGD_PORT = 56031 @@ -53,17 +53,19 @@ class AuthError(Exception): def get_server_path() -> str: - """Return the path of the daemon port/token file.""" - return str(config_dir() / f"{CONNECT_ID}.port") + """Return the path of the daemon auth token file.""" + return str(config_dir() / f"{CONNECT_ID}.token") async def connect_to_server(): """Connect to the emrgd server over WebSocket. - Reads the auth token from ``~/.emrg/emrgd.port``, connects to the FIXED - daemon port ``ws://127.0.0.1:`` (rant 2026-08-19T08:05:21 — - the port is a constant; the file only carries the token), sends the - first-frame auth message and waits for the ``auth_ok`` confirmation. + Reads the auth token from ``~/.emrg/emrgd.token`` (single line, rant + 2026-08-20T14:32:52 — the file carries ONLY the token), connects to the + FIXED daemon port ``ws://127.0.0.1:`` (rant + 2026-08-19T08:05:21 — the port is a constant; the file only carries the + token), sends the first-frame auth message and waits for the + ``auth_ok`` confirmation. Returns the connected WebSocket object (single ws — no ``(reader, writer)`` tuple anymore). @@ -72,7 +74,7 @@ async def connect_to_server(): ConnectionRefusedError / OSError / FileNotFoundError: daemon not running. """ port_path = Path(get_server_path()) - _, token = port_path.read_text(encoding="utf-8").split() + token = port_path.read_text(encoding="utf-8").strip() # proxy=None: loopback connections must never go through a system proxy. # websockets 17 defaults proxy=True and reads the OS proxy settings — when a # Windows system proxy is enabled (e.g. 10.10.0.28:6501 for HN/Reddit access), @@ -100,19 +102,19 @@ async def connect_to_server(): def cleanup_server() -> None: - """Remove the daemon port/token file on shutdown.""" + """Remove the daemon auth token file on shutdown.""" port_path = Path(get_server_path()) if port_path.exists(): port_path.unlink() - logger.debug("removed port file: %s", port_path) + logger.debug("removed token file: %s", port_path) def is_server_running_sync(timeout: float = 2.0) -> bool: """Synchronous health-check probe (for client startup). Blocking TCP connect to the FIXED daemon port ``127.0.0.1:EMRGD_PORT`` - (rant 2026-08-19T08:05:21). No port-file read: the fixed port is the - ground truth, so a missing/stale ``emrgd.port`` never makes the probe + (rant 2026-08-19T08:05:21). No token-file read: the fixed port is the + ground truth, so a missing/stale ``emrgd.token`` never makes the probe report "not running" while a daemon is actually alive (the dual-instance root cause). Real auth happens on the first frame of a real connection. """ diff --git a/emrg/gui/daemon_client.js b/emrg/gui/daemon_client.js index 3ccfc270..eb738bd5 100644 --- a/emrg/gui/daemon_client.js +++ b/emrg/gui/daemon_client.js @@ -3,7 +3,7 @@ * daemon_client.js — main 进程内唯一与 emrgd 通信的模块。 * * 协议语义完全对照 emrg/client/daemon_manager.py(Phase 2 参考实现): - * - 读 ~/.emrg/emrgd.port(port\n token,0o600)→ ws://127.0.0.1: → auth 首帧 → auth_ok + * - 读 ~/.emrg/emrgd.token(单行 token,0o600)→ ws://127.0.0.1:56031(EMRGD_PORT 常量)→ auth 首帧 → auth_ok * - 坏 JSON 帧忽略(对照 daemon_manager.recv R53) * - ConnectionClosed 传播 → 触发重连(对照 R11) * - auth 失败(auth_ok 前 close)= AuthError 语义(G88:停止自动重试,防无限重连) @@ -18,22 +18,27 @@ const crypto = require("crypto"); const { spawn } = require("child_process"); const WebSocket = require("ws"); -// G129 (rant 2026-08-09T08:03:46): PORT_FILE 必须接受 projectDir——硬编码 +// G129 (rant 2026-08-09T08:03:46): TOKEN_FILE 必须接受 projectDir——硬编码 // os.homedir() 时,Windows 测试的 setupTempHome() 只设 HOME 不设 USERPROFILE, -// os.homedir() 仍读 USERPROFILE(真实用户目录)→ 测试把假 port/token 写进 -// 真实的 ~/.emrg/emrgd.port → 演化周期 10 小时连不上 daemon(WinError 1225)。 +// os.homedir() 仍读 USERPROFILE(真实用户目录)→ 测试把假 token 写进 +// 真实的 ~/.emrg/emrgd.token → 演化周期 10 小时连不上 daemon(WinError 1225)。 // 所有调用点必须传 this.projectDir(默认 os.homedir() 保持生产行为不变)。 -const PORT_FILE = (projectDir = os.homedir()) => path.join(projectDir, ".emrg", "emrgd.port"); +const TOKEN_FILE = (projectDir = os.homedir()) => path.join(projectDir, ".emrg", "emrgd.token"); const EMRGD_LOG = (projectDir = os.homedir()) => path.join(projectDir, ".emrg", "emrgd.log"); // Rant 2026-08-09T18:47:37(GUI 连不上 daemon 回归):daemon 的规范运行时目录永远是 // ~/.emrg(daemon.py config_dir() = Path.home()/".emrg";connect.py 无条件读 -// ~/.emrg/emrgd.port)。GUI 的 projectDir 若被 config gui.project_dir 指向别处 -// (非 home),按 projectDir 读 port/pid/log 全部落空 → 误判 daemon 不存在 → +// ~/.emrg/emrgd.token)。GUI 的 projectDir 若被 config gui.project_dir 指向别处 +// (非 home),按 projectDir 读 token/pid/log 全部落空 → 误判 daemon 不存在 → // 反复 spawn 撞 PID 锁 → "failed to start after 3 attempts" 假错误,而真 daemon 一直活着。 // 规范位置常量:作为 projectDir 读取失败时的权威回退。 -const HOME_PORT_FILE = () => path.join(os.homedir(), ".emrg", "emrgd.port"); +const HOME_TOKEN_FILE = () => path.join(os.homedir(), ".emrg", "emrgd.token"); const HOME_PID_FILE = () => path.join(os.homedir(), ".emrg", "emrgd.pid"); const HOME_EMRGD_LOG = () => path.join(os.homedir(), ".emrg", "emrgd.log"); +// Fixed daemon port (rant 2026-08-19T08:05:21 + 2026-08-20T14:32:52): the +// daemon always listens on this constant — keep in sync with emrg/connect.py +// EMRGD_PORT and emrg/_stop_all.py _EMRGD_PORT. The token file no longer +// carries a port; all connections/probes use this constant. +const EMRGD_PORT = 56031; const MAX_PAYLOAD = 16 * 1024 * 1024; // G62/G105:16MB 双向一致(工具输出上限 200KB) const AUTH_TIMEOUT_MS = 10_000; const SPAWN_WAIT_MS = 5_000; @@ -120,38 +125,39 @@ class DaemonClient { // ── 生命周期 ──────────────────────────────────────────── - // Rant 2026-08-09T18:47:37:读 port/token 的权威入口。先试 projectDir(G129 语义), - // 缺失/畸形时回退 daemon 规范位置 ~/.emrg。返回 {port, token, source} 或 null。 + // Rant 2026-08-09T18:47:37:读 token 的权威入口。先试 projectDir(G129 语义), + // 缺失/畸形时回退 daemon 规范位置 ~/.emrg。文件仅含单行 token(rant + // 2026-08-20T14:32:52);端口一律用 EMRGD_PORT 常量。返回 {token, source, port} 或 null。 _readPortToken() { const tryRead = (file) => { try { const text = fs.readFileSync(file, "utf8"); - const [port, token] = text.split(/\s+/); - if (port && token) return { port, token }; + const token = text.trim(); + if (token) return { token }; } catch { /* missing/unreadable → try next */ } return null; }; - const project = tryRead(PORT_FILE(this.projectDir)); - if (project) return { ...project, source: "projectDir" }; - const home = tryRead(HOME_PORT_FILE()); + const project = tryRead(TOKEN_FILE(this.projectDir)); + if (project) return { ...project, source: "projectDir", port: EMRGD_PORT }; + const home = tryRead(HOME_TOKEN_FILE()); if (home) { this.logger.warn( - `[gui] port file not found at projectDir (${PORT_FILE(this.projectDir)}) — ` + - `reusing canonical ~/.emrg/emrgd.port (port=${home.port})` + `[gui] token file not found at projectDir (${TOKEN_FILE(this.projectDir)}) — ` + + `reusing canonical ~/.emrg/emrgd.token` ); - return { ...home, source: "home" }; + return { ...home, source: "home", port: EMRGD_PORT }; } return null; } isRunning(timeoutMs = 1500) { - // G43/G90:TCP 探测(不可简化为 port 文件存在)。18:47:37:port 源改为权威读取 + // G43/G90:TCP 探测(不可简化为 token 文件存在)。18:47:37:token 源改为权威读取 // (projectDir 回退 ~/.emrg),否则 projectDir≠home 时永远探测假路径 → 假 false。 + // 14:32:52:端口用常量 EMRGD_PORT,不再从文件读 port。 const pt = this._readPortToken(); if (!pt) return Promise.resolve(false); - const port = Number(pt.port); return new Promise((resolve) => { - const sock = net.connect({ host: "127.0.0.1", port, timeout: timeoutMs }); + const sock = net.connect({ host: "127.0.0.1", port: EMRGD_PORT, timeout: timeoutMs }); sock.once("connect", () => { sock.destroy(); resolve(true); }); sock.once("error", () => { sock.destroy(); resolve(false); }); sock.once("timeout", () => { sock.destroy(); resolve(false); }); @@ -283,31 +289,31 @@ class DaemonClient { } // Rant 2026-08-09T18:47:37(A1 + B1):探测"已存在的 daemon"——4 状态诊断日志 - // (port_file_exists / port_file_content / daemon_alive(ping) / spawn_result)。 - // spawn 失败 ≠ daemon 不存在:GUI 可能因 projectDir≠home 读错 port 文件, - // 或 daemon 早已被 scheduler/TUI 拉起。返回 {port, token} 或 null。 + // (token_file_exists / token_file_content / daemon_alive(ping) / spawn_result)。 + // spawn 失败 ≠ daemon 不存在:GUI 可能因 projectDir≠home 读错 token 文件, + // 或 daemon 早已被 scheduler/TUI 拉起。返回 {token, source, port} 或 null。 async _probeExistingDaemon(spawnResult = "n/a") { const pt = this._readPortToken(); - const portFileExists = !!(pt || this._readPortTokenRaw()); + const tokenFileExists = !!(pt || this._readPortTokenRaw()); const alive = pt ? await this.isRunning(1000) : false; this.logger.info( - `[gui] probe: port_file_exists=${portFileExists}, port_file_content=${pt ? pt.port : "—"}, ` + + `[gui] probe: token_file_exists=${tokenFileExists}, token_file_content=${pt ? "present" : "—"}, ` + `daemon_alive(ping)=${alive}, spawn_result=${spawnResult}` ); if (pt && alive) return pt; return null; } - // 读 port 文件原始存在性(不含解析),供 probe 日志用。 + // 读 token 文件原始存在性(不含解析),供 probe 日志用。 _readPortTokenRaw() { - for (const file of [PORT_FILE(this.projectDir), HOME_PORT_FILE()]) { + for (const file of [TOKEN_FILE(this.projectDir), HOME_TOKEN_FILE()]) { try { if (fs.readFileSync(file, "utf8").trim()) return true; } catch { /* next */ } } return false; } // Rant 2026-08-09T18:47:37(A1):spawn 失败(含 3 次节流)→ 探测已有 daemon → - // 活着直接复用;确实无 daemon 才抛原始错误。spawn 成功则读回 port/token。 + // 活着直接复用;确实无 daemon 才抛原始错误。spawn 成功则读回 token。 async _spawnOrProbe() { try { await this.startDaemon(); @@ -315,23 +321,23 @@ class DaemonClient { const existing = await this._probeExistingDaemon(`failed(${String(spawnErr.message).slice(0, 60)})`); if (existing) { this.logger.warn( - `[gui] spawn failed (${spawnErr.message}) — existing daemon detected at port=${existing.port}, reusing` + `[gui] spawn failed (${spawnErr.message}) — existing daemon detected at port=${EMRGD_PORT}, reusing` ); return existing; } this.logger.warn(`[gui] spawn failed (${spawnErr.message}) — no existing daemon reachable, giving up`); throw spawnErr; } - // spawn 成功:daemon 永远写规范 ~/.emrg/emrgd.port(daemon.py config_dir()), + // spawn 成功:daemon 永远写规范 ~/.emrg/emrgd.token(daemon.py config_dir()), // 用权威读取(projectDir 回退 home),不假设 projectDir==home。 const pt = this._readPortToken(); - if (!pt) throw new Error("port file not written after spawn"); - this.logger.info(`[gui] daemon spawned ok: port=${pt.port}`); + if (!pt) throw new Error("token file not written after spawn"); + this.logger.info(`[gui] daemon spawned ok: port=${EMRGD_PORT}`); return pt; } async ensureConnected({ skipStart = false } = {}) { - // Rant 2026-08-09T18:47:37:1. 读 port 文件(projectDir → 规范 ~/.emrg 回退)→ + // Rant 2026-08-09T18:47:37:1. 读 token 文件(projectDir → 规范 ~/.emrg 回退)→ // 无则拉 daemon;spawn 失败先探测已有 daemon,活着直接复用,不再盲报 // "failed to start after 3 attempts"。每步打结构化诊断日志(B1-B5)。 // P2 connManager(rant 2026-08-10T15:07:19):skipStart=true 时 daemon 生命周期 @@ -342,23 +348,23 @@ class DaemonClient { port = pt.port; token = pt.token; this.logger.info( - `[gui] ensureConnected: port_file_exists=true, port_file_content=${port}, source=${pt.source}` + `[gui] ensureConnected: token_file_exists=true, port=${port}, source=${pt.source}` ); } else { if (skipStart) { throw new Error( - `daemon not running (skipStart): no port file at ${PORT_FILE(this.projectDir)}` + `daemon not running (skipStart): no token file at ${TOKEN_FILE(this.projectDir)}` ); } - this.logger.info(`[gui] ensureConnected: port_file_exists=false — spawning daemon`); + this.logger.info(`[gui] ensureConnected: token_file_exists=false — spawning daemon`); const r = await this._spawnOrProbe(); port = r.port; token = r.token; } - // 2. ws 连接(G43 stale port:连接失败删文件重拉一次) + // 2. ws 连接(G43 stale token:连接失败删文件重拉一次) try { - this.ws = new WebSocket(`ws://127.0.0.1:${port}`, { maxPayload: MAX_PAYLOAD }); + this.ws = new WebSocket(`ws://127.0.0.1:${EMRGD_PORT}`, { maxPayload: MAX_PAYLOAD }); } catch (e) { // ws 构造一般异步失败;在 open 事件处理 throw e; @@ -366,31 +372,31 @@ class DaemonClient { try { await this._awaitOpen(); } catch (e) { - // G43 加固(rant 2026-08-09T13:16:36 根因):port 文件存在但连不上时, - // 先查 emrgd.pid —— daemon 进程还活着就【绝不删 port 文件】。旧 G43 直接 - // unlink 会把健康 daemon 的 port 文件删掉 → 僵尸态(daemon 活着、scheduler + // G43 加固(rant 2026-08-09T13:16:36 根因):token 文件存在但连不上时, + // 先查 emrgd.pid —— daemon 进程还活着就【绝不删 token 文件】。旧 G43 直接 + // unlink 会把健康 daemon 的 token 文件删掉 → 僵尸态(daemon 活着、scheduler // 永远 cannot connect、PID 锁挡住新 spawn)。只有 daemon 真死了才删+重拉。 if (this._daemonProcessAlive()) { this.logger.warn( - `[gui] ws connect failed: ${e.message} — daemon pid alive, keeping port file (transient)` + `[gui] ws connect failed: ${e.message} — daemon pid alive, keeping token file (transient)` ); try { this.ws.close(); } catch { /* ignore */ } throw new Error(`daemon unreachable (pid alive): ${e.message}`); } if (skipStart) { this.logger.warn( - `[gui] ws connect failed: ${e.message} — stale port, daemon dead (skipStart: not respawning)` + `[gui] ws connect failed: ${e.message} — stale token, daemon dead (skipStart: not respawning)` ); try { this.ws.close(); } catch { /* ignore */ } throw new Error(`daemon unreachable (skipStart): ${e.message}`); } - this.logger.warn(`[gui] ws connect failed: ${e.message} — stale port, respawning daemon`); + this.logger.warn(`[gui] ws connect failed: ${e.message} — stale token, respawning daemon`); try { this.ws.close(); } catch { /* ignore */ } - try { fs.unlinkSync(PORT_FILE(this.projectDir)); } catch { /* ignore */ } + try { fs.unlinkSync(TOKEN_FILE(this.projectDir)); } catch { /* ignore */ } const r = await this._spawnOrProbe(); port = r.port; token = r.token; - this.ws = new WebSocket(`ws://127.0.0.1:${port}`, { maxPayload: MAX_PAYLOAD }); + this.ws = new WebSocket(`ws://127.0.0.1:${EMRGD_PORT}`, { maxPayload: MAX_PAYLOAD }); await this._awaitOpen(); } @@ -781,4 +787,4 @@ function generateSessionId(seed) { return sid; } -module.exports = { DaemonClient, generateSessionId, PORT_FILE, SESSION_ID_RE, MAX_PAYLOAD }; +module.exports = { DaemonClient, generateSessionId, TOKEN_FILE, SESSION_ID_RE, MAX_PAYLOAD, EMRGD_PORT }; diff --git a/emrg/gui/test/conn-manager.test.js b/emrg/gui/test/conn-manager.test.js index 0b75aa59..f9fde515 100644 --- a/emrg/gui/test/conn-manager.test.js +++ b/emrg/gui/test/conn-manager.test.js @@ -58,12 +58,12 @@ function setupTempHome() { process.env.HOME = tmpHome; process.env.USERPROFILE = tmpHome; // 预写 port 文件(模拟已运行 daemon)——路径必须落在 tmpHome 内 - const portFile = path.join(tmpHome, ".emrg", "emrgd.port"); + const portFile = path.join(tmpHome, ".emrg", "emrgd.token"); assert.ok( path.resolve(portFile).startsWith(path.resolve(tmpHome) + path.sep), "port file escapes tmpHome", ); - fs.writeFileSync(portFile, "41234\nseekrit-token"); + fs.writeFileSync(portFile, "seekrit-token"); } function teardownTempHome() { @@ -142,7 +142,7 @@ test("P2 open: 引导 daemon + skipStart 会话连接 + resume_session 自动订 assert.strictEqual(manager.get("sess-1"), conn); assert.deepStrictEqual(manager.all(), ["sess-1"]); // 会话连接 url = 预写 port 文件端口(daemon 未重启) - assert.strictEqual(sessionWs.url, "ws://127.0.0.1:41234"); + assert.strictEqual(sessionWs.url, "ws://127.0.0.1:56031"); }); test("P2 open: 已打开 sid → 复用连接,不重复 resume_session", async () => { diff --git a/emrg/gui/test/daemon_client.test.js b/emrg/gui/test/daemon_client.test.js index c65b873f..cd9674d6 100644 --- a/emrg/gui/test/daemon_client.test.js +++ b/emrg/gui/test/daemon_client.test.js @@ -4,7 +4,7 @@ * 零依赖:mock ws(Module._load 注入)+ 临时 HOME + 无真实 daemon。 * * 覆盖(设计文档 §6.1): - * - ensureConnected:port 文件读取 + auth 首帧 + auth_ok 处理 + * - ensureConnected:token 文件读取 + auth 首帧 + auth_ok 处理 * - 坏 JSON 帧 → 忽略不崩 * - ws close → 触发 disconnected 事件(重连回调由 main 层调度) * - sendTask:payload(type=task + session_id + prompt + images + id,无 stream 字段——非 stream 路径已删) @@ -60,21 +60,21 @@ Module._load = function (request, parent, isMain) { if (request === "ws") return MockWs; return origLoad.apply(this, arguments); }; -const { DaemonClient, generateSessionId, PORT_FILE } = require("../daemon_client.js"); +const { DaemonClient, generateSessionId, TOKEN_FILE, EMRGD_PORT } = require("../daemon_client.js"); let tmpHome = null; let origHome = null; let origUserProfile = null; -// G129 (rant 2026-08-09T08:03:46): 测试隔离守卫——写 port 文件前断言目标路径 +// G129 (rant 2026-08-09T08:03:46): 测试隔离守卫——写 token 文件前断言目标路径 // 位于临时目录内。Windows 上 Node os.homedir() 优先读 USERPROFILE(HOME 无效), -// 若无此守卫,PORT_FILE() 会解析到真实 ~/.emrg/emrgd.port,测试假值 -// ("41234\nseekrit-token") 会覆盖真实 daemon 端口文件 → 演化周期 10h 连不上。 -function assertPortFileInTmp(portFile) { - const resolved = path.resolve(portFile); +// 若无此守卫,TOKEN_FILE() 会解析到真实 ~/.emrg/emrgd.token,测试假值 +// ("seekrit-token") 会覆盖真实 daemon 认证文件 → 演化周期 10h 连不上。 +function assertTokenFileInTmp(tokenFile) { + const resolved = path.resolve(tokenFile); const tmpResolved = path.resolve(tmpHome); assert.ok( resolved.startsWith(tmpResolved + path.sep), - `port file ${resolved} escapes tmpHome ${tmpResolved} — refusing to write`, + `token file ${resolved} escapes tmpHome ${tmpResolved} — refusing to write`, ); } @@ -87,10 +87,10 @@ function setupTempHome() { // G129: Windows os.homedir() 读 USERPROFILE —— 必须一并重定向,否则 // os.homedir() 仍返回真实用户目录(这是 10h daemon gap 的直接根因)。 process.env.USERPROFILE = tmpHome; - // 预写 port 文件(模拟已运行 daemon)—— 路径必须落在 tmpHome 内 - const portFile = PORT_FILE(tmpHome); - assertPortFileInTmp(portFile); - fs.writeFileSync(portFile, "41234\nseekrit-token"); + // 预写 token 文件(模拟已运行 daemon)—— 路径必须落在 tmpHome 内 + const tokenFile = TOKEN_FILE(tmpHome); + assertTokenFileInTmp(tokenFile); + fs.writeFileSync(tokenFile, "seekrit-token"); } function teardownTempHome() { @@ -143,21 +143,21 @@ afterEach(() => { currentMockWs = null; }); -test("ensureConnected: port 文件读取 + auth 首帧 + auth_ok", async () => { +test("ensureConnected: token 文件读取 + auth 首帧 + auth_ok", async () => { const client = new DaemonClient({ projectDir: tmpHome }); await connectClient(client); assert.strictEqual(client.connected, true); assert.strictEqual(client._authFailed, false); }); -test("ensureConnected: port 文件缺失 → 拉起 daemon(spawn 参数正确 G28/G68/G125)", async () => { - fs.rmSync(PORT_FILE(tmpHome), { force: true }); +test("ensureConnected: token 文件缺失 → 拉起 daemon(spawn 参数正确 G28/G68/G125)", async () => { + fs.rmSync(TOKEN_FILE(tmpHome), { force: true }); const client = new DaemonClient({ projectDir: tmpHome }); - // stub startDaemon:模拟拉起后写 port 文件 + // stub startDaemon:模拟拉起后写 token 文件 let spawnCalls = null; client.startDaemon = async function () { spawnCalls = { python: this._findPython(), projectDir: this.projectDir }; - fs.writeFileSync(PORT_FILE(tmpHome), "41235\nseekrit-token"); + fs.writeFileSync(TOKEN_FILE(tmpHome), "seekrit-token"); }; await connectClient(client); assert.ok(spawnCalls, "startDaemon should be called"); @@ -259,8 +259,8 @@ test("P2 deltaBatchMs: error 终态 → 冲刷残留 delta 再发 error", async assert.strictEqual(seen.find(([t]) => t === "message_delta")[1].chunks.length, 1); }); -test("P2 skipStart: port 文件缺失 → 抛错不拉起 daemon(connManager 独占 daemon 生命周期)", async () => { - fs.rmSync(PORT_FILE(tmpHome), { force: true }); +test("P2 skipStart: token 文件缺失 → 抛错不拉起 daemon(connManager 独占 daemon 生命周期)", async () => { + fs.rmSync(TOKEN_FILE(tmpHome), { force: true }); const client = new DaemonClient({ projectDir: tmpHome }); let spawnCalls = 0; client.startDaemon = async function () { @@ -269,15 +269,15 @@ test("P2 skipStart: port 文件缺失 → 抛错不拉起 daemon(connManager }; await assert.rejects( () => client.ensureConnected({ skipStart: true }), - /daemon not running \(skipStart\): no port file/ + /daemon not running \(skipStart\): no token file/ ); assert.strictEqual(spawnCalls, 0, "startDaemon must never be called"); assert.strictEqual(client.connected, false); }); test("P2 skipStart: stale port + daemon 已死 → 抛错不重拉(不删文件不 spawn)", async () => { - // 预写 port 文件指向一个无人监听的端口(连接必然失败) - fs.writeFileSync(PORT_FILE(tmpHome), "1\nseekrit-token"); // 127.0.0.1:1 拒绝连接 + // 预写 token 文件(连接用常量端口,必然失败场景由 ws error 模拟) + fs.writeFileSync(TOKEN_FILE(tmpHome), "seekrit-token"); // 127.0.0.1:1 拒绝连接 const client = new DaemonClient({ projectDir: tmpHome }); // daemon 进程已死(无 pid 文件)→ 旧路径会删文件重拉;skipStart 必须拒绝 let spawnCalls = 0; @@ -291,8 +291,8 @@ test("P2 skipStart: stale port + daemon 已死 → 抛错不重拉(不删文 firstWs.emit("error", new Error("connect ECONNREFUSED")); await assert.rejects(p, /daemon unreachable \(skipStart\)/); assert.strictEqual(spawnCalls, 0, "startDaemon must never be called"); - // port 文件保留(connManager 重启恢复依赖它判断 daemon 状态) - assert.ok(fs.existsSync(PORT_FILE(tmpHome)), "port file must be kept"); + // token 文件保留(connManager 重启恢复依赖它判断 daemon 状态) + assert.ok(fs.existsSync(TOKEN_FILE(tmpHome)), "port file must be kept"); assert.strictEqual(client.connected, false); }); @@ -353,7 +353,7 @@ test("G43 stale port: 连接失败(port 文件存在但拒绝)→ 删文件 let respawned = false; client.startDaemon = async function () { respawned = true; - fs.writeFileSync(PORT_FILE(tmpHome), "41236\nseekrit-token"); + fs.writeFileSync(TOKEN_FILE(tmpHome), "seekrit-token"); }; const p = client.ensureConnected(); await waitForWs(); @@ -362,8 +362,8 @@ test("G43 stale port: 连接失败(port 文件存在但拒绝)→ 删文件 // 重拉后创建第二个 ws → open → auth → auth_ok await waitForWs(() => currentMockWs !== firstWs); assert.ok(respawned, "startDaemon should respawn after stale port"); - assert.strictEqual(fs.existsSync(PORT_FILE(tmpHome)), true); - assert.strictEqual(currentMockWs.url, "ws://127.0.0.1:41236"); + assert.strictEqual(fs.existsSync(TOKEN_FILE(tmpHome)), true); + assert.strictEqual(currentMockWs.url, "ws://127.0.0.1:" + EMRGD_PORT); currentMockWs.emit("open"); await waitForAuthSent(currentMockWs); currentMockWs.emit("message", Buffer.from(JSON.stringify({ type: "auth_ok" }))); @@ -375,8 +375,8 @@ test("rant 13:16:36 G43 加固:daemon 进程活着 → ws 失败不删 port const client = new DaemonClient({ projectDir: tmpHome }); // 写入 emrgd.pid(当前进程 = 活着) fs.writeFileSync(path.join(tmpHome, ".emrg", "emrgd.pid"), String(process.pid)); - const portFile = PORT_FILE(tmpHome); - fs.writeFileSync(portFile, "41237\nseekrit-token"); + const tokenFile = TOKEN_FILE(tmpHome); + fs.writeFileSync(tokenFile, "seekrit-token"); assert.strictEqual(client._daemonProcessAlive(), true, "pid alive → true"); let respawned = false; @@ -387,7 +387,7 @@ test("rant 13:16:36 G43 加固:daemon 进程活着 → ws 失败不删 port firstWs.emit("error", new Error("connect ECONNREFUSED")); // 守卫路径:不删文件、不重拉,直接抛"daemon unreachable (pid alive)" await assert.rejects(p, /daemon unreachable \(pid alive\)/); - assert.strictEqual(fs.existsSync(portFile), true, "port 文件必须保留(daemon 还活着)"); + assert.strictEqual(fs.existsSync(tokenFile), true, "token 文件必须保留(daemon 还活着)"); assert.strictEqual(respawned, false, "pid 活着 → 不重拉 daemon(防风暴)"); }); @@ -400,7 +400,7 @@ test("rant 13:16:36 G43 加固:daemon 真死了(pid 不存在)→ 仍删 let respawned = false; client.startDaemon = async function () { respawned = true; - fs.writeFileSync(PORT_FILE(tmpHome), "41238\nseekrit-token"); + fs.writeFileSync(TOKEN_FILE(tmpHome), "seekrit-token"); }; const p = client.ensureConnected(); await waitForWs(); @@ -454,7 +454,7 @@ test("rant 13:16:36 ⑤ spawn 节流计数在成功连接后归零", async () => client.isRunning = async () => false; await assert.rejects(client.startDaemon(), /emrgd failed to start within timeout/); assert.strictEqual(client._spawnAttempts, 1); - // 恢复真实 startDaemon(port 文件已预写 → ensureConnected 直接 ws → auth_ok) + // 恢复真实 startDaemon(token 文件已预写 → ensureConnected 直接 ws → auth_ok) delete client.startDaemon; const p = client.ensureConnected(); await waitForWs(); @@ -839,8 +839,8 @@ test("isRunning:TCP 探测(G43/G90)", async () => { } finally { net.connect = origConnect; } - // port 文件缺失 → false - fs.rmSync(PORT_FILE(tmpHome), { force: true }); + // token 文件缺失 → false + fs.rmSync(TOKEN_FILE(tmpHome), { force: true }); assert.strictEqual(await client.isRunning(), false); }); @@ -861,31 +861,31 @@ test("断连 pending 请求全部 reject + disconnected(G89)", async () => { // ── Rant 2026-08-09T18:47:37(GUI 连不上 daemon 回归)────────────────── test("18:47:37: projectDir port 文件缺失 → 回退规范 ~/.emrg(home)→ 不 spawn 直接连接", async () => { - // 宿主场景:config gui.project_dir 指向非 home 目录 → projectDir/.emrg 无 port 文件, - // 真 daemon 写在 ~/.emrg/emrgd.port(setupTempHome 已预写 41234)。 + // 宿主场景:config gui.project_dir 指向非 home 目录 → projectDir/.emrg 无 token 文件, + // 真 daemon 写在 ~/.emrg/emrgd.token(setupTempHome 已预写 seekrit-token)。 const elsewhere = path.join(tmpHome, "elsewhere"); fs.mkdirSync(path.join(elsewhere, ".emrg"), { recursive: true }); const client = new DaemonClient({ projectDir: elsewhere }); let spawned = false; client.startDaemon = async function () { spawned = true; }; await connectClient(client); - assert.strictEqual(spawned, false, "home 有 port 文件 → 必须复用,不 spawn"); + assert.strictEqual(spawned, false, "home 有 token 文件 → 必须复用,不 spawn"); assert.strictEqual(client.connected, true); - assert.strictEqual(currentMockWs.url, "ws://127.0.0.1:41234", "连接 canonical home port"); + assert.strictEqual(currentMockWs.url, "ws://127.0.0.1:" + EMRGD_PORT, "连接 canonical home port"); }); test("18:47:37: stale projectDir port + spawn 节流失败 → probe 复用 canonical home daemon", async () => { - // 宿主场景变体:projectDir 有 STALE port 文件(ws 连不上),真 daemon 在 home。 + // 宿主场景变体:projectDir 有 STALE token 文件(ws 连不上),真 daemon 在 home。 // ws 失败 → 删 projectDir stale 文件 → spawn 节流抛错 → probe 发现 home 活着 → 复用。 const elsewhere = path.join(tmpHome, "elsewhere"); fs.mkdirSync(path.join(elsewhere, ".emrg"), { recursive: true }); - fs.writeFileSync(PORT_FILE(elsewhere), "41299\nstale-token"); // stale:无 daemon 监听 + fs.writeFileSync(TOKEN_FILE(elsewhere), "stale-token"); // stale:无 daemon 监听 const client = new DaemonClient({ projectDir: elsewhere }); // spawn 命中节流(正是宿主看到的假错误 "after 3 attempts") client.startDaemon = async function () { throw new Error("daemon failed to start after 3 attempts — please start it manually"); }; - // TCP 探测:home port 文件指向的 41234 "可连接"(模拟真 daemon 在跑) + // TCP 探测:常量端口 56031 "可连接"(模拟真 daemon 在跑) client.isRunning = async () => true; // 捕获日志 → 断言 4 状态诊断字段齐全(B1/B3) const logs = []; @@ -893,10 +893,10 @@ test("18:47:37: stale projectDir port + spawn 节流失败 → probe 复用 cano const p = client.ensureConnected(); await waitForWs(); const firstWs = currentMockWs; - firstWs.emit("error", new Error("connect ECONNREFUSED")); // 41299 拒绝 - // probe 复用 → 新 ws 到 canonical home 41234 → open → auth → auth_ok + firstWs.emit("error", new Error("connect ECONNREFUSED")); // 拒绝 + // probe 复用 → 新 ws 到常量端口 56031 → open → auth → auth_ok await waitForWs(() => currentMockWs !== firstWs); - assert.strictEqual(currentMockWs.url, "ws://127.0.0.1:41234", "复用 canonical home port"); + assert.strictEqual(currentMockWs.url, "ws://127.0.0.1:" + EMRGD_PORT, "复用 canonical home port"); currentMockWs.emit("open"); await waitForAuthSent(currentMockWs); currentMockWs.emit("message", Buffer.from(JSON.stringify({ type: "auth_ok" }))); @@ -904,12 +904,12 @@ test("18:47:37: stale projectDir port + spawn 节流失败 → probe 复用 cano assert.strictEqual(client.connected, true, "probe 到已有 daemon → 直接连接"); const probeLine = logs.find((l) => l.includes("probe:")); assert.ok(probeLine, "必须输出 probe 诊断日志"); - assert.match(probeLine, /port_file_exists=/); - assert.match(probeLine, /port_file_content=/); + assert.match(probeLine, /token_file_exists=/); + assert.match(probeLine, /token_file_content=/); assert.match(probeLine, /daemon_alive\(ping\)=/); assert.match(probeLine, /spawn_result=/); assert.match(probeLine, /failed\(daemon failed to start after 3 attempts/); - assert.ok(logs.some((l) => l.includes("existing daemon detected at port=41234, reusing")), "复用日志"); + assert.ok(logs.some((l) => l.includes("existing daemon detected at port=" + EMRGD_PORT + ", reusing")), "复用日志"); }); // ── P2 自有流锁(G65 每连接独立;rant 15:07:19)────────────────────────── diff --git a/emrg/gui/test/integration.test.js b/emrg/gui/test/integration.test.js index a8cff0a7..49a7aea9 100644 --- a/emrg/gui/test/integration.test.js +++ b/emrg/gui/test/integration.test.js @@ -26,7 +26,7 @@ if (SKIP) { skip("EMRG_SKIP_INTEGRATION=1 — 集成测试跳过(本地运行)"); } -const { DaemonClient, generateSessionId, PORT_FILE } = require("../daemon_client.js"); +const { DaemonClient, generateSessionId, TOKEN_FILE } = require("../daemon_client.js"); // ── 环境 ───────────────────────────────────────────────── const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "emrg-gui-integ-")); @@ -69,10 +69,10 @@ function waitForPortFile(timeoutMs = 15000) { return new Promise((resolve, reject) => { const check = () => { try { - const text = fs.readFileSync(PORT_FILE(tmp), "utf8"); + const text = fs.readFileSync(TOKEN_FILE(tmp), "utf8"); if (text && text.trim()) return resolve(text); } catch { /* not yet */ } - if (Date.now() > deadline) return reject(new Error("daemon port file timeout")); + if (Date.now() > deadline) return reject(new Error("daemon token file timeout")); setTimeout(check, 200); }; check(); @@ -195,14 +195,14 @@ test("delete_session(不存在)→ 错误帧(无破坏性)", { skip: SKI }); test("daemon 被杀 → ensureConnected 重连(G43 stale port 流程)", { skip: SKIP }, async () => { - // 杀 daemon(不删 port 文件)→ 连接应失败 → stale 检测 → 删文件重拉 + // 杀 daemon(不删 token 文件)→ 连接应失败 → stale 检测 → 删文件重拉 killProcessTree(daemonProc.pid); await new Promise((r) => setTimeout(r, 800)); assert.strictEqual(client.connected, false, "daemon killed → disconnected"); // ensureConnected 应自动重拉(G43 stale port) await client.ensureConnected(); assert.strictEqual(client.connected, true); - assert.ok(fs.existsSync(PORT_FILE(tmp)), "new port file written"); + assert.ok(fs.existsSync(TOKEN_FILE(tmp)), "new token file written"); // 重连后可继续通信 const frame = await client.sendCommandAndWait("list_sessions", { cwd: tmp }, 5000); assert.strictEqual(frame.type, "sessions_list"); diff --git a/emrg/server/atomic.py b/emrg/server/atomic.py index ab5ff53d..608f8480 100644 --- a/emrg/server/atomic.py +++ b/emrg/server/atomic.py @@ -66,7 +66,7 @@ def atomic_write_bytes( Writes to a temp file in the same directory, then chmod + os.replace() to atomically swap. On error, the temp file is cleaned up. - Used for the daemon port/token file (``emrgd.port``) where mode 0o600 + Used for the daemon auth token file (``emrgd.token``) where mode 0o600 is required (token must not leak to other local users). """ target.parent.mkdir(parents=True, exist_ok=True) diff --git a/emrg/server/daemon.py b/emrg/server/daemon.py index 8a60e3ef..d2a68b93 100644 --- a/emrg/server/daemon.py +++ b/emrg/server/daemon.py @@ -351,23 +351,23 @@ async def serve(self) -> None: ) port = EMRGD_PORT self._auth_token = secrets.token_urlsafe(32) - self._assert_port_file(port) - # Rant 2026-08-09T18:47:37 B4:启动完成一行自证——pid/port/port 文件路径/写入成功, + self._assert_token_file() + # Rant 2026-08-09T18:47:37 B4:启动完成一行自证——pid/port/token 文件路径/写入成功, # 宿主拿到 emrgd.log 就知道 daemon 到底起没起、写没写对文件。 logger.info( - "emrgd listening on 127.0.0.1:%d | identity=%s | pid=%d | port_file=%s | port_file_written_ok=%s", + "emrgd listening on 127.0.0.1:%d | identity=%s | pid=%d | token_file=%s | token_file_written_ok=%s", port, self.identity.instance_id[:8], os.getpid(), - config_dir() / "emrgd.port", - (config_dir() / "emrgd.port").exists(), + config_dir() / "emrgd.token", + (config_dir() / "emrgd.token").exists(), ) # Rant 2026-08-09T13:16:36 root-cause self-heal: G43 stale-port logic - # deleted a healthy daemon's emrgd.port after a transient ws failure → + # deleted a healthy daemon's emrgd.token after a transient ws failure → # the daemon's OWN scheduler lost the file (93× "cannot connect") while # GUI respawns hit the PID lock and exited. The daemon re-asserts its - # port file periodically so any external deletion self-heals. + # token file periodically so any external deletion self-heals. self._port_keepalive_task = asyncio.create_task(self._port_keepalive_loop()) self._scheduler = TaskScheduler(self.identity) @@ -486,33 +486,33 @@ async def _shutdown_all(self, pid_file: Path) -> None: ) async def _port_keepalive_loop(self) -> None: - """Re-assert the port file if it was deleted or overwritten. + """Re-assert the token file if it was deleted or overwritten. Rant 2026-08-09T13:16:36 root cause: a client's stale-port unlink - (G43) can remove a healthy daemon's emrgd.port after one transient + (G43) can remove a healthy daemon's emrgd.token after one transient ws failure. The daemon's own scheduler reads that file to reconnect, so it then fails forever while the PID lock blocks new spawns — the zombie state behind the Windows v0.2.15 storm. Re-writing the file every 60s makes the daemon self-healing. """ - port_path = config_dir() / "emrgd.port" + token_path = config_dir() / "emrgd.token" while self._running: await asyncio.sleep(60) try: - if not port_path.exists(): - port = self._server.sockets[0].getsockname()[1] - self._assert_port_file(port) + if not token_path.exists(): + self._assert_token_file() logger.warning( - "emrgd.port was missing — re-asserted (external deletion?)" + "emrgd.token was missing — re-asserted (external deletion?)" ) except (OSError, IndexError, AttributeError): pass - def _assert_port_file(self, port: int) -> None: - """(Re)write the port/token file for the current listener.""" + def _assert_token_file(self) -> None: + """(Re)write the auth token file for the current daemon (single-line + token, mode 0o600 — rant 2026-08-20T14:32:52: emrgd.port → emrgd.token).""" atomic_write_bytes( - f"{port}\n{self._auth_token}", - config_dir() / "emrgd.port", + self._auth_token, + config_dir() / "emrgd.token", mode=0o600, ) diff --git a/emrg/server/scheduler.py b/emrg/server/scheduler.py index 0796436b..8c3b0c97 100644 --- a/emrg/server/scheduler.py +++ b/emrg/server/scheduler.py @@ -243,7 +243,7 @@ def __init__( # The flag is persisted to disk (~/.emrg/saturation/.json) so # it survives daemon restarts. # G129: 连续连接失败告警阈值——达到后升级为 ERROR(防静默吞掉, - # rant 2026-08-09T08:03:46:GUI 测试覆盖真实 emrgd.port 致 10h 连不上)。 + # rant 2026-08-09T08:03:46:GUI 测试覆盖真实 emrgd.token 致 10h 连不上)。 self._CONNECT_FAIL_ALERT = 3 self._connect_failures = 0 self._saturation_dir = config_dir() / "saturation" @@ -568,7 +568,7 @@ def _connect_backoff(self) -> float: """Exponential backoff while the daemon is unreachable. Rant 2026-08-09T13:16:36 (v0.2.15 Windows regression): when the - daemon is down (emrgd.port missing), every tick's connect failure + daemon is down (emrgd.token missing), every tick's connect failure returned immediately and the loop re-ran at full interval — with multiple handlers that produced a per-second retry/window storm. Backoff = max(30s, interval * 2^n) capped at 10 minutes, where n @@ -655,11 +655,11 @@ async def _run_evolution_cycle(self) -> None: self._connect_failures = 0 except (ConnectionRefusedError, FileNotFoundError, OSError) as e: # G129 (rant 2026-08-09T08:03:46): 连接失败不得静默吞掉——GUI 测试曾把 - # 假 port 值写进真实 ~/.emrg/emrgd.port,导致演化周期 10 小时连不上 + # 假 token 值写进真实 ~/.emrg/emrgd.token,导致演化周期 10 小时连不上 # daemon(WinError 1225)只留下 WARNING。累计失败达到阈值后升级为 - # ERROR 告警,提示 port 文件可能被外部覆盖(检查 ~/.emrg/emrgd.port)。 + # ERROR 告警,提示 token 文件可能被外部覆盖(检查 ~/.emrg/emrgd.token)。 self._connect_failures += 1 - port_path = config_dir() / "emrgd.port" + port_path = config_dir() / "emrgd.token" if self._connect_failures >= self._CONNECT_FAIL_ALERT: self._logger.error( "TaskHandler[%s]: cannot connect for %d consecutive cycles " diff --git a/packaging/smoke-test.sh b/packaging/smoke-test.sh index 03d0eef8..d4da3d5e 100755 --- a/packaging/smoke-test.sh +++ b/packaging/smoke-test.sh @@ -68,22 +68,21 @@ else nohup emrgd >"$HOME/.emrg/emrgd-debug.log" 2>&1 & fi for i in $(seq 1 30); do - [ -f "$HOME/.emrg/emrgd.port" ] && break + [ -f "$HOME/.emrg/emrgd.token" ] && break sleep 0.5 done -if [ ! -f "$HOME/.emrg/emrgd.port" ]; then +if [ ! -f "$HOME/.emrg/emrgd.token" ]; then echo " [debug] emrgd-debug.log:" >&2 cat "$HOME/.emrg/emrgd-debug.log" 2>/dev/null || true echo " [debug] emrgd.log (tail):" >&2 tail -20 "$HOME/.emrg/emrgd.log" 2>/dev/null || true - fail "daemon did not write port file" + fail "daemon did not write token file" else - port=$(head -1 "$HOME/.emrg/emrgd.port") - token=$(sed -n 2p "$HOME/.emrg/emrgd.port") - if python3 - "$port" "$token" <<'PYEOF' + token=$(cat "$HOME/.emrg/emrgd.token") + if python3 - "$token" <<'PYEOF' import json, sys from websockets.sync.client import connect -port, token = sys.argv[1], sys.argv[2] +port, token = 56031, sys.argv[1] try: ws = connect(f"ws://127.0.0.1:{port}", open_timeout=3) ws.send(json.dumps({"type": "auth", "token": token})) @@ -104,10 +103,10 @@ fi # 3. 会话持久化核心链路(R79 降级:无 LLM) say "3. session persistence core (no-LLM)" -if [ -f "$HOME/.emrg/emrgd.port" ]; then - if python3 - "$HOME/.emrg/emrgd.port" <<'PYEOF' +if [ -f "$HOME/.emrg/emrgd.token" ]; then + if python3 - "$HOME/.emrg/emrgd.token" <<'PYEOF' import json, sys, websockets.sync.client -port, token = open(sys.argv[1]).read().split() +port, token = 56031, open(sys.argv[1]).read().strip() ws = websockets.sync.client.connect(f"ws://127.0.0.1:{port}", open_timeout=3) ws.send(json.dumps({"type": "auth", "token": token})) json.loads(ws.recv()) diff --git a/tests/conftest.py b/tests/conftest.py index b2b742a8..ae011105 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -18,7 +18,7 @@ This autouse fixture makes any write to the REAL ~/.emrg/projects.yml or ~/.emrg/tasks.yml a hard test failure: the offending test is named immediately instead of the pollution being discovered later (precedent: -#583 assertPortFileInTmp sandbox guard for the emrgd.port file). +#583 assertPortFileInTmp sandbox guard for the emrgd.token file). """ from __future__ import annotations diff --git a/tests/test_atomic.py b/tests/test_atomic.py index e2bf660e..902bf625 100644 --- a/tests/test_atomic.py +++ b/tests/test_atomic.py @@ -72,18 +72,18 @@ def test_atomic_write_no_temp_leak(tmp_path: Path): def test_atomic_write_bytes_basic(tmp_path: Path): """Writes a text blob and reads it back.""" - target = tmp_path / "emrgd.port" - atomic_write_bytes("49152\ns3cret-token", target) + target = tmp_path / "emrgd.token" + atomic_write_bytes("s3cret-token", target) assert target.exists() - assert target.read_text(encoding="utf-8") == "49152\ns3cret-token" + assert target.read_text(encoding="utf-8") == "s3cret-token" @pytest.mark.skipif(sys.platform == "win32", reason="chmod 0600 semantics differ on Windows") def test_atomic_write_bytes_mode_600(tmp_path: Path): """Writes with mode 0o600 by default (token file must be private).""" - target = tmp_path / "emrgd.port" - atomic_write_bytes("49152\ntoken", target) + target = tmp_path / "emrgd.token" + atomic_write_bytes("token", target) mode = stat.S_IMODE(os.stat(str(target)).st_mode) assert mode == 0o600 @@ -91,16 +91,16 @@ def test_atomic_write_bytes_mode_600(tmp_path: Path): def test_atomic_write_bytes_creates_parent_dir(tmp_path: Path): """Creates parent directories if they don't exist.""" - target = tmp_path / "deep" / "nested" / "emrgd.port" - atomic_write_bytes("1\nt", target) + target = tmp_path / "deep" / "nested" / "emrgd.token" + atomic_write_bytes("t", target) assert target.exists() - assert target.read_text(encoding="utf-8") == "1\nt" + assert target.read_text(encoding="utf-8") == "t" def test_atomic_write_bytes_overwrites(tmp_path: Path): """Overwrites existing file atomically.""" - target = tmp_path / "emrgd.port" + target = tmp_path / "emrgd.token" target.write_text("old", encoding="utf-8") atomic_write_bytes("new", target) @@ -111,12 +111,12 @@ def test_atomic_write_bytes_overwrites(tmp_path: Path): def test_atomic_write_bytes_no_temp_leak(tmp_path: Path): """Verifies no temp files remain after write.""" before = set(os.listdir(str(tmp_path))) - target = tmp_path / "emrgd.port" - atomic_write_bytes("1\nt", target) + target = tmp_path / "emrgd.token" + atomic_write_bytes("t", target) after = set(os.listdir(str(tmp_path))) - assert "emrgd.port" in after - assert after - before == {"emrgd.port"} + assert "emrgd.token" in after + assert after - before == {"emrgd.token"} def test_atomic_write_custom_prefix(tmp_path: Path): diff --git a/tests/test_connect.py b/tests/test_connect.py index 26ab5a46..c686d15c 100644 --- a/tests/test_connect.py +++ b/tests/test_connect.py @@ -10,12 +10,12 @@ class TestGetServerPath: """Tests for get_server_path — daemon port/token file path.""" - def test_returns_port_file_path(self, monkeypatch, tmp_path): - """Returns ~/.emrg/emrgd.port in the config dir.""" + def test_returns_token_file_path(self, monkeypatch, tmp_path): + """Returns ~/.emrg/emrgd.token in the config dir.""" monkeypatch.setattr("emrg.connect.config_dir", lambda: tmp_path) result = get_server_path() - expected = str(tmp_path / f"{CONNECT_ID}.port") + expected = str(tmp_path / f"{CONNECT_ID}.token") assert result == expected def test_connect_id_constant(self): @@ -29,29 +29,29 @@ def test_is_exception(self): class TestCleanupServer: - def test_removes_port_file(self, monkeypatch, tmp_path): - """Removes the port file if present.""" + def test_removes_token_file(self, monkeypatch, tmp_path): + """Removes the token file if present.""" monkeypatch.setattr("emrg.connect.config_dir", lambda: tmp_path) - port_file = tmp_path / f"{CONNECT_ID}.port" - port_file.write_text("49152\ntoken", encoding="utf-8") + port_file = tmp_path / f"{CONNECT_ID}.token" + port_file.write_text("token", encoding="utf-8") cleanup_server() assert not port_file.exists() def test_noop_when_absent(self, monkeypatch, tmp_path): - """Does nothing when the port file doesn't exist.""" + """Does nothing when the token file doesn't exist.""" monkeypatch.setattr("emrg.connect.config_dir", lambda: tmp_path) cleanup_server() # must not raise def test_leaves_other_files(self, monkeypatch, tmp_path): - """Only removes the port file, not other config files.""" + """Only removes the token file, not other config files.""" monkeypatch.setattr("emrg.connect.config_dir", lambda: tmp_path) other = tmp_path / "config.toml" other.write_text("x", encoding="utf-8") - port_file = tmp_path / f"{CONNECT_ID}.port" - port_file.write_text("1\nt", encoding="utf-8") + port_file = tmp_path / f"{CONNECT_ID}.token" + port_file.write_text("t", encoding="utf-8") cleanup_server() @@ -60,8 +60,8 @@ def test_leaves_other_files(self, monkeypatch, tmp_path): class TestIsServerRunningSync: - """Probes the FIXED daemon port (rant 2026-08-19T08:05:21) — no port-file - read, so a missing/stale emrgd.port never hides a live daemon.""" + """Probes the FIXED daemon port (rant 2026-08-19T08:05:21) — no token-file + read, so a missing/stale emrgd.token never hides a live daemon.""" def _free_port(self) -> int: """Bind a probe socket to port 0 to get a free port (avoids colliding @@ -134,12 +134,12 @@ async def fake_connect(uri, **kwargs): monkeypatch.setattr(connect_mod, "config_dir", lambda: tmp_path) monkeypatch.setattr(connect_mod, "connect", fake_connect) - (tmp_path / f"{CONNECT_ID}.port").write_text("49152\ntoken", encoding="utf-8") + (tmp_path / f"{CONNECT_ID}.token").write_text("token", encoding="utf-8") asyncio.run(connect_mod.connect_to_server()) - # Fixed daemon port (rant 2026-08-19T08:05:21) — the URI no longer - # depends on the port file's port value, only the token is read from it. + # Fixed daemon port (rant 2026-08-19T08:05:21 + 2026-08-20T14:32:52) — + # the URI no longer depends on the file (single-line token only). assert captured["uri"] == f"ws://127.0.0.1:{connect_mod.EMRGD_PORT}" assert captured["kwargs"]["proxy"] is None assert captured["kwargs"]["max_size"] == 16 * 1024 * 1024 diff --git a/tests/test_daemon.py b/tests/test_daemon.py index 9d373533..16931f40 100644 --- a/tests/test_daemon.py +++ b/tests/test_daemon.py @@ -676,43 +676,43 @@ def test_list_rants_missing_file_returns_empty(tmp_path, monkeypatch): assert frame["rants"] == [] -# ── Port-file self-heal (rant 2026-08-09T13:16:36 root cause) ───────── -# G43 stale-port logic once deleted a healthy daemon's emrgd.port after a +# ── Token-file self-heal (rant 2026-08-09T13:16:36 root cause) ───────── +# G43 stale-port logic once deleted a healthy daemon's emrgd.token after a # transient ws failure → daemon's own scheduler lost the file (93× "cannot # connect") while the PID lock blocked new spawns. The daemon re-asserts -# its port file so any external deletion self-heals. +# its token file so any external deletion self-heals. -def test_assert_port_file_writes_port_and_token(tmp_path, monkeypatch): - """_assert_port_file writes '\\n' with mode 0o600.""" +def test_assert_token_file_writes_token(tmp_path, monkeypatch): + """_assert_token_file writes the single-line token with mode 0o600.""" monkeypatch.setattr("emrg.server.daemon.config_dir", lambda: tmp_path) server = _make_server() server._auth_token = "tok123" - server._assert_port_file(43210) - text = (tmp_path / "emrgd.port").read_text(encoding="utf-8") - assert text == "43210\ntok123" + server._assert_token_file() + text = (tmp_path / "emrgd.token").read_text(encoding="utf-8") + assert text == "tok123" -def test_assert_port_file_rewrites_deleted_file(tmp_path, monkeypatch): - """A deleted port file is re-asserted on the next keepalive tick.""" +def test_assert_token_file_rewrites_deleted_file(tmp_path, monkeypatch): + """A deleted token file is re-asserted on the next keepalive tick.""" monkeypatch.setattr("emrg.server.daemon.config_dir", lambda: tmp_path) server = _make_server() server._auth_token = "tok456" - server._assert_port_file(45678) - port_path = tmp_path / "emrgd.port" - assert port_path.exists() + server._assert_token_file() + token_path = tmp_path / "emrgd.token" + assert token_path.exists() # 外部删除(模拟 G43 unlink 竞态) - port_path.unlink() - assert not port_path.exists() + token_path.unlink() + assert not token_path.exists() # keepalive loop 的恢复逻辑:缺失 → 重新断言 - server._assert_port_file(45678) - text = (tmp_path / "emrgd.port").read_text(encoding="utf-8") - assert text == "45678\ntok456" + server._assert_token_file() + text = (tmp_path / "emrgd.token").read_text(encoding="utf-8") + assert text == "tok456" def test_port_keepalive_loop_restores_missing_file(tmp_path, monkeypatch): - """The keepalive loop re-asserts a deleted port file within one tick.""" + """The keepalive loop re-asserts a deleted token file within one tick.""" import asyncio monkeypatch.setattr("emrg.server.daemon.config_dir", lambda: tmp_path) @@ -720,17 +720,17 @@ def test_port_keepalive_loop_restores_missing_file(tmp_path, monkeypatch): server._auth_token = "tok789" server._server = type("S", (), {"sockets": [type("Sock", (), {"getsockname": lambda self: (None, 9999)})()]})() server._running = True - server._assert_port_file(9999) - port_path = tmp_path / "emrgd.port" - port_path.unlink() + server._assert_token_file() + token_path = tmp_path / "emrgd.token" + token_path.unlink() # 执行与 loop 相同的恢复逻辑(loop 本体 sleep 60s,测试直接驱动检查体) async def one_tick(): - if not port_path.exists(): - server._assert_port_file(9999) + if not token_path.exists(): + server._assert_token_file() asyncio.run(one_tick()) - assert port_path.exists() - assert port_path.read_text(encoding="utf-8") == "9999\ntok789" + assert token_path.exists() + assert token_path.read_text(encoding="utf-8") == "tok789" # ── _redact 日志脱敏(rant 10:21 + 跨项目 base64 教训)────────────── @@ -1628,19 +1628,20 @@ def _boom(port): raise AssertionError("expected OSError(EACCES) to propagate") -def test_assert_port_file_writes_fixed_port(tmp_path): - """_assert_port_file persists the FIXED port (56031) + auth token.""" +def test_assert_token_file_writes_token_only(tmp_path): + """_assert_token_file persists ONLY the auth token (no port — rant + 2026-08-20T14:32:52: emrgd.port → emrgd.token, single-line token).""" from unittest.mock import patch server = _make_server() server._auth_token = "tok-123" with patch("emrg.server.daemon.config_dir", return_value=tmp_path): - server._assert_port_file(56031) - port_file = tmp_path / "emrgd.port" - assert port_file.exists() - lines = port_file.read_text(encoding="utf-8").split() - assert lines[0] == "56031" - assert lines[1] == "tok-123" + server._assert_token_file() + token_file = tmp_path / "emrgd.token" + assert token_file.exists() + assert token_file.read_text(encoding="utf-8").strip() == "tok-123" + # 旧 emrgd.port 不再被写入 + assert not (tmp_path / "emrgd.port").exists() # ── Daemon stop-path logging (rant 2026-08-19T14:02:37) ────────────── diff --git a/tests/test_daemon_manager.py b/tests/test_daemon_manager.py index e78b47a4..e6a439ad 100644 --- a/tests/test_daemon_manager.py +++ b/tests/test_daemon_manager.py @@ -69,7 +69,7 @@ def _ping_pong_frame() -> str: class TestIsRunning: @patch("emrg.client.daemon_manager.is_server_running_sync", return_value=True) - def test_true_when_port_file_ok(self, mock_probe): + def test_true_when_token_file_ok(self, mock_probe): assert daemon_manager.is_running() is True mock_probe.assert_called_once() @@ -120,9 +120,9 @@ async def _run(): # ── check_and_restart_if_stale ─────────────────────────────── class TestCheckAndRestartIfStale: - def test_no_port_file_returns_early(self, tmp_path): + def test_no_token_file_returns_early(self, tmp_path): with patch("emrg.client.daemon_manager.get_server_path", - return_value=str(tmp_path / "nope.port")): + return_value=str(tmp_path / "nope.token")): asyncio.run(daemon_manager.check_and_restart_if_stale()) # no exceptions = pass @@ -132,12 +132,12 @@ def test_no_port_file_returns_early(self, tmp_path): @patch("emrg.client.daemon_manager.connect_to_server", new_callable=AsyncMock) def test_mtime_unchanged_no_restart(self, mock_connect, mock_running, mock_src, mock_cfg, tmp_path): - port_file = tmp_path / "emrgd.port" - port_file.write_text("12345\ntoken\n") + token_file = tmp_path / "emrgd.token" + token_file.write_text("token\n") mock_connect.return_value = FakeWS([_ping_pong_frame()]) with patch("emrg.client.daemon_manager.get_server_path", - return_value=str(port_file)): + return_value=str(token_file)): asyncio.run(daemon_manager.check_and_restart_if_stale()) # No restart: the frame's started_at (2026) > mtimes (0), so no SIGTERM. # We only assert connect was used (ping roundtrip happened). @@ -151,8 +151,8 @@ def test_mtime_unchanged_no_restart(self, mock_connect, mock_running, def test_source_newer_triggers_restart(self, mock_connect, mock_kill, mock_cleanup, mock_running, mock_src, mock_cfg, tmp_path): - port_file = tmp_path / "emrgd.port" - port_file.write_text("12345\ntoken\n") + token_file = tmp_path / "emrgd.token" + token_file.write_text("token\n") # started_at in the past → source mtime (1e12) > server_start mock_connect.return_value = FakeWS([_ping_pong_frame()]) @@ -164,7 +164,7 @@ def fake_kill(pid, sig): mock_kill.side_effect = fake_kill with patch("emrg.client.daemon_manager.get_server_path", - return_value=str(port_file)): + return_value=str(token_file)): asyncio.run(daemon_manager.check_and_restart_if_stale()) # SIGTERM sent to pid 9999 kill_calls = [c.args for c in mock_kill.call_args_list] @@ -183,8 +183,8 @@ def test_restart_waits_until_old_pid_dead_before_cleanup( mock_src, mock_cfg, tmp_path): """rant 12:49:09 ② — old daemon takes ~0.4s to die: cleanup_server() must NOT run while the old pid is still alive (multi-instance guard).""" - port_file = tmp_path / "emrgd.port" - port_file.write_text("12345\ntoken\n") + token_file = tmp_path / "emrgd.token" + token_file.write_text("token\n") mock_connect.return_value = FakeWS([_ping_pong_frame()]) probe_calls = {"n": 0} @@ -199,7 +199,7 @@ def fake_kill(pid, sig): mock_kill.side_effect = fake_kill with patch("emrg.client.daemon_manager.get_server_path", - return_value=str(port_file)): + return_value=str(token_file)): asyncio.run(daemon_manager.check_and_restart_if_stale()) # waited ≥2 probe rounds (old pid alive → no cleanup yet), then cleanup after death assert probe_calls["n"] >= 3, f"should probe liveness ≥3 times, got {probe_calls['n']}" @@ -216,13 +216,13 @@ def test_restart_force_kills_stuck_old_pid( mock_src, mock_cfg, tmp_path): """rant 12:49:09 ② — old daemon never dies on SIGTERM → SIGKILL fallback, and cleanup still happens after the kill.""" - port_file = tmp_path / "emrgd.port" - port_file.write_text("12345\ntoken\n") + token_file = tmp_path / "emrgd.token" + token_file.write_text("token\n") mock_connect.return_value = FakeWS([_ping_pong_frame()]) mock_kill.side_effect = lambda pid, sig: None # pid stays "alive" forever with patch("emrg.client.daemon_manager.get_server_path", - return_value=str(port_file)): + return_value=str(token_file)): asyncio.run(daemon_manager.check_and_restart_if_stale()) kill_calls = [c.args for c in mock_kill.call_args_list] assert any(c[1] == signal.SIGKILL for c in kill_calls), ( @@ -233,12 +233,12 @@ def test_restart_force_kills_stuck_old_pid( @patch("emrg.client.daemon_manager._get_server_source_mtime", return_value=0.0) @patch("emrg.client.daemon_manager.connect_to_server", new_callable=AsyncMock) def test_server_unreachable_silent(self, mock_connect, mock_src, mock_cfg, tmp_path): - port_file = tmp_path / "emrgd.port" - port_file.write_text("12345\ntoken\n") + token_file = tmp_path / "emrgd.token" + token_file.write_text("token\n") mock_connect.side_effect = ConnectionRefusedError("no daemon") with patch("emrg.client.daemon_manager.get_server_path", - return_value=str(port_file)): + return_value=str(token_file)): asyncio.run(daemon_manager.check_and_restart_if_stale()) # no raise @patch("emrg.client.daemon_manager._get_config_mtime", return_value=0.0) @@ -247,12 +247,12 @@ def test_server_unreachable_silent(self, mock_connect, mock_src, mock_cfg, tmp_p def test_server_auth_error_propagates(self, mock_connect, mock_src, mock_cfg, tmp_path): """G129: AuthError (token mismatch) must NOT be swallowed — it's a config/install problem the user must see, not a transient disconnect.""" - port_file = tmp_path / "emrgd.port" - port_file.write_text("12345\ntoken\n") + token_file = tmp_path / "emrgd.token" + token_file.write_text("token\n") mock_connect.side_effect = daemon_manager.AuthError("authentication failed") with patch("emrg.client.daemon_manager.get_server_path", - return_value=str(port_file)): + return_value=str(token_file)): with pytest.raises(daemon_manager.AuthError): asyncio.run(daemon_manager.check_and_restart_if_stale()) @@ -261,12 +261,12 @@ def test_server_auth_error_propagates(self, mock_connect, mock_src, mock_cfg, tm @patch("emrg.client.daemon_manager.connect_to_server", new_callable=AsyncMock) def test_server_programming_error_propagates(self, mock_connect, mock_src, mock_cfg, tmp_path): """G129: genuine bugs must surface, not vanish into a bare except Exception.""" - port_file = tmp_path / "emrgd.port" - port_file.write_text("12345\ntoken\n") + token_file = tmp_path / "emrgd.token" + token_file.write_text("token\n") mock_connect.side_effect = AttributeError("boom") with patch("emrg.client.daemon_manager.get_server_path", - return_value=str(port_file)): + return_value=str(token_file)): with pytest.raises(AttributeError): asyncio.run(daemon_manager.check_and_restart_if_stale()) diff --git a/tests/test_daemon_manager_e2e.py b/tests/test_daemon_manager_e2e.py index 1064cefa..0650fd65 100644 --- a/tests/test_daemon_manager_e2e.py +++ b/tests/test_daemon_manager_e2e.py @@ -1,6 +1,6 @@ """DaemonManager end-to-end tests (real daemon, mocked LLM). -⚠️ 不经 ensure_connected() 的完整路径(它会读真实 ~/.emrg/emrgd.port 连错 +⚠️ 不经 ensure_connected() 的完整路径(它会读真实 ~/.emrg/emrgd.token 连错 daemon)——先复用 test_ws_e2e 的 _boot_server 起隔离 daemon,再对 ensure_connected 的三个内部调用打桩(check_and_restart / is_running / connect_to_server),验证 DaemonConnection 全链路:ping → list_models → diff --git a/tests/test_installer_stop.py b/tests/test_installer_stop.py index 54f3c589..00485a14 100644 --- a/tests/test_installer_stop.py +++ b/tests/test_installer_stop.py @@ -102,7 +102,7 @@ def test_stop_all_py_restart_manager_lock_owners(): """rant 2026-08-17T17:55:42 — DeleteFile code 5 通用解(Restart Manager)。 0.2.43 安装实测根因:占用 install\\ 下文件的是【外来进程】(browser-harness - daemon,独立 uv CPython,AppData\\Roaming\\uv\\tools),emrgd.pid/emrgd.port + daemon,独立 uv CPython,AppData\\Roaming\\uv\\tools),emrgd.pid/emrgd.token 全空、无任何 -m emrg 进程 → 命令行扫描永远找不到。修复 = Restart Manager (rstrtmgr.dll)扫 install\\ 全部文件收集占用者 → 排除自身+祖先进程链 (stop_all 由 install\\python-dist\\python.exe 执行,自身加载 install\\python313.dll; diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py index 3b2c4291..baa379e2 100644 --- a/tests/test_scheduler.py +++ b/tests/test_scheduler.py @@ -1530,10 +1530,10 @@ def test_evolution_cycle_aborted_leaves_slowdown_state(tmp_path): # ── Connect-failure alerting (G129, rant 2026-08-09T08:03:46) ───── -# GUI tests once overwrote the real ~/.emrg/emrgd.port with fake values, +# GUI tests once overwrote the real ~/.emrg/emrgd.token with fake values, # so the evolution cycle failed to reach the daemon for 10 hours with only # a WARNING log. Consecutive failures must escalate to ERROR + carry an -# actionable hint (check the port file), never silently swallow. +# actionable hint (check the token file), never silently swallow. def test_evolution_cycle_connect_failure_escalates_to_error(tmp_path, caplog): """Repeated connect failures must escalate from warning to error alert.""" @@ -1556,7 +1556,7 @@ async def _refuse(): # 第 3 次(达到阈值)必须出现 ERROR 告警,且提示检查 port 文件 error_msgs = [r.message for r in caplog.records if r.levelno >= logging.ERROR] assert error_msgs, "expected an ERROR alert after threshold" - assert any("emrgd.port" in m for m in error_msgs), error_msgs + assert any("emrgd.token" in m for m in error_msgs), error_msgs assert any("consecutive" in m for m in error_msgs), error_msgs finally: mod.connect_to_server = _original_connect_to_server() diff --git a/tests/test_ws_e2e.py b/tests/test_ws_e2e.py index 684c7c0d..7ceef162 100644 --- a/tests/test_ws_e2e.py +++ b/tests/test_ws_e2e.py @@ -111,12 +111,12 @@ async def _boot_server(tmp: Path): server.llm.chat_stream = _make_fake_chat_stream() task = asyncio.create_task(server.serve()) - # Wait for the port file (daemon ready) + # Wait for the token file (daemon ready) for _ in range(200): - if (tmp / "emrgd.port").exists(): + if (tmp / "emrgd.token").exists(): break await asyncio.sleep(0.05) - assert (tmp / "emrgd.port").exists(), "daemon did not publish port file" + assert (tmp / "emrgd.token").exists(), "daemon did not publish token file" async def _cleanup(): server._server.close()