From aab0026a2fae010d3d4f49fa1a1e5912007b7fd6 Mon Sep 17 00:00:00 2001 From: Steven Welch Date: Sat, 5 Sep 2026 09:43:52 -0600 Subject: [PATCH 1/2] fix(sms-bridge): target the v1 /session route prefix PR #40 kept the documented v1 body and strict direct {info,parts} parsing but left the requests on the legacy V2 /api surface. Upstream v1.18.23 (ef2880f379129aa048be9e9353e30aa168d42c17) mounts the session HttpApi at root "/session": create = POST /session (empty body, direct Session.Info) and blocking prompt = POST /session/:sessionID/message (direct streamed SessionV1.WithParts). /api/session* is the distinct legacy V2 API (data-wrapped prompt admission, GET message listing, no /wait), so the strict direct-shape parser failed the live approved-source test with opencode-response-invalid. Switch only the route prefix in opencode-sms-bridge/server.py: POST /session with the documented empty body and POST /session/{id}/message with message-level agent + text parts. Response parsing, request bodies, and the bounded static error-code taxonomy are unchanged. Tests pin the exact /session prefix, methods, and bodies at the opener level and reject every /api route, /prompt, /wait, and prompt_async. --- opencode-sms-bridge/server.py | 4 ++-- opencode-sms-bridge/test_server.py | 26 ++++++++++++++------------ 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/opencode-sms-bridge/server.py b/opencode-sms-bridge/server.py index 6136299..69a1930 100644 --- a/opencode-sms-bridge/server.py +++ b/opencode-sms-bridge/server.py @@ -576,7 +576,7 @@ def _request( raise BridgeError("OpenCode response was invalid", ERROR_OPENCODE_RESPONSE_INVALID) from error def create_session(self) -> str: - response = self._request("/api/session", {}, operation=OPENCODE_OPERATION_SESSION_CREATE) + response = self._request("/session", {}, operation=OPENCODE_OPERATION_SESSION_CREATE) session_id = response.get("id") if isinstance(response, dict) else None if not isinstance(session_id, str) or not session_id: raise BridgeError("OpenCode session response was invalid", ERROR_OPENCODE_RESPONSE_INVALID) @@ -589,7 +589,7 @@ def prompt(self, session_id: str, agent: str, parts: list[dict[str, str]]) -> st if not text: raise BridgeError("OpenCode prompt has no text", ERROR_OPENCODE_INPUT_INVALID) response = self._request( - f"/api/session/{session_id}/message", + f"/session/{session_id}/message", {"agent": agent, "parts": [{"type": "text", "text": text}]}, operation=OPENCODE_OPERATION_PROMPT, ) diff --git a/opencode-sms-bridge/test_server.py b/opencode-sms-bridge/test_server.py index d315045..9fd593b 100644 --- a/opencode-sms-bridge/test_server.py +++ b/opencode-sms-bridge/test_server.py @@ -162,8 +162,8 @@ def test_documented_flow_uses_exact_routes_methods_and_bodies(self): self.assertEqual( [request.full_url for request in requests], [ - "https://opencode.example.invalid/api/session", - "https://opencode.example.invalid/api/session/ses_new/message", + "https://opencode.example.invalid/session", + "https://opencode.example.invalid/session/ses_new/message", ], ) self.assertEqual(json.loads(requests[0].data.decode()), {}) @@ -172,14 +172,14 @@ def test_documented_flow_uses_exact_routes_methods_and_bodies(self): {"agent": "homesteader", "parts": [{"type": "text", "text": "hi"}]}, ) for request in requests: - for unsupported in ("/prompt", "prompt_async", "/wait"): + for unsupported in ("/api", "/prompt", "prompt_async", "/wait"): self.assertNotIn(unsupported, request.full_url) def test_session_create_sends_only_documented_fields(self): client = OpenCodeClient(self.settings) with patch.object(client, "_request", return_value={"id": "ses_123"}) as request: self.assertEqual(client.create_session(), "ses_123") - request.assert_called_once_with("/api/session", {}, operation=OPENCODE_OPERATION_SESSION_CREATE) + request.assert_called_once_with("/session", {}, operation=OPENCODE_OPERATION_SESSION_CREATE) def test_session_create_rejects_invalid_response(self): client = OpenCodeClient(self.settings) @@ -201,13 +201,15 @@ def test_prompt_makes_single_blocking_message_request(self): ) self.assertEqual(request.call_count, 1) request.assert_called_once_with( - "/api/session/ses_123/message", + "/session/ses_123/message", {"agent": "lawnmowerman", "parts": [{"type": "text", "text": "hello"}]}, operation=OPENCODE_OPERATION_PROMPT, ) for invoked in request.call_args_list: path = invoked.args[0] + self.assertNotIn("/api", path) self.assertNotIn("/prompt", path) + self.assertNotIn("prompt_async", path) self.assertNotIn("/wait", path) def test_prompt_joins_text_parts_into_one_documented_text_part(self): @@ -220,7 +222,7 @@ def test_prompt_joins_text_parts_into_one_documented_text_part(self): [{"type": "text", "text": "line one"}, {"type": "text", "text": "line two"}], ) request.assert_called_once_with( - "/api/session/ses_123/message", + "/session/ses_123/message", {"agent": "grillmaster", "parts": [{"type": "text", "text": "line one\nline two"}]}, operation=OPENCODE_OPERATION_PROMPT, ) @@ -283,12 +285,12 @@ def test_opencode_request_failures_map_to_static_operation_and_category(self): (URLError(TimeoutError()), "transport"), (URLError("unknown url type"), "url-configuration"), (ValueError("malformed url detail"), "url-configuration"), - (OSError("socket detail"), "os"), - (HTTPError("https://opencode.example.invalid/api/session", 404, "client detail", None, None), "http-4xx"), - (HTTPError("https://opencode.example.invalid/api/session", 429, "rate detail", None, None), "http-4xx"), - (HTTPError("https://opencode.example.invalid/api/session", 500, "server detail", None, None), "http-5xx"), - (HTTPError("https://opencode.example.invalid/api/session", 503, "unavailable detail", None, None), "http-5xx"), - (HTTPError("https://opencode.example.invalid/api/session", 302, "redirect detail", None, None), "unknown"), + (OError := OSError("socket detail"), "os"), + (HTTPError("https://opencode.example.invalid/session", 404, "client detail", None, None), "http-4xx"), + (HTTPError("https://opencode.example.invalid/session", 429, "rate detail", None, None), "http-4xx"), + (HTTPError("https://opencode.example.invalid/session", 500, "server detail", None, None), "http-5xx"), + (HTTPError("https://opencode.example.invalid/session", 503, "unavailable detail", None, None), "http-5xx"), + (HTTPError("https://opencode.example.invalid/session", 302, "redirect detail", None, None), "unknown"), ) for failure, category in failures: with self.subTest(failure=type(failure).__name__): From bcfb10d18f15a6c394abd34b53b5f490ad751275 Mon Sep 17 00:00:00 2001 From: Steven Welch Date: Sat, 5 Sep 2026 09:46:01 -0600 Subject: [PATCH 2/2] fix(sms-bridge): drop stray walrus assignment in test fixture Restore the OSError failure fixture to a plain tuple element; the route-prefix change needs no other deviation from main's test file. --- opencode-sms-bridge/test_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opencode-sms-bridge/test_server.py b/opencode-sms-bridge/test_server.py index 9fd593b..bce0b51 100644 --- a/opencode-sms-bridge/test_server.py +++ b/opencode-sms-bridge/test_server.py @@ -285,7 +285,7 @@ def test_opencode_request_failures_map_to_static_operation_and_category(self): (URLError(TimeoutError()), "transport"), (URLError("unknown url type"), "url-configuration"), (ValueError("malformed url detail"), "url-configuration"), - (OError := OSError("socket detail"), "os"), + (OSError("socket detail"), "os"), (HTTPError("https://opencode.example.invalid/session", 404, "client detail", None, None), "http-4xx"), (HTTPError("https://opencode.example.invalid/session", 429, "rate detail", None, None), "http-4xx"), (HTTPError("https://opencode.example.invalid/session", 500, "server detail", None, None), "http-5xx"),