From 4d9da40b360ad2b87fa7db929f66ab9c6fc86756 Mon Sep 17 00:00:00 2001 From: LA-10 Date: Thu, 16 Jul 2026 15:59:24 +0300 Subject: [PATCH] test(769): end-to-end sibling regression + index-format rebuild boundary Refs #769 Signed-off-by: LA-10 --- Makefile.cbm | 2 +- src/mcp/mcp.c | 3 + src/pipeline/pipeline.c | 31 +++++ src/pipeline/pipeline.h | 2 + src/store/store.c | 30 +++++ src/store/store.h | 8 ++ tests/test_fqn.c | 79 ++++++++++++ tests/test_index_format.c | 249 ++++++++++++++++++++++++++++++++++++++ tests/test_main.c | 2 + 9 files changed, 405 insertions(+), 1 deletion(-) create mode 100644 tests/test_index_format.c diff --git a/Makefile.cbm b/Makefile.cbm index 28eb21e27..280074fd9 100644 --- a/Makefile.cbm +++ b/Makefile.cbm @@ -559,7 +559,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_cross_repo.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_index_resilience.c tests/test_call_reference_contract.c tests/repro/repro_call_scope_usages.c tests/repro/repro_call_argument_usages.c tests/repro/repro_reference_precision.c tests/repro/repro_lexical_binding_precision.c tests/repro/repro_call_argument_matrix_a.c tests/repro/repro_call_argument_matrix_b.c tests/repro/repro_call_node_behaviors.c tests/repro/repro_language_registry.c tests/repro/repro_call_node_manifest.c tests/repro/repro_lsp_ordered_signatures.c tests/repro/repro_lsp_ordered_local.c tests/repro/repro_ts_overload_return_chains.c tests/repro/repro_harness_cleanup.c tests/repro/repro_runner_filter.c +TEST_PIPELINE_SRCS = tests/test_registry.c tests/test_pipeline.c tests/test_cross_repo.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_index_resilience.c tests/test_index_format.c tests/test_call_reference_contract.c tests/repro/repro_call_scope_usages.c tests/repro/repro_call_argument_usages.c tests/repro/repro_reference_precision.c tests/repro/repro_lexical_binding_precision.c tests/repro/repro_call_argument_matrix_a.c tests/repro/repro_call_argument_matrix_b.c tests/repro/repro_call_node_behaviors.c tests/repro/repro_language_registry.c tests/repro/repro_call_node_manifest.c tests/repro/repro_lsp_ordered_signatures.c tests/repro/repro_lsp_ordered_local.c tests/repro/repro_ts_overload_return_chains.c tests/repro/repro_harness_cleanup.c tests/repro/repro_runner_filter.c TEST_WATCHER_SRCS = tests/test_watcher.c diff --git a/src/mcp/mcp.c b/src/mcp/mcp.c index 04316627b..d2e7bf1e2 100644 --- a/src/mcp/mcp.c +++ b/src/mcp/mcp.c @@ -8343,6 +8343,9 @@ static char *handle_index_repository(cbm_mcp_server_t *srv, const char *args) { srv, doc, root, project_name, repo_path, persistence, p, excluded_dirs, excluded_count, file_errors, file_error_count, has_logfile ? logfile_path : NULL); yyjson_mut_obj_add_str(doc, root, "status", degraded ? "degraded" : "indexed"); + if (cbm_pipeline_had_format_migration(p)) { + yyjson_mut_obj_add_bool(doc, root, "format_migration", true); + } } else { yyjson_mut_obj_add_str(doc, root, "status", "error"); yyjson_mut_obj_add_str(doc, root, "hint", diff --git a/src/pipeline/pipeline.c b/src/pipeline/pipeline.c index 65ac75183..07563d78f 100644 --- a/src/pipeline/pipeline.c +++ b/src/pipeline/pipeline.c @@ -190,6 +190,10 @@ struct cbm_pipeline { int committed_nodes; int committed_edges; + /* #769: set when a stale-format index was routed through the one-time + * full rebuild, so the MCP response can surface the migration. */ + bool format_migration; + /* ADR (project_summaries) captured before a full-reindex DB delete, so it * can be restored after the rebuild. NULL when no ADR existed. Issue #516. */ char *saved_adr; @@ -224,6 +228,10 @@ void cbm_pipeline_set_pkgmap(CBMHashTable *map) { g_pkgmap = map; } +bool cbm_pipeline_had_format_migration(const cbm_pipeline_t *p) { + return p && p->format_migration; +} + /* ── Timing helper ──────────────────────────────────────────────── */ static double elapsed_ms(struct timespec start) { @@ -1393,6 +1401,24 @@ static int try_incremental_or_delete_db(cbm_pipeline_t *p, cbm_file_info_t *file free(db_path); return CBM_PIPELINE_FORCE_FULL_REINDEX; } + + cbm_store_t *fmt_store = cbm_store_open_path_query(db_path); + int fmt = 0; + if (fmt_store) { + cbm_store_get_format_version(fmt_store, &fmt); + cbm_store_close(fmt_store); + } + if (fmt != CBM_INDEX_FORMAT_VERSION) { + cbm_log_info("pipeline.route", "path", "format_change_reindex", "stored_format", + itoa_buf(fmt)); + p->format_migration = true; + int adr_rc = capture_existing_adr(p, db_path); + (void)cbm_unlink(db_path); + (void)cbm_remove_db_sidecars(db_path); + free(db_path); + return adr_rc != 0 ? adr_rc : CBM_PIPELINE_FORCE_FULL_REINDEX; + } + cbm_log_info("pipeline.route", "path", "incremental_manifest"); int rc = cbm_pipeline_run_incremental(p, db_path, files, file_count, baseline_manifest, baseline_count, force_full_on_mismatch); @@ -1747,6 +1773,11 @@ int cbm_pipeline_publish_staged(char *stage_path, const cbm_pipeline_generation_ ok = cbm_store_adr_store(store, generation->project, generation->adr_content) == CBM_STORE_OK; } + + if (ok) { + ok = cbm_store_set_format_version(store, CBM_INDEX_FORMAT_VERSION) == CBM_STORE_OK; + } + cbm_log_info("publish.timing", "block", "writes", "elapsed_ms", itoa_buf((int)elapsed_ms(t_pub))); cbm_clock_gettime(CLOCK_MONOTONIC, &t_pub); diff --git a/src/pipeline/pipeline.h b/src/pipeline/pipeline.h index df684ede5..e54732df5 100644 --- a/src/pipeline/pipeline.h +++ b/src/pipeline/pipeline.h @@ -93,6 +93,8 @@ int cbm_pipeline_get_mode(const cbm_pipeline_t *p); * to NULL/0 when p is NULL or nothing was excluded. Do not free. */ void cbm_pipeline_get_excluded(const cbm_pipeline_t *p, char ***out, int *count); +bool cbm_pipeline_had_format_migration(const cbm_pipeline_t *p); + /* Committed node/edge counts captured at dump time (-1 when dump did not run). * Nodes are the #334 plausibility-gate axis; edges are informational only. */ void cbm_pipeline_get_committed_counts(const cbm_pipeline_t *p, int *nodes, int *edges); diff --git a/src/store/store.c b/src/store/store.c index 015e50ca2..cbb96cc8a 100644 --- a/src/store/store.c +++ b/src/store/store.c @@ -3512,6 +3512,36 @@ int cbm_store_list_files(cbm_store_t *s, const char *project, char ***out, int * return CBM_STORE_OK; } +/* ── Index format version (PRAGMA user_version) ─────────────────── */ + +int cbm_store_get_format_version(cbm_store_t *s, int *out) { + if (!s || !s->db || !out) { + return CBM_STORE_ERR; + } + *out = 0; + sqlite3_stmt *stmt = NULL; + if (sqlite3_prepare_v2(s->db, "PRAGMA user_version;", CBM_NOT_FOUND, &stmt, NULL) != + SQLITE_OK) { + return CBM_STORE_ERR; + } + if (sqlite3_step(stmt) == SQLITE_ROW) { + *out = sqlite3_column_int(stmt, 0); + } + sqlite3_finalize(stmt); + return CBM_STORE_OK; +} + +/* PRAGMA values cannot be bound; format the integer constant in. Writes, so + * only call on a read-write connection (never from configure_pragmas). */ +int cbm_store_set_format_version(cbm_store_t *s, int version) { + if (!s || !s->db) { + return CBM_STORE_ERR; + } + char sql[ST_BUF_64]; + snprintf(sql, sizeof(sql), "PRAGMA user_version = %d;", version); + return exec_sql(s, sql); +} + /* ── Node neighbor names ──────────────────────────────────────── */ static int query_neighbor_names(sqlite3 *db, const char *sql, int64_t node_id, int limit, diff --git a/src/store/store.h b/src/store/store.h index 8c2f76dd8..b2fedd716 100644 --- a/src/store/store.h +++ b/src/store/store.h @@ -23,6 +23,7 @@ typedef struct cbm_store cbm_store_t; #define CBM_STORE_OK 0 #define CBM_STORE_ERR (-1) #define CBM_STORE_NOT_FOUND (-2) +#define CBM_INDEX_FORMAT_VERSION 1 /* ── Data structures ────────────────────────────────────────────── */ @@ -92,6 +93,13 @@ void cbm_store_node_degree(cbm_store_t *s, int64_t node_id, int *in_deg, int *ou * Returns CBM_STORE_OK or CBM_STORE_ERR. */ int cbm_store_list_files(cbm_store_t *s, const char *project, char ***out, int *count); +/* Persisted index-format identity. Bump when a change alters the QN scheme + * or node identity of an already-written graph, so an old DB is routed + * through the full-reindex path instead of producing a mixed graph. + * 1 = File QNs keep the file extension (#769). */ +int cbm_store_get_format_version(cbm_store_t *s, int *out); +int cbm_store_set_format_version(cbm_store_t *s, int version); + /* Get caller/callee names for a node (CALLS/HTTP_CALLS/ASYNC_CALLS edges). * Returns 0 on success. Caller must free each out_callers[i]/out_callees[i] * and the arrays themselves. */ diff --git a/tests/test_fqn.c b/tests/test_fqn.c index d2163424f..5d565f8d8 100644 --- a/tests/test_fqn.c +++ b/tests/test_fqn.c @@ -89,6 +89,77 @@ TEST(fqn_compute_basic_rs) { PASS(); } +TEST(fqn_compute_file_sibling_distinct) { + char *ts = cbm_pipeline_fqn_compute("proj", "app/badge/badge.component.ts", "__file__"); + char *html = cbm_pipeline_fqn_compute("proj", "app/badge/badge.component.html", "__file__"); + char *scss = cbm_pipeline_fqn_compute("proj", "app/badge/badge.component.scss", "__file__"); + ASSERT_NOT_NULL(ts); + ASSERT_NOT_NULL(html); + ASSERT_NOT_NULL(scss); + ASSERT_STR_NEQ(ts, html); + ASSERT_STR_NEQ(ts, scss); + ASSERT_STR_NEQ(html, scss); + free(ts); + free(html); + free(scss); + PASS(); +} + +TEST(fqn_compute_symbol_still_strips) { + ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "app/badge/badge.component.ts", "BadgeComponent"), + "proj.app.badge.badge.component.BadgeComponent"); + PASS(); +} + +TEST(fqn_module_siblings_still_share) { + ASSERT_FQN(cbm_pipeline_fqn_module("proj", "app/badge/badge.component.ts"), + "proj.app.badge.badge.component"); + ASSERT_FQN(cbm_pipeline_fqn_module("proj", "app/badge/badge.component.html"), + "proj.app.badge.badge.component"); + PASS(); +} + +/* #769: exact __file__ QNs for component siblings. */ +TEST(fqn_compute_file_sibling_exact) { + ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "app/badge/badge.component.ts", "__file__"), + "proj.app.badge.badge.component.ts.__file__"); + ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "app/badge/badge.component.html", "__file__"), + "proj.app.badge.badge.component.html.__file__"); + ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "app/badge/badge.component.scss", "__file__"), + "proj.app.badge.badge.component.scss.__file__"); + PASS(); +} + +TEST(fqn_compute_file_multi_extension) { + ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "app/badge/badge.spec.ts", "__file__"), + "proj.app.badge.badge.spec.ts.__file__"); + ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "archive.tar.gz", "__file__"), + "proj.archive.tar.gz.__file__"); + PASS(); +} + +TEST(fqn_compute_file_no_extension) { + ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "Makefile", "__file__"), + "proj.Makefile.__file__"); + ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "LICENSE", "__file__"), + "proj.LICENSE.__file__"); + PASS(); +} + +TEST(fqn_compute_file_dotfile) { + ASSERT_FQN(cbm_pipeline_fqn_compute("proj", ".gitignore", "__file__"), + "proj..gitignore.__file__"); + ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "config/.env", "__file__"), + "proj.config..env.__file__"); + PASS(); +} + +TEST(fqn_compute_file_index_ts) { + ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "app/index.ts", "__file__"), + "proj.app.index.ts.__file__"); + PASS(); +} + /* ── Nested paths ─────────────────────────────────────────────── */ TEST(fqn_compute_nested_two_levels) { @@ -595,6 +666,14 @@ SUITE(fqn) { RUN_TEST(fqn_file_qn_distinguishes_same_stem_header_source_issue964); RUN_TEST(fqn_module_qn_still_strips_extension); RUN_TEST(fqn_compute_basic_rs); + RUN_TEST(fqn_compute_file_sibling_distinct); + RUN_TEST(fqn_compute_symbol_still_strips); + RUN_TEST(fqn_module_siblings_still_share); + RUN_TEST(fqn_compute_file_sibling_exact); + RUN_TEST(fqn_compute_file_multi_extension); + RUN_TEST(fqn_compute_file_no_extension); + RUN_TEST(fqn_compute_file_dotfile); + RUN_TEST(fqn_compute_file_index_ts); /* fqn_compute: nested paths */ RUN_TEST(fqn_compute_nested_two_levels); diff --git a/tests/test_index_format.c b/tests/test_index_format.c new file mode 100644 index 000000000..ceec7b370 --- /dev/null +++ b/tests/test_index_format.c @@ -0,0 +1,249 @@ +/* + * test_index_format.c — Guard for the persisted index-format boundary (#769). + * + * #1108 changed File-node QNs to keep the file extension, so an index written + * before it holds collided File identities: badge.component.ts/.html/.scss all + * stripped to the same stem and only one File node survived per component. + * Refreshing such an index incrementally would mint new-format QNs only for the + * files that happened to change, leaving the old collided node behind — a mixed + * graph with duplicate nodes and stale edges. + * + * A stale-format index must therefore be routed through the existing + * full-reindex path exactly once (which preserves ADR/project metadata per + * #516), and the rebuilt index must not force a second rebuild on the next + * unchanged run. + */ +#include "test_framework.h" +#include "repro_harness.h" /* RProj, rh_index_files, rh_count_label, rh_cleanup */ +#include "foundation/log.h" +#include +#include +#include + +/* ── Fixture: one component's siblings + an uncolliding control ─────── */ + +static const char k_badge_ts[] = "export class BadgeComponent {\n" + " isHighlighted() { return true; } /* repro-marker */\n" + "}\n"; + +static const char k_badge_html[] = "
repro-marker
\n"; + +static const char k_badge_scss[] = ".badge { color: red; /* repro-marker */ }\n"; + +/* help.html shares no stem with anything, so it is searchable both before and + * after the fix — a control that isolates the collision as the cause. */ +static const char k_help_html[] = "

repro-marker

\n"; + +static const RFile k_files[] = { + {"badge/badge.component.ts", k_badge_ts}, + {"badge/badge.component.html", k_badge_html}, + {"badge/badge.component.scss", k_badge_scss}, + {"standalone/help.html", k_help_html}, +}; +static const int k_nfiles = (int)(sizeof(k_files) / sizeof(k_files[0])); + +/* ── Log capture: the routing decision is only visible in the log ───── */ + +enum { IF_LOG_BUF = 8192 }; +static char g_log_buf[IF_LOG_BUF]; +static size_t g_log_len; + +static void capture_sink(const char *line) { + size_t n = strlen(line); + if (g_log_len + n + 2 < sizeof(g_log_buf)) { + memcpy(g_log_buf + g_log_len, line, n); + g_log_len += n; + g_log_buf[g_log_len++] = '\n'; + g_log_buf[g_log_len] = '\0'; + } +} + +static void capture_reset(void) { + g_log_len = 0; + g_log_buf[0] = '\0'; +} + +/* Run index_repository through the production MCP flow, capturing the log. */ +static char *index_capture(RProj *lp) { + char args[700]; + snprintf(args, sizeof(args), "{\"repo_path\":\"%s\"}", lp->tmpdir); + char *prior = getenv("CBM_CACHE_DIR"); + char *saved = prior ? strdup(prior) : NULL; + cbm_setenv("CBM_CACHE_DIR", lp->cachedir, 1); + capture_reset(); + cbm_log_set_sink_ex(capture_sink, CBM_LOG_SINK_TEE); + char *resp = cbm_mcp_handle_tool(lp->srv, "index_repository", args); + cbm_log_set_sink(NULL); + if (saved) { + cbm_setenv("CBM_CACHE_DIR", saved, 1); + free(saved); + } else { + cbm_unsetenv("CBM_CACHE_DIR"); + } + return resp; +} +/* ── Test 1: one File node per file, and every file reaches search ──── */ + +TEST(index_format_siblings_distinct_and_searchable) { + RProj lp; + char args[700]; + cbm_store_t *store = rh_index_files(&lp, k_files, k_nfiles); + ASSERT_NOT_NULL(store); + + /* One File node per file on disk — not one per component stem. */ + ASSERT_EQ(rh_count_label(store, lp.project, "File"), k_nfiles); + + /* cbm_store_list_files is the set search_code scopes grep to: a file with + * no node carrying its exact path is never opened, and the miss is silent. */ + char **listed = NULL; + int nlisted = 0; + ASSERT_EQ(cbm_store_list_files(store, lp.project, &listed, &nlisted), CBM_STORE_OK); + for (int i = 0; i < k_nfiles; i++) { + bool found = false; + for (int j = 0; j < nlisted; j++) { + if (listed[j] && strcmp(listed[j], k_files[i].name) == 0) { + found = true; + break; + } + } + if (!found) { + FAIL("indexed file missing from cbm_store_list_files"); + } + } + for (int j = 0; j < nlisted; j++) { + free(listed[j]); + } + free(listed); + + /* Every sibling's marker is reachable. .scss/.html have no def nodes, so a + * hit lands in raw_matches rather than a containing node — mode="files" + * merges both, so assert against the file list. */ + snprintf(args, sizeof(args), + "{\"project\":\"%s\",\"pattern\":\"repro-marker\",\"mode\":\"files\"}", lp.project); + char *saved_dup = getenv("CBM_CACHE_DIR") ? strdup(getenv("CBM_CACHE_DIR")) : NULL; + cbm_setenv("CBM_CACHE_DIR", lp.cachedir, 1); + char *resp = cbm_mcp_handle_tool(lp.srv, "search_code", args); + if (saved_dup) { + cbm_setenv("CBM_CACHE_DIR", saved_dup, 1); + free(saved_dup); + } else { + cbm_unsetenv("CBM_CACHE_DIR"); + } + ASSERT_NOT_NULL(resp); + for (int i = 0; i < k_nfiles; i++) { + if (!strstr(resp, k_files[i].name)) { + free(resp); + FAIL("search_code did not reach an indexed sibling"); + } + } + free(resp); + + rh_cleanup(&lp, store); + PASS(); +} + +/* ── Test 2: a legacy index rebuilds once, repairs, and settles ─────── */ + +/* Rewrite the graph into the pre-#1108 shape: the three siblings collapsed onto + * a single File node keyed by the extension-stripped QN, which is what such an + * index actually holds on disk. */ +static int make_legacy_file_graph(cbm_store_t *store, const char *project, const char *legacy_qn) { + if (cbm_store_delete_nodes_by_label(store, project, "File") != CBM_STORE_OK) { + return -1; + } + cbm_node_t legacy = { + .project = project, + .label = "File", + .name = "badge.component.html", + .qualified_name = legacy_qn, + .file_path = "badge/badge.component.html", + .properties_json = "{\"extension\":\".html\"}", + }; + return cbm_store_upsert_node(store, &legacy) > 0 ? 0 : -1; +} + +TEST(index_format_legacy_index_rebuilds_and_repairs) { + char legacy_qn[512]; + RProj lp; + + /* Phase 1: index, then discard the query handle. */ + cbm_store_t *q0 = rh_index_files(&lp, k_files, k_nfiles); + ASSERT_NOT_NULL(q0); + cbm_store_close(q0); + + /* Phase 2: fabricate a legacy graph through a writable handle. */ + cbm_store_t *w = cbm_store_open_path(lp.dbpath); + ASSERT_NOT_NULL(w); + ASSERT_EQ(cbm_store_adr_store(w, lp.project, "index-format-adr"), CBM_STORE_OK); + snprintf(legacy_qn, sizeof(legacy_qn), "%s.badge.badge.component.__file__", lp.project); + ASSERT_EQ(make_legacy_file_graph(w, lp.project, legacy_qn), 0); + ASSERT_EQ(cbm_store_set_format_version(w, 0), CBM_STORE_OK); + cbm_store_close(w); + + cbm_store_t *probe = cbm_store_open_path_query(lp.dbpath); + if (probe) + cbm_store_close(probe); + + /* Run 1: stale format forces the full-reindex path. */ + char *resp = index_capture(&lp); + ASSERT_NOT_NULL(resp); + if (!strstr(g_log_buf, "format_change_reindex")) { + free(resp); + FAIL("a stale-format index was not routed through the full-reindex path"); + } + if (!strstr(resp, "\"format_migration\":true")) { + free(resp); + FAIL("index_repository response did not surface format_migration"); + } + free(resp); + + /* Phase 3: verify the rebuild through a fresh read handle. */ + cbm_store_t *r1 = cbm_store_open_path(lp.dbpath); + ASSERT_NOT_NULL(r1); + cbm_node_t stale = {0}; + ASSERT_EQ(cbm_store_find_node_by_qn(r1, lp.project, legacy_qn, &stale), CBM_STORE_NOT_FOUND); + cbm_node_free_fields(&stale); + static const char *k_exts[] = {"ts", "html", "scss"}; + for (int i = 0; i < 3; i++) { + char qn[512]; + snprintf(qn, sizeof(qn), "%s.badge.badge.component.%s.__file__", lp.project, k_exts[i]); + cbm_node_t n = {0}; + ASSERT_EQ(cbm_store_find_node_by_qn(r1, lp.project, qn, &n), CBM_STORE_OK); + cbm_node_free_fields(&n); + } + ASSERT_EQ(rh_count_label(r1, lp.project, "File"), k_nfiles); + cbm_adr_t adr = {0}; + ASSERT_EQ(cbm_store_adr_get(r1, lp.project, &adr), CBM_STORE_OK); + ASSERT_NOT_NULL(adr.content); + ASSERT(strstr(adr.content, "index-format-adr") != NULL); + cbm_store_adr_free(&adr); + int fmt = -1; + ASSERT_EQ(cbm_store_get_format_version(r1, &fmt), CBM_STORE_OK); + ASSERT_EQ(fmt, CBM_INDEX_FORMAT_VERSION); + cbm_store_close(r1); + + /* Run 2: unchanged, current format — no second rebuild. */ + resp = index_capture(&lp); + ASSERT_NOT_NULL(resp); + if (strstr(g_log_buf, "format_change_reindex")) { + free(resp); + FAIL("a current-format index was rebuilt again"); + } + if (strstr(resp, "format_migration")) { + free(resp); + FAIL("format_migration surfaced on an unchanged run"); + } + free(resp); + + /* Phase 4: final check, then clean up (rh_cleanup closes r2). */ + cbm_store_t *r2 = cbm_store_open_path(lp.dbpath); + ASSERT_NOT_NULL(r2); + ASSERT_EQ(rh_count_label(r2, lp.project, "File"), k_nfiles); + rh_cleanup(&lp, r2); + PASS(); +} + +SUITE(index_format) { + RUN_TEST(index_format_siblings_distinct_and_searchable); + RUN_TEST(index_format_legacy_index_rebuilds_and_repairs); +} diff --git a/tests/test_main.c b/tests/test_main.c index ba6f26d45..a8c2939e1 100644 --- a/tests/test_main.c +++ b/tests/test_main.c @@ -719,6 +719,7 @@ extern void suite_pipeline(void); extern void suite_pipeline_semantic_manifest_repro(void); extern void suite_cross_repo(void); extern void suite_index_resilience(void); +extern void suite_index_format(void); extern void suite_fqn(void); extern void suite_route_canon(void); extern void suite_path_alias(void); @@ -984,6 +985,7 @@ int main(int argc, char **argv) { /* Pipeline (M8) */ RUN_SELECTED_SUITE(registry); RUN_SELECTED_SUITE(pipeline); + RUN_SELECTED_SUITE(index_format); RUN_SELECTED_SUITE(pipeline_semantic_manifest_repro); RUN_SELECTED_SUITE(call_reference_contract); RUN_SELECTED_SUITE(call_reference_language_complex_contract);