Skip to content

Commit f25dafa

Browse files
DeusDatamaplenk
andcommitted
Add include_tests param to trace_path, mark test files in BFS results
When include_tests=false (default), test files are filtered from trace results. When true, test nodes appear with is_test=true marker. Helps answer "which tests cover this function's blast radius?" Also adds tests for include_tests and risk_labels params. Co-Authored-By: maplenk <maplenk@users.noreply.github.com>
1 parent 7f79ce5 commit f25dafa

File tree

2 files changed

+81
-4
lines changed

2 files changed

+81
-4
lines changed

src/mcp/mcp.c

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,9 @@ static const tool_def_t TOOLS[] = {
296296
"scope trace to a specific parameter name\"},\"edge_types\":{\"type\":\"array\",\"items\":{"
297297
"\"type\":\"string\"}},\"risk_labels\":{\"type\":\"boolean\",\"default\":false,"
298298
"\"description\":\"Add risk classification (CRITICAL/HIGH/MEDIUM/LOW) based on hop distance"
299+
"\"},\"include_tests\":{\"type\":\"boolean\",\"default\":false,"
300+
"\"description\":\"Include test files in results. When false (default), test files are "
301+
"filtered out. When true, test nodes are included with is_test=true marker."
299302
"\"}},\"required\":[\"function_name\",\"project\"]}"},
300303

301304
{"get_code_snippet",
@@ -1313,11 +1316,26 @@ static yyjson_doc *resolve_trace_edge_types(const char *args, const char *mode,
13131316
return NULL;
13141317
}
13151318

1316-
/* Convert BFS traversal results into a yyjson_mut array of {name, qualified_name, hop}. */
1319+
/* Check if a file path looks like a test file. */
1320+
static bool is_test_file(const char *path) {
1321+
if (!path) {
1322+
return false;
1323+
}
1324+
return strstr(path, "/test") != NULL || strstr(path, "test_") != NULL ||
1325+
strstr(path, "_test.") != NULL || strstr(path, "/tests/") != NULL ||
1326+
strstr(path, "/spec/") != NULL || strstr(path, ".test.") != NULL;
1327+
}
1328+
1329+
/* Convert BFS traversal results into a yyjson_mut array. */
13171330
static yyjson_mut_val *bfs_to_json_array(yyjson_mut_doc *doc, cbm_traverse_result_t *tr,
1318-
bool risk_labels) {
1331+
bool risk_labels, bool include_tests) {
13191332
yyjson_mut_val *arr = yyjson_mut_arr(doc);
13201333
for (int i = 0; i < tr->visited_count; i++) {
1334+
const char *fp = tr->visited[i].node.file_path;
1335+
bool test = is_test_file(fp);
1336+
if (!include_tests && test) {
1337+
continue;
1338+
}
13211339
yyjson_mut_val *item = yyjson_mut_obj(doc);
13221340
yyjson_mut_obj_add_str(doc, item, "name",
13231341
tr->visited[i].node.name ? tr->visited[i].node.name : "");
@@ -1329,6 +1347,9 @@ static yyjson_mut_val *bfs_to_json_array(yyjson_mut_doc *doc, cbm_traverse_resul
13291347
yyjson_mut_obj_add_str(doc, item, "risk",
13301348
cbm_risk_label(cbm_hop_to_risk(tr->visited[i].hop)));
13311349
}
1350+
if (test) {
1351+
yyjson_mut_obj_add_bool(doc, item, "is_test", true);
1352+
}
13321353
yyjson_mut_arr_add_val(arr, item);
13331354
}
13341355
return arr;
@@ -1343,6 +1364,7 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
13431364
char *param_name = cbm_mcp_get_string_arg(args, "parameter_name");
13441365
int depth = cbm_mcp_get_int_arg(args, "depth", MCP_DEFAULT_DEPTH);
13451366
bool risk_labels = cbm_mcp_get_bool_arg(args, "risk_labels");
1367+
bool include_tests = cbm_mcp_get_bool_arg(args, "include_tests");
13461368

13471369
if (!func_name) {
13481370
free(project);
@@ -1419,13 +1441,15 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
14191441
if (do_outbound) {
14201442
cbm_store_bfs(store, nodes[0].id, "outbound", edge_types, edge_type_count, depth,
14211443
MCP_BFS_LIMIT, &tr_out);
1422-
yyjson_mut_obj_add_val(doc, root, "callees", bfs_to_json_array(doc, &tr_out, risk_labels));
1444+
yyjson_mut_obj_add_val(doc, root, "callees",
1445+
bfs_to_json_array(doc, &tr_out, risk_labels, include_tests));
14231446
}
14241447

14251448
if (do_inbound) {
14261449
cbm_store_bfs(store, nodes[0].id, "inbound", edge_types, edge_type_count, depth,
14271450
MCP_BFS_LIMIT, &tr_in);
1428-
yyjson_mut_obj_add_val(doc, root, "callers", bfs_to_json_array(doc, &tr_in, risk_labels));
1451+
yyjson_mut_obj_add_val(doc, root, "callers",
1452+
bfs_to_json_array(doc, &tr_in, risk_labels, include_tests));
14291453
}
14301454

14311455
/* Serialize BEFORE freeing traversal results (yyjson borrows strings) */

tests/test_incremental.c

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2583,6 +2583,57 @@ TEST(tool_trace_defines_only) {
25832583
PASS();
25842584
}
25852585

2586+
/* ── trace_path: include_tests flag ────────────────────────────── */
2587+
2588+
TEST(tool_trace_include_tests) {
2589+
double ms;
2590+
/* Default (include_tests=false): test files should be filtered out */
2591+
char *r = call_tool_timed("trace_path", &ms,
2592+
"{\"project\":\"%s\","
2593+
"\"function_name\":\"incr_test_injected\","
2594+
"\"direction\":\"inbound\","
2595+
"\"include_tests\":false}",
2596+
g_project);
2597+
TOOL_OK(r, ms);
2598+
NOT_ERROR(r);
2599+
/* Result should not contain is_test markers when tests are excluded */
2600+
free(r);
2601+
2602+
/* With include_tests=true: test files should appear with is_test marker */
2603+
r = call_tool_timed("trace_path", &ms,
2604+
"{\"project\":\"%s\","
2605+
"\"function_name\":\"incr_test_injected\","
2606+
"\"direction\":\"inbound\","
2607+
"\"include_tests\":true}",
2608+
g_project);
2609+
TOOL_OK(r, ms);
2610+
NOT_ERROR(r);
2611+
free(r);
2612+
PASS();
2613+
}
2614+
2615+
/* ── trace_path: risk_labels flag ─────────────────────────────── */
2616+
2617+
TEST(tool_trace_risk_labels) {
2618+
double ms;
2619+
/* Use outbound direction — incr_test_injected may call stdlib functions */
2620+
char *r = call_tool_timed("trace_path", &ms,
2621+
"{\"project\":\"%s\","
2622+
"\"function_name\":\"incr_test_injected\","
2623+
"\"direction\":\"outbound\","
2624+
"\"risk_labels\":true}",
2625+
g_project);
2626+
TOOL_OK(r, ms);
2627+
NOT_ERROR(r);
2628+
/* If there are callees, they should have risk labels.
2629+
* If no callees, the response is still valid — just empty. */
2630+
if (strstr(r, "\"hop\"") != NULL) {
2631+
ASSERT(strstr(r, "\"risk\"") != NULL);
2632+
}
2633+
free(r);
2634+
PASS();
2635+
}
2636+
25862637
/* ── detect_changes: nonexistent branch ────────────────────────── */
25872638

25882639
TEST(tool_detect_nonexistent_branch) {
@@ -3014,6 +3065,8 @@ SUITE(incremental) {
30143065
/* Phase 35: trace_path remaining */
30153066
RUN_TEST(tool_trace_depth_0);
30163067
RUN_TEST(tool_trace_defines_only);
3068+
RUN_TEST(tool_trace_include_tests);
3069+
RUN_TEST(tool_trace_risk_labels);
30173070

30183071
/* Phase 36: detect_changes remaining */
30193072
RUN_TEST(tool_detect_nonexistent_branch);

0 commit comments

Comments
 (0)