diff --git a/Makefile.cbm b/Makefile.cbm index bfa0ab46a..a911b250d 100644 --- a/Makefile.cbm +++ b/Makefile.cbm @@ -412,7 +412,10 @@ TEST_REPRO_SRCS = \ tests/repro/repro_issue333.c \ tests/repro/repro_issue570.c \ tests/repro/repro_issue409.c \ - tests/repro/repro_issue431.c + tests/repro/repro_issue431.c \ + tests/repro/repro_issue607.c \ + tests/repro/repro_issue403.c \ + tests/repro/repro_issue434.c ALL_TEST_SRCS =$(TEST_FOUNDATION_SRCS) $(TEST_EXTRACTION_SRCS) $(TEST_STORE_SRCS) $(TEST_CYPHER_SRCS) $(TEST_MCP_SRCS) $(TEST_DISCOVER_SRCS) $(TEST_GRAPH_BUFFER_SRCS) $(TEST_PIPELINE_SRCS) $(TEST_WATCHER_SRCS) $(TEST_LZ4_SRCS) $(TEST_ZSTD_SRCS) $(TEST_ARTIFACT_SRCS) $(TEST_SQLITE_WRITER_SRCS) $(TEST_GO_LSP_SRCS) $(TEST_C_LSP_SRCS) $(TEST_PHP_LSP_SRCS) $(TEST_CS_LSP_SRCS) $(TEST_CS_LSP_BENCH_SRCS) $(TEST_SCOPE_SRCS) $(TEST_TYPE_REP_SRCS) $(TEST_PY_LSP_SRCS) $(TEST_PY_LSP_BENCH_SRCS) $(TEST_PY_LSP_STRESS_SRCS) $(TEST_PY_LSP_SCALE_SRCS) $(TEST_TS_LSP_SRCS) $(TEST_JAVA_LSP_SRCS) $(TEST_KOTLIN_LSP_SRCS) $(TEST_RUST_LSP_SRCS) $(TEST_TRACES_SRCS) $(TEST_CLI_SRCS) $(TEST_MEM_SRCS) $(TEST_UI_SRCS) $(TEST_HTTPD_SRCS) $(TEST_SECURITY_SRCS) $(TEST_YAML_SRCS) $(TEST_SIMHASH_SRCS) $(TEST_STACK_OVERFLOW_SRCS) $(TEST_INTEGRATION_SRCS) diff --git a/tests/repro/repro_issue403.c b/tests/repro/repro_issue403.c new file mode 100644 index 000000000..09f4e4bbe --- /dev/null +++ b/tests/repro/repro_issue403.c @@ -0,0 +1,159 @@ +/* + * repro_issue403.c -- Reproduce-first case for OPEN bug #403. + * + * Issue: #403 -- "The IDE's installation directory is unnecessarily indexed" + * https://github.com/DeusData/codebase-memory-mcp/issues/403 + * + * Wrongly-indexed directory: AppData/Local/Programs/Antigravity + * (the Antigravity IDE install tree; reported name confirmed in issue comments) + * + * Root cause (src/discover/discover.c): + * cbm_should_skip_dir() (line 339) tests only the BARE directory name + * (entry->name, the last path component) against ALWAYS_SKIP_DIRS and + * FAST_SKIP_DIRS. None of "AppData", "Local", "Programs", or "Antigravity" + * appears in either list. Therefore cbm_discover() walks straight into the + * IDE install tree and indexes every source-like file it contains. + * + * There is no install-directory guard at ANY layer: + * - ALWAYS_SKIP_DIRS covers VCS, build tools, and caches -- not IDE + * install prefixes (Programs, AppData/Local/Programs, etc.). + * - The .gitignore path is only loaded when a .git directory is present + * (is_git_repo gate, line 777 of discover.c). An IDE install dir does + * not contain .git, so .gitignore exclusions never fire. + * - The cbmignore path (opts->ignore_file or .cbmignore at root) is + * similarly absent from an install dir by default. + * Result: any source-extension file found under Antigravity/ is returned + * as a discovered file, bloating the graph with IDE internals. + * + * Expected (correct) behaviour: + * When cbm_discover() is called on a directory that contains an + * "Antigravity" subdirectory (or more generally any IDE install subtree), + * files under that subdirectory must NOT appear in the discovered file list. + * The correct fix (per the issue owner's comment) is to add "Antigravity" + * (and the broader "Programs" / install-dir pattern) to the exclusion layer, + * OR to extend the exclusion to root-path patterns so auto-index never picks + * an install dir as a project root in the first place. + * + * Actual (buggy) behaviour: + * cbm_discover() returns files under Antigravity/ as normal discovered + * files because the bare dirname "Antigravity" is absent from ALWAYS_SKIP_DIRS. + * + * Why RED on current code: + * The fixture creates a temp dir with: + * normal.py -- a legitimate source file (control: MUST appear) + * Antigravity/ide.py -- sentinel inside the IDE install dir (MUST NOT appear) + * cbm_discover() is called on the temp dir. The loop below asserts that + * ide.py is NOT in the result. On current code "Antigravity" is not skipped, + * so ide.py IS discovered and the ASSERT_FALSE fires RED. + * + * Fix location (not implemented here): + * src/discover/discover.c, ALWAYS_SKIP_DIRS array: + * Add "Antigravity" (and any other IDE install dir names to be excluded) + * to the NULL-terminated list. The broader fix is to extend the list with + * install-path components ("Programs", "AppData") or, per the issue owner, + * to implement a root-path exclusion in the auto-index root-selection logic + * so directories under AppData/Local/Programs are never chosen as repo roots. + * + * Exclusion is NOT config-driven in the current code. The closest knob is a + * .cbmignore file at the repo root (loaded unconditionally, unlike .gitignore + * which requires .git/). Passing opts->ignore_file also works. However, + * neither is set in this test -- we assert on the default behaviour, which is + * what the bug reporter experiences. + */ + +#include +#include "test_framework.h" +#include "test_helpers.h" +#include "discover/discover.h" + +#include +#include +#include + +/* ── Fixture ──────────────────────────────────────────────────────────────── + * + * Directory layout (NOT a git repo -- no .git/ subdir): + * + * / + * normal.py <- legitimate source file; MUST be discovered + * Antigravity/ + * ide.py <- sentinel inside IDE install dir; must NOT appear + * + * cbm_discover() is called on with no opts (NULL) so all default + * exclusions apply and no extra ignore file is consulted. + * + * Control assertion (expected GREEN even on buggy code): + * normal.py IS in the result -- proves discovery ran at all. + * + * Primary assertion (RED on buggy code): + * ide.py is NOT in the result -- the Antigravity subtree was skipped. + */ + +TEST(repro_issue403_install_dir_excluded) { + /* --- set up temp directory --- */ + char tmpdir[256]; + snprintf(tmpdir, sizeof(tmpdir), "%s/cbm_repro403_XXXXXX", cbm_tmpdir()); + ASSERT_NOT_NULL(cbm_mkdtemp(tmpdir)); + + /* Control file: a normal Python source at the repo root. */ + ASSERT_EQ(0, th_write_file(TH_PATH(tmpdir, "normal.py"), + "def hello(): return 1\n")); + + /* Sentinel file: a Python source inside the Antigravity install dir. + * This is the file that MUST be absent from discovery results. + * th_write_file creates intermediate directories automatically. */ + ASSERT_EQ(0, th_write_file(TH_PATH(tmpdir, "Antigravity/ide.py"), + "# Antigravity IDE internal module\ndef _internal(): pass\n")); + + /* --- Run discovery (default opts: no .git, no .cbmignore, no opts) --- */ + cbm_file_info_t *files = NULL; + int count = 0; + int rc = cbm_discover(tmpdir, NULL, &files, &count); + ASSERT_EQ(0, rc); + + /* --- Scan results --- */ + bool normal_found = false; + bool ide_file_found = false; + for (int i = 0; i < count; i++) { + if (strcmp(files[i].rel_path, "normal.py") == 0) { + normal_found = true; + } + /* Match any path that descends into the Antigravity directory. */ + if (strncmp(files[i].rel_path, "Antigravity/", 12) == 0 || + strcmp(files[i].rel_path, "Antigravity") == 0) { + ide_file_found = true; + printf(" BUG #403 reproduced: IDE install-dir file indexed: %s\n", + files[i].rel_path); + } + } + + cbm_discover_free(files, count); + th_rmtree(tmpdir); + + /* Control: normal.py must be discovered -- discovery ran correctly. */ + ASSERT_TRUE(normal_found); + + /* + * PRIMARY assertion (RED on buggy code): + * + * No file under Antigravity/ may appear in the discovered set. + * On current code, "Antigravity" is absent from ALWAYS_SKIP_DIRS so + * cbm_should_skip_dir("Antigravity", ...) returns false and the walk + * descends into it. ide.py is discovered, ide_file_found is true, and + * this ASSERT_FALSE fires RED. + * + * After the fix -- "Antigravity" added to ALWAYS_SKIP_DIRS (or an + * equivalent install-path exclusion applied) -- cbm_should_skip_dir + * returns true, the subtree is skipped, ide_file_found stays false, + * and this assertion passes GREEN. + */ + ASSERT_FALSE(ide_file_found); + + PASS(); +} + +/* ── Suite ──────────────────────────────────────────────────────────────── */ + +SUITE(repro_issue403) { + RUN_TEST(repro_issue403_install_dir_excluded); +} diff --git a/tests/repro/repro_issue434.c b/tests/repro/repro_issue434.c new file mode 100644 index 000000000..307b7e45d --- /dev/null +++ b/tests/repro/repro_issue434.c @@ -0,0 +1,166 @@ +/* + * repro_issue434.c - Reproduce-first case for OPEN bug #434. + * + * Issue: #434 - "cursor | vscode : persistence=true is silently ignored on + * first artifact creation" + * + * Root cause: + * In src/pipeline/pipeline_incremental.c, the static function + * dump_and_persist() (around line 668) auto-exports the artifact only when + * one ALREADY exists on disk: + * + * if (repo_path && cbm_artifact_exists(repo_path)) { + * cbm_artifact_export(db_path, repo_path, project, CBM_ARTIFACT_FAST); + * } + * + * It never consults p->persistence. So when index_repository is called with + * persistence=true for the FIRST time (no prior artifact), the incremental + * path skips the export entirely. The full-pipeline path in pipeline.c + * correctly gates on p->persistence (line 933: if (p->persistence) {...}), + * but cbm_pipeline_run_incremental() calls the local dump_and_persist() + * which only checks cbm_artifact_exists(), not the pipeline flag. + * + * The MCP handler in mcp.c (line 2794) further exposes the symptom: + * if (persistence && has_artifact) { ... artifact_hint ... } + * This condition can never be true on a first run because has_artifact is + * checked AFTER the incremental path ran and produced no artifact. + * + * Expected (correct) behaviour: + * Calling index_repository with persistence=true on a repo that has no + * prior artifact MUST create .codebase-memory/graph.db.zst after the run. + * cbm_artifact_exists(repo_path) MUST return true after the first + * persistence=true index, not only after a second run. + * + * Actual (buggy) behaviour: + * After the first persistence=true call on a fresh repo, no artifact is + * written. cbm_artifact_exists() returns false. Only a SECOND call (when + * the artifact now exists from a prior run) writes the file. + * + * Why RED on current code: + * We call index_repository once with persistence=true on a fresh fixture + * repo (no prior artifact). We then assert cbm_artifact_exists() returns + * true. On buggy code dump_and_persist() skips the export because + * cbm_artifact_exists() was false at the time of the check, so the + * assertion fires RED. + * + * Fix location (not implemented here): + * src/pipeline/pipeline_incremental.c, dump_and_persist(): + * The function must accept (or read) the pipeline persistence flag and + * call cbm_artifact_export() when persistence=true, regardless of whether + * an artifact already exists. The existing auto-update branch should be + * merged with a new persistence-flag branch so that: + * if (repo_path && (persistence || cbm_artifact_exists(repo_path))) { + * cbm_artifact_export(...); + * } + * The pipeline struct's persistence field must be threaded through to + * dump_and_persist() (currently it is not passed at all). + */ + +#include "test_framework.h" +#include "repro_harness.h" +#include +#include +#include + +#include +#include +#include +#include + +/* ── Test ────────────────────────────────────────────────────────────────── */ + +TEST(repro_issue434_persistence_honored_on_first_create) { + /* Set up a minimal fixture repo with one C file so the pipeline has + * something to index. We go through the MCP index_repository tool + * (the production path) so the persistence flag travels through + * cbm_mcp_get_bool_arg -> cbm_pipeline_set_persistence -> the pipeline. */ + RProj lp; + memset(&lp, 0, sizeof(lp)); + + /* Create a fresh temp directory for the fixture repo */ + snprintf(lp.tmpdir, sizeof(lp.tmpdir), "/tmp/cbm_repro434_XXXXXX"); + if (!cbm_mkdtemp(lp.tmpdir)) + FAIL("cbm_mkdtemp failed"); + + /* Write a minimal C source file so discovery finds something */ + char src_path[512]; + snprintf(src_path, sizeof(src_path), "%s/main.c", lp.tmpdir); + FILE *fp = fopen(src_path, "w"); + if (!fp) { + th_rmtree(lp.tmpdir); + FAIL("fopen main.c failed"); + } + fputs("int main(void) { return 0; }\n", fp); + fclose(fp); + + /* Verify: NO artifact exists before the first run */ + ASSERT_FALSE(cbm_artifact_exists(lp.tmpdir)); + + /* Build the MCP JSON args with persistence=true */ + char args[700]; + snprintf(args, sizeof(args), + "{\"repo_path\":\"%s\",\"persistence\":true}", lp.tmpdir); + + /* Create an MCP server and run index_repository with persistence=true. + * This is the exact production code path that Cursor/VSCode calls. */ + lp.srv = cbm_mcp_server_new(NULL); + if (!lp.srv) { + th_rmtree(lp.tmpdir); + FAIL("cbm_mcp_server_new failed"); + } + + char *resp = cbm_mcp_handle_tool(lp.srv, "index_repository", args); + if (resp) + free(resp); + + /* + * RED assertion: after a FIRST index_repository call with persistence=true + * the artifact MUST exist in .codebase-memory/graph.db.zst. + * + * On buggy code (pipeline_incremental.c dump_and_persist only checks + * cbm_artifact_exists() not p->persistence) the artifact is NOT written + * on the first run, so cbm_artifact_exists() returns false here and this + * ASSERT fires RED — that is the reproduce-first deliverable. + * + * On fixed code the assertion will be GREEN (persistence=true creates + * the artifact even when no prior artifact existed). + */ + bool artifact_created = cbm_artifact_exists(lp.tmpdir); + + /* Derive project name before rmtree (still valid as a string after rmtree, + * but cleaner to resolve while the directory exists) */ + char *proj = cbm_project_name_from_path(lp.tmpdir); + + /* Cleanup before asserting so temp files are always removed */ + if (lp.srv) { + cbm_mcp_server_free(lp.srv); + lp.srv = NULL; + } + + /* Remove the artifact dir and the fixture repo */ + char art_dir[600]; + snprintf(art_dir, sizeof(art_dir), "%s/.codebase-memory", lp.tmpdir); + th_rmtree(art_dir); + th_rmtree(lp.tmpdir); + + /* Clean up the cache DB the pipeline wrote */ + if (proj) { + const char *home = getenv("HOME"); + if (!home) home = "/tmp"; + char dbpath[600]; + snprintf(dbpath, sizeof(dbpath), "%s/.cache/codebase-memory-mcp/%s.db", + home, proj); + unlink(dbpath); + free(proj); + } + + ASSERT_TRUE(artifact_created); + + PASS(); +} + +/* ── Suite ──────────────────────────────────────────────────────────────── */ + +SUITE(repro_issue434) { + RUN_TEST(repro_issue434_persistence_honored_on_first_create); +} diff --git a/tests/repro/repro_issue607.c b/tests/repro/repro_issue607.c new file mode 100644 index 000000000..9ae143a43 --- /dev/null +++ b/tests/repro/repro_issue607.c @@ -0,0 +1,258 @@ +/* + * repro_issue607.c -- Reproduce-first case for OPEN bug #607. + * + * Issue #607: "installing again via install script is dark pattern: + * 'rebuild index' message followed by delete index action" + * + * DESTROYING CODE PATH: + * src/cli/cli.c cbm_cmd_install() lines 3434-3446 + * src/cli/cli.c cbm_remove_indexes() lines 2440-2468 + * + * The sequence is: + * 1. cbm_cmd_install() at line 3434 calls count_db_indexes() -- finds at + * least one .db file in the cache dir. + * 2. Line 3436 prints: + * "Found %d existing index(es) that must be rebuilt:\n" + * The word "rebuilt" implies the index data will be preserved and + * re-ingested. The user consents on this basis. + * 3. Line 3444 calls cbm_remove_indexes(home), which in turn calls + * cbm_unlink(path) at line 2462 for every .db file it finds. + * No rename, no backup, no re-index is triggered. The data is gone. + * 4. cbm_install_agent_configs() is called (Step 3 of install) -- this + * does NOT touch the store and does NOT rebuild the index. + * 5. The user is left with an empty cache and must re-run + * index_repository to recover, with no warning that data was lost. + * + * ROOT CAUSE: + * The prompt message says "rebuilt" but the action is "delete". There is no + * rebuild triggered anywhere in cbm_cmd_install() after the delete. The UI + * contract ("rebuilt") is broken by the implementation ("deleted"). + * + * See also related bug #557 (mcp.c resolve_store unlink on corrupt detection) + * which shares the same pattern of silent, irrecoverable data loss. + * + * EXPECTED (correct) behaviour: + * After cbm_cmd_install() runs its reinstall path: + * (a) the index DB MUST still exist at its original path -- OR -- + * (b) a rebuild (full re-ingestion) is triggered automatically so the + * user ends up with an equivalent index without data loss. + * The index MUST NOT be silently deleted with no recovery. + * + * ACTUAL (buggy) behaviour: + * cbm_remove_indexes() at cli.c:2444 calls cbm_unlink() on every .db + * file in the cache dir. After cbm_cmd_install() returns, access(db_path, + * F_OK) returns -1 (ENOENT). The index is gone. No rebuild runs. + * + * WHY RED on current code: + * The final ASSERT_TRUE checks that the index DB still exists after + * reinstall. On buggy code cbm_remove_indexes() removes it and no rebuild + * creates a new one, so access() returns ENOENT and the assertion fires RED. + * + * TRIGGER: + * We drive the specific C functions that perform the destroy path, with + * CBM_CACHE_DIR redirected to a temp dir so the real user cache is never + * touched and the exact db_path is known before the call. + * + * We do NOT call cbm_cmd_install() directly -- that function reads HOME, + * prompts the user via prompt_yn(), and copies a real binary. Instead we + * call the two functions that embody the dangerous sequence: + * + * cbm_list_indexes() -- produces the "must be rebuilt" message + * cbm_remove_indexes() -- destroys the index (the bug) + * + * This isolates the destroy path without triggering I/O on /proc/self/exe, + * editor config detection, or interactive prompts. + * + * FIX LOCATION (not implemented here): + * src/cli/cli.c cbm_cmd_install() around lines 3436/3444: + * Either (a) change the prompt to say "delete" not "rebuild" AND trigger + * a real rebuild after install, or (b) preserve the DB and trigger an + * incremental re-index rather than deleting. The message and the action + * must agree; currently they do not. + */ + +#include +#include "test_framework.h" + +#include +#include + +#include +#include +#include +#include +#include + +/* ── Helper: check whether a file exists ─────────────────────────── */ + +static int file_exists_607(const char *path) { + struct stat st; + return (stat(path, &st) == 0) ? 1 : 0; +} + +/* ── Test ───────────────────────────────────────────────────────────── + * + * repro_issue607_reinstall_preserves_index + * + * Setup (preconditions that must be GREEN to prove the harness is correct): + * 1. CBM_CACHE_DIR is redirected to a temp dir. + * 2. A project DB is created at /cbm-repro607-test.db via + * cbm_store_open_path() and closed immediately. + * 3. The DB file is confirmed to exist (precondition). + * + * The dangerous sequence under test: + * cbm_list_indexes() -- the "must be rebuilt" message path + * cbm_remove_indexes() -- the actual destroy path (the bug) + * + * The failing assertion (RED on buggy code): + * After cbm_remove_indexes() runs, the DB must still exist at db_path. + * On buggy code: cbm_unlink() at cli.c:2462 removes the file. + * db_exists == 0, ASSERT_TRUE fires -- RED. + * ─────────────────────────────────────────────────────────────────── */ + +#define REPRO607_PROJECT "cbm-repro607-test" + +TEST(repro_issue607_reinstall_preserves_index) { + /* ── Step 1: redirect CBM_CACHE_DIR to a temp dir ───────────────── + * + * cbm_resolve_cache_dir() (platform.c:404) checks CBM_CACHE_DIR first. + * Pointing it at a fresh temp dir ensures: + * - the test DB is isolated from the user real cache + * - count_db_indexes() / cbm_remove_indexes() see exactly the DB we + * create here and nothing else + */ + char tmp_cache[512]; + snprintf(tmp_cache, sizeof(tmp_cache), "/tmp/cbm_repro607_XXXXXX"); + if (!cbm_mkdtemp(tmp_cache)) { + ASSERT_NOT_NULL(NULL); /* marks setup failure clearly */ + } + +#if defined(_WIN32) + char ev[600]; + snprintf(ev, sizeof(ev), "CBM_CACHE_DIR=%s", tmp_cache); + _putenv(ev); +#else + setenv("CBM_CACHE_DIR", tmp_cache, 1 /* overwrite */); +#endif + + /* ── Step 2: build the DB path we will inspect ──────────────────── + * + * cbm_remove_indexes() (cli.c:2440) iterates over the cache dir and + * unlinks every file ending in ".db". Mirror the path formula so + * db_path matches what cbm_remove_indexes() will delete. + */ + char db_path[700]; + snprintf(db_path, sizeof(db_path), "%s/%s.db", tmp_cache, REPRO607_PROJECT); + + /* ── Step 3: create a real index DB at db_path ──────────────────── + * + * cbm_store_open_path() (store.h:199) creates the DB file with full + * schema (projects table, nodes table, etc.). We insert one project + * row so the DB is non-empty, then close it. This simulates the state + * a real user would have after running index_repository once. + */ + cbm_store_t *setup_store = cbm_store_open_path(db_path); + ASSERT_NOT_NULL(setup_store); /* precondition: store creation must work */ + + int upsert_rc = cbm_store_upsert_project(setup_store, + REPRO607_PROJECT, + "/home/user/my-project"); + ASSERT_EQ(upsert_rc, CBM_STORE_OK); /* precondition: row must be written */ + + cbm_store_close(setup_store); + setup_store = NULL; + + /* ── Step 4: verify the DB exists before triggering the danger path */ + ASSERT_TRUE(file_exists_607(db_path)); /* precondition: DB must exist now */ + + /* ── Step 5: run the install "rebuild" message path ──────────────── + * + * cbm_list_indexes() (cli.c:2416) is the function whose output + * contains "must be rebuilt" (the dark-pattern message). We call it + * first to mirror the exact sequence inside cbm_cmd_install(): + * + * line 3436: printf("Found %d existing index(es) that must be rebuilt:\n", ...) + * line 3437: cbm_list_indexes(home); <-- message visible to user + * ... + * line 3444: cbm_remove_indexes(home); <-- silent destroy + * + * We pass tmp_cache as a fake home_dir. Because get_cache_dir() calls + * cbm_resolve_cache_dir() which reads CBM_CACHE_DIR, the home_dir + * argument is not used in the path -- the env var takes precedence. + * We pass it anyway to match the real call signature. + */ + int listed = cbm_list_indexes(tmp_cache /* fake home */); + ASSERT_GT(listed, 0); /* precondition: must see our DB before removing */ + + /* ── Step 6: run the destroying path ────────────────────────────── + * + * cbm_remove_indexes() (cli.c:2440) is the function that performs the + * actual unlink. This is what cbm_cmd_install() calls at line 3444 + * after the user has been told the index will be "rebuilt": + * + * int removed = cbm_remove_indexes(home); // cli.c:3444 + * + * cbm_remove_indexes() iterates the cache dir and calls + * cbm_unlink(path) at cli.c:2462 for every .db file it finds. + * It returns the count of files successfully unlinked. + * + * On buggy code this removes db_path permanently. + * On correct code the DB would be preserved (or a rebuild triggered). + */ + (void)cbm_remove_indexes(tmp_cache /* fake home */); + + /* ── Step 7: PRIMARY ASSERTION -- the index DB must still exist ──── + * + * Correct behaviour: cbm_cmd_install() tells the user the index will be + * "rebuilt", so EITHER: + * (a) the DB was never deleted (preserved in-place), OR + * (b) a new DB was created at db_path as a result of the rebuild. + * + * In either case, access(db_path, F_OK) must succeed after install. + * + * WHY RED on buggy code: + * cbm_remove_indexes() calls cbm_unlink(db_path) at cli.c:2462. + * No rebuild runs inside cbm_cmd_install() after the delete. + * file_exists_607(db_path) returns 0. + * ASSERT_TRUE(0) fires -- RED. + * + * WHY GREEN on fixed code: + * Either cbm_remove_indexes() no longer unlinks the DB (it renames or + * preserves it) OR cbm_cmd_install() triggers a rebuild that creates + * a fresh DB at db_path before returning. file_exists_607() returns 1. + */ + int db_exists = file_exists_607(db_path); + + /* Clean up temp dir (best effort -- before the assertion so the dir + * is removed even when the assertion fails and longjmp unwinds). */ + unlink(db_path); + char wal[730], shm[730]; + snprintf(wal, sizeof(wal), "%s-wal", db_path); + snprintf(shm, sizeof(shm), "%s-shm", db_path); + unlink(wal); + unlink(shm); + rmdir(tmp_cache); + +#if defined(_WIN32) + _putenv("CBM_CACHE_DIR="); +#else + unsetenv("CBM_CACHE_DIR"); +#endif + + /* + * THE KEY ASSERTION -- must be RED on unpatched code: + * + * The user was told the index would be REBUILT. After the install code + * path runs, the index DB must still exist (preserved or rebuilt). + * On buggy code cbm_remove_indexes() deleted it and no rebuild ran: + * db_exists == 0 and ASSERT_TRUE fires RED. + */ + ASSERT_TRUE(db_exists); + + PASS(); +} + +/* ── Suite ─────────────────────────────────────────────────────────── */ +SUITE(repro_issue607) { + RUN_TEST(repro_issue607_reinstall_preserves_index); +} diff --git a/tests/repro/repro_main.c b/tests/repro/repro_main.c index d3d739b75..5f3abdac8 100644 --- a/tests/repro/repro_main.c +++ b/tests/repro/repro_main.c @@ -47,6 +47,9 @@ extern void suite_repro_issue333(void); extern void suite_repro_issue570(void); extern void suite_repro_issue409(void); extern void suite_repro_issue431(void); +extern void suite_repro_issue607(void); +extern void suite_repro_issue403(void); +extern void suite_repro_issue434(void); int main(void) { /* Unbuffered: a reproduction may crash/_exit (or a sanitizer may _exit on a @@ -81,6 +84,9 @@ int main(void) { RUN_SUITE(repro_issue570); RUN_SUITE(repro_issue409); RUN_SUITE(repro_issue431); + RUN_SUITE(repro_issue607); + RUN_SUITE(repro_issue403); + RUN_SUITE(repro_issue434); TEST_SUMMARY(); }