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 numberDiff line numberDiff 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.token; python -m emrg
```

Python: `uv run pytest tests/ -v` (1054) — import check: `uv run python -c "from emrg.client.app import run_client"`
Python: `uv run pytest tests/ -v` (1058) — import check: `uv run python -c "from emrg.client.app import run_client"`
GUI: `cd emrg/gui && npm test` (259: 45 daemon_client + 20 conn-manager + 22 app-commands + 129 renderer smoke + 15 i18n + 8 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 路径不受影响)
Expand Down
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,3 +31,8 @@ build-backend = "hatchling.build"
dev = [
"pytest>=9.1.1",
]

[tool.pytest.ini_options]
markers = [
"allow_real_stop: opt out of the conftest stop_all hermeticity guard for the listed stop function(s) — use only when the test has already isolated the function's internal kill machinery",
]
57 changes: 41 additions & 16 deletions tests/conftest.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -104,26 +104,51 @@ def _ensure_git_on_path(monkeypatch):


@pytest.fixture(autouse=True)
def _guard_stop_daemon_hermeticity(monkeypatch):
"""⛔ Red line (host 2026-08-18T22:58): tests must NEVER trigger a real
stop_daemon() — it sends a shutdown over the websocket and kills the live
emrgd (2026-08-20 16:18/16:25 real incidents; the daemon is EMRG's life
core; rant 2026-08-20T16:32:30). Any test that calls stop_all()/
stop_daemon() without isolating stop_daemon now fails loudly with
AssertionError instead of killing the daemon. Tests that DO isolate it
(monkeypatch.setattr(_stop_all, "stop_daemon", lambda: None)) patch after
this fixture and override it as usual.
def _guard_stop_all_hermeticity(monkeypatch, request):
"""⛔ Red line (host 2026-08-18T22:58, extended by rant 2026-08-25T10:42:47):
tests must NEVER trigger a real stop step. stop_daemon() sends a shutdown
over the websocket and kills the live emrgd (2026-08-20 16:18/16:25 real
incidents; the daemon is EMRG's life core; rant 2026-08-20T16:32:30), and
stop_gui()/stop_tui()/stop_bundled_git()/stop_lock_owners() kill the live
GUI/TUI/processes on a Windows run (rant 2026-08-25T10:42:47:
test_stop_all.py:770 only isolated stop_daemon, so the other four ran for
real under the mocked Windows branch). Any test that calls stop_all()/
stop_*() without isolating the killer now fails loudly with AssertionError
instead of killing processes. Tests that DO isolate (monkeypatch.setattr(
_stop_all, "stop_x", lambda: None)) patch after this fixture and override
it as usual.

Escape hatch: a test that calls a stop function directly while having
already isolated its internals (e.g. stop_lock_owners with
find_install_module_holders + _lock_owner_ps mocked, or a POSIX noop)
opts out per function with @pytest.mark.allow_real_stop("stop_lock_owners").
"""
import emrg._stop_all as stop_mod

def _no_real_stop_daemon(*args, **kwargs):
raise AssertionError(
"test triggered a REAL stop_daemon() — ⛔ red-line violation "
"(host 2026-08-18T22:58); tests must isolate it via "
"monkeypatch.setattr(emrg._stop_all, 'stop_daemon', lambda: None)"
)
_STOP_FUNCS = (
"stop_daemon", "stop_gui", "stop_tui", "stop_bundled_git",
"stop_lock_owners",
)

allowed = set()
marker = request.node.get_closest_marker("allow_real_stop")
if marker is not None:
allowed = set(marker.args)

def _no_real_stop(name):
def _raises(*args, **kwargs):
raise AssertionError(
f"test triggered a REAL {name}() — ⛔ red-line violation "
f"(host 2026-08-18T22:58 / rant 2026-08-25T10:42:47); tests must "
f"isolate it via monkeypatch.setattr(emrg._stop_all, {name!r}, "
f"lambda: None)"
)
_raises.__name__ = f"_no_real_{name}"
return _raises

monkeypatch.setattr(stop_mod, "stop_daemon", _no_real_stop_daemon)
for name in _STOP_FUNCS:
if name not in allowed:
monkeypatch.setattr(stop_mod, name, _no_real_stop(name))


@pytest.fixture(autouse=True)
Expand Down
40 changes: 29 additions & 11 deletions tests/test_stop_all.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -407,7 +407,10 @@ class TestLockOwners:
EXTERNAL browser-harness daemon locking install\\ files; emrgd.pid/port
empty and no -m emrg process, so the cmdline scan could never see it)."""

@pytest.mark.allow_real_stop("stop_lock_owners")
def test_posix_noop(self, monkeypatch):
# Direct call is hermetic: is_win=False → early noop, and the only
# internal killer _lock_owner_ps is mocked (guard escape hatch).
monkeypatch.setattr(_stop_all, "is_win", lambda: False)
called: list = []
monkeypatch.setattr(
Expand DownExpand Up@@ -575,7 +578,11 @@ def test_lock_owner_diag_parsed(self):
assert _stop_all._lock_owner_diag("rm-diag\tabc\t2\t3\t0\n") is None
assert _stop_all._lock_owner_diag("") is None

@pytest.mark.allow_real_stop("stop_lock_owners")
def test_stop_lock_owners_logs_diag(self, monkeypatch, capsys):
# Direct call is hermetic: both internal killers are mocked
# (find_install_module_holders → [] and _lock_owner_ps) — no real
# taskkill can fire (guard escape hatch).
monkeypatch.setattr(_stop_all, "is_win", lambda: True)
monkeypatch.setattr(_stop_all, "find_install_module_holders", lambda: [])
monkeypatch.setattr(
Expand DownExpand Up@@ -772,10 +779,16 @@ def test_stop_all_process_residual_still_aborts(self, monkeypatch, capsys):
residuals do not (rant 2026-08-18T21:24:48 #2c/#5)."""
monkeypatch.setattr(_stop_all, "is_win", lambda: True)
monkeypatch.setattr(_stop_all.time, "sleep", lambda *a, **k: None)
# ⛔ 最高原则(宿主 2026-08-18T22:58):必须隔离 stop_daemon——本机运行
# pytest 时真实 stop_all() 会连 ws 发 shutdown 杀死正在运行的 emrgd
# (2026-08-20 16:18/16:25 两次实证)。同文件其它 stop_all() 测试均有此隔离。
# ⛔ 最高原则(宿主 2026-08-18T22:58)+ rant 2026-08-25T10:42:47:必须隔离
# 全部 5 个 stop 函数——本测试跑 Windows 分支,真实 stop_all() 会杀 GUI/
# TUI/daemon/bundled git/lock owners。此前只隔离了 stop_daemon(2026-08-20
# 16:18/16:25 实证),Windows 上跑 pytest 会真实杀掉正在运行的 GUI/TUI。
# 隔离模式与 test_stop_all_retries_lock_kill 保持一致。
monkeypatch.setattr(_stop_all, "stop_gui", lambda: None)
monkeypatch.setattr(_stop_all, "stop_tui", lambda: None)
monkeypatch.setattr(_stop_all, "stop_daemon", lambda: None)
monkeypatch.setattr(_stop_all, "stop_bundled_git", lambda: None)
monkeypatch.setattr(_stop_all, "stop_lock_owners", lambda: None)
monkeypatch.setattr(_stop_all, "check_install_writable", lambda: [])
monkeypatch.setattr(
_stop_all, "verify",
Expand DownExpand Up@@ -1075,14 +1088,19 @@ def test_stop_all_prints_tee_path(self, monkeypatch, tmp_path, capsys):
assert "exit code 0 (clean)" in out


class TestStopDaemonHermeticityGuard:
"""⛔ Red line (host 2026-08-18T22:58): conftest's autouse guard must keep
stop_daemon patched so a forgetful test can never kill the live daemon
again (rant 2026-08-20T16:32:30). Canary checks function identity only —
it never calls any stop path."""
class TestStopAllHermeticityGuard:
"""⛔ Red line (host 2026-08-18T22:58, extended by rant 2026-08-25T10:42:47):
conftest's autouse guard must keep ALL FIVE stop functions patched so a
forgetful test can never kill the live daemon/GUI/TUI again (rants
2026-08-20T16:32:30 / 2026-08-25T10:42:47). Canary checks function identity
only — it never calls any stop path."""

def test_conftest_guard_stop_daemon_is_installed(self):
@pytest.mark.parametrize(
"name",
["stop_daemon", "stop_gui", "stop_tui", "stop_bundled_git", "stop_lock_owners"],
)
def test_conftest_guard_installed(self, name):
import emrg._stop_all as stop_mod
assert stop_mod.stop_daemon.__name__ == "_no_real_stop_daemon", (
"conftest stop_daemon guard missing — real stop_daemon reachable from tests"
assert getattr(stop_mod, name).__name__ == f"_no_real_{name}", (
f"conftest {name} guard missing — real {name} reachable from tests"
)
Loading