From e128eaa561d2964bffcd0b93b25eb9a82ef30b29 Mon Sep 17 00:00:00 2001 From: EMRG Evolution Date: Tue, 11 Aug 2026 19:54:57 +0800 Subject: [PATCH 1/2] emrg: stop-git.ps1 guilt-by-association kill + README attraction pass (rants 2026-08-11T19:47:44/19:50:37) --- Agent.md | 2 +- README.cn.md | 9 ++- README.md | 9 ++- bin/stop-emrg.cmd | 65 +++++++++------------ bin/stop-git.ps1 | 35 ++++++++++++ packaging/build-runtime.sh | 1 + packaging/make-installer.sh | 20 +++++-- tests/test_doc_counts.py | 32 ++++++----- tests/test_installer_stop.py | 108 ++++++++++++++++++++--------------- 9 files changed, 173 insertions(+), 108 deletions(-) create mode 100644 bin/stop-git.ps1 diff --git a/Agent.md b/Agent.md index 031ffd9..3448f79 100644 --- a/Agent.md +++ b/Agent.md @@ -112,7 +112,7 @@ Community needs voiced in HN agent-UI discussions map directly to EMRG's design: pkill -f "emrg.server"; rm -f ~/.emrg/emrgd.port; python -m emrg ``` -Python: `uv run pytest tests/ -v` (694) — import check: `uv run python -c "from emrg.client.app import run_client"` +Python: `uv run pytest tests/ -v` (695) — import check: `uv run python -c "from emrg.client.app import run_client"` GUI: `cd emrg/gui && npm test` (212: 43 daemon_client + 19 conn-manager + 22 app-commands + 91 renderer smoke + 15 i18n + 7 integration + 3 commands + 5 build-config + 7 gui-state) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js` CI: `uv run pytest` + 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/README.cn.md b/README.cn.md index 00447c2..499038f 100644 --- a/README.cn.md +++ b/README.cn.md @@ -1,5 +1,9 @@ # EMRG +

+ EMRG +

+

一个会写代码的 AI 编程助手——而且它还会重写自己

@@ -35,9 +39,10 @@ EMRG 是一个关于*自主进化*的实验:一个能帮你写代码的 AI 智 **一句话定位**:EMRG 是唯一一个**会因你的反馈而自我改进**的编程智能体——吐槽哪里不好,下一个演化周期它就自己写代码修好并上线。 +> **唯一核心卖点:自我改进。** 每一条 `/rant` 都会变成真实的 PR——分析、编码、测试、合并,全程无人值守。没有其他编程智能体做得到。 + | 特性 | 差异点 | |---|---| -| **越用越好(核心)** | 你的 `/rant`、GitHub Issues、竞品动态都会变成真实改进——分析、编码、测试、提 PR、合并,全自动无人值守 | | **Electron GUI(主入口)** | 安装即用:首次启动引导配置 API Key;全部 `/` 指令可用;结果面板、Ask/Auto 模式、自进化可见化。GUI 配好即 TUI 可用 | | **全功能 TUI + 守护进程** | 流式 Markdown、`/` 自动补全、会话管理、快捷键——跑在常驻的 `emrgd` 守护进程上,随时重连 | | **100% 开源** | MIT 协议——没有围墙,没有厂商锁定。默认英文,提供中文版 | @@ -161,7 +166,7 @@ EMRG 不只是追赶——它自己追上来。 贡献指南、源码安装、架构、详细 FAQ → [DEVELOPMENT.md](DEVELOPMENT.md)。 -快速检查:`uv run pytest tests/ -v`(当前 694 项)· `cd emrg/gui && npm test`(212 项:43 daemon_client + 19 conn-manager + 22 app-commands + 91 renderer smoke + 15 i18n + 7 integration + 3 commands + 5 build-config + 7 gui-state) +快速检查:`uv run pytest tests/ -v` · `cd emrg/gui && npm test`(测试:见上方徽章) --- diff --git a/README.md b/README.md index bb1f24d..24d33fb 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # EMRG +

+ EMRG +

+

The AI coding agent that writes code — and rewrites itself.

@@ -35,9 +39,10 @@ EMRG is an experiment in *autonomous self-improvement*: an AI agent that helps y **The one-line pitch**: EMRG is the only coding agent that **improves itself from your feedback** — rant about what bothers you, and the next evolution cycle writes the fix and ships it. +> **The one feature: self-improvement.** Every `/rant` becomes a real PR — coded, tested, merged, unattended. No other coding agent does this. + | What | Why it's different | |---|---| -| **Gets better on its own** | **The core feature.** Your `/rant`s, GitHub issues, and competitor updates become real improvements — analyzed, coded, tested, PR'd, merged, unattended | | **Electron GUI (main entry)** | Install & go: first-run wizard, all slash commands, results panel, Ask/Auto modes, visible self-evolution. GUI configured = TUI ready | | **Full-featured TUI + daemon** | Streaming markdown, `/` autocomplete, sessions, shortcuts — on a persistent `emrgd` daemon you can reconnect to anytime | | **100% open source** | MIT — no walled garden, no vendor lock-in. English default, Chinese version available | @@ -161,7 +166,7 @@ They're products. EMRG is an experiment in *closing the loop* — the AI improve Contributing, source installs, architecture, and the full FAQ → [DEVELOPMENT.md](DEVELOPMENT.md). -Quick checks: `uv run pytest tests/ -v` (currently 694 items) · `cd emrg/gui && npm test` (212: 43 daemon_client + 19 conn-manager + 22 app-commands + 91 renderer smoke + 15 i18n + 7 integration + 3 commands + 5 build-config + 7 gui-state) +Quick checks: `uv run pytest tests/ -v` · `cd emrg/gui && npm test` (tests: see badge above) --- diff --git a/bin/stop-emrg.cmd b/bin/stop-emrg.cmd index bf21614..921340a 100644 --- a/bin/stop-emrg.cmd +++ b/bin/stop-emrg.cmd @@ -13,12 +13,13 @@ REM fallback), excludes the daemon (pythonw.exe -m emrg.server) REM 3. daemon: `emrg server stop` protocol shutdown via the OLD install's CLI REM (present since #364 -- version-safe), emrgd.pid poll (<=10s), then REM taskkill /F /PID fallback -REM 4. bundled git: kill ONLY EMRG-owned git/ssh/bash subprocess trees under -REM %INSTALL%\git\ (portable Git the daemon spawns for evolution-cycle ops). -REM Rant 2026-08-11T17:56:25: orphans hold msys-2.0.dll -> Inno code 5. -REM Rant 2026-08-11T18:56:58: blanket path kill also hit the HOST's Git Bash -REM sh/vim -> only EMRG trees (ancestor = daemon/TUI) are killed; host tools -REM are never touched (same class as R125). +REM 4. bundled git: GUILT-BY-ASSOCIATION force-kill via stop-git.ps1 +REM (host 2026-08-11T19:47:44 FINAL decision OVERRIDES #689: install success +REM has priority -- if sh/vim hold msys-2.0.dll and block the git-tree kill, +REM they are killed too; only the install\git\ prefix is touched, system Git +REM in Program Files is never affected). stop-git.ps1 is a standalone script +REM (no cmd inline-PowerShell quoting escapes). The installer passes the +REM {tmp}-extracted copy as %~1 when the OLD install lacks it. REM REM Returns 0 when nothing EMRG-related survives; 1 if a process could not be REM stopped (installer aborts with a clear message instead of hanging). @@ -26,19 +27,7 @@ REM Safe on clean install: no old install dir -> everything is skipped -> 0. setlocal enabledelayedexpansion set "EMRG_DIR=%USERPROFILE%\.emrg" set "INSTALL=%EMRG_DIR%\install" -REM PID snapshot file: EMRG-owned process tree captured BEFORE any kill, so dead -REM parents are irrelevant (review 2026-08-11T19:03: ancestor walk cannot resolve -REM dead daemon parents -- the #683 primary orphan case regresses). -set "PIDFILE=%TEMP%\emrg-stop-pids.txt" -del /q "%PIDFILE%" >nul 2>&1 - -REM --- 0. snapshot EMRG-owned tree (daemon + TUI + GUI + descendants) --- -REM Walk DOWN from the roots (emrgd.pid, EMRG.exe, python.exe -m emrg) while they -REM are still alive, collecting the full descendant PID set. Step 4 / :verify use -REM ONLY this recorded set -> host Git Bash sh/vim (not descendants of EMRG roots) -REM are never touched (rant 2026-08-11T18:56:58), and orphans of an already-dead -REM daemon are still captured (their PID was recorded while the daemon lived). -powershell -NoProfile -Command "$f=Get-CimInstance Win32_Process; $roots=New-Object System.Collections.Generic.List[int]; if(Test-Path \"%EMRG_DIR%\emrgd.pid\"){ $d=Get-Content \"%EMRG_DIR%\emrgd.pid\" -ErrorAction SilentlyContinue; if($d -match '^\d+$'){ $roots.Add([int]$d) } }; $f | Where-Object { $_.Name -eq 'EMRG.exe' -or ($_.Name -eq 'python.exe' -and $_.CommandLine -match '-m emrg' -and $_.CommandLine -notmatch 'emrg\.server') } | ForEach-Object { $roots.Add([int]$_.ProcessId) }; $set=New-Object 'System.Collections.Generic.HashSet[int]'; foreach($r in $roots){ [void]$set.Add($r) }; $changed=$true; while($changed){ $changed=$false; foreach($p in $f){ if($set.Contains([int]$p.ParentProcessId) -and -not $set.Contains([int]$p.ProcessId)){ [void]$set.Add([int]$p.ProcessId); $changed=$true } } }; $set | ForEach-Object { $_ } | Set-Content -Path \"%PIDFILE%\" -Encoding ascii" >nul 2>&1 +set "EXIT_CODE=0" REM --- 1. GUI: graceful WM_CLOSE, then unconditional /F after the grace window --- REM (host report 2026-08-10T01:27:07Z: a long-lived ~15h GUI session deferred @@ -80,20 +69,22 @@ set "DPID=" for /f "usebackq delims=" %%p in ("%EMRG_DIR%\emrgd.pid") do set "DPID=%%p" if defined DPID taskkill /F /PID %DPID% >nul 2>&1 -REM --- 4. bundled git: kill only RECORDED EMRG-owned git/ssh/bash (snapshot step 0) --- -REM (rant 2026-08-11T17:56:25: daemon killed mid-git-op leaves orphans holding -REM install\git\usr\bin\msys-2.0.dll -> Inno DeleteFile code 5.) -REM (host 2026-08-11T18:56:58: blanket path kill also caught the HOST's Git Bash -REM sh/vim -- vim edits lost + installer aborted. Same class as R125.) Only PIDs -REM recorded in step 0 (EMRG tree captured while daemon/TUI were alive) are killed -REM when their executable is under install\git\. Host Git Bash sh/vim are not in -REM the recorded set -> never touched. -if exist "%PIDFILE%" ( - powershell -NoProfile -Command "$f=Get-CimInstance Win32_Process; $ids=@(Get-Content \"%PIDFILE%\" | ForEach-Object { [int]$_ }); $f | Where-Object { $ids -contains [int]$_.ProcessId -and $_.ExecutablePath -like \"$env:USERPROFILE\.emrg\install\git\*\" } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }" >nul 2>&1 +REM --- 4. bundled git: guilt-by-association force-kill via stop-git.ps1 --- +REM (host 2026-08-11T19:47:44 FINAL decision OVERRIDES #689's snapshot-only +REM approach -- "如果杀死git时,发现sh/vim导致杀不掉,则一起杀": install success +REM has priority; sh/vim holding msys-2.0.dll under install\git\ are killed too. +REM Only the install\git\ prefix is touched; system Git (Program Files) is never +REM matched. 0.2.26's cmd inline-PowerShell quoting escape is avoided by calling +REM the standalone stop-git.ps1 via -File.) +set "GITSTOP=%INSTALL%\bin\stop-git.ps1" +if not exist "%GITSTOP%" set "GITSTOP=%~1" +if not exist "%GITSTOP%" set "GITSTOP=%TEMP%\emrg-stop-git.ps1" +if exist "%GITSTOP%" ( + powershell -NoProfile -ExecutionPolicy Bypass -File "%GITSTOP%" >nul 2>&1 + if errorlevel 1 set "EXIT_CODE=1" ) :verify -set "EXIT_CODE=0" tasklist /FI "IMAGENAME eq EMRG.exe" 2>nul | findstr /i "EMRG.exe" >nul && set "EXIT_CODE=1" REM %-variables inside a parenthesized block expand at parse time (after set "DPID=" they hold the old/empty value) -> must use REM enabledelayedexpansion !DPID! (runtime expansion); if defined itself is a runtime check. @@ -104,12 +95,10 @@ if exist "%EMRG_DIR%\emrgd.pid" ( tasklist /FI "PID eq !DPID!" 2>nul | findstr /i "!DPID!" >nul && set "EXIT_CODE=1" ) ) -REM EMRG-owned bundled-git survival check: only RECORDED EMRG git PIDs still alive -REM -> exit 1 (installer aborts with a restart hint). Host Git Bash sh/vim alive is -REM NOT a failure (rant 2026-08-11T18:56:58). -if exist "%PIDFILE%" ( - powershell -NoProfile -Command "$f=Get-CimInstance Win32_Process; $ids=@(Get-Content \"%PIDFILE%\" | ForEach-Object { [int]$_ }); $surv = $f | Where-Object { $ids -contains [int]$_.ProcessId -and $_.ExecutablePath -like \"$env:USERPROFILE\.emrg\install\git\*\" }; if ($surv) { exit 1 }" >nul 2>&1 - if errorlevel 1 set "EXIT_CODE=1" -) -del /q "%PIDFILE%" >nul 2>&1 +REM Bundled-git survival check (plain prefix; guilt-by-association semantics): +REM any process still under install\git\ -> exit 1 (installer aborts). This is the +REM same condition stop-git.ps1 reports; kept as belt-and-braces for the case the +REM script file could not be located (no old install, no {tmp} arg). +powershell -NoProfile -Command "$p = Get-CimInstance Win32_Process | Where-Object { $_.ExecutablePath -like \"$env:USERPROFILE\.emrg\install\git\*\" }; if ($p) { exit 1 }" >nul 2>&1 +if errorlevel 1 set "EXIT_CODE=1" endlocal & exit /b %EXIT_CODE% diff --git a/bin/stop-git.ps1 b/bin/stop-git.ps1 new file mode 100644 index 0000000..4b57f0e --- /dev/null +++ b/bin/stop-git.ps1 @@ -0,0 +1,35 @@ +# stop-git.ps1 -- force-kill every process under the bundled git install prefix +# (host 2026-08-11T19:47:44 FINAL decision, OVERRIDES #689: install success has +# priority -- "要杀死相关进程,如果杀死git时,发现sh/vim导致杀不掉,则一起杀。" +# If sh/vim hold msys-2.0.dll and block the git-tree kill, they are killed too.) +# +# 1. collect all processes whose ExecutablePath is under +# %USERPROFILE%\.emrg\install\git\ (the portable Git the daemon spawns for +# evolution-cycle ops; system Git under Program Files is NEVER matched) +# 2. pass 1: force-kill the git tree (git.exe/ssh.exe/plink.exe/bash.exe) +# 3. pass 2: guilt-by-association -- force-kill EVERY survivor under the prefix +# (sh/vim included, per host decision; 0.2.25 msys-2.0.dll popup must not regress) +# 4. exit 0 when nothing survives (or nothing was running -- clean install safe); +# exit 1 when a process still holds the prefix (installer aborts with a hint) +# +# Run via `powershell -NoProfile -ExecutionPolicy Bypass -File stop-git.ps1` +# (a standalone script -- no cmd inline-PowerShell quoting escapes, the root +# cause of the 0.2.26 step-4 escape bug). + +$ErrorActionPreference = 'SilentlyContinue' +$prefix = "$env:USERPROFILE\.emrg\install\git\*" + +# pass 1: git tree (portable Git executables under the prefix) +Get-CimInstance Win32_Process | + Where-Object { $_.ExecutablePath -like $prefix -and $_.Name -in @('git.exe', 'ssh.exe', 'plink.exe', 'bash.exe') } | + ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue } +Start-Sleep -Milliseconds 300 + +# pass 2: guilt-by-association -- anything still under the prefix (sh/vim/...) +Get-CimInstance Win32_Process | + Where-Object { $_.ExecutablePath -like $prefix } | + ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue } +Start-Sleep -Milliseconds 300 + +$left = @(Get-CimInstance Win32_Process | Where-Object { $_.ExecutablePath -like $prefix }) +if ($left.Count -gt 0) { exit 1 } else { exit 0 } diff --git a/packaging/build-runtime.sh b/packaging/build-runtime.sh index 5fdf5ae..5bc1514 100755 --- a/packaging/build-runtime.sh +++ b/packaging/build-runtime.sh @@ -69,6 +69,7 @@ cp -R "$PY_ROOT/." "$DIST/bin/python-dist/" cp "$ROOT/bin/emrg.cmd" emrg.cmd 2>/dev/null || true cp "$ROOT/bin/emrgd.cmd" emrgd.cmd 2>/dev/null || true cp "$ROOT/bin/stop-emrg.cmd" stop-emrg.cmd 2>/dev/null || true + cp "$ROOT/bin/stop-git.ps1" stop-git.ps1 2>/dev/null || true cp "$ROOT/bin/emrg-uninstall" emrg-uninstall chmod +x emrg emrgd emrg-uninstall ) diff --git a/packaging/make-installer.sh b/packaging/make-installer.sh index 8f36b95..7348676 100755 --- a/packaging/make-installer.sh +++ b/packaging/make-installer.sh @@ -251,6 +251,10 @@ Source: "$STAGE_WIN/payload\\*"; DestDir: "{app}"; Flags: recursesubdirs createa ; pythonw daemon 锁文件导致卡在"停止已有进程")。正常安装时该文件仍由上方通配符 ; 装入 {app}\bin\stop-emrg.cmd。 Source: "$STAGE_WIN/payload\\bin\\stop-emrg.cmd"; DestDir: "{tmp}"; Flags: dontcopy +; R128: dontcopy — 供 [Code] PrepareToInstall 在覆盖文件前 ExtractTemporaryFile 取出并 +; 作为 %~1 传给 stop-emrg.cmd step 4(连坐强杀 bundled git,宿主 2026-08-11T19:47:44 +; 拍板覆盖 #689:旧安装 %INSTALL%\bin\stop-git.ps1 不存在时用 {tmp} 提取版)。 +Source: "$STAGE_WIN/payload\\bin\\stop-git.ps1"; DestDir: "{tmp}"; Flags: dontcopy [Icons] Name: "{userprograms}\\EMRG"; Filename: "{app}\\emrg-gui\\EMRG\\EMRG.exe"; IconFilename: "{app}\\emrg-gui\\EMRG\\EMRG.exe" [UninstallRun] @@ -372,20 +376,26 @@ end; // -m emrg.server 常驻锁文件),覆盖 ~/.emrg\install 时卡在"停止已有进程"。 // PrepareToInstall 在安装开始前运行 bin\stop-emrg.cmd:taskkill EMRG.exe // 优雅→/F 兜底、wmic/PowerShell 命令行过滤 TUI、emrg server stop 协议关闭 -// daemon + emrgd.pid 轮询兜底、按可执行文件路径杀 bundled git 孤儿 -// (rant 2026-08-11T17:56:25: daemon 演化周期 git 操作中途被杀 → git/ssh/bash -// 孤儿锁 install\git\usr\bin\msys-2.0.dll → Inno DeleteFile code 5)。干净安装 -// (无旧 install)脚本自行跳过。返回非空字符串 = 中止安装并显示该消息(宁可中止也不卡死)。 +// daemon + emrgd.pid 轮询兜底、step 4 调 stop-git.ps1 连坐强杀 bundled git +// (宿主 2026-08-11T19:47:44 拍板覆盖 #689:sh/vim 锁 install\git\usr\bin\ +// msys-2.0.dll 导致 Inno DeleteFile code 5 时一并强杀,安装成功优先;只碰 +// install\git\ 前缀,系统 Git 不受影响)。旧安装可能没有 stop-git.ps1 → +// {tmp} dontcopy 提取版作为 %~1 传入(cmd 内先查 %INSTALL%\bin,缺失才用 +// 参数)。干净安装(无旧 install)脚本自行跳过。返回非空字符串 = 中止安装 +// 并显示该消息(宁可中止也不卡死)。 // {cmd} = cmd.exe(Inno 预定义常量,批处理文件须经 cmd 启动)。 function PrepareToInstall(var NeedsRestart: Boolean): String; var ResultCode: Integer; StopScript: string; + GitStopScript: string; begin Result := ''; ExtractTemporaryFile('stop-emrg.cmd'); + ExtractTemporaryFile('stop-git.ps1'); StopScript := ExpandConstant('{tmp}\\stop-emrg.cmd'); - if Exec(ExpandConstant('{cmd}'), '/c "' + StopScript + '"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode) then + GitStopScript := ExpandConstant('{tmp}\\stop-git.ps1'); + if Exec(ExpandConstant('{cmd}'), '/c "' + StopScript + '" "' + GitStopScript + '"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode) then begin if ResultCode <> 0 then Result := 'EMRG could not stop all running processes (stop-emrg.cmd exit code ' + IntToStr(ResultCode) + '). Please close EMRG (GUI/TUI) and retry the install, or restart the computer and retry (a helper process such as the bundled Git may still hold a file lock).'; diff --git a/tests/test_doc_counts.py b/tests/test_doc_counts.py index 9bc81db..ee43d62 100644 --- a/tests/test_doc_counts.py +++ b/tests/test_doc_counts.py @@ -10,6 +10,10 @@ after #580 added 3 GUI tests. Both checks now cover all three docs (README.md, README.cn.md, Agent.md); CJK full-width parens and the "项:" separator are normalized before matching. +#697: rant 2026-08-11T19:50:37 — README.md/README.cn.md switched to the +Tests badge (no hardcoded counts); the python-count check now guards +Agent.md only, while the GUI-breakdown check still picks up any +"(N: ...)" line it finds in any doc (Agent.md keeps the breakdown). """ import re @@ -60,21 +64,19 @@ def _gui_breakdowns() -> list[tuple[str, int, list[int]]]: def test_python_count_matches_docs() -> None: collected = _collected_pytest_count() - for doc in ("README.md", "README.cn.md", "Agent.md"): - text = (REPO_ROOT / doc).read_text(encoding="utf-8") - # README: "run tests (currently N items)" | README.cn: "(当前 N 项)" - # Agent.md: "pytest tests/ -v` (N)" - m = re.search(r"currently (\d+) items", text) or re.search( - r"当前 (\d+) 项", text - ) or re.search( - r"uv run pytest tests/ -v` \((\d+)\)", text - ) - assert m, f"no documented Python count found in {doc}" - documented = int(m.group(1)) - assert documented == collected, ( - f"{doc} documents {documented} Python tests but {collected} are collected " - f"(--collect-only). Sync the doc (and this guard) when adding/removing tests." - ) + # rant 2026-08-11T19:50:37: README.md/README.cn.md dropped hardcoded counts in + # favor of the Tests badge (dynamic — no more doc drift). Agent.md keeps the + # number (project-context file, checked by the same guard). + doc = "Agent.md" + text = (REPO_ROOT / doc).read_text(encoding="utf-8") + # Agent.md: "pytest tests/ -v` (N)" + m = re.search(r"uv run pytest tests/ -v` \((\d+)\)", text) + assert m, f"no documented Python count found in {doc}" + documented = int(m.group(1)) + assert documented == collected, ( + f"{doc} documents {documented} Python tests but {collected} are collected " + f"(--collect-only). Sync the doc (and this guard) when adding/removing tests." + ) def test_gui_breakdown_sums_to_headline() -> None: diff --git a/tests/test_installer_stop.py b/tests/test_installer_stop.py index 6984778..1cd542c 100644 --- a/tests/test_installer_stop.py +++ b/tests/test_installer_stop.py @@ -1,14 +1,17 @@ -"""Windows 安装器预停止接线回归测试(rant 2026-08-10T08:50:44)。 +"""Windows 安装器预停止接线回归测试(rant 2026-08-10T08:50:44 + 宿主 2026-08-11T19:47:44 连坐强杀拍板)。 安装器(Inno Setup,make-installer.sh 生成 emrg.iss)覆盖 ~/.emrg/install 前必须 -先优雅关闭 GUI/TUI/daemon,否则无窗口的 pythonw daemon 独占锁文件 → 卡在 +先优雅关闭 GUI/TUI/daemon/bundled-git,否则无窗口的 pythonw daemon 独占锁文件 → 卡在 "停止已有进程"(宿主只能重启系统)。本测试纯文本断言(不执行 iscc/cmd —— -macOS/CI 无 Windows),钉死四处接线: - 1. bin/stop-emrg.cmd 存在且覆盖三步:GUI 优雅关闭+/F 兜底、TUI 命令行过滤、 - daemon 协议关闭 + emrgd.pid 轮询兜底(顺序 GUI → daemon) - 2. bin/emrgd.cmd 含 stop 分支(复用 `emrg server stop`) - 3. make-installer.sh 的 .iss 模板含 [Files] dontcopy + [Code] PrepareToInstall - 4. build-runtime.sh 把 stop-emrg.cmd 复制进 runtime bin/ +macOS/CI 无 Windows),钉死接线: + 1. bin/stop-emrg.cmd 覆盖三步 + step 4 调 stop-git.ps1:GUI 优雅关闭+/F 兜底、 + TUI 命令行过滤、daemon 协议关闭 + emrgd.pid 轮询兜底(顺序 GUI → daemon) + 2. bin/stop-git.ps1 独立脚本:install\\git\\ 前缀收集 → 先杀 git 树 → 连坐强杀全部 + 残留(sh/vim 一并杀,宿主拍板覆盖 #689)→ 无残留 exit 0 / 有残留 exit 1 + 3. bin/emrgd.cmd 含 stop 分支(复用 `emrg server stop`) + 4. make-installer.sh 的 .iss 模板含 [Files] dontcopy(stop-emrg.cmd + stop-git.ps1) + + [Code] PrepareToInstall 传 {tmp} 提取版作 %~1 + 5. build-runtime.sh 把 stop-emrg.cmd / stop-git.ps1 复制进 runtime bin/ """ from pathlib import Path @@ -16,7 +19,7 @@ REPO_ROOT = Path(__file__).resolve().parent.parent -def test_stop_emrg_cmd_covers_gui_tui_daemon_in_order(): +def test_stop_emrg_cmd_covers_gui_tui_daemon_and_calls_stop_git(): content = (REPO_ROOT / "bin" / "stop-emrg.cmd").read_text(encoding="utf-8") # GUI:无 /F 优雅 WM_CLOSE 优先,/F 兜底 assert "taskkill /IM EMRG.exe" in content @@ -24,8 +27,7 @@ def test_stop_emrg_cmd_covers_gui_tui_daemon_in_order(): assert content.index("taskkill /IM EMRG.exe") < content.index("taskkill /F /IM EMRG.exe") # 宿主 2026-08-10T01:27:07Z 实测:长活 ~15h GUI 会话 WM_CLOSE 在 5s 窗内未退出, # 两次整跑未终止、直接 taskkill /F 才终止 → /F 必须是无条件兜底(不 gate 在 - # survivor 检查上),否则旧 GUI 会话可无限期拖住安装器。判别:ping 等待之后、 - # /F 之前不得再有 findstr 判定。 + # survivor 检查上)。判别:ping 等待之后、/F 之前不得再有 findstr 判定。 ping_idx = content.index("ping -n 6 127.0.0.1") f_idx = content.index("taskkill /F /IM EMRG.exe") assert ping_idx < f_idx @@ -39,50 +41,60 @@ def test_stop_emrg_cmd_covers_gui_tui_daemon_in_order(): # 顺序:GUI 在 daemon 之前(GUI 不能复活 daemon) daemon_line = content.index('call "%INSTALL%\\bin\\emrg.cmd" server stop') assert content.index("taskkill /IM EMRG.exe") < daemon_line - # step 4(rant 2026-08-11T17:56:25 → 18:56:58 修正):只杀 EMRG 自己 git 子进程树, - # 不碰宿主 Git Bash sh/vim(R125 同族——宿主工具不干涉)。 - # 判别:ExecutablePath 前缀过滤只命中 %INSTALL%\git\(不误杀系统 Git)。 - # ⚡ review 2026-08-11T19:03:祖先回溯(ancestor walk)无法解析"已死父进程"—— - # #683 主场景正是 daemon 已死后的孤儿 → 改为 step 0 **向下**快照 - # (daemon/TUI/GUI 根 + BFS 子孙集,写入 %TEMP%\emrg-stop-pids.txt), - # step 4 / :verify 只作用于快照记录集。 - assert 'Get-CimInstance Win32_Process' in content - assert r'$env:USERPROFILE\.emrg\install\git\*' in content - # step 0 快照:向下 BFS(ParentProcessId → 子进程加入集合)在杀任何进程之前 - assert "emrg-stop-pids.txt" in content - assert "ParentProcessId" in content - assert "-match '-m emrg'" in content - assert "Set-Content" in content # 快照写盘 - # step 4 的 kill 必须基于快照记录集($ids -contains)+ ExecutablePath 过滤 - step4_idx = content.index("REM --- 4. bundled git: kill only RECORDED") - daemon_line2 = content.index('call "%INSTALL%\\bin\\emrg.cmd" server stop') - verify_idx = content.index(':verify\nset "EXIT_CODE=0"') # 标签定义处(goto :verify 在前,勿用裸 index(":verify")) - assert daemon_line2 < step4_idx < verify_idx - assert "$ids -contains [int]$_.ProcessId" in content + # step 4(宿主 2026-08-11T19:47:44 拍板覆盖 #689):调独立 stop-git.ps1 连坐强杀。 + # 判别:powershell -File 调用(无 cmd 内联转义)+ -ExecutionPolicy Bypass。 + step4_idx = content.index("REM --- 4. bundled git: guilt-by-association") + verify_idx = content.index("\n:verify\n") # 标签定义处(\n 前缀排除前面 goto :verify 行) + assert daemon_line < step4_idx < verify_idx + assert "stop-git.ps1" in content + assert "-ExecutionPolicy Bypass -File" in content + assert r'"%GITSTOP%"' in content + # 旧安装可能没有 stop-git.ps1 → {tmp} 提取版(%~1)与 %TEMP% 兜底 + assert 'set "GITSTOP=%~1"' in content + assert 'set "GITSTOP=%TEMP%\\emrg-stop-git.ps1"' in content + # #689 的 snapshot/step0/PIDFILE 机制已删除(宿主否决——误伤宿主工具的根源) + assert "emrg-stop-pids.txt" not in content + assert "ParentProcessId" not in content + assert "PIDFILE" not in content + assert "Set-Content" not in content # 快照写盘命令不复存在 + # step 4 失败必须传导到退出码(EXIT_CODE 初始化在 setlocal 之后、verify 不再清零) + assert 'set "EXIT_CODE=0"' in content + assert "if errorlevel 1 set \"EXIT_CODE=1\"" in content + assert content.index('set "EXIT_CODE=0"') < step4_idx # 干净安装安全:无旧 install 目录时跳过 assert 'set "INSTALL=%EMRG_DIR%\\install"' in content # 括号块内 pid 判定必须用延迟展开(!DPID!)——%DPID% 在块解析时展开, # set "DPID=" 后取到旧值/空值 → if defined 恒假 → daemon 存活误报干净 # (rant 2026-08-10T08:50:44,cmd.exe 经典括号块展开坑) assert "setlocal enabledelayedexpansion" in content - # 从标签定义处(而非前面 wait 循环的 goto :verify)截取校验块 - verify_block = content[content.index(":verify\nset \"EXIT_CODE=0\""):] + # 从标签定义处截取校验块 + verify_block = content[content.index("\n:verify\n"):] assert "PID eq !DPID!" in verify_block # 非延迟展开 %DPID% 不得出现在括号块内(块解析时展开=恒旧值) assert "%DPID%" not in verify_block - # :verify 的 EMRG-owned bundled-git 存活判定:只有快照记录集内的 EMRG git PID 残留 - # → exit 1;宿主 sh/vim 存活不是失败(rant 2026-08-11T18:56:58)——判别:verify 块 - # 含 $ids -contains(快照集过滤),但不再有无差别 ExecutablePath 存活检查 + # :verify 的 bundled-git 存活判定:无差别前缀检查(连坐语义——连坐杀后仍存活才中止) assert "exit 1" in verify_block assert "if errorlevel 1 set \"EXIT_CODE=1\"" in verify_block - assert "$ids -contains" in verify_block - # 无差别 bundled-git 存活检查必须已删除:verify 块里不得再有"任何 install\git\ 进程 - # 即 exit 1"的旧逻辑(旧式:if (Get-CimInstance ...) { exit 1 }) - assert "}) { exit 1 }" not in verify_block - # 而 step 4 的 kill 命令必须保留(快照集过滤 + ExecutablePath) - assert "Stop-Process" in content - # 快照文件清理 - assert 'del /q "%PIDFILE%"' in content + assert r'$env:USERPROFILE\.emrg\install\git\*' in verify_block + assert "Get-CimInstance" in verify_block + + +def test_stop_git_ps1_guilt_by_association(): + content = (REPO_ROOT / "bin" / "stop-git.ps1").read_text(encoding="utf-8") + # 前缀过滤:只碰 %USERPROFILE%\.emrg\install\git\*(系统 Git 永不命中) + assert r'$env:USERPROFILE\.emrg\install\git\*' in content + # pass 1:先杀 git 树(git/ssh/plink/bash) + assert "-in @('git.exe', 'ssh.exe', 'plink.exe', 'bash.exe')" in content + # pass 2:连坐强杀——前缀内全部残留(sh/vim 一并杀,宿主拍板) + # 判别:第二次 Get-CimInstance 无 Name 过滤(不再区分工具类型) + assert content.count("Get-CimInstance Win32_Process") >= 3 + pass2_idx = content.index("# pass 2") + assert "Stop-Process -Id $_.ProcessId -Force" in content + # 退出码语义:无残留(或本来无进程)→ 0;仍有存活 → 1 + assert "exit 1" in content and "exit 0" in content + assert 'if ($left.Count -gt 0) { exit 1 } else { exit 0 }' in content + # 独立脚本 + 非交互(无 cmd 内联转义问题,0.2.26 转义 bug 根因规避) + assert "$ErrorActionPreference = 'SilentlyContinue'" in content def test_emrgd_cmd_has_stop_branch(): @@ -96,13 +108,19 @@ def test_make_installer_iss_has_prepare_to_install(): content = (REPO_ROOT / "packaging" / "make-installer.sh").read_text(encoding="utf-8") assert "dontcopy" in content assert "stop-emrg.cmd" in content + assert "stop-git.ps1" in content assert "PrepareToInstall" in content assert "ExtractTemporaryFile('stop-emrg.cmd')" in content + assert "ExtractTemporaryFile('stop-git.ps1')" in content + # {tmp} 提取版 stop-git.ps1 作为 %~1 传给 stop-emrg.cmd(旧安装可能无此文件) + assert "GitStopScript" in content + assert "'/c \"' + StopScript + '\" \"' + GitStopScript" in content assert "SW_HIDE" in content # 批处理执行不弹控制台窗口(#592 纪律) # rant 2026-08-11T17:56:25:中止消息含重启兜底引导(杀不掉时宿主可重启后重试) assert "restart the computer" in content -def test_build_runtime_copies_stop_emrg_cmd(): +def test_build_runtime_copies_stop_scripts(): content = (REPO_ROOT / "packaging" / "build-runtime.sh").read_text(encoding="utf-8") assert 'cp "$ROOT/bin/stop-emrg.cmd" stop-emrg.cmd' in content + assert 'cp "$ROOT/bin/stop-git.ps1" stop-git.ps1' in content From 774216deffa2d41617f5f6b3d442bc8890707d88 Mon Sep 17 00:00:00 2001 From: EMRG Evolution Date: Tue, 11 Aug 2026 19:59:48 +0800 Subject: [PATCH 2/2] =?UTF-8?q?emrg:=20address=20review=20nits=20on=20#692?= =?UTF-8?q?=20=E2=80=94=20fix=20PR=20ref=20in=20docstring,=20drop=20dead?= =?UTF-8?q?=20%TEMP%=20fallback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/stop-emrg.cmd | 1 - tests/test_doc_counts.py | 2 +- tests/test_installer_stop.py | 5 +++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bin/stop-emrg.cmd b/bin/stop-emrg.cmd index 921340a..3cec911 100644 --- a/bin/stop-emrg.cmd +++ b/bin/stop-emrg.cmd @@ -78,7 +78,6 @@ REM matched. 0.2.26's cmd inline-PowerShell quoting escape is avoided by call REM the standalone stop-git.ps1 via -File.) set "GITSTOP=%INSTALL%\bin\stop-git.ps1" if not exist "%GITSTOP%" set "GITSTOP=%~1" -if not exist "%GITSTOP%" set "GITSTOP=%TEMP%\emrg-stop-git.ps1" if exist "%GITSTOP%" ( powershell -NoProfile -ExecutionPolicy Bypass -File "%GITSTOP%" >nul 2>&1 if errorlevel 1 set "EXIT_CODE=1" diff --git a/tests/test_doc_counts.py b/tests/test_doc_counts.py index ee43d62..4f119b2 100644 --- a/tests/test_doc_counts.py +++ b/tests/test_doc_counts.py @@ -10,7 +10,7 @@ after #580 added 3 GUI tests. Both checks now cover all three docs (README.md, README.cn.md, Agent.md); CJK full-width parens and the "项:" separator are normalized before matching. -#697: rant 2026-08-11T19:50:37 — README.md/README.cn.md switched to the +#692: rant 2026-08-11T19:50:37 — README.md/README.cn.md switched to the Tests badge (no hardcoded counts); the python-count check now guards Agent.md only, while the GUI-breakdown check still picks up any "(N: ...)" line it finds in any doc (Agent.md keeps the breakdown). diff --git a/tests/test_installer_stop.py b/tests/test_installer_stop.py index 1cd542c..6d742a9 100644 --- a/tests/test_installer_stop.py +++ b/tests/test_installer_stop.py @@ -49,9 +49,10 @@ def test_stop_emrg_cmd_covers_gui_tui_daemon_and_calls_stop_git(): assert "stop-git.ps1" in content assert "-ExecutionPolicy Bypass -File" in content assert r'"%GITSTOP%"' in content - # 旧安装可能没有 stop-git.ps1 → {tmp} 提取版(%~1)与 %TEMP% 兜底 + # 旧安装可能没有 stop-git.ps1 → {tmp} 提取版(%~1)兜底 assert 'set "GITSTOP=%~1"' in content - assert 'set "GITSTOP=%TEMP%\\emrg-stop-git.ps1"' in content + # %TEMP% 兜底已移除(review 2026-08-11T19:57: {tmp} 参数或已装副本恒胜出;缺失时 :verify 前缀检查兜底) + assert "GITSTOP=%TEMP%" not in content # #689 的 snapshot/step0/PIDFILE 机制已删除(宿主否决——误伤宿主工具的根源) assert "emrg-stop-pids.txt" not in content assert "ParentProcessId" not in content