diff --git a/Agent.md b/Agent.md index 82d4e6e..2817b5b 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.port; python -m emrg ``` -Python: `uv run pytest tests/ -v` (799) — import check: `uv run python -c "from emrg.client.app import run_client"` +Python: `uv run pytest tests/ -v` (800) — import check: `uv run python -c "from emrg.client.app import run_client"` GUI: `cd emrg/gui && npm test` (236: 44 daemon_client + 19 conn-manager + 22 app-commands + 114 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` (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/sessions_index.py b/emrg/sessions_index.py index 0ff5b36..549b241 100644 --- a/emrg/sessions_index.py +++ b/emrg/sessions_index.py @@ -158,6 +158,9 @@ def rebuild_sessions_index( Scans ``config_root`` recursively (covers ~/.emrg and anything nested under it) plus each explicit project path (covers projects outside ~/.emrg), and upserts every discovered session into ``config_root/sessions_index.json``. + Entries whose session directory no longer exists on disk are pruned (e.g. + sessions deleted out-of-band); entries pointing to live directories are + preserved even when the scan did not rediscover them. Returns the number of distinct sessions indexed. """ index_path = config_root / _INDEX_FILENAME @@ -177,5 +180,21 @@ def rebuild_sessions_index( for sid, sdir in found.items(): data[sid] = sdir + + # Prune stale entries whose session directory no longer exists on disk + # (removed out-of-band, outside the Session.delete hook). Rebuild stays a + # pure backfill for everything still alive — manual entries that point to a + # live directory are preserved. + stale = [] + for sid, sdir in data.items(): + try: + alive = Path(sdir).exists() + except (OSError, ValueError): + alive = False + if not alive: + stale.append(sid) + for sid in stale: + del data[sid] + _write(data, index_path) return len(data) diff --git a/tests/test_sessions_index.py b/tests/test_sessions_index.py index 865e5bf..8798f00 100644 --- a/tests/test_sessions_index.py +++ b/tests/test_sessions_index.py @@ -3,6 +3,7 @@ from __future__ import annotations import json +import shutil from pathlib import Path from emrg.session import Session @@ -138,16 +139,41 @@ def test_rebuild_skips_meta_without_sid(self, tmp_path): assert count == 0 def test_rebuild_preserves_manual_entries(self, tmp_path): + """Manual entries pointing to a live directory survive the rebuild.""" cfg = tmp_path / "cfg" cfg.mkdir(parents=True, exist_ok=True) + manual_dir = tmp_path / "manual_dir" + manual_dir.mkdir(parents=True, exist_ok=True) (cfg / "sessions_index.json").write_text( - json.dumps({"s_manual": "/tmp/manual"}), encoding="utf-8" + json.dumps({"s_manual": str(manual_dir)}), encoding="utf-8" ) self._make_session(cfg, "s_scanned") count = rebuild_sessions_index(cfg) assert count == 2 data = _load(cfg / "sessions_index.json") - assert data["s_manual"] == "/tmp/manual" + assert data["s_manual"] == str(manual_dir) + assert "s_scanned" in data + + def test_rebuild_prunes_stale_entries(self, tmp_path): + """Index entries whose session dir was deleted out-of-band are dropped.""" + cfg = tmp_path / "cfg" + cfg.mkdir(parents=True, exist_ok=True) + live_dir = tmp_path / "live" + live_dir.mkdir(parents=True, exist_ok=True) + dead_dir = tmp_path / "dead" + dead_dir.mkdir(parents=True, exist_ok=True) + (cfg / "sessions_index.json").write_text( + json.dumps({"s_live": str(live_dir), "s_dead": str(dead_dir)}), + encoding="utf-8", + ) + # Delete the dead session's directory out-of-band (no Session.delete hook). + shutil.rmtree(dead_dir) + self._make_session(cfg, "s_scanned") + count = rebuild_sessions_index(cfg) + assert count == 2 + data = _load(cfg / "sessions_index.json") + assert "s_dead" not in data + assert data["s_live"] == str(live_dir) assert "s_scanned" in data