watcher: register all session-accessed projects for auto-reindex

Previously only the CWD project at startup was watched for file changes.
Now any project the AI session interacts with gets registered:

- handle_index_repository: call cbm_watcher_watch() after successful
  index+pagerank, so explicitly indexed projects get auto-reindexed on
  file changes (same pattern as auto-index thread at lines 3503-3505)

- resolve_store: call cbm_store_get_project() to get root_path from DB,
  then cbm_watcher_watch() when a new store is opened. Only runs on the
  new-store path (early-return skips already-cached projects). Covers
  all data-access tool paths: search_code_graph, trace_call_path, get_code.

TDD: 3 new tests in test_tool_consolidation.c (all pass, 2193 total):
  watcher_registered_after_index_repository
  watcher_registered_on_resolve_store
  watcher_not_registered_for_unknown_path
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
This commit is contained in:
Andrew Hundt
2026-03-23 04:21:17 -04:00
parent afb07d69c1
commit 47bbe5cd51
2 changed files with 114 additions and 0 deletions
+12
View File
@@ -914,6 +914,15 @@ static cbm_store_t *resolve_store(cbm_mcp_server_t *srv, const char *project) {
srv->owns_store = true;
free(srv->current_project);
srv->current_project = heap_strdup(db_project);
/* Register newly-accessed project with watcher (root_path from DB) */
if (srv->watcher && srv->store) {
cbm_project_t proj = {0};
if (cbm_store_get_project(srv->store, db_project, &proj) == CBM_STORE_OK
&& proj.root_path && proj.root_path[0]) {
cbm_watcher_watch(srv->watcher, db_project, proj.root_path);
cbm_project_free_fields(&proj); /* store.h:578 */
}
}
return srv->store;
}
@@ -2266,6 +2275,9 @@ static char *handle_index_repository(cbm_mcp_server_t *srv, const char *args) {
/* Compute PageRank + LinkRank on full graph (project + deps).
* Uses config-backed edge weights when config is available. */
cbm_pagerank_compute_with_config(store, project_name, srv->config);
/* Register project with watcher so future file changes trigger auto-reindex */
if (srv->watcher)
cbm_watcher_watch(srv->watcher, project_name, repo_path);
int nodes = cbm_store_count_nodes(store, project_name);
int edges = cbm_store_count_edges(store, project_name);
+102
View File
@@ -12,6 +12,7 @@
#include <yyjson/yyjson.h>
#include <string.h>
#include <stdlib.h>
#include <watcher/watcher.h>
/* ── 1. Tool visibility tests ─────────────────────────────── */
@@ -1311,6 +1312,104 @@ TEST(get_code_cold_start_parses_project_from_qn) {
PASS();
}
/* ── Watcher registration tests ──────────────────────────── */
TEST(watcher_registered_after_index_repository) {
/* Create a tiny temp repo so indexing succeeds quickly */
char repo_path[] = "/tmp/cbm_watch_test_XXXXXX";
ASSERT_NOT_NULL(mkdtemp(repo_path));
char src_path[256];
snprintf(src_path, sizeof(src_path), "%s/test.c", repo_path);
FILE *f = fopen(src_path, "w");
if (f) { fprintf(f, "void hello(void) {}\n"); fclose(f); }
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
ASSERT_NOT_NULL(srv);
cbm_watcher_t *w = cbm_watcher_new(NULL, NULL, NULL);
ASSERT_NOT_NULL(w);
cbm_mcp_server_set_watcher(srv, w);
char args[512];
snprintf(args, sizeof(args), "{\"repo_path\":\"%s\"}", repo_path);
char *resp = cbm_mcp_handle_tool(srv, "index_repository", args);
ASSERT_NOT_NULL(resp);
free(resp);
ASSERT_TRUE(cbm_watcher_watch_count(w) > 0);
cbm_mcp_server_free(srv);
cbm_watcher_free(w);
(void)unlink(src_path);
(void)rmdir(repo_path);
PASS();
}
TEST(watcher_registered_on_resolve_store) {
/* Pre-populate a DB with a project that has a known root_path */
char db_path[1024];
snprintf(db_path, sizeof(db_path), "%s/.cache/codebase-memory-mcp/_tc_watcher_.db",
getenv("HOME"));
cbm_store_t *s = cbm_store_open_path(db_path);
ASSERT_NOT_NULL(s);
cbm_store_upsert_project(s, "_tc_watcher_", "/tmp/cbm_watcher_root");
cbm_node_t n = {.project = "_tc_watcher_", .label = "Function",
.name = "watcher_fn", .qualified_name = "_tc_watcher_.watcher_fn",
.file_path = "watcher_fn.c"};
cbm_store_upsert_node(s, &n);
cbm_store_close(s);
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
ASSERT_NOT_NULL(srv);
cbm_watcher_t *w = cbm_watcher_new(NULL, NULL, NULL);
ASSERT_NOT_NULL(w);
cbm_mcp_server_set_watcher(srv, w);
char *resp = cbm_mcp_handle_tool(srv, "search_code_graph",
"{\"project\":\"_tc_watcher_\",\"name_pattern\":\"watcher_fn\",\"limit\":1}");
ASSERT_NOT_NULL(resp);
free(resp);
ASSERT_TRUE(cbm_watcher_watch_count(w) > 0);
cbm_mcp_server_free(srv);
cbm_watcher_free(w);
(void)unlink(db_path);
PASS();
}
TEST(watcher_not_registered_for_unknown_path) {
/* Project entry exists but root_path is empty — watcher must NOT be registered */
char db_path[1024];
snprintf(db_path, sizeof(db_path), "%s/.cache/codebase-memory-mcp/_tc_watcher_nopath_.db",
getenv("HOME"));
cbm_store_t *s = cbm_store_open_path(db_path);
ASSERT_NOT_NULL(s);
cbm_store_upsert_project(s, "_tc_watcher_nopath_", "");
cbm_node_t n = {.project = "_tc_watcher_nopath_", .label = "Function",
.name = "nopath_fn", .qualified_name = "_tc_watcher_nopath_.nopath_fn",
.file_path = "nopath_fn.c"};
cbm_store_upsert_node(s, &n);
cbm_store_close(s);
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
ASSERT_NOT_NULL(srv);
cbm_watcher_t *w = cbm_watcher_new(NULL, NULL, NULL);
ASSERT_NOT_NULL(w);
cbm_mcp_server_set_watcher(srv, w);
char *resp = cbm_mcp_handle_tool(srv, "search_code_graph",
"{\"project\":\"_tc_watcher_nopath_\",\"name_pattern\":\"nopath_fn\",\"limit\":1}");
ASSERT_NOT_NULL(resp);
free(resp);
ASSERT_EQ(cbm_watcher_watch_count(w), 0);
cbm_mcp_server_free(srv);
cbm_watcher_free(w);
(void)unlink(db_path);
PASS();
}
/* ── Suite registration ──────────────────────────────────── */
SUITE(tool_consolidation) {
@@ -1395,4 +1494,7 @@ SUITE(tool_consolidation) {
RUN_TEST(get_code_no_project_uses_open_store_tier1);
RUN_TEST(get_code_single_fuzzy_result_resolves_not_ambiguous);
RUN_TEST(get_code_cold_start_parses_project_from_qn);
RUN_TEST(watcher_registered_after_index_repository);
RUN_TEST(watcher_registered_on_resolve_store);
RUN_TEST(watcher_not_registered_for_unknown_path);
}