mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
- #607 (RED, DATA LOSS): cbm_cmd_install prints 'must be rebuilt' then calls cbm_remove_indexes() which unlink()s every .db in the cache and never rebuilds — re-running install destroys the user's index. Asserts the DB survives. - #403 (RED): cbm_should_skip_dir excludes neither Antigravity / Programs / AppData, and the .gitignore path is git-gated, so an IDE install tree is fully indexed. Asserts a sentinel under the install dir is not discovered. - #434 (RED): the incremental dump_and_persist only re-exports the artifact when one already exists and never receives the persistence flag, so persistence=true is silently dropped on first index. Asserts the artifact exists after one index_repository with persistence=true. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
1 parent
ed15d0611a
commit
806da45896
5 files changed
+593
-1
No files matched your search
+4
-1
@@ -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)
|
||||
|
||||
|
||||
@@ -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 <foundation/compat.h>
|
||||
#include "test_framework.h"
|
||||
#include "test_helpers.h"
|
||||
#include "discover/discover.h"
|
||||
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
|
||||
/* ── Fixture ────────────────────────────────────────────────────────────────
|
||||
*
|
||||
* Directory layout (NOT a git repo -- no .git/ subdir):
|
||||
*
|
||||
* <tmpdir>/
|
||||
* normal.py <- legitimate source file; MUST be discovered
|
||||
* Antigravity/
|
||||
* ide.py <- sentinel inside IDE install dir; must NOT appear
|
||||
*
|
||||
* cbm_discover() is called on <tmpdir> 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);
|
||||
}
|
||||
@@ -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 <pipeline/artifact.h>
|
||||
#include <foundation/compat.h>
|
||||
#include <foundation/compat_fs.h>
|
||||
|
||||
#include <sys/stat.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
|
||||
/* ── 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);
|
||||
}
|
||||
@@ -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 <foundation/compat.h>
|
||||
#include "test_framework.h"
|
||||
|
||||
#include <cli/cli.h>
|
||||
#include <store/store.h>
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
/* ── 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 <cache_dir>/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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
Reference in new issue
Block a user