mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
test(routes): test-file callers and handlers pair only within their file
Design decision 2026-10-18 (R1 + R2): a DATA_FLOWS caller in a test file pairs only with handlers in that same file, and a handler in a test file pairs only with callers in that same file. infrascan: test callers by each language rule (a tests/ directory, a Java *Test.java, a TS .spec.ts) get no flow to a production handler; a production caller gets no flow to a test mock server; a test reaches the mock server of its own file but not one in another test file. Controls: a production caller keeps its flow, and so does a same-file test pair. edge_types_probe: a Spring controller, a mock controller under src/test/, a production TS client and a TS .test.ts client meet on the same __route__ANY__ nodes. Exactly one flow (production client to production controller) on the sequential and on the parallel path. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
1 parent
684dcf5447
commit
30bd5aaa91
2 files changed
+202
No files matched your search
@@ -1088,6 +1088,132 @@ static int et_handles_exact(const EtFile *files, int nfiles, const EtHandles *wa
|
||||
return et_handles_exact_routes(files, nfiles, want, parallel, NULL);
|
||||
}
|
||||
|
||||
/* The exact DATA_FLOWS set, caller -> handler by QN tail. route_qns (NULL-terminated) must
|
||||
* exist, so a test that expects NO flow on a path cannot pass because the caller's or the
|
||||
* handler's Route was never created; handles is the exact HANDLES count. */
|
||||
typedef struct {
|
||||
const char *caller_tail;
|
||||
const char *handler_tail;
|
||||
} EtFlow;
|
||||
|
||||
static int et_flow_matches(cbm_store_t *store, const cbm_edge_t *e, const EtFlow *want,
|
||||
int wanted, int *found) {
|
||||
cbm_node_t src = {0};
|
||||
cbm_node_t tgt = {0};
|
||||
int have_src = cbm_store_find_node_by_id(store, e->source_id, &src) == CBM_STORE_OK;
|
||||
int have_tgt = cbm_store_find_node_by_id(store, e->target_id, &tgt) == CBM_STORE_OK;
|
||||
int matched = 0;
|
||||
for (int wi = 0; have_src && have_tgt && wi < wanted; wi++) {
|
||||
if (!found[wi] && et_qn_has_tail(src.qualified_name, want[wi].caller_tail) &&
|
||||
et_qn_has_tail(tgt.qualified_name, want[wi].handler_tail)) {
|
||||
found[wi] = matched = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!matched) {
|
||||
fprintf(stderr, " [ET-FLOWS] unexpected %s -> %s\n",
|
||||
have_src && src.qualified_name ? src.qualified_name : "<?>",
|
||||
have_tgt && tgt.qualified_name ? tgt.qualified_name : "<?>");
|
||||
}
|
||||
if (have_src) cbm_node_free_fields(&src);
|
||||
if (have_tgt) cbm_node_free_fields(&tgt);
|
||||
return matched;
|
||||
}
|
||||
|
||||
static int et_data_flows_exact(const EtFile *files, int nfiles, const EtFlow *want, int parallel,
|
||||
const char *const *route_qns, int handles) {
|
||||
EtProj lp;
|
||||
cbm_store_t *store =
|
||||
parallel ? et_index_parallel(&lp, files, nfiles) : et_index_files(&lp, files, nfiles);
|
||||
int wanted = 0;
|
||||
int found[ET_ROUTE_ASSERT_MAX] = {0};
|
||||
while (want[wanted].caller_tail && wanted < ET_ROUTE_ASSERT_MAX) {
|
||||
wanted++;
|
||||
}
|
||||
cbm_edge_t *edges = NULL;
|
||||
int n = 0;
|
||||
int ok = store != NULL &&
|
||||
cbm_store_find_edges_by_type(store, lp.project, "DATA_FLOWS", &edges, &n) ==
|
||||
CBM_STORE_OK &&
|
||||
n == wanted;
|
||||
for (int i = 0; store && i < n; i++) {
|
||||
if (!et_flow_matches(store, &edges[i], want, wanted, found)) {
|
||||
ok = 0;
|
||||
}
|
||||
}
|
||||
for (int wi = 0; wi < wanted; wi++) {
|
||||
if (!found[wi]) {
|
||||
ok = 0;
|
||||
fprintf(stderr, " [ET-FLOWS] missing %s -> %s\n", want[wi].caller_tail,
|
||||
want[wi].handler_tail);
|
||||
}
|
||||
}
|
||||
for (int ri = 0; route_qns && route_qns[ri]; ri++) {
|
||||
cbm_node_t route = {0};
|
||||
if (!store ||
|
||||
cbm_store_find_node_by_qn(store, lp.project, route_qns[ri], &route) != CBM_STORE_OK) {
|
||||
ok = 0;
|
||||
fprintf(stderr, " [ET-FLOWS] missing Route %s\n", route_qns[ri]);
|
||||
} else {
|
||||
cbm_node_free_fields(&route);
|
||||
}
|
||||
}
|
||||
int got_handles = store ? cbm_store_count_edges_by_type(store, lp.project, "HANDLES") : -1;
|
||||
if (got_handles != handles) {
|
||||
ok = 0;
|
||||
fprintf(stderr, " [ET-FLOWS] HANDLES=%d expected %d\n", got_handles, handles);
|
||||
}
|
||||
if (!ok) {
|
||||
fprintf(stderr, " [ET-FLOWS] FAIL (%s path) expected=%d actual=%d\n",
|
||||
parallel ? "parallel" : "sequential", wanted, n);
|
||||
}
|
||||
if (edges) cbm_store_free_edges(edges, n);
|
||||
et_cleanup(&lp, store);
|
||||
return ok;
|
||||
}
|
||||
|
||||
/* R1 + R2 (user decision, 2026-10-18): DATA_FLOWS pair a test-file caller only with handlers in its
|
||||
* own file, and a test-file handler only with callers in its own file. A Spring controller and a
|
||||
* mock controller under src/test/ both answer `@RequestMapping` (any method). A production TS
|
||||
* client and a TS test (.test.ts) call them with fetch, which names no method, so every caller and
|
||||
* handler meets on the same __route__ANY__ node. Only the production client -> production
|
||||
* controller pair is a data flow of the application, on the sequential and the parallel path. */
|
||||
static const EtFile et_r12_files[] = {
|
||||
{"server/OrderController.java",
|
||||
"package com.example;\n\n"
|
||||
"import org.springframework.web.bind.annotation.RequestMapping;\n"
|
||||
"import org.springframework.web.bind.annotation.RestController;\n\n"
|
||||
"@RestController\n@RequestMapping(\"/api\")\npublic class OrderController {\n"
|
||||
" @RequestMapping(\"/orders\")\n"
|
||||
" public String orders() {\n return \"orders\";\n }\n}\n"},
|
||||
{"server/src/test/java/com/example/MockPaymentController.java",
|
||||
"package com.example;\n\n"
|
||||
"import org.springframework.web.bind.annotation.RequestMapping;\n"
|
||||
"import org.springframework.web.bind.annotation.RestController;\n\n"
|
||||
"@RestController\n@RequestMapping(\"/api\")\npublic class MockPaymentController {\n"
|
||||
" @RequestMapping(\"/payments\")\n"
|
||||
" public String pay() {\n return \"paid\";\n }\n}\n"},
|
||||
{"web/src/api.ts",
|
||||
"export async function loadOrders() {\n return fetch(\"/api/orders\");\n}\n\n"
|
||||
"export async function loadPayments() {\n return fetch(\"/api/payments\");\n}\n"},
|
||||
{"web/src/api.test.ts",
|
||||
"export async function testLoadOrders() {\n return fetch(\"/api/orders\");\n}\n"}};
|
||||
|
||||
static const EtFlow et_r12_flows[] = {{"api.loadOrders", "OrderController.orders"}, {NULL, NULL}};
|
||||
|
||||
static const char *const et_r12_routes[] = {"__route__ANY__/api/orders",
|
||||
"__route__ANY__/api/payments", NULL};
|
||||
|
||||
TEST(data_flows_test_files_pair_within_their_file_sequential) {
|
||||
ASSERT_TRUE(et_data_flows_exact(et_r12_files, 4, et_r12_flows, 0, et_r12_routes, 2));
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(data_flows_test_files_pair_within_their_file_parallel) {
|
||||
ASSERT_TRUE(et_data_flows_exact(et_r12_files, 4, et_r12_flows, 1, et_r12_routes, 2));
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* #1146: Laravel's two class-based handler forms —
|
||||
* Route::get('/x', [UserController::class, 'show']) (controller method)
|
||||
* Route::get('/x', GetCurrentUserController::class) (invokable: __invoke)
|
||||
@@ -2589,6 +2715,8 @@ SUITE(edge_types_probe) {
|
||||
RUN_TEST(handles_fiber_group_prefix_parallel_issue686);
|
||||
RUN_TEST(handles_gin_group_prefix_issue686);
|
||||
RUN_TEST(handles_spring_java);
|
||||
RUN_TEST(data_flows_test_files_pair_within_their_file_sequential);
|
||||
RUN_TEST(data_flows_test_files_pair_within_their_file_parallel);
|
||||
RUN_TEST(handles_spring_java_path_attribute_fourth);
|
||||
RUN_TEST(handles_spring_kotlin);
|
||||
RUN_TEST(handles_jaxrs_java);
|
||||
|
||||
@@ -477,6 +477,78 @@ TEST(infrascan_data_flows_reach_every_infra_handler) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* R1 + R2 (user decision, 2026-10-18): a caller in a test file pairs only with handlers in that same
|
||||
* file, and a handler in a test file pairs only with callers in that same file. A test's request to
|
||||
* the application, a production caller matched to a test's mock server, and the private mock servers
|
||||
* of two tests pairing across files are not data flows of the application. */
|
||||
static int64_t r12_fn(cbm_gbuf_t *gb, const char *name, const char *file) {
|
||||
char qn[96];
|
||||
snprintf(qn, sizeof(qn), "test.%s", name);
|
||||
return cbm_gbuf_upsert_node(gb, "Function", name, qn, file, 1, 3, "{}");
|
||||
}
|
||||
|
||||
static int64_t r12_route(cbm_gbuf_t *gb, const char *path) {
|
||||
char qn[128];
|
||||
snprintf(qn, sizeof(qn), "__route__GET__%s", path);
|
||||
return cbm_gbuf_upsert_node(gb, "Route", path, qn, "", 0, 0, "{\"method\":\"GET\"}");
|
||||
}
|
||||
|
||||
static void r12_call(cbm_gbuf_t *gb, int64_t caller, int64_t route, const char *path) {
|
||||
char props[160];
|
||||
snprintf(props, sizeof(props),
|
||||
"{\"callee\":\"requests.get\",\"url_path\":\"%s\",\"method\":\"GET\"}", path);
|
||||
cbm_gbuf_insert_edge(gb, caller, route, "HTTP_CALLS", props);
|
||||
}
|
||||
|
||||
TEST(infrascan_data_flows_test_callers_pair_only_within_their_file) {
|
||||
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp/cbm_infrascan_r12_callers");
|
||||
ASSERT_NOT_NULL(gb);
|
||||
int64_t route = r12_route(gb, "/api/orders");
|
||||
int64_t handler = r12_fn(gb, "list_orders", "app/views.py");
|
||||
cbm_gbuf_insert_edge(gb, handler, route, "HANDLES", "{}");
|
||||
/* Test files by each language's own rule: a tests/ directory, a Java *Test.java, a TS .spec.ts. */
|
||||
int64_t py_test = r12_fn(gb, "test_list_orders", "tests/test_orders.py");
|
||||
int64_t java_test = r12_fn(gb, "listsOrders", "app/OrderServiceTest.java");
|
||||
int64_t ts_spec = r12_fn(gb, "fetchesOrders", "web/orders.spec.ts");
|
||||
int64_t prod = r12_fn(gb, "load_orders", "app/client.py");
|
||||
r12_call(gb, py_test, route, "/api/orders");
|
||||
r12_call(gb, java_test, route, "/api/orders");
|
||||
r12_call(gb, ts_spec, route, "/api/orders");
|
||||
r12_call(gb, prod, route, "/api/orders");
|
||||
cbm_pipeline_create_route_nodes(gb);
|
||||
ASSERT_FALSE(has_data_flow(gb, py_test, handler));
|
||||
ASSERT_FALSE(has_data_flow(gb, java_test, handler));
|
||||
ASSERT_FALSE(has_data_flow(gb, ts_spec, handler));
|
||||
/* Control: the production caller keeps its flow. */
|
||||
ASSERT_TRUE(has_data_flow(gb, prod, handler));
|
||||
cbm_gbuf_free(gb);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(infrascan_data_flows_test_handlers_pair_only_within_their_file) {
|
||||
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp/cbm_infrascan_r12_handlers");
|
||||
ASSERT_NOT_NULL(gb);
|
||||
int64_t route = r12_route(gb, "/api/s3");
|
||||
/* Two tests each run their own mock server on the same path. */
|
||||
int64_t mock_a = r12_fn(gb, "mock_s3_a", "tests/test_upload.py");
|
||||
int64_t mock_b = r12_fn(gb, "mock_s3_b", "tests/conftest.py");
|
||||
cbm_gbuf_insert_edge(gb, mock_a, route, "HANDLES", "{}");
|
||||
cbm_gbuf_insert_edge(gb, mock_b, route, "HANDLES", "{}");
|
||||
int64_t prod = r12_fn(gb, "upload", "app/storage.py");
|
||||
int64_t test_a = r12_fn(gb, "test_upload", "tests/test_upload.py");
|
||||
r12_call(gb, prod, route, "/api/s3");
|
||||
r12_call(gb, test_a, route, "/api/s3");
|
||||
cbm_pipeline_create_route_nodes(gb);
|
||||
/* A production caller never reaches a test's mock server. */
|
||||
ASSERT_FALSE(has_data_flow(gb, prod, mock_a));
|
||||
ASSERT_FALSE(has_data_flow(gb, prod, mock_b));
|
||||
/* A test reaches its own file's mock server, not another test file's. */
|
||||
ASSERT_TRUE(has_data_flow(gb, test_a, mock_a));
|
||||
ASSERT_FALSE(has_data_flow(gb, test_a, mock_b));
|
||||
cbm_gbuf_free(gb);
|
||||
PASS();
|
||||
}
|
||||
|
||||
SUITE(infrascan) {
|
||||
RUN_TEST(infrascan_http_route_literal_guard_rejects_filesystem_paths);
|
||||
RUN_TEST(infrascan_http_route_literal_guard_rejects_document_extensions);
|
||||
@@ -488,4 +560,6 @@ SUITE(infrascan) {
|
||||
RUN_TEST(infrascan_data_flows_link_every_caller_of_a_route);
|
||||
RUN_TEST(infrascan_data_flows_do_not_depend_on_caller_insertion_order);
|
||||
RUN_TEST(infrascan_data_flows_reach_every_infra_handler);
|
||||
RUN_TEST(infrascan_data_flows_test_callers_pair_only_within_their_file);
|
||||
RUN_TEST(infrascan_data_flows_test_handlers_pair_only_within_their_file);
|
||||
}
|
||||
Reference in new issue
Block a user