@@ -8230,6 +8230,56 @@ TEST(pipeline_capitalized_axios_import_is_http_client_sequential) {
82308230 return capitalized_axios_case(0);
82318231}
82328232
8233+ /* FastAPI/Starlette WebSocket route registration (distilled from #1245). The
8234+ * route-registration suffix table knew `.add_api_websocket_route` but not the
8235+ * decorator forms `.websocket` / `.websocket_route`, so a WebSocket endpoint
8236+ * registered via `@router.websocket('/ws')` produced no Route node at all —
8237+ * the endpoint was invisible to the graph. `@app.get('/health')` is the
8238+ * positive control (already a route suffix). Fewer than 50 files exercises
8239+ * pass_calls.c; the same suffix table drives the parallel resolver. */
8240+ TEST(pipeline_python_websocket_decorators_register_routes) {
8241+ char tmp[256];
8242+ snprintf(tmp, sizeof(tmp), "/tmp/cbm_py_ws_routes_XXXXXX");
8243+ if (!cbm_mkdtemp(tmp)) {
8244+ FAIL("tmpdir");
8245+ }
8246+
8247+ write_temp_file(tmp, "app.py",
8248+ "from fastapi import APIRouter, FastAPI, WebSocket\n\n"
8249+ "app = FastAPI()\n"
8250+ "router = APIRouter()\n\n"
8251+ "@app.get('/health')\n"
8252+ "def health():\n"
8253+ " return {'ok': True}\n\n"
8254+ "@router.websocket('/ws')\n"
8255+ "async def ws_endpoint(websocket: WebSocket):\n"
8256+ " await websocket.accept()\n\n"
8257+ "@app.websocket_route('/events')\n"
8258+ "async def events_endpoint(websocket: WebSocket):\n"
8259+ " await websocket.accept()\n");
8260+
8261+ char db_path[512];
8262+ snprintf(db_path, sizeof(db_path), "%s/py_ws.db", tmp);
8263+ cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
8264+ ASSERT_NOT_NULL(p);
8265+ ASSERT_EQ(cbm_pipeline_run(p), 0);
8266+ const char *project = cbm_pipeline_project_name(p);
8267+
8268+ cbm_store_t *s = cbm_store_open_path(db_path);
8269+ ASSERT_NOT_NULL(s);
8270+
8271+ /* Positive control: the HTTP decorator mints its Route node. */
8272+ ASSERT_GTE(count_nodes_named(s, project, "/health"), 1);
8273+ /* The two WebSocket decorator forms must mint Route nodes too. */
8274+ ASSERT_GTE(count_nodes_named(s, project, "/ws"), 1);
8275+ ASSERT_GTE(count_nodes_named(s, project, "/events"), 1);
8276+
8277+ cbm_store_close(s);
8278+ cbm_pipeline_free(p);
8279+ th_rmtree(tmp);
8280+ PASS();
8281+ }
8282+
82338283/* Python bare-call local-binding suppression, sequential path. The bare-call
82348284 * counterpart of the receiver guard above: `run` is a PARAMETER, so `run()`
82358285 * cannot be the module-level `run` and must not bind SatoriLive.run.
@@ -18410,6 +18460,7 @@ SUITE(pipeline) {
1841018460 RUN_TEST(pipeline_tsjs_receiver_parallel_keeps_service_edges);
1841118461 RUN_TEST(pipeline_capitalized_axios_import_is_http_client);
1841218462 RUN_TEST(pipeline_capitalized_axios_import_is_http_client_sequential);
18463+ RUN_TEST(pipeline_python_websocket_decorators_register_routes);
1841318464 RUN_TEST(pipeline_python_receiver_parallel_suppresses_weak_method_edges);
1841418465 RUN_TEST(pipeline_python_bare_local_binding_suppresses_weak_edge);
1841518466 RUN_TEST(pipeline_python_bare_local_binding_parallel_suppresses_weak_edge);
0 commit comments