From dd4c819df8a7a8829a3d9436e20fd06cc776eb80 Mon Sep 17 00:00:00 2001 From: Ace Data Cloud Dev Date: Mon, 13 Jul 2026 09:26:49 +0800 Subject: [PATCH] fix(channels): verify real WeChat task endpoint --- README.md | 26 +++++++++++++++++++++++ coding_bridge/__init__.py | 7 +++++- coding_bridge/channels/wechat/__init__.py | 5 ++--- coding_bridge/channels/wechat/client.py | 2 +- coding_bridge/channels_cli.py | 4 ++-- tests/test_channels_cli.py | 23 ++++++-------------- tests/test_version.py | 7 ++++++ tests/test_wechat_task_status.py | 12 ++++------- 8 files changed, 55 insertions(+), 31 deletions(-) create mode 100644 tests/test_version.py diff --git a/README.md b/README.md index de49e59..2deb2c8 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,20 @@ pipx install coding-bridge # recommended pip install coding-bridge ``` +On Windows, if `pipx` is not installed, use the Python launcher directly: + +```powershell +py -m pip install --user --upgrade coding-bridge +``` + +If `pip` reports an old `0.1.0` editable install or `coding_bridge` cannot be +imported, remove the stale install before upgrading: + +```powershell +py -m pip uninstall -y coding-bridge +py -m pip install --user --no-cache-dir "coding-bridge[wechat]" +``` + For the ASCII-QR pairing helper, install the optional extra: ```bash @@ -161,6 +175,18 @@ or a secrets-file path (`token_file`). Export it before starting: export WECHAT_TOKEN_MY_WECHAT="…" ``` +Windows PowerShell: + +```powershell +$env:WECHAT_TOKEN_MY_WECHAT = "…" +``` + +Windows Command Prompt: + +```bat +set "WECHAT_TOKEN_MY_WECHAT=…" +``` + #### Provider sign-in Each message runs a real **Claude Code** (or Codex) turn **on your machine**, so diff --git a/coding_bridge/__init__.py b/coding_bridge/__init__.py index a746dff..f32aef0 100644 --- a/coding_bridge/__init__.py +++ b/coding_bridge/__init__.py @@ -5,4 +5,9 @@ code execution stays local; the bridge only relays messages. """ -__version__ = "0.1.0" +from importlib.metadata import PackageNotFoundError, version + +try: + __version__ = version("coding-bridge") +except PackageNotFoundError: + __version__ = "0+unknown" diff --git a/coding_bridge/channels/wechat/__init__.py b/coding_bridge/channels/wechat/__init__.py index 606c2c4..47ee421 100644 --- a/coding_bridge/channels/wechat/__init__.py +++ b/coding_bridge/channels/wechat/__init__.py @@ -10,9 +10,8 @@ and hands anything left to the dispatcher — policy (trigger prefix, allowlist, rate limit) lives one layer up in P7. * ``POST /api/messages/send`` with ``Authorization: Bearer `` — returns - ``202 Accepted`` because the gateway queues a background UI task. P2 fires and - forgets; P7 will poll ``/api/messages/tasks/{id}`` for delivery - confirmation. + ``202 Accepted`` because the gateway queues a background UI task. Delivery + diagnostics use ``GET /api/tasks/{id}``. The adapter is import-safe without extras — it delegates to :mod:`websockets` and :mod:`httpx`, both already required by CodingBridge core. The diff --git a/coding_bridge/channels/wechat/client.py b/coding_bridge/channels/wechat/client.py index 12e0a73..aa1fc2f 100644 --- a/coding_bridge/channels/wechat/client.py +++ b/coding_bridge/channels/wechat/client.py @@ -133,7 +133,7 @@ async def get_task_status(self, task_id: str) -> dict[str, Any]: raise ValueError("task_id contains invalid characters") # Belt-and-suspenders: `quote(safe="")` still produces a plain # segment since the regex already rejects unsafe input. - path = f"/api/messages/tasks/{quote(task_id, safe='')}" + path = f"/api/tasks/{quote(task_id, safe='')}" resp = await self._client.get(path) resp.raise_for_status() try: diff --git a/coding_bridge/channels_cli.py b/coding_bridge/channels_cli.py index 656f561..16b6124 100644 --- a/coding_bridge/channels_cli.py +++ b/coding_bridge/channels_cli.py @@ -147,7 +147,7 @@ async def _doctor_one(inst: WeChatInstanceConfig) -> tuple[bool, str]: """Return (ok, message) for one instance. Never raises; never logs secrets. Actually tests that the token is accepted by hitting an authenticated - endpoint (``GET /api/messages/tasks/``). The gateway's response: + endpoint (``GET /api/tasks/``). The gateway's response: * 401 → token rejected → FAIL (loudly, since this is the whole point of doctor) * 404 → token accepted but probe id unknown → PASS (this is expected) @@ -193,7 +193,7 @@ async def _doctor_one(inst: WeChatInstanceConfig) -> tuple[bool, str]: resp = await client._client.get("/health") # noqa: SLF001 if 200 <= resp.status_code < 300: return False, ( - f"reachable at /health but /api/messages/tasks " + f"reachable at /health but /api/tasks " f"returned {code} (gateway may be misconfigured)" ) except Exception: # noqa: BLE001 diff --git a/tests/test_channels_cli.py b/tests/test_channels_cli.py index e628122..50179c2 100644 --- a/tests/test_channels_cli.py +++ b/tests/test_channels_cli.py @@ -59,9 +59,7 @@ def test_refuses_to_overwrite(self, tmp_path: Path) -> None: # Original file untouched assert s.channels_config_path.read_text(encoding="utf-8") == "# already here" - @pytest.mark.skipif( - os.name == "nt", reason="POSIX permission bits don't apply on Windows" - ) + @pytest.mark.skipif(os.name == "nt", reason="POSIX permission bits don't apply on Windows") def test_posix_permissions_are_0600(self, tmp_path: Path) -> None: import stat as _stat @@ -134,7 +132,7 @@ def test_enabled_reachable_endpoint_marked_ok( def patched_init(self, base_url, token, *, timeout=10.0, transport=None): def handler(request: httpx.Request) -> httpx.Response: - assert request.url.path.startswith("/api/messages/tasks/") + assert request.url.path.startswith("/api/tasks/") # Token was accepted; the probe task just doesn't exist. return httpx.Response(404, json={"error": "task not found"}) @@ -171,6 +169,7 @@ def test_enabled_401_marked_failure( def patched_init(self, base_url, token, *, timeout=10.0, transport=None): def handler(request: httpx.Request) -> httpx.Response: + assert request.url.path.startswith("/api/tasks/") return httpx.Response(401, json={"error": "unauthorized"}) original_init( @@ -352,9 +351,7 @@ def script(sid): _install_stub_factory(monkeypatch, script) s = _settings(tmp_path) rc, out, err = _capture( - lambda: channels_cli.cmd_channels_smoke( - s, provider="claude", prompt="x", timeout=5.0 - ) + lambda: channels_cli.cmd_channels_smoke(s, provider="claude", prompt="x", timeout=5.0) ) assert rc == 1 assert "kaboom" in out @@ -369,9 +366,7 @@ def script(sid): _install_stub_factory(monkeypatch, script) s = _settings(tmp_path) rc, out, err = _capture( - lambda: channels_cli.cmd_channels_smoke( - s, provider="claude", prompt="x", timeout=5.0 - ) + lambda: channels_cli.cmd_channels_smoke(s, provider="claude", prompt="x", timeout=5.0) ) assert rc == 1 @@ -387,9 +382,7 @@ def script(_sid): _install_stub_factory(monkeypatch, script) s = _settings(tmp_path) rc, out, err = _capture( - lambda: channels_cli.cmd_channels_smoke( - s, provider="claude", prompt="x", timeout=0.2 - ) + lambda: channels_cli.cmd_channels_smoke(s, provider="claude", prompt="x", timeout=0.2) ) assert rc == 1 assert "timed out" in out @@ -399,9 +392,7 @@ def test_smoke_unknown_provider_exits_2(self, tmp_path: Path) -> None: # --provider must fail loudly (rc=2), not silently fall back to Claude. s = _settings(tmp_path) rc, out, err = _capture( - lambda: channels_cli.cmd_channels_smoke( - s, provider="gpt4", prompt="x", timeout=5.0 - ) + lambda: channels_cli.cmd_channels_smoke(s, provider="gpt4", prompt="x", timeout=5.0) ) assert rc == 2 assert "unknown provider" in err diff --git a/tests/test_version.py b/tests/test_version.py new file mode 100644 index 0000000..e6f765d --- /dev/null +++ b/tests/test_version.py @@ -0,0 +1,7 @@ +from importlib.metadata import version + +import coding_bridge + + +def test_runtime_version_matches_distribution_metadata(): + assert coding_bridge.__version__ == version("coding-bridge") diff --git a/tests/test_wechat_task_status.py b/tests/test_wechat_task_status.py index 440e879..68e8537 100644 --- a/tests/test_wechat_task_status.py +++ b/tests/test_wechat_task_status.py @@ -11,7 +11,7 @@ @pytest.mark.asyncio async def test_get_task_status_returns_json_dict() -> None: def handler(request: httpx.Request) -> httpx.Response: - assert request.url.path == "/api/messages/tasks/task-42" + assert request.url.path == "/api/tasks/task-42" assert request.headers.get("authorization") == "Bearer tok" return httpx.Response(200, json={"status": "delivered", "task_id": "task-42"}) @@ -46,9 +46,7 @@ def handler(request: httpx.Request) -> httpx.Response: seen.append(str(request.url)) return httpx.Response(200, json={}) - client = WeChatClient( - "http://wechat.local", "tok", transport=httpx.MockTransport(handler) - ) + client = WeChatClient("http://wechat.local", "tok", transport=httpx.MockTransport(handler)) try: for bad in [ "42?admin=1", @@ -71,13 +69,11 @@ def handler(request: httpx.Request) -> httpx.Response: async def test_get_task_status_accepts_safe_uuid_shaped_id() -> None: def handler(request: httpx.Request) -> httpx.Response: # Ensure the path was NOT tampered with - assert request.url.path == "/api/messages/tasks/abc-DEF_123" + assert request.url.path == "/api/tasks/abc-DEF_123" assert request.url.query == b"" return httpx.Response(200, json={"status": "ok"}) - client = WeChatClient( - "http://wechat.local", "tok", transport=httpx.MockTransport(handler) - ) + client = WeChatClient("http://wechat.local", "tok", transport=httpx.MockTransport(handler)) try: assert await client.get_task_status("abc-DEF_123") == {"status": "ok"} finally: