Files
codebase-memory-mcp/tests/test_store_graph_digest.c
T
Martin Vogel ea54e63d55 Merge pinned main 7b398de4 into the strict shared resolver
Resolves the nine textual conflicts with the reviewed reconciliation: semantic index version 12 on top of main's migrations, digest stream v3 because both sides independently claimed v2, incremental metadata-current requires main completeness and the strict resolver generation receipt, and exact call joins by extraction index, full span and origin. Unresolved-call diagnostics reconcile only after final proof sealing and successful projection.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-31 21:34:07 +01:00

1449 lines
72 KiB
C

#include "test_framework.h"
#include "test_helpers.h"
#include <store/store_graph_digest.h>
#include <store/store_resolver.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] = {
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);
}