@@ -3725,6 +3725,118 @@ TEST(tool_trace_path_evidence_is_opt_in_and_class_mapped) {
37253725 PASS();
37263726}
37273727
3728+ /* #1542 leftover: header order is strategy,confidence then args, but json
3729+ * used to emit args first; tree flat_trace (risk_labels || data_flow) used
3730+ * to call bfs_to_toon_table without include_evidence. Pin both: every row
3731+ * has len(cols)==len(row), and the strategy cell is the class not the args
3732+ * array. */
3733+ TEST(tool_trace_path_evidence_columns_match_header_issue1542) {
3734+ cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
3735+ cbm_store_t *st = cbm_mcp_server_store(srv);
3736+ const char *proj = "ev-order";
3737+ cbm_mcp_server_set_project(srv, proj);
3738+ cbm_store_upsert_project(st, proj, "/tmp/ev-order");
3739+ cbm_node_t caller = {.project = proj,
3740+ .label = "Function",
3741+ .name = "caller",
3742+ .qualified_name = "ev-order.src.caller",
3743+ .file_path = "src/a.c",
3744+ .start_line = 1,
3745+ .end_line = 5};
3746+ cbm_node_t callee = {.project = proj,
3747+ .label = "Function",
3748+ .name = "target",
3749+ .qualified_name = "ev-order.src.target",
3750+ .file_path = "src/a.c",
3751+ .start_line = 10,
3752+ .end_line = 20};
3753+ int64_t id_caller = cbm_store_upsert_node(st, &caller);
3754+ int64_t id_callee = cbm_store_upsert_node(st, &callee);
3755+ ASSERT_GT(id_caller, 0);
3756+ ASSERT_GT(id_callee, 0);
3757+ cbm_edge_t e = {.project = proj,
3758+ .source_id = id_caller,
3759+ .target_id = id_callee,
3760+ .type = "CALLS",
3761+ .properties_json = "{\"callee\":\"target\",\"confidence\":0.95,"
3762+ "\"strategy\":\"lsp_trait_dispatch\",\"candidates\":1,"
3763+ "\"args\":[\"x\"]}"};
3764+ ASSERT_GT(cbm_store_insert_edge(st, &e), 0);
3765+
3766+ /* json × data_flow × include_evidence: cols identity, not just count. */
3767+ char *js = cbm_mcp_server_handle(
3768+ srv, "{\"jsonrpc\":\"2.0\",\"id\":94,\"method\":\"tools/call\","
3769+ "\"params\":{\"name\":\"trace_path\",\"arguments\":{\"function_name\":\"caller\","
3770+ "\"project\":\"ev-order\",\"direction\":\"outbound\",\"include_evidence\":true,"
3771+ "\"mode\":\"data_flow\",\"format\":\"json\"}}}");
3772+ ASSERT_NOT_NULL(js);
3773+ char *js_txt = extract_text_content(js);
3774+ ASSERT_NOT_NULL(js_txt);
3775+ yyjson_doc *doc = yyjson_read(js_txt, strlen(js_txt), 0);
3776+ ASSERT_NOT_NULL(doc);
3777+ yyjson_val *callees = yyjson_obj_get(yyjson_doc_get_root(doc), "callees");
3778+ ASSERT_NOT_NULL(callees);
3779+ yyjson_val *cols = yyjson_obj_get(callees, "cols");
3780+ ASSERT_NOT_NULL(cols);
3781+ static const char *want[] = {"name", "hop", "strategy", "confidence", "args"};
3782+ ASSERT_EQ((int)yyjson_arr_size(cols), 5);
3783+ for (int i = 0; i < 5; i++) {
3784+ ASSERT_STR_EQ(yyjson_get_str(yyjson_arr_get(cols, i)), want[i]);
3785+ }
3786+ yyjson_val *hop1 = NULL;
3787+ yyjson_val *groups = yyjson_obj_get(callees, "groups");
3788+ ASSERT_NOT_NULL(groups);
3789+ size_t ng = yyjson_arr_size(groups);
3790+ for (size_t g = 0; g < ng; g++) {
3791+ yyjson_val *rows = yyjson_obj_get(yyjson_arr_get(groups, g), "rows");
3792+ if (!rows) {
3793+ continue;
3794+ }
3795+ size_t nr = yyjson_arr_size(rows);
3796+ for (size_t r = 0; r < nr; r++) {
3797+ yyjson_val *row = yyjson_arr_get(rows, r);
3798+ yyjson_val *hop = row ? yyjson_arr_get(row, 1) : NULL;
3799+ if (hop && yyjson_get_int(hop) >= 1) {
3800+ hop1 = row;
3801+ break;
3802+ }
3803+ }
3804+ if (hop1) {
3805+ break;
3806+ }
3807+ }
3808+ ASSERT_NOT_NULL(hop1);
3809+ ASSERT_EQ((int)yyjson_arr_size(hop1), 5);
3810+ ASSERT_TRUE(yyjson_is_str(yyjson_arr_get(hop1, 2)));
3811+ ASSERT_STR_EQ(yyjson_get_str(yyjson_arr_get(hop1, 2)), "lsp");
3812+ ASSERT_TRUE(yyjson_is_num(yyjson_arr_get(hop1, 3)));
3813+ ASSERT_TRUE(yyjson_is_arr(yyjson_arr_get(hop1, 4)));
3814+ yyjson_doc_free(doc);
3815+ free(js_txt);
3816+ free(js);
3817+
3818+ /* tree × risk_labels × include_evidence used to drop evidence entirely
3819+ * because flat_trace routed through bfs_to_toon_table without the flag. */
3820+ char *tree = cbm_mcp_server_handle(
3821+ srv, "{\"jsonrpc\":\"2.0\",\"id\":95,\"method\":\"tools/call\","
3822+ "\"params\":{\"name\":\"trace_path\",\"arguments\":{\"function_name\":\"caller\","
3823+ "\"project\":\"ev-order\",\"direction\":\"outbound\",\"include_evidence\":true,"
3824+ "\"risk_labels\":true}}}");
3825+ ASSERT_NOT_NULL(tree);
3826+ char *tree_txt = extract_text_content(tree);
3827+ ASSERT_NOT_NULL(tree_txt);
3828+ ASSERT_NOT_NULL(strstr(tree_txt, "strategy"));
3829+ ASSERT_NOT_NULL(strstr(tree_txt, "confidence"));
3830+ ASSERT_NOT_NULL(strstr(tree_txt, "lsp"));
3831+ ASSERT_NOT_NULL(strstr(tree_txt, "0.95"));
3832+ ASSERT_NULL(strstr(tree_txt, "lsp_trait_dispatch"));
3833+ free(tree_txt);
3834+ free(tree);
3835+
3836+ cbm_mcp_server_free(srv);
3837+ PASS();
3838+ }
3839+
37283840/* Reproduce-first (#887): the client-supplied `depth` on trace_call_path must be
37293841 * clamped to the MCP ceiling (cbm_mcp_max_depth(), default 15). On origin/main
37303842 * an MCP_MAX_DEPTH=15 constant was defined but never applied — `depth` flowed
@@ -13757,6 +13869,7 @@ SUITE(mcp) {
1375713869 RUN_TEST(tool_trace_call_path_prefers_definition);
1375813870 RUN_TEST(trace_evidence_strategy_class_vocabulary_is_closed);
1375913871 RUN_TEST(tool_trace_path_evidence_is_opt_in_and_class_mapped);
13872+ RUN_TEST(tool_trace_path_evidence_columns_match_header_issue1542);
1376013873 RUN_TEST(tool_trace_call_path_depth_clamped);
1376113874 RUN_TEST(tool_trace_call_path_distinct_defs_not_over_unioned);
1376213875 RUN_TEST(tool_trace_call_path_dts_stub_unions_with_impl);
0 commit comments