Merge branch 'main' into fix/cypher-coalesce-where-874

This commit is contained in:
Harshita Joshi authored and GitHub committed 2026-07-15 16:59:09 -05:00
commit 57cd3f9e78
14 files changed
+618 -38

No files matched your search

+88 -29
View File
@@ -1317,7 +1317,8 @@ static const char *annotation_route_method(const char *name) {
}
/* JAX-RS bare-verb annotations (@GET/@POST/...) — path comes from @Path. */
if (strcmp(name, "GET") == 0 || strcmp(name, "POST") == 0 || strcmp(name, "PUT") == 0 ||
strcmp(name, "DELETE") == 0 || strcmp(name, "PATCH") == 0) {
strcmp(name, "DELETE") == 0 || strcmp(name, "PATCH") == 0 || strcmp(name, "HEAD") == 0 ||
strcmp(name, "OPTIONS") == 0) {
return name;
}
return NULL;
@@ -1626,35 +1627,85 @@ static bool try_route_from_annotation(CBMArena *a, TSNode annotation, const char
return true;
}
/* Scan the annotation nodes nested in a JVM/C# `modifiers`/`attribute_list`
* wrapper (and direct children) for a route-mapping annotation. Java/Kotlin
* Spring annotations (@GetMapping, @RequestMapping, ...) live here rather than
* as prev-siblings, so the prev-sibling decorator walk never sees them. */
static bool extract_route_from_annotations(CBMArena *a, TSNode func_node, const char *source,
const CBMLangSpec *spec, const char **out_path,
const char **out_method) {
TSNode modifiers = find_jvm_modifiers(func_node, spec->language);
/* Scan ALL annotation nodes nested in a JVM/C# `modifiers`/`attribute_list`
* wrapper (and direct children), collecting route information from the whole
* set instead of stopping at the first mapping annotation. Java/Kotlin Spring
* annotations (@GetMapping, @RequestMapping, ...) live here rather than as
* prev-siblings, so the prev-sibling decorator walk never sees them.
*
* JAX-RS splits the route across two annotations: the verb comes from a bare
* @GET/@POST/... and the path from a sibling @Path("..."). Returning on the
* first mapping annotation therefore dropped every method-level @Path
* (the @GET matched first, defaulted the path to "/", and @Path was never
* read), and class-level @Path prefixes were never recognized at all. */
static void scan_route_annotations(CBMArena *a, TSNode owner, const char *source,
const CBMLangSpec *spec, const char **out_map_path,
const char **out_method, const char **out_jax_path) {
*out_map_path = NULL;
*out_method = NULL;
*out_jax_path = NULL;
TSNode wrappers[2];
int wn = 0;
TSNode modifiers = find_jvm_modifiers(owner, spec->language);
if (!ts_node_is_null(modifiers)) {
uint32_t mc = ts_node_child_count(modifiers);
for (uint32_t mi = 0; mi < mc; mi++) {
TSNode mchild = ts_node_child(modifiers, mi);
if (cbm_kind_in_set(mchild, spec->decorator_node_types) &&
try_route_from_annotation(a, mchild, source, out_path, out_method)) {
return true;
}
}
wrappers[wn++] = modifiers;
}
/* Direct-child annotations (some grammars attach the annotation as a child
* of the method node rather than under `modifiers`). */
uint32_t cc = ts_node_child_count(func_node);
for (uint32_t ci = 0; ci < cc; ci++) {
TSNode child = ts_node_child(func_node, ci);
if (cbm_kind_in_set(child, spec->decorator_node_types) &&
try_route_from_annotation(a, child, source, out_path, out_method)) {
return true;
wrappers[wn++] = owner;
for (int w = 0; w < wn; w++) {
uint32_t cc = ts_node_child_count(wrappers[w]);
for (uint32_t ci = 0; ci < cc; ci++) {
TSNode child = ts_node_child(wrappers[w], ci);
if (!cbm_kind_in_set(child, spec->decorator_node_types)) {
continue;
}
TSNode name_node = annotation_name_node(child);
if (ts_node_is_null(name_node)) {
continue;
}
char *name = cbm_node_text(a, name_node, source);
if (!name) {
continue;
}
if (!*out_jax_path && strcmp(name, "Path") == 0) {
TSNode args = annotation_args_node(child);
if (!ts_node_is_null(args)) {
*out_jax_path = extract_route_path_from_args(a, args, source);
}
continue;
}
if (!*out_method) {
const char *method = annotation_route_method(name);
if (method) {
*out_method = method;
TSNode args = annotation_args_node(child);
if (!ts_node_is_null(args)) {
*out_map_path = extract_route_path_from_args(a, args, source);
}
}
}
}
}
return false;
}
static bool extract_route_from_annotations(CBMArena *a, TSNode func_node, const char *source,
const CBMLangSpec *spec, const char **out_path,
const char **out_method) {
const char *map_path = NULL;
const char *method = NULL;
const char *jax_path = NULL;
scan_route_annotations(a, func_node, source, spec, &map_path, &method, &jax_path);
/* Method-level routes still require a verb/mapping annotation; a lone
* @Path (JAX-RS sub-resource locator) is not an endpoint by itself. */
if (!method) {
return false;
}
*out_method = method;
*out_path = map_path ? map_path : (jax_path ? jax_path : "/");
return true;
}
static void extract_route_from_decorators(CBMArena *a, TSNode func_node, const char *source,
@@ -1723,10 +1774,18 @@ static const char *join_route_paths(CBMArena *a, const char *prefix, const char
static const char *spring_class_route_prefix(CBMArena *a, TSNode class_node, const char *source,
const CBMLangSpec *spec) {
const char *prefix = NULL;
const char *map_path = NULL;
const char *method = NULL;
if (extract_route_from_annotations(a, class_node, source, spec, &prefix, &method)) {
return prefix;
const char *jax_path = NULL;
scan_route_annotations(a, class_node, source, spec, &map_path, &method, &jax_path);
if (map_path) {
return map_path; /* @RequestMapping("/api") and friends */
}
if (jax_path) {
return jax_path; /* JAX-RS class-level @Path("/api") carries no verb */
}
if (method) {
return "/"; /* mapping annotation without a path argument */
}
return NULL;
}
@@ -5901,8 +5960,8 @@ static void extract_class_fields(CBMExtractCtx *ctx, TSNode class_node, const ch
* For the nested case, the child has no "type" field directly. Detect by
* walking named children for a variable_declaration. */
TSNode type_node = ts_node_child_by_field_name(child, TS_FIELD("type"));
TSNode name_node =
ts_node_is_null(type_node) ? (TSNode){0} : resolve_field_name_node(child);
TSNode empty_node = {0};
TSNode name_node = ts_node_is_null(type_node) ? empty_node : resolve_field_name_node(child);
if (ts_node_is_null(type_node)) {
uint32_t cnc = ts_node_named_child_count(child);
+17 -4
View File
@@ -1793,10 +1793,23 @@ static bool db_internal_project_name(const char *full_path, char *name_out, size
cbm_project_t *projs = NULL;
int n = 0;
bool ok = false;
if (cbm_store_list_projects(st, &projs, &n) == CBM_STORE_OK && n == 1 && projs[0].name &&
projs[0].name[0]) {
snprintf(name_out, name_sz, "%s", projs[0].name);
ok = true;
if (cbm_store_list_projects(st, &projs, &n) == CBM_STORE_OK) {
/* Ignore internal shadow projects ("<name>::missed" miss-graph rows):
* they share the db with the primary project and must not make it
* unresolvable — requiring n == 1 over ALL rows made every project
* with a miss graph vanish from list_projects and the UI (#1044). */
int primary = -1;
int primary_count = 0;
for (int i = 0; i < n; i++) {
if (projs[i].name && projs[i].name[0] && !strstr(projs[i].name, "::")) {
primary = i;
primary_count++;
}
}
if (primary_count == 1) {
snprintf(name_out, name_sz, "%s", projs[primary].name);
ok = true;
}
}
cbm_store_free_projects(projs, n);
if (ok && out_store) {
+159 -2
View File
@@ -426,8 +426,12 @@ static void parse_pyproject_toml(const char *source, int source_len, const char
pkg_entries_push(entries, name, entry);
/* Also register <name>/__init__ as alternative (no src/ prefix) */
snprintf(suffix, sizeof(suffix), "%s/__init__", name_copy);
char *alt_entry = build_entry_path(rel_path, suffix);
char *alt_entry = NULL;
if (name_copy) {
snprintf(suffix, sizeof(suffix), "%s/__init__", name_copy);
alt_entry = build_entry_path(rel_path, suffix);
}
if (name_copy && alt_entry) {
pkg_entries_push(entries, name_copy, alt_entry);
} else {
@@ -1265,6 +1269,151 @@ static bool import_targetable_label(const char *label) {
return false;
}
static const char *path_leaf(const char *path) {
const char *leaf = path;
for (const char *p = path; p && *p; p++) {
if (*p == '/' || *p == '\\') {
leaf = p + 1;
}
}
return leaf;
}
static bool is_header_include(const char *path) {
if (!path || !path[0]) {
return false;
}
static const char *header_exts[] = {".h", ".hh", ".hpp", ".hxx", ".inc", ".inl", ".ipp", NULL};
for (const char **ext = header_exts; *ext; ext++) {
size_t path_len = strlen(path);
size_t ext_len = strlen(*ext);
if (path_len > ext_len && strcmp(path + path_len - ext_len, *ext) == 0) {
return true;
}
}
return false;
}
static bool is_c_family_source(const char *source_rel) {
if (!source_rel || !source_rel[0]) {
return false;
}
static const char *exts[] = {".c", ".cc", ".cpp", ".cxx", ".c++",
".h", ".hh", ".hpp", ".hxx", NULL};
for (const char **ext = exts; *ext; ext++) {
size_t path_len = strlen(source_rel);
size_t ext_len = strlen(*ext);
if (path_len > ext_len && strcmp(source_rel + path_len - ext_len, *ext) == 0) {
return true;
}
}
return false;
}
static const cbm_gbuf_node_t *resolve_exact_file_node(const cbm_pipeline_ctx_t *ctx,
const char *file_path,
const char *source_file_qn) {
if (!ctx || !file_path || !file_path[0]) {
return NULL;
}
const char *leaf = path_leaf(file_path);
if (!leaf || !leaf[0]) {
return NULL;
}
char stem[PKGMAP_PATH_BUF];
snprintf(stem, sizeof(stem), "%s", leaf);
char *last_dot = strrchr(stem, '.');
if (last_dot && last_dot != stem) {
*last_dot = '\0';
}
const char *names[2] = {stem[0] ? stem : NULL, leaf};
const cbm_gbuf_node_t *best = NULL;
for (int ni = 0; ni < 2; ni++) {
const char *name = names[ni];
if (!name || !name[0]) {
continue;
}
const cbm_gbuf_node_t **hits = NULL;
int hit_count = 0;
if (cbm_gbuf_find_by_name(ctx->gbuf, name, &hits, &hit_count) != 0 || !hits) {
continue;
}
for (int i = 0; i < hit_count; i++) {
const cbm_gbuf_node_t *cand = hits[i];
if (!cand || !cand->file_path) {
continue;
}
/* Tie-breaker: Ensure the candidate's actual file_path ends with the
* specific include path requested (e.g., 'a/util.h' instead of just 'util.h'). */
if (!ends_with(cand->file_path, file_path)) {
continue;
}
if (!cand->label || !import_targetable_label(cand->label)) {
continue;
}
if (source_file_qn && cand->qualified_name &&
strcmp(cand->qualified_name, source_file_qn) == 0) {
continue;
}
if (strcmp(cand->label, "File") == 0) {
return cand;
}
if (!best) {
best = cand;
}
}
if (best) {
return best;
}
}
return NULL;
}
static const cbm_gbuf_node_t *resolve_header_include(const cbm_pipeline_ctx_t *ctx,
const char *source_rel,
const char *source_file_qn,
const char *module_path) {
if (!is_c_family_source(source_rel) || !is_header_include(module_path)) {
return NULL;
}
const char *base = module_path;
if (base[0] == '.' && base[1] == '/') {
base += 2;
}
const cbm_gbuf_node_t *exact = resolve_exact_file_node(ctx, base, source_file_qn);
if (exact) {
return exact;
}
if (source_rel && source_rel[0]) {
char *dir = path_dirname(source_rel);
if (dir) {
char candidate[PKGMAP_PATH_BUF];
if (dir[0]) {
snprintf(candidate, sizeof(candidate), "%s/%s", dir, base);
} else {
snprintf(candidate, sizeof(candidate), "%s", base);
}
free(dir);
exact = resolve_exact_file_node(ctx, candidate, source_file_qn);
if (exact) {
return exact;
}
}
}
return NULL;
}
/* Resolve a sibling-file import: a bare path/name (no leading "./") that names
* a file relative to the importer's directory. This covers build/markup
* grammars whose import string is a sibling filename or directory rather than a
@@ -1352,6 +1501,14 @@ const cbm_gbuf_node_t *cbm_pipeline_resolve_import_node(const cbm_pipeline_ctx_t
return NULL;
}
/* Prefer exact header-file nodes for C/C++ includes so same-stem source or
* module nodes do not steal the edge target. */
const cbm_gbuf_node_t *header_target =
resolve_header_include(ctx, source_rel, source_file_qn, imp->module_path);
if (header_target) {
return header_target;
}
/* Strategy 1: module-path resolution → existing node (Python/TS/Go).
* No label filter here: directory-module languages (Go/Java packages)
* legitimately resolve straight to a Folder node -- that's the intended,
+11 -2
View File
@@ -885,8 +885,17 @@ int cbm_pipeline_run_incremental(cbm_pipeline_t *p, const char *db_path, cbm_fil
for (int i = 0; i < ci; i++) {
char *file_qn = cbm_pipeline_fqn_compute(project, changed_files[i].rel_path, "__file__");
if (file_qn) {
cbm_gbuf_upsert_node(existing, "File", changed_files[i].rel_path, file_qn,
changed_files[i].rel_path, 0, 0, "{}");
/* #994: the name must be the BASENAME with extension props,
* mirroring the full build's File node (pipeline.c) — upserts
* match by QN, so any other name here renames the node in place
* and the incremental graph diverges from a full build. */
const char *rel = changed_files[i].rel_path;
const char *slash = strrchr(rel, '/');
const char *basename = slash ? slash + SKIP_ONE : rel;
char props[CBM_SZ_256];
const char *ext = strrchr(basename, '.');
snprintf(props, sizeof(props), "{\"extension\":\"%s\"}", ext ? ext : "");
cbm_gbuf_upsert_node(existing, "File", basename, file_qn, rel, 0, 0, props);
free(file_qn);
}
}
+5
View File
@@ -0,0 +1,5 @@
#include "NodeController.h"
int node_controller_value() {
return 1;
}
+5
View File
@@ -0,0 +1,5 @@
#pragma once
struct NodeController {
int value;
};
+5
View File
@@ -0,0 +1,5 @@
#include "SystemController.h"
int system_controller_value() {
return 2;
}
+5
View File
@@ -0,0 +1,5 @@
#pragma once
struct SystemController {
int value;
};
+6
View File
@@ -0,0 +1,6 @@
#include "NodeController.h"
#include <SystemController.h>
int main() {
return 0;
}
+150
View File
@@ -72,6 +72,10 @@ typedef struct {
const char *content;
} EILangFile;
typedef struct {
const char *name; /* fixture filename relative to a checked-in fixture root */
} EILangFixtureFile;
static void ei_to_fwd_slashes(char *p) {
for (; *p; p++) {
if (*p == '\\') *p = '/';
@@ -123,6 +127,89 @@ static cbm_store_t *ei_index_files(EILangProj *lp, const EILangFile *files, int
return cbm_store_open_path(lp->dbpath);
}
static char *ei_slurp_file(const char *path) {
FILE *f = fopen(path, "rb");
if (!f) {
return NULL;
}
if (fseek(f, 0, SEEK_END) != 0) {
fclose(f);
return NULL;
}
long size = ftell(f);
if (size < 0) {
fclose(f);
return NULL;
}
if (fseek(f, 0, SEEK_SET) != 0) {
fclose(f);
return NULL;
}
char *buf = (char *)calloc((size_t)size + 1, 1);
if (!buf) {
fclose(f);
return NULL;
}
size_t nread = fread(buf, 1, (size_t)size, f);
fclose(f);
buf[nread] = '\0';
return buf;
}
static cbm_store_t *ei_index_fixture_files(EILangProj *lp, const char *fixture_root,
const EILangFixtureFile *files, int nfiles) {
EILangFile *loaded = (EILangFile *)calloc((size_t)nfiles, sizeof(EILangFile));
char **contents = (char **)calloc((size_t)nfiles, sizeof(char *));
if (!loaded || !contents) {
free(loaded);
free(contents);
return NULL;
}
cbm_store_t *store = NULL;
for (int i = 0; i < nfiles; i++) {
char path[512];
snprintf(path, sizeof(path), "%s/%s", fixture_root, files[i].name);
contents[i] = ei_slurp_file(path);
if (!contents[i]) {
goto done;
}
loaded[i].name = files[i].name;
loaded[i].content = contents[i];
}
store = ei_index_files(lp, loaded, nfiles);
done:
for (int i = 0; i < nfiles; i++) {
free(contents[i]);
}
free(contents);
free(loaded);
return store;
}
static int64_t ei_node_id_for_file_label(cbm_store_t *store, const char *project,
const char *file_path, const char *label) {
cbm_node_t *nodes = NULL;
int count = 0;
if (cbm_store_find_nodes_by_file(store, project, file_path, &nodes, &count) != CBM_STORE_OK) {
return 0;
}
int64_t id = 0;
for (int i = 0; i < count; i++) {
if (nodes[i].label && strcmp(nodes[i].label, label) == 0) {
id = nodes[i].id;
break;
}
}
if (id == 0 && count > 0) {
id = nodes[0].id;
}
cbm_store_free_nodes(nodes, count);
return id;
}
static void ei_cleanup(EILangProj *lp, cbm_store_t *store) {
if (store) cbm_store_close(store);
if (lp->srv) { cbm_mcp_server_free(lp->srv); lp->srv = NULL; }
@@ -409,6 +496,68 @@ TEST(ei_go_two_consumers_same_package) {
PASS();
}
/* C++: header include should resolve to the header file node, not the same-stem
* source node. Also exercises angle-bracket include resolution. */
TEST(ei_cpp_header_include_targets_header_file) {
static const EILangFixtureFile fixture_files[] = {
{"main.cpp"},
{"NodeController.h"},
{"NodeController.cpp"},
{"SystemController.h"},
{"SystemController.cpp"},
};
EILangProj lp;
cbm_store_t *store = ei_index_fixture_files(&lp, "tests/fixtures/cpp_include",
fixture_files,
(int)(sizeof(fixture_files) / sizeof(fixture_files[0])));
ASSERT_NOT_NULL(store);
int64_t main_id = ei_node_id_for_file_label(store, lp.project, "main.cpp", "File");
int64_t node_source_id = ei_node_id_for_file_label(store, lp.project, "NodeController.cpp", "File");
int64_t system_source_id = ei_node_id_for_file_label(store, lp.project, "SystemController.cpp", "File");
ASSERT_GT(main_id, 0);
ASSERT_GT(node_source_id, 0);
ASSERT_GT(system_source_id, 0);
cbm_edge_t *edges = NULL;
int edge_count = 0;
int rc = cbm_store_find_edges_by_source_type(store, main_id, "IMPORTS", &edges, &edge_count);
ASSERT_EQ(rc, CBM_STORE_OK);
ASSERT_TRUE(edge_count >= 2);
bool saw_node_header = false;
bool saw_system_header = false;
for (int i = 0; i < edge_count; i++) {
cbm_node_t *target = (cbm_node_t *)calloc(1, sizeof(cbm_node_t));
/* Pass target directly (no &) because it is already a pointer */
ASSERT_EQ(cbm_store_find_node_by_id(store, edges[i].target_id, target), CBM_STORE_OK);
ASSERT_EQ(edges[i].source_id, main_id);
ASSERT_NEQ(edges[i].target_id, node_source_id);
ASSERT_NEQ(edges[i].target_id, system_source_id);
/* Use -> instead of . to access fields on a pointer */
if (target->file_path && strcmp(target->file_path, "NodeController.h") == 0) {
saw_node_header = true;
}
if (target->file_path && strcmp(target->file_path, "SystemController.h") == 0) {
saw_system_header = true;
}
/* Free the node inside the loop */
cbm_store_free_nodes(target, 1);
}
cbm_store_free_edges(edges, edge_count);
ASSERT_TRUE(saw_node_header);
ASSERT_TRUE(saw_system_header);
ei_cleanup(&lp, store);
PASS();
}
/* ═══════════════════════════════════════════════════════════════════════════
* RED REPRODUCTION — Rust
*
@@ -868,6 +1017,7 @@ SUITE(edge_imports) {
RUN_TEST(ei_go_subpackage_import);
RUN_TEST(ei_go_blank_import);
RUN_TEST(ei_go_two_consumers_same_package);
RUN_TEST(ei_cpp_header_include_targets_header_file);
/* ── RED REPRODUCTIONS — Rust (expected to FAIL until pipeline fixed) ── */
RUN_TEST(ei_rust_mod_plus_use);
+27 -1
View File
@@ -381,6 +381,31 @@ TEST(handles_spring_kotlin) {
PASS();
}
/* JAX-RS (Java): the verb (@GET) and the path (@Path) are sibling
* annotations, and the class-level @Path must prefix method-level paths.
* Reproduce-first (#1005): the first-mapping-annotation scan dropped every
* method-level @Path, collapsing all methods onto a single "/" Route node;
* only the exact Route-name set catches that through the emission dedup. */
TEST(handles_jaxrs_java) {
static const char *routes[] = {"/api/v1/widgets", "/api/v1/widgets/count", NULL};
static const EtFile f[] = {
{"WidgetResource.java",
"package com.example;\n\n"
"import jakarta.ws.rs.GET;\n"
"import jakarta.ws.rs.Path;\n\n"
"@Path(\"/api/v1/widgets\")\npublic class WidgetResource {\n"
" @GET\n"
" public String list() {\n"
" return \"widgets\";\n }\n\n"
" @GET\n"
" @Path(\"/count\")\n"
" public String count() {\n"
" return \"42\";\n }\n}\n"}};
ASSERT_TRUE(et_edge_present(f, 1, "HANDLES", 2));
ASSERT_TRUE(et_routes_exact(f, 1, routes));
PASS();
}
/* ASP.NET Minimal API (C#) — route registration via static MapGet/MapPost calls
* with identifier handlers, under a Microsoft/AspNetCore path so the resolved
* callee QN carries the "MapGet"/"Microsoft.AspNetCore" route-reg substrings.
@@ -1488,7 +1513,7 @@ TEST(override_go_interface) {
* ══════════════════════════════════════════════════════════════════ */
SUITE(edge_types_probe) {
/* HANDLES — route→handler across web frameworks (8 frameworks) */
/* HANDLES — route→handler across web frameworks */
RUN_TEST(handles_flask_python);
RUN_TEST(handles_fastapi_python);
RUN_TEST(handles_drf_action_python);
@@ -1497,6 +1522,7 @@ SUITE(edge_types_probe) {
RUN_TEST(handles_gin_go);
RUN_TEST(handles_spring_java);
RUN_TEST(handles_spring_kotlin);
RUN_TEST(handles_jaxrs_java);
RUN_TEST(handles_aspnet_csharp);
RUN_TEST(handles_laravel_php);
RUN_TEST(handles_laravel_facade_routes_issue952);
+33
View File
@@ -2738,6 +2738,38 @@ TEST(extract_java_method_annotations_issue382) {
PASS();
}
/* Issue #1005: JAX-RS splits a route across two annotations (@GET carries the
* verb, a sibling @Path carries the path). Returning on the first mapping
* annotation dropped every method-level @Path, and the class-level @Path
* prefix was never recognized at all. */
TEST(extract_java_jaxrs_path_composition_issue1005) {
CBMFileResult *r = extract("import jakarta.ws.rs.GET;\n"
"import jakarta.ws.rs.Path;\n"
"@Path(\"/api/v1/widgets\")\n"
"public class WidgetResource {\n"
" @GET\n"
" public String list() { return \"\"; }\n"
" @GET\n"
" @Path(\"/count\")\n"
" public String count() { return \"\"; }\n"
"}\n",
CBM_LANG_JAVA, "t", "WidgetResource.java");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
const CBMDefinition *list = find_def_by_name(r, "list");
ASSERT_NOT_NULL(list);
ASSERT_NOT_NULL(list->route_path);
ASSERT_STR_EQ(list->route_path, "/api/v1/widgets");
ASSERT_STR_EQ(list->route_method, "GET");
const CBMDefinition *count = find_def_by_name(r, "count");
ASSERT_NOT_NULL(count);
ASSERT_NOT_NULL(count->route_path);
ASSERT_STR_EQ(count->route_path, "/api/v1/widgets/count");
ASSERT_STR_EQ(count->route_method, "GET");
cbm_free_result(r);
PASS();
}
/* Find an in-body call by its raw callee text; returns the call or NULL. */
static const CBMCall *find_call_by_callee(CBMFileResult *r, const char *callee) {
for (int i = 0; i < r->calls.count; i++) {
@@ -4625,6 +4657,7 @@ SUITE(extraction) {
RUN_TEST(js_index_module_qn_not_collide_with_folder);
RUN_TEST(python_regular_module_qn_unchanged);
RUN_TEST(extract_java_method_annotations_issue382);
RUN_TEST(extract_java_jaxrs_path_composition_issue1005);
RUN_TEST(extract_java_no_double_class_qn);
RUN_TEST(extract_go_no_filename_in_module_qn);
RUN_TEST(extract_large_ts_has_functions_issue213);
+31
View File
@@ -964,6 +964,36 @@ static int resp_lacks_key(const char *resp, const char *key) {
/* ── list_projects ─────────────────────────────────────────────── */
TEST(incr_file_node_name_stays_basename) {
/* #994: File nodes must keep their basename NAME through an incremental
* re-index (the bug renamed touched files' nodes to rel_path, diverging
* from a full build). Modify a file, re-index, assert no File node name
* contains a path separator. */
char path[512];
snprintf(path, sizeof(path), "%s/fastapi/applications.py", g_repodir);
FILE *f = fopen(path, "a");
ASSERT(f != NULL);
fprintf(f, "\n# incr_file_node_name_stays_basename touch\n");
fclose(f);
char *resp = index_repo();
ASSERT(resp != NULL);
free(resp);
double ms;
char *r = call_tool_timed("query_graph", &ms,
"{\"project\":\"%s\",\"format\":\"json\","
"\"query\":\"MATCH (n:File) WHERE n.name CONTAINS '/'"
" RETURN n.name LIMIT 5\"}",
g_project);
TOOL_OK(r, ms);
/* No File node may carry a path separator in its name; the query engine
* returns an empty rows array when nothing matches. */
ASSERT(strstr(r, "\"rows\":[]") != NULL || strstr(r, "\\\"rows\\\":[]") != NULL);
free(r);
PASS();
}
TEST(tool_list_projects_basic) {
double ms;
char *r = call_tool_timed("list_projects", &ms, "{}");
@@ -2970,6 +3000,7 @@ SUITE(incremental) {
/* Phase 3: Incremental deltas */
RUN_TEST(incr_modify_file);
RUN_TEST(incr_file_node_name_stays_basename);
RUN_TEST(incr_formatter_run);
RUN_TEST(incr_add_file);
RUN_TEST(incr_delete_file);
+76
View File
@@ -4780,6 +4780,81 @@ TEST(tool_resolve_store_by_internal_name_issue704) {
PASS();
}
/* ── #1044: a "<name>::missed" shadow row must not hide the project ──
*
* The miss-graph pass inserts a second `projects` row ("<name>::missed") so
* its nodes satisfy the FK on nodes.project. db_internal_project_name
* required the projects table to hold EXACTLY ONE row, so any project with
* a miss graph vanished from list_projects and the graph UI, and the
* fallback-scan resolve path failed.
*
* RED on buggy code / GREEN on the fix:
* A. list_projects still advertises "delta1044" while the shadow row exists.
* B. the shadow name itself is never advertised.
* C. search_graph(project="delta1044") still resolves and returns the node.
*/
TEST(tool_list_projects_ignores_missed_shadow_issue1044) {
char cache[256];
snprintf(cache, sizeof(cache), "/tmp/cbm-issue1044-XXXXXX");
if (!cbm_mkdtemp(cache)) {
PASS(); /* skip if mkdtemp fails — not a #1044 signal */
}
const char *saved = getenv("CBM_CACHE_DIR");
char *saved_copy = saved ? strdup(saved) : NULL;
cbm_setenv("CBM_CACHE_DIR", cache, 1);
ASSERT_TRUE(issue704_make_db(cache, "delta1044.db", "delta1044", "deltaFunc1044"));
/* Add the shadow row exactly the way the miss-graph pass does. */
char db_path[700];
snprintf(db_path, sizeof(db_path), "%s/delta1044.db", cache);
cbm_store_t *st = cbm_store_open_path(db_path);
ASSERT_NOT_NULL(st);
ASSERT_EQ(cbm_store_upsert_project(st, "delta1044::missed", ""), CBM_STORE_OK);
cbm_store_close(st);
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
ASSERT_NOT_NULL(srv);
/* ── A + B: primary advertised, shadow hidden ─────────────────── */
char *list =
cbm_mcp_server_handle(srv, "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\","
"\"params\":{\"name\":\"list_projects\",\"arguments\":{}}}");
ASSERT_NOT_NULL(list);
ASSERT_NOT_NULL(strstr(list, "delta1044")); /* RED before: db skipped as ghost */
ASSERT_NULL(strstr(list, "::missed")); /* shadow never advertised */
free(list);
/* ── C: the project still resolves and returns its node ───────── */
char *q = cbm_mcp_server_handle(
srv, "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\","
"\"params\":{\"name\":\"search_graph\",\"arguments\":{"
"\"project\":\"delta1044\",\"name_pattern\":\"deltaFunc1044\",\"limit\":5}}}");
ASSERT_NOT_NULL(q);
ASSERT_NOT_NULL(strstr(q, "deltaFunc1044"));
ASSERT_NULL(strstr(q, "not found"));
free(q);
cbm_mcp_server_free(srv);
/* ── cleanup ───────────────────────────────────────────────────── */
if (saved_copy) {
cbm_setenv("CBM_CACHE_DIR", saved_copy, 1);
free(saved_copy);
} else {
cbm_unsetenv("CBM_CACHE_DIR");
}
cbm_unlink(db_path);
char side1044[740];
snprintf(side1044, sizeof(side1044), "%s-wal", db_path);
cbm_unlink(side1044);
snprintf(side1044, sizeof(side1044), "%s-shm", db_path);
cbm_unlink(side1044);
cbm_rmdir(cache);
PASS();
}
/* ══════════════════════════════════════════════════════════════════
* QUERY STORE READ-ONLY (data-integrity reproductions)
*
@@ -6439,6 +6514,7 @@ SUITE(mcp) {
RUN_TEST(tool_bad_project_name_no_overflow_issue235);
RUN_TEST(tool_bad_project_error_valid_json_issue235);
RUN_TEST(tool_resolve_store_by_internal_name_issue704);
RUN_TEST(tool_list_projects_ignores_missed_shadow_issue1044);
/* auto_watch gate (distilled from PR #625) */
RUN_TEST(mcp_auto_watch_default_registers_watcher_on_connect);