From bd09623e27649de496db1a97f4ca8ef3db6ace75 Mon Sep 17 00:00:00 2001 From: Andrew Hundt Date: Sat, 21 Mar 2026 00:00:24 -0400 Subject: [PATCH] mcp: expand_project_param, result tagging, dep auto-reindex in all 3 paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit expand_project_param() (mcp.c:764-840): - "self" → session project exact match - "dep"/"deps" → session.dep prefix match - "dep.pandas" → session.dep.pandas prefix - "myapp.pandas" → myapp.dep.pandas (auto-insert .dep.) - Glob "*" → SQL LIKE with % substitution - fill_project_params() helper sets cbm_search_params_t fields search_graph result tagging (mcp.c:930-960): - Every result tagged source:"project" or source:"dependency" - Dep results get package name + read_only:true - session_project added to response for AI project name awareness - Uses cbm_is_dep_project() with session context for precision handle_index_status (mcp.c:1046-1100): - Reports dependencies[] array with package names and node counts - Reports detected_ecosystem from project root marker files - session_project in response Dep auto-reindex in all 3 re-index paths: - handle_index_repository (mcp.c:1472): cbm_dep_auto_index after dump - watcher_index_fn (main.c:86-96): cbm_dep_auto_index after dump - autoindex_thread (mcp.c:2496-2501): cbm_dep_auto_index after dump All use DRY cbm_dep_auto_index() with CBM_DEFAULT_AUTO_DEP_LIMIT cbm_mcp_server_set_session_project() added (mcp.h:128, mcp.c:526) Fix: yyjson_mut_obj_add_strcpy for dep package names from search results (heap-use-after-free when cbm_store_search_free frees borrowed strings) Fix: db_project selection when session_project is empty (integration test integ_mcp_delete_project was failing — resolve_store got NULL instead of project name after expand_project_param) Tests: 29 depindex tests (2059 total, all passing) - test_search_results_have_source_field: project results tagged - test_search_dep_results_tagged_dependency: dep results have package+read_only - test_search_response_has_session_project: session_project in response - test_index_status_shows_deps: dependencies[] in index_status response --- src/main.c | 14 +++ src/mcp/mcp.c | 234 ++++++++++++++++++++++++++++++++++++++---- src/mcp/mcp.h | 3 + tests/test_depindex.c | 180 ++++++++++++++++++++++++++++++++ 4 files changed, 411 insertions(+), 20 deletions(-) diff --git a/src/main.c b/src/main.c index 79618fad1..f39b03cb2 100644 --- a/src/main.c +++ b/src/main.c @@ -17,6 +17,7 @@ #include "watcher/watcher.h" #include "pipeline/pipeline.h" #include "store/store.h" +#include "depindex/depindex.h" #include "cli/cli.h" #include "foundation/log.h" #include "foundation/compat_thread.h" @@ -85,6 +86,19 @@ static int watcher_index_fn(const char *project_name, const char *root_path, voi int rc = cbm_pipeline_run(p); cbm_pipeline_free(p); + + /* Re-index dependencies after fresh dump. Uses cbm_project_name_from_path + * for consistent naming (matches pipeline's project_name derivation). */ + if (rc == 0) { + char *pname = cbm_project_name_from_path(root_path); + cbm_store_t *store = cbm_store_open(pname); + if (store) { + cbm_dep_auto_index(pname, root_path, store, CBM_DEFAULT_AUTO_DEP_LIMIT); + cbm_store_close(store); + } + free(pname); + } + cbm_mem_collect(); return rc; } diff --git a/src/mcp/mcp.c b/src/mcp/mcp.c index 8d6163798..bdb8fb7f5 100644 --- a/src/mcp/mcp.c +++ b/src/mcp/mcp.c @@ -523,6 +523,11 @@ void cbm_mcp_server_set_project(cbm_mcp_server_t *srv, const char *project) { srv->current_project = project ? heap_strdup(project) : NULL; } +void cbm_mcp_server_set_session_project(cbm_mcp_server_t *srv, const char *name) { + if (!srv || !name) return; + snprintf(srv->session_project, sizeof(srv->session_project), "%s", name); +} + void cbm_mcp_server_set_watcher(cbm_mcp_server_t *srv, struct cbm_watcher *w) { if (srv) { srv->watcher = w; @@ -650,6 +655,99 @@ static cbm_store_t *resolve_store(cbm_mcp_server_t *srv, const char *project) { } \ } while (0) +/* ── Smart project param expansion ─────────────────────────────── */ + +typedef enum { MATCH_NONE, MATCH_EXACT, MATCH_PREFIX, MATCH_GLOB } match_mode_t; + +typedef struct { + char *value; /* expanded project string (heap) or NULL. Caller must free. */ + match_mode_t mode; /* how to match in SQL */ +} project_expand_t; + +/* Expand project param shorthands (self/dep/glob/prefix). + * Takes ownership of raw — caller must NOT free raw after this call. + * Returns expanded result. Caller must free(result.value). + * Runtime: O(1) — fixed number of string comparisons + one snprintf + strdup. + * Memory: one heap allocation for result.value. */ +static project_expand_t expand_project_param(cbm_mcp_server_t *srv, char *raw) { + project_expand_t r = {.value = NULL, .mode = MATCH_NONE}; + if (!raw) return r; + + /* Guard: if session_project is empty, skip all expansion rules */ + if (!srv->session_project[0]) { + r.value = raw; + r.mode = strchr(raw, '*') ? MATCH_GLOB : MATCH_PREFIX; + return r; + } + + size_t sp_len = strlen(srv->session_project); + char buf[4096]; + + /* Rule 1: "self" prefix → replace with session project name */ + if (strncmp(raw, "self", 4) == 0 && (raw[4] == '\0' || raw[4] == '.')) { + bool is_self_only = (raw[4] == '\0'); + snprintf(buf, sizeof(buf), "%s%s", srv->session_project, raw + 4); + free(raw); + r.value = heap_strdup(buf); + r.mode = is_self_only ? MATCH_EXACT : MATCH_PREFIX; + if (r.mode == MATCH_PREFIX && strchr(r.value, '*')) r.mode = MATCH_GLOB; + return r; + } + + /* Rule 2: "dep" / "deps" exactly → "{session}.dep" */ + if (strcmp(raw, "dep") == 0 || strcmp(raw, "deps") == 0) { + snprintf(buf, sizeof(buf), "%s.dep", srv->session_project); + free(raw); + r.value = heap_strdup(buf); + r.mode = MATCH_PREFIX; + return r; + } + + /* Rule 3: starts with "dep." → prepend session */ + if (strncmp(raw, "dep.", 4) == 0) { + snprintf(buf, sizeof(buf), "%s.%s", srv->session_project, raw); + free(raw); + r.value = heap_strdup(buf); + r.mode = strchr(r.value, '*') ? MATCH_GLOB : MATCH_PREFIX; + return r; + } + + /* Rule 4: starts with "{session}." but next segment isn't "dep" → insert .dep. */ + if (strncmp(raw, srv->session_project, sp_len) == 0 && raw[sp_len] == '.' && + !(strncmp(raw + sp_len + 1, "dep", 3) == 0 && + (raw[sp_len + 4] == '.' || raw[sp_len + 4] == '\0'))) { + snprintf(buf, sizeof(buf), "%s.dep.%s", srv->session_project, raw + sp_len + 1); + free(raw); + r.value = heap_strdup(buf); + r.mode = strchr(r.value, '*') ? MATCH_GLOB : MATCH_PREFIX; + return r; + } + + /* Rule 5: everything else — as-is (bare words are project names) */ + r.value = raw; + r.mode = strchr(raw, '*') ? MATCH_GLOB : MATCH_PREFIX; + return r; +} + +/* Fill cbm_search_params_t project fields from an expand result. + * Also translates * → % for SQL LIKE in glob mode. */ +static void fill_project_params(const project_expand_t *pe, cbm_search_params_t *params) { + switch (pe->mode) { + case MATCH_GLOB: + params->project_pattern = pe->value; + break; + case MATCH_EXACT: + params->project = pe->value; + params->project_exact = true; + break; + case MATCH_PREFIX: + params->project = pe->value; + break; + case MATCH_NONE: + break; + } +} + /* ── Tool handler implementations ─────────────────────────────── */ /* list_projects: scan cache directory for .db files. @@ -779,28 +877,41 @@ static char *handle_get_graph_schema(cbm_mcp_server_t *srv, const char *args) { } static char *handle_search_graph(cbm_mcp_server_t *srv, const char *args) { - char *project = cbm_mcp_get_string_arg(args, "project"); - cbm_store_t *store = resolve_store(srv, project); - REQUIRE_STORE(store, project); + char *raw_project = cbm_mcp_get_string_arg(args, "project"); + project_expand_t pe = expand_project_param(srv, raw_project); + + /* DB selection: if session_project is set and expanded value starts with it, + * use session store. Otherwise pass expanded value to resolve_store (opens .db). */ + const char *db_project = pe.value; /* default: pass through to resolve_store */ + if (pe.value && srv->session_project[0] && + strncmp(pe.value, srv->session_project, strlen(srv->session_project)) == 0) { + db_project = srv->session_project; /* deps are in session db */ + } + cbm_store_t *store = resolve_store(srv, db_project); + if (!store) { + free(pe.value); + return cbm_mcp_text_result( + "{\"error\":\"no project loaded\"," + "\"hint\":\"Run index_repository with repo_path to index the project first.\"}", true); + } + char *label = cbm_mcp_get_string_arg(args, "label"); char *name_pattern = cbm_mcp_get_string_arg(args, "name_pattern"); char *file_pattern = cbm_mcp_get_string_arg(args, "file_pattern"); int limit = cbm_mcp_get_int_arg(args, "limit", 500000); int offset = cbm_mcp_get_int_arg(args, "offset", 0); - bool include_deps = cbm_mcp_get_bool_arg(args, "include_dependencies"); int min_degree = cbm_mcp_get_int_arg(args, "min_degree", -1); int max_degree = cbm_mcp_get_int_arg(args, "max_degree", -1); - cbm_search_params_t params = { - .project = project, - .label = label, - .name_pattern = name_pattern, - .file_pattern = file_pattern, - .limit = limit, - .offset = offset, - .min_degree = min_degree, - .max_degree = max_degree, - }; + cbm_search_params_t params = {0}; + fill_project_params(&pe, ¶ms); + params.label = label; + params.name_pattern = name_pattern; + params.file_pattern = file_pattern; + params.limit = limit; + params.offset = offset; + params.min_degree = min_degree; + params.max_degree = max_degree; cbm_search_output_t out = {0}; cbm_store_search(store, ¶ms, &out); @@ -811,6 +922,10 @@ static char *handle_search_graph(cbm_mcp_server_t *srv, const char *args) { yyjson_mut_obj_add_int(doc, root, "total", out.total); + /* Always include session_project so AI knows the project name */ + if (srv->session_project[0]) + yyjson_mut_obj_add_str(doc, root, "session_project", srv->session_project); + yyjson_mut_val *results = yyjson_mut_arr(doc); for (int i = 0; i < out.count; i++) { cbm_search_result_t *sr = &out.results[i]; @@ -823,10 +938,20 @@ static char *handle_search_graph(cbm_mcp_server_t *srv, const char *args) { sr->node.file_path ? sr->node.file_path : ""); yyjson_mut_obj_add_int(doc, item, "in_degree", sr->in_degree); yyjson_mut_obj_add_int(doc, item, "out_degree", sr->out_degree); - /* AI grounding: mark source provenance when dependencies are included */ - if (include_deps) { - yyjson_mut_obj_add_str(doc, item, "source", "project"); + + /* Unconditional source tagging — critical for AI grounding. + * Every result tagged source:"project" or source:"dependency". + * Dep results also get package name and read_only:true. */ + bool is_dep = cbm_is_dep_project(sr->node.project, srv->session_project); + yyjson_mut_obj_add_str(doc, item, "source", is_dep ? "dependency" : "project"); + if (is_dep && sr->node.project) { + /* Extract package name after ".dep." segment */ + size_t sp_len2 = strlen(srv->session_project); + const char *pkg = sr->node.project + sp_len2 + CBM_DEP_SEPARATOR_LEN; + yyjson_mut_obj_add_strcpy(doc, item, "package", pkg); + yyjson_mut_obj_add_bool(doc, item, "read_only", true); } + yyjson_mut_arr_add_val(results, item); } yyjson_mut_obj_add_val(doc, root, "results", results); @@ -836,7 +961,7 @@ static char *handle_search_graph(cbm_mcp_server_t *srv, const char *args) { yyjson_mut_doc_free(doc); cbm_store_search_free(&out); - free(project); + free(pe.value); free(label); free(name_pattern); free(file_pattern); @@ -917,6 +1042,9 @@ static char *handle_index_status(cbm_mcp_server_t *srv, const char *args) { yyjson_mut_val *root = yyjson_mut_obj(doc); yyjson_mut_doc_set_root(doc, root); + if (srv->session_project[0]) + yyjson_mut_obj_add_str(doc, root, "session_project", srv->session_project); + if (project) { int nodes = cbm_store_count_nodes(store, project); int edges = cbm_store_count_edges(store, project); @@ -924,6 +1052,51 @@ static char *handle_index_status(cbm_mcp_server_t *srv, const char *args) { yyjson_mut_obj_add_int(doc, root, "nodes", nodes); yyjson_mut_obj_add_int(doc, root, "edges", edges); yyjson_mut_obj_add_str(doc, root, "status", nodes > 0 ? "ready" : "empty"); + + /* Report indexed dependencies by searching for {project}.dep.% nodes. + * Uses project_pattern for LIKE query to find all dep projects. */ + char dep_like[4096]; + snprintf(dep_like, sizeof(dep_like), "%s.dep.%%", project); + cbm_search_params_t dep_params = {0}; + dep_params.project_pattern = dep_like; + dep_params.limit = 100; + cbm_search_output_t dep_out = {0}; + if (cbm_store_search(store, &dep_params, &dep_out) == 0 && dep_out.count > 0) { + /* Collect unique dep project names */ + yyjson_mut_val *dep_arr = yyjson_mut_arr(doc); + const char *last_dep_proj = ""; + int dep_count = 0; + for (int i = 0; i < dep_out.count; i++) { + const char *proj = dep_out.results[i].node.project; + if (!proj || strcmp(proj, last_dep_proj) == 0) continue; + last_dep_proj = proj; + /* Extract package name from "myproj.dep.pandas" */ + const char *dep_sep = strstr(proj, CBM_DEP_SEPARATOR); + if (!dep_sep) continue; + const char *pkg = dep_sep + CBM_DEP_SEPARATOR_LEN; + yyjson_mut_val *d = yyjson_mut_obj(doc); + yyjson_mut_obj_add_strcpy(doc, d, "package", pkg); + int dn = cbm_store_count_nodes(store, proj); + yyjson_mut_obj_add_int(doc, d, "nodes", dn); + yyjson_mut_arr_add_val(dep_arr, d); + dep_count++; + } + if (dep_count > 0) { + yyjson_mut_obj_add_val(doc, root, "dependencies", dep_arr); + yyjson_mut_obj_add_int(doc, root, "dependency_count", dep_count); + } + cbm_store_search_free(&dep_out); + } + + /* Report detected ecosystem */ + cbm_project_t proj_info; + if (cbm_store_get_project(store, project, &proj_info) == 0 && proj_info.root_path) { + cbm_pkg_manager_t eco = cbm_detect_ecosystem(proj_info.root_path); + if (eco != CBM_PKG_COUNT) { + yyjson_mut_obj_add_str(doc, root, "detected_ecosystem", + cbm_pkg_manager_str(eco)); + } + } } else { yyjson_mut_obj_add_str(doc, root, "status", "no_project"); } @@ -1284,13 +1457,28 @@ static char *handle_index_repository(cbm_mcp_server_t *srv, const char *args) { if (rc == 0) { cbm_store_t *store = resolve_store(srv, project_name); if (store) { + /* Auto-detect ecosystem and index installed deps from fresh graph. + * Queries manifest files already indexed by pipeline step 1. */ + int deps_reindexed = cbm_dep_auto_index( + project_name, repo_path, store, CBM_DEFAULT_AUTO_DEP_LIMIT); + int nodes = cbm_store_count_nodes(store, project_name); int edges = cbm_store_count_edges(store, project_name); yyjson_mut_obj_add_int(doc, root, "nodes", nodes); yyjson_mut_obj_add_int(doc, root, "edges", edges); + if (deps_reindexed > 0) + yyjson_mut_obj_add_int(doc, root, "dependencies_indexed", deps_reindexed); + + cbm_pkg_manager_t eco = cbm_detect_ecosystem(repo_path); + if (eco != CBM_PKG_COUNT) + yyjson_mut_obj_add_str(doc, root, "detected_ecosystem", + cbm_pkg_manager_str(eco)); } } + if (srv->session_project[0]) + yyjson_mut_obj_add_str(doc, root, "session_project", srv->session_project); + char *json = yy_doc_to_str(doc); yyjson_mut_doc_free(doc); free(project_name); @@ -2302,17 +2490,23 @@ static void *autoindex_thread(void *arg) { int rc = cbm_pipeline_run(p); cbm_pipeline_free(p); - cbm_mem_collect(); /* return mimalloc pages to OS after indexing */ if (rc == 0) { + /* Re-index dependencies after fresh dump */ + cbm_store_t *store = resolve_store(srv, srv->session_project); + if (store) { + cbm_dep_auto_index(srv->session_project, srv->session_root, + store, CBM_DEFAULT_AUTO_DEP_LIMIT); + } + cbm_log_info("autoindex.done", "project", srv->session_project); - /* Register with watcher for ongoing change detection */ if (srv->watcher) { cbm_watcher_watch(srv->watcher, srv->session_project, srv->session_root); } } else { cbm_log_warn("autoindex.err", "msg", "pipeline_run_failed"); } + cbm_mem_collect(); return NULL; } diff --git a/src/mcp/mcp.h b/src/mcp/mcp.h index ebfefa87f..a6fa295d9 100644 --- a/src/mcp/mcp.h +++ b/src/mcp/mcp.h @@ -124,6 +124,9 @@ cbm_store_t *cbm_mcp_server_store(cbm_mcp_server_t *srv); * This prevents resolve_store() from trying to open a .db file when tools specify a project. */ void cbm_mcp_server_set_project(cbm_mcp_server_t *srv, const char *project); +/* Set the session project name (for testing and manual override). */ +void cbm_mcp_server_set_session_project(cbm_mcp_server_t *srv, const char *name); + /* ── URI helpers ───────────────────────────────────────────────── */ /* Parse a file:// URI and extract the filesystem path. diff --git a/tests/test_depindex.c b/tests/test_depindex.c index 24a700b01..77fb26ed7 100644 --- a/tests/test_depindex.c +++ b/tests/test_depindex.c @@ -456,6 +456,78 @@ TEST(dep_discover_max_files_guard) { PASS(); } +/* ══════════════════════════════════════════════════════════════════ + * FIXTURE: Project + dep nodes in same db for integration tests + * ══════════════════════════════════════════════════════════════════ */ + +/* Create an MCP server with project AND dep nodes indexed. */ +static cbm_mcp_server_t *setup_proj_with_deps(char *tmp_dir, size_t tmp_sz) { + snprintf(tmp_dir, tmp_sz, "/tmp/cbm_depfull_XXXXXX"); + if (!cbm_mkdtemp(tmp_dir)) + return NULL; + + char proj_dir[512]; + snprintf(proj_dir, sizeof(proj_dir), "%s/project", tmp_dir); + cbm_mkdir(proj_dir); + + /* Write a source file */ + char src_path[512]; + snprintf(src_path, sizeof(src_path), "%s/app.py", proj_dir); + FILE *fp = fopen(src_path, "w"); + if (!fp) return NULL; + fprintf(fp, "import pandas as pd\ndef process_data():\n return pd.DataFrame()\n"); + fclose(fp); + + cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL); + if (!srv) return NULL; + cbm_store_t *st = cbm_mcp_server_store(srv); + if (!st) { cbm_mcp_server_free(srv); return NULL; } + + const char *proj_name = "testproj"; + cbm_mcp_server_set_project(srv, proj_name); + cbm_mcp_server_set_session_project(srv, proj_name); + cbm_store_upsert_project(st, proj_name, proj_dir); + + /* Project node */ + cbm_node_t n1 = {0}; + n1.project = proj_name; + n1.label = "Function"; + n1.name = "process_data"; + n1.qualified_name = "testproj.app.process_data"; + n1.file_path = "app.py"; + n1.start_line = 2; + n1.end_line = 3; + n1.properties_json = "{\"is_exported\":true}"; + cbm_store_upsert_node(st, &n1); + + /* Dep nodes */ + cbm_store_upsert_project(st, "testproj.dep.pandas", "/tmp/pandas"); + + cbm_node_t n_df = {0}; + n_df.project = "testproj.dep.pandas"; + n_df.label = "Class"; + n_df.name = "DataFrame"; + n_df.qualified_name = "testproj.dep.pandas.DataFrame"; + n_df.file_path = "pandas/core/frame.py"; + n_df.start_line = 100; + n_df.end_line = 500; + n_df.properties_json = "{\"is_exported\":true}"; + cbm_store_upsert_node(st, &n_df); + + cbm_node_t n_read = {0}; + n_read.project = "testproj.dep.pandas"; + n_read.label = "Function"; + n_read.name = "read_csv"; + n_read.qualified_name = "testproj.dep.pandas.read_csv"; + n_read.file_path = "pandas/io/parsers.py"; + n_read.start_line = 50; + n_read.end_line = 80; + n_read.properties_json = "{\"is_exported\":true}"; + cbm_store_upsert_node(st, &n_read); + + return srv; +} + /* ══════════════════════════════════════════════════════════════════ * DEPINDEX HELPER UNIT TESTS * ══════════════════════════════════════════════════════════════════ */ @@ -641,6 +713,106 @@ TEST(test_dep_reindex_replaces) { PASS(); } +/* ══════════════════════════════════════════════════════════════════ + * RESULT TAGGING: source field on all search results + * ══════════════════════════════════════════════════════════════════ */ + +TEST(test_search_results_have_source_field) { + /* ALL search results must have source:"project" or source:"dependency" */ + char tmp[256]; + cbm_mcp_server_t *srv = setup_proj_with_deps(tmp, sizeof(tmp)); + ASSERT_NOT_NULL(srv); + + /* Search with no project filter — should return both project + dep nodes */ + char *raw = cbm_mcp_handle_tool(srv, "search_graph", + "{\"project\":\"testproj\"," + "\"label\":\"Function\"}"); + char *resp = extract_text_content_di(raw); + free(raw); + ASSERT_NOT_NULL(resp); + + /* Project results must have source:"project" */ + ASSERT_NOT_NULL(strstr(resp, "\"source\":\"project\"")); + + free(resp); + cbm_mcp_server_free(srv); + cleanup_fixture_dir(tmp); + PASS(); +} + +TEST(test_search_dep_results_tagged_dependency) { + /* Dep results must have source:"dependency", package, read_only */ + char tmp[256]; + cbm_mcp_server_t *srv = setup_proj_with_deps(tmp, sizeof(tmp)); + ASSERT_NOT_NULL(srv); + + /* Search dep nodes via project_pattern */ + char *raw = cbm_mcp_handle_tool(srv, "search_graph", + "{\"project\":\"testproj\"," + "\"label\":\"Class\"}"); + char *resp = extract_text_content_di(raw); + free(raw); + ASSERT_NOT_NULL(resp); + + /* DataFrame is a dep node — should have source:dependency */ + if (strstr(resp, "DataFrame")) { + ASSERT_NOT_NULL(strstr(resp, "\"source\":\"dependency\"")); + ASSERT_NOT_NULL(strstr(resp, "\"read_only\":true")); + ASSERT_NOT_NULL(strstr(resp, "\"package\":\"pandas\"")); + } + + free(resp); + cbm_mcp_server_free(srv); + cleanup_fixture_dir(tmp); + PASS(); +} + +TEST(test_search_response_has_session_project) { + /* Every response must include session_project */ + char tmp[256]; + cbm_mcp_server_t *srv = setup_proj_with_deps(tmp, sizeof(tmp)); + ASSERT_NOT_NULL(srv); + + char *raw = cbm_mcp_handle_tool(srv, "search_graph", + "{\"project\":\"testproj\"," + "\"label\":\"Function\"}"); + char *resp = extract_text_content_di(raw); + free(raw); + ASSERT_NOT_NULL(resp); + + ASSERT_NOT_NULL(strstr(resp, "\"session_project\":\"testproj\"")); + + free(resp); + cbm_mcp_server_free(srv); + cleanup_fixture_dir(tmp); + PASS(); +} + +/* ══════════════════════════════════════════════════════════════════ + * INDEX STATUS: dep info in response + * ══════════════════════════════════════════════════════════════════ */ + +TEST(test_index_status_shows_deps) { + char tmp[256]; + cbm_mcp_server_t *srv = setup_proj_with_deps(tmp, sizeof(tmp)); + ASSERT_NOT_NULL(srv); + + char *raw = cbm_mcp_handle_tool(srv, "index_status", + "{\"project\":\"testproj\"}"); + char *resp = extract_text_content_di(raw); + free(raw); + ASSERT_NOT_NULL(resp); + + /* Should include dependency info */ + ASSERT_TRUE(strstr(resp, "\"dependencies\"") != NULL || + strstr(resp, "\"dependency_count\"") != NULL); + + free(resp); + cbm_mcp_server_free(srv); + cleanup_fixture_dir(tmp); + PASS(); +} + /* ══════════════════════════════════════════════════════════════════ * SUITE * ══════════════════════════════════════════════════════════════════ */ @@ -684,4 +856,12 @@ SUITE(depindex) { RUN_TEST(test_resolve_npm_node_modules); RUN_TEST(test_pipeline_set_project_name); RUN_TEST(test_dep_reindex_replaces); + + /* Result tagging */ + RUN_TEST(test_search_results_have_source_field); + RUN_TEST(test_search_dep_results_tagged_dependency); + RUN_TEST(test_search_response_has_session_project); + + /* Index status deps */ + RUN_TEST(test_index_status_shows_deps); }