diff --git a/Agent.md b/Agent.md index 271aefbe..d6439f9c 100644 --- a/Agent.md +++ b/Agent.md @@ -118,8 +118,8 @@ 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` (955) — import check: `uv run python -c "from emrg.client.app import run_client"` -GUI: `cd emrg/gui && npm test` (259: 45 daemon_client + 19 conn-manager + 22 app-commands + 130 renderer smoke + 16 i18n + 7 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` +Python: `uv run pytest tests/ -v` (958) — import check: `uv run python -c "from emrg.client.app import run_client"` +GUI: `cd emrg/gui && npm test` (260: 45 daemon_client + 19 conn-manager + 22 app-commands + 131 renderer smoke + 16 i18n + 7 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/renderer/css/components.css b/emrg/gui/renderer/css/components.css index 9c514828..d7969798 100644 --- a/emrg/gui/renderer/css/components.css +++ b/emrg/gui/renderer/css/components.css @@ -1024,6 +1024,65 @@ dialog::backdrop { border-radius: 8px; border: 1px solid var(--warn-soft, var(--border)); } +/* rant 2026-08-18T21:32:32:任务卡点击展开的最近运行子表(时间/干了什么/降频) */ +.task-run-detail { + flex-basis: 100%; + display: flex; + flex-direction: column; + gap: 2px; + margin-top: 4px; + padding-top: 4px; + border-top: 1px dashed var(--border); + min-width: 0; +} +.task-run-detail.hidden { + display: none; +} +.task-run-head, +.task-run-row { + display: grid; + grid-template-columns: 72px 1fr 92px; + gap: 6px; + font-size: var(--fs-aux); + min-width: 0; + align-items: center; +} +.task-run-head { + color: var(--text-3); + font-weight: 600; +} +.task-run-time { + color: var(--text-3); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.task-run-done { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--text-2); +} +.task-run-flag { + display: flex; + gap: 4px; + align-items: center; + min-width: 0; +} +.task-run-badge-warn { + background: var(--warn-soft, var(--bg-soft)); + color: var(--warn, var(--text-3)); + border-color: var(--warn-soft, var(--border)); +} +.task-run-badge-idle { + color: var(--text-3); +} +.task-run-empty { + flex-basis: 100%; + font-size: var(--fs-aux); + color: var(--text-3); + padding: 4px 0; +} /* rant 09:17:45:提示词编辑器 Monaco 挂载容器(长提示词 300px 起步) */ .monaco-host { diff --git a/emrg/gui/renderer/js/dialogs.js b/emrg/gui/renderer/js/dialogs.js index 845d58d0..448c7797 100644 --- a/emrg/gui/renderer/js/dialogs.js +++ b/emrg/gui/renderer/js/dialogs.js @@ -699,6 +699,39 @@ const Dialogs = (() => { } catch { taskProjects = []; } } + // rant 2026-08-18T21:32:32:任务卡点击 → 手风琴展开最近运行子表 + // (时间 / 干了什么 / 降频)。数据来自 daemon handler.status().recent_runs + // (最多 5 条);旧数据无 summary → fallback impact 拼接;无记录 → 占位文案。 + function buildTaskRunDetail(t) { + const wrap = el("div", { class: "task-run-detail hidden" }); + const runs = Array.isArray(t.recent_runs) ? t.recent_runs : []; + if (runs.length === 0) { + wrap.appendChild(el("div", { class: "task-run-empty" }, _t("app.taskRunsEmpty"))); + return wrap; + } + const head = el("div", { class: "task-run-head" }); + head.appendChild(el("span", {}, _t("app.taskRunsColTime"))); + head.appendChild(el("span", {}, _t("app.taskRunsColDone"))); + head.appendChild(el("span", {}, _t("app.taskRunsColThrottle"))); + wrap.appendChild(head); + for (const r of runs) { + const row = el("div", { class: "task-run-row" }); + row.appendChild(el("span", { class: "task-run-time" }, formatRelativeTime(r.timestamp))); + let done = (typeof r.summary === "string" && r.summary) ? r.summary : ""; + if (!done && Array.isArray(r.impact) && r.impact.length) done = r.impact.join(", "); + row.appendChild(el("span", { class: "task-run-done" }, done || "-")); + const flagCell = el("span", { class: "task-run-flag" }); + if (r.recommend_slowdown) { + flagCell.appendChild(el("span", { class: "task-badge task-run-badge-warn" }, _t("app.taskRunThrottle"))); + } else if (r.meaningful === false) { + flagCell.appendChild(el("span", { class: "task-badge task-run-badge-idle" }, _t("app.taskRunIdle"))); + } + row.appendChild(flagCell); + wrap.appendChild(row); + } + return wrap; + } + async function renderTaskList() { const list = $("task-list"); if (!list) return; // 元素缺失(测试桩)时忽略 @@ -804,6 +837,17 @@ const Dialogs = (() => { }); actions.appendChild(delBtn); row.appendChild(actions); + // rant 2026-08-18T21:32:32:点击任务卡(非按钮)→ 手风琴展开最近运行子表。 + // 真实 DOM 中按钮点击通过 e.target.closest("button") 拦截(不触发展开); + // 测试沙箱的 click() 无 target → 直接切换(沙箱中按钮点击不冒泡,互不影响)。 + const runDetail = buildTaskRunDetail(t); + row.appendChild(runDetail); + let runDetailOpen = false; + row.addEventListener("click", (e) => { + if (e && e.target && typeof e.target.closest === "function" && e.target.closest("button")) return; + runDetailOpen = !runDetailOpen; + runDetail.classList.toggle("hidden", !runDetailOpen); + }); list.appendChild(row); } taskCountdowns = countdowns; diff --git a/emrg/gui/renderer/js/i18n.js b/emrg/gui/renderer/js/i18n.js index 0d800db8..d9950757 100644 --- a/emrg/gui/renderer/js/i18n.js +++ b/emrg/gui/renderer/js/i18n.js @@ -412,6 +412,12 @@ const I18N = (() => { "app.taskLastRunSummary": "干了:{n}", "app.taskNoRunYet": "尚未运行", "app.taskSaturation": "空转 {n} 轮,降频至 {m}s heartbeat", + "app.taskRunsEmpty": "暂无运行记录", + "app.taskRunsColTime": "时间", + "app.taskRunsColDone": "干了什么", + "app.taskRunsColThrottle": "降频", + "app.taskRunThrottle": "建议降频", + "app.taskRunIdle": "空转", "app.deletedSwitch": "这个对话已被删除,已帮你切到最近的对话。", "app.switched": "已切换对话。", "app.sessionDisconnected": "该会话连接已断开,正在自动重连…", @@ -836,6 +842,12 @@ const I18N = (() => { "app.taskLastRunSummary": "did: {n}", "app.taskNoRunYet": "never ran", "app.taskSaturation": "{n} empty cycles, throttled to {m}s heartbeat", + "app.taskRunsEmpty": "No run records yet", + "app.taskRunsColTime": "Time", + "app.taskRunsColDone": "What was done", + "app.taskRunsColThrottle": "Throttle", + "app.taskRunThrottle": "slowdown advised", + "app.taskRunIdle": "idle", "app.deletedSwitch": "This conversation was deleted — switched to the most recent one.", "app.switched": "Conversation switched.", "app.sessionDisconnected": "This session's connection is lost — reconnecting automatically…", diff --git a/emrg/gui/test/renderer.smoke.test.js b/emrg/gui/test/renderer.smoke.test.js index 0dd80596..fa87bad1 100644 --- a/emrg/gui/test/renderer.smoke.test.js +++ b/emrg/gui/test/renderer.smoke.test.js @@ -2645,6 +2645,61 @@ test("P3:设置面板打开 → 任务列表渲染(名称/类型/项目/间 assert.strictEqual(updated.enabled, true); }); +test("rant 21:32:32:任务卡点击展开最近运行子表(时间/干了什么/降频徽章)", async () => { + const { ctx, els } = makeSandbox({ + listTasks: async () => [ + { + name: "emrg-task", type: "evolution", config: { project: "emrg" }, + interval: 60, enabled: true, last_run_at: "2026-08-18T10:00:00", + last_cycle_summary: "修了双实例根因", + recent_runs: [ + { timestamp: "2026-08-18T10:00:00", summary: "修了双实例根因,提交 PR #854", + impact: ["cycle-ts-complete", "tools-executed=26"], meaningful: true, + recommend_slowdown: false, tool_count: 26 }, + { timestamp: "2026-08-18T09:00:00", summary: "", + impact: ["cycle-ts-complete", "tools-executed=3"], meaningful: false, + recommend_slowdown: false, tool_count: 3 }, + { timestamp: "2026-08-18T08:00:00", summary: "NTE", + impact: ["cycle-ts-complete"], meaningful: false, + recommend_slowdown: true, tool_count: 0 }, + ], + }, + { name: "fresh-task", type: "sync", config: { project: "docs" }, interval: 3600, enabled: true }, + ], + taskTemplateList: async () => [], + listProjects: async () => [{ name: "emrg", path: "/p/emrg" }], + }); + await tick(); + await vm.runInContext("App.openTasksPanel()", ctx); + await tick(); + // 初始:子表隐藏(行内含 .task-run-detail.hidden) + const hiddenBefore = vm.runInContext(`Array.from(document.getElementById("task-list").children[0].querySelectorAll(".task-run-detail")).every((d) => d.classList.contains("hidden"))`, ctx); + assert.strictEqual(hiddenBefore, true, "子表初始应隐藏"); + // 点击任务卡 → 展开 + await vm.runInContext(`document.getElementById("task-list").children[0].click()`, ctx); + await tick(); + const detail = vm.runInContext(`document.getElementById("task-list").children[0].querySelector(".task-run-detail")`, ctx); + assert.strictEqual(detail.classList.contains("hidden"), false, "点击后子表应展开"); + // 第一行 run:Agent 总结展示(自然语言) + const doneTxt = vm.runInContext(`document.getElementById("task-list").children[0].querySelectorAll(".task-run-done")[0].textContent`, ctx); + assert.strictEqual(doneTxt, "修了双实例根因,提交 PR #854", `子表应显示 Agent 总结,实际: ${doneTxt}`); + // 降频徽章:recommend_slowdown=true → 建议降频;meaningful=false → 空转 + const warnCount = vm.runInContext(`document.getElementById("task-list").children[0].querySelectorAll(".task-run-badge-warn").length`, ctx); + assert.ok(warnCount >= 1, "recommend_slowdown=true 应显示建议降频徽章"); + const idleCount = vm.runInContext(`document.getElementById("task-list").children[0].querySelectorAll(".task-run-badge-idle").length`, ctx); + assert.ok(idleCount >= 1, "meaningful=false 应显示空转徽章"); + // 旧数据无 summary → fallback impact 拼接(第二行 run) + const doneTexts = vm.runInContext(`Array.from(document.getElementById("task-list").children[0].querySelectorAll(".task-run-done")).map((n) => n.textContent)`, ctx); + assert.ok(doneTexts[1].includes("cycle-ts-complete"), `无 summary 应 fallback impact,实际: ${doneTexts[1]}`); + // 再次点击 → 折叠 + await vm.runInContext(`document.getElementById("task-list").children[0].click()`, ctx); + await tick(); + assert.strictEqual(vm.runInContext(`document.getElementById("task-list").children[0].querySelector(".task-run-detail").classList.contains("hidden")`, ctx), true, "再次点击应折叠"); + // 无 recent_runs → 占位文案 + const emptyTxt = vm.runInContext(`document.getElementById("task-list").children[1].querySelector(".task-run-empty").textContent`, ctx); + assert.ok(emptyTxt.includes("暂无运行记录"), `无 recent_runs 应显示占位,实际: ${emptyTxt}`); +}); + test("P3:新增任务表单 —— 间隔 <60 客户端拒绝;≥60 提交 taskCreate", async () => { let created = null; const { ctx, els } = makeSandbox({ diff --git a/emrg/protocol.py b/emrg/protocol.py index c2fdbfcd..7b6a543c 100644 --- a/emrg/protocol.py +++ b/emrg/protocol.py @@ -125,6 +125,13 @@ class EvolutionLog: impact: list[str] = field(default_factory=list) operations: list[str] = field(default_factory=list) upstream_contribution: Optional[dict] = None + # rant 2026-08-18T21:32:32: the agent's own natural-language summary of + # what meaningful work was done this cycle (from the vibe check "done" + # field) + the vibe result flags, surfaced in GUI task recent-runs. + summary: str = "" + meaningful: Optional[bool] = None + recommend_slowdown: bool = False + tool_count: int = 0 @dataclass diff --git a/emrg/server/daemon.py b/emrg/server/daemon.py index 9772f8a0..298169cd 100644 --- a/emrg/server/daemon.py +++ b/emrg/server/daemon.py @@ -1154,7 +1154,11 @@ async def _task_vibe_check(self, task_name: str, prompt: str, completion_summary Asks whether a just-finished scheduled task produced meaningful value. The agent must answer in strict JSON: - ``{"meaningful": bool, "recommend_slowdown": bool, "reason": str}``. + ``{"meaningful": bool, "recommend_slowdown": bool, "reason": str, "done": str}``. + + ``done`` (rant 2026-08-18T21:32:32) is a natural-language summary of + what meaningful work was done this cycle, for humans to read in the + GUI task recent-runs table. Old models / old parsing omit it → "". Raises on any failure (caller sends ``ok: false``); the scheduler conservatively leaves its empty-cycle counter unchanged then. @@ -1164,12 +1168,14 @@ async def _task_vibe_check(self, task_name: str, prompt: str, completion_summary "任务要求与最终回复摘要如下。\n" "请用 JSON 严格回答(不要任何其他文字),格式:\n" '{"meaningful": true|false, "recommend_slowdown": true|false, ' - '"reason": "一句话原因"}\n' + '"reason": "一句话原因", "done": "这次干了哪些有意义有价值的事"}\n' "- meaningful:这轮是否对项目产生了有意义的价值(产出/提交/分析/决策/" "维护动作都算;纯空转/无可做=NTE 算 false)\n" "- recommend_slowdown:若本任务长期无有意义产出,是否建议降频省 token" "(true=建议降低检查频率)\n" - "- reason:简短中文原因" + "- reason:简短中文原因(给降频判断用)\n" + "- done:这次干了哪些有意义有价值的事(自然语言列举,如:修了 XX bug / " + "加了 XX 功能 / 分析了 XX;没有则空字符串)" ) user = ( "任务名称:" + (task_name or "") + "\n" @@ -1193,6 +1199,7 @@ async def _task_vibe_check(self, task_name: str, prompt: str, completion_summary "meaningful": bool(data.get("meaningful")), "recommend_slowdown": bool(data.get("recommend_slowdown")), "reason": str(data.get("reason", ""))[:200], + "done": str(data.get("done", ""))[:500], } def _build_system_prompt(self, session: Session | None = None) -> str: diff --git a/emrg/server/scheduler.py b/emrg/server/scheduler.py index 2f93a5e1..7027607c 100644 --- a/emrg/server/scheduler.py +++ b/emrg/server/scheduler.py @@ -750,9 +750,26 @@ def status(self) -> dict: if self.evolutions: last = self.evolutions[-1] last_run_at = last.timestamp - # brief summary: tools-executed / cycle-complete etc. from impact - parts = [str(i) for i in last.impact if i] - last_cycle_summary = ", ".join(parts[:3]) if parts else None + # brief summary: agent's natural-language "done" summary preferred + # (rant 2026-08-18T21:32:32), fallback to machine impact tags. + if last.summary: + last_cycle_summary = last.summary + else: + parts = [str(i) for i in last.impact if i] + last_cycle_summary = ", ".join(parts[:3]) if parts else None + # rant 2026-08-18T21:32:32: last 5 run records for the GUI accordion + # subtable — {timestamp, summary, impact, meaningful, + # recommend_slowdown, tool_count}; all in-memory, no extra I/O. + recent_runs = [] + for log in self.evolutions[-5:]: + recent_runs.append({ + "timestamp": log.timestamp, + "summary": log.summary, + "impact": list(log.impact), + "meaningful": log.meaningful, + "recommend_slowdown": log.recommend_slowdown, + "tool_count": log.tool_count, + }) saturation = { "empty_cycles": self._empty_cycles, "threshold": self._saturation_threshold(), @@ -766,6 +783,7 @@ def status(self) -> dict: "interval": self.interval, "last_run_at": last_run_at, "last_cycle_summary": last_cycle_summary, + "recent_runs": recent_runs, "saturation": saturation, } @@ -851,6 +869,7 @@ async def _request_vibe_check(self, ws, prompt: str, completion_summary: str) -> "meaningful": result.get("meaningful"), "recommend_slowdown": result.get("recommend_slowdown"), "reason": result.get("reason", ""), + "done": result.get("done", ""), } except Exception: logger.debug("TaskHandler[%s]: vibe check failed", self.name, exc_info=True) @@ -1065,11 +1084,31 @@ async def _run_evolution_cycle(self) -> None: if truncated: impact.append("truncated=max-tool-rounds") + # rant 2026-08-18T21:32:32: persist the agent's own summary of what + # meaningful work was done (vibe check "done" field) + the vibe flags, + # so the GUI task recent-runs table shows real value, not a machine + # string. Fallbacks: vibe unavailable → None flags + first line of the + # completion summary as a rough summary (never crash). + summary = "" + meaningful = None + recommend = False + if vibe_result is not None: + summary = str(vibe_result.get("done") or "")[:500] + meaningful = vibe_result.get("meaningful") + recommend = bool(vibe_result.get("recommend_slowdown")) + if not summary and completion_content: + first = completion_content.strip().splitlines()[0] if completion_content.strip() else "" + summary = first[:500] + log = EvolutionLog( timestamp=cycle_ts, trigger=f"evolution-{self.name}-{cycle_ts}", impact=impact, operations=["llm-reflection", "tool-execution", "self-improvement"], + summary=summary, + meaningful=meaningful, + recommend_slowdown=recommend, + tool_count=tool_count, ) await self._write_evolution_log(log) self.evolutions.append(log) @@ -1126,6 +1165,10 @@ async def _write_evolution_log(self, entry: EvolutionLog) -> None: "trigger": entry.trigger, "impact": entry.impact, "operations": entry.operations, + "summary": entry.summary, + "meaningful": entry.meaningful, + "recommend_slowdown": entry.recommend_slowdown, + "tool_count": entry.tool_count, } path.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8") diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py index 11d84fc4..cbd3641f 100644 --- a/tests/test_scheduler.py +++ b/tests/test_scheduler.py @@ -506,6 +506,8 @@ def test_evolution_handler_status_last_run_fields(): assert "threshold" in st["saturation"] assert "heartbeat_interval" in st["saturation"] assert "heartbeat_active" in st["saturation"] + # rant 2026-08-18T21:32:32: recent_runs present, empty before any run + assert st["recent_runs"] == [] # after one evolution → last-run populated from the latest log handler.evolutions.append(EvolutionLog( timestamp="2026-08-18T10:00:00", @@ -518,6 +520,52 @@ def test_evolution_handler_status_last_run_fields(): assert st["last_run_at"] == "2026-08-18T10:00:00" assert st["last_cycle_summary"] == "tools-executed=24, cycle-complete" assert st["saturation"]["empty_cycles"] == 3 + assert len(st["recent_runs"]) == 1 + r0 = st["recent_runs"][0] + assert r0["timestamp"] == "2026-08-18T10:00:00" + assert r0["summary"] == "" + assert r0["impact"] == ["tools-executed=24", "cycle-complete"] + assert r0["meaningful"] is None + assert r0["recommend_slowdown"] is False + assert r0["tool_count"] == 0 + # agent summary preferred over machine impact tags + handler.evolutions.append(EvolutionLog( + timestamp="2026-08-18T11:00:00", + trigger="evolution-test-ts", + impact=["tools-executed=5", "cycle-complete"], + operations=[], + summary="修了 stop_all 双实例根因,提交 PR #854", + meaningful=True, + recommend_slowdown=False, + tool_count=5, + )) + st = handler.status() + assert st["last_cycle_summary"] == "修了 stop_all 双实例根因,提交 PR #854" + assert len(st["recent_runs"]) == 2, "recent_runs holds last 5 runs" + assert st["recent_runs"][1]["summary"] == "修了 stop_all 双实例根因,提交 PR #854" + assert st["recent_runs"][1]["meaningful"] is True + assert st["recent_runs"][1]["tool_count"] == 5 + assert st["recent_runs"][0]["meaningful"] is None + + +def test_evolution_handler_recent_runs_capped_at_five(): + """recent_runs keeps only the last 5 evolutions (rant 2026-08-18T21:32:32).""" + from emrg.protocol import EvolutionLog + handler = TaskHandler( + name="test", config={}, interval=60, + identity=InstanceIdentity(), + ) + for i in range(7): + handler.evolutions.append(EvolutionLog( + timestamp=f"2026-08-18T10:0{i}:00", + trigger=f"t{i}", + impact=[f"cycle-{i}-complete"], + )) + st = handler.status() + runs = st["recent_runs"] + assert len(runs) == 5 + assert runs[0]["timestamp"] == "2026-08-18T10:02:00" + assert runs[-1]["timestamp"] == "2026-08-18T10:06:00" def test_evolution_handler_default_owner(): @@ -1549,7 +1597,8 @@ def test_evolution_cycle_complete_agent_says_meaningful_resets_streak(tmp_path): "done": True, "delta": False, "session_id": "s"}, {"type": "vibe_check_result", "ok": True, "result": {"meaningful": True, "recommend_slowdown": False, - "reason": "completed analysis"}}, + "reason": "completed analysis", + "done": "分析了 scheduler 空转判定 bug,写了 memory 记录"}}, ]) handler._empty_cycles = 5 handler._slowdown_hits = 2 @@ -1557,6 +1606,43 @@ def test_evolution_cycle_complete_agent_says_meaningful_resets_streak(tmp_path): assert handler._empty_cycles == 0, "meaningful work resets the empty streak" assert handler._slowdown_hits == 0, "meaningful work resets slowdown votes" assert "log" in captured + # rant 2026-08-18T21:32:32: agent's natural-language summary persisted + log = captured["log"] + assert log.summary == "分析了 scheduler 空转判定 bug,写了 memory 记录" + assert log.meaningful is True + assert log.recommend_slowdown is False + assert log.tool_count == 0 + + +def test_evolution_cycle_log_summary_falls_back_to_completion(tmp_path): + """Vibe check ok but missing 'done' → log.summary falls back to the first + line of the completion content; vibe unavailable → empty summary (never + crash). Rant 2026-08-18T21:32:32.""" + handler, captured = _make_cycle_handler(tmp_path, frames=[ + {"request_id": "r1", "content": "Reviewed PR and posted LGTM", + "done": True, "delta": False, "session_id": "s"}, + {"type": "vibe_check_result", "ok": True, + "result": {"meaningful": True, "recommend_slowdown": False, + "reason": "reviewed"}}, + ]) + asyncio.run(handler._run_evolution_cycle()) + log = captured["log"] + assert log.summary == "Reviewed PR and posted LGTM", \ + "missing done → completion first line fallback" + assert log.meaningful is True + + # vibe check entirely unavailable → summary falls back to the first line + # of the completion content (per rant design), flags None/False + handler2, captured2 = _make_cycle_handler(tmp_path, frames=[ + {"request_id": "r1", "content": "Done", "done": True, + "delta": False, "session_id": "s"}, + ]) + asyncio.run(handler2._run_evolution_cycle()) + log2 = captured2["log"] + assert log2.summary == "Done", "vibe unavailable → completion first line fallback" + assert log2.meaningful is None + assert log2.recommend_slowdown is False + assert log2.tool_count == 0 def test_evolution_cycle_vibe_unavailable_streak_unchanged(tmp_path): diff --git a/tests/test_ws_e2e.py b/tests/test_ws_e2e.py index 0483ba36..8561e872 100644 --- a/tests/test_ws_e2e.py +++ b/tests/test_ws_e2e.py @@ -182,7 +182,7 @@ async def _test(): try: async def fake_chat(messages, tools=None): # echo back a strict-JSON answer; fenced JSON tolerated - return {"content": '```json\n{"meaningful": false, "recommend_slowdown": true, "reason": "长期无产出"}\n```'} + return {"content": '```json\n{"meaningful": false, "recommend_slowdown": true, "reason": "长期无产出", "done": "分析了双实例根因,提交 PR #854"}\n```'} server.llm.chat = fake_chat ws = await connect_to_server() @@ -202,6 +202,9 @@ async def fake_chat(messages, tools=None): assert result.get("meaningful") is False assert result.get("recommend_slowdown") is True assert result.get("reason") == "长期无产出" + # rant 2026-08-18T21:32:32: natural-language "done" + # summary of what meaningful work was done this cycle + assert result.get("done") == "分析了双实例根因,提交 PR #854" # the ask must carry the fixed system prompt + no tools sent = server.llm.chat assert sent is fake_chat @@ -211,6 +214,38 @@ async def fake_chat(messages, tools=None): await cleanup() asyncio.run(_test()) + def test_vibe_check_missing_done_field_is_compatible(self): + """Old models / old parsing omit 'done' → empty string, no crash.""" + async def _test(): + with tempfile.TemporaryDirectory() as tmp: + server, _, cleanup = await _boot_server(Path(tmp)) + try: + async def fake_chat(messages, tools=None): + return {"content": '{"meaningful": true, "recommend_slowdown": false, "reason": "ok"}'} + server.llm.chat = fake_chat + + ws = await connect_to_server() + try: + await ws.send(json.dumps({ + "type": "task_vibe_check", + "session_id": "s-vibe", + "task_name": "t", + "prompt": "p", + "completion_summary": "c", + })) + frame = await asyncio.wait_for(ws.recv(), timeout=10) + data = json.loads(frame) + assert data.get("type") == "vibe_check_result" + assert data.get("ok") is True + result = data.get("result", {}) + assert result.get("done") == "" + assert result.get("meaningful") is True + finally: + await ws.close() + finally: + await cleanup() + asyncio.run(_test()) + def test_vibe_check_bad_llm_answer_returns_ok_false(self): async def _test(): with tempfile.TemporaryDirectory() as tmp: