mcp: expand_project_param, result tagging, dep auto-reindex in all 3 paths

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
This commit is contained in:
Andrew Hundt
2026-03-21 00:00:24 -04:00
parent b9a1ad59b8
commit bd09623e27
4 changed files with 411 additions and 20 deletions
+14
View File
@@ -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;
}
+214 -20
View File
@@ -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, &params);
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, &params, &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;
}
+3
View File
@@ -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.
+180
View File
@@ -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);
}