test(mcp): give the #1115 store-switch coverage its own test

The store-switch regression for #1115 was added inside
tool_list_projects_tree_uses_one_stable_header_and_keeps_json_direct
and that test was renamed after it, so the name no longer said what
its seven list_projects shape assertions check.

Restore that test exactly as it is on main and move the switch loop
unchanged into tool_project_discovery_agrees_across_store_switches_issue1115
with its own twelve-project fixture. The new test also checks
list_projects before the switches, so it covers both the first-touch
index_status/query case and the before/after list agreement the issue
asked for.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel committed 2026-10-20 07:46:54 +02:00
1 parent 26cd2608f5
commit 2e7244fe4c
1 file changed
+101 -36
+101 -36
View File
@@ -5914,7 +5914,7 @@ TEST(tool_query_graph_prefix_directory_recovers_rows_beyond_raw_estimate) {
PASS();
}
TEST(tool_list_projects_stays_complete_across_store_switches_issue1115) {
TEST(tool_list_projects_tree_uses_one_stable_header_and_keeps_json_direct) {
char cache[256];
snprintf(cache, sizeof(cache), "%s/cbm-list-lean-XXXXXX", cbm_tmpdir());
ASSERT_NOT_NULL(cbm_mkdtemp(cache));
@@ -5971,38 +5971,6 @@ TEST(tool_list_projects_stays_complete_across_store_switches_issue1115) {
bool tree_not_duplicated = tree_response && !strstr(tree_response, "structuredContent");
bool page_truthful = page && strstr(page, "projects: 5") && strstr(page, "returned: 5") &&
strstr(page, "has_more: true") && strstr(page, "next_offset: 5");
bool projects_resolve_after_switches = true;
for (int i = PROJECTS - 1; i >= 0; i--) {
char project[32];
char args[96];
snprintf(project, sizeof(project), "lean-project-%02d", i);
snprintf(args, sizeof(args), "{\"project\":\"%s\"}", project);
char *status_response = cbm_mcp_handle_tool(srv, "index_status", args);
char *status = extract_text_content(status_response);
if (!status || !strstr(status, "ready")) {
projects_resolve_after_switches = false;
}
free(status);
free(status_response);
snprintf(args, sizeof(args), "{\"project\":\"%s\",\"name_pattern\":\"entry\"}",
project);
char *search_response = cbm_mcp_handle_tool(srv, "search_graph", args);
char *search = extract_text_content(search_response);
if (!search || !strstr(search, "entry")) {
projects_resolve_after_switches = false;
}
free(search);
free(search_response);
}
char *post_switch_response = cbm_mcp_handle_tool(srv, "list_projects", "{\"limit\":50}");
char *post_switch = extract_text_content(post_switch_response);
bool list_complete_after_switches =
post_switch && strstr(post_switch, "total: 12") && strstr(post_switch, "lean-project-00") &&
strstr(post_switch, "lean-project-11");
free(post_switch);
free(post_switch_response);
bool json_direct = false;
bool json_structured = json_response && strstr(json_response, "structuredContent");
yyjson_doc *doc = json ? yyjson_read(json, strlen(json), 0) : NULL;
@@ -6039,13 +6007,109 @@ TEST(tool_list_projects_stays_complete_across_store_switches_issue1115) {
ASSERT_TRUE(tree_is_leaner);
ASSERT_TRUE(tree_not_duplicated);
ASSERT_TRUE(page_truthful);
ASSERT_TRUE(projects_resolve_after_switches);
ASSERT_TRUE(list_complete_after_switches);
ASSERT_TRUE(json_direct);
ASSERT_TRUE(json_structured);
PASS();
}
/* #1115: with more indexed projects than were resident at once, the
* least-recently-used one answered "project not found or not indexed" from
* index_status, and list_projects and index_status disagreed in the same
* session. The server keeps one project store open and switches it whenever a
* call names another project, so twelve projects visited one after another
* force a switch on every step and each visit is the first touch of a project
* that is not resident. Every project must resolve through index_status and a
* query, and list_projects must report all twelve before and after the
* switches. */
TEST(tool_project_discovery_agrees_across_store_switches_issue1115) {
char cache[256];
snprintf(cache, sizeof(cache), "%s/cbm-switch-1115-XXXXXX", cbm_tmpdir());
ASSERT_NOT_NULL(cbm_mkdtemp(cache));
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);
enum { PROJECTS = 12 };
for (int i = 0; i < PROJECTS; i++) {
char project[32];
char db_path[512];
char root_path[512];
snprintf(project, sizeof(project), "lean-project-%02d", i);
snprintf(db_path, sizeof(db_path), "%s/%s.db", cache, project);
snprintf(root_path, sizeof(root_path),
"/workspaces/organization/shared/services/lean-project-%02d", i);
cbm_store_t *store = cbm_store_open_path(db_path);
ASSERT_NOT_NULL(store);
ASSERT_EQ(cbm_store_upsert_project(store, project, root_path), CBM_STORE_OK);
cbm_node_t node = {.project = project,
.label = "Function",
.name = "entry",
.qualified_name = "shared.module.entry",
.file_path = "src/main.c",
.start_line = 1,
.end_line = 1};
ASSERT_GT(cbm_store_upsert_node(store, &node), 0);
cbm_store_close(store);
}
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
ASSERT_NOT_NULL(srv);
char *pre_switch_response = cbm_mcp_handle_tool(srv, "list_projects", "{\"limit\":50}");
char *pre_switch = extract_text_content(pre_switch_response);
bool list_complete_before_switches = pre_switch && strstr(pre_switch, "total: 12") &&
strstr(pre_switch, "lean-project-00") &&
strstr(pre_switch, "lean-project-11");
free(pre_switch);
free(pre_switch_response);
bool projects_resolve_after_switches = true;
for (int i = PROJECTS - 1; i >= 0; i--) {
char project[32];
char args[96];
snprintf(project, sizeof(project), "lean-project-%02d", i);
snprintf(args, sizeof(args), "{\"project\":\"%s\"}", project);
char *status_response = cbm_mcp_handle_tool(srv, "index_status", args);
char *status = extract_text_content(status_response);
if (!status || !strstr(status, "ready")) {
projects_resolve_after_switches = false;
}
free(status);
free(status_response);
snprintf(args, sizeof(args), "{\"project\":\"%s\",\"name_pattern\":\"entry\"}", project);
char *search_response = cbm_mcp_handle_tool(srv, "search_graph", args);
char *search = extract_text_content(search_response);
if (!search || !strstr(search, "entry")) {
projects_resolve_after_switches = false;
}
free(search);
free(search_response);
}
char *post_switch_response = cbm_mcp_handle_tool(srv, "list_projects", "{\"limit\":50}");
char *post_switch = extract_text_content(post_switch_response);
bool list_complete_after_switches = post_switch && strstr(post_switch, "total: 12") &&
strstr(post_switch, "lean-project-00") &&
strstr(post_switch, "lean-project-11");
free(post_switch);
free(post_switch_response);
cbm_mcp_server_free(srv);
restore_cache_dir(saved_cache_copy);
free(saved_cache_copy);
for (int i = 0; i < PROJECTS; i++) {
char project[32];
snprintf(project, sizeof(project), "lean-project-%02d", i);
cleanup_project_db(cache, project);
}
cbm_rmdir(cache);
ASSERT_TRUE(list_complete_before_switches);
ASSERT_TRUE(projects_resolve_after_switches);
ASSERT_TRUE(list_complete_after_switches);
PASS();
}
/* The stored project root is dynamic identity, not a display-sized field.
* Keep a syntactically valid segmented path beyond 1 KiB intact in both wire
* encodings; the default detail also routes that same value through branch
@@ -25105,7 +25169,8 @@ SUITE(mcp) {
RUN_TEST(tool_query_graph_budget_bounds_first_row_and_json_escaping);
RUN_TEST(tool_query_graph_prefix_directory_is_lossless_and_json_stays_direct);
RUN_TEST(tool_query_graph_prefix_directory_recovers_rows_beyond_raw_estimate);
RUN_TEST(tool_list_projects_stays_complete_across_store_switches_issue1115);
RUN_TEST(tool_list_projects_tree_uses_one_stable_header_and_keeps_json_direct);
RUN_TEST(tool_project_discovery_agrees_across_store_switches_issue1115);
RUN_TEST(tool_list_projects_preserves_root_beyond_one_kib);
RUN_TEST(tool_index_status_no_project);
RUN_TEST(tool_ingest_traces_stores_observed_calls);