@@ -3385,6 +3385,118 @@ TEST(tool_trace_path_evidence_is_opt_in_and_class_mapped) {
33853385 PASS ();
33863386}
33873387
3388+ /* #1542 leftover: header order is strategy,confidence then args, but json
3389+ * used to emit args first; tree flat_trace (risk_labels || data_flow) used
3390+ * to call bfs_to_toon_table without include_evidence. Pin both: every row
3391+ * has len(cols)==len(row), and the strategy cell is the class not the args
3392+ * array. */
3393+ TEST (tool_trace_path_evidence_columns_match_header_issue1542 ) {
3394+ cbm_mcp_server_t * srv = cbm_mcp_server_new (NULL );
3395+ cbm_store_t * st = cbm_mcp_server_store (srv );
3396+ const char * proj = "ev-order" ;
3397+ cbm_mcp_server_set_project (srv , proj );
3398+ cbm_store_upsert_project (st , proj , "/tmp/ev-order" );
3399+ cbm_node_t caller = {.project = proj ,
3400+ .label = "Function" ,
3401+ .name = "caller" ,
3402+ .qualified_name = "ev-order.src.caller" ,
3403+ .file_path = "src/a.c" ,
3404+ .start_line = 1 ,
3405+ .end_line = 5 };
3406+ cbm_node_t callee = {.project = proj ,
3407+ .label = "Function" ,
3408+ .name = "target" ,
3409+ .qualified_name = "ev-order.src.target" ,
3410+ .file_path = "src/a.c" ,
3411+ .start_line = 10 ,
3412+ .end_line = 20 };
3413+ int64_t id_caller = cbm_store_upsert_node (st , & caller );
3414+ int64_t id_callee = cbm_store_upsert_node (st , & callee );
3415+ ASSERT_GT (id_caller , 0 );
3416+ ASSERT_GT (id_callee , 0 );
3417+ cbm_edge_t e = {.project = proj ,
3418+ .source_id = id_caller ,
3419+ .target_id = id_callee ,
3420+ .type = "CALLS" ,
3421+ .properties_json = "{\"callee\":\"target\",\"confidence\":0.95,"
3422+ "\"strategy\":\"lsp_trait_dispatch\",\"candidates\":1,"
3423+ "\"args\":[\"x\"]}" };
3424+ ASSERT_GT (cbm_store_insert_edge (st , & e ), 0 );
3425+
3426+ /* json × data_flow × include_evidence: cols identity, not just count. */
3427+ char * js = cbm_mcp_server_handle (
3428+ srv , "{\"jsonrpc\":\"2.0\",\"id\":94,\"method\":\"tools/call\","
3429+ "\"params\":{\"name\":\"trace_path\",\"arguments\":{\"function_name\":\"caller\","
3430+ "\"project\":\"ev-order\",\"direction\":\"outbound\",\"include_evidence\":true,"
3431+ "\"mode\":\"data_flow\",\"format\":\"json\"}}}" );
3432+ ASSERT_NOT_NULL (js );
3433+ char * js_txt = extract_text_content (js );
3434+ ASSERT_NOT_NULL (js_txt );
3435+ yyjson_doc * doc = yyjson_read (js_txt , strlen (js_txt ), 0 );
3436+ ASSERT_NOT_NULL (doc );
3437+ yyjson_val * callees = yyjson_obj_get (yyjson_doc_get_root (doc ), "callees" );
3438+ ASSERT_NOT_NULL (callees );
3439+ yyjson_val * cols = yyjson_obj_get (callees , "cols" );
3440+ ASSERT_NOT_NULL (cols );
3441+ static const char * want [] = {"name" , "hop" , "strategy" , "confidence" , "args" };
3442+ ASSERT_EQ ((int )yyjson_arr_size (cols ), 5 );
3443+ for (int i = 0 ; i < 5 ; i ++ ) {
3444+ ASSERT_STR_EQ (yyjson_get_str (yyjson_arr_get (cols , i )), want [i ]);
3445+ }
3446+ yyjson_val * hop1 = NULL ;
3447+ yyjson_val * groups = yyjson_obj_get (callees , "groups" );
3448+ ASSERT_NOT_NULL (groups );
3449+ size_t ng = yyjson_arr_size (groups );
3450+ for (size_t g = 0 ; g < ng ; g ++ ) {
3451+ yyjson_val * rows = yyjson_obj_get (yyjson_arr_get (groups , g ), "rows" );
3452+ if (!rows ) {
3453+ continue ;
3454+ }
3455+ size_t nr = yyjson_arr_size (rows );
3456+ for (size_t r = 0 ; r < nr ; r ++ ) {
3457+ yyjson_val * row = yyjson_arr_get (rows , r );
3458+ yyjson_val * hop = row ? yyjson_arr_get (row , 1 ) : NULL ;
3459+ if (hop && yyjson_get_int (hop ) >= 1 ) {
3460+ hop1 = row ;
3461+ break ;
3462+ }
3463+ }
3464+ if (hop1 ) {
3465+ break ;
3466+ }
3467+ }
3468+ ASSERT_NOT_NULL (hop1 );
3469+ ASSERT_EQ ((int )yyjson_arr_size (hop1 ), 5 );
3470+ ASSERT_TRUE (yyjson_is_str (yyjson_arr_get (hop1 , 2 )));
3471+ ASSERT_STR_EQ (yyjson_get_str (yyjson_arr_get (hop1 , 2 )), "lsp" );
3472+ ASSERT_TRUE (yyjson_is_num (yyjson_arr_get (hop1 , 3 )));
3473+ ASSERT_TRUE (yyjson_is_arr (yyjson_arr_get (hop1 , 4 )));
3474+ yyjson_doc_free (doc );
3475+ free (js_txt );
3476+ free (js );
3477+
3478+ /* tree × risk_labels × include_evidence used to drop evidence entirely
3479+ * because flat_trace routed through bfs_to_toon_table without the flag. */
3480+ char * tree = cbm_mcp_server_handle (
3481+ srv , "{\"jsonrpc\":\"2.0\",\"id\":95,\"method\":\"tools/call\","
3482+ "\"params\":{\"name\":\"trace_path\",\"arguments\":{\"function_name\":\"caller\","
3483+ "\"project\":\"ev-order\",\"direction\":\"outbound\",\"include_evidence\":true,"
3484+ "\"risk_labels\":true}}}" );
3485+ ASSERT_NOT_NULL (tree );
3486+ char * tree_txt = extract_text_content (tree );
3487+ ASSERT_NOT_NULL (tree_txt );
3488+ ASSERT_NOT_NULL (strstr (tree_txt , "strategy" ));
3489+ ASSERT_NOT_NULL (strstr (tree_txt , "confidence" ));
3490+ ASSERT_NOT_NULL (strstr (tree_txt , "lsp" ));
3491+ ASSERT_NOT_NULL (strstr (tree_txt , "0.95" ));
3492+ ASSERT_NULL (strstr (tree_txt , "lsp_trait_dispatch" ));
3493+ free (tree_txt );
3494+ free (tree );
3495+
3496+ cbm_mcp_server_free (srv );
3497+ PASS ();
3498+ }
3499+
33883500/* Reproduce-first (#887): the client-supplied `depth` on trace_call_path must be
33893501 * clamped to the MCP ceiling (cbm_mcp_max_depth(), default 15). On origin/main
33903502 * an MCP_MAX_DEPTH=15 constant was defined but never applied — `depth` flowed
@@ -11188,6 +11300,7 @@ SUITE(mcp) {
1118811300 RUN_TEST (tool_trace_call_path_prefers_definition );
1118911301 RUN_TEST (trace_evidence_strategy_class_vocabulary_is_closed );
1119011302 RUN_TEST (tool_trace_path_evidence_is_opt_in_and_class_mapped );
11303+ RUN_TEST (tool_trace_path_evidence_columns_match_header_issue1542 );
1119111304 RUN_TEST (tool_trace_call_path_depth_clamped );
1119211305 RUN_TEST (tool_trace_call_path_distinct_defs_not_over_unioned );
1119311306 RUN_TEST (tool_trace_call_path_dts_stub_unions_with_impl );
0 commit comments