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` (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 路径不受影响)
Expand Down
8 changes: 8 additions & 0 deletions emrg/connect.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}))
Expand Down
41 changes: 41 additions & 0 deletions tests/test_connect.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Loading