/* * repro_issue480.c — Reproduce-first case for OPEN bug #480. * * Issue: #480 — "trace_path returns empty for all functions despite * traversable CALLS edges (v0.8.1, macOS arm64)" * * Root cause (identified by maintainer DeusData + reporter halindrome): * handle_trace_call_path() calls cbm_store_find_nodes_by_name() to locate * the start node for BFS. On the affected build, the name-to-node lookup * returns node_count == 0 for EVERY function name — even names that the * graph clearly contains (confirmed by query_graph Cypher returning the same * function with 5–8 inbound CALLS edges). The fallback to * cbm_store_find_node_by_qn() also returns nothing, so the handler exits * with a "function not found" error OR (when the node IS found by name) * the BFS start-node id does not match any edge endpoint stored in the * graph, so cbm_store_bfs() returns visited_count == 0 and the "callers" * / "callees" JSON arrays are serialised empty. * * The split: query_graph Cypher (direct SQL) traverses the same edges * correctly, while trace_path (BFS via start-node id) yields nothing. * This isolates the bug to trace_path's own start-node lookup or to how * the resolved node id is passed to cbm_store_bfs(), NOT to edge creation. * * Expected (correct) behaviour: * After indexing a two-function Python file where caller() calls callee(), * trace_path for "callee" with direction="inbound" must return a non-empty * "callers" array that contains a node named "caller". * * Actual (buggy) behaviour: * trace_path returns {"function":"callee","direction":"inbound","callers":[]} * — an empty "callers" array — even though CALLS edges exist in the graph * and are walkable via query_graph. * * Why RED on current code: * The precondition assertion (CALLS edges > 0) passes because edge creation * is correct. The subsequent assertion that resp contains the string * "\"caller\"" (the caller function's name embedded in the callers array) * FAILS because cbm_store_bfs() finds no hops from the resolved start node. * * How this isolates the traversal bug from an extraction bug: * If CALLS edges were the problem, rh_count_edges(store, …, "CALLS") would * return 0 and the ASSERT_GT precondition would fire RED — visibly flagging * an extraction failure instead. By asserting the precondition GREEN and * the trace_path result RED, we prove the edges exist and the fault lies * exclusively in trace_path's traversal layer. * * Fix location (not implemented here): * cbm_store_find_nodes_by_name() or cbm_store_bfs() in * src/store/store.c — the node id returned by name lookup must match * the source/target ids stored in the edges table. */ #include #include "test_framework.h" #include "repro_harness.h" #include #include #include /* ── Fixture ──────────────────────────────────────────────────────────────── * * Two Python functions in one file: * * def callee(): * return 42 * * def caller(): * return callee() * * Python has proven reliable CALLS extraction (test_extraction.c:python_calls * asserts calls.count > 0 for a simpler fixture; the integration suite's * main.py fixture yields CALLS edges that are visible via query_graph). * caller() → callee() is a simple, unambiguous intra-file call: the extractor * sees exactly one callee() call expression inside caller(), so the graph * must have ≥ 1 CALLS edge after indexing. */ static const RFile k_files[] = { { "main.py", "def callee():\n" " return 42\n" "\n" "def caller():\n" " return callee()\n" } }; /* ───────────────────────────────────────────────────────────────────────── * repro_issue480_trace_path_nonempty_with_calls * * Precondition (must be GREEN to prove this is a traversal bug): * rh_count_edges(store, project, "CALLS") > 0 * * The failing assertion (RED on buggy code): * The "callers" array in the trace_path response is non-empty and contains * the string "caller" (the name of the caller function). * ───────────────────────────────────────────────────────────────────────── */ TEST(repro_issue480_trace_path_nonempty_with_calls) { 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: extraction must have produced ≥ 1 CALLS edge ────── * If this fires RED, the fixture or language has an extraction bug — * that is a different problem from #480. Switch to a different * language fixture (e.g. Go utils.go with Multiply→Add) in that case. */ int calls_count = rh_count_edges(store, lp.project, "CALLS"); ASSERT_GT(calls_count, 0); /* ── Invoke trace_path for "callee" with direction="inbound" ───────── * * Args match the trace_path schema (required: function_name, project): * function_name — bare name "callee"; also tested by the reporter with * the fully-qualified name, both yield empty on buggy code * project — lp.project (derived from tmpdir by cbm_project_name_from_path) * direction — "inbound": ask for callers of callee() * depth — 2: enough to reach one hop (caller → callee) * * Expected response shape (correct): * {"function":"callee","direction":"inbound","callers":[{"name":"caller",...},...]} * * Buggy response shape: * {"function":"callee","direction":"inbound","callers":[]} * (or: {"error":"function not found",...} if the name lookup fails entirely) */ char args[512]; snprintf(args, sizeof(args), "{\"function_name\":\"callee\"," "\"project\":\"%s\"," "\"direction\":\"inbound\"," "\"depth\":2}", lp.project); char *resp = cbm_mcp_handle_tool(lp.srv, "trace_path", args); ASSERT_NOT_NULL(resp); /* The response must NOT be a "function not found" error. * If the name lookup itself fails, this fires first and pinpoints the * start-node lookup as the breakage site. */ ASSERT_NULL(strstr(resp, "function not found")); /* The response is the MCP tool-result envelope * {"content":[{"type":"text","text":""}]} * so the inner json is embedded as a STRING value and its quotes are * backslash-escaped: the "callers" key appears as \"callers\" in the * serialized response. Match the escaped form — the project's own * passing trace_path tests (test_incremental.c, via resp_has_key) do the * same. (The earlier unescaped strstr could never match a correctly * escaped MCP envelope, which is why this repro was mis-targeted.) * * The "callers" key must appear (always emitted for inbound). */ ASSERT_NOT_NULL(strstr(resp, "\\\"callers\\\"")); /* The "callers" array must be NON-EMPTY. WHY RED on the #480 bug: * cbm_store_bfs() returning 0 hops serialises \"callers\":[] (no caller * QN in the response), so BOTH the empty-array guard and the caller-QN * assertion fire RED. We assert the caller's qualified-name tail * "main.caller" (unambiguous vs the callee "main.callee", and immune to * escaping) so a populated, correctly-named caller hop is required. */ ASSERT_NULL(strstr(resp, "\\\"callers\\\":[]")); /* empty array = traversal bug */ ASSERT_NOT_NULL(strstr(resp, "main.caller")); /* caller QN in results */ free(resp); rh_cleanup(&lp, store); PASS(); } /* ── Suite ─────────────────────────────────────────────────────────────── */ SUITE(repro_issue480) { RUN_TEST(repro_issue480_trace_path_nonempty_with_calls); }