From 3b1ea5826e5904ef43bdb3b257f518103ce70d15 Mon Sep 17 00:00:00 2001 From: metehanulusoy Date: Sun, 12 Jul 2026 14:55:22 +0300 Subject: [PATCH] fix(mcp): ignore ::missed shadow rows when resolving a db's project name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The miss-graph pass inserts a second `projects` row ("::missed") so its nodes satisfy the FK on nodes.project. Since #704, 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 — the index looked empty while fully present. Filter out internal shadow rows (names containing "::") and require exactly one primary row instead. Adds a reproduce-first regression test modeled on the #704 fixture (red on the old code: 150 passed, 1 failed; green with the fix: 151 passed). Fixes #1044 Signed-off-by: metehanulusoy Co-Authored-By: Claude Fable 5 --- src/mcp/mcp.c | 21 ++++++++++--- tests/test_mcp.c | 76 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 4 deletions(-) diff --git a/src/mcp/mcp.c b/src/mcp/mcp.c index 0d7aa14d6..59c1f94a0 100644 --- a/src/mcp/mcp.c +++ b/src/mcp/mcp.c @@ -1335,10 +1335,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 ("::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) { diff --git a/tests/test_mcp.c b/tests/test_mcp.c index e24ec0d73..a5ae60ceb 100644 --- a/tests/test_mcp.c +++ b/tests/test_mcp.c @@ -4134,6 +4134,81 @@ TEST(tool_resolve_store_by_internal_name_issue704) { PASS(); } +/* ── #1044: a "::missed" shadow row must not hide the project ── + * + * The miss-graph pass inserts a second `projects` row ("::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) * @@ -5763,6 +5838,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);