Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` (747) — import check: `uv run python -c "from emrg.client.app import run_client"`
Python: `uv run pytest tests/ -v` (748) — import check: `uv run python -c "from emrg.client.app import run_client"`
GUI: `cd emrg/gui && npm test` (229: 44 daemon_client + 19 conn-manager + 22 app-commands + 107 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 路径不受影响)
Expand Down
22 changes: 22 additions & 0 deletions emrg/server/scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -1282,6 +1282,28 @@ def _ensure_self_evolution_task(self) -> None:
logger.info(
"TaskScheduler: self-heal — added emrg entry to projects.yml"
)
else:
# Repair a stale emrg entry whose path no longer exists
# (2026-08-12 incident: a pytest temp dir leaked into
# projects.yml by a test run; the dir is deleted after the
# suite, leaving the entry pointing at a dead path forever —
# list_projects/GUI pickers show a wrong path and the handler
# re-resolves a dangling dir every cycle). Dev machines with a
# real custom checkout keep their path (is_dir() True).
for entry in entries:
if entry.get("name") != "emrg":
continue
existing = entry.get("path")
if existing and Path(existing).is_dir():
break # real checkout — preserved as-is
entry["path"] = str(EVOLUTION_CWD / "emrg")
entry["last_active"] = datetime.now().isoformat()
atomic_write_yaml(entries, projects_file, prefix=".projects_")
logger.info(
"TaskScheduler: self-heal — repaired stale emrg entry "
"%r -> %s", existing, entry["path"],
)
break
except (yaml.YAMLError, OSError) as e:
logger.warning(
"TaskScheduler: projects.yml self-heal failed: %s", e
Expand Down
66 changes: 55 additions & 11 deletions tests/test_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -776,9 +776,12 @@ def test_ensure_self_evolution_task_preserves_existing_project_entry(tmp_path):
from emrg.server import scheduler as mod
from emrg.server.scheduler import TaskScheduler

# A real existing checkout dir (dev machine) — preserved, never repaired.
dev_path = tmp_path / "dev" / "emrg"
dev_path.mkdir(parents=True)
projects_yml = tmp_path / "projects.yml"
projects_yml.write_text(yaml.safe_dump([
{"name": "emrg", "path": "/dev/machine/custom/emrg",
{"name": "emrg", "path": str(dev_path),
"last_active": "2026-01-01T00:00:00"},
]))

Expand All @@ -794,7 +797,39 @@ def test_ensure_self_evolution_task_preserves_existing_project_entry(tmp_path):
data = yaml.safe_load(projects_yml.read_text(encoding="utf-8"))
assert len(data) == 1
assert data[0]["name"] == "emrg"
assert data[0]["path"] == "/dev/machine/custom/emrg" # untouched
assert data[0]["path"] == str(dev_path) # untouched


def test_ensure_self_evolution_task_repairs_stale_project_entry(tmp_path):
"""A dead emrg path (deleted pytest-temp dir) is repaired to the canonical
workspace (2026-08-12 incident: a test run leaked a pytest temp path into
the real ~/.emrg/projects.yml; the dir is gone after the suite, leaving a
dangling entry that list_projects/GUI pickers would show forever)."""
from emrg.server import scheduler as mod
from emrg.server.scheduler import EVOLUTION_CWD, TaskScheduler

stale = tmp_path / "gone" / "emrg" # never created → dead path
projects_yml = tmp_path / "projects.yml"
projects_yml.write_text(yaml.safe_dump([
{"name": "emrg", "path": str(stale),
"last_active": "2026-08-12T18:44:50"},
{"name": "other", "path": str(tmp_path / "other")},
]))

sched = TaskScheduler(InstanceIdentity())

orig_config = mod.config_dir
try:
mod.config_dir = lambda: tmp_path
sched._ensure_self_evolution_task()
finally:
mod.config_dir = orig_config

data = yaml.safe_load(projects_yml.read_text(encoding="utf-8"))
by_name = {e["name"]: e for e in data}
assert by_name["emrg"]["path"] == str(EVOLUTION_CWD / "emrg") # repaired
assert by_name["other"]["path"] == str(tmp_path / "other") # untouched
assert len(data) == 2


def test_ensure_self_evolution_task_other_entries_preserved(tmp_path):
Expand Down Expand Up @@ -844,25 +879,27 @@ def test_ensure_evolution_workspace_dev_repo_untouched(tmp_path):

orig_config = mod.config_dir
mod.config_dir = lambda: tmp_path
fake = FakeGitRun()
orig_run = mod.subprocess.run
mod.subprocess.run = fake
try:
handler = TaskHandler(
name="emrg-task",
config={"project": "emrg"},
interval=60,
identity=InstanceIdentity(),
)
finally:
mod.config_dir = orig_config
handler._source_dir = str(repo)
handler.project_path = str(repo)

fake = FakeGitRun()
orig_run = mod.subprocess.run
mod.subprocess.run = fake
try:
# config_dir must stay patched through _ensure_evolution_workspace():
# its clone branch calls _ensure_project_entry(), which writes
# config_dir()/projects.yml — an unpatched call would pollute the real
# ~/.emrg/projects.yml (2026-08-12 incident: pytest temp path leaked
# into real home).
handler._source_dir = str(repo)
handler.project_path = str(repo)
ok = handler._ensure_evolution_workspace()
finally:
mod.subprocess.run = orig_run
mod.config_dir = orig_config

assert ok is True
assert handler._source_dir == str(repo) # unchanged
Expand Down Expand Up @@ -933,12 +970,19 @@ def test_ensure_evolution_workspace_clone_failure_skips(tmp_path):
fake = FakeGitRun(git_repo=False, clone_fails=True)
orig_run = mod.subprocess.run
orig_evolve = mod.EVOLUTION_CWD
orig_config = mod.config_dir
mod.subprocess.run = fake
# config_dir patched through the call: the clone branch would call
# _ensure_project_entry() and write config_dir()/projects.yml — keep it
# hermetic so a future fake change can't pollute real ~/.emrg/projects.yml
# (2026-08-12 pytest-temp-path leak incident).
mod.config_dir = lambda: tmp_path
try:
ok = handler._ensure_evolution_workspace()
finally:
mod.subprocess.run = orig_run
mod.EVOLUTION_CWD = orig_evolve
mod.config_dir = orig_config

assert ok is False
assert handler._source_dir != str(mod.EVOLUTION_CWD / "emrg")
Expand Down
Loading