mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-12 09:47:20 +08:00
Merge upstream/main at 2469ecc3 into integrate-upstream-split-search-tools while retaining branch-specific incremental indexing, MCP API consolidation, and graph extraction behavior.
The staged tree adds upstream's Mojo and Perl grammar support, issue reproduction suites, Graph UI changes, Windows smoke coverage, TOON graph output, subprocess supervision, and Laravel facade route handling. Conflict resolutions in src/pipeline/httplink.c and pass_httplinks.c retain dynamic growth and IMPORTS edge identity; scripts/smoke-test.sh and scripts/soak-test.sh retain hermetic cache isolation.
Validation against the merge plus the follow-up working-tree reconciliation: ASan/UBSan MCP suite 216/216 passed. Follow-up semantic and concurrency fixes remain intentionally unstaged for the next focused commit.
566 lines
20 KiB
C
566 lines
20 KiB
C
/*
|
|
* test_artifact.c — Tests for persistent artifact export/import.
|
|
*/
|
|
#include "test_framework.h"
|
|
#include "store/store.h"
|
|
#include "pipeline/artifact.h"
|
|
#include "pipeline/pipeline.h"
|
|
#include "foundation/compat.h"
|
|
#include "foundation/compat_fs.h"
|
|
#include "foundation/log.h"
|
|
|
|
#include <sys/stat.h>
|
|
#include <stdio.h>
|
|
|
|
/* ── Helpers ─────────────────────────────────────────────────────── */
|
|
|
|
static char g_tmpdir[1024];
|
|
static char g_repo[1024];
|
|
static char g_db[1024];
|
|
enum { ART_TEST_LOG_BUF = 32768 };
|
|
static char g_log_capture[ART_TEST_LOG_BUF];
|
|
static CBMLogLevel g_prev_log_level;
|
|
|
|
static void setup_artifact_test(void) {
|
|
snprintf(g_tmpdir, sizeof(g_tmpdir), "%s/cbm_test_artifact_XXXXXX", cbm_tmpdir());
|
|
cbm_mkdtemp(g_tmpdir);
|
|
|
|
snprintf(g_repo, sizeof(g_repo), "%s/repo", g_tmpdir);
|
|
cbm_mkdir_p(g_repo, 0755);
|
|
|
|
snprintf(g_db, sizeof(g_db), "%s/test.db", g_tmpdir);
|
|
}
|
|
|
|
/* Create a minimal but valid DB with some nodes and edges. */
|
|
static void create_test_db(const char *path) {
|
|
cbm_store_t *s = cbm_store_open_path(path);
|
|
if (!s) {
|
|
return;
|
|
}
|
|
|
|
cbm_store_exec(s, "INSERT OR IGNORE INTO projects(name, indexed_at, root_path) "
|
|
"VALUES('test-proj', '2026-01-01', '/tmp/test');");
|
|
|
|
cbm_store_exec(s, "INSERT INTO nodes(project, label, name, qualified_name, file_path) "
|
|
"VALUES('test-proj', 'Function', 'foo', 'test-proj.foo', 'main.c');");
|
|
cbm_store_exec(s, "INSERT INTO nodes(project, label, name, qualified_name, file_path) "
|
|
"VALUES('test-proj', 'Function', 'bar', 'test-proj.bar', 'main.c');");
|
|
|
|
cbm_store_exec(s, "INSERT INTO edges(project, source_id, target_id, type) "
|
|
"VALUES('test-proj', 1, 2, 'CALLS');");
|
|
|
|
cbm_store_close(s);
|
|
}
|
|
|
|
static void cleanup_dir(const char *path) {
|
|
char cmd[2048];
|
|
snprintf(cmd, sizeof(cmd), "rm -rf '%s'", path);
|
|
(void)system(cmd);
|
|
}
|
|
|
|
static void write_text_file(const char *path, const char *text) {
|
|
FILE *fp = fopen(path, "w");
|
|
if (!fp) {
|
|
return;
|
|
}
|
|
fputs(text, fp);
|
|
fclose(fp);
|
|
}
|
|
|
|
static void capture_log_sink(const char *line) {
|
|
size_t used = strlen(g_log_capture);
|
|
size_t avail = sizeof(g_log_capture) - used;
|
|
if (avail <= 1) {
|
|
return;
|
|
}
|
|
int n = snprintf(g_log_capture + used, avail, "%s\n", line);
|
|
if (n < 0 || (size_t)n >= avail) {
|
|
g_log_capture[sizeof(g_log_capture) - 1] = '\0';
|
|
}
|
|
}
|
|
|
|
static void capture_logs_start(void) {
|
|
g_log_capture[0] = '\0';
|
|
g_prev_log_level = cbm_log_get_level();
|
|
cbm_log_set_level(CBM_LOG_DEBUG);
|
|
cbm_log_set_sink(capture_log_sink);
|
|
}
|
|
|
|
static const char *capture_logs_end(void) {
|
|
cbm_log_set_sink(NULL);
|
|
cbm_log_set_level(g_prev_log_level);
|
|
return g_log_capture;
|
|
}
|
|
|
|
/* ── Tests ───────────────────────────────────────────────────────── */
|
|
|
|
/* Rewrite the "original_size" number in an artifact.json in place, adding
|
|
* `delta` to it. Returns false if the field / a digit run isn't found. */
|
|
static bool bump_artifact_original_size(const char *meta_path, long delta) {
|
|
FILE *fp = fopen(meta_path, "rb");
|
|
if (!fp) {
|
|
return false;
|
|
}
|
|
char buf[4096];
|
|
size_t n = fread(buf, 1, sizeof(buf) - 1, fp);
|
|
fclose(fp);
|
|
buf[n] = '\0';
|
|
char *key = strstr(buf, "\"original_size\"");
|
|
if (!key) {
|
|
return false;
|
|
}
|
|
char *colon = strchr(key, ':');
|
|
if (!colon) {
|
|
return false;
|
|
}
|
|
char *ds = colon + 1;
|
|
while (*ds == ' ' || *ds == '\t') {
|
|
ds++;
|
|
}
|
|
char *de = ds;
|
|
while (*de >= '0' && *de <= '9') {
|
|
de++;
|
|
}
|
|
if (de == ds) {
|
|
return false;
|
|
}
|
|
long val = strtol(ds, NULL, 10) + delta;
|
|
char out[4096];
|
|
int pre = (int)(ds - buf);
|
|
snprintf(out, sizeof(out), "%.*s%ld%s", pre, buf, val, de);
|
|
fp = fopen(meta_path, "wb");
|
|
if (!fp) {
|
|
return false;
|
|
}
|
|
fwrite(out, 1, strlen(out), fp);
|
|
fclose(fp);
|
|
return true;
|
|
}
|
|
|
|
/* The decompressed size is driven by the zstd frame's own content-size header,
|
|
* not the separately-stored original_size field (which travels in plaintext
|
|
* artifact.json and is trivially editable). A mismatch between the two must be
|
|
* rejected — this is the check that keeps the destination allocation and the
|
|
* decoder capacity pinned to the same verified size, so a doctored size can
|
|
* never make the decoder write past the buffer. */
|
|
TEST(artifact_import_rejects_size_mismatch) {
|
|
setup_artifact_test();
|
|
create_test_db(g_db);
|
|
ASSERT_EQ(cbm_artifact_export(g_db, g_repo, "test-proj", CBM_ARTIFACT_FAST), 0);
|
|
|
|
char meta[1024];
|
|
snprintf(meta, sizeof(meta), "%s/.codebase-memory/artifact.json", g_repo);
|
|
ASSERT_TRUE(
|
|
bump_artifact_original_size(meta, 4096)); /* claim 4 KiB more than the frame holds */
|
|
|
|
char import_db[1024];
|
|
snprintf(import_db, sizeof(import_db), "%s/imported.db", g_tmpdir);
|
|
int rc = cbm_artifact_import(g_repo, import_db);
|
|
ASSERT_NEQ(rc, 0); /* must reject the mismatch, not import on the doctored size */
|
|
|
|
cleanup_dir(g_tmpdir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(artifact_export_fast_roundtrip) {
|
|
setup_artifact_test();
|
|
create_test_db(g_db);
|
|
|
|
/* Export with fast quality (zstd -3, no index stripping) */
|
|
int rc = cbm_artifact_export(g_db, g_repo, "test-proj", CBM_ARTIFACT_FAST);
|
|
ASSERT_EQ(rc, 0);
|
|
|
|
/* Verify artifact files exist */
|
|
char zst[1024];
|
|
snprintf(zst, sizeof(zst), "%s/.codebase-memory/graph.db.zst", g_repo);
|
|
struct stat st;
|
|
ASSERT_EQ(stat(zst, &st), 0);
|
|
ASSERT_GT((int)st.st_size, 0);
|
|
|
|
char meta[1024];
|
|
snprintf(meta, sizeof(meta), "%s/.codebase-memory/artifact.json", g_repo);
|
|
ASSERT_EQ(stat(meta, &st), 0);
|
|
|
|
/* Import to a new path */
|
|
char import_db[1024];
|
|
snprintf(import_db, sizeof(import_db), "%s/imported.db", g_tmpdir);
|
|
rc = cbm_artifact_import(g_repo, import_db);
|
|
ASSERT_EQ(rc, 0);
|
|
|
|
/* Verify imported DB has correct data */
|
|
cbm_store_t *s = cbm_store_open_path(import_db);
|
|
ASSERT_NOT_NULL(s);
|
|
int nodes = cbm_store_count_nodes(s, "test-proj");
|
|
int edges = cbm_store_count_edges(s, "test-proj");
|
|
ASSERT_EQ(nodes, 2);
|
|
ASSERT_EQ(edges, 1);
|
|
cbm_store_close(s);
|
|
|
|
cleanup_dir(g_tmpdir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(artifact_export_best_roundtrip) {
|
|
setup_artifact_test();
|
|
create_test_db(g_db);
|
|
|
|
/* Export with best quality (zstd -9, index stripping + VACUUM) */
|
|
int rc = cbm_artifact_export(g_db, g_repo, "test-proj", CBM_ARTIFACT_BEST);
|
|
ASSERT_EQ(rc, 0);
|
|
|
|
/* Source DB should be untouched (VACUUM INTO doesn't modify source) */
|
|
cbm_store_t *src = cbm_store_open_path(g_db);
|
|
ASSERT_NOT_NULL(src);
|
|
ASSERT_EQ(cbm_store_count_nodes(src, "test-proj"), 2);
|
|
cbm_store_close(src);
|
|
|
|
/* Import and verify */
|
|
char import_db[1024];
|
|
snprintf(import_db, sizeof(import_db), "%s/imported.db", g_tmpdir);
|
|
rc = cbm_artifact_import(g_repo, import_db);
|
|
ASSERT_EQ(rc, 0);
|
|
|
|
cbm_store_t *s = cbm_store_open_path(import_db);
|
|
ASSERT_NOT_NULL(s);
|
|
ASSERT_EQ(cbm_store_count_nodes(s, "test-proj"), 2);
|
|
ASSERT_EQ(cbm_store_count_edges(s, "test-proj"), 1);
|
|
cbm_store_close(s);
|
|
|
|
cleanup_dir(g_tmpdir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(artifact_exists_check) {
|
|
setup_artifact_test();
|
|
create_test_db(g_db);
|
|
|
|
/* No artifact yet */
|
|
ASSERT_FALSE(cbm_artifact_exists(g_repo));
|
|
|
|
/* Export creates the artifact */
|
|
cbm_artifact_export(g_db, g_repo, "test-proj", CBM_ARTIFACT_FAST);
|
|
ASSERT_TRUE(cbm_artifact_exists(g_repo));
|
|
|
|
cleanup_dir(g_tmpdir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(artifact_commit_hash) {
|
|
setup_artifact_test();
|
|
create_test_db(g_db);
|
|
|
|
cbm_artifact_export(g_db, g_repo, "test-proj", CBM_ARTIFACT_FAST);
|
|
|
|
/* commit hash may be empty if repo is not a git repo, but should not crash */
|
|
char *commit = cbm_artifact_commit(g_repo);
|
|
/* For a non-git directory, commit will be NULL (git rev-parse HEAD fails) */
|
|
free(commit);
|
|
|
|
cleanup_dir(g_tmpdir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(artifact_schema_version_mismatch) {
|
|
setup_artifact_test();
|
|
create_test_db(g_db);
|
|
cbm_artifact_export(g_db, g_repo, "test-proj", CBM_ARTIFACT_FAST);
|
|
|
|
/* Overwrite artifact.json with incompatible schema version */
|
|
char meta[1024];
|
|
snprintf(meta, sizeof(meta), "%s/.codebase-memory/artifact.json", g_repo);
|
|
FILE *fp = fopen(meta, "w");
|
|
ASSERT_NOT_NULL(fp);
|
|
fprintf(fp, "{\"schema_version\": 999, \"original_size\": 1000}");
|
|
fclose(fp);
|
|
|
|
/* exists should return false for incompatible version */
|
|
ASSERT_FALSE(cbm_artifact_exists(g_repo));
|
|
|
|
/* Import should fail */
|
|
char import_db[1024];
|
|
snprintf(import_db, sizeof(import_db), "%s/imported.db", g_tmpdir);
|
|
int rc = cbm_artifact_import(g_repo, import_db);
|
|
ASSERT_NEQ(rc, 0);
|
|
|
|
cleanup_dir(g_tmpdir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(artifact_import_missing) {
|
|
setup_artifact_test();
|
|
|
|
/* Import from repo without artifact should fail gracefully */
|
|
char import_db[1024];
|
|
snprintf(import_db, sizeof(import_db), "%s/imported.db", g_tmpdir);
|
|
int rc = cbm_artifact_import(g_repo, import_db);
|
|
ASSERT_NEQ(rc, 0);
|
|
|
|
cleanup_dir(g_tmpdir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(artifact_gitattributes_created) {
|
|
setup_artifact_test();
|
|
create_test_db(g_db);
|
|
|
|
cbm_artifact_export(g_db, g_repo, "test-proj", CBM_ARTIFACT_FAST);
|
|
|
|
char ga[1024];
|
|
snprintf(ga, sizeof(ga), "%s/.codebase-memory/.gitattributes", g_repo);
|
|
struct stat st;
|
|
ASSERT_EQ(stat(ga, &st), 0);
|
|
|
|
/* Attribute ORDER is load-bearing: gitattributes apply left to right and
|
|
* the `binary` macro expands to `-diff -merge -text`, so a trailing
|
|
* `binary` unsets a preceding `merge=ours` (git check-attr merge reports
|
|
* "unset" and concurrent artifact refreshes produce binary conflicts
|
|
* instead of auto-resolving). The driver must come after the macro. */
|
|
FILE *gaf = fopen(ga, "r");
|
|
ASSERT_NOT_NULL(gaf);
|
|
char content[512] = {0};
|
|
size_t rd = fread(content, 1, sizeof(content) - 1, gaf);
|
|
(void)fclose(gaf);
|
|
ASSERT_TRUE(rd > 0);
|
|
ASSERT_NOT_NULL(strstr(content, CBM_ARTIFACT_FILENAME " binary merge=ours"));
|
|
ASSERT_TRUE(strstr(content, "merge=ours binary") == NULL);
|
|
|
|
cleanup_dir(g_tmpdir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(artifact_export_rename_failure_logs_specific_error) {
|
|
setup_artifact_test();
|
|
create_test_db(g_db);
|
|
|
|
char art_dir[1024];
|
|
snprintf(art_dir, sizeof(art_dir), "%s/.codebase-memory", g_repo);
|
|
cbm_mkdir_p(art_dir, 0755);
|
|
|
|
char zst[1024];
|
|
snprintf(zst, sizeof(zst), "%s/graph.db.zst", art_dir);
|
|
cbm_mkdir_p(zst, 0755);
|
|
|
|
capture_logs_start();
|
|
int rc = cbm_artifact_export(g_db, g_repo, "test-proj", CBM_ARTIFACT_FAST);
|
|
const char *logs = capture_logs_end();
|
|
|
|
ASSERT_NEQ(rc, 0);
|
|
ASSERT_FALSE(cbm_artifact_exists(g_repo));
|
|
ASSERT_NOT_NULL(cbm_artifact_export_last_error());
|
|
ASSERT(strstr(cbm_artifact_export_last_error(), "write_artifact") != NULL);
|
|
ASSERT(strstr(cbm_artifact_export_last_error(), "rename_temp") != NULL);
|
|
ASSERT(strstr(logs, "msg=artifact.export") != NULL);
|
|
ASSERT(strstr(logs, "stage=write_artifact") != NULL);
|
|
ASSERT(strstr(logs, "err=rename_temp") != NULL);
|
|
|
|
cleanup_dir(g_tmpdir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(pipeline_persistence_export_failure_returns_error) {
|
|
setup_artifact_test();
|
|
|
|
char src[1024];
|
|
snprintf(src, sizeof(src), "%s/main.c", g_repo);
|
|
write_text_file(src, "int main(void) { return 0; }\n");
|
|
|
|
char art_dir[1024];
|
|
snprintf(art_dir, sizeof(art_dir), "%s/.codebase-memory", g_repo);
|
|
cbm_mkdir_p(art_dir, 0755);
|
|
|
|
char zst[1024];
|
|
snprintf(zst, sizeof(zst), "%s/graph.db.zst", art_dir);
|
|
cbm_mkdir_p(zst, 0755);
|
|
|
|
cbm_pipeline_t *p = cbm_pipeline_new(g_repo, g_db, CBM_MODE_FAST);
|
|
ASSERT_NOT_NULL(p);
|
|
cbm_pipeline_set_persistence(p, true);
|
|
|
|
capture_logs_start();
|
|
int rc = cbm_pipeline_run(p);
|
|
const char *logs = capture_logs_end();
|
|
cbm_pipeline_free(p);
|
|
|
|
ASSERT_NEQ(rc, 0);
|
|
ASSERT_FALSE(cbm_artifact_exists(g_repo));
|
|
ASSERT(strstr(logs, "msg=pipeline.err") != NULL);
|
|
ASSERT(strstr(logs, "phase=artifact_export") != NULL);
|
|
|
|
cleanup_dir(g_tmpdir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(artifact_null_safety) {
|
|
ASSERT_NEQ(cbm_artifact_export(NULL, "/tmp", "p", 0), 0);
|
|
ASSERT_NEQ(cbm_artifact_export("/tmp/x.db", NULL, "p", 0), 0);
|
|
ASSERT_NEQ(cbm_artifact_import(NULL, "/tmp/x.db"), 0);
|
|
ASSERT_NEQ(cbm_artifact_import("/tmp", NULL), 0);
|
|
ASSERT_FALSE(cbm_artifact_exists(NULL));
|
|
ASSERT_NULL(cbm_artifact_commit(NULL));
|
|
PASS();
|
|
}
|
|
|
|
/* ── git shell-out path safety ────────────────────────────────────────────────
|
|
*
|
|
* artifact.c shells out to git via cbm_popen with the repo path interpolated into
|
|
* the command. It previously used single quotes (`git -C '%s'`) with NO validation
|
|
* — but cmd.exe does not honor single quotes, so on Windows a repo path with a space
|
|
* broke argument grouping, and an embedded quote/metacharacter could break out of the
|
|
* intended argument entirely. The hardening validates the path and switches to double
|
|
* quotes; cbm_artifact_repo_path_is_shell_safe() is the guard. Rejecting quotes and
|
|
* shell/cmd.exe metacharacters is the contract; spaces must stay allowed (double
|
|
* quotes handle them) — that is the concrete regression the single-quote form caused. */
|
|
TEST(artifact_repo_path_shell_safe_accepts_plain_and_spaced) {
|
|
ASSERT_TRUE(cbm_artifact_repo_path_is_shell_safe("/home/user/repo"));
|
|
ASSERT_TRUE(cbm_artifact_repo_path_is_shell_safe("C:/Users/me/repo"));
|
|
ASSERT_TRUE(cbm_artifact_repo_path_is_shell_safe("/home/user/my repo")); /* space OK */
|
|
PASS();
|
|
}
|
|
|
|
TEST(artifact_repo_path_shell_safe_rejects_injection) {
|
|
ASSERT_FALSE(cbm_artifact_repo_path_is_shell_safe(NULL));
|
|
ASSERT_FALSE(cbm_artifact_repo_path_is_shell_safe("it's")); /* single quote */
|
|
ASSERT_FALSE(cbm_artifact_repo_path_is_shell_safe("a\"b")); /* double quote */
|
|
ASSERT_FALSE(cbm_artifact_repo_path_is_shell_safe("x; rm -rf /")); /* command sep */
|
|
ASSERT_FALSE(cbm_artifact_repo_path_is_shell_safe("$(whoami)")); /* substitution */
|
|
ASSERT_FALSE(cbm_artifact_repo_path_is_shell_safe("a`id`b")); /* backtick */
|
|
ASSERT_FALSE(cbm_artifact_repo_path_is_shell_safe("a|b")); /* pipe */
|
|
PASS();
|
|
}
|
|
|
|
TEST(artifact_repo_path_shell_safe_rejects_cmd_metachars_on_windows) {
|
|
#ifdef _WIN32
|
|
/* cmd.exe expands %VAR%, delayed !VAR!, and escapes with ^ even inside double
|
|
* quotes — git_context.c rejects these on Windows and this must match. */
|
|
ASSERT_FALSE(cbm_artifact_repo_path_is_shell_safe("C:/a%USERPROFILE%b"));
|
|
ASSERT_FALSE(cbm_artifact_repo_path_is_shell_safe("C:/a!b"));
|
|
ASSERT_FALSE(cbm_artifact_repo_path_is_shell_safe("C:/a^b"));
|
|
#else
|
|
/* POSIX shells treat % ! ^ literally inside double quotes — allowed. */
|
|
ASSERT_TRUE(cbm_artifact_repo_path_is_shell_safe("/a%b"));
|
|
ASSERT_TRUE(cbm_artifact_repo_path_is_shell_safe("/a^b"));
|
|
#endif
|
|
PASS();
|
|
}
|
|
|
|
/* #895: the FAST export path (watcher/incremental auto-update) read the
|
|
* raw main-file bytes of a live WAL-mode store — committed rows still in
|
|
* the -wal were missing and mid-checkpoint reads produced torn snapshots
|
|
* that imported as page-corrupted caches. Export must snapshot
|
|
* consistently (VACUUM INTO) on BOTH quality levels. */
|
|
TEST(artifact_fast_export_snapshots_live_wal_store) {
|
|
setup_artifact_test();
|
|
enum { WAL_NODES = 60 };
|
|
|
|
/* Live store: rows committed but NOT checkpointed into the main file —
|
|
* exactly the state the watcher export runs against. */
|
|
cbm_store_t *s = cbm_store_open_path(g_db);
|
|
ASSERT_NOT_NULL(s);
|
|
cbm_store_upsert_project(s, "test-proj", "/tmp/test");
|
|
for (int i = 0; i < WAL_NODES; i++) {
|
|
char name[64];
|
|
char qn[128];
|
|
snprintf(name, sizeof(name), "walnode_%03d", i);
|
|
snprintf(qn, sizeof(qn), "test-proj.mod.%s", name);
|
|
cbm_node_t n = {.project = "test-proj",
|
|
.label = "Function",
|
|
.name = name,
|
|
.qualified_name = qn,
|
|
.file_path = "mod.py",
|
|
.start_line = i + 1,
|
|
.end_line = i + 2};
|
|
ASSERT_TRUE(cbm_store_upsert_node(s, &n) > 0);
|
|
}
|
|
|
|
/* Export WHILE the writer connection is still open. */
|
|
ASSERT_EQ(cbm_artifact_export(g_db, g_repo, "test-proj", CBM_ARTIFACT_FAST), 0);
|
|
cbm_store_close(s);
|
|
|
|
char import_db[1024];
|
|
snprintf(import_db, sizeof(import_db), "%s/imported_wal.db", g_tmpdir);
|
|
ASSERT_EQ(cbm_artifact_import(g_repo, import_db), 0);
|
|
|
|
cbm_store_t *imp = cbm_store_open_path(import_db);
|
|
ASSERT_NOT_NULL(imp);
|
|
/* Torn snapshot = the WAL-resident rows are missing. */
|
|
ASSERT_EQ(cbm_store_count_nodes(imp, "test-proj"), WAL_NODES);
|
|
cbm_store_close(imp);
|
|
PASS();
|
|
}
|
|
|
|
/* #895 (import half): page-level corruption must be refused at import.
|
|
* Schema/index initialization may reject the damaged file during open. If it
|
|
* remains readable enough to open, the deep check's PRAGMA quick_check must
|
|
* reject it. Both are safe fail-closed outcomes; accepting it is not. */
|
|
TEST(store_open_or_deep_integrity_rejects_page_corruption) {
|
|
setup_artifact_test();
|
|
enum { DEEP_NODES = 800, PAGE = 4096, ZERO_PAGES = 10 };
|
|
char db2[1024];
|
|
snprintf(db2, sizeof(db2), "%s/deep.db", g_tmpdir);
|
|
cbm_store_t *s = cbm_store_open_path(db2);
|
|
ASSERT_NOT_NULL(s);
|
|
cbm_store_upsert_project(s, "deep", "/tmp/deep");
|
|
for (int i = 0; i < DEEP_NODES; i++) {
|
|
char name[64];
|
|
char qn[192];
|
|
snprintf(name, sizeof(name), "deep_probe_%04d", i);
|
|
snprintf(qn, sizeof(qn), "deep.rather.long.module.path.for.page.fill.%s_pad_pad_pad",
|
|
name);
|
|
cbm_node_t n = {.project = "deep",
|
|
.label = "Function",
|
|
.name = name,
|
|
.qualified_name = qn,
|
|
.file_path = "deep.py",
|
|
.start_line = i + 1,
|
|
.end_line = i + 2};
|
|
ASSERT_TRUE(cbm_store_upsert_node(s, &n) > 0);
|
|
}
|
|
cbm_store_close(s);
|
|
|
|
/* Healthy file passes the deep check. */
|
|
cbm_store_t *ok = cbm_store_open_path(db2);
|
|
ASSERT_NOT_NULL(ok);
|
|
ASSERT_TRUE(cbm_store_check_integrity_deep(ok));
|
|
cbm_store_close(ok);
|
|
|
|
/* Zero a mid-file band and the deep check must refuse. */
|
|
FILE *f = fopen(db2, "rb+");
|
|
ASSERT_NOT_NULL(f);
|
|
(void)fseek(f, 0, SEEK_END);
|
|
long pages = ftell(f) / PAGE;
|
|
ASSERT_TRUE(pages > ZERO_PAGES + 6);
|
|
char zero[PAGE];
|
|
memset(zero, 0, sizeof(zero));
|
|
(void)fseek(f, (pages / 2) * (long)PAGE, SEEK_SET);
|
|
for (int i = 0; i < ZERO_PAGES; i++) {
|
|
ASSERT_EQ(fwrite(zero, 1, PAGE, f), (size_t)PAGE);
|
|
}
|
|
(void)fclose(f);
|
|
|
|
cbm_store_t *bad = cbm_store_open_path(db2);
|
|
if (bad) {
|
|
ASSERT_FALSE(cbm_store_check_integrity_deep(bad));
|
|
cbm_store_close(bad);
|
|
}
|
|
PASS();
|
|
}
|
|
|
|
SUITE(artifact) {
|
|
RUN_TEST(artifact_fast_export_snapshots_live_wal_store);
|
|
RUN_TEST(store_open_or_deep_integrity_rejects_page_corruption);
|
|
RUN_TEST(artifact_repo_path_shell_safe_accepts_plain_and_spaced);
|
|
RUN_TEST(artifact_repo_path_shell_safe_rejects_injection);
|
|
RUN_TEST(artifact_repo_path_shell_safe_rejects_cmd_metachars_on_windows);
|
|
RUN_TEST(artifact_export_fast_roundtrip);
|
|
RUN_TEST(artifact_export_best_roundtrip);
|
|
RUN_TEST(artifact_exists_check);
|
|
RUN_TEST(artifact_commit_hash);
|
|
RUN_TEST(artifact_schema_version_mismatch);
|
|
RUN_TEST(artifact_import_missing);
|
|
RUN_TEST(artifact_gitattributes_created);
|
|
RUN_TEST(artifact_export_rename_failure_logs_specific_error);
|
|
RUN_TEST(pipeline_persistence_export_failure_returns_error);
|
|
RUN_TEST(artifact_import_rejects_size_mismatch);
|
|
RUN_TEST(artifact_null_safety);
|
|
}
|