Files
codebase-memory-mcp/tests/test_graph_buffer.c
juan Carlos Estrada MontoyaandClaude Opus 5 460ebefc87 fix(graph-buffer): log why an atomic publish failed instead of a successful dump
cbm_gbuf_dump_to_sqlite() emitted gbuf.dump regardless of the writer's
result, so a run that published nothing still logged node and edge counts
as if it had worked, and the failure reached the user only as the generic
"Pipeline failed. Check repo_path exists and contains source files."

The writer's temp -> final rename in publish_writer_output() returned a
bare ERR_WRITE_FAILED. #1628 taught cbm_rename_replace() to translate the
platform error into errno so callers could report why, and wired up the
stage -> final rename in pipeline.c. This is the other rename on that
path, and it is the one that runs first.

Preserve errno across the cleanup in every publish error branch. That
includes cbm_writer_open(), where the cleanup unlinks a file that was never
created: without the save it leaves ENOENT behind, so a directory that
denied the create is reported as a missing path — the #1620 case, stated
wrongly. Read the value in graph_buffer.c before the profiling macro can
clobber it, and report gbuf.dump_failed with the return code and that errno.
A sticky append failure clears errno rather than attach a reason it does not
have, and the field is omitted when there is none. No logging is added
inside internal/cbm/, which has none today.

Tests cover both halves: the dump reports a failure instead of a summary,
the writer names its truncation reason, and the publish rename's errno
survives a cleanup that itself fails.

Refs #1620, #2001.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012FxkVF773gDGNmBmsLnzN7
Signed-off-by: juan Carlos Estrada Montoya <junk151516@users.noreply.github.com>
2026-09-20 17:52:45 +02:00

1298 lines
47 KiB
C

/*
* test_graph_buffer.c — Tests for in-memory graph buffer.
*
* RED phase: Tests define expected behavior for node/edge insertion,
* lookup, dedup, delete, and dump to SQLite.
*/
#include "test_framework.h"
#include "graph_buffer/graph_buffer.h"
#include "foundation/mem_core.h"
#include <stdatomic.h>
#include "store/store.h"
#include "../src/foundation/compat.h"
#include "foundation/compat_fs.h"
#include "foundation/log.h"
#include <stdio.h>
#include <string.h>
/* ── Node operations ───────────────────────────────────────────── */
TEST(gbuf_create_free) {
cbm_gbuf_t *gb = cbm_gbuf_new("test-proj", "/tmp/repo");
ASSERT_NOT_NULL(gb);
ASSERT_EQ(cbm_gbuf_node_count(gb), 0);
ASSERT_EQ(cbm_gbuf_edge_count(gb), 0);
cbm_gbuf_free(gb);
PASS();
}
TEST(gbuf_free_null) {
cbm_gbuf_free(NULL); /* should not crash */
PASS();
}
TEST(gbuf_upsert_node) {
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
int64_t id = cbm_gbuf_upsert_node(gb, "Function", "main", "pkg.main", "main.go", 1, 10, "{}");
ASSERT_GT(id, 0);
ASSERT_EQ(cbm_gbuf_node_count(gb), 1);
const cbm_gbuf_node_t *n = cbm_gbuf_find_by_qn(gb, "pkg.main");
ASSERT_NOT_NULL(n);
ASSERT_STR_EQ(n->label, "Function");
ASSERT_STR_EQ(n->name, "main");
ASSERT_STR_EQ(n->qualified_name, "pkg.main");
ASSERT_STR_EQ(n->file_path, "main.go");
ASSERT_EQ(n->start_line, 1);
ASSERT_EQ(n->end_line, 10);
ASSERT_EQ(n->id, id);
cbm_gbuf_free(gb);
PASS();
}
TEST(gbuf_upsert_updates) {
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
int64_t id1 = cbm_gbuf_upsert_node(gb, "Function", "main", "pkg.main", "main.go", 1, 10, "{}");
/* Upsert same QN with different fields */
int64_t id2 = cbm_gbuf_upsert_node(gb, "Method", "main", "pkg.main", "main.go", 5, 20,
"{\"key\":\"val\"}");
ASSERT_EQ(id1, id2); /* same temp ID */
ASSERT_EQ(cbm_gbuf_node_count(gb), 1); /* still one node */
const cbm_gbuf_node_t *n = cbm_gbuf_find_by_qn(gb, "pkg.main");
ASSERT_NOT_NULL(n);
ASSERT_STR_EQ(n->label, "Method"); /* updated */
ASSERT_EQ(n->end_line, 20); /* updated */
cbm_gbuf_free(gb);
PASS();
}
TEST(gbuf_find_by_id) {
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
int64_t id = cbm_gbuf_upsert_node(gb, "Function", "foo", "pkg.foo", "foo.go", 1, 5, "{}");
const cbm_gbuf_node_t *n = cbm_gbuf_find_by_id(gb, id);
ASSERT_NOT_NULL(n);
ASSERT_STR_EQ(n->name, "foo");
/* Not found */
ASSERT_NULL(cbm_gbuf_find_by_id(gb, 999));
cbm_gbuf_free(gb);
PASS();
}
TEST(gbuf_find_by_label) {
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
cbm_gbuf_upsert_node(gb, "Function", "foo", "pkg.foo", "f.go", 1, 5, "{}");
cbm_gbuf_upsert_node(gb, "Function", "bar", "pkg.bar", "f.go", 6, 10, "{}");
cbm_gbuf_upsert_node(gb, "Class", "Baz", "pkg.Baz", "f.go", 11, 20, "{}");
const cbm_gbuf_node_t **nodes = NULL;
int count = 0;
int rc = cbm_gbuf_find_by_label(gb, "Function", &nodes, &count);
ASSERT_EQ(rc, 0);
ASSERT_EQ(count, 2);
rc = cbm_gbuf_find_by_label(gb, "Class", &nodes, &count);
ASSERT_EQ(rc, 0);
ASSERT_EQ(count, 1);
rc = cbm_gbuf_find_by_label(gb, "Module", &nodes, &count);
ASSERT_EQ(rc, 0);
ASSERT_EQ(count, 0);
cbm_gbuf_free(gb);
PASS();
}
TEST(gbuf_find_by_name) {
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
cbm_gbuf_upsert_node(gb, "Function", "main", "a.main", "a.go", 1, 5, "{}");
cbm_gbuf_upsert_node(gb, "Function", "main", "b.main", "b.go", 1, 5, "{}");
const cbm_gbuf_node_t **nodes = NULL;
int count = 0;
int rc = cbm_gbuf_find_by_name(gb, "main", &nodes, &count);
ASSERT_EQ(rc, 0);
ASSERT_EQ(count, 2);
cbm_gbuf_free(gb);
PASS();
}
TEST(gbuf_delete_by_label) {
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
int64_t f1 = cbm_gbuf_upsert_node(gb, "Function", "foo", "pkg.foo", "f.go", 1, 5, "{}");
int64_t f2 = cbm_gbuf_upsert_node(gb, "Function", "bar", "pkg.bar", "f.go", 6, 10, "{}");
cbm_gbuf_upsert_node(gb, "Class", "Baz", "pkg.Baz", "f.go", 11, 20, "{}");
/* Add edge between functions */
cbm_gbuf_insert_edge(gb, f1, f2, "CALLS", "{}");
ASSERT_EQ(cbm_gbuf_edge_count(gb), 1);
/* Delete all functions — should cascade-delete the CALLS edge */
cbm_gbuf_delete_by_label(gb, "Function");
ASSERT_EQ(cbm_gbuf_node_count(gb), 1); /* only Class remains */
ASSERT_EQ(cbm_gbuf_edge_count(gb), 0); /* edge cascade-deleted */
ASSERT_NULL(cbm_gbuf_find_by_qn(gb, "pkg.foo"));
ASSERT_NULL(cbm_gbuf_find_by_qn(gb, "pkg.bar"));
ASSERT_NOT_NULL(cbm_gbuf_find_by_qn(gb, "pkg.Baz"));
cbm_gbuf_free(gb);
PASS();
}
/* ── Edge operations ───────────────────────────────────────────── */
TEST(gbuf_insert_edge) {
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
int64_t n1 = cbm_gbuf_upsert_node(gb, "Function", "a", "pkg.a", "f.go", 1, 5, "{}");
int64_t n2 = cbm_gbuf_upsert_node(gb, "Function", "b", "pkg.b", "f.go", 6, 10, "{}");
int64_t eid = cbm_gbuf_insert_edge(gb, n1, n2, "CALLS", "{}");
ASSERT_GT(eid, 0);
ASSERT_EQ(cbm_gbuf_edge_count(gb), 1);
cbm_gbuf_free(gb);
PASS();
}
TEST(gbuf_edge_dedup) {
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
int64_t n1 = cbm_gbuf_upsert_node(gb, "Function", "a", "pkg.a", "f.go", 1, 5, "{}");
int64_t n2 = cbm_gbuf_upsert_node(gb, "Function", "b", "pkg.b", "f.go", 6, 10, "{}");
int64_t eid1 = cbm_gbuf_insert_edge(gb, n1, n2, "CALLS", "{}");
int64_t eid2 = cbm_gbuf_insert_edge(gb, n1, n2, "CALLS", "{\"weight\":5}");
ASSERT_EQ(eid1, eid2); /* same edge, deduped */
ASSERT_EQ(cbm_gbuf_edge_count(gb), 1);
/* Different type = different edge */
int64_t eid3 = cbm_gbuf_insert_edge(gb, n1, n2, "IMPORTS", "{}");
ASSERT_NEQ(eid1, eid3);
ASSERT_EQ(cbm_gbuf_edge_count(gb), 2);
cbm_gbuf_free(gb);
PASS();
}
/* Properties on a deduped edge must not depend on arrival order.
*
* confidence/strategy/via are not part of the edge key, and the same logical
* CALLS edge is legitimately minted by two strategies (LSP resolution and
* registry-textual matching) carrying different values. Per-worker edge buffers
* merge in worker-slot order, so an arrival-order-dependent merge makes the
* stored attributes a function of thread scheduling. Insert the same pair of
* blobs in both orders; the stored properties must match. */
TEST(gbuf_edge_props_merge_is_order_independent) {
const char *lsp = "{\"callee\":\"f\",\"confidence\":0.95,\"strategy\":\"lsp\"}";
const char *txt = "{\"callee\":\"f\",\"confidence\":0.40,\"strategy\":\"registry\"}";
cbm_gbuf_t *fwd = cbm_gbuf_new("test", "/tmp");
int64_t a1 = cbm_gbuf_upsert_node(fwd, "Function", "a", "pkg.a", "f.go", 1, 5, "{}");
int64_t b1 = cbm_gbuf_upsert_node(fwd, "Function", "b", "pkg.b", "f.go", 6, 10, "{}");
cbm_gbuf_insert_edge(fwd, a1, b1, "CALLS", lsp);
cbm_gbuf_insert_edge(fwd, a1, b1, "CALLS", txt);
cbm_gbuf_t *rev = cbm_gbuf_new("test", "/tmp");
int64_t a2 = cbm_gbuf_upsert_node(rev, "Function", "a", "pkg.a", "f.go", 1, 5, "{}");
int64_t b2 = cbm_gbuf_upsert_node(rev, "Function", "b", "pkg.b", "f.go", 6, 10, "{}");
cbm_gbuf_insert_edge(rev, a2, b2, "CALLS", txt);
cbm_gbuf_insert_edge(rev, a2, b2, "CALLS", lsp);
const cbm_gbuf_edge_t **fe = NULL;
const cbm_gbuf_edge_t **re = NULL;
int fc = 0;
int rc = 0;
cbm_gbuf_find_edges_by_type(fwd, "CALLS", &fe, &fc);
cbm_gbuf_find_edges_by_type(rev, "CALLS", &re, &rc);
ASSERT_EQ(fc, 1);
ASSERT_EQ(rc, 1);
ASSERT_STR_EQ(fe[0]->properties_json, re[0]->properties_json);
cbm_gbuf_free(fwd);
cbm_gbuf_free(rev);
PASS();
}
/* The order-independent winner is also the semantically right one: a
* higher-confidence discovery outranks a lower-confidence one regardless of
* which arrived first. */
TEST(gbuf_edge_props_merge_prefers_higher_confidence) {
const char *lsp = "{\"callee\":\"f\",\"confidence\":0.95,\"strategy\":\"lsp\"}";
const char *txt = "{\"callee\":\"f\",\"confidence\":0.40,\"strategy\":\"registry\"}";
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
int64_t a = cbm_gbuf_upsert_node(gb, "Function", "a", "pkg.a", "f.go", 1, 5, "{}");
int64_t b = cbm_gbuf_upsert_node(gb, "Function", "b", "pkg.b", "f.go", 6, 10, "{}");
cbm_gbuf_insert_edge(gb, a, b, "CALLS", lsp);
cbm_gbuf_insert_edge(gb, a, b, "CALLS", txt); /* lower confidence, arrives last */
const cbm_gbuf_edge_t **edges = NULL;
int count = 0;
cbm_gbuf_find_edges_by_type(gb, "CALLS", &edges, &count);
ASSERT_EQ(count, 1);
ASSERT_TRUE(strstr(edges[0]->properties_json, "\"strategy\":\"lsp\"") != NULL);
cbm_gbuf_free(gb);
PASS();
}
/* A confidence the code cannot read is not evidence of anything, so it must
* not outrank an edge that simply carries no confidence at all.
*
* edge_props_confidence answers -1 for "absent" so that any real confidence
* beats it. strtod answers 0.0 for text it cannot read, so an unreadable
* value used to come back as a real confidence of zero -- which beats -1 and
* displaced the stored blob. The function's own comment already promised
* that "absent/unparseable reads as -1"; only the absent half was true. */
TEST(gbuf_edge_props_unreadable_confidence_does_not_displace_absent) {
const char *no_conf = "{\"callee\":\"f\",\"strategy\":\"lsp\"}";
const char *bad_conf = "{\"callee\":\"f\",\"confidence\":null,\"strategy\":\"registry\"}";
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
int64_t a = cbm_gbuf_upsert_node(gb, "Function", "a", "pkg.a", "f.go", 1, 5, "{}");
int64_t b = cbm_gbuf_upsert_node(gb, "Function", "b", "pkg.b", "f.go", 6, 10, "{}");
cbm_gbuf_insert_edge(gb, a, b, "CALLS", no_conf);
cbm_gbuf_insert_edge(gb, a, b, "CALLS", bad_conf); /* unreadable, arrives last */
const cbm_gbuf_edge_t **edges = NULL;
int count = 0;
cbm_gbuf_find_edges_by_type(gb, "CALLS", &edges, &count);
ASSERT_EQ(count, 1);
ASSERT_TRUE(strstr(edges[0]->properties_json, "\"strategy\":\"lsp\"") != NULL);
cbm_gbuf_free(gb);
PASS();
}
/* An empty incoming blob must never displace real stored properties. */
TEST(gbuf_edge_props_merge_keeps_existing_on_empty) {
const char *lsp = "{\"callee\":\"f\",\"confidence\":0.95,\"strategy\":\"lsp\"}";
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
int64_t a = cbm_gbuf_upsert_node(gb, "Function", "a", "pkg.a", "f.go", 1, 5, "{}");
int64_t b = cbm_gbuf_upsert_node(gb, "Function", "b", "pkg.b", "f.go", 6, 10, "{}");
cbm_gbuf_insert_edge(gb, a, b, "CALLS", lsp);
cbm_gbuf_insert_edge(gb, a, b, "CALLS", "{}");
const cbm_gbuf_edge_t **edges = NULL;
int count = 0;
cbm_gbuf_find_edges_by_type(gb, "CALLS", &edges, &count);
ASSERT_EQ(count, 1);
ASSERT_TRUE(strstr(edges[0]->properties_json, "\"strategy\":\"lsp\"") != NULL);
cbm_gbuf_free(gb);
PASS();
}
/* #768: two named imports from the same specifier (same source, same target
* file) must produce two distinct IMPORTS edges, keyed apart by local_name --
* not collapse into one edge that silently drops whichever import lost the
* dedup race. Re-inserting the SAME local_name (e.g. an idempotent re-index)
* must still dedup to one edge. */
TEST(gbuf_imports_multi_symbol_dedup) {
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
int64_t consumer =
cbm_gbuf_upsert_node(gb, "File", "consumer.ts", "pkg.consumer", "consumer.ts", 1, 1, "{}");
int64_t lib = cbm_gbuf_upsert_node(gb, "File", "lib.ts", "pkg.lib", "lib.ts", 1, 1, "{}");
int64_t eid_a = cbm_gbuf_insert_edge(gb, consumer, lib, "IMPORTS", "{\"local_name\":\"A\"}");
int64_t eid_b = cbm_gbuf_insert_edge(gb, consumer, lib, "IMPORTS", "{\"local_name\":\"B\"}");
ASSERT_GT(eid_a, 0);
ASSERT_GT(eid_b, 0);
ASSERT_NEQ(eid_a, eid_b); /* distinct symbols -> distinct edges */
ASSERT_EQ(cbm_gbuf_edge_count(gb), 2);
const cbm_gbuf_edge_t **edges = NULL;
int count = 0;
cbm_gbuf_find_edges_by_source_type(gb, consumer, "IMPORTS", &edges, &count);
ASSERT_EQ(count, 2);
ASSERT_TRUE(strstr(edges[0]->properties_json, "\"local_name\":\"A\"") != NULL ||
strstr(edges[1]->properties_json, "\"local_name\":\"A\"") != NULL);
ASSERT_TRUE(strstr(edges[0]->properties_json, "\"local_name\":\"B\"") != NULL ||
strstr(edges[1]->properties_json, "\"local_name\":\"B\"") != NULL);
/* Re-inserting the same symbol (idempotent re-index) still dedups. */
int64_t eid_a_again =
cbm_gbuf_insert_edge(gb, consumer, lib, "IMPORTS", "{\"local_name\":\"A\"}");
ASSERT_EQ(eid_a_again, eid_a);
ASSERT_EQ(cbm_gbuf_edge_count(gb), 2);
cbm_gbuf_free(gb);
PASS();
}
/* #768 hardening: the dedup key lives in a fixed-size stack buffer. Two long
* local_names sharing a prefix must NOT silently collide when the verbatim
* key would be truncated — the key builder re-keys oversized local_names with
* a hash of the FULL name. Determinism must hold: re-inserting the same long
* name still dedups to the same edge. */
TEST(gbuf_imports_long_local_name_no_collision) {
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
int64_t consumer =
cbm_gbuf_upsert_node(gb, "File", "consumer.ts", "pkg.consumer", "consumer.ts", 1, 1, "{}");
int64_t lib = cbm_gbuf_upsert_node(gb, "File", "lib.ts", "pkg.lib", "lib.ts", 1, 1, "{}");
/* 300-char shared prefix, distinct 4-char tails — a truncated verbatim
* key keeps only the shared prefix and would merge the two edges. */
enum { LONG_PREFIX = 300 };
char name_a[LONG_PREFIX + 8];
char name_b[LONG_PREFIX + 8];
memset(name_a, 'x', LONG_PREFIX);
memset(name_b, 'x', LONG_PREFIX);
memcpy(name_a + LONG_PREFIX, "AAAA", 5);
memcpy(name_b + LONG_PREFIX, "BBBB", 5);
char props_a[512];
char props_b[512];
snprintf(props_a, sizeof(props_a), "{\"local_name\":\"%s\"}", name_a);
snprintf(props_b, sizeof(props_b), "{\"local_name\":\"%s\"}", name_b);
int64_t eid_a = cbm_gbuf_insert_edge(gb, consumer, lib, "IMPORTS", props_a);
int64_t eid_b = cbm_gbuf_insert_edge(gb, consumer, lib, "IMPORTS", props_b);
ASSERT_GT(eid_a, 0);
ASSERT_GT(eid_b, 0);
ASSERT_NEQ(eid_a, eid_b); /* prefix-sharing long names stay distinct */
ASSERT_EQ(cbm_gbuf_edge_count(gb), 2);
/* Hash re-keying is deterministic: same long name dedups. */
int64_t eid_a_again = cbm_gbuf_insert_edge(gb, consumer, lib, "IMPORTS", props_a);
ASSERT_EQ(eid_a_again, eid_a);
ASSERT_EQ(cbm_gbuf_edge_count(gb), 2);
cbm_gbuf_free(gb);
PASS();
}
TEST(gbuf_find_edges_by_source_type) {
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
int64_t a = cbm_gbuf_upsert_node(gb, "Function", "a", "pkg.a", "f.go", 1, 5, "{}");
int64_t b = cbm_gbuf_upsert_node(gb, "Function", "b", "pkg.b", "f.go", 6, 10, "{}");
int64_t c = cbm_gbuf_upsert_node(gb, "Function", "c", "pkg.c", "f.go", 11, 15, "{}");
cbm_gbuf_insert_edge(gb, a, b, "CALLS", "{}");
cbm_gbuf_insert_edge(gb, a, c, "CALLS", "{}");
cbm_gbuf_insert_edge(gb, a, b, "IMPORTS", "{}");
const cbm_gbuf_edge_t **edges = NULL;
int count = 0;
cbm_gbuf_find_edges_by_source_type(gb, a, "CALLS", &edges, &count);
ASSERT_EQ(count, 2);
cbm_gbuf_find_edges_by_source_type(gb, a, "IMPORTS", &edges, &count);
ASSERT_EQ(count, 1);
cbm_gbuf_find_edges_by_source_type(gb, b, "CALLS", &edges, &count);
ASSERT_EQ(count, 0);
cbm_gbuf_free(gb);
PASS();
}
TEST(gbuf_find_edges_by_target_type) {
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
int64_t a = cbm_gbuf_upsert_node(gb, "Function", "a", "pkg.a", "f.go", 1, 5, "{}");
int64_t b = cbm_gbuf_upsert_node(gb, "Function", "b", "pkg.b", "f.go", 6, 10, "{}");
cbm_gbuf_insert_edge(gb, a, b, "CALLS", "{}");
const cbm_gbuf_edge_t **edges = NULL;
int count = 0;
cbm_gbuf_find_edges_by_target_type(gb, b, "CALLS", &edges, &count);
ASSERT_EQ(count, 1);
cbm_gbuf_find_edges_by_target_type(gb, a, "CALLS", &edges, &count);
ASSERT_EQ(count, 0);
cbm_gbuf_free(gb);
PASS();
}
TEST(gbuf_find_edges_by_type) {
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
int64_t a = cbm_gbuf_upsert_node(gb, "Function", "a", "pkg.a", "f.go", 1, 5, "{}");
int64_t b = cbm_gbuf_upsert_node(gb, "Function", "b", "pkg.b", "f.go", 6, 10, "{}");
cbm_gbuf_insert_edge(gb, a, b, "CALLS", "{}");
cbm_gbuf_insert_edge(gb, b, a, "CALLS", "{}");
const cbm_gbuf_edge_t **edges = NULL;
int count = 0;
cbm_gbuf_find_edges_by_type(gb, "CALLS", &edges, &count);
ASSERT_EQ(count, 2);
cbm_gbuf_find_edges_by_type(gb, "IMPORTS", &edges, &count);
ASSERT_EQ(count, 0);
cbm_gbuf_free(gb);
PASS();
}
TEST(gbuf_delete_edges_by_type) {
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
int64_t a = cbm_gbuf_upsert_node(gb, "Function", "a", "pkg.a", "f.go", 1, 5, "{}");
int64_t b = cbm_gbuf_upsert_node(gb, "Function", "b", "pkg.b", "f.go", 6, 10, "{}");
cbm_gbuf_insert_edge(gb, a, b, "CALLS", "{}");
cbm_gbuf_insert_edge(gb, a, b, "IMPORTS", "{}");
ASSERT_EQ(cbm_gbuf_edge_count(gb), 2);
cbm_gbuf_delete_edges_by_type(gb, "CALLS");
ASSERT_EQ(cbm_gbuf_edge_count(gb), 1);
ASSERT_EQ(cbm_gbuf_edge_count_by_type(gb, "CALLS"), 0);
ASSERT_EQ(cbm_gbuf_edge_count_by_type(gb, "IMPORTS"), 1);
cbm_gbuf_free(gb);
PASS();
}
TEST(gbuf_edge_count_by_type) {
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
int64_t a = cbm_gbuf_upsert_node(gb, "Function", "a", "pkg.a", "f.go", 1, 5, "{}");
int64_t b = cbm_gbuf_upsert_node(gb, "Function", "b", "pkg.b", "f.go", 6, 10, "{}");
int64_t c = cbm_gbuf_upsert_node(gb, "Function", "c", "pkg.c", "f.go", 11, 15, "{}");
cbm_gbuf_insert_edge(gb, a, b, "CALLS", "{}");
cbm_gbuf_insert_edge(gb, a, c, "CALLS", "{}");
cbm_gbuf_insert_edge(gb, b, c, "IMPORTS", "{}");
ASSERT_EQ(cbm_gbuf_edge_count_by_type(gb, "CALLS"), 2);
ASSERT_EQ(cbm_gbuf_edge_count_by_type(gb, "IMPORTS"), 1);
ASSERT_EQ(cbm_gbuf_edge_count_by_type(gb, "HTTP_CALLS"), 0);
cbm_gbuf_free(gb);
PASS();
}
/* ── Dump to SQLite ────────────────────────────────────────────── */
TEST(gbuf_dump_empty) {
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
/* Dump empty buffer should succeed */
int rc = cbm_gbuf_flush_to_store(gb, NULL);
/* NULL store should be handled gracefully — we just skip */
(void)rc;
cbm_gbuf_free(gb);
PASS();
}
TEST(gbuf_flush_to_store) {
/* Create a buffer with some data */
cbm_gbuf_t *gb = cbm_gbuf_new("test-proj", "/tmp/repo");
int64_t n1 = cbm_gbuf_upsert_node(gb, "Function", "main", "test-proj::main.go::main", "main.go",
1, 10, "{}");
int64_t n2 = cbm_gbuf_upsert_node(gb, "Function", "helper", "test-proj::helper.go::helper",
"helper.go", 1, 5, "{}");
cbm_gbuf_insert_edge(gb, n1, n2, "CALLS", "{}");
/* Open an in-memory store and flush */
cbm_store_t *store = cbm_store_open_memory();
ASSERT_NOT_NULL(store);
int rc = cbm_gbuf_flush_to_store(gb, store);
ASSERT_EQ(rc, 0);
/* Verify data landed in store */
int node_count = cbm_store_count_nodes(store, "test-proj");
ASSERT_EQ(node_count, 2);
int edge_count = cbm_store_count_edges(store, "test-proj");
ASSERT_EQ(edge_count, 1);
cbm_store_close(store);
cbm_gbuf_free(gb);
PASS();
}
TEST(gbuf_many_nodes) {
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
/* Insert 1000 nodes */
for (int i = 0; i < 1000; i++) {
char name[32], qn[64];
snprintf(name, sizeof(name), "func_%d", i);
snprintf(qn, sizeof(qn), "pkg.func_%d", i);
int64_t id = cbm_gbuf_upsert_node(gb, "Function", name, qn, "f.go", i, i + 5, "{}");
ASSERT_GT(id, 0);
}
ASSERT_EQ(cbm_gbuf_node_count(gb), 1000);
/* Verify lookup */
const cbm_gbuf_node_t *n = cbm_gbuf_find_by_qn(gb, "pkg.func_500");
ASSERT_NOT_NULL(n);
ASSERT_STR_EQ(n->name, "func_500");
cbm_gbuf_free(gb);
PASS();
}
/* ── Node edge cases ───────────────────────────────────────────── */
TEST(gbuf_upsert_null_qn) {
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
/* NULL qualified_name → returns 0 (error), no node inserted */
int64_t id = cbm_gbuf_upsert_node(gb, "Function", "foo", NULL, "f.go", 1, 5, "{}");
ASSERT_EQ(id, 0);
ASSERT_EQ(cbm_gbuf_node_count(gb), 0);
cbm_gbuf_free(gb);
PASS();
}
TEST(gbuf_upsert_empty_qn) {
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
/* Empty string QN is valid — it's a non-NULL key */
int64_t id = cbm_gbuf_upsert_node(gb, "Function", "anon", "", "f.go", 1, 5, "{}");
ASSERT_GT(id, 0);
ASSERT_EQ(cbm_gbuf_node_count(gb), 1);
const cbm_gbuf_node_t *n = cbm_gbuf_find_by_qn(gb, "");
ASSERT_NOT_NULL(n);
ASSERT_STR_EQ(n->qualified_name, "");
cbm_gbuf_free(gb);
PASS();
}
TEST(gbuf_upsert_same_qn_updates_all_fields) {
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
int64_t id1 = cbm_gbuf_upsert_node(gb, "Function", "old_name", "pkg.fn", "old.go", 1, 10,
"{\"k\":\"v1\"}");
int64_t id2 = cbm_gbuf_upsert_node(gb, "Method", "new_name", "pkg.fn", "new.go", 20, 30,
"{\"k\":\"v2\"}");
ASSERT_EQ(id1, id2);
ASSERT_EQ(cbm_gbuf_node_count(gb), 1);
const cbm_gbuf_node_t *n = cbm_gbuf_find_by_qn(gb, "pkg.fn");
ASSERT_NOT_NULL(n);
ASSERT_STR_EQ(n->label, "Method");
ASSERT_STR_EQ(n->name, "new_name");
ASSERT_STR_EQ(n->file_path, "new.go");
ASSERT_EQ(n->start_line, 20);
ASSERT_EQ(n->end_line, 30);
ASSERT_STR_EQ(n->properties_json, "{\"k\":\"v2\"}");
cbm_gbuf_free(gb);
PASS();
}
TEST(gbuf_upsert_long_qn) {
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
/* Build a 1200-char QN */
char long_qn[1201];
memset(long_qn, 'a', 1200);
long_qn[1200] = '\0';
int64_t id = cbm_gbuf_upsert_node(gb, "Function", "long", long_qn, "f.go", 1, 5, "{}");
ASSERT_GT(id, 0);
const cbm_gbuf_node_t *n = cbm_gbuf_find_by_qn(gb, long_qn);
ASSERT_NOT_NULL(n);
ASSERT_EQ(strlen(n->qualified_name), 1200);
cbm_gbuf_free(gb);
PASS();
}
TEST(gbuf_find_by_qn_missing) {
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
cbm_gbuf_upsert_node(gb, "Function", "foo", "pkg.foo", "f.go", 1, 5, "{}");
ASSERT_NULL(cbm_gbuf_find_by_qn(gb, "does.not.exist"));
ASSERT_NULL(cbm_gbuf_find_by_qn(gb, ""));
ASSERT_NULL(cbm_gbuf_find_by_qn(gb, "pkg.FOO")); /* case sensitive */
cbm_gbuf_free(gb);
PASS();
}
TEST(gbuf_find_by_id_missing) {
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
cbm_gbuf_upsert_node(gb, "Function", "foo", "pkg.foo", "f.go", 1, 5, "{}");
ASSERT_NULL(cbm_gbuf_find_by_id(gb, 0));
ASSERT_NULL(cbm_gbuf_find_by_id(gb, -1));
ASSERT_NULL(cbm_gbuf_find_by_id(gb, 999));
ASSERT_NULL(cbm_gbuf_find_by_id(gb, INT64_MAX));
cbm_gbuf_free(gb);
PASS();
}
TEST(gbuf_find_by_label_no_matches) {
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
cbm_gbuf_upsert_node(gb, "Function", "foo", "pkg.foo", "f.go", 1, 5, "{}");
const cbm_gbuf_node_t **nodes = NULL;
int count = 0;
int rc = cbm_gbuf_find_by_label(gb, "Route", &nodes, &count);
ASSERT_EQ(rc, 0);
ASSERT_EQ(count, 0);
cbm_gbuf_free(gb);
PASS();
}
TEST(gbuf_find_by_name_multiple) {
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
cbm_gbuf_upsert_node(gb, "Function", "init", "a.init", "a.go", 1, 5, "{}");
cbm_gbuf_upsert_node(gb, "Function", "init", "b.init", "b.go", 1, 5, "{}");
cbm_gbuf_upsert_node(gb, "Method", "init", "c.S.init", "c.go", 1, 5, "{}");
cbm_gbuf_upsert_node(gb, "Function", "other", "d.other", "d.go", 1, 5, "{}");
const cbm_gbuf_node_t **nodes = NULL;
int count = 0;
int rc = cbm_gbuf_find_by_name(gb, "init", &nodes, &count);
ASSERT_EQ(rc, 0);
ASSERT_EQ(count, 3);
rc = cbm_gbuf_find_by_name(gb, "other", &nodes, &count);
ASSERT_EQ(rc, 0);
ASSERT_EQ(count, 1);
rc = cbm_gbuf_find_by_name(gb, "missing", &nodes, &count);
ASSERT_EQ(rc, 0);
ASSERT_EQ(count, 0);
cbm_gbuf_free(gb);
PASS();
}
TEST(gbuf_delete_by_label_cascades_edges) {
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
int64_t f = cbm_gbuf_upsert_node(gb, "Function", "fn", "pkg.fn", "f.go", 1, 5, "{}");
int64_t c = cbm_gbuf_upsert_node(gb, "Class", "Cls", "pkg.Cls", "f.go", 10, 20, "{}");
int64_t m = cbm_gbuf_upsert_node(gb, "Method", "meth", "pkg.Cls.meth", "f.go", 12, 18, "{}");
/* Edges: fn→Cls, fn→meth, meth→fn */
cbm_gbuf_insert_edge(gb, f, c, "CALLS", "{}");
cbm_gbuf_insert_edge(gb, f, m, "CALLS", "{}");
cbm_gbuf_insert_edge(gb, m, f, "CALLS", "{}");
ASSERT_EQ(cbm_gbuf_edge_count(gb), 3);
/* Delete all Class nodes — should remove fn→Cls edge only */
cbm_gbuf_delete_by_label(gb, "Class");
ASSERT_EQ(cbm_gbuf_node_count(gb), 2);
ASSERT_EQ(cbm_gbuf_edge_count(gb), 2); /* fn→meth and meth→fn survive */
/* Verify edge source/target type lookups are consistent after cascade */
const cbm_gbuf_edge_t **edges = NULL;
int ecount = 0;
cbm_gbuf_find_edges_by_source_type(gb, f, "CALLS", &edges, &ecount);
ASSERT_EQ(ecount, 1); /* only fn→meth remains */
cbm_gbuf_free(gb);
PASS();
}
TEST(gbuf_node_count_empty) {
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
ASSERT_EQ(cbm_gbuf_node_count(gb), 0);
ASSERT_EQ(cbm_gbuf_node_count(NULL), 0);
cbm_gbuf_free(gb);
PASS();
}
TEST(gbuf_upsert_100_nodes_stress) {
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
for (int i = 0; i < 100; i++) {
char name[32], qn[64];
snprintf(name, sizeof(name), "f%d", i);
snprintf(qn, sizeof(qn), "pkg.f%d", i);
int64_t id = cbm_gbuf_upsert_node(gb, "Function", name, qn, "f.go", i, i + 1, "{}");
ASSERT_GT(id, 0);
}
ASSERT_EQ(cbm_gbuf_node_count(gb), 100);
/* Verify each node is findable */
for (int i = 0; i < 100; i++) {
char qn[64];
snprintf(qn, sizeof(qn), "pkg.f%d", i);
ASSERT_NOT_NULL(cbm_gbuf_find_by_qn(gb, qn));
}
cbm_gbuf_free(gb);
PASS();
}
/* ── Edge edge cases ──────────────────────────────────────────── */
TEST(gbuf_edge_nonexistent_endpoints) {
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
/* Edges with non-existent source/target IDs are accepted (no FK validation
* in the buffer — validation happens at flush time when remapping IDs) */
int64_t eid = cbm_gbuf_insert_edge(gb, 9999, 8888, "CALLS", "{}");
ASSERT_GT(eid, 0);
ASSERT_EQ(cbm_gbuf_edge_count(gb), 1);
cbm_gbuf_free(gb);
PASS();
}
TEST(gbuf_edge_dedup_merges_properties) {
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
int64_t a = cbm_gbuf_upsert_node(gb, "Function", "a", "pkg.a", "f.go", 1, 5, "{}");
int64_t b = cbm_gbuf_upsert_node(gb, "Function", "b", "pkg.b", "f.go", 6, 10, "{}");
int64_t eid1 = cbm_gbuf_insert_edge(gb, a, b, "CALLS", "{\"weight\":1}");
int64_t eid2 = cbm_gbuf_insert_edge(gb, a, b, "CALLS", "{\"weight\":5}");
ASSERT_EQ(eid1, eid2);
ASSERT_EQ(cbm_gbuf_edge_count(gb), 1);
/* Verify second insert's properties win (merge = replace) */
const cbm_gbuf_edge_t **edges = NULL;
int count = 0;
cbm_gbuf_find_edges_by_source_type(gb, a, "CALLS", &edges, &count);
ASSERT_EQ(count, 1);
ASSERT_STR_EQ(edges[0]->properties_json, "{\"weight\":5}");
cbm_gbuf_free(gb);
PASS();
}
TEST(gbuf_edge_count_empty) {
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
ASSERT_EQ(cbm_gbuf_edge_count(gb), 0);
ASSERT_EQ(cbm_gbuf_edge_count(NULL), 0);
cbm_gbuf_free(gb);
PASS();
}
TEST(gbuf_edge_count_by_type_missing) {
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
ASSERT_EQ(cbm_gbuf_edge_count_by_type(gb, "CALLS"), 0);
ASSERT_EQ(cbm_gbuf_edge_count_by_type(gb, "UNKNOWN"), 0);
ASSERT_EQ(cbm_gbuf_edge_count_by_type(gb, ""), 0);
cbm_gbuf_free(gb);
PASS();
}
TEST(gbuf_delete_edges_preserves_other_types) {
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
int64_t a = cbm_gbuf_upsert_node(gb, "Function", "a", "pkg.a", "f.go", 1, 5, "{}");
int64_t b = cbm_gbuf_upsert_node(gb, "Function", "b", "pkg.b", "f.go", 6, 10, "{}");
int64_t c = cbm_gbuf_upsert_node(gb, "Function", "c", "pkg.c", "f.go", 11, 15, "{}");
cbm_gbuf_insert_edge(gb, a, b, "CALLS", "{}");
cbm_gbuf_insert_edge(gb, b, c, "CALLS", "{}");
cbm_gbuf_insert_edge(gb, a, b, "IMPORTS", "{}");
cbm_gbuf_insert_edge(gb, a, c, "HTTP_CALLS", "{}");
ASSERT_EQ(cbm_gbuf_edge_count(gb), 4);
cbm_gbuf_delete_edges_by_type(gb, "CALLS");
ASSERT_EQ(cbm_gbuf_edge_count(gb), 2);
ASSERT_EQ(cbm_gbuf_edge_count_by_type(gb, "CALLS"), 0);
ASSERT_EQ(cbm_gbuf_edge_count_by_type(gb, "IMPORTS"), 1);
ASSERT_EQ(cbm_gbuf_edge_count_by_type(gb, "HTTP_CALLS"), 1);
/* Verify secondary indexes are consistent after rebuild */
const cbm_gbuf_edge_t **edges = NULL;
int count = 0;
cbm_gbuf_find_edges_by_source_type(gb, a, "IMPORTS", &edges, &count);
ASSERT_EQ(count, 1);
cbm_gbuf_find_edges_by_target_type(gb, c, "HTTP_CALLS", &edges, &count);
ASSERT_EQ(count, 1);
cbm_gbuf_free(gb);
PASS();
}
TEST(gbuf_find_edges_by_target_type_multiple) {
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
int64_t a = cbm_gbuf_upsert_node(gb, "Function", "a", "pkg.a", "f.go", 1, 5, "{}");
int64_t b = cbm_gbuf_upsert_node(gb, "Function", "b", "pkg.b", "f.go", 6, 10, "{}");
int64_t c = cbm_gbuf_upsert_node(gb, "Function", "c", "pkg.c", "f.go", 11, 15, "{}");
/* Both a and c call b */
cbm_gbuf_insert_edge(gb, a, b, "CALLS", "{}");
cbm_gbuf_insert_edge(gb, c, b, "CALLS", "{}");
const cbm_gbuf_edge_t **edges = NULL;
int count = 0;
cbm_gbuf_find_edges_by_target_type(gb, b, "CALLS", &edges, &count);
ASSERT_EQ(count, 2);
/* No IMPORTS edges targeting b */
cbm_gbuf_find_edges_by_target_type(gb, b, "IMPORTS", &edges, &count);
ASSERT_EQ(count, 0);
cbm_gbuf_free(gb);
PASS();
}
/* ── Merge tests ──────────────────────────────────────────────── */
TEST(gbuf_merge_overlapping_qns) {
cbm_gbuf_t *dst = cbm_gbuf_new("test", "/tmp");
cbm_gbuf_t *src = cbm_gbuf_new("test", "/tmp");
/* dst has node with QN "pkg.fn" */
cbm_gbuf_upsert_node(dst, "Function", "fn_old", "pkg.fn", "old.go", 1, 10,
"{\"from\":\"dst\"}");
cbm_gbuf_upsert_node(dst, "Function", "unique_dst", "pkg.unique_dst", "u.go", 1, 5, "{}");
/* src has same QN with different fields — src should win */
cbm_gbuf_upsert_node(src, "Method", "fn_new", "pkg.fn", "new.go", 20, 30, "{\"from\":\"src\"}");
cbm_gbuf_upsert_node(src, "Function", "unique_src", "pkg.unique_src", "s.go", 1, 5, "{}");
int rc = cbm_gbuf_merge(dst, src);
ASSERT_EQ(rc, 0);
/* Total: 3 nodes (1 merged + 1 dst-only + 1 src-only) */
ASSERT_EQ(cbm_gbuf_node_count(dst), 3);
/* Verify src fields won for the overlapping QN */
const cbm_gbuf_node_t *n = cbm_gbuf_find_by_qn(dst, "pkg.fn");
ASSERT_NOT_NULL(n);
ASSERT_STR_EQ(n->label, "Method");
ASSERT_STR_EQ(n->name, "fn_new");
ASSERT_STR_EQ(n->file_path, "new.go");
ASSERT_EQ(n->start_line, 20);
ASSERT_STR_EQ(n->properties_json, "{\"from\":\"src\"}");
/* Both unique nodes present */
ASSERT_NOT_NULL(cbm_gbuf_find_by_qn(dst, "pkg.unique_dst"));
ASSERT_NOT_NULL(cbm_gbuf_find_by_qn(dst, "pkg.unique_src"));
cbm_gbuf_free(dst);
cbm_gbuf_free(src);
PASS();
}
TEST(gbuf_merge_edge_dedup) {
_Atomic int64_t shared = 1;
cbm_gbuf_t *dst = cbm_gbuf_new_shared_ids("test", "/tmp", &shared);
cbm_gbuf_t *src = cbm_gbuf_new_shared_ids("test", "/tmp", &shared);
int64_t a = cbm_gbuf_upsert_node(dst, "Function", "a", "pkg.a", "f.go", 1, 5, "{}");
int64_t b = cbm_gbuf_upsert_node(dst, "Function", "b", "pkg.b", "f.go", 6, 10, "{}");
cbm_gbuf_insert_edge(dst, a, b, "CALLS", "{}");
/* src has same nodes (by QN) and same edge */
cbm_gbuf_upsert_node(src, "Function", "a", "pkg.a", "f.go", 1, 5, "{}");
cbm_gbuf_upsert_node(src, "Function", "b", "pkg.b", "f.go", 6, 10, "{}");
/* This edge's src/target IDs differ from dst's, but after merge remap
* it becomes the same (src, tgt, type) tuple → deduped */
int64_t sa = cbm_gbuf_find_by_qn(src, "pkg.a")->id;
int64_t sb = cbm_gbuf_find_by_qn(src, "pkg.b")->id;
cbm_gbuf_insert_edge(src, sa, sb, "CALLS", "{}");
int rc = cbm_gbuf_merge(dst, src);
ASSERT_EQ(rc, 0);
ASSERT_EQ(cbm_gbuf_node_count(dst), 2);
ASSERT_EQ(cbm_gbuf_edge_count(dst), 1); /* deduped */
cbm_gbuf_free(dst);
cbm_gbuf_free(src);
PASS();
}
TEST(gbuf_merge_empty_src_into_populated_dst) {
cbm_gbuf_t *dst = cbm_gbuf_new("test", "/tmp");
cbm_gbuf_t *src = cbm_gbuf_new("test", "/tmp");
cbm_gbuf_upsert_node(dst, "Function", "foo", "pkg.foo", "f.go", 1, 5, "{}");
cbm_gbuf_upsert_node(dst, "Function", "bar", "pkg.bar", "f.go", 6, 10, "{}");
int rc = cbm_gbuf_merge(dst, src);
ASSERT_EQ(rc, 0);
ASSERT_EQ(cbm_gbuf_node_count(dst), 2); /* unchanged */
cbm_gbuf_free(dst);
cbm_gbuf_free(src);
PASS();
}
TEST(gbuf_merge_populated_src_into_empty_dst) {
cbm_gbuf_t *dst = cbm_gbuf_new("test", "/tmp");
cbm_gbuf_t *src = cbm_gbuf_new("test", "/tmp");
int64_t a = cbm_gbuf_upsert_node(src, "Function", "foo", "pkg.foo", "f.go", 1, 5, "{}");
int64_t b = cbm_gbuf_upsert_node(src, "Function", "bar", "pkg.bar", "f.go", 6, 10, "{}");
cbm_gbuf_insert_edge(src, a, b, "CALLS", "{}");
int rc = cbm_gbuf_merge(dst, src);
ASSERT_EQ(rc, 0);
ASSERT_EQ(cbm_gbuf_node_count(dst), 2);
ASSERT_EQ(cbm_gbuf_edge_count(dst), 1);
/* Verify nodes are findable in dst */
ASSERT_NOT_NULL(cbm_gbuf_find_by_qn(dst, "pkg.foo"));
ASSERT_NOT_NULL(cbm_gbuf_find_by_qn(dst, "pkg.bar"));
cbm_gbuf_free(dst);
cbm_gbuf_free(src);
PASS();
}
TEST(gbuf_merge_null_args) {
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
ASSERT_EQ(cbm_gbuf_merge(NULL, gb), -1);
ASSERT_EQ(cbm_gbuf_merge(gb, NULL), -1);
ASSERT_EQ(cbm_gbuf_merge(NULL, NULL), -1);
cbm_gbuf_free(gb);
PASS();
}
/* ── Flush / merge-into-store tests ───────────────────────────── */
TEST(gbuf_flush_to_store_null) {
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
/* NULL store returns -1 */
ASSERT_EQ(cbm_gbuf_flush_to_store(gb, NULL), -1);
/* NULL gbuf returns -1 */
cbm_store_t *store = cbm_store_open_memory();
ASSERT_EQ(cbm_gbuf_flush_to_store(NULL, store), -1);
cbm_store_close(store);
cbm_gbuf_free(gb);
PASS();
}
TEST(gbuf_flush_verify_store_data) {
cbm_gbuf_t *gb = cbm_gbuf_new("proj", "/tmp/repo");
int64_t n1 = cbm_gbuf_upsert_node(gb, "Function", "alpha", "proj::alpha", "a.go", 1, 10, "{}");
int64_t n2 = cbm_gbuf_upsert_node(gb, "Class", "Beta", "proj::Beta", "b.go", 1, 20, "{}");
int64_t n3 = cbm_gbuf_upsert_node(gb, "Function", "gamma", "proj::gamma", "c.go", 1, 5, "{}");
cbm_gbuf_insert_edge(gb, n1, n2, "CALLS", "{}");
cbm_gbuf_insert_edge(gb, n1, n3, "IMPORTS", "{}");
cbm_store_t *store = cbm_store_open_memory();
ASSERT_NOT_NULL(store);
int rc = cbm_gbuf_flush_to_store(gb, store);
ASSERT_EQ(rc, 0);
/* Verify counts */
ASSERT_EQ(cbm_store_count_nodes(store, "proj"), 3);
ASSERT_EQ(cbm_store_count_edges(store, "proj"), 2);
/* Verify node lookup by QN */
cbm_node_t out;
rc = cbm_store_find_node_by_qn(store, "proj", "proj::alpha", &out);
ASSERT_EQ(rc, CBM_STORE_OK);
ASSERT_STR_EQ(out.name, "alpha");
ASSERT_STR_EQ(out.label, "Function");
cbm_node_free_fields(&out);
rc = cbm_store_find_node_by_qn(store, "proj", "proj::Beta", &out);
ASSERT_EQ(rc, CBM_STORE_OK);
ASSERT_STR_EQ(out.label, "Class");
cbm_node_free_fields(&out);
cbm_store_close(store);
cbm_gbuf_free(gb);
PASS();
}
TEST(gbuf_merge_into_store_preserves) {
/* First, flush initial data via flush_to_store */
cbm_gbuf_t *gb1 = cbm_gbuf_new("proj", "/tmp/repo");
cbm_gbuf_upsert_node(gb1, "Function", "existing", "proj::existing", "e.go", 1, 10, "{}");
cbm_store_t *store = cbm_store_open_memory();
ASSERT_NOT_NULL(store);
cbm_store_upsert_project(store, "proj", "/tmp/repo");
int rc = cbm_gbuf_flush_to_store(gb1, store);
ASSERT_EQ(rc, 0);
ASSERT_EQ(cbm_store_count_nodes(store, "proj"), 1);
/* Now merge_into_store with new data — should NOT delete existing */
cbm_gbuf_t *gb2 = cbm_gbuf_new("proj", "/tmp/repo");
cbm_gbuf_upsert_node(gb2, "Function", "newone", "proj::newone", "n.go", 1, 5, "{}");
rc = cbm_gbuf_merge_into_store(gb2, store);
ASSERT_EQ(rc, 0);
/* Both nodes should exist */
ASSERT_EQ(cbm_store_count_nodes(store, "proj"), 2);
cbm_node_t out;
rc = cbm_store_find_node_by_qn(store, "proj", "proj::existing", &out);
ASSERT_EQ(rc, CBM_STORE_OK);
cbm_node_free_fields(&out);
rc = cbm_store_find_node_by_qn(store, "proj", "proj::newone", &out);
ASSERT_EQ(rc, CBM_STORE_OK);
cbm_node_free_fields(&out);
cbm_store_close(store);
cbm_gbuf_free(gb1);
cbm_gbuf_free(gb2);
PASS();
}
/* ── Shared ID tests ──────────────────────────────────────────── */
TEST(gbuf_shared_ids_unique) {
_Atomic int64_t shared = 1;
cbm_gbuf_t *gb1 = cbm_gbuf_new_shared_ids("test", "/tmp", &shared);
cbm_gbuf_t *gb2 = cbm_gbuf_new_shared_ids("test", "/tmp", &shared);
int64_t id1 = cbm_gbuf_upsert_node(gb1, "Function", "a", "pkg.a", "f.go", 1, 5, "{}");
int64_t id2 = cbm_gbuf_upsert_node(gb2, "Function", "b", "pkg.b", "f.go", 6, 10, "{}");
int64_t id3 = cbm_gbuf_upsert_node(gb1, "Function", "c", "pkg.c", "f.go", 11, 15, "{}");
int64_t id4 = cbm_gbuf_upsert_node(gb2, "Function", "d", "pkg.d", "f.go", 16, 20, "{}");
/* All IDs must be unique */
ASSERT_NEQ(id1, id2);
ASSERT_NEQ(id1, id3);
ASSERT_NEQ(id1, id4);
ASSERT_NEQ(id2, id3);
ASSERT_NEQ(id2, id4);
ASSERT_NEQ(id3, id4);
/* IDs should be sequential from the shared source */
ASSERT_EQ(id1, 1);
ASSERT_EQ(id2, 2);
ASSERT_EQ(id3, 3);
ASSERT_EQ(id4, 4);
cbm_gbuf_free(gb1);
cbm_gbuf_free(gb2);
PASS();
}
TEST(gbuf_shared_ids_null_fallback) {
/* NULL id_source → behaves like cbm_gbuf_new() */
cbm_gbuf_t *gb = cbm_gbuf_new_shared_ids("test", "/tmp", NULL);
ASSERT_NOT_NULL(gb);
int64_t id = cbm_gbuf_upsert_node(gb, "Function", "a", "pkg.a", "f.go", 1, 5, "{}");
ASSERT_EQ(id, 1);
cbm_gbuf_free(gb);
PASS();
}
TEST(gbuf_next_id_set_next_id_roundtrip) {
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
/* Initial next_id is 1 */
ASSERT_EQ(cbm_gbuf_next_id(gb), 1);
/* Insert a node, next_id advances */
cbm_gbuf_upsert_node(gb, "Function", "a", "pkg.a", "f.go", 1, 5, "{}");
ASSERT_EQ(cbm_gbuf_next_id(gb), 2);
/* Set next_id to an arbitrary value */
cbm_gbuf_set_next_id(gb, 100);
ASSERT_EQ(cbm_gbuf_next_id(gb), 100);
/* Next insert uses the new base */
int64_t id = cbm_gbuf_upsert_node(gb, "Function", "b", "pkg.b", "f.go", 6, 10, "{}");
ASSERT_EQ(id, 100);
ASSERT_EQ(cbm_gbuf_next_id(gb), 101);
cbm_gbuf_free(gb);
PASS();
}
TEST(gbuf_next_id_null_safe) {
/* next_id on NULL returns 1 (default) */
ASSERT_EQ(cbm_gbuf_next_id(NULL), 1);
/* set_next_id on NULL should not crash */
cbm_gbuf_set_next_id(NULL, 42);
PASS();
}
/* ── Flush edge case: orphan edges skipped ────────────────────── */
TEST(gbuf_flush_skips_orphan_edges) {
cbm_gbuf_t *gb = cbm_gbuf_new("proj", "/tmp/repo");
int64_t n1 = cbm_gbuf_upsert_node(gb, "Function", "real", "proj::real", "f.go", 1, 5, "{}");
/* Valid edge */
cbm_gbuf_insert_edge(gb, n1, n1, "CALLS", "{}");
/* Orphan edge — target ID 9999 does not map to any node */
cbm_gbuf_insert_edge(gb, n1, 9999, "CALLS", "{}");
cbm_store_t *store = cbm_store_open_memory();
ASSERT_NOT_NULL(store);
int rc = cbm_gbuf_flush_to_store(gb, store);
ASSERT_EQ(rc, 0);
/* Only the valid edge should land in the store */
ASSERT_EQ(cbm_store_count_nodes(store, "proj"), 1);
ASSERT_EQ(cbm_store_count_edges(store, "proj"), 1);
cbm_store_close(store);
cbm_gbuf_free(gb);
PASS();
}
/* ── Publish failure reporting ───────────────────────────────── */
static char g_log_capture[4096];
static CBMLogLevel g_prev_log_level;
static CBMLogFormat g_prev_log_format;
static void capture_log_sink(const char *line) {
size_t used = strlen(g_log_capture);
size_t avail = sizeof(g_log_capture) - used;
if (avail <= 1) {
return;
}
int n = snprintf(g_log_capture + used, avail, "%s\n", line);
if (n < 0 || (size_t)n >= avail) {
g_log_capture[sizeof(g_log_capture) - 1] = '\0';
}
}
static void capture_logs_start(void) {
g_log_capture[0] = '\0';
g_prev_log_level = cbm_log_get_level();
g_prev_log_format = cbm_log_get_format();
cbm_log_set_level(CBM_LOG_DEBUG);
/* The assertions below read the text encoding, so pin it rather than
* inherit whatever CBM_LOG_FORMAT left set. */
cbm_log_set_format(CBM_LOG_FORMAT_TEXT);
cbm_log_set_sink(capture_log_sink);
}
static const char *capture_logs_end(void) {
cbm_log_set_sink(NULL);
cbm_log_set_level(g_prev_log_level);
cbm_log_set_format(g_prev_log_format);
return g_log_capture;
}
/* A dump that publishes nothing has to say so, and say why. Renaming onto an
* existing directory is how test_sqlite_writer already forces the publish to
* fail; here it stands in for any host that denies the rename (#1620). */
TEST(gbuf_dump_failure_logs_reason) {
char dir[256];
snprintf(dir, sizeof(dir), "/tmp/cbm_gbuf_pub_XXXXXX");
ASSERT_NOT_NULL(cbm_mkdtemp(dir));
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp/repo");
ASSERT_NOT_NULL(gb);
int64_t id = cbm_gbuf_upsert_node(gb, "Function", "main", "pkg.main", "main.go", 1, 10, "{}");
ASSERT_GT(id, 0);
capture_logs_start();
int rc = cbm_gbuf_dump_to_sqlite(gb, dir);
const char *logs = capture_logs_end();
ASSERT(rc != 0);
ASSERT_NOT_NULL(strstr(logs, "gbuf.dump_failed"));
/* The reason survived the cleanup unlink. */
ASSERT_NOT_NULL(strstr(logs, "errno="));
ASSERT(strstr(logs, "errno=0 ") == NULL);
/* And the run is not also reported as a successful dump. */
ASSERT(strstr(logs, "msg=gbuf.dump ") == NULL);
cbm_gbuf_free(gb);
cbm_rmdir(dir);
PASS();
}
/* ── Suite ─────────────────────────────────────────────────────── */
/* A worker buffer draws ids from the shared counter, so a dense id -> node
* array in it spans the whole global id space: 18 workers x (next power of
* two above the highest id) x 8 B, doubling in lockstep -- a 1 GB step
* inside one gate interval on the kernel at 8M ids (2026-09-14). A worker
* buffer is never asked by id before the merge, so it keeps no such array;
* the main buffer it merges into still answers by id. One node at id 2M
* would cost a 16 MB array; the index class must not grow by even 1 MB. */
TEST(gbuf_worker_buffer_keeps_no_by_id_array) {
_Atomic int64_t ids;
atomic_init(&ids, (int64_t)1 << 21);
size_t before = cbm_mem_class_live_bytes(CBM_MEM_CLASS_GBUF_INDEX);
cbm_gbuf_t *w = cbm_gbuf_new_worker("p", "/r", &ids);
ASSERT_NOT_NULL(w);
int64_t id = cbm_gbuf_upsert_node(w, "Function", "f", "p.f", "a.c", 1, 2, "{}");
ASSERT_TRUE(id >= ((int64_t)1 << 21));
size_t after = cbm_mem_class_live_bytes(CBM_MEM_CLASS_GBUF_INDEX);
size_t grown = after > before ? after - before : 0;
ASSERT_TRUE(grown < ((size_t)1 << 20));
ASSERT_TRUE(cbm_gbuf_find_by_id(w, id) == NULL);
ASSERT_NOT_NULL(cbm_gbuf_find_by_qn(w, "p.f"));
cbm_gbuf_t *main_gb = cbm_gbuf_new("p", "/r");
ASSERT_NOT_NULL(main_gb);
cbm_gbuf_merge(main_gb, w);
ASSERT_NOT_NULL(cbm_gbuf_find_by_id(main_gb, id));
cbm_gbuf_free(w);
cbm_gbuf_free(main_gb);
PASS();
}
SUITE(graph_buffer) {
RUN_TEST(gbuf_worker_buffer_keeps_no_by_id_array);
/* Original tests */
RUN_TEST(gbuf_create_free);
RUN_TEST(gbuf_free_null);
RUN_TEST(gbuf_upsert_node);
RUN_TEST(gbuf_upsert_updates);
RUN_TEST(gbuf_find_by_id);
RUN_TEST(gbuf_find_by_label);
RUN_TEST(gbuf_find_by_name);
RUN_TEST(gbuf_delete_by_label);
RUN_TEST(gbuf_insert_edge);
RUN_TEST(gbuf_edge_dedup);
RUN_TEST(gbuf_imports_multi_symbol_dedup);
RUN_TEST(gbuf_imports_long_local_name_no_collision);
RUN_TEST(gbuf_find_edges_by_source_type);
RUN_TEST(gbuf_find_edges_by_target_type);
RUN_TEST(gbuf_find_edges_by_type);
RUN_TEST(gbuf_delete_edges_by_type);
RUN_TEST(gbuf_edge_count_by_type);
RUN_TEST(gbuf_dump_empty);
RUN_TEST(gbuf_flush_to_store);
RUN_TEST(gbuf_many_nodes);
/* Node edge cases */
RUN_TEST(gbuf_upsert_null_qn);
RUN_TEST(gbuf_upsert_empty_qn);
RUN_TEST(gbuf_upsert_same_qn_updates_all_fields);
RUN_TEST(gbuf_upsert_long_qn);
RUN_TEST(gbuf_find_by_qn_missing);
RUN_TEST(gbuf_find_by_id_missing);
RUN_TEST(gbuf_find_by_label_no_matches);
RUN_TEST(gbuf_find_by_name_multiple);
RUN_TEST(gbuf_delete_by_label_cascades_edges);
RUN_TEST(gbuf_node_count_empty);
RUN_TEST(gbuf_upsert_100_nodes_stress);
/* Edge edge cases */
RUN_TEST(gbuf_edge_nonexistent_endpoints);
RUN_TEST(gbuf_edge_dedup_merges_properties);
RUN_TEST(gbuf_edge_count_empty);
RUN_TEST(gbuf_edge_count_by_type_missing);
RUN_TEST(gbuf_delete_edges_preserves_other_types);
RUN_TEST(gbuf_find_edges_by_target_type_multiple);
/* Merge tests */
RUN_TEST(gbuf_merge_overlapping_qns);
RUN_TEST(gbuf_merge_edge_dedup);
RUN_TEST(gbuf_merge_empty_src_into_populated_dst);
RUN_TEST(gbuf_merge_populated_src_into_empty_dst);
RUN_TEST(gbuf_merge_null_args);
/* Flush/merge-into-store tests */
RUN_TEST(gbuf_flush_to_store_null);
RUN_TEST(gbuf_flush_verify_store_data);
RUN_TEST(gbuf_merge_into_store_preserves);
RUN_TEST(gbuf_flush_skips_orphan_edges);
/* Edge property merge determinism */
RUN_TEST(gbuf_edge_props_merge_is_order_independent);
RUN_TEST(gbuf_edge_props_merge_prefers_higher_confidence);
RUN_TEST(gbuf_edge_props_unreadable_confidence_does_not_displace_absent);
RUN_TEST(gbuf_edge_props_merge_keeps_existing_on_empty);
/* Shared ID tests */
RUN_TEST(gbuf_shared_ids_unique);
RUN_TEST(gbuf_shared_ids_null_fallback);
RUN_TEST(gbuf_next_id_set_next_id_roundtrip);
RUN_TEST(gbuf_next_id_null_safe);
/* Publish failure reporting */
RUN_TEST(gbuf_dump_failure_logs_reason);
}