diff --git a/Agent.md b/Agent.md index 9643c40..afe1a0c 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` (806) — import check: `uv run python -c "from emrg.client.app import run_client"` +Python: `uv run pytest tests/ -v` (807) — import check: `uv run python -c "from emrg.client.app import run_client"` GUI: `cd emrg/gui && npm test` (246: 44 daemon_client + 19 conn-manager + 22 app-commands + 121 renderer smoke + 16 i18n + 7 integration + 3 commands + 5 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 路径不受影响) diff --git a/emrg/connect.py b/emrg/connect.py index 2ff7d64..2c14b7b 100644 --- a/emrg/connect.py +++ b/emrg/connect.py @@ -60,8 +60,16 @@ async def connect_to_server(): """ port_path = Path(get_server_path()) port, token = port_path.read_text(encoding="utf-8").split() + # proxy=None: loopback connections must never go through a system proxy. + # websockets 17 defaults proxy=True and reads the OS proxy settings — when a + # Windows system proxy is enabled (e.g. 10.10.0.28:6501 for HN/Reddit access), + # the ws://127.0.0.1 handshake is sent to the proxy → InvalidMessage → all + # Python clients (TUI `emrg`, scheduler internal connections) cannot reach the + # local daemon, while the Node.js GUI is unaffected (2026-08-14 incident; root + # cause of continuous emrg-task/emrg-promote-task crashes since 2026-08-13). ws = await connect( f"ws://127.0.0.1:{port}", + proxy=None, max_size=16 * 1024 * 1024, ) await ws.send(json.dumps({"type": "auth", "token": token})) diff --git a/tests/test_connect.py b/tests/test_connect.py index d3fcd2f..c3bce61 100644 --- a/tests/test_connect.py +++ b/tests/test_connect.py @@ -1,5 +1,7 @@ """Tests for connect module — WebSocket IPC connection layer (Phase 1).""" +import asyncio +import json from pathlib import Path from emrg.connect import CONNECT_ID, AuthError, cleanup_server, get_server_path, is_server_running_sync @@ -77,3 +79,42 @@ def test_false_when_connection_refused(self, monkeypatch, tmp_path): (tmp_path / f"{CONNECT_ID}.port").write_text("1\nno-token", encoding="utf-8") assert is_server_running_sync(timeout=0.1) is False + + +class TestConnectToServer: + def test_connect_uses_proxy_none(self, monkeypatch, tmp_path): + """Loopback WS must never route through a system proxy. + + websockets 17 defaults proxy=True and reads the OS proxy settings; when a + Windows system proxy is configured, the ws://127.0.0.1 handshake is sent + to the proxy → InvalidMessage → Python clients cannot reach the local + daemon (2026-08-14 incident). proxy=None pins direct loopback. + """ + from emrg import connect as connect_mod + + captured = {} + + class FakeWS: + async def send(self, data): + self.sent = data + + async def recv(self): + return json.dumps({"type": "auth_ok"}) + + async def close(self): + pass + + async def fake_connect(uri, **kwargs): + captured["uri"] = uri + captured["kwargs"] = kwargs + return FakeWS() + + monkeypatch.setattr(connect_mod, "config_dir", lambda: tmp_path) + monkeypatch.setattr(connect_mod, "connect", fake_connect) + (tmp_path / f"{CONNECT_ID}.port").write_text("49152\ntoken", encoding="utf-8") + + asyncio.run(connect_mod.connect_to_server()) + + assert captured["uri"] == "ws://127.0.0.1:49152" + assert captured["kwargs"]["proxy"] is None + assert captured["kwargs"]["max_size"] == 16 * 1024 * 1024