From b46413c7541bf4154339245bf0973d826f0cb591 Mon Sep 17 00:00:00 2001 From: Will Washburn Date: Fri, 20 Feb 2026 09:31:03 -0500 Subject: [PATCH] Update Python SDK WS endpoint and tests Switch Python SDK websocket URL from /v1/stream to /v1/ws and update the ws test to assert the new endpoint. Remove the stale root-level test-prod-ws.mjs smoke script (coverage duplicated by CI and used the legacy /v1/stream route). Add trajectory metadata files and update .trajectories index lastUpdated and entries. --- .../completed/2026-02/traj_nck4ip3pfbfz.json | 53 ++++++ .../completed/2026-02/traj_nck4ip3pfbfz.md | 31 ++++ .../completed/2026-02/traj_vwrhp5hvaml2.json | 53 ++++++ .../completed/2026-02/traj_vwrhp5hvaml2.md | 31 ++++ .trajectories/index.json | 16 +- packages/python-sdk/src/relay_sdk/ws.py | 2 +- packages/python-sdk/tests/test_ws.py | 1 + test-prod-ws.mjs | 167 ------------------ 8 files changed, 185 insertions(+), 169 deletions(-) create mode 100644 .trajectories/completed/2026-02/traj_nck4ip3pfbfz.json create mode 100644 .trajectories/completed/2026-02/traj_nck4ip3pfbfz.md create mode 100644 .trajectories/completed/2026-02/traj_vwrhp5hvaml2.json create mode 100644 .trajectories/completed/2026-02/traj_vwrhp5hvaml2.md delete mode 100644 test-prod-ws.mjs diff --git a/.trajectories/completed/2026-02/traj_nck4ip3pfbfz.json b/.trajectories/completed/2026-02/traj_nck4ip3pfbfz.json new file mode 100644 index 00000000..5c816c8c --- /dev/null +++ b/.trajectories/completed/2026-02/traj_nck4ip3pfbfz.json @@ -0,0 +1,53 @@ +{ + "id": "traj_nck4ip3pfbfz", + "version": 1, + "task": { + "title": "Update Python SDK WebSocket endpoint to /v1/ws" + }, + "status": "completed", + "startedAt": "2026-02-20T14:14:09.112Z", + "agents": [ + { + "name": "default", + "role": "lead", + "joinedAt": "2026-02-20T14:14:18.841Z" + } + ], + "chapters": [ + { + "id": "chap_cfpp72xul54u", + "title": "Work", + "agentName": "default", + "startedAt": "2026-02-20T14:14:18.841Z", + "events": [ + { + "ts": 1771596858842, + "type": "decision", + "content": "Switch Python SDK websocket endpoint to /v1/ws: Switch Python SDK websocket endpoint to /v1/ws", + "raw": { + "question": "Switch Python SDK websocket endpoint to /v1/ws", + "chosen": "Switch Python SDK websocket endpoint to /v1/ws", + "alternatives": [], + "reasoning": "Server route is /v1/ws and /v1/stream returns 404 Route not found" + }, + "significance": "high" + } + ], + "endedAt": "2026-02-20T14:15:19.338Z" + } + ], + "commits": [], + "filesChanged": [], + "projectId": "/Users/will/Projects/relaycast", + "tags": [], + "_trace": { + "startRef": "e08d637cf6e6311c9a2c30e5c88c07ad4622903a", + "endRef": "e08d637cf6e6311c9a2c30e5c88c07ad4622903a" + }, + "completedAt": "2026-02-20T14:15:19.338Z", + "retrospective": { + "summary": "Updated Python SDK websocket endpoint to /v1/ws and added regression assertion in ws tests", + "approach": "Standard approach", + "confidence": 0.95 + } +} \ No newline at end of file diff --git a/.trajectories/completed/2026-02/traj_nck4ip3pfbfz.md b/.trajectories/completed/2026-02/traj_nck4ip3pfbfz.md new file mode 100644 index 00000000..962a0c80 --- /dev/null +++ b/.trajectories/completed/2026-02/traj_nck4ip3pfbfz.md @@ -0,0 +1,31 @@ +# Trajectory: Update Python SDK WebSocket endpoint to /v1/ws + +> **Status:** ✅ Completed +> **Confidence:** 95% +> **Started:** February 20, 2026 at 09:14 AM +> **Completed:** February 20, 2026 at 09:15 AM + +--- + +## Summary + +Updated Python SDK websocket endpoint to /v1/ws and added regression assertion in ws tests + +**Approach:** Standard approach + +--- + +## Key Decisions + +### Switch Python SDK websocket endpoint to /v1/ws +- **Chose:** Switch Python SDK websocket endpoint to /v1/ws +- **Reasoning:** Server route is /v1/ws and /v1/stream returns 404 Route not found + +--- + +## Chapters + +### 1. Work +*Agent: default* + +- Switch Python SDK websocket endpoint to /v1/ws: Switch Python SDK websocket endpoint to /v1/ws diff --git a/.trajectories/completed/2026-02/traj_vwrhp5hvaml2.json b/.trajectories/completed/2026-02/traj_vwrhp5hvaml2.json new file mode 100644 index 00000000..bd8f1358 --- /dev/null +++ b/.trajectories/completed/2026-02/traj_vwrhp5hvaml2.json @@ -0,0 +1,53 @@ +{ + "id": "traj_vwrhp5hvaml2", + "version": 1, + "task": { + "title": "Remove stale root-level test-prod-ws.mjs script" + }, + "status": "completed", + "startedAt": "2026-02-20T14:23:42.546Z", + "agents": [ + { + "name": "default", + "role": "lead", + "joinedAt": "2026-02-20T14:23:49.484Z" + } + ], + "chapters": [ + { + "id": "chap_jaariusqakfd", + "title": "Work", + "agentName": "default", + "startedAt": "2026-02-20T14:23:49.484Z", + "events": [ + { + "ts": 1771597429485, + "type": "decision", + "content": "Remove stale manual WS smoke script: Remove stale manual WS smoke script", + "raw": { + "question": "Remove stale manual WS smoke script", + "chosen": "Remove stale manual WS smoke script", + "alternatives": [], + "reasoning": "Its coverage is duplicated by scripts/e2e.ts in CI and it used legacy /v1/stream route" + }, + "significance": "high" + } + ], + "endedAt": "2026-02-20T14:23:52.308Z" + } + ], + "commits": [], + "filesChanged": [], + "projectId": "/Users/will/Projects/relaycast", + "tags": [], + "_trace": { + "startRef": "e08d637cf6e6311c9a2c30e5c88c07ad4622903a", + "endRef": "e08d637cf6e6311c9a2c30e5c88c07ad4622903a" + }, + "completedAt": "2026-02-20T14:23:52.308Z", + "retrospective": { + "summary": "Deleted stale test-prod-ws.mjs script", + "approach": "Standard approach", + "confidence": 0.98 + } +} \ No newline at end of file diff --git a/.trajectories/completed/2026-02/traj_vwrhp5hvaml2.md b/.trajectories/completed/2026-02/traj_vwrhp5hvaml2.md new file mode 100644 index 00000000..b5488dd4 --- /dev/null +++ b/.trajectories/completed/2026-02/traj_vwrhp5hvaml2.md @@ -0,0 +1,31 @@ +# Trajectory: Remove stale root-level test-prod-ws.mjs script + +> **Status:** ✅ Completed +> **Confidence:** 98% +> **Started:** February 20, 2026 at 09:23 AM +> **Completed:** February 20, 2026 at 09:23 AM + +--- + +## Summary + +Deleted stale test-prod-ws.mjs script + +**Approach:** Standard approach + +--- + +## Key Decisions + +### Remove stale manual WS smoke script +- **Chose:** Remove stale manual WS smoke script +- **Reasoning:** Its coverage is duplicated by scripts/e2e.ts in CI and it used legacy /v1/stream route + +--- + +## Chapters + +### 1. Work +*Agent: default* + +- Remove stale manual WS smoke script: Remove stale manual WS smoke script diff --git a/.trajectories/index.json b/.trajectories/index.json index 0db76fea..a7685d9f 100644 --- a/.trajectories/index.json +++ b/.trajectories/index.json @@ -1,6 +1,6 @@ { "version": 1, - "lastUpdated": "2026-02-20T01:42:34.863Z", + "lastUpdated": "2026-02-20T14:23:52.381Z", "trajectories": { "traj_mcpcredfallback": { "title": "Add credential file fallback to MCP stdio entry point", @@ -698,6 +698,20 @@ "startedAt": "2026-02-20T01:35:18.494Z", "completedAt": "2026-02-20T01:42:34.786Z", "path": "/Users/will/Projects/relaycast/.trajectories/completed/2026-02/traj_6vawys36wf3u.json" + }, + "traj_nck4ip3pfbfz": { + "title": "Update Python SDK WebSocket endpoint to /v1/ws", + "status": "completed", + "startedAt": "2026-02-20T14:14:09.112Z", + "completedAt": "2026-02-20T14:15:19.338Z", + "path": "/Users/will/Projects/relaycast/.trajectories/completed/2026-02/traj_nck4ip3pfbfz.json" + }, + "traj_vwrhp5hvaml2": { + "title": "Remove stale root-level test-prod-ws.mjs script", + "status": "completed", + "startedAt": "2026-02-20T14:23:42.546Z", + "completedAt": "2026-02-20T14:23:52.308Z", + "path": "/Users/will/Projects/relaycast/.trajectories/completed/2026-02/traj_vwrhp5hvaml2.json" } } } \ No newline at end of file diff --git a/packages/python-sdk/src/relay_sdk/ws.py b/packages/python-sdk/src/relay_sdk/ws.py index 74c0bfd6..6afe8d67 100644 --- a/packages/python-sdk/src/relay_sdk/ws.py +++ b/packages/python-sdk/src/relay_sdk/ws.py @@ -73,7 +73,7 @@ async def connect(self) -> None: async def _connect_once(self) -> None: url = ( - f"{self._base_url}/v1/stream" + f"{self._base_url}/v1/ws" f"?token={quote(self._token, safe='')}" f"&origin_surface={quote(self._origin_surface, safe='')}" f"&origin_client={quote(self._origin_client, safe='')}" diff --git a/packages/python-sdk/tests/test_ws.py b/packages/python-sdk/tests/test_ws.py index 6ff17c0d..f39ac649 100644 --- a/packages/python-sdk/tests/test_ws.py +++ b/packages/python-sdk/tests/test_ws.py @@ -150,6 +150,7 @@ async def test_connect_url_includes_origin_query_params(self): await ws._connect_once() url = connect_mock.call_args.args[0] + assert "/v1/ws?" in url assert "token=at_xxx" in url assert "origin_surface=sdk" in url assert "origin_client=%40relaycast%2Fpython-sdk" in url diff --git a/test-prod-ws.mjs b/test-prod-ws.mjs deleted file mode 100644 index 91b075b8..00000000 --- a/test-prod-ws.mjs +++ /dev/null @@ -1,167 +0,0 @@ -/** - * Relaycast Production WebSocket End-to-End Test - * - * 1. Create workspace - * 2. Register agents (sender + listener) - * 3. Create channel - * 4. Connect WebSocket & subscribe - * 5. Send message via REST API - * 6. Verify WebSocket receives message.created event - */ - -import WebSocket from "ws"; - -const BASE = "https://api.relaycast.dev"; -const WS_BASE = "wss://api.relaycast.dev"; -const CHANNEL = `test-ws-${Date.now()}`; - -async function api(method, path, body, token) { - const headers = { "Content-Type": "application/json" }; - if (token) headers["Authorization"] = `Bearer ${token}`; - - const res = await fetch(`${BASE}${path}`, { - method, - headers, - body: body ? JSON.stringify(body) : undefined, - }); - - const text = await res.text(); - let json; - try { - json = JSON.parse(text); - } catch { - console.error(` Response (${res.status}): ${text}`); - throw new Error(`Non-JSON response from ${method} ${path}`); - } - - if (!json.ok) { - console.error(` FAIL ${method} ${path}:`, json.error); - throw new Error(json.error?.message || "API error"); - } - - return json.data; -} - -async function main() { - console.log("=== Relaycast Production WebSocket Test ===\n"); - - // 1. Create workspace - console.log("1. Creating workspace..."); - const ws = await api("POST", "/v1/workspaces", { - name: `ws-test-${Date.now()}`, - }); - const apiKey = ws.apiKey || ws.api_key; - console.log(` Workspace: ${ws.name} (id: ${ws.id})`); - console.log(` API Key: ${apiKey.slice(0, 20)}...\n`); - - // 2. Register agents - console.log("2. Registering agents..."); - const listener = await api( - "POST", - "/v1/agents", - { name: "listener-bot" }, - apiKey - ); - console.log(` Listener: ${listener.name} (token: ${listener.token.slice(0, 20)}...)`); - - const sender = await api( - "POST", - "/v1/agents", - { name: "sender-bot" }, - apiKey - ); - console.log(` Sender: ${sender.name} (token: ${sender.token.slice(0, 20)}...)\n`); - - // 3. Create channel - console.log(`3. Creating channel '${CHANNEL}'...`); - await api("POST", "/v1/channels", { name: CHANNEL }, apiKey); - console.log(" Done\n"); - - // 4. Join channel - console.log("4. Joining channel..."); - await api("POST", `/v1/channels/${CHANNEL}/join`, null, listener.token); - await api("POST", `/v1/channels/${CHANNEL}/join`, null, sender.token); - console.log(" Both agents joined\n"); - - // 5. Connect WebSocket - console.log("5. Connecting WebSocket..."); - const wsUrl = `${WS_BASE}/v1/stream?token=${listener.token}`; - - const result = await new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - sock.close(); - reject(new Error("Timed out waiting for message.created event (15s)")); - }, 15000); - - const events = []; - const sock = new WebSocket(wsUrl); - - sock.on("error", (err) => { - clearTimeout(timeout); - reject(new Error(`WebSocket error: ${err.message}`)); - }); - - sock.on("open", () => { - console.log(" WebSocket connected\n"); - }); - - sock.on("message", async (data) => { - const msg = JSON.parse(data.toString()); - console.log(` WS event: ${JSON.stringify(msg)}`); - events.push(msg); - - if (msg.type === "connected") { - // Subscribe to channel - console.log(`\n6. Subscribing to '${CHANNEL}' and sending message...`); - sock.send( - JSON.stringify({ type: "subscribe", channels: [CHANNEL] }) - ); - - // Brief delay then send message - await new Promise((r) => setTimeout(r, 1000)); - - const msgText = `Hello from prod test at ${new Date().toISOString()}`; - console.log(` Sending: "${msgText}"`); - const sent = await api( - "POST", - `/v1/channels/${CHANNEL}/messages`, - { text: msgText }, - sender.token - ); - console.log(` Message sent (id: ${sent.id})\n`); - console.log("7. Waiting for WebSocket event...\n"); - } - - if (msg.type === "message.created") { - clearTimeout(timeout); - sock.close(); - resolve({ success: true, event: msg, allEvents: events }); - } - }); - - sock.on("close", (code, reason) => { - console.log( - ` WebSocket closed: ${code} ${reason?.toString() || ""}` - ); - }); - }); - - console.log("\n=== RESULTS ==="); - if (result.success) { - console.log("SUCCESS: Received message.created via WebSocket!"); - console.log(` Channel: ${result.event.channel}`); - console.log(` Message: ${JSON.stringify(result.event.message, null, 2)}`); - } - console.log( - `\nTotal WS events received: ${result.allEvents.length}` - ); - console.log( - "Events:", - result.allEvents.map((e) => e.type) - ); -} - -main().catch((err) => { - console.error(`\nFAILED: ${err.message}`); - process.exit(1); -});