fix: WsDevTest non-standard error format and align doc examples

- Fix two error returns in ws_dev_test.py using non-standard {"_error": True, ...} format, which _collect_results misidentifies as a success response and wraps as ok=True, sending a false-success to the client - Switch to WsResult.error(code=..., message=...) standard API, consistent with all other handlers - Fix 5 occurrences of process_event → process in documentation examples - Remove non-existent HANDLER_ID and HANDLED_EVENTS class attribute examples from docs - Fix validate_event_types (plural) → validate_event_type (singular) in docs

keyboardstaff committed Mar 27, 2026 at 23:34 UTC 0749ddc932f206c3d451e33be33e475bf3d1808a
2 files changed +23 -19
api/ws_dev_test.py
+10 -5
@@ -2,6 +2,7 @@ import asyncio
2 from typing import Any
3
4 from helpers.ws import WsHandler
5 +from helpers.ws_manager import WsResult
6 from helpers.print_style import PrintStyle
7 from helpers import runtime
8
@@ -9,15 +10,19 @@ from helpers import runtime
10 class WsDevTest(WsHandler):
11 """Developer-only WebSocket test harness handler."""
12
12 - async def process(self, event: str, data: dict, sid: str) -> dict[str, Any] | None:
13 + async def process(self, event: str, data: dict, sid: str) -> dict[str, Any] | WsResult | None:
14 if event == "ws_event_console_subscribe":
15 if not runtime.is_development():
15 - return {"_error": True, "code": "NOT_AVAILABLE",
16 - "message": "Event console is available only in development mode"}
16 + return WsResult.error(
17 + code="NOT_AVAILABLE",
18 + message="Event console is available only in development mode",
19 + )
20 registered = self.manager.register_diagnostic_watcher(self.namespace, sid)
21 if not registered:
19 - return {"_error": True, "code": "SUBSCRIBE_FAILED",
20 - "message": "Unable to subscribe to diagnostics"}
22 + return WsResult.error(
23 + code="SUBSCRIBE_FAILED",
24 + message="Unable to subscribe to diagnostics",
25 + )
26 return {"status": "subscribed", "timestamp": data.get("requestedAt")}
27
28 if event == "ws_event_console_unsubscribe":
docs/developer/websockets.md
+13 -14
@@ -148,15 +148,13 @@ Create new handler files as `api/ws_<name>.py` and inherit from `WsHandler`.
148 from helpers.ws import WsHandler
149
150 class WsMyFeature(WsHandler):
151 - HANDLER_ID = "my_feature"
152 - HANDLED_EVENTS = ["my_event_a", "my_event_b"]
151
154 - async def process_event(self, event_type: str, data: dict[str, Any], sid: str) -> dict | None:
155 - if event_type == "dashboard_refresh":
152 + async def process(self, event: str, data: dict, sid: str) -> dict | None:
153 + if event == "dashboard_refresh":
154 stats = await self._load_stats(data.get("scope", "all"))
155 return {"ok": True, "stats": stats}
156
159 - if event_type == "dashboard_push":
157 + if event == "dashboard_push":
158 await self.broadcast(
159 "dashboard_update",
160 {"stats": data.get("stats", {}), "source": sid},
@@ -165,16 +163,16 @@ class WsMyFeature(WsHandler):
163 return None
164 ```
165
168 -Handlers are auto-loaded on startup; duplicate event declarations produce warnings but are supported. Use `validate_event_types` to ensure names follow lowercase snake_case and avoid Socket.IO reserved events.
166 +Handlers are auto-loaded on startup. The `handlerId` is derived automatically from the fully-qualified class name (e.g., `api.ws_my_feature.WsMyFeature`). All registered handlers receive every event; use conditional logic inside `process()` to filter by event type.
167
168 ### 2. Consuming Client Events (Server as Consumer)
169
172 -- Implement `process_event` and return either `None` (fire-and-forget) or a dict that becomes the handler’s contribution in `results[]`.
170 +- Implement `process` and return either `None` (fire-and-forget) or a dict that becomes the handler's contribution in `results[]`.
171 - Use dependency injection (async functions, database calls, etc.) but keep event loop friendly—no blocking calls.
172 - Validate input vigorously and return structured errors as needed.
173
174 ```python
177 -async def process_event(self, event_type: str, data: dict, sid: str) -> dict | None:
175 +async def process(self, event: str, data: dict, sid: str) -> dict | None:
176 if "query" not in data:
177 return {"ok": False, "error": {"code": "VALIDATION", "error": "Missing query"}}
178
@@ -271,7 +269,7 @@ console.log(window.runtimeInfo.id, window.runtimeInfo.isDevelopment);
269
270 ### Namespaces (end-state)
271
274 -- The root namespace (`/`) is reserved and intentionally unhandled by default for application events. Feature code should connect to an explicit namespace (for example `/webui`).
272 +- The root namespace (`/`) is reserved and intentionally unhandled by default for application events. Feature code should connect to the `/ws` namespace (defined as `NAMESPACE` in `helpers/ws.py`).
273 - The frontend exposes `createNamespacedClient(namespace)` and `getNamespacedClient(namespace)` (one client instance per namespace per tab). Namespaced clients expose the same minimal API: `emit`, `request`, `on`, `off`.
274 - Unknown namespaces are rejected deterministically during the Socket.IO connect handshake with a `connect_error` payload:
275 - `err.message === "UNKNOWN_NAMESPACE"`
@@ -509,8 +507,8 @@ websocket.on("confirm_close_tab", async ({ data, correlationId }) => {
507 Sometimes you want to acknowledge work immediately but stream additional updates later. Combine `request()` for the initial confirmation and `emit_to()` for follow-up events using the same correlation ID.
508
509 ```python
512 -async def process_event(self, event_type: str, data: dict, sid: str) -> dict | None:
513 - if event_type != "start_long_task":
510 +async def process(self, event: str, data: dict, sid: str) -> dict | None:
511 + if event != "start_long_task":
512 return None
513
514 correlation_id = data.get("correlationId")
@@ -609,7 +607,7 @@ The manager validates the payload, resolves/creates `correlationId`, and passes
607
608 ## Best Practices Checklist
609
612 -- [ ] Always validate inbound payloads in `process_event` (required fields, type constraints, length limits).
610 +- [ ] Always validate inbound payloads in `process()` (required fields, type constraints, length limits).
611 - [ ] Propagate `correlationId` through multi-step workflows so logs and envelopes align.
612 - [ ] Respect the 50 MB payload cap; prefer HTTP + polling for bulk data transfers.
613 - [ ] Ensure long-running operations emit progress via `emit_to` or switch to an async task with periodic updates.
@@ -657,7 +655,7 @@ The manager validates the payload, resolves/creates `correlationId`, and passes
655
656 > **Tip:** When extending the infrastructure (new metadata) start by updating the contracts, sync the manager/frontend helpers, and then document the change here so producers and consumers stay in lockstep.
657
660 -## Error Codes Registry (Draft for Phase 6)
658 +## Error Codes Registry
659
660 The WebSocket stack standardizes backend error codes returned in `RequestResultItem.error.code`. This registry documents the currently used codes and their intended meaning. Client and server implementations should reference these values verbatim (UPPER_SNAKE_CASE).
661
@@ -666,7 +664,8 @@ The WebSocket stack standardizes backend error codes returned in `RequestResultI
664 | `NO_HANDLERS` | Manager routing | No handler is registered for the requested `eventType`. | Register a handler for the event or correct the event name. | `{ "handlerId": "WsManager", "ok": false, "error": { "code": "NO_HANDLERS", "error": "No handler for 'missing'" } }` |
665 | `TIMEOUT` | Aggregated or single request | The request exceeded `timeoutMs`. | Increase `timeoutMs`, reduce handler processing time, or split work. | `{ "handlerId": "ExampleHandler", "ok": false, "error": { "code": "TIMEOUT", "error": "Request timeout" } }` |
666 | `CONNECTION_NOT_FOUND` | Single‑sid request | Target `sid` is not connected/known. | Use an active `sid` or retry after reconnect. | `{ "handlerId": "WsManager", "ok": false, "error": { "code": "CONNECTION_NOT_FOUND", "error": "Connection 'sid-123' not found" } }` |
669 -| `HARNESS_UNKNOWN_EVENT` | Developer harness | Harness test handler received an unsupported event name. | Update harness sources or disable the step before running automation. | `{ "handlerId": "api.ws_dev_test.WsDevTest", "ok": false, "error": { "code": "HARNESS_UNKNOWN_EVENT", "error": "Unhandled event", "details": "ws_tester_foo" } }` |
667 +| `NOT_AVAILABLE` | Developer harness | Feature is restricted to development mode. | Ensure `runtime.is_development()` returns `True` or skip the operation. | `{ "handlerId": "api.ws_dev_test.WsDevTest", "ok": false, "error": { "code": "NOT_AVAILABLE", "error": "Event console is available only in development mode" } }` |
668 +| `SUBSCRIBE_FAILED` | Developer harness | Diagnostic watcher subscription failed. | Verify the SID is connected and retry. | `{ "handlerId": "api.ws_dev_test.WsDevTest", "ok": false, "error": { "code": "SUBSCRIBE_FAILED", "error": "Unable to subscribe to diagnostics" } }` |
669
670 Notes
671 - Error payload shape follows the contract documented in `contracts/event-schemas.md` (`RequestResultItem.error`).