Files
wehub-resource-sync 41cb1c0170
OpenSSF Scorecard / scorecard (push) Failing after 0s
DCO / dco (push) Failing after 0s
CodeQL SAST / analyze (push) Failing after 1s
Deploy Pages / deploy (push) Failing after 1s
chore: import upstream snapshot with attribution
2026-07-13 12:28:05 +08:00

174 lines
8.3 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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 58 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);
}