chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
* 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 <foundation/compat.h>
|
||||
#include "test_framework.h"
|
||||
#include "repro_harness.h"
|
||||
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
|
||||
/* ── 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":"<inner trace_path json>"}]}
|
||||
* 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);
|
||||
}
|
||||
Reference in New Issue
Block a user