diff --git a/Agent.md b/Agent.md index 63fbdb0..b07a279 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` (773) — import check: `uv run python -c "from emrg.client.app import run_client"` +Python: `uv run pytest tests/ -v` (774) — import check: `uv run python -c "from emrg.client.app import run_client"` GUI: `cd emrg/gui && npm test` (232: 44 daemon_client + 19 conn-manager + 22 app-commands + 110 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/server/scheduler.py b/emrg/server/scheduler.py index 1b09061..cd95d06 100644 --- a/emrg/server/scheduler.py +++ b/emrg/server/scheduler.py @@ -1105,12 +1105,32 @@ class TaskScheduler: def __init__(self, identity: InstanceIdentity) -> None: self.identity = identity - self._tasks_file = config_dir() / "tasks.yml" + self._tasks_file_override: Path | None = None self._handlers: list[TaskHandler] = [] self._coros: list[asyncio.Task] = [] # cfg (from tasks.yml) used to start each handler — for hot-reload diffing. self._handler_cfgs: dict[str, dict] = {} + @property + def _tasks_file(self) -> Path: + """Resolve tasks.yml at call time, never capture it at construction. + + Previously the path was computed in __init__ (scheduler.py:1108). PR + #738's hermeticity guard exposed the consequence: tests that construct + a TaskScheduler BEFORE patching config_dir caused part 2 of + _ensure_self_evolution_task (and any _load_tasks/_save_tasks) to read + and write the REAL ~/.emrg/tasks.yml. Lazy resolution makes the + capture impossible — the current config_dir always wins. An explicit + assignment (setter) still overrides for tests that pin a custom file. + """ + if self._tasks_file_override is not None: + return self._tasks_file_override + return config_dir() / "tasks.yml" + + @_tasks_file.setter + def _tasks_file(self, value: Path) -> None: + self._tasks_file_override = value + def _start_handler_for(self, cfg: dict) -> TaskHandler: """Create + start a handler for a task cfg; returns the handler.""" template_path = _resolve_task_template(cfg["type"]) diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py index 41659d7..fa4ac28 100644 --- a/tests/test_scheduler.py +++ b/tests/test_scheduler.py @@ -67,6 +67,31 @@ def test_resolve_project_path_invalid_yaml(tmp_path): # ── TaskScheduler._save_tasks ───────────────────────────────────── +def test_tasks_file_resolves_config_dir_at_call_time(tmp_path): + """#738 root cause: _tasks_file must not capture config_dir at __init__. + + A TaskScheduler constructed BEFORE config_dir is patched must still read + and write tasks.yml from the CURRENT config_dir (tmp), never the real + ~/.emrg/tasks.yml. The path assertion comes first, so the old captured + behavior fails red before any write reaches the real file. + """ + from emrg.server import scheduler as mod + from emrg.server.scheduler import TaskScheduler + + sched = TaskScheduler(InstanceIdentity()) # constructed pre-patch (the bug) + orig_config = mod.config_dir + try: + mod.config_dir = lambda: tmp_path + # Property must resolve to tmp even though __init__ ran pre-patch. + assert str(sched._tasks_file) == str(tmp_path / "tasks.yml") + # Reads and writes land in tmp, not the real ~/.emrg/tasks.yml. + assert sched._load_tasks() == [] + sched._save_tasks([{"name": "x"}]) + assert (tmp_path / "tasks.yml").exists() + finally: + mod.config_dir = orig_config + + def test_save_tasks_atomic_write(tmp_path): """_save_tasks writes YAML atomically via tempfile + rename."""