Skip to content
Closed
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` (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 路径不受影响)
Expand Down
22 changes: 21 additions & 1 deletion emrg/server/scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"])
Expand Down
25 changes: 25 additions & 0 deletions tests/test_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
Loading