Files
codebase-memory-mcp/tests/test_store_graph_digest.c
Martin Vogel 6a29a19c23 fix(test-impact): build and run on Linux GCC, git before 2.44, LeakSanitizer
PR CI on the first push of these commits found what no local leg had
built yet:

- test_impact_result_validate.c: initialize the count GCC's
  -Wmaybe-uninitialized flags (-Werror on every Linux build: smoke,
  shadow, analyze).
- tests: split one-line `if` chains GCC reports as misleading
  indentation, and drop a redundant `= {0}` it reads as missing braces
  (the GCC test runner did not compile).
- tests/test_test_impact_engine.c: realpath() into a PATH_MAX buffer;
  glibc's _FORTIFY_SOURCE aborts on anything smaller, as
  test_index_resilience.c already notes.
- test_impact_git.c: pass --no-lazy-fetch only when the git executable
  takes it (git 2.44+, probed once per session with the option on
  --version). Older git rejected it and every engine command failed;
  Ubuntu 24.04 ships git 2.43. `-c protocol.allow=never` already turns a
  lazy fetch into an error there, which is what the option asks for.
- test_impact_engine.c: the selection and publish job threads release
  their thread-local node-type bitset cache before they end, as the
  parallel workers and the MCP index thread do. LeakSanitizer reported it
  (488 bytes in 13 allocations, test-diag); the Linux arm64 ASan leg
  reproduced exactly that and is clean with the fix.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-05 23:55:25 +02:00

611 lines
42 KiB
C

#include "test_framework.h"
#include "test_helpers.h"
#include <store/store_graph_digest.h>
#include <foundation/compat_thread.h>
#include <sqlite3.h>
#include <limits.h>
#include <stdatomic.h>
#include <stdint.h>
/* Fixed D2a fixture; generated by the independent contract oracle. */
static const char *const gd_schema[] = {
"CREATE TABLE projects(name TEXT PRIMARY KEY,indexed_at TEXT,root_path TEXT)",
"CREATE TABLE file_hashes(project TEXT,rel_path TEXT,sha256 TEXT,mtime_ns INTEGER,size INTEGER,PRIMARY KEY(project,rel_path))",
"CREATE TABLE nodes(id INTEGER PRIMARY KEY,project TEXT,label TEXT,name TEXT,qualified_name TEXT,file_path TEXT,start_line INTEGER,end_line INTEGER,properties TEXT)",
"CREATE TABLE edges(id INTEGER PRIMARY KEY,project TEXT,source_id INTEGER,target_id INTEGER,type TEXT,properties TEXT,url_path_gen TEXT GENERATED ALWAYS AS (properties || ':u'),local_name_gen TEXT GENERATED ALWAYS AS (properties || ':l'))",
"CREATE TABLE project_summaries(project TEXT PRIMARY KEY,summary TEXT,source_hash TEXT,created_at TEXT,updated_at TEXT)",
"CREATE TABLE lsp_surface(project TEXT,rel_path TEXT,surface_sha TEXT,defs_json TEXT,ref_bloom BLOB,config_ctx TEXT,PRIMARY KEY(project,rel_path))",
"CREATE TABLE index_coverage(project TEXT,rel_path TEXT,kind TEXT,detail TEXT,PRIMARY KEY(project,rel_path,kind))",
"CREATE TABLE index_coverage_meta(project TEXT PRIMARY KEY,generation TEXT,index_mode TEXT,recorded_at TEXT,recording_status TEXT,ignored_files_stored INTEGER,ignored_files_total INTEGER,coverage_version INTEGER,hash_records_complete INTEGER)",
"CREATE TABLE store_meta(k TEXT PRIMARY KEY,v TEXT)",
};
static const char *const gd_inserts[] = {
"INSERT INTO projects(name,indexed_at,root_path) VALUES(CAST(X'70' AS TEXT),CAST(X'7430' AS TEXT),CAST(X'2f726f6f74' AS TEXT));",
"INSERT INTO projects(name,indexed_at,root_path) VALUES(CAST(X'71' AS TEXT),CAST(X'7431' AS TEXT),CAST(X'2f6f74686572' AS TEXT));",
"INSERT INTO file_hashes(project,rel_path,sha256,mtime_ns,size) VALUES(CAST(X'70' AS TEXT),CAST(X'' AS TEXT),CAST(X'656d707479' AS TEXT),0,0);",
"INSERT INTO file_hashes(project,rel_path,sha256,mtime_ns,size) VALUES(CAST(X'70' AS TEXT),CAST(X'61' AS TEXT),CAST(X'736861' AS TEXT),-1,1);",
"INSERT INTO file_hashes(project,rel_path,sha256,mtime_ns,size) VALUES(CAST(X'70' AS TEXT),CAST(X'610062' AS TEXT),CAST(X'73686132' AS TEXT),2,3);",
"INSERT INTO file_hashes(project,rel_path,sha256,mtime_ns,size) VALUES(CAST(X'70' AS TEXT),CAST(X'ff' AS TEXT),CAST(X'73686133' AS TEXT),4,5);",
"INSERT INTO nodes(id,project,label,name,qualified_name,file_path,start_line,end_line,properties) VALUES(-7,CAST(X'70' AS TEXT),CAST(X'46756e6374696f6e' AS TEXT),CAST(X'' AS TEXT),CAST(X'702e61' AS TEXT),NULL,-9223372036854775808,9223372036854775807,CAST(X'7b7d0078' AS TEXT));",
"INSERT INTO nodes(id,project,label,name,qualified_name,file_path,start_line,end_line,properties) VALUES(11,CAST(X'71' AS TEXT),CAST(X'46756e6374696f6e' AS TEXT),CAST(X'62' AS TEXT),CAST(X'712e62' AS TEXT),CAST(X'' AS TEXT),NULL,-1,NULL);",
"INSERT INTO edges(id,project,source_id,target_id,type,properties) VALUES(3,CAST(X'71' AS TEXT),11,-7,CAST(X'43414c4c53' AS TEXT),CAST(X'' AS TEXT));",
"INSERT INTO edges(id,project,source_id,target_id,type,properties) VALUES(8,CAST(X'70' AS TEXT),-7,11,CAST(X'5553414745' AS TEXT),NULL);",
"INSERT INTO project_summaries(project,summary,source_hash,created_at,updated_at) VALUES(CAST(X'70' AS TEXT),CAST(X'6c696e65310a6c696e6532' AS TEXT),CAST(X'68' AS TEXT),CAST(X'63' AS TEXT),CAST(X'75' AS TEXT));",
"INSERT INTO lsp_surface(project,rel_path,surface_sha,defs_json,ref_bloom,config_ctx) VALUES(CAST(X'70' AS TEXT),CAST(X'61' AS TEXT),CAST(X'7330' AS TEXT),CAST(X'7b7d' AS TEXT),NULL,CAST(X'637478' AS TEXT));",
"INSERT INTO lsp_surface(project,rel_path,surface_sha,defs_json,ref_bloom,config_ctx) VALUES(CAST(X'70' AS TEXT),CAST(X'6161' AS TEXT),CAST(X'7331' AS TEXT),CAST(X'5b5d' AS TEXT),X'',CAST(X'' AS TEXT));",
"INSERT INTO lsp_surface(project,rel_path,surface_sha,defs_json,ref_bloom,config_ctx) VALUES(CAST(X'70' AS TEXT),CAST(X'62' AS TEXT),CAST(X'7332' AS TEXT),CAST(X'7b7d' AS TEXT),X'00ff800d0a',CAST(X'63747832' AS TEXT));",
"INSERT INTO index_coverage(project,rel_path,kind,detail) VALUES(CAST(X'70' AS TEXT),CAST(X'61' AS TEXT),CAST(X'7061727469616c' AS TEXT),NULL);",
"INSERT INTO index_coverage(project,rel_path,kind,detail) VALUES(CAST(X'70' AS TEXT),CAST(X'62' AS TEXT),CAST(X'736b6970' AS TEXT),CAST(X'' AS TEXT));",
"INSERT INTO index_coverage_meta(project,generation,index_mode,recorded_at,recording_status,ignored_files_stored,ignored_files_total,coverage_version,hash_records_complete) VALUES(CAST(X'70' AS TEXT),CAST(X'67656e' AS TEXT),CAST(X'66756c6c' AS TEXT),CAST(X'74' AS TEXT),CAST(X'636f6d706c657465' AS TEXT),-1,0,1,1);",
};
static const unsigned char gd_golden_sha[32] = { 0x46, 0x44, 0x71, 0x72, 0xdc, 0x9b, 0x2d, 0xa8, 0xda, 0xc8, 0x54, 0x5a, 0x56, 0x88, 0xab, 0x59, 0x1d, 0xad, 0x39, 0xe2, 0x9b, 0xe6, 0x33, 0x9c, 0x3d, 0x26, 0x49, 0x27, 0xd0, 0x94, 0xe9, 0x3e };
#define GD_GOLDEN_ROWS UINT64_C(17)
#define GD_GOLDEN_BYTES UINT64_C(4038)
static const cbm_store_graph_digest_limits_t gd_room = {UINT64_C(100000), UINT64_C(8388608)};
static int gd_sql(sqlite3 *db, const char *sql) {
return sqlite3_exec(db, sql, NULL, NULL, NULL);
}
static bool gd_int(sqlite3 *db, const char *sql, int64_t want) {
sqlite3_stmt *q=NULL;
bool ok=sqlite3_prepare_v2(db,sql,-1,&q,NULL)==SQLITE_OK && sqlite3_step(q)==SQLITE_ROW &&
sqlite3_column_int64(q,0)==want && sqlite3_step(q)==SQLITE_DONE;
if (q && sqlite3_finalize(q)!=SQLITE_OK) ok=false;
return ok;
}
static bool gd_seed(sqlite3 *db, bool reverse, bool meta) {
static const char drop[] = "PRAGMA foreign_keys=OFF;"
"DROP TABLE IF EXISTS edges;DROP TABLE IF EXISTS nodes;DROP TABLE IF EXISTS file_hashes;"
"DROP TABLE IF EXISTS project_summaries;DROP TABLE IF EXISTS lsp_surface;"
"DROP TABLE IF EXISTS index_coverage;DROP TABLE IF EXISTS index_coverage_meta;"
"DROP TABLE IF EXISTS projects;DROP TABLE IF EXISTS store_meta;";
if (!db || gd_sql(db,drop)!=SQLITE_OK) return false;
for (size_t i=0;i<8+(size_t)meta;i++) if (gd_sql(db,gd_schema[i])!=SQLITE_OK) return false;
size_t n=sizeof(gd_inserts)/sizeof(gd_inserts[0]);
for (size_t i=0;i<n;i++) if (gd_sql(db,gd_inserts[reverse?n-i-1:i])!=SQLITE_OK) return false;
return !meta || gd_sql(db,"INSERT INTO store_meta VALUES('db_uid','0123456789abcdef'),"
"('mutation_gen','7'),('future','opaque')")==SQLITE_OK;
}
static cbm_store_t *gd_fixture(bool reverse, bool meta) {
cbm_store_t *s=cbm_store_open_memory();
if (s && !gd_seed(cbm_store_get_db(s),reverse,meta)) { cbm_store_close(s);s=NULL; }
return s;
}
static bool gd_zero(const cbm_store_graph_digest_t *v) {
if (v->version || v->rows || v->framed_bytes) return false;
for (size_t i=0;i<sizeof(v->sha256);i++) if (v->sha256[i]) return false;
return true;
}
static bool gd_equal(const cbm_store_graph_digest_t *a,const cbm_store_graph_digest_t *b) {
return a->version==b->version && a->rows==b->rows && a->framed_bytes==b->framed_bytes &&
memcmp(a->sha256,b->sha256,sizeof(a->sha256))==0;
}
static bool gd_different(const cbm_store_graph_digest_t *a,const cbm_store_graph_digest_t *b) {
return memcmp(a->sha256,b->sha256,sizeof(a->sha256))!=0;
}
static bool gd_read(cbm_store_t *s,const unsigned char *project,size_t len,
cbm_store_graph_digest_t *out) {
cbm_store_read_scope_t *scope=NULL;
int opened=s?cbm_store_read_scope_open(s,NULL,NULL,&scope):CBM_STORE_ERR;
int rc=opened==CBM_STORE_OK?cbm_store_graph_digest(scope,project,len,&gd_room,out):CBM_STORE_GRAPH_DIGEST_ERROR;
int closed=cbm_store_read_scope_close(scope);
return opened==CBM_STORE_OK && rc==CBM_STORE_GRAPH_DIGEST_OK && closed==CBM_STORE_OK;
}
static bool gd_p(cbm_store_t *s,cbm_store_graph_digest_t *out) {
return gd_read(s,(const unsigned char *)"p",1,out);
}
/* Caller closes the connection immediately after this failure helper. */
static bool gd_reject(cbm_store_t *s,cbm_store_graph_digest_status_t want) {
cbm_store_read_scope_t *scope=NULL; cbm_store_graph_digest_t out;
memset(&out,0xa5,sizeof(out));
int opened=cbm_store_read_scope_open(s,NULL,NULL,&scope);
int rc=opened==CBM_STORE_OK?cbm_store_graph_digest(scope,(const unsigned char *)"p",1,&gd_room,&out):-1;
bool zero=gd_zero(&out);
int latch=scope?cbm_store_read_scope_check(scope):0;
int closed=cbm_store_read_scope_close(scope);
return opened==CBM_STORE_OK && rc==(int)want && zero && latch==CBM_STORE_ERR && closed==CBM_STORE_ERR;
}
static int gd_stmt_count(sqlite3 *db) {
int n=0;for(sqlite3_stmt *s=sqlite3_next_stmt(db,NULL);s;s=sqlite3_next_stmt(db,s))n++;
return n;
}
typedef struct { uint64_t calls,trip; bool armed,stop; } gd_cancel_t;
/* Pure callback, including cancellation: only caller-owned scalar state. */
static bool gd_cancel(void *opaque) {
gd_cancel_t *c=opaque;
if (c->armed && c->calls<UINT64_MAX) c->calls++;
return c->stop || (c->armed && c->trip && c->calls>=c->trip);
}
TEST(store_graph_digest_independent_canonical_golden) {
cbm_store_t *s=gd_fixture(false,false); cbm_store_graph_digest_t out={0};
bool setup=s && gd_int(cbm_store_get_db(s),"SELECT count(*) FROM nodes",2) &&
gd_int(cbm_store_get_db(s),"SELECT length(CAST(properties AS BLOB)) FROM nodes WHERE id=-7",4) &&
gd_int(cbm_store_get_db(s),"SELECT count(*) FROM lsp_surface WHERE typeof(ref_bloom)='blob'",2);
bool got=setup && gd_p(s,&out); cbm_store_close(s);
ASSERT_TRUE(setup); ASSERT_TRUE(got); ASSERT_EQ(out.version,1);
ASSERT_TRUE(out.rows==GD_GOLDEN_ROWS); ASSERT_TRUE(out.framed_bytes==GD_GOLDEN_BYTES);
ASSERT_TRUE(memcmp(out.sha256,gd_golden_sha,32)==0); PASS();
}
TEST(store_graph_digest_logical_order_and_excluded_storage_are_stable) {
cbm_store_t *a=gd_fixture(false,false),*b=gd_fixture(true,false);
cbm_store_graph_digest_t x={0},y={0},z={0},q={0};
bool setup=a && b;
bool controls=setup && gd_p(a,&x) && gd_p(b,&y);
bool layout=controls && gd_sql(cbm_store_get_db(b),
"CREATE TABLE noise(id INTEGER PRIMARY KEY AUTOINCREMENT,value BLOB);"
"INSERT INTO noise VALUES(999,zeroblob(70000));DELETE FROM noise;"
"CREATE INDEX irrelevant_node_name ON nodes(name);PRAGMA reverse_unordered_selects=ON;") == SQLITE_OK;
bool again=layout && gd_p(b,&z) && gd_read(b,(const unsigned char *)"q",1,&q);
char dir[1024]={0},path[1200]={0};const char *tmp=th_mktempdir("cbm-digest-layout");
bool named=tmp && strlen(tmp)<sizeof(dir);if(named)memcpy(dir,tmp,strlen(tmp)+1);
int n=named?snprintf(path,sizeof(path),"%s/other-name.db",dir):-1;
sqlite3 *raw=NULL;bool file=n>0 && (size_t)n<sizeof(path) && sqlite3_open(path,&raw)==SQLITE_OK;
/* VACUUM uses ATTACH internally. Exercise physical layout on the raw
* fixture, without changing the store connection's ATTACH protection. */
bool prepared=file && gd_sql(raw,"PRAGMA page_size=8192")==SQLITE_OK &&
gd_seed(raw,true,false) && gd_sql(raw,"VACUUM")==SQLITE_OK;
if(raw)sqlite3_close(raw);
cbm_store_t *disk=prepared?cbm_store_open_path_query(path):NULL;cbm_store_graph_digest_t on_disk={0};
bool disk_ok=disk && gd_int(cbm_store_get_db(disk),"PRAGMA page_size",8192) && gd_p(disk,&on_disk);
cbm_store_close(disk);bool cleanup=!dir[0] || th_rmtree(dir)==0;
cbm_store_close(a);cbm_store_close(b);
ASSERT_TRUE(named);ASSERT_TRUE(file);ASSERT_TRUE(prepared);ASSERT_TRUE(disk_ok);ASSERT_TRUE(cleanup);
ASSERT_TRUE(gd_equal(&x,&on_disk));
ASSERT_TRUE(setup);ASSERT_TRUE(controls);ASSERT_TRUE(gd_equal(&x,&y));
ASSERT_TRUE(layout);ASSERT_TRUE(again);ASSERT_TRUE(gd_equal(&x,&z));ASSERT_TRUE(gd_different(&x,&q));PASS();
}
TEST(store_graph_digest_every_known_value_and_cross_project_edge_is_bound) {
/* Every ordinary descriptor column has a valid one-column mutation.
* Generated columns cannot be assigned; both expression outputs change via properties. */
static const char *const changes[] = {
"UPDATE projects SET name='r' WHERE name='q'",
"UPDATE projects SET indexed_at='later' WHERE name='p'",
"UPDATE projects SET root_path='/elsewhere' WHERE name='p'",
"UPDATE file_hashes SET project='q' WHERE rel_path='a'",
"UPDATE file_hashes SET rel_path='z' WHERE rel_path='a'",
"UPDATE file_hashes SET sha256='different' WHERE rel_path='a'",
"UPDATE file_hashes SET mtime_ns=-2 WHERE rel_path='a'",
"UPDATE file_hashes SET size=2 WHERE rel_path='a'",
"UPDATE nodes SET id=-8 WHERE id=-7",
"UPDATE nodes SET project='q' WHERE id=-7",
"UPDATE nodes SET label='Method' WHERE id=-7",
"UPDATE nodes SET name='named' WHERE id=-7",
"UPDATE nodes SET qualified_name='p.changed' WHERE id=-7",
"UPDATE nodes SET file_path='' WHERE id=-7",
"UPDATE nodes SET start_line=-1 WHERE id=-7",
"UPDATE nodes SET end_line=0 WHERE id=-7",
"UPDATE nodes SET properties=CAST(X'7b7d0079' AS TEXT) WHERE id=-7",
"UPDATE edges SET id=4 WHERE id=3",
"UPDATE edges SET project='p' WHERE id=3",
"UPDATE edges SET source_id=-7 WHERE id=3",
"UPDATE edges SET target_id=11 WHERE id=3",
"UPDATE edges SET type='ASYNC_CALLS' WHERE id=3",
"UPDATE edges SET properties=' ' WHERE id=3",
"UPDATE project_summaries SET project='q'",
"UPDATE project_summaries SET summary='new'",
"UPDATE project_summaries SET source_hash='new'",
"UPDATE project_summaries SET created_at='new'",
"UPDATE project_summaries SET updated_at='new'",
"UPDATE lsp_surface SET project='q' WHERE rel_path='a'",
"UPDATE lsp_surface SET rel_path='z' WHERE rel_path='a'",
"UPDATE lsp_surface SET surface_sha='new' WHERE rel_path='a'",
"UPDATE lsp_surface SET defs_json=' { } ' WHERE rel_path='a'",
"UPDATE lsp_surface SET ref_bloom=X'' WHERE rel_path='a'",
"UPDATE lsp_surface SET config_ctx='new' WHERE rel_path='a'",
"UPDATE index_coverage SET project='q' WHERE rel_path='a'",
"UPDATE index_coverage SET rel_path='z' WHERE rel_path='a'",
"UPDATE index_coverage SET kind='new' WHERE rel_path='a'",
"UPDATE index_coverage SET detail='' WHERE rel_path='a'",
"UPDATE index_coverage_meta SET project='q'",
"UPDATE index_coverage_meta SET generation='new'",
"UPDATE index_coverage_meta SET index_mode='new'",
"UPDATE index_coverage_meta SET recorded_at='new'",
"UPDATE index_coverage_meta SET recording_status='new'",
"UPDATE index_coverage_meta SET ignored_files_stored=2",
"UPDATE index_coverage_meta SET ignored_files_total=2",
"UPDATE index_coverage_meta SET coverage_version=2",
"UPDATE index_coverage_meta SET hash_records_complete=0",
"UPDATE store_meta SET k='future2' WHERE k='future'",
"UPDATE store_meta SET v='changed' WHERE k='future'",
"UPDATE store_meta SET v='fedcba9876543210' WHERE k='db_uid'",
"UPDATE store_meta SET v='8' WHERE k='mutation_gen'"
};
for(size_t i=0;i<sizeof(changes)/sizeof(changes[0]);i++) {
cbm_store_t *s=gd_fixture(false,true);cbm_store_graph_digest_t before={0},after={0};
bool setup=s && gd_p(s,&before);
bool changed=setup && gd_sql(cbm_store_get_db(s),changes[i])==SQLITE_OK &&
sqlite3_changes(cbm_store_get_db(s))==1;
bool got=changed && gd_p(s,&after);
cbm_store_close(s);
if(!setup || !changed || !got || !gd_different(&before,&after)) {
fprintf(stderr,"graph digest mutation %zu: %s\n",i,changes[i]);FAIL("known column mutation did not change digest");
}
}
PASS();
}
TEST(store_graph_digest_raw_types_project_bytes_and_schema_sql_are_distinct) {
static const char *const changes[] = {
"UPDATE nodes SET properties=CAST(X'7b7d007900' AS TEXT) WHERE id=-7",
"UPDATE lsp_surface SET ref_bloom=X'00' WHERE rel_path='aa'",
"UPDATE lsp_surface SET ref_bloom=NULL WHERE rel_path='aa'",
"UPDATE nodes SET file_path=NULL WHERE id=11",
"UPDATE nodes SET start_line=0 WHERE id=11",
"ALTER TABLE projects RENAME TO old_projects;"
"CREATE TABLE projects(name TEXT COLLATE NOCASE PRIMARY KEY,indexed_at TEXT,root_path TEXT);"
"INSERT INTO projects SELECT * FROM old_projects;DROP TABLE old_projects;",
"ALTER TABLE projects RENAME TO old_projects;"
"CREATE TABLE projects( name TEXT PRIMARY KEY, indexed_at TEXT, root_path TEXT );"
"INSERT INTO projects SELECT * FROM old_projects;DROP TABLE old_projects;",
"ALTER TABLE projects RENAME TO old_projects;"
"CREATE TABLE projects(root_path TEXT,indexed_at TEXT,name TEXT PRIMARY KEY);"
"INSERT INTO projects(root_path,indexed_at,name) SELECT root_path,indexed_at,name FROM old_projects;DROP TABLE old_projects;",
"ALTER TABLE edges RENAME TO old_edges;"
"CREATE TABLE edges(id INTEGER PRIMARY KEY,project TEXT,source_id INTEGER,target_id INTEGER,type TEXT,properties TEXT,"
"url_path_gen TEXT GENERATED ALWAYS AS (properties || ':v'),local_name_gen TEXT GENERATED ALWAYS AS (properties || ':l'));"
"INSERT INTO edges(id,project,source_id,target_id,type,properties) SELECT id,project,source_id,target_id,type,properties FROM old_edges;DROP TABLE old_edges;",
"ALTER TABLE edges RENAME TO old_edges;"
"CREATE TABLE edges(id INTEGER PRIMARY KEY,project TEXT,source_id INTEGER,target_id INTEGER,type TEXT,properties TEXT,"
"url_path_gen TEXT GENERATED ALWAYS AS (properties || ':u'),local_name_gen TEXT GENERATED ALWAYS AS (properties || ':m'));"
"INSERT INTO edges(id,project,source_id,target_id,type,properties) SELECT id,project,source_id,target_id,type,properties FROM old_edges;DROP TABLE old_edges;"
};
for(size_t i=0;i<sizeof(changes)/sizeof(changes[0]);i++) {
cbm_store_t *s=gd_fixture(false,false);cbm_store_graph_digest_t a={0},b={0};
bool before=s && gd_p(s,&a), mutation=before && gd_sql(cbm_store_get_db(s),changes[i])==SQLITE_OK;
bool after=mutation && gd_p(s,&b);cbm_store_close(s);
ASSERT_TRUE(before);ASSERT_TRUE(mutation);ASSERT_TRUE(after);ASSERT_TRUE(gd_different(&a,&b));
}
cbm_store_t *s=gd_fixture(false,false);cbm_store_graph_digest_t raw={0},p={0};
const unsigned char project[]={0xff,':','p'};
bool added=s && gd_sql(cbm_store_get_db(s),"INSERT INTO projects VALUES(CAST(X'ff3a70' AS TEXT),'t','r')")==SQLITE_OK;
bool both=added && gd_read(s,project,sizeof(project),&raw) && gd_p(s,&p);
cbm_store_close(s);ASSERT_TRUE(added);ASSERT_TRUE(both);ASSERT_TRUE(gd_different(&raw,&p));PASS();
}
TEST(store_graph_digest_legacy_generation_and_live_schema_compatibility) {
cbm_store_t *s=gd_fixture(false,false);cbm_store_graph_digest_t old={0},present={0},max={0};
bool before=s && gd_p(s,&old);
bool meta=before && gd_sql(cbm_store_get_db(s),gd_schema[8])==SQLITE_OK &&
gd_sql(cbm_store_get_db(s),"INSERT INTO store_meta VALUES('db_uid','0123456789abcdef'),('mutation_gen','0')")==SQLITE_OK;
bool control=meta && gd_p(s,&present);
bool maximum=control && gd_sql(cbm_store_get_db(s),"UPDATE store_meta SET v='18446744073709551615' WHERE k='mutation_gen'")==SQLITE_OK && gd_p(s,&max);
cbm_store_close(s);
ASSERT_TRUE(before);ASSERT_TRUE(meta);ASSERT_TRUE(control);ASSERT_TRUE(maximum);
ASSERT_TRUE(gd_different(&old,&present));ASSERT_TRUE(gd_different(&present,&max));
static const char *const bad[] = {
"DELETE FROM store_meta", "DELETE FROM store_meta WHERE k='db_uid'",
"DELETE FROM store_meta WHERE k='mutation_gen'",
"UPDATE store_meta SET v='0123456789abcdeF' WHERE k='db_uid'",
"UPDATE store_meta SET v='0123456789abcdeg' WHERE k='db_uid'",
"UPDATE store_meta SET v=CAST(X'3031323334353637383961626364656600' AS TEXT) WHERE k='db_uid'",
"UPDATE store_meta SET v='00' WHERE k='mutation_gen'",
"UPDATE store_meta SET v='+1' WHERE k='mutation_gen'",
"UPDATE store_meta SET v='-1' WHERE k='mutation_gen'",
"UPDATE store_meta SET v='18446744073709551616' WHERE k='mutation_gen'",
"UPDATE store_meta SET v=CAST(X'370078' AS TEXT) WHERE k='mutation_gen'",
("ALTER TABLE store_meta RENAME TO old_meta;CREATE TABLE store_meta(k TEXT,v TEXT);"
"INSERT INTO store_meta SELECT * FROM old_meta;INSERT INTO store_meta VALUES('db_uid','0123456789abcdef');DROP TABLE old_meta;")
};
for(size_t i=0;i<sizeof(bad)/sizeof(bad[0]);i++) {
s=gd_fixture(false,true); bool setup=s && gd_sql(cbm_store_get_db(s),bad[i])==SQLITE_OK;
bool rejected=setup && gd_reject(s,CBM_STORE_GRAPH_DIGEST_SCHEMA);cbm_store_close(s);
ASSERT_TRUE(setup);ASSERT_TRUE(rejected);
}
/* Compatibility with the real default schema and sparse/empty metadata. */
s=cbm_store_open_memory();cbm_store_graph_digest_t live={0};
bool seeded=s && cbm_store_upsert_project(s,"p","/live")==CBM_STORE_OK;
bool compatible=seeded && gd_p(s,&live);cbm_store_close(s);
ASSERT_TRUE(seeded);ASSERT_TRUE(compatible);PASS();
}
TEST(store_graph_digest_schema_and_runtime_types_fail_closed) {
static const char *const bad[] = {
"DROP TABLE file_hashes", "DROP TABLE project_summaries", "DROP TABLE lsp_surface",
"DROP TABLE index_coverage", "DROP TABLE index_coverage_meta", "DROP TABLE edges",
"DROP TABLE projects",
"ALTER TABLE file_hashes ADD COLUMN ignored TEXT",
"ALTER TABLE file_hashes DROP COLUMN size",
"ALTER TABLE file_hashes RENAME TO old_hashes;CREATE VIEW file_hashes AS SELECT * FROM old_hashes;",
"DROP TABLE file_hashes;CREATE VIRTUAL TABLE file_hashes USING fts5(project,rel_path,sha256,mtime_ns,size);",
"ALTER TABLE edges DROP COLUMN local_name_gen;ALTER TABLE edges ADD COLUMN local_name_gen TEXT",
"ALTER TABLE file_hashes RENAME TO old_hashes;"
"CREATE TABLE file_hashes(project TEXT,rel_path TEXT,sha256 TEXT,mtime_ns INTEGER,size INTEGER,PRIMARY KEY(rel_path,project));"
"INSERT INTO file_hashes SELECT * FROM old_hashes;DROP TABLE old_hashes;",
"CREATE TEMP TABLE file_hashes(project TEXT)",
"CREATE TEMP VIEW projects AS SELECT 'p' AS name",
"UPDATE file_hashes SET size=1.5 WHERE rel_path='a'",
"UPDATE nodes SET name=X'6162' WHERE id=-7",
"UPDATE lsp_surface SET ref_bloom='bytes' WHERE rel_path='a'",
"UPDATE nodes SET label=NULL WHERE id=-7",
"UPDATE projects SET name=NULL WHERE name='q'",
"UPDATE file_hashes SET rel_path=NULL WHERE rel_path='a'",
("ALTER TABLE projects RENAME TO old_projects;CREATE TABLE projects(name TEXT,indexed_at TEXT,root_path TEXT);"
"INSERT INTO projects SELECT * FROM old_projects;INSERT INTO projects VALUES('p','t','r');DROP TABLE old_projects;")
};
for(size_t i=0;i<sizeof(bad)/sizeof(bad[0]);i++) {
cbm_store_t *s=gd_fixture(false,false);bool setup=s && gd_sql(cbm_store_get_db(s),bad[i])==SQLITE_OK;
bool rejected=setup && gd_reject(s,CBM_STORE_GRAPH_DIGEST_SCHEMA);cbm_store_close(s);
if(!setup || !rejected) {fprintf(stderr,"graph schema case %zu: %s\n",i,bad[i]);FAIL("schema/type refusal or fixture failed");}
}
/* UTF-16 needs a fresh raw database: an existing UTF-8 schema cannot be recoded by PRAGMA. */
char dir[1024]={0},path[1200]={0};const char *tmp=th_mktempdir("cbm-digest-utf16");
bool named=tmp && strlen(tmp)<sizeof(dir);if(named)memcpy(dir,tmp,strlen(tmp)+1);
int n=named?snprintf(path,sizeof(path),"%s/graph.db",dir):-1;
sqlite3 *db=NULL;bool file=n>0 && (size_t)n<sizeof(path) && sqlite3_open(path,&db)==SQLITE_OK;
bool utf16=file && gd_sql(db,"PRAGMA encoding='UTF-16le'")==SQLITE_OK;
/* The golden rows deliberately CAST raw UTF-8 bytes to TEXT. Those casts
* have different meanings in a UTF-16 database, so seed ordinary SQL text. */
for (size_t i=0;utf16 && i<8;i++) utf16=gd_sql(db,gd_schema[i])==SQLITE_OK;
utf16=utf16 && gd_sql(db,"INSERT INTO projects VALUES('p','t','r')")==SQLITE_OK;
sqlite3_stmt *encoding=NULL;
bool checked=utf16 && sqlite3_prepare_v2(db,"PRAGMA encoding",-1,&encoding,NULL)==SQLITE_OK &&
sqlite3_step(encoding)==SQLITE_ROW && sqlite3_column_type(encoding,0)==SQLITE_TEXT &&
sqlite3_column_text(encoding,0)!=NULL && sqlite3_column_bytes(encoding,0)==8 &&
memcmp(sqlite3_column_text(encoding,0),"UTF-16le",8)==0 &&
sqlite3_step(encoding)==SQLITE_DONE;
if(encoding && sqlite3_finalize(encoding)!=SQLITE_OK) checked=false;
utf16=utf16 && checked;
if(db)sqlite3_close(db);
cbm_store_t *s=utf16?cbm_store_open_path_query(path):NULL;
bool rejected=s && gd_reject(s,CBM_STORE_GRAPH_DIGEST_SCHEMA);cbm_store_close(s);
bool cleanup=!dir[0] || th_rmtree(dir)==0;
ASSERT_TRUE(named);ASSERT_TRUE(file);ASSERT_TRUE(utf16);ASSERT_TRUE(rejected);ASSERT_TRUE(cleanup);PASS();
}
TEST(store_graph_digest_wal_pins_old_content_then_observes_next_scope) {
char dir[1024]={0},path[1200]={0};const char *tmp=th_mktempdir("cbm-digest-wal");
bool named=tmp && strlen(tmp)<sizeof(dir);if(named)memcpy(dir,tmp,strlen(tmp)+1);
int n=named?snprintf(path,sizeof(path),"%s/graph.db",dir):-1;
cbm_store_t *writer=n>0 && (size_t)n<sizeof(path)?cbm_store_open_path(path):NULL;
bool seeded=writer && gd_seed(cbm_store_get_db(writer),false,true) &&
gd_int(cbm_store_get_db(writer),"SELECT count(*) FROM pragma_journal_mode WHERE journal_mode='wal'",1);
cbm_store_t *reader=seeded?cbm_store_open_path_query(path):NULL;
cbm_store_graph_digest_t before={0},pinned={0},after={0};
bool control=reader && gd_p(reader,&before);
cbm_store_read_scope_t *scope=NULL;
int opened=control?cbm_store_read_scope_open(reader,NULL,NULL,&scope):CBM_STORE_ERR;
bool committed=opened==CBM_STORE_OK && gd_sql(cbm_store_get_db(writer),
"BEGIN;UPDATE nodes SET name='new' WHERE id=-7;UPDATE edges SET type='READS' WHERE id=3;"
"UPDATE store_meta SET v='8' WHERE k='mutation_gen';COMMIT;")==SQLITE_OK;
int rc=committed?cbm_store_graph_digest(scope,(const unsigned char *)"p",1,&gd_room,&pinned):-1;
int closed=cbm_store_read_scope_close(scope);
bool next=closed==CBM_STORE_OK && reader && gd_p(reader,&after);
cbm_store_close(reader);cbm_store_close(writer);bool cleanup=!dir[0] || th_rmtree(dir)==0;
ASSERT_TRUE(named);ASSERT_TRUE(seeded);ASSERT_TRUE(control);ASSERT_EQ(opened,CBM_STORE_OK);
ASSERT_TRUE(committed);ASSERT_EQ(rc,CBM_STORE_GRAPH_DIGEST_OK);ASSERT_EQ(closed,CBM_STORE_OK);
ASSERT_TRUE(gd_equal(&before,&pinned));ASSERT_TRUE(next);ASSERT_TRUE(gd_different(&before,&after));ASSERT_TRUE(cleanup);PASS();
}
TEST(store_graph_digest_exact_budgets_zero_output_and_terminal_scope) {
cbm_store_t *control=gd_fixture(false,false);cbm_store_graph_digest_t whole={0};
bool good=control && gd_p(control,&whole);cbm_store_close(control);ASSERT_TRUE(good);
/* Exact equality; row exhaustion; final trailer exhaustion; mid-stream framing. */
cbm_store_graph_digest_limits_t cases[] = {
{whole.rows,whole.framed_bytes},{whole.rows-1,whole.framed_bytes},
{whole.rows,whole.framed_bytes-1},{whole.rows,whole.framed_bytes/2}
};
for(size_t i=0;i<sizeof(cases)/sizeof(cases[0]);i++) {
cbm_store_t *s=gd_fixture(false,false);cbm_store_read_scope_t *scope=NULL;
int opened=s?cbm_store_read_scope_open(s,NULL,NULL,&scope):CBM_STORE_ERR;
cbm_store_graph_digest_t out,retry;memset(&out,0xa5,sizeof(out));memset(&retry,0xa5,sizeof(retry));
int rc=opened==CBM_STORE_OK?cbm_store_graph_digest(scope,(const unsigned char *)"p",1,&cases[i],&out):-1;
int latch=scope?cbm_store_read_scope_check(scope):0;
int rrc=scope?cbm_store_graph_digest(scope,(const unsigned char *)"p",1,&gd_room,&retry):-1;
int closed=cbm_store_read_scope_close(scope);cbm_store_close(s);
ASSERT_EQ(opened,CBM_STORE_OK);
if(i==0) {ASSERT_EQ(rc,CBM_STORE_GRAPH_DIGEST_OK);ASSERT_TRUE(gd_equal(&whole,&out));ASSERT_EQ(latch,CBM_STORE_OK);ASSERT_EQ(rrc,CBM_STORE_GRAPH_DIGEST_OK);ASSERT_EQ(closed,CBM_STORE_OK);}
else {ASSERT_EQ(rc,CBM_STORE_GRAPH_DIGEST_LIMIT);ASSERT_TRUE(gd_zero(&out));ASSERT_EQ(latch,CBM_STORE_ERR);ASSERT_EQ(rrc,CBM_STORE_GRAPH_DIGEST_ERROR);ASSERT_TRUE(gd_zero(&retry));ASSERT_EQ(closed,CBM_STORE_ERR);}
}
cbm_store_t *s=gd_fixture(false,false);bool big=s && gd_sql(cbm_store_get_db(s),"UPDATE lsp_surface SET ref_bloom=zeroblob(262145) WHERE rel_path='b'")==SQLITE_OK;
cbm_store_graph_digest_t large={0};bool complete=big && gd_p(s,&large);
cbm_store_read_scope_t *scope=NULL;int opened=complete?cbm_store_read_scope_open(s,NULL,NULL,&scope):CBM_STORE_ERR;
cbm_store_graph_digest_limits_t half={gd_room.max_rows,large.framed_bytes-131072};cbm_store_graph_digest_t out;memset(&out,0xa5,sizeof(out));
int rc=opened==CBM_STORE_OK?cbm_store_graph_digest(scope,(const unsigned char *)"p",1,&half,&out):-1;
int closed=cbm_store_read_scope_close(scope);cbm_store_close(s);
ASSERT_TRUE(big);ASSERT_TRUE(complete);ASSERT_EQ(opened,CBM_STORE_OK);ASSERT_EQ(rc,CBM_STORE_GRAPH_DIGEST_LIMIT);ASSERT_TRUE(gd_zero(&out));ASSERT_EQ(closed,CBM_STORE_ERR);PASS();
}
TEST(store_graph_digest_invalid_arguments_missing_project_and_initial_latches) {
for(int which=0;which<11;which++) {
cbm_store_t *s=gd_fixture(false,false);cbm_store_read_scope_t *scope=NULL;
bool collation=which!=10 || (s && gd_sql(cbm_store_get_db(s),
"ALTER TABLE projects RENAME TO old_projects;"
"CREATE TABLE projects(name TEXT COLLATE NOCASE PRIMARY KEY,indexed_at TEXT,root_path TEXT);"
"INSERT INTO projects SELECT * FROM old_projects;DROP TABLE old_projects;")==SQLITE_OK);
int opened=s && collation?cbm_store_read_scope_open(s,NULL,NULL,&scope):CBM_STORE_ERR;
cbm_store_graph_digest_limits_t lim=gd_room;cbm_store_graph_digest_t out;memset(&out,0xa5,sizeof(out));
const unsigned char *p=(const unsigned char *)"p";size_t len=1;
if(which==2)p=NULL;
if(which==3)len=0;
if(which==4)len=(size_t)INT_MAX+1;
if(which==5){p=(const unsigned char *)"p\0q";len=3;}
if(which==7)lim.max_rows=0;
if(which==8)lim.max_framed_bytes=0;
if(which==9)lim.max_framed_bytes=UINT64_MAX/8+1;
if(which==10)p=(const unsigned char *)"P";
int rc=opened==CBM_STORE_OK?cbm_store_graph_digest(which==0?NULL:scope,p,len,which==6?NULL:&lim,which==1?NULL:&out):-1;
int latch=scope?cbm_store_read_scope_check(scope):0;
int closed=cbm_store_read_scope_close(scope);cbm_store_close(s);
ASSERT_EQ(opened,CBM_STORE_OK);ASSERT_EQ(rc,which==10?CBM_STORE_GRAPH_DIGEST_PROJECT_MISSING:CBM_STORE_GRAPH_DIGEST_INVALID);
if(which!=1)ASSERT_TRUE(gd_zero(&out));
ASSERT_EQ(latch,which==0?CBM_STORE_OK:CBM_STORE_ERR);ASSERT_EQ(closed,which==0?CBM_STORE_OK:CBM_STORE_ERR);
}
for(int cancelled=0;cancelled<2;cancelled++) {
cbm_store_t *s=gd_fixture(false,false);cbm_store_read_scope_t *scope=NULL;
int opened=s?cbm_store_read_scope_open(s,NULL,NULL,&scope):CBM_STORE_ERR;
int wanted=cancelled?CBM_STORE_CANCELLED:CBM_STORE_ERR;
int failed=scope?cbm_store_read_scope_fail(scope,wanted):0;
cbm_store_graph_digest_t out;memset(&out,0xa5,sizeof(out));
int rc=scope?cbm_store_graph_digest(scope,(const unsigned char *)"p",1,&gd_room,&out):-1;
int closed=cbm_store_read_scope_close(scope);cbm_store_close(s);
ASSERT_EQ(opened,CBM_STORE_OK);ASSERT_EQ(failed,wanted);ASSERT_EQ(rc,cancelled?CBM_STORE_GRAPH_DIGEST_CANCELLED:CBM_STORE_GRAPH_DIGEST_ERROR);ASSERT_TRUE(gd_zero(&out));ASSERT_EQ(closed,wanted);
}
PASS();
}
TEST(store_graph_digest_cancellation_large_blob_never_publishes_prefix) {
cbm_store_t *control=gd_fixture(false,false);gd_cancel_t c={0};cbm_store_read_scope_t *scope=NULL;
bool large=control && gd_sql(cbm_store_get_db(control),"UPDATE lsp_surface SET ref_bloom=zeroblob(262145) WHERE rel_path='b'")==SQLITE_OK;
int opened=large?cbm_store_read_scope_open(control,gd_cancel,&c,&scope):CBM_STORE_ERR;
cbm_store_graph_digest_t expected={0};c.armed=true;
int rc=scope?cbm_store_graph_digest(scope,(const unsigned char *)"p",1,&gd_room,&expected):-1;
uint64_t polls=c.calls;c.armed=false;
int closed=cbm_store_read_scope_close(scope);cbm_store_close(control);
ASSERT_TRUE(large);ASSERT_EQ(opened,CBM_STORE_OK);ASSERT_EQ(rc,CBM_STORE_GRAPH_DIGEST_OK);ASSERT_EQ(closed,CBM_STORE_OK);ASSERT_TRUE(polls>1);
/* Nine bounded, observed positions. Counts do not identify SHA/SQL phases. */
for(unsigned i=0;i<9;i++) {
cbm_store_t *s=gd_fixture(false,false);bool setup=s && gd_sql(cbm_store_get_db(s),"UPDATE lsp_surface SET ref_bloom=zeroblob(262145) WHERE rel_path='b'")==SQLITE_OK;
gd_cancel_t cancel={0};scope=NULL;opened=setup?cbm_store_read_scope_open(s,gd_cancel,&cancel,&scope):CBM_STORE_ERR;
cbm_store_graph_digest_t out,retry;memset(&out,0xa5,sizeof(out));memset(&retry,0xa5,sizeof(retry));
cancel.trip=1+(polls-1)*i/8;cancel.armed=true;
rc=scope?cbm_store_graph_digest(scope,(const unsigned char *)"p",1,&gd_room,&out):-1;
cancel.armed=false;int latch=scope?cbm_store_read_scope_check(scope):0;
int again=scope?cbm_store_graph_digest(scope,(const unsigned char *)"p",1,&gd_room,&retry):-1;
closed=cbm_store_read_scope_close(scope);cbm_store_close(s);
ASSERT_TRUE(setup);ASSERT_EQ(opened,CBM_STORE_OK);ASSERT_EQ(rc,CBM_STORE_GRAPH_DIGEST_CANCELLED);ASSERT_TRUE(gd_zero(&out));ASSERT_EQ(latch,CBM_STORE_CANCELLED);ASSERT_EQ(again,CBM_STORE_GRAPH_DIGEST_CANCELLED);ASSERT_TRUE(gd_zero(&retry));ASSERT_EQ(closed,CBM_STORE_CANCELLED);
}
PASS();
}
typedef struct { const char *denied_table; bool restore; unsigned denials; } gd_auth_t;
static int gd_authorizer(void *opaque,int op,const char *a,const char *b,const char *db,const char *trigger) {
gd_auth_t *c=opaque;(void)db;(void)trigger;
if(op==SQLITE_READ && a && c->denied_table &&
(strcmp(a,c->denied_table)==0 || (strcmp(c->denied_table,"@schema")==0 &&
(strcmp(a,"sqlite_master")==0 || strcmp(a,"sqlite_schema")==0)))) {
c->denials++;return SQLITE_DENY;
}
if(c->restore && op==SQLITE_PRAGMA && a && b && strcmp(a,"query_only")==0)return SQLITE_DENY;
return SQLITE_OK;
}
TEST(store_graph_digest_sql_errors_cleanup_no_mutation_and_close_discard) {
for(int phase=0;phase<2;phase++) {
cbm_store_t *s=gd_fixture(false,false);sqlite3 *db=s?cbm_store_get_db(s):NULL;
gd_auth_t auth={0};
bool installed=db && sqlite3_set_authorizer(db,gd_authorizer,&auth)==SQLITE_OK;
cbm_store_read_scope_t *scope=NULL;int opened=installed?cbm_store_read_scope_open(s,NULL,NULL,&scope):CBM_STORE_ERR;
auth.denied_table=phase?"file_hashes":"@schema";
int before=db?gd_stmt_count(db):-1;cbm_store_graph_digest_t out;memset(&out,0xa5,sizeof(out));
int rc=scope?cbm_store_graph_digest(scope,(const unsigned char *)"p",1,&gd_room,&out):-1;
int after=db?gd_stmt_count(db):-2;
int closed=cbm_store_read_scope_close(scope);cbm_store_close(s);
ASSERT_TRUE(installed);ASSERT_TRUE(auth.denials>0);ASSERT_EQ(opened,CBM_STORE_OK);ASSERT_EQ(rc,CBM_STORE_GRAPH_DIGEST_ERROR);ASSERT_TRUE(gd_zero(&out));ASSERT_EQ(before,after);ASSERT_EQ(closed,CBM_STORE_ERR);
}
cbm_store_t *s=gd_fixture(false,false);sqlite3 *db=s?cbm_store_get_db(s):NULL;
int changes=db?sqlite3_total_changes(db):-1;
cbm_store_graph_digest_t a={0},b={0};bool read=s && gd_p(s,&a) && gd_p(s,&b);
bool unchanged=read && sqlite3_total_changes(db)==changes && gd_equal(&a,&b) &&
gd_int(db,"SELECT count(*) FROM sqlite_schema WHERE name='store_meta'",0) &&
gd_int(db,"SELECT count(*) FROM projects WHERE name='p' AND indexed_at='t0'",1);
cbm_store_close(s);ASSERT_TRUE(read);ASSERT_TRUE(unchanged);
s=gd_fixture(false,false);db=s?cbm_store_get_db(s):NULL;gd_auth_t auth={0};
bool installed=db && sqlite3_set_authorizer(db,gd_authorizer,&auth)==SQLITE_OK;
cbm_store_read_scope_t *scope=NULL;int opened=installed?cbm_store_read_scope_open(s,NULL,NULL,&scope):CBM_STORE_ERR;
int rc=scope?cbm_store_graph_digest(scope,(const unsigned char *)"p",1,&gd_room,&a):-1;
auth.restore=true;int closed=cbm_store_read_scope_close(scope);
/* A successful calculation is deliberately not consumed after DISCARD. */
memset(&a,0,sizeof(a));cbm_store_close(s);
ASSERT_TRUE(installed);ASSERT_EQ(opened,CBM_STORE_OK);ASSERT_EQ(rc,CBM_STORE_GRAPH_DIGEST_OK);ASSERT_EQ(closed,CBM_STORE_SCOPE_DISCARD);
s=gd_fixture(false,false);scope=NULL;opened=s?cbm_store_read_scope_open(s,NULL,NULL,&scope):CBM_STORE_ERR;
bool lost=scope && gd_sql(cbm_store_get_db(s),"ROLLBACK")==SQLITE_OK;memset(&a,0xa5,sizeof(a));
rc=lost?cbm_store_graph_digest(scope,(const unsigned char *)"p",1,&gd_room,&a):-1;
closed=cbm_store_read_scope_close(scope);cbm_store_close(s);
ASSERT_EQ(opened,CBM_STORE_OK);ASSERT_TRUE(lost);ASSERT_EQ(rc,CBM_STORE_GRAPH_DIGEST_ERROR);ASSERT_TRUE(gd_zero(&a));ASSERT_TRUE(closed==CBM_STORE_ERR || closed==CBM_STORE_SCOPE_DISCARD);PASS();
}
typedef struct {
cbm_store_t *store;cbm_mutex_t gate;atomic_bool arrived,done;
uint64_t calls,trip;bool armed,cancel;
int opened,rc,latch,closed;cbm_store_graph_digest_t out;
} gd_job_t;
/* Synchronizes only caller-owned state. No SQLite/store/scope calls in predicate. */
static bool gd_gate_cancel(void *opaque) {
gd_job_t *j=opaque;
if(!j->armed)return false;
j->calls++;
if(j->trip && j->calls==j->trip) {
atomic_store_explicit(&j->arrived,true,memory_order_release);
cbm_mutex_lock(&j->gate);cbm_mutex_unlock(&j->gate);
return j->cancel;
}
return false;
}
static void *gd_worker(void *opaque) {
gd_job_t *j=opaque;cbm_store_read_scope_t *scope=NULL;
j->opened=cbm_store_read_scope_open(j->store,gd_gate_cancel,j,&scope);
memset(&j->out,0xa5,sizeof(j->out));j->armed=true;
j->rc=scope?cbm_store_graph_digest(scope,(const unsigned char *)"p",1,&gd_room,&j->out):-1;
j->armed=false;j->latch=scope?cbm_store_read_scope_check(scope):CBM_STORE_ERR;
j->closed=cbm_store_read_scope_close(scope);
atomic_store_explicit(&j->done,true,memory_order_release);return NULL;
}
static void gd_job_init(gd_job_t *j,cbm_store_t *s) {
memset(j,0,sizeof(*j));j->store=s;atomic_init(&j->arrived,false);atomic_init(&j->done,false);cbm_mutex_init(&j->gate);
}
static bool gd_wait_arrived(gd_job_t *j) {
while(!atomic_load_explicit(&j->arrived,memory_order_acquire) &&
!atomic_load_explicit(&j->done,memory_order_acquire)) { /* Held-gate rendezvous, no clock. */ }
return atomic_load_explicit(&j->arrived,memory_order_acquire);
}
TEST(store_graph_digest_concurrent_isolation_with_held_callback_gates) {
cbm_store_t *a=gd_fixture(false,false),*b=gd_fixture(true,true);
gd_job_t control_a,control_b;gd_job_init(&control_a,a);gd_job_init(&control_b,b);
bool setup=a && b;
if(setup){gd_worker(&control_a);gd_worker(&control_b);}
bool controls=setup && control_a.opened==CBM_STORE_OK && control_b.opened==CBM_STORE_OK &&
control_a.rc==CBM_STORE_GRAPH_DIGEST_OK && control_b.rc==CBM_STORE_GRAPH_DIGEST_OK &&
control_a.closed==CBM_STORE_OK && control_b.closed==CBM_STORE_OK &&
control_a.calls>1 && control_b.calls>1 && gd_different(&control_a.out,&control_b.out);
cbm_mutex_destroy(&control_a.gate);cbm_mutex_destroy(&control_b.gate);
if(!controls){cbm_store_close(a);cbm_store_close(b);ASSERT_TRUE(setup);ASSERT_TRUE(controls);}
for(int cancel=0;cancel<2;cancel++) {
gd_job_t ja,jb;gd_job_init(&ja,a);gd_job_init(&jb,b);
ja.trip=control_a.calls/2;jb.trip=control_b.calls/2;ja.cancel=cancel!=0;
cbm_mutex_lock(&ja.gate);cbm_mutex_lock(&jb.gate);
cbm_thread_t ta,tb;int ca=cbm_thread_create(&ta,0,gd_worker,&ja);
int cb=ca==0?cbm_thread_create(&tb,0,gd_worker,&jb):-1;
bool arrived_a=ca==0 && cb==0 && gd_wait_arrived(&ja);
bool arrived_b=ca==0 && cb==0 && gd_wait_arrived(&jb);
int joined_a=-1,joined_b=-1;
if(cancel) {
/* Cancellation completes while B is suspended; B must still resume correctly. */
cbm_mutex_unlock(&ja.gate);if(ca==0)joined_a=cbm_thread_join(&ta);
cbm_mutex_unlock(&jb.gate);if(cb==0)joined_b=cbm_thread_join(&tb);
} else {
/* B finishes while A retains its in-progress state. */
cbm_mutex_unlock(&jb.gate);if(cb==0)joined_b=cbm_thread_join(&tb);
cbm_mutex_unlock(&ja.gate);if(ca==0)joined_a=cbm_thread_join(&ta);
}
bool b_ok=jb.opened==CBM_STORE_OK && jb.rc==CBM_STORE_GRAPH_DIGEST_OK && jb.latch==CBM_STORE_OK &&
jb.closed==CBM_STORE_OK && gd_equal(&jb.out,&control_b.out);
bool a_ok=ja.opened==CBM_STORE_OK && (cancel?
(ja.rc==CBM_STORE_GRAPH_DIGEST_CANCELLED && ja.latch==CBM_STORE_CANCELLED && ja.closed==CBM_STORE_CANCELLED && gd_zero(&ja.out)):
(ja.rc==CBM_STORE_GRAPH_DIGEST_OK && ja.latch==CBM_STORE_OK && ja.closed==CBM_STORE_OK && gd_equal(&ja.out,&control_a.out)));
cbm_mutex_destroy(&ja.gate);cbm_mutex_destroy(&jb.gate);
if(ca || cb || !arrived_a || !arrived_b || joined_a || joined_b || !a_ok || !b_ok) {
cbm_store_close(a);cbm_store_close(b);FAIL("held-gate invocation isolation failed");
}
}
cbm_store_close(a);cbm_store_close(b);PASS();
}
SUITE(store_graph_digest) {
RUN_TEST(store_graph_digest_independent_canonical_golden);
RUN_TEST(store_graph_digest_logical_order_and_excluded_storage_are_stable);
RUN_TEST(store_graph_digest_every_known_value_and_cross_project_edge_is_bound);
RUN_TEST(store_graph_digest_raw_types_project_bytes_and_schema_sql_are_distinct);
RUN_TEST(store_graph_digest_legacy_generation_and_live_schema_compatibility);
RUN_TEST(store_graph_digest_schema_and_runtime_types_fail_closed);
RUN_TEST(store_graph_digest_wal_pins_old_content_then_observes_next_scope);
RUN_TEST(store_graph_digest_exact_budgets_zero_output_and_terminal_scope);
RUN_TEST(store_graph_digest_invalid_arguments_missing_project_and_initial_latches);
RUN_TEST(store_graph_digest_cancellation_large_blob_never_publishes_prefix);
RUN_TEST(store_graph_digest_sql_errors_cleanup_no_mutation_and_close_discard);
RUN_TEST(store_graph_digest_concurrent_isolation_with_held_callback_gates);
}