/* * repro_issue514.c -- Reproduce-first case for OPEN bug #514. * * Issue: #514 -- "trace_path data_flow mode doesn't surface arg expressions; * NestJS DI patterns defeat ~70% of caller resolution" * * Sub-claim reproduced: (A) data_flow mode omits argument expressions. * * Why sub-claim A over sub-claim B (NestJS DI caller resolution): * (A) has a crisp binary assertion: the "e" field either appears in the JSON * output or it does not. (B) is a statistical claim (~70% failure rate) that * requires a NestJS-specific fixture and a headcount of resolved callers across * many call sites -- impossible to assert precisely in a unit test. (A) can * be reproduced with a small two-function Python fixture and one strstr check. * * Root cause: * The MCP schema for trace_path documents data_flow mode as "follow CALLS + * DATA_FLOWS with arg expressions" (mcp.c line 356-357 and 363-364). Argument * expressions at each call site ARE stored in the graph: pass_parallel.c:: * append_args_json serializes each CBMCallArg as {"i":,"e":",...} * into the CALLS edge properties_json column. However, * bfs_to_json_array() (mcp.c ~line 2283) only emits the node fields (name, * qualified_name, hop, risk, is_test) from cbm_node_hop_t. The edge that * carried the arg expressions is NOT propagated by cbm_store_bfs() into the * cbm_traverse_result_t (cbm_edge_info_t carries only from_name, to_name, * type, confidence -- no properties_json). So even if the user requests * mode="data_flow", every hop in the response lacks the "args" field and the * individual arg expression text ("e") is permanently absent from the output. * * Expected (correct) behaviour: * After indexing a two-function Python file where caller() passes a compound * expression (payload_info + 1) to callee(), a trace_path call with * mode="data_flow" and direction="outbound" on "caller" must include the * argument expression text "payload_info" in the response JSON -- either in an * "args" array inside the hop object, or as a standalone "e" field. * * Actual (buggy) behaviour: * The response is: * {"function":"caller","direction":"outbound","mode":"data_flow", * "callees":[{"name":"callee","qualified_name":"...","hop":1}]} * The hop object contains NO "args" and NO "e"/"arg_expr" field. * strstr(resp, "payload_info") returns NULL. * * Why RED on current code: * The precondition assertion (CALLS edges >= 1) passes -- edge creation * and arg serialisation in pass_parallel.c are correct. The final * ASSERT_NOT_NULL(strstr(resp, "payload_info")) FAILS because * bfs_to_json_array() never reads or re-emits edge properties_json, so the * arg expression "payload_info" stored in the CALLS edge is permanently * discarded before it reaches the MCP JSON output. * * Fix location (not implemented here): * cbm_store_bfs() in src/store/store.c must propagate edge properties_json * into the cbm_traverse_result_t (extend cbm_edge_info_t or cbm_node_hop_t). * bfs_to_json_array() in src/mcp/mcp.c must then emit an "args" field when * mode == "data_flow" and the incoming edge has a non-empty args array. */ #include #include "test_framework.h" #include "repro_harness.h" #include #include #include /* * Fixture: two Python functions in one file. * * def callee(x): * return x * 2 * * def caller(): * result = callee(payload_info + 1) * return result * * caller() passes the compound expression (payload_info + 1) as the first * positional argument to callee(). The extractor captures this as a CBMCallArg * with .expr == "payload_info + 1" (or a prefix thereof after sanitization). * append_args_json serializes it into the CALLS edge as: * {"args":[{"i":0,"e":"payload_info + 1"}]} * * The expression token "payload_info" is unique enough to identify in the * output: strstr(resp, "payload_info") is the assertion anchor. * * Python is used here because its CALLS extraction (including arg expressions) * is proven reliable -- see repro_issue480.c for the same fixture approach. */ static const RFile k_files[] = { { "service.py", "def callee(x):\n" " return x * 2\n" "\n" "def caller():\n" " result = callee(payload_info + 1)\n" " return result\n" } }; /* * TEST: repro_issue514_data_flow_surfaces_arg_expr * * Precondition (must be GREEN to prove this is a data_flow surfacing bug): * rh_count_edges(store, project, "CALLS") >= 1 * If this fires RED, the extractor has a regression unrelated to #514. * * Failing assertion (RED on current code): * strstr(resp, "payload_info") != NULL * i.e. the argument expression text must appear somewhere in the response. */ TEST(repro_issue514_data_flow_surfaces_arg_expr) { RProj lp; cbm_store_t *store = rh_index_files(&lp, k_files, (int)(sizeof(k_files) / sizeof(k_files[0]))); ASSERT_NOT_NULL(store); /* * Precondition: at least one CALLS edge must exist after indexing. * If this fires RED the fixture is broken, not data_flow mode. * The caller() -> callee(payload_info + 1) call must produce one edge. */ int calls_count = rh_count_edges(store, lp.project, "CALLS"); fprintf(stderr, " [514] CALLS edges=%d (expected>=1; 0=extraction regression)\n", calls_count); ASSERT_GT(calls_count, 0); /* * Invoke trace_path with mode="data_flow", direction="outbound" on "caller". * * Args (matching the trace_path JSON schema in mcp.c ~line 355-374): * function_name -- "caller": the function that passes the argument * project -- lp.project: derived from the temp dir * direction -- "outbound": follow callees (caller -> callee) * depth -- 2: one hop is enough * mode -- "data_flow": the mode that promises arg expressions * * Expected response (correct): * {"function":"caller","direction":"outbound","mode":"data_flow", * "callees":[{"name":"callee","qualified_name":"...","hop":1, * "args":[{"i":0,"e":"payload_info + 1"}]}]} * -- or any JSON structure that includes the string "payload_info". * * Buggy response: * {"function":"caller","direction":"outbound","mode":"data_flow", * "callees":[{"name":"callee","qualified_name":"...","hop":1}]} * -- no "args", no "e", no "payload_info" anywhere. */ char args[512]; snprintf(args, sizeof(args), "{\"function_name\":\"caller\"," "\"project\":\"%s\"," "\"direction\":\"outbound\"," "\"depth\":2," "\"mode\":\"data_flow\"}", lp.project); char *resp = cbm_mcp_handle_tool(lp.srv, "trace_path", args); ASSERT_NOT_NULL(resp); fprintf(stderr, " [514] trace_path data_flow response: %.400s\n", resp); /* The response must not be an error -- the node must be found. */ ASSERT_NULL(strstr(resp, "function not found")); /* The response is the MCP tool-result envelope (inner json embedded as an * escaped string value), so the "callees" key appears as \"callees\". * Match the escaped form (see repro_issue480 / test_incremental's * resp_has_key idiom). */ ASSERT_NOT_NULL(strstr(resp, "\\\"callees\\\"")); /* The callees array must be non-empty: the callee's QN tail "service.callee" * must appear as a hop (unambiguous + escaping-proof). RED if the CALLS * traversal is broken (separate from #514). */ ASSERT_NULL(strstr(resp, "\\\"callees\\\":[]")); ASSERT_NOT_NULL(strstr(resp, "service.callee")); /* * THE CORE ASSERTION FOR BUG #514: * * The argument expression "payload_info" (part of "payload_info + 1" passed * to callee()) must appear in the response JSON when mode="data_flow". * * WHY RED on current code: * bfs_to_json_array() (mcp.c ~line 2283) only emits cbm_node_hop_t fields * (name, qualified_name, hop). cbm_edge_info_t (store.h ~line 146) does * not carry properties_json, so the "e":"payload_info + 1" stored in the * CALLS edge never reaches the JSON output. strstr returns NULL. * * This assertion is the canonical RED line for bug #514. */ ASSERT_NOT_NULL(strstr(resp, "payload_info")); free(resp); rh_cleanup(&lp, store); PASS(); } /* ── Suite ─────────────────────────────────────────────────────────────────── */ SUITE(repro_issue514) { RUN_TEST(repro_issue514_data_flow_surfaces_arg_expr); }