diff --git a/Agent.md b/Agent.md index 2530a48a..fdfc00dd 100644 --- a/Agent.md +++ b/Agent.md @@ -118,7 +118,7 @@ Community needs voiced in HN agent-UI discussions map directly to EMRG's design: 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"` +Python: `uv run pytest tests/ -v` (989) — import check: `uv run python -c "from emrg.client.app import run_client"` GUI: `cd emrg/gui && npm test` (256: 45 daemon_client + 19 conn-manager + 22 app-commands + 127 renderer smoke + 15 i18n + 8 integration + 3 commands + 8 build-config + 7 gui-state + 2 tool-group) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js` CI: `uv run pytest` (ubuntu + **windows-2025 matrix** — Windows pytest 回归在 PR CI 即失败,v0.2.29 教训 #725) + GUI tests + **actionlint workflow lint** (`rhysd/actionlint@v1.7.12` gate, #444 — workflow 解析错误在 PR CI 即失败,如 `if:` secrets 上下文) Re-trigger: `scripts/re-trigger-ci.sh [branch]` (workflow_dispatch, #527 — 替代空 commit 重触发:Actions outage 会整段丢弃 push 事件,dispatch 走 API 路径不受影响) diff --git a/emrg/gui/main.js b/emrg/gui/main.js index 56a62a0d..d2b27af3 100644 --- a/emrg/gui/main.js +++ b/emrg/gui/main.js @@ -285,6 +285,7 @@ vision = false model: pong?.model || "", evolution_count: pong?.evolution_count ?? 0, // G19:init 透传演化计数(waitForPong 已消耗 pong) current_version: pong?.current_version || "", // rant 18:30:57:安装版本(GUI 对比显示升级横幅) + previous_version: pong?.previous_version || "", // rant 12:44:34:升级前版本(横幅 from→to) version: APP_VERSION, // WorkBuddy P3:版本号随 package.json 走(此前 renderer 硬编码 v0.2.7) sessions, open_sessions: openSessionsList(), @@ -871,7 +872,7 @@ vision = false const sessions = await listSessions(); sendToRenderer("sessions", { sessions }); const pong = await waitForPong(); - sendToRenderer("status", { connected: true, server_id: pong?.identity?.instance_id, model: pong?.model, current_version: pong?.current_version || "" }); + sendToRenderer("status", { connected: true, server_id: pong?.identity?.instance_id, model: pong?.model, current_version: pong?.current_version || "", previous_version: pong?.previous_version || "" }); logger.info("[gui] connManager recovery complete"); } catch (e) { logger.warn(`[gui] post-recovery refresh failed: ${e.message}`); @@ -1098,7 +1099,7 @@ vision = false const sessions = await listSessions(); sendToRenderer("sessions", { sessions }); const pong = await waitForPong(); - sendToRenderer("status", { connected: true, server_id: pong?.identity?.instance_id, model: pong?.model, current_version: pong?.current_version || "" }); + sendToRenderer("status", { connected: true, server_id: pong?.identity?.instance_id, model: pong?.model, current_version: pong?.current_version || "", previous_version: pong?.previous_version || "" }); } }, delay); } diff --git a/emrg/gui/renderer/js/app.js b/emrg/gui/renderer/js/app.js index dd7a3191..cdaaf504 100644 --- a/emrg/gui/renderer/js/app.js +++ b/emrg/gui/renderer/js/app.js @@ -77,6 +77,7 @@ const App = (() => { state.model = init.model || ""; state.version = init.version || ""; state.currentVersion = init.current_version || ""; // rant 18:30:57:安装版本(升级横幅对比基准) + state.previousVersion = init.previous_version || ""; // rant 12:44:34:升级前版本(横幅 from→to) state.lastKnownVersion = state.currentVersion; state.evolutionCount = init.evolution_count ?? null; state.lastKnownEvolutionCount = state.evolutionCount; @@ -1337,14 +1338,21 @@ const App = (() => { $("conn-banner").classList.add("hidden"); } - // ── 升级完成横幅(rant 2026-08-20T18:30:57) ───────────── - function maybeShowUpgradeBanner(currentVersion) { + // ── 升级完成横幅(rant 2026-08-20T18:30:57 + 2026-08-21T12:44:34) ───── + function maybeShowUpgradeBanner(currentVersion, previousVersion) { if (!currentVersion) return; // 无版本数据(dev 运行)→ 不显示 if (currentVersion === state.lastKnownVersion) return; // 版本未变 const b = $("upgrade-banner"); if (!b) return; const msg = $("upgrade-banner-msg"); - if (msg) msg.textContent = _t("app.upgradeBannerMsg", { version: currentVersion }); + if (msg) { + // rant 12:44:34:daemon 提供升级前版本 → 显示 "from → to";否则回退旧文案 + if (previousVersion && previousVersion !== currentVersion) { + msg.textContent = _t("app.upgradeBannerMsgFromTo", { from: previousVersion, to: currentVersion }); + } else { + msg.textContent = _t("app.upgradeBannerMsg", { version: currentVersion }); + } + } b.classList.remove("hidden"); state.lastKnownVersion = currentVersion; // 已提示,防重复弹 } @@ -1574,7 +1582,9 @@ const App = (() => { if (data.model) state.model = data.model; state.evolutionCount = data.evolution_count ?? state.evolutionCount; // rant 18:30:57:pong 携带 current_version → 对比已知版本,变化则弹升级横幅 - maybeShowUpgradeBanner(data.current_version || state.currentVersion); + // rant 12:44:34:同时携带 previous_version → 横幅显示 from → to + if (data.previous_version) state.previousVersion = data.previous_version; + maybeShowUpgradeBanner(data.current_version || state.currentVersion, state.previousVersion); updateModelSwitcher(); updateGrowthCard(); maybeShowEvolutionToast(); diff --git a/emrg/gui/renderer/js/i18n.js b/emrg/gui/renderer/js/i18n.js index 783205d6..afb085a4 100644 --- a/emrg/gui/renderer/js/i18n.js +++ b/emrg/gui/renderer/js/i18n.js @@ -415,6 +415,7 @@ const I18N = (() => { "app.modelSwitchFailed": "切换模型失败了:{msg}", "app.modelListFailed": "读取模型列表失败了:{msg}", "app.upgradeBannerMsg": "EMRG 已升级到 {version},重启后生效", + "app.upgradeBannerMsgFromTo": "EMRG 已从 {from} 升级到 {to},重启后生效", "app.upgradeRestartFailed": "重启失败:{msg}", "app.upgradeRestartBtn": "重启生效", "app.unknownResult": "结果未知——连接中断", @@ -826,6 +827,7 @@ const I18N = (() => { "app.modelSwitchFailed": "Failed to switch model: {msg}", "app.modelListFailed": "Failed to load models: {msg}", "app.upgradeBannerMsg": "EMRG upgraded to {version} — restart to apply", + "app.upgradeBannerMsgFromTo": "EMRG upgraded from {from} to {to} — restart to apply", "app.upgradeRestartFailed": "Restart failed: {msg}", "app.upgradeRestartBtn": "Restart to apply", "app.unknownResult": "Result unknown — connection lost", diff --git a/emrg/gui/test/renderer.smoke.test.js b/emrg/gui/test/renderer.smoke.test.js index b557a631..2663ae36 100644 --- a/emrg/gui/test/renderer.smoke.test.js +++ b/emrg/gui/test/renderer.smoke.test.js @@ -735,13 +735,14 @@ test("GCM rant Stage 2:演化增长 + 未认证 → GitHub 连接横幅出现 assert.ok(toastBlock.includes("maybeShowGithubBanner()"), "演化增长应触发 GitHub 横幅检查"); }); -test("rant 18:30:57:版本变化 → 升级横幅出现 + 重启按钮触发 restartDaemon(正反两态)", async () => { - // 正态:status current_version 与已知版本不同 → 横幅出现 +test("rant 18:30:57 + 12:44:34:版本变化 → 升级横幅(from→to)+ 重启按钮触发 restartDaemon(正反两态)", async () => { + // 正态:status current_version 与已知版本不同 → 横幅出现;有 previous_version → 显示 from→to const { ctx } = makeSandbox({ init: async () => ({ config_exists: true, api_key_configured: true, current_version: "0.2.58", + previous_version: "", sessions: [], }), }); @@ -749,13 +750,15 @@ test("rant 18:30:57:版本变化 → 升级横幅出现 + 重启按钮触发 r await vm.runInContext(`(function() { document.getElementById("upgrade-banner").classList.add("hidden"); App.state.lastKnownVersion = "0.2.58"; - App.handleEvent({ type: "status", data: { connected: true, current_version: "0.2.59" } }); + App.handleEvent({ type: "status", data: { connected: true, current_version: "0.2.61", previous_version: "0.2.57" } }); })()`, ctx); const visible = vm.runInContext( '!document.getElementById("upgrade-banner").classList.contains("hidden")', ctx ); assert.strictEqual(visible, true, "版本变化 → 升级横幅应出现"); + const bannerText = vm.runInContext('document.getElementById("upgrade-banner-msg").textContent', ctx); + assert.ok(bannerText.includes("0.2.57") && bannerText.includes("0.2.61"), `横幅应显示 from→to,实际 ${bannerText}`); // 负态:版本未变 → 横幅保持隐藏 const { ctx: ctx2 } = makeSandbox({}); diff --git a/emrg/server/daemon.py b/emrg/server/daemon.py index 15f97053..7e14499a 100644 --- a/emrg/server/daemon.py +++ b/emrg/server/daemon.py @@ -645,6 +645,21 @@ def _current_installed_version(self) -> str: except (OSError, ValueError): return "" + def _previous_installed_version(self) -> str: + """Pre-upgrade EMRG version from ~/.emrg/install/previous-version.txt. + + Rant 2026-08-21T12:44:34: the upgrade agent writes the version it is + replacing into previous-version.txt before overwriting version.txt, + so the GUI banner can show "upgraded from X to Y" instead of only the + target version. Raw data only; "" when missing (dev/standalone or + first install). + """ + try: + v = (Path.home() / ".emrg" / "install" / "previous-version.txt").read_text(encoding="utf-8").strip() + return v + except (OSError, ValueError): + return "" + def _evolution_count(self) -> int: """Total completed evolution cycles across scheduler handlers + disk. @@ -1421,6 +1436,9 @@ async def _process_message( # 上次已知版本,发现变化 → 弹"已升级,重启生效"横幅。daemon 只回原始数据, # 零判断逻辑(升级判断由 GUI 负责)。 "current_version": self._current_installed_version(), + # Rant 2026-08-21T12:44:34:并入升级前版本——GUI 横幅显示 "from → to" + # (升级 agent 在覆盖 version.txt 前写入 previous-version.txt)。 + "previous_version": self._previous_installed_version(), }) return diff --git a/emrg/server/prompts/upgrade_prompt.j2 b/emrg/server/prompts/upgrade_prompt.j2 index fd39973f..4ac00cfd 100644 --- a/emrg/server/prompts/upgrade_prompt.j2 +++ b/emrg/server/prompts/upgrade_prompt.j2 @@ -44,6 +44,11 @@ release installer" means for this version. update or add — the target tag's pyproject.toml dependencies are the source of truth. Operate as needed (pip --target install/uninstall/update). - `{{ install_dir }}/version.txt` ← {{ target_tag }} (the version). +- Before overwriting `version.txt`: if it already exists with a different + content, copy its current content to `{{ install_dir }}/previous-version.txt` + (rant 2026-08-21T12:44:34 — the GUI upgrade banner shows "from X to Y"). + If `version.txt` did not exist (first install), delete any stale + `previous-version.txt`. - `{{ install_dir }}/bin/` launcher scripts (emrg/emrgd/emrg.cmd/emrgd.cmd etc.) — sync if the target tag changed them. - GUI as needed: whether `~/Applications/EMRG.app` is updated is YOUR call — diff --git a/tests/test_daemon.py b/tests/test_daemon.py index 0c8a2508..9412a546 100644 --- a/tests/test_daemon.py +++ b/tests/test_daemon.py @@ -1751,3 +1751,38 @@ def test_pong_includes_current_version(tmp_path, monkeypatch): frame = _last_frame(writer) assert frame["type"] == "pong" assert frame["current_version"] == "0.2.59" + + +def test_pong_includes_previous_version(tmp_path, monkeypatch): + """Pong carries previous_version from ~/.emrg/install/previous-version.txt. + + Rant 2026-08-21T12:44:34: the upgrade agent records the pre-upgrade + version in previous-version.txt before overwriting version.txt, so the + GUI banner can show "upgraded from X to Y". Missing file → "" (dev runs + or first install), never an error. + """ + import emrg.server.daemon as daemon_mod + install = tmp_path / ".emrg" / "install" + install.mkdir(parents=True) + (install / "version.txt").write_text("0.2.61\n", encoding="utf-8") + (install / "previous-version.txt").write_text("0.2.57\n", encoding="utf-8") + + monkeypatch.setattr(daemon_mod.Path, "home", lambda: tmp_path) + + server = _make_server() + assert server._previous_installed_version() == "0.2.57" + + # Missing file → "" (no crash) + empty = tmp_path / "no-install" + monkeypatch.setattr(daemon_mod.Path, "home", lambda: empty) + assert server._previous_installed_version() == "" + + # Pong payload includes the field + monkeypatch.setattr(daemon_mod.Path, "home", lambda: tmp_path) + writer = _FakeWriter() + import asyncio + asyncio.run(server._process_message({"type": "ping"}, writer)) + frame = _last_frame(writer) + assert frame["type"] == "pong" + assert frame["current_version"] == "0.2.61" + assert frame["previous_version"] == "0.2.57"