Skip to content

Commit c5a3c6e

Browse files
DeusDataahundt
andcommitted
fix(routes): register FastAPI and Starlette WebSocket decorators
Distilled from #1245 by Andrew Hundt (a slice of 1ac3c13). The route-registration suffix table knew .add_api_websocket_route but not the decorator forms FastAPI and Starlette actually use: @router.websocket("/ws") @app.websocket_route("/events") so a WebSocket endpoint produced no Route node on either resolver path and was invisible to the graph. Two entries join the table, both recorded as ANY like the existing WebSocket entry -- a socket upgrade is not an HTTP verb. Both are needed: matching is a pure suffix compare and .route does not match ...websocket_route, whose tail is _route. Upstream also changed pass_calls.c, pass_parallel.c and pipeline_internal.h. None of that is taken: main already carries the #952 sequential unresolved-callee route fallback its July base lacked, so the table entries are the only missing piece. RED before the fix, each form recorded on its own (the harness stops at the first failing assert, so the two were swapped and re-run): pipeline_python_websocket_decorators_register_routes FAIL tests/test_pipeline.c:6163: count_nodes_named(s, project, "/ws") (0) not >= 1 (1) FAIL tests/test_pipeline.c:6163: count_nodes_named(s, project, "/events") (0) not >= 1 (1) The @app.get("/health") control above those lines passed before and after. GREEN after: pipeline 282 passed; pipeline parallel extraction edge_types_probe route_canon infrascan 779 passed, 0 failed. The test drives the sequential resolver (one file). The parallel resolver reads the same table through cbm_service_pattern_route_method and its suite is green; a CBM_WORKERS=4 twin of this test is a small follow-up. Co-authored-by: Andrew Hundt <ATHundt@gmail.com> Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
1 parent 8a7519d commit c5a3c6e

2 files changed

Lines changed: 56 additions & 0 deletions

File tree

‎internal/cbm/service_patterns.c‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,11 @@ static const method_suffix_t route_reg_suffixes[] = {
577577
{".add_route", "ANY"},
578578
{".add_api_route", "ANY"},
579579
{".add_api_websocket_route", "ANY"},
580+
/* FastAPI/Starlette WebSocket decorators (`@router.websocket('/ws')`,
581+
* `@app.websocket_route('/events')`): not an HTTP verb, so ANY — mirrors
582+
* .add_api_websocket_route above (#1245). */
583+
{".websocket", "ANY"},
584+
{".websocket_route", "ANY"},
580585
{NULL, NULL},
581586
};
582587

‎tests/test_pipeline.c‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7487,6 +7487,56 @@ TEST(pipeline_capitalized_axios_import_is_http_client_sequential) {
74877487
return capitalized_axios_case(0);
74887488
}
74897489

7490+
/* FastAPI/Starlette WebSocket route registration (distilled from #1245). The
7491+
* route-registration suffix table knew `.add_api_websocket_route` but not the
7492+
* decorator forms `.websocket` / `.websocket_route`, so a WebSocket endpoint
7493+
* registered via `@router.websocket('/ws')` produced no Route node at all —
7494+
* the endpoint was invisible to the graph. `@app.get('/health')` is the
7495+
* positive control (already a route suffix). Fewer than 50 files exercises
7496+
* pass_calls.c; the same suffix table drives the parallel resolver. */
7497+
TEST(pipeline_python_websocket_decorators_register_routes) {
7498+
char tmp[256];
7499+
snprintf(tmp, sizeof(tmp), "/tmp/cbm_py_ws_routes_XXXXXX");
7500+
if (!cbm_mkdtemp(tmp)) {
7501+
FAIL("tmpdir");
7502+
}
7503+
7504+
write_temp_file(tmp, "app.py",
7505+
"from fastapi import APIRouter, FastAPI, WebSocket\n\n"
7506+
"app = FastAPI()\n"
7507+
"router = APIRouter()\n\n"
7508+
"@app.get('/health')\n"
7509+
"def health():\n"
7510+
" return {'ok': True}\n\n"
7511+
"@router.websocket('/ws')\n"
7512+
"async def ws_endpoint(websocket: WebSocket):\n"
7513+
" await websocket.accept()\n\n"
7514+
"@app.websocket_route('/events')\n"
7515+
"async def events_endpoint(websocket: WebSocket):\n"
7516+
" await websocket.accept()\n");
7517+
7518+
char db_path[512];
7519+
snprintf(db_path, sizeof(db_path), "%s/py_ws.db", tmp);
7520+
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
7521+
ASSERT_NOT_NULL(p);
7522+
ASSERT_EQ(cbm_pipeline_run(p), 0);
7523+
const char *project = cbm_pipeline_project_name(p);
7524+
7525+
cbm_store_t *s = cbm_store_open_path(db_path);
7526+
ASSERT_NOT_NULL(s);
7527+
7528+
/* Positive control: the HTTP decorator mints its Route node. */
7529+
ASSERT_GTE(count_nodes_named(s, project, "/health"), 1);
7530+
/* The two WebSocket decorator forms must mint Route nodes too. */
7531+
ASSERT_GTE(count_nodes_named(s, project, "/ws"), 1);
7532+
ASSERT_GTE(count_nodes_named(s, project, "/events"), 1);
7533+
7534+
cbm_store_close(s);
7535+
cbm_pipeline_free(p);
7536+
th_rmtree(tmp);
7537+
PASS();
7538+
}
7539+
74907540
/* Python bare-call local-binding suppression, sequential path. The bare-call
74917541
* counterpart of the receiver guard above: `run` is a PARAMETER, so `run()`
74927542
* cannot be the module-level `run` and must not bind SatoriLive.run.
@@ -17568,6 +17618,7 @@ SUITE(pipeline) {
1756817618
RUN_TEST(pipeline_tsjs_receiver_parallel_keeps_service_edges);
1756917619
RUN_TEST(pipeline_capitalized_axios_import_is_http_client);
1757017620
RUN_TEST(pipeline_capitalized_axios_import_is_http_client_sequential);
17621+
RUN_TEST(pipeline_python_websocket_decorators_register_routes);
1757117622
RUN_TEST(pipeline_python_receiver_parallel_suppresses_weak_method_edges);
1757217623
RUN_TEST(pipeline_python_bare_local_binding_suppresses_weak_edge);
1757317624
RUN_TEST(pipeline_python_bare_local_binding_parallel_suppresses_weak_edge);

0 commit comments

Comments
 (0)