From f7b112e3e11067af7c4823352a412aa88d7aec86 Mon Sep 17 00:00:00 2001 From: Andy11-cpu Date: Wed, 1 Jul 2026 21:53:18 -0400 Subject: [PATCH 1/2] Deduplicate project indexes by git/path identity Reuse an existing cached project when the same repository is opened from a different path. Match by git canonical root or canonical filesystem path so duplicate .db files are not created. Signed-off-by: Andy11-cpu --- Makefile.cbm | 3 +- src/mcp/mcp.c | 16 +++- src/pipeline/pipeline.c | 4 +- src/pipeline/project_resolve.c | 148 +++++++++++++++++++++++++++++++ src/pipeline/project_resolve.h | 17 ++++ tests/test_project_resolve.c | 155 +++++++++++++++++++++++++++++++++ 6 files changed, 337 insertions(+), 6 deletions(-) create mode 100644 src/pipeline/project_resolve.c create mode 100644 src/pipeline/project_resolve.h create mode 100644 tests/test_project_resolve.c diff --git a/Makefile.cbm b/Makefile.cbm index 2bcf7b4d7..261ce1766 100644 --- a/Makefile.cbm +++ b/Makefile.cbm @@ -175,6 +175,7 @@ GRAPH_BUFFER_SRCS = src/graph_buffer/graph_buffer.c # Pipeline module (new) PIPELINE_SRCS = \ src/pipeline/fqn.c \ + src/pipeline/project_resolve.c \ src/pipeline/path_alias.c \ src/pipeline/registry.c \ src/pipeline/pipeline.c \ @@ -332,7 +333,7 @@ TEST_DISCOVER_SRCS = \ TEST_GRAPH_BUFFER_SRCS = tests/test_graph_buffer.c -TEST_PIPELINE_SRCS = tests/test_registry.c tests/test_pipeline.c tests/test_fqn.c tests/test_route_canon.c tests/test_path_alias.c tests/test_configlink.c tests/test_infrascan.c tests/test_worker_pool.c tests/test_parallel.c +TEST_PIPELINE_SRCS = tests/test_registry.c tests/test_pipeline.c tests/test_fqn.c tests/test_route_canon.c tests/test_path_alias.c tests/test_configlink.c tests/test_infrascan.c tests/test_worker_pool.c tests/test_parallel.c tests/test_project_resolve.c TEST_WATCHER_SRCS = tests/test_watcher.c diff --git a/src/mcp/mcp.c b/src/mcp/mcp.c index 7016a0d21..3ac34c4ef 100644 --- a/src/mcp/mcp.c +++ b/src/mcp/mcp.c @@ -42,6 +42,7 @@ enum { #include #include "cypher/cypher.h" #include "pipeline/pipeline.h" +#include "pipeline/project_resolve.h" #include "pipeline/pass_cross_repo.h" #include "git/git_context.h" #include "cli/cli.h" @@ -4568,10 +4569,17 @@ static void detect_session(cbm_mcp_server_t *srv) { * used by the pipeline, otherwise session queries look for a .db file * that doesn't match the indexed project name. */ if (srv->session_root[0]) { - char *pname = cbm_project_name_from_path(srv->session_root); - if (pname) { - snprintf(srv->session_project, sizeof(srv->session_project), "%s", pname); - free(pname); + char *existing = cbm_find_existing_project_name(srv->session_root); + if (existing) { + snprintf(srv->session_project, sizeof(srv->session_project), "%s", existing); + cbm_log_info("session.project.reuse", "project", existing, "path", srv->session_root); + free(existing); + } else { + char *pname = cbm_project_name_from_path(srv->session_root); + if (pname) { + snprintf(srv->session_project, sizeof(srv->session_project), "%s", pname); + free(pname); + } } } } diff --git a/src/pipeline/pipeline.c b/src/pipeline/pipeline.c index 8e370f7c3..554a0e3e1 100644 --- a/src/pipeline/pipeline.c +++ b/src/pipeline/pipeline.c @@ -15,6 +15,7 @@ enum { CBM_DIR_PERMS = 0755, PL_RING = 4, PL_RING_MASK = 3, PL_SEQ_PASSES = 6, PL_WAL_BUF = 1040 }; #define PL_NSEC_PER_SEC 1000000000LL #include "pipeline/pipeline.h" +#include "pipeline/project_resolve.h" #include "pipeline/artifact.h" #include "pipeline/pipeline_internal.h" #include "pipeline/pass_lsp_cross.h" @@ -153,7 +154,8 @@ cbm_pipeline_t *cbm_pipeline_new(const char *repo_path, const char *db_path, p->repo_path = strdup(repo_path); p->db_path = db_path ? strdup(db_path) : NULL; - p->project_name = cbm_project_name_from_path(repo_path); + char *existing = cbm_find_existing_project_name(repo_path); + p->project_name = existing ? existing : cbm_project_name_from_path(repo_path); (void)cbm_git_context_resolve(repo_path, &p->git_ctx); p->branch_qn = cbm_git_context_branch_qn(p->project_name, &p->git_ctx); p->mode = mode; diff --git a/src/pipeline/project_resolve.c b/src/pipeline/project_resolve.c new file mode 100644 index 000000000..2c9ac22fb --- /dev/null +++ b/src/pipeline/project_resolve.c @@ -0,0 +1,148 @@ +/* + * project_resolve.c — Canonical path identity and duplicate-index prevention. + */ +#include "pipeline/project_resolve.h" +#include "pipeline/pipeline.h" +#include "foundation/platform.h" +#include "foundation/compat_fs.h" +#include "git/git_context.h" +#include "store/store.h" + +#include +#include +#include + +bool cbm_path_canonicalize(const char *path, char *out, size_t out_sz) { + if (!path || !out || out_sz == 0) { + return false; + } + out[0] = '\0'; +#ifdef _WIN32 + if (!_fullpath(out, path, out_sz)) { + return false; + } + cbm_normalize_path_sep(out); +#else + if (!realpath(path, out)) { + return false; + } +#endif + return out[0] != '\0'; +} + +bool cbm_project_identity_key(const char *repo_path, char *out, size_t out_sz) { + if (!repo_path || !out || out_sz == 0) { + return false; + } + + cbm_git_context_t ctx = {0}; + if (cbm_git_context_resolve(repo_path, &ctx) == 0 && ctx.canonical_root && + ctx.canonical_root[0]) { + snprintf(out, out_sz, "%s", ctx.canonical_root); + cbm_normalize_path_sep(out); + cbm_git_context_free(&ctx); + return true; + } + cbm_git_context_free(&ctx); + return cbm_path_canonicalize(repo_path, out, out_sz); +} + +static bool identity_nested(const char *child, const char *parent) { + if (!child[0] || !parent[0]) { + return false; + } + if (strcmp(child, parent) == 0) { + return true; + } + size_t plen = strlen(parent); + if (strncmp(child, parent, plen) != 0) { + return false; + } + return child[plen] == '/'; +} + +static bool is_project_db_file(const char *name, size_t len) { + if (len < 5 || strcmp(name + len - 3, ".db") != 0) { + return false; + } + if (name[0] == '_') { + return false; + } + return true; +} + +char *cbm_find_existing_project_name(const char *repo_path) { + if (!repo_path || !repo_path[0]) { + return NULL; + } + + char query_key[4096]; + if (!cbm_project_identity_key(repo_path, query_key, sizeof(query_key))) { + return NULL; + } + + char cache_dir[1024]; + snprintf(cache_dir, sizeof(cache_dir), "%s", cbm_resolve_cache_dir()); + + cbm_dir_t *d = cbm_opendir(cache_dir); + if (!d) { + return NULL; + } + + char *best_name = NULL; + size_t best_root_len = 0; + + cbm_dirent_t *entry; + while ((entry = cbm_readdir(d)) != NULL) { + const char *name = entry->name; + size_t len = strlen(name); + if (!is_project_db_file(name, len)) { + continue; + } + + char db_path[2048]; + snprintf(db_path, sizeof(db_path), "%s/%s", cache_dir, name); + + cbm_store_t *store = cbm_store_open_path(db_path); + if (!store) { + continue; + } + + char project_name[1024]; + snprintf(project_name, sizeof(project_name), "%.*s", (int)(len - 3), name); + + cbm_project_t proj = {0}; + if (cbm_store_get_project(store, project_name, &proj) != CBM_STORE_OK || !proj.root_path) { + safe_str_free(&proj.name); + safe_str_free(&proj.indexed_at); + safe_str_free(&proj.root_path); + cbm_store_close(store); + continue; + } + + char indexed_key[4096]; + bool has_key = cbm_project_identity_key(proj.root_path, indexed_key, sizeof(indexed_key)); + + safe_str_free(&proj.name); + safe_str_free(&proj.indexed_at); + safe_str_free(&proj.root_path); + cbm_store_close(store); + + if (!has_key) { + continue; + } + + if (strcmp(query_key, indexed_key) == 0 || identity_nested(query_key, indexed_key) || + identity_nested(indexed_key, query_key)) { + size_t root_len = strlen(indexed_key); + if (!best_name || root_len > best_root_len) { + free(best_name); + best_name = strdup(project_name); + best_root_len = root_len; + } + } + } + + cbm_closedir(d); + return best_name; +} diff --git a/src/pipeline/project_resolve.h b/src/pipeline/project_resolve.h new file mode 100644 index 000000000..d36b724fc --- /dev/null +++ b/src/pipeline/project_resolve.h @@ -0,0 +1,17 @@ +#ifndef CBM_PROJECT_RESOLVE_H +#define CBM_PROJECT_RESOLVE_H + +#include +#include + +/* Canonicalize path (realpath / _fullpath). Returns false if path is invalid. */ +bool cbm_path_canonicalize(const char *path, char *out, size_t out_sz); + +/* Stable identity for dedup: git canonical_root when available, else canonical path. */ +bool cbm_project_identity_key(const char *repo_path, char *out, size_t out_sz); + +/* Return heap-allocated existing project name when repo_path matches a cached index + * (same identity or nested under an indexed root). Caller frees; NULL if no match. */ +char *cbm_find_existing_project_name(const char *repo_path); + +#endif diff --git a/tests/test_project_resolve.c b/tests/test_project_resolve.c new file mode 100644 index 000000000..874f728d9 --- /dev/null +++ b/tests/test_project_resolve.c @@ -0,0 +1,155 @@ +/* + * test_project_resolve.c — Canonical project identity and duplicate-index prevention. + */ +#include "../src/foundation/compat.h" +#include "test_framework.h" +#include "test_helpers.h" +#include "pipeline/project_resolve.h" +#include "pipeline/pipeline.h" +#include + +#include +#include +#include +#include + +typedef struct { + const char *cache; + const char *project; + const char *root; +} seed_ctx_t; + +typedef struct { + const char *query_root; + char **found; +} find_ctx_t; + +typedef struct { + const char *root; + cbm_pipeline_t **pipeline; +} pipeline_ctx_t; + +static void with_cache_dir(const char *cache, void (*fn)(void *), void *ctx) { + const char *saved = getenv("CBM_CACHE_DIR"); + char *saved_copy = saved ? strdup(saved) : NULL; + cbm_setenv("CBM_CACHE_DIR", cache, 1); + fn(ctx); + if (saved_copy) { + cbm_setenv("CBM_CACHE_DIR", saved_copy, 1); + free(saved_copy); + } else { + cbm_unsetenv("CBM_CACHE_DIR"); + } +} + +static void seed_project_db(void *vctx) { + seed_ctx_t *ctx = (seed_ctx_t *)vctx; + char db_path[1024]; + snprintf(db_path, sizeof(db_path), "%s/%s.db", ctx->cache, ctx->project); + cbm_store_t *store = cbm_store_open_path(db_path); + ASSERT_NOT_NULL(store); + ASSERT_EQ(cbm_store_upsert_project(store, ctx->project, ctx->root), CBM_STORE_OK); + cbm_store_close(store); +} + +static void find_existing_project(void *vctx) { + find_ctx_t *ctx = (find_ctx_t *)vctx; + *(ctx->found) = cbm_find_existing_project_name(ctx->query_root); +} + +static void open_pipeline_for_root(void *vctx) { + pipeline_ctx_t *ctx = (pipeline_ctx_t *)vctx; + *(ctx->pipeline) = cbm_pipeline_new(ctx->root, NULL, CBM_MODE_FAST); +} + +TEST(project_resolve_path_canonicalize) { + char tmpdir[256]; + snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm-projres-XXXXXX"); + if (!cbm_mkdtemp(tmpdir)) + FAIL("cbm_mkdtemp failed"); + + char file[512]; + snprintf(file, sizeof(file), "%s/readme.txt", tmpdir); + th_write_file(file, "x"); + + char canon[1024]; + ASSERT_TRUE(cbm_path_canonicalize(file, canon, sizeof(canon))); + ASSERT(strstr(canon, "readme.txt") != NULL); + + test_rmdir_r(tmpdir); + PASS(); +} + +TEST(project_resolve_identity_key_stable) { + char key1[1024]; + char key2[1024]; + ASSERT_TRUE(cbm_project_identity_key("/tmp/foo/bar", key1, sizeof(key1))); + ASSERT_TRUE(cbm_project_identity_key("/tmp/foo/bar/", key2, sizeof(key2))); + ASSERT_STR_EQ(key1, key2); + PASS(); +} + +TEST(project_resolve_find_existing_by_root_path) { + char cache[256]; + snprintf(cache, sizeof(cache), "/tmp/cbm-projres-cache-XXXXXX"); + if (!cbm_mkdtemp(cache)) + FAIL("cbm_mkdtemp failed"); + + char root[512]; + snprintf(root, sizeof(root), "%s/repo-root", cache); + test_mkdirp(root); + + seed_ctx_t seed = {.cache = cache, .project = "indexed-project", .root = root}; + with_cache_dir(cache, seed_project_db, &seed); + + char *found = NULL; + find_ctx_t fctx = {.query_root = root, .found = &found}; + with_cache_dir(cache, find_existing_project, &fctx); + + ASSERT_NOT_NULL(found); + ASSERT_STR_EQ(found, "indexed-project"); + free(found); + + char db_path[1024]; + snprintf(db_path, sizeof(db_path), "%s/indexed-project.db", cache); + cbm_unlink(db_path); + test_rmdir_r(root); + cbm_rmdir(cache); + PASS(); +} + +TEST(project_resolve_pipeline_reuses_existing_name) { + char cache[256]; + snprintf(cache, sizeof(cache), "/tmp/cbm-projres-pl-XXXXXX"); + if (!cbm_mkdtemp(cache)) + FAIL("cbm_mkdtemp failed"); + + char root[512]; + snprintf(root, sizeof(root), "%s/worktree", cache); + test_mkdirp(root); + + seed_ctx_t seed = {.cache = cache, .project = "canonical-name", .root = root}; + with_cache_dir(cache, seed_project_db, &seed); + + cbm_pipeline_t *p = NULL; + pipeline_ctx_t pctx = {.root = root, .pipeline = &p}; + with_cache_dir(cache, open_pipeline_for_root, &pctx); + + ASSERT_NOT_NULL(p); + ASSERT_STR_EQ(cbm_pipeline_project_name(p), "canonical-name"); + cbm_pipeline_free(p); + + char db_path[1024]; + snprintf(db_path, sizeof(db_path), "%s/canonical-name.db", cache); + cbm_unlink(db_path); + test_rmdir_r(root); + cbm_rmdir(cache); + PASS(); +} + +SUITE(project_resolve) { + RUN_TEST(project_resolve_path_canonicalize); + RUN_TEST(project_resolve_identity_key_stable); + RUN_TEST(project_resolve_find_existing_by_root_path); + RUN_TEST(project_resolve_pipeline_reuses_existing_name); +} From fae93c33590d01928b947f4304c1174c52bd369c Mon Sep 17 00:00:00 2001 From: Andy11-cpu Date: Wed, 1 Jul 2026 21:55:22 -0400 Subject: [PATCH 2/2] Gate git watcher behind auto_watch config Do not register the git watcher on MCP initialize unless auto_watch is explicitly enabled (default false). Re-index on connect only when auto_index is set; ongoing git polling requires auto_watch after a manual index. Signed-off-by: Andy11-cpu --- src/cli/cli.c | 4 +++ src/cli/cli.h | 1 + src/mcp/mcp.c | 36 +++++++++++++++++----- tests/test_cli.c | 3 ++ tests/test_mcp.c | 78 ++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 114 insertions(+), 8 deletions(-) diff --git a/src/cli/cli.c b/src/cli/cli.c index f159f5914..089d01cfe 100644 --- a/src/cli/cli.c +++ b/src/cli/cli.c @@ -2625,6 +2625,8 @@ int cbm_cmd_config(int argc, char **argv) { "Enable auto-indexing on MCP session start"); printf(" %-25s default=%-10s %s\n", CBM_CONFIG_AUTO_INDEX_LIMIT, "50000", "Max files for auto-indexing new projects"); + printf(" %-25s default=%-10s %s\n", CBM_CONFIG_AUTO_WATCH, "false", + "Enable git watcher background re-indexing (off by default)"); return 0; } @@ -2650,6 +2652,8 @@ int cbm_cmd_config(int argc, char **argv) { cbm_config_get(cfg, CBM_CONFIG_AUTO_INDEX, "false")); printf(" %-25s = %-10s\n", CBM_CONFIG_AUTO_INDEX_LIMIT, cbm_config_get(cfg, CBM_CONFIG_AUTO_INDEX_LIMIT, "50000")); + printf(" %-25s = %-10s\n", CBM_CONFIG_AUTO_WATCH, + cbm_config_get(cfg, CBM_CONFIG_AUTO_WATCH, "false")); } else if (strcmp(argv[0], "get") == 0) { if (argc < MIN_ARGC_GET) { (void)fprintf(stderr, "Usage: config get \n"); diff --git a/src/cli/cli.h b/src/cli/cli.h index 9efe67896..e7c8ac1b2 100644 --- a/src/cli/cli.h +++ b/src/cli/cli.h @@ -264,6 +264,7 @@ int cbm_config_delete(cbm_config_t *cfg, const char *key); /* Well-known config keys */ #define CBM_CONFIG_AUTO_INDEX "auto_index" #define CBM_CONFIG_AUTO_INDEX_LIMIT "auto_index_limit" +#define CBM_CONFIG_AUTO_WATCH "auto_watch" /* ── Subcommands (wired from main.c) ─────────────────────────── */ diff --git a/src/mcp/mcp.c b/src/mcp/mcp.c index 3ac34c4ef..094248f9c 100644 --- a/src/mcp/mcp.c +++ b/src/mcp/mcp.c @@ -2782,6 +2782,9 @@ static bool build_index_success_response(cbm_mcp_server_t *srv, yyjson_mut_doc * return degraded; } +static bool auto_watch_enabled(cbm_mcp_server_t *srv); +static void register_watcher_if_enabled(cbm_mcp_server_t *srv); + static char *handle_index_repository(cbm_mcp_server_t *srv, const char *args) { char *repo_path = cbm_mcp_get_string_arg(args, "repo_path"); char *mode_str = cbm_mcp_get_string_arg(args, "mode"); @@ -2865,6 +2868,9 @@ static char *handle_index_repository(cbm_mcp_server_t *srv, const char *args) { bool degraded = build_index_success_response(srv, doc, root, project_name, repo_path, persistence, p, excluded_dirs, excluded_count); yyjson_mut_obj_add_str(doc, root, "status", degraded ? "degraded" : "indexed"); + if (srv->watcher && auto_watch_enabled(srv)) { + cbm_watcher_watch(srv->watcher, project_name, repo_path); + } } else { yyjson_mut_obj_add_str(doc, root, "status", "error"); yyjson_mut_obj_add_str(doc, root, "hint", @@ -4585,6 +4591,25 @@ static void detect_session(cbm_mcp_server_t *srv) { } /* Background auto-index thread function */ +static bool auto_watch_enabled(cbm_mcp_server_t *srv) { + if (!srv || !srv->config) { + return false; + } + return cbm_config_get_bool(srv->config, CBM_CONFIG_AUTO_WATCH, false); +} + +static void register_watcher_if_enabled(cbm_mcp_server_t *srv) { + if (!srv || !srv->watcher || srv->session_project[0] == '\0' || srv->session_root[0] == '\0') { + return; + } + if (!auto_watch_enabled(srv)) { + cbm_log_info("watcher.skip", "reason", "auto_watch_disabled", "hint", + "run: codebase-memory-mcp config set auto_watch true"); + return; + } + cbm_watcher_watch(srv->watcher, srv->session_project, srv->session_root); +} + static void *autoindex_thread(void *arg) { cbm_mcp_server_t *srv = (cbm_mcp_server_t *)arg; @@ -4606,10 +4631,7 @@ static void *autoindex_thread(void *arg) { if (rc == 0) { 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); - } + register_watcher_if_enabled(srv); } else { cbm_log_warn("autoindex.err", "msg", "pipeline_run_failed"); } @@ -4629,12 +4651,10 @@ static void maybe_auto_index(cbm_mcp_server_t *srv) { snprintf(db_check, sizeof(db_check), "%s/%s.db", cbm_resolve_cache_dir(), srv->session_project); if (cbm_file_size(db_check) >= 0) { - /* Already indexed → register watcher for change detection */ + /* Already indexed — use existing graph; never auto re-index on connect. */ cbm_log_info("autoindex.skip", "reason", "already_indexed", "project", srv->session_project); - if (srv->watcher) { - cbm_watcher_watch(srv->watcher, srv->session_project, srv->session_root); - } + register_watcher_if_enabled(srv); return; } } diff --git a/tests/test_cli.c b/tests/test_cli.c index 0b78537c4..4ae8bb7e1 100644 --- a/tests/test_cli.c +++ b/tests/test_cli.c @@ -2493,6 +2493,9 @@ TEST(cli_config_get_bool) { ASSERT_FALSE(cbm_config_get_bool(cfg, "auto_index", false)); ASSERT_TRUE(cbm_config_get_bool(cfg, "auto_index", true)); + ASSERT_FALSE(cbm_config_get_bool(cfg, CBM_CONFIG_AUTO_WATCH, false)); + ASSERT_TRUE(cbm_config_get_bool(cfg, CBM_CONFIG_AUTO_WATCH, true)); + /* true variants */ cbm_config_set(cfg, "k1", "true"); ASSERT_TRUE(cbm_config_get_bool(cfg, "k1", false)); diff --git a/tests/test_mcp.c b/tests/test_mcp.c index 68edc0e97..d3fbb8969 100644 --- a/tests/test_mcp.c +++ b/tests/test_mcp.c @@ -6,12 +6,17 @@ #include "../src/foundation/compat.h" #include "../src/foundation/compat_fs.h" /* cbm_unlink / cbm_rmdir */ #include "test_framework.h" +#include "test_helpers.h" #include #include +#include +#include "cli/cli.h" +#include "pipeline/pipeline.h" #include #include #include #include +#include /* ══════════════════════════════════════════════════════════════════ * JSON-RPC PARSING @@ -2209,6 +2214,78 @@ TEST(tool_bad_project_name_no_overflow_issue235) { } #undef ISSUE235_DBNAME +TEST(mcp_auto_watch_disabled_skips_watcher_on_connect) { + char cache[256]; + snprintf(cache, sizeof(cache), "/tmp/cbm-autowatch-XXXXXX"); + if (!cbm_mkdtemp(cache)) + PASS(); + + char repodir[512]; + snprintf(repodir, sizeof(repodir), "%s/repo", cache); + test_mkdirp(repodir); + + char *project = cbm_project_name_from_path(repodir); + ASSERT_NOT_NULL(project); + + char db_path[1024]; + snprintf(db_path, sizeof(db_path), "%s/%s.db", cache, project); + cbm_store_t *db = cbm_store_open_path(db_path); + ASSERT_NOT_NULL(db); + ASSERT_EQ(cbm_store_upsert_project(db, project, repodir), CBM_STORE_OK); + cbm_store_close(db); + + const char *saved_cache = getenv("CBM_CACHE_DIR"); + char *saved_cache_copy = saved_cache ? strdup(saved_cache) : NULL; + cbm_setenv("CBM_CACHE_DIR", cache, 1); + + char old_cwd[1024]; + ASSERT_NOT_NULL(getcwd(old_cwd, sizeof(old_cwd))); + ASSERT_EQ(chdir(repodir), 0); + + cbm_config_t *cfg = cbm_config_open(cache); + ASSERT_NOT_NULL(cfg); + + cbm_store_t *wstore = cbm_store_open_memory(); + cbm_watcher_t *watcher = cbm_watcher_new(wstore, NULL, NULL); + cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL); + cbm_mcp_server_set_watcher(srv, watcher); + cbm_mcp_server_set_config(srv, cfg); + + char *resp = cbm_mcp_server_handle( + srv, "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{}}"); + free(resp); + ASSERT_EQ(cbm_watcher_watch_count(watcher), 0); + + cbm_mcp_server_free(srv); + ASSERT_EQ(cbm_config_set(cfg, CBM_CONFIG_AUTO_WATCH, "true"), 0); + + srv = cbm_mcp_server_new(NULL); + cbm_mcp_server_set_watcher(srv, watcher); + cbm_mcp_server_set_config(srv, cfg); + resp = cbm_mcp_server_handle( + srv, "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"initialize\",\"params\":{}}"); + free(resp); + ASSERT_EQ(cbm_watcher_watch_count(watcher), 1); + + cbm_mcp_server_free(srv); + cbm_watcher_free(watcher); + cbm_store_close(wstore); + cbm_config_close(cfg); + free(project); + chdir(old_cwd); + + if (saved_cache_copy) { + cbm_setenv("CBM_CACHE_DIR", saved_cache_copy, 1); + free(saved_cache_copy); + } else { + cbm_unsetenv("CBM_CACHE_DIR"); + } + cbm_unlink(db_path); + test_rmdir_r(repodir); + cbm_rmdir(cache); + PASS(); +} + /* ══════════════════════════════════════════════════════════════════ * SUITE * ══════════════════════════════════════════════════════════════════ */ @@ -2353,4 +2430,5 @@ SUITE(mcp) { RUN_TEST(snippet_include_neighbors_enabled); RUN_TEST(snippet_source_invalid_utf8); RUN_TEST(tool_bad_project_name_no_overflow_issue235); + RUN_TEST(mcp_auto_watch_disabled_skips_watcher_on_connect); }