#include "test_framework.h" #include "test_helpers.h" #include #include #include #include #include #include #include /* 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] = { 0x49, 0xc9, 0x8c, 0x82, 0x0a, 0xa8, 0xcc, 0x9b, 0xc2, 0x7f, 0x5e, 0xe5, 0x16, 0x6f, 0xf1, 0x06, 0xd2, 0x03, 0xec, 0x07, 0xb4, 0xd2, 0xab, 0x8f, 0x4e, 0xac, 0xc0, 0xab, 0xd3, 0xdf, 0x37, 0x05}; #define GD_GOLDEN_ROWS UINT64_C(17) #define GD_GOLDEN_BYTES UINT64_C(5835) 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_unresolved_candidates;" "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_binds_unresolved_candidate_index) { cbm_store_t *s = cbm_store_open_memory(); cbm_coverage_row_t row = {.rel_path = "calls.js", .kind = "unresolved_calls", .detail = "[{\"candidate\":\"p.target\"}]"}; bool setup = s && cbm_store_upsert_project(s, "p", "/tmp/digest-candidates") == CBM_STORE_OK && cbm_store_upsert_file_hash(s, "p", "calls.js", "fixture", 0, 0) == CBM_STORE_OK && cbm_store_coverage_replace(s, "p", &row, 1) == CBM_STORE_OK; bool candidate_before = false, candidate_after = true; cbm_store_graph_digest_t before = {0}, after = {0}; unsigned char content_before[CBM_STORE_GRAPH_DIGEST_BYTES] = {0}, content_after[CBM_STORE_GRAPH_DIGEST_BYTES] = {0}; bool read_before = setup && cbm_store_coverage_has_unresolved_candidate(s, "p", "p.target", &candidate_before) == CBM_STORE_OK && gd_p(s, &before) && cbm_store_graph_content_digest(s, "p", content_before) == CBM_STORE_OK; /* Deliberately bypass publication/generation advancement: inbound reads this table. */ bool removed = read_before && gd_sql(cbm_store_get_db(s), "DELETE FROM index_unresolved_candidates WHERE project='p'") == SQLITE_OK; bool read_after = removed && cbm_store_coverage_has_unresolved_candidate(s, "p", "p.target", &candidate_after) == CBM_STORE_OK && gd_p(s, &after) && cbm_store_graph_content_digest(s, "p", content_after) == CBM_STORE_OK; cbm_store_close(s); ASSERT_TRUE(setup); ASSERT_TRUE(read_before); ASSERT_TRUE(candidate_before); ASSERT_TRUE(removed); ASSERT_TRUE(read_after); ASSERT_FALSE(candidate_after); ASSERT_TRUE(gd_different(&before, &after)); ASSERT_TRUE(memcmp(content_before, content_after, sizeof(content_before)) != 0); PASS(); } TEST(store_graph_digest_candidate_index_errors_are_not_legacy_absence) { cbm_store_t *s = gd_fixture(false, false); unsigned char legacy[CBM_STORE_GRAPH_DIGEST_BYTES] = {0}, again[CBM_STORE_GRAPH_DIGEST_BYTES] = {0}; bool old = s && cbm_store_graph_content_digest(s, "p", legacy) == CBM_STORE_OK; bool malformed = old && gd_sql(cbm_store_get_db(s), "CREATE TABLE index_unresolved_candidates(project TEXT,unexpected TEXT)") == SQLITE_OK; int content_rc = malformed ? cbm_store_graph_content_digest(s, "p", again) : CBM_STORE_OK; bool graph_rejected = malformed && gd_reject(s, CBM_STORE_GRAPH_DIGEST_SCHEMA); bool absent = graph_rejected && gd_sql(cbm_store_get_db(s), "DROP TABLE index_unresolved_candidates") == SQLITE_OK && cbm_store_graph_content_digest(s, "p", again) == CBM_STORE_OK; cbm_store_close(s); ASSERT_TRUE(old); ASSERT_TRUE(malformed); ASSERT_EQ(content_rc, CBM_STORE_ERR); ASSERT_TRUE(graph_rejected); ASSERT_TRUE(absent); ASSERT_TRUE(memcmp(legacy, again, sizeof(legacy)) == 0); PASS(); } 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, 3); 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(); } /* Legacy metadata remains supported, while the independent capture signal * must be bound: changing only that value changes the digest. */ TEST(store_graph_digest_binds_unresolved_capture_completeness) { cbm_store_t *store = gd_fixture(false, false); ASSERT_NOT_NULL(store); cbm_store_graph_digest_t incomplete = {0}, complete = {0}; ASSERT_EQ(gd_sql(cbm_store_get_db(store), "ALTER TABLE index_coverage_meta ADD COLUMN " "unresolved_calls_complete INTEGER NOT NULL DEFAULT 0;"), SQLITE_OK); ASSERT_TRUE(gd_p(store, &incomplete)); ASSERT_EQ(gd_sql(cbm_store_get_db(store), "UPDATE index_coverage_meta SET unresolved_calls_complete=1;"), SQLITE_OK); ASSERT_TRUE(gd_p(store, &complete)); cbm_store_close(store); ASSERT_TRUE(gd_different(&incomplete, &complete)); 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(); } /* Resolver rows are part of the same private generation as graph rows. * This fixture is independent of resolver output and covers both projects. */ static const char *const gd_resolver_schema[] = { "CREATE TABLE resolver_occurrences(project TEXT NOT NULL,ordinal INTEGER NOT NULL,file_path " "TEXT NOT NULL,scope TEXT NOT NULL,relation TEXT NOT NULL,spelling TEXT NOT NULL,start_byte " "INTEGER NOT NULL,end_byte INTEGER NOT NULL,source_origin INTEGER NOT NULL,binding INTEGER NOT " "NULL,domain INTEGER NOT NULL,analysis INTEGER NOT NULL,rule INTEGER NOT NULL,facts INTEGER " "NOT NULL,target TEXT NOT NULL,candidate_count INTEGER NOT NULL,reason TEXT NOT NULL,emitted " "INTEGER NOT NULL,rules_version INTEGER NOT NULL,properties_json TEXT NOT NULL,PRIMARY " "KEY(project,ordinal))", "CREATE TABLE resolver_generations(project TEXT NOT NULL,rules_version INTEGER NOT " "NULL,PRIMARY KEY(project))", "CREATE TABLE resolver_facts(project TEXT NOT NULL,id INTEGER NOT NULL,kind INTEGER NOT " "NULL,source_file TEXT NOT NULL,digest TEXT NOT NULL,payload TEXT NOT NULL,PRIMARY " "KEY(project,id))", "CREATE TABLE resolver_supports(project TEXT NOT NULL,fact INTEGER NOT NULL,ordinal INTEGER " "NOT NULL,premise INTEGER NOT NULL,PRIMARY KEY(project,fact,ordinal))", "CREATE TABLE resolver_lookups(project TEXT NOT NULL,fact INTEGER NOT NULL,domain TEXT NOT " "NULL,key TEXT NOT NULL,status TEXT NOT NULL,PRIMARY KEY(project,fact))", "CREATE TABLE resolver_occurrence_evidence(project TEXT NOT NULL,ordinal INTEGER NOT " "NULL,content_identity TEXT NOT NULL,configuration_identity TEXT NOT NULL,proof INTEGER NOT " "NULL,source_binding_proof INTEGER NOT NULL,PRIMARY KEY(project,ordinal))", "CREATE TABLE resolver_file_summaries(project TEXT NOT NULL,file_path TEXT NOT NULL,module " "TEXT NOT NULL,language INTEGER NOT NULL,raw_content_identity TEXT NOT " "NULL,preprocessed_content_identity TEXT NOT NULL,configuration_identity TEXT NOT " "NULL,raw_length INTEGER NOT NULL,preprocessed_length INTEGER NOT NULL,extracted_summary TEXT " "NOT NULL,derived_summary TEXT NOT NULL,complete INTEGER NOT NULL,PRIMARY " "KEY(project,file_path))", "CREATE TABLE resolver_derived_bindings(project TEXT NOT NULL,file_path TEXT NOT NULL,key TEXT " "NOT NULL,kind INTEGER NOT NULL,module INTEGER NOT NULL,closed INTEGER NOT NULL,binding " "INTEGER NOT NULL,domain INTEGER NOT NULL,analysis INTEGER NOT NULL,target TEXT NOT NULL,proof " "INTEGER NOT NULL,payload_json TEXT NOT NULL DEFAULT '{}',PRIMARY " "KEY(project,file_path,key,kind))", }; static const char *const gd_resolver_inserts[] = { "INSERT INTO resolver_occurrences " "VALUES('p',1,'file_path1','scope1','relation1','spelling1',1,1,1,1,1,1,1,1,'target1',1,'" "reason1',1,1,'properties_json1')", "INSERT INTO resolver_occurrences " "VALUES('q',2,'file_path2','scope2','relation2','spelling2',2,2,2,2,2,2,2,2,'target2',2,'" "reason2',2,2,'properties_json2')", "INSERT INTO resolver_generations VALUES('p',1)", "INSERT INTO resolver_generations VALUES('q',2)", "INSERT INTO resolver_facts VALUES('p',1,1,'source_file1','digest1','payload1')", "INSERT INTO resolver_facts VALUES('q',2,2,'source_file2','digest2','payload2')", "INSERT INTO resolver_supports VALUES('p',1,1,1)", "INSERT INTO resolver_supports VALUES('q',2,2,2)", "INSERT INTO resolver_lookups VALUES('p',1,'domain1','key1','status1')", "INSERT INTO resolver_lookups VALUES('q',2,'domain2','key2','status2')", "INSERT INTO resolver_occurrence_evidence " "VALUES('p',1,'content_identity1','configuration_identity1',1,1)", "INSERT INTO resolver_occurrence_evidence " "VALUES('q',2,'content_identity2','configuration_identity2',2,2)", "INSERT INTO resolver_file_summaries " "VALUES('p','file_path1','module1',1,'raw_content_identity1','preprocessed_content_identity1','" "configuration_identity1',1,1,'extracted_summary1','derived_summary1',1)", "INSERT INTO resolver_file_summaries " "VALUES('q','file_path2','module2',2,'raw_content_identity2','preprocessed_content_identity2','" "configuration_identity2',2,2,'extracted_summary2','derived_summary2',2)", "INSERT INTO resolver_derived_bindings " "VALUES('p','file_path1','key1',1,1,1,1,1,1,'target1',1,'{}')", "INSERT INTO resolver_derived_bindings " "VALUES('q','file_path2','key2',2,2,2,2,2,2,'target2',2,'{}')", }; static cbm_store_t *gd_resolver_fixture(bool reverse) { cbm_store_t *s = gd_fixture(reverse, false); sqlite3 *db = s ? cbm_store_get_db(s) : NULL; bool ok = db != NULL; for (size_t i = 0; ok && i < sizeof(gd_resolver_schema) / sizeof(gd_resolver_schema[0]); i++) ok = gd_sql(db, gd_resolver_schema[i]) == SQLITE_OK; size_t n = sizeof(gd_resolver_inserts) / sizeof(gd_resolver_inserts[0]); for (size_t i = 0; ok && i < n; i++) ok = gd_sql(db, gd_resolver_inserts[reverse ? n - i - 1 : i]) == SQLITE_OK; if (!ok) { cbm_store_close(s); return NULL; } return s; } TEST(store_graph_digest_resolver_and_capture_metadata_are_independent) { cbm_store_t *store = gd_resolver_fixture(false); sqlite3 *db = store ? cbm_store_get_db(store) : NULL; bool setup = db && gd_sql(db, "CREATE TABLE index_unresolved_candidates(project TEXT NOT NULL," "candidate TEXT NOT NULL,rel_path TEXT NOT NULL,kind TEXT NOT NULL," "PRIMARY KEY(project,candidate,rel_path));" "INSERT INTO index_unresolved_candidates VALUES" "('p','p.target','calls.js','unresolved_calls');" "ALTER TABLE index_coverage_meta ADD COLUMN " "unresolved_calls_complete INTEGER NOT NULL DEFAULT 0;") == SQLITE_OK; cbm_store_graph_digest_t initial = {0}, candidate = {0}, capture = {0}, resolver = {0}, without_candidate = {0}; bool before = setup && gd_p(store, &initial); bool candidate_bound = before && gd_sql(db, "UPDATE index_unresolved_candidates SET candidate='p.changed';") == SQLITE_OK && gd_p(store, &candidate); bool capture_bound = candidate_bound && gd_sql(db, "UPDATE index_coverage_meta SET unresolved_calls_complete=1;") == SQLITE_OK && gd_p(store, &capture); bool resolver_bound = capture_bound && gd_sql(db, "UPDATE resolver_occurrences SET target='changed' WHERE project='q';") == SQLITE_OK && gd_p(store, &resolver); /* The optional candidate table is independent of the atomic resolver group. */ bool candidate_absent = resolver_bound && gd_sql(db, "DROP TABLE index_unresolved_candidates;") == SQLITE_OK && gd_p(store, &without_candidate); bool partial_resolver_rejected = candidate_absent && gd_sql(db, "DROP TABLE resolver_facts;") == SQLITE_OK && gd_reject(store, CBM_STORE_GRAPH_DIGEST_SCHEMA); cbm_store_close(store); ASSERT_TRUE(setup); ASSERT_TRUE(before); ASSERT_TRUE(candidate_bound); ASSERT_TRUE(gd_different(&initial, &candidate)); ASSERT_TRUE(capture_bound); ASSERT_TRUE(gd_different(&candidate, &capture)); ASSERT_TRUE(resolver_bound); ASSERT_TRUE(gd_different(&capture, &resolver)); ASSERT_TRUE(candidate_absent); ASSERT_TRUE(gd_different(&resolver, &without_candidate)); ASSERT_EQ(without_candidate.rows + 1, resolver.rows); ASSERT_TRUE(partial_resolver_rejected); PASS(); } TEST(store_graph_digest_resolver_values_and_removals_are_bound) { static const char *const changes[] = { "UPDATE resolver_occurrences SET project='changed' WHERE project='q'", "UPDATE resolver_occurrences SET ordinal=99 WHERE project='q'", "UPDATE resolver_occurrences SET file_path='changed' WHERE project='q'", "UPDATE resolver_occurrences SET scope='changed' WHERE project='q'", "UPDATE resolver_occurrences SET relation='changed' WHERE project='q'", "UPDATE resolver_occurrences SET spelling='changed' WHERE project='q'", "UPDATE resolver_occurrences SET start_byte=99 WHERE project='q'", "UPDATE resolver_occurrences SET end_byte=99 WHERE project='q'", "UPDATE resolver_occurrences SET source_origin=99 WHERE project='q'", "UPDATE resolver_occurrences SET binding=99 WHERE project='q'", "UPDATE resolver_occurrences SET domain=99 WHERE project='q'", "UPDATE resolver_occurrences SET analysis=99 WHERE project='q'", "UPDATE resolver_occurrences SET rule=99 WHERE project='q'", "UPDATE resolver_occurrences SET facts=99 WHERE project='q'", "UPDATE resolver_occurrences SET target='changed' WHERE project='q'", "UPDATE resolver_occurrences SET candidate_count=99 WHERE project='q'", "UPDATE resolver_occurrences SET reason='changed' WHERE project='q'", "UPDATE resolver_occurrences SET emitted=99 WHERE project='q'", "UPDATE resolver_occurrences SET rules_version=99 WHERE project='q'", "UPDATE resolver_occurrences SET properties_json='changed' WHERE project='q'", "DELETE FROM resolver_occurrences WHERE project='q'", "UPDATE resolver_generations SET project='changed' WHERE project='q'", "UPDATE resolver_generations SET rules_version=99 WHERE project='q'", "DELETE FROM resolver_generations WHERE project='q'", "UPDATE resolver_facts SET project='changed' WHERE project='q'", "UPDATE resolver_facts SET id=99 WHERE project='q'", "UPDATE resolver_facts SET kind=99 WHERE project='q'", "UPDATE resolver_facts SET source_file='changed' WHERE project='q'", "UPDATE resolver_facts SET digest='changed' WHERE project='q'", "UPDATE resolver_facts SET payload='changed' WHERE project='q'", "DELETE FROM resolver_facts WHERE project='q'", "UPDATE resolver_supports SET project='changed' WHERE project='q'", "UPDATE resolver_supports SET fact=99 WHERE project='q'", "UPDATE resolver_supports SET ordinal=99 WHERE project='q'", "UPDATE resolver_supports SET premise=99 WHERE project='q'", "DELETE FROM resolver_supports WHERE project='q'", "UPDATE resolver_lookups SET project='changed' WHERE project='q'", "UPDATE resolver_lookups SET fact=99 WHERE project='q'", "UPDATE resolver_lookups SET domain='changed' WHERE project='q'", "UPDATE resolver_lookups SET key='changed' WHERE project='q'", "UPDATE resolver_lookups SET status='changed' WHERE project='q'", "DELETE FROM resolver_lookups WHERE project='q'", "UPDATE resolver_occurrence_evidence SET project='changed' WHERE project='q'", "UPDATE resolver_occurrence_evidence SET ordinal=99 WHERE project='q'", "UPDATE resolver_occurrence_evidence SET content_identity='changed' WHERE project='q'", "UPDATE resolver_occurrence_evidence SET configuration_identity='changed' WHERE " "project='q'", "UPDATE resolver_occurrence_evidence SET proof=99 WHERE project='q'", "UPDATE resolver_occurrence_evidence SET source_binding_proof=99 WHERE project='q'", "DELETE FROM resolver_occurrence_evidence WHERE project='q'", "UPDATE resolver_file_summaries SET project='changed' WHERE project='q'", "UPDATE resolver_file_summaries SET file_path='changed' WHERE project='q'", "UPDATE resolver_file_summaries SET module='changed' WHERE project='q'", "UPDATE resolver_file_summaries SET language=99 WHERE project='q'", "UPDATE resolver_file_summaries SET raw_content_identity='changed' WHERE project='q'", "UPDATE resolver_file_summaries SET preprocessed_content_identity='changed' WHERE " "project='q'", "UPDATE resolver_file_summaries SET configuration_identity='changed' WHERE project='q'", "UPDATE resolver_file_summaries SET raw_length=99 WHERE project='q'", "UPDATE resolver_file_summaries SET preprocessed_length=99 WHERE project='q'", "UPDATE resolver_file_summaries SET extracted_summary='changed' WHERE project='q'", "UPDATE resolver_file_summaries SET derived_summary='changed' WHERE project='q'", "UPDATE resolver_file_summaries SET complete=99 WHERE project='q'", "DELETE FROM resolver_file_summaries WHERE project='q'", "UPDATE resolver_derived_bindings SET project='changed' WHERE project='q'", "UPDATE resolver_derived_bindings SET file_path='changed' WHERE project='q'", "UPDATE resolver_derived_bindings SET key='changed' WHERE project='q'", "UPDATE resolver_derived_bindings SET kind=99 WHERE project='q'", "UPDATE resolver_derived_bindings SET module=99 WHERE project='q'", "UPDATE resolver_derived_bindings SET closed=99 WHERE project='q'", "UPDATE resolver_derived_bindings SET binding=99 WHERE project='q'", "UPDATE resolver_derived_bindings SET domain=99 WHERE project='q'", "UPDATE resolver_derived_bindings SET analysis=99 WHERE project='q'", "UPDATE resolver_derived_bindings SET target='changed' WHERE project='q'", "UPDATE resolver_derived_bindings SET proof=99 WHERE project='q'", "UPDATE resolver_derived_bindings SET payload_json='changed' WHERE project='q'", "DELETE FROM resolver_derived_bindings WHERE project='q'", }; for (size_t i = 0; i < sizeof(changes) / sizeof(changes[0]); i++) { cbm_store_t *s = gd_resolver_fixture(false); cbm_store_graph_digest_t a = {0}, b = {0}; bool control = s && gd_p(s, &a); bool changed = control && gd_sql(cbm_store_get_db(s), changes[i]) == SQLITE_OK; bool read = changed && gd_p(s, &b); cbm_store_close(s); if (!read || !gd_different(&a, &b)) { fprintf(stderr, "resolver digest mutation %zu: %s\n", i, changes[i]); FAIL("resolver content was not bound"); } } PASS(); } TEST(store_graph_digest_resolver_canonical_golden_and_order) { static const unsigned char expected[32] = {0x74, 0x20, 0xbd, 0x40, 0x60, 0xbf, 0x80, 0x54, 0x55, 0x92, 0x0a, 0x04, 0xf1, 0xdf, 0xbe, 0xba, 0xe9, 0x3e, 0xbe, 0xae, 0x95, 0x9e, 0x74, 0x39, 0x28, 0xe9, 0x8e, 0x52, 0xf6, 0x71, 0x57, 0xfa}; cbm_store_t *a = gd_resolver_fixture(false), *b = gd_resolver_fixture(true); cbm_store_graph_digest_t x = {0}, y = {0}; bool ok = a && b && gd_p(a, &x) && gd_p(b, &y); cbm_store_close(a); cbm_store_close(b); ASSERT_TRUE(ok); ASSERT_EQ(x.version, 3); ASSERT_TRUE(x.rows == UINT64_C(33)); ASSERT_TRUE(x.framed_bytes == UINT64_C(10486)); ASSERT_TRUE(memcmp(x.sha256, expected, sizeof(expected)) == 0); ASSERT_TRUE(gd_equal(&x, &y)); PASS(); } TEST(store_graph_digest_resolver_partial_schema_and_bad_types_fail_closed) { static const char *const bad[] = { "DROP TABLE resolver_occurrences", "DROP TABLE resolver_generations", "DROP TABLE resolver_facts", "DROP TABLE resolver_supports", "DROP TABLE resolver_lookups", "DROP TABLE resolver_occurrence_evidence", "DROP TABLE resolver_file_summaries", "DROP TABLE resolver_derived_bindings", "UPDATE resolver_occurrences SET source_origin=1.5 WHERE project='q'", "UPDATE resolver_occurrence_evidence SET content_identity=X'6162' WHERE project='q'", "UPDATE resolver_generations SET rules_version='unknown' WHERE project='q'", "CREATE TEMP TABLE resolver_facts(project TEXT)", "ALTER TABLE resolver_facts RENAME TO saved_facts;CREATE VIEW resolver_facts AS SELECT * " "FROM saved_facts", "ALTER TABLE resolver_supports RENAME TO saved_supports;CREATE TABLE " "resolver_supports(project TEXT NOT NULL,fact INTEGER NOT NULL,ordinal INTEGER NOT " "NULL,premise INTEGER NOT NULL,PRIMARY KEY(project,ordinal,fact));INSERT INTO " "resolver_supports SELECT * FROM saved_supports", "ALTER TABLE resolver_lookups ADD COLUMN ignored TEXT", "ALTER TABLE resolver_file_summaries DROP COLUMN complete", }; for (size_t i = 0; i < sizeof(bad) / sizeof(bad[0]); i++) { cbm_store_t *s = gd_resolver_fixture(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, "resolver schema mutation %zu: %s\n", i, bad[i]); FAIL("incomplete or malformed resolver state was accepted"); } } cbm_store_t *legacy = gd_fixture(false, false); bool added = legacy && gd_sql(cbm_store_get_db(legacy), gd_resolver_schema[1]) == SQLITE_OK; bool rejected = added && gd_reject(legacy, CBM_STORE_GRAPH_DIGEST_SCHEMA); cbm_store_close(legacy); ASSERT_TRUE(added); ASSERT_TRUE(rejected); PASS(); } TEST(store_graph_digest_resolver_rows_and_bytes_obey_limits) { cbm_store_t *s = gd_resolver_fixture(false); cbm_store_graph_digest_t full = {0}; bool control = s && gd_p(s, &full); cbm_store_close(s); ASSERT_TRUE(control); ASSERT_EQ(full.rows, 33); cbm_store_graph_digest_limits_t limits[] = {{full.rows, full.framed_bytes}, {full.rows - 1, full.framed_bytes}, {full.rows, full.framed_bytes - 1}}; for (size_t i = 0; i < sizeof(limits) / sizeof(limits[0]); i++) { s = gd_resolver_fixture(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; memset(&out, 0xa5, sizeof(out)); int rc = opened == CBM_STORE_OK ? cbm_store_graph_digest(scope, (const unsigned char *)"p", 1, &limits[i], &out) : -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_EQ(closed, CBM_STORE_OK); ASSERT_TRUE(gd_equal(&full, &out)); } else { ASSERT_EQ(rc, CBM_STORE_GRAPH_DIGEST_LIMIT); ASSERT_EQ(closed, CBM_STORE_ERR); ASSERT_TRUE(gd_zero(&out)); } } PASS(); } TEST(store_graph_digest_resolver_live_schema_and_explicit_absence) { cbm_store_t *s = cbm_store_open_memory(); cbm_store_graph_digest_t absent = {0}, present = {0}; bool seeded = s && cbm_store_upsert_project(s, "p", "/live") == CBM_STORE_OK; bool before = seeded && gd_p(s, &absent); bool replaced = before && cbm_store_resolver_replace(s, "p", NULL, 0) == CBM_STORE_OK; bool after = replaced && gd_p(s, &present); bool added_rows = after && gd_int(cbm_store_get_db(s), "SELECT count(*) FROM resolver_generations WHERE project='p'", 1) && gd_int(cbm_store_get_db(s), "SELECT count(*) FROM store_meta WHERE k='resolver_receipt:p' " "AND v='1'", 1); cbm_store_close(s); ASSERT_TRUE(before); ASSERT_TRUE(replaced); ASSERT_TRUE(after); ASSERT_TRUE(gd_different(&absent, &present)); ASSERT_TRUE(added_rows); ASSERT_EQ(present.rows, absent.rows + 2); PASS(); } SUITE(store_graph_digest) { RUN_TEST(store_graph_digest_resolver_and_capture_metadata_are_independent); RUN_TEST(store_graph_digest_resolver_values_and_removals_are_bound); RUN_TEST(store_graph_digest_resolver_canonical_golden_and_order); RUN_TEST(store_graph_digest_resolver_partial_schema_and_bad_types_fail_closed); RUN_TEST(store_graph_digest_resolver_rows_and_bytes_obey_limits); RUN_TEST(store_graph_digest_resolver_live_schema_and_explicit_absence); RUN_TEST(store_graph_digest_binds_unresolved_candidate_index); RUN_TEST(store_graph_digest_candidate_index_errors_are_not_legacy_absence); 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_binds_unresolved_capture_completeness); 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); }