/* * test_store_search.c — Tests for search and traversal operations. * * Ported from internal/store/store_test.go (TestSearch, TestBFS, etc.) */ #include "../src/foundation/compat.h" #include "test_framework.h" #include "test_helpers.h" #include #include #include #include /* Helper: create a typical graph for search/traversal tests. * * Nodes: SubmitOrder (Function), ProcessOrder (Function), OrderService (Class) * Edges: SubmitOrder → ProcessOrder (CALLS) * * Returns store handle. Fills ids[3]. */ static cbm_store_t *setup_search_store(int64_t *ids) { cbm_store_t *s = cbm_store_open_memory(); cbm_store_upsert_project(s, "test", "/tmp/test"); cbm_node_t n1 = {.project = "test", .label = "Function", .name = "SubmitOrder", .qualified_name = "test.main.SubmitOrder", .file_path = "main.go"}; cbm_node_t n2 = {.project = "test", .label = "Function", .name = "ProcessOrder", .qualified_name = "test.service.ProcessOrder", .file_path = "service.go"}; cbm_node_t n3 = {.project = "test", .label = "Class", .name = "OrderService", .qualified_name = "test.service.OrderService", .file_path = "service.go"}; ids[0] = cbm_store_upsert_node(s, &n1); ids[1] = cbm_store_upsert_node(s, &n2); ids[2] = cbm_store_upsert_node(s, &n3); cbm_edge_t e = {.project = "test", .source_id = ids[0], .target_id = ids[1], .type = "CALLS"}; cbm_store_insert_edge(s, &e); return s; } /* ── Search by label ────────────────────────────────────────────── */ TEST(store_search_by_label) { int64_t ids[3]; cbm_store_t *s = setup_search_store(ids); cbm_search_params_t params = { .project = "test", .label = "Function", .min_degree = -1, .max_degree = -1}; cbm_search_output_t out = {0}; int rc = cbm_store_search(s, ¶ms, &out); ASSERT_EQ(rc, CBM_STORE_OK); ASSERT_EQ(out.count, 2); ASSERT_EQ(out.total, 2); cbm_store_search_free(&out); cbm_store_close(s); PASS(); } /* ── Search by name pattern ─────────────────────────────────────── */ TEST(store_search_by_name_pattern) { int64_t ids[3]; cbm_store_t *s = setup_search_store(ids); cbm_search_params_t params = { .project = "test", .name_pattern = ".*Submit.*", .min_degree = -1, .max_degree = -1}; cbm_search_output_t out = {0}; int rc = cbm_store_search(s, ¶ms, &out); ASSERT_EQ(rc, CBM_STORE_OK); ASSERT_EQ(out.count, 1); ASSERT_STR_EQ(out.results[0].node.name, "SubmitOrder"); cbm_store_search_free(&out); cbm_store_close(s); PASS(); } /* ── Empty-string label is ignored (issue #481) ────────────────── */ /* An empty-string label must behave like an omitted label (no filter), not be * applied as a literal `n.label = ''` that matches nothing. Previously a * name_pattern/qn_pattern search passing label="" returned zero results, while * the BM25 query path ignored the empty label — an inconsistency that made * structural class/service discovery silently fail. */ TEST(store_search_empty_label_ignored) { int64_t ids[3]; cbm_store_t *s = setup_search_store(ids); /* name_pattern + label="" must match the same as name_pattern alone. */ cbm_search_params_t empty_label = {.project = "test", .name_pattern = ".*Submit.*", .label = "", .min_degree = -1, .max_degree = -1}; cbm_search_output_t out = {0}; int rc = cbm_store_search(s, &empty_label, &out); ASSERT_EQ(rc, CBM_STORE_OK); ASSERT_EQ(out.count, 1); ASSERT_STR_EQ(out.results[0].node.name, "SubmitOrder"); cbm_store_search_free(&out); /* A non-empty label still filters: ".*Order.*" matches three names but only * OrderService is a Class. */ cbm_search_params_t cls = {.project = "test", .name_pattern = ".*Order.*", .label = "Class", .min_degree = -1, .max_degree = -1}; cbm_search_output_t out2 = {0}; rc = cbm_store_search(s, &cls, &out2); ASSERT_EQ(rc, CBM_STORE_OK); ASSERT_EQ(out2.count, 1); ASSERT_STR_EQ(out2.results[0].node.name, "OrderService"); cbm_store_search_free(&out2); /* qn_pattern shares the same WHERE builder, so empty label must be ignored * there too. */ cbm_search_params_t qn = {.project = "test", .qn_pattern = ".*SubmitOrder", .label = "", .min_degree = -1, .max_degree = -1}; cbm_search_output_t out3 = {0}; rc = cbm_store_search(s, &qn, &out3); ASSERT_EQ(rc, CBM_STORE_OK); ASSERT_EQ(out3.count, 1); ASSERT_STR_EQ(out3.results[0].node.name, "SubmitOrder"); cbm_store_search_free(&out3); cbm_store_close(s); PASS(); } /* ── Search by file pattern ─────────────────────────────────────── */ TEST(store_search_by_file_pattern) { int64_t ids[3]; cbm_store_t *s = setup_search_store(ids); cbm_search_params_t params = { .project = "test", .file_pattern = "service*", .min_degree = -1, .max_degree = -1}; cbm_search_output_t out = {0}; int rc = cbm_store_search(s, ¶ms, &out); ASSERT_EQ(rc, CBM_STORE_OK); ASSERT_EQ(out.count, 2); cbm_store_search_free(&out); cbm_store_close(s); PASS(); } /* Issue #200: a wildcard-free file_pattern must match as a path substring, * not require the path to equal it. "offer-server" should match * "src/offer-server/decision.js". */ TEST(store_search_file_pattern_substring_issue200) { cbm_store_t *s = cbm_store_open_memory(); cbm_store_upsert_project(s, "test", "/tmp/test"); cbm_node_t n = {.project = "test", .label = "Function", .name = "evaluate", .qualified_name = "test.offer.evaluate", .file_path = "src/offer-server/decision.js"}; cbm_store_upsert_node(s, &n); cbm_search_params_t params = { .project = "test", .file_pattern = "offer-server", .min_degree = -1, .max_degree = -1}; cbm_search_output_t out = {0}; int rc = cbm_store_search(s, ¶ms, &out); ASSERT_EQ(rc, CBM_STORE_OK); ASSERT_EQ(out.count, 1); ASSERT_STR_EQ(out.results[0].node.name, "evaluate"); cbm_store_search_free(&out); cbm_store_close(s); PASS(); } /* ── Search pagination ──────────────────────────────────────────── */ TEST(store_search_pagination) { int64_t ids[3]; cbm_store_t *s = setup_search_store(ids); /* limit=1 */ cbm_search_params_t params = { .project = "test", .limit = 1, .min_degree = -1, .max_degree = -1}; cbm_search_output_t out = {0}; int rc = cbm_store_search(s, ¶ms, &out); ASSERT_EQ(rc, CBM_STORE_OK); ASSERT_EQ(out.count, 1); ASSERT_EQ(out.total, 3); cbm_store_search_free(&out); /* limit=1, offset=1 */ params.offset = 1; rc = cbm_store_search(s, ¶ms, &out); ASSERT_EQ(rc, CBM_STORE_OK); ASSERT_EQ(out.count, 1); ASSERT_EQ(out.total, 3); cbm_store_search_free(&out); /* offset past end */ params.offset = 100; rc = cbm_store_search(s, ¶ms, &out); ASSERT_EQ(rc, CBM_STORE_OK); ASSERT_EQ(out.count, 0); ASSERT_EQ(out.total, 3); cbm_store_search_free(&out); cbm_store_close(s); PASS(); } /* ── Search with degree filter ──────────────────────────────────── */ static int search_result_index_by_name(const cbm_search_output_t *out, const char *name) { for (int i = 0; i < out->count; i++) { if (out->results[i].node.name && strcmp(out->results[i].node.name, name) == 0) { return i; } } return -1; } TEST(store_search_degree_filter) { int64_t ids[3]; cbm_store_t *s = setup_search_store(ids); /* SubmitOrder has out_degree=1, ProcessOrder has in_degree=1. * Degree filters: -1 = no filter, 0+ = active. */ cbm_search_params_t params = { .project = "test", .label = "Function", .min_degree = 1, .max_degree = -1}; cbm_search_output_t out = {0}; int rc = cbm_store_search(s, ¶ms, &out); ASSERT_EQ(rc, CBM_STORE_OK); /* Both functions have degree >= 1 */ ASSERT_EQ(out.count, 2); cbm_store_search_free(&out); /* max_degree = 0 should find nodes with no counted degree edges */ params.min_degree = -1; /* no min */ params.max_degree = 0; /* only zero-degree nodes */ params.label = "Function"; rc = cbm_store_search(s, ¶ms, &out); ASSERT_EQ(rc, CBM_STORE_OK); /* Neither function has degree 0, so 0 results */ ASSERT_EQ(out.count, 0); cbm_store_search_free(&out); cbm_store_close(s); PASS(); } TEST(store_search_degree_counts_inherits) { cbm_store_t *s = cbm_store_open_memory(); cbm_store_upsert_project(s, "test", "/tmp/test"); cbm_node_t parent = {.project = "test", .label = "Class", .name = "AbstractAttachmentDto", .qualified_name = "test.AbstractAttachmentDto"}; int64_t parent_id = cbm_store_upsert_node(s, &parent); ASSERT_GT(parent_id, 0); const char *child_names[] = {"AttachmentDtoA", "AttachmentDtoB", "AttachmentDtoC"}; const char *child_qns[] = {"test.AttachmentDtoA", "test.AttachmentDtoB", "test.AttachmentDtoC"}; for (int i = 0; i < 3; i++) { cbm_node_t child = {.project = "test", .label = "Class", .name = child_names[i], .qualified_name = child_qns[i]}; int64_t child_id = cbm_store_upsert_node(s, &child); ASSERT_GT(child_id, 0); cbm_edge_t edge = { .project = "test", .source_id = child_id, .target_id = parent_id, .type = "INHERITS"}; ASSERT_GT(cbm_store_insert_edge(s, &edge), 0); } int in_deg = 0; int out_deg = 0; cbm_store_node_degree(s, parent_id, &in_deg, &out_deg); ASSERT_EQ(in_deg, 0); ASSERT_EQ(out_deg, 0); cbm_search_params_t params = { .project = "test", .label = "Class", .min_degree = -1, .max_degree = -1}; cbm_search_output_t out = {0}; int rc = cbm_store_search(s, ¶ms, &out); ASSERT_EQ(rc, CBM_STORE_OK); int parent_idx = search_result_index_by_name(&out, "AbstractAttachmentDto"); ASSERT_GTE(parent_idx, 0); ASSERT_EQ(out.results[parent_idx].in_degree, 3); ASSERT_EQ(out.results[parent_idx].out_degree, 0); cbm_store_search_free(&out); cbm_store_close(s); PASS(); } TEST(store_search_degree_calls_plus_inherits_no_double_count) { cbm_store_t *s = cbm_store_open_memory(); cbm_store_upsert_project(s, "test", "/tmp/test"); cbm_node_t child = {.project = "test", .label = "Class", .name = "AttachmentDto", .qualified_name = "test.AttachmentDto"}; cbm_node_t parent = {.project = "test", .label = "Class", .name = "BaseAttachmentDto", .qualified_name = "test.BaseAttachmentDto"}; cbm_node_t fn = {.project = "test", .label = "Function", .name = "normalizeAttachment", .qualified_name = "test.normalizeAttachment"}; int64_t child_id = cbm_store_upsert_node(s, &child); int64_t parent_id = cbm_store_upsert_node(s, &parent); int64_t fn_id = cbm_store_upsert_node(s, &fn); ASSERT_GT(child_id, 0); ASSERT_GT(parent_id, 0); ASSERT_GT(fn_id, 0); cbm_edge_t calls = { .project = "test", .source_id = child_id, .target_id = fn_id, .type = "CALLS"}; cbm_edge_t inherits = { .project = "test", .source_id = child_id, .target_id = parent_id, .type = "INHERITS"}; ASSERT_GT(cbm_store_insert_edge(s, &calls), 0); ASSERT_GT(cbm_store_insert_edge(s, &inherits), 0); int in_deg = 0; int out_deg = 0; cbm_store_node_degree(s, child_id, &in_deg, &out_deg); ASSERT_EQ(in_deg, 0); ASSERT_EQ(out_deg, 1); cbm_search_params_t params = { .project = "test", .label = "Class", .min_degree = -1, .max_degree = -1}; cbm_search_output_t out = {0}; int rc = cbm_store_search(s, ¶ms, &out); ASSERT_EQ(rc, CBM_STORE_OK); int child_idx = search_result_index_by_name(&out, "AttachmentDto"); ASSERT_GTE(child_idx, 0); ASSERT_EQ(out.results[child_idx].in_degree, 0); ASSERT_EQ(out.results[child_idx].out_degree, 2); cbm_store_search_free(&out); cbm_store_close(s); PASS(); } TEST(store_search_min_degree_includes_inherits_only) { cbm_store_t *s = cbm_store_open_memory(); cbm_store_upsert_project(s, "test", "/tmp/test"); cbm_node_t child = {.project = "test", .label = "Class", .name = "InheritanceOnlyChild", .qualified_name = "test.InheritanceOnlyChild"}; cbm_node_t parent = {.project = "test", .label = "Class", .name = "InheritanceOnlyParent", .qualified_name = "test.InheritanceOnlyParent"}; int64_t child_id = cbm_store_upsert_node(s, &child); int64_t parent_id = cbm_store_upsert_node(s, &parent); ASSERT_GT(child_id, 0); ASSERT_GT(parent_id, 0); cbm_edge_t edge = { .project = "test", .source_id = child_id, .target_id = parent_id, .type = "INHERITS"}; ASSERT_GT(cbm_store_insert_edge(s, &edge), 0); cbm_search_params_t params = { .project = "test", .label = "Class", .min_degree = 1, .max_degree = -1}; cbm_search_output_t out = {0}; int rc = cbm_store_search(s, ¶ms, &out); ASSERT_EQ(rc, CBM_STORE_OK); ASSERT_GTE(search_result_index_by_name(&out, "InheritanceOnlyParent"), 0); cbm_store_search_free(&out); cbm_store_close(s); PASS(); } TEST(store_search_isolated_node_zero_degree) { cbm_store_t *s = cbm_store_open_memory(); cbm_store_upsert_project(s, "test", "/tmp/test"); cbm_node_t node = {.project = "test", .label = "Class", .name = "LonelyClass", .qualified_name = "test.LonelyClass"}; int64_t node_id = cbm_store_upsert_node(s, &node); ASSERT_GT(node_id, 0); int in_deg = 0; int out_deg = 0; cbm_store_node_degree(s, node_id, &in_deg, &out_deg); ASSERT_EQ(in_deg, 0); ASSERT_EQ(out_deg, 0); cbm_search_params_t params = { .project = "test", .label = "Class", .min_degree = -1, .max_degree = -1}; cbm_search_output_t out = {0}; int rc = cbm_store_search(s, ¶ms, &out); ASSERT_EQ(rc, CBM_STORE_OK); int idx = search_result_index_by_name(&out, "LonelyClass"); ASSERT_GTE(idx, 0); ASSERT_EQ(out.results[idx].in_degree, 0); ASSERT_EQ(out.results[idx].out_degree, 0); cbm_store_search_free(&out); cbm_store_close(s); PASS(); } /* ── Search all (no filters) ────────────────────────────────────── */ TEST(store_search_all) { int64_t ids[3]; cbm_store_t *s = setup_search_store(ids); cbm_search_params_t params = {.project = "test", .min_degree = -1, .max_degree = -1}; cbm_search_output_t out = {0}; int rc = cbm_store_search(s, ¶ms, &out); ASSERT_EQ(rc, CBM_STORE_OK); ASSERT_EQ(out.count, 3); ASSERT_EQ(out.total, 3); cbm_store_search_free(&out); cbm_store_close(s); PASS(); } /* ── BFS traversal ──────────────────────────────────────────────── */ TEST(store_bfs_outbound) { int64_t ids[4]; cbm_store_t *s = cbm_store_open_memory(); cbm_store_upsert_project(s, "test", "/tmp/test"); /* A → B → C → D chain */ cbm_node_t na = { .project = "test", .label = "Function", .name = "A", .qualified_name = "test.A"}; cbm_node_t nb = { .project = "test", .label = "Function", .name = "B", .qualified_name = "test.B"}; cbm_node_t nc = { .project = "test", .label = "Function", .name = "C", .qualified_name = "test.C"}; cbm_node_t nd = { .project = "test", .label = "Function", .name = "D", .qualified_name = "test.D"}; ids[0] = cbm_store_upsert_node(s, &na); ids[1] = cbm_store_upsert_node(s, &nb); ids[2] = cbm_store_upsert_node(s, &nc); ids[3] = cbm_store_upsert_node(s, &nd); cbm_edge_t e1 = {.project = "test", .source_id = ids[0], .target_id = ids[1], .type = "CALLS"}; cbm_edge_t e2 = {.project = "test", .source_id = ids[1], .target_id = ids[2], .type = "CALLS"}; cbm_edge_t e3 = {.project = "test", .source_id = ids[2], .target_id = ids[3], .type = "CALLS"}; cbm_store_insert_edge(s, &e1); cbm_store_insert_edge(s, &e2); cbm_store_insert_edge(s, &e3); /* BFS from A, outbound, depth 3 */ const char *types[] = {"CALLS"}; cbm_traverse_result_t result = {0}; int rc = cbm_store_bfs(s, ids[0], "outbound", types, 1, 3, 100, &result); ASSERT_EQ(rc, CBM_STORE_OK); ASSERT_STR_EQ(result.root.name, "A"); ASSERT_GTE(result.visited_count, 3); /* B, C, D */ cbm_store_traverse_free(&result); /* BFS with depth=1 */ rc = cbm_store_bfs(s, ids[0], "outbound", types, 1, 1, 100, &result); ASSERT_EQ(rc, CBM_STORE_OK); ASSERT_EQ(result.visited_count, 1); /* only B */ cbm_store_traverse_free(&result); cbm_store_close(s); PASS(); } TEST(store_bfs_inbound) { int64_t ids[3]; cbm_store_t *s = cbm_store_open_memory(); cbm_store_upsert_project(s, "test", "/tmp/test"); cbm_node_t na = { .project = "test", .label = "Function", .name = "A", .qualified_name = "test.A"}; cbm_node_t nb = { .project = "test", .label = "Function", .name = "B", .qualified_name = "test.B"}; cbm_node_t nc = { .project = "test", .label = "Function", .name = "C", .qualified_name = "test.C"}; ids[0] = cbm_store_upsert_node(s, &na); ids[1] = cbm_store_upsert_node(s, &nb); ids[2] = cbm_store_upsert_node(s, &nc); /* A → C, B → C */ cbm_edge_t e1 = {.project = "test", .source_id = ids[0], .target_id = ids[2], .type = "CALLS"}; cbm_edge_t e2 = {.project = "test", .source_id = ids[1], .target_id = ids[2], .type = "CALLS"}; cbm_store_insert_edge(s, &e1); cbm_store_insert_edge(s, &e2); /* BFS from C, inbound → should find A and B */ const char *types[] = {"CALLS"}; cbm_traverse_result_t result = {0}; int rc = cbm_store_bfs(s, ids[2], "inbound", types, 1, 3, 100, &result); ASSERT_EQ(rc, CBM_STORE_OK); ASSERT_EQ(result.visited_count, 2); /* A and B */ cbm_store_traverse_free(&result); cbm_store_close(s); PASS(); } /* ── Transaction ────────────────────────────────────────────────── */ TEST(store_transaction_commit) { cbm_store_t *s = cbm_store_open_memory(); cbm_store_upsert_project(s, "test", "/tmp/test"); cbm_store_begin(s); cbm_node_t n = { .project = "test", .label = "Function", .name = "TxTest", .qualified_name = "test.TxTest"}; cbm_store_upsert_node(s, &n); cbm_store_commit(s); int cnt = cbm_store_count_nodes(s, "test"); ASSERT_EQ(cnt, 1); cbm_store_close(s); PASS(); } TEST(store_transaction_rollback) { cbm_store_t *s = cbm_store_open_memory(); cbm_store_upsert_project(s, "test", "/tmp/test"); cbm_store_begin(s); cbm_node_t n = { .project = "test", .label = "Function", .name = "TxTest", .qualified_name = "test.TxTest"}; cbm_store_upsert_node(s, &n); cbm_store_rollback(s); int cnt = cbm_store_count_nodes(s, "test"); ASSERT_EQ(cnt, 0); cbm_store_close(s); PASS(); } /* ── Bulk write mode ────────────────────────────────────────────── */ TEST(store_bulk_write_mode) { cbm_store_t *s = cbm_store_open_memory(); cbm_store_upsert_project(s, "test", "/tmp/test"); cbm_store_begin_bulk(s); cbm_store_drop_indexes(s); /* Insert many nodes in bulk */ for (int i = 0; i < 50; i++) { char name[16], qn[32]; snprintf(name, sizeof(name), "f%d", i); snprintf(qn, sizeof(qn), "test.f%d", i); cbm_node_t n = {.project = "test", .label = "Function", .name = name, .qualified_name = qn}; cbm_store_upsert_node(s, &n); } cbm_store_create_indexes(s); cbm_store_end_bulk(s); int cnt = cbm_store_count_nodes(s, "test"); ASSERT_EQ(cnt, 50); cbm_store_close(s); PASS(); } /* ── Schema introspection ───────────────────────────────────────── */ TEST(store_schema_info) { int64_t ids[3]; cbm_store_t *s = setup_search_store(ids); cbm_schema_info_t schema = {0}; int rc = cbm_store_get_schema(s, "test", &schema); ASSERT_EQ(rc, CBM_STORE_OK); /* Should have labels: Function, Class */ ASSERT_GTE(schema.node_label_count, 2); /* Should have edge type: CALLS */ ASSERT_GTE(schema.edge_type_count, 1); cbm_store_schema_free(&schema); cbm_store_close(s); PASS(); } /* ── Search with exclude_labels ─────────────────────────────────── */ TEST(store_search_exclude_labels) { cbm_store_t *s = cbm_store_open_memory(); cbm_store_upsert_project(s, "test", "/tmp/test"); /* Create nodes with different labels */ cbm_node_t n1 = {.project = "test", .label = "Function", .name = "node_Function", .qualified_name = "test.Function.node_0", .file_path = "test.go"}; cbm_node_t n2 = {.project = "test", .label = "Route", .name = "node_Route", .qualified_name = "test.Route.node_1", .file_path = "test.go"}; cbm_node_t n3 = {.project = "test", .label = "Method", .name = "node_Method", .qualified_name = "test.Method.node_2", .file_path = "test.go"}; cbm_node_t n4 = {.project = "test", .label = "Route", .name = "node_Route2", .qualified_name = "test.Route.node_3", .file_path = "test.go"}; cbm_store_upsert_node(s, &n1); cbm_store_upsert_node(s, &n2); cbm_store_upsert_node(s, &n3); cbm_store_upsert_node(s, &n4); /* Search without exclusion */ cbm_search_params_t params = { .project = "test", .limit = 100, .min_degree = -1, .max_degree = -1}; cbm_search_output_t out = {0}; int rc = cbm_store_search(s, ¶ms, &out); ASSERT_EQ(rc, CBM_STORE_OK); int total = out.total; ASSERT_EQ(total, 4); cbm_store_search_free(&out); /* Search with Route excluded */ const char *excl[] = {"Route", NULL}; cbm_search_params_t params2 = {.project = "test", .limit = 100, .min_degree = -1, .max_degree = -1, .exclude_labels = excl}; cbm_search_output_t out2 = {0}; rc = cbm_store_search(s, ¶ms2, &out2); ASSERT_EQ(rc, CBM_STORE_OK); ASSERT_TRUE(out2.total < total); /* Verify no Route nodes in results */ for (int i = 0; i < out2.count; i++) { ASSERT_FALSE(strcmp(out2.results[i].node.label, "Route") == 0); } cbm_store_search_free(&out2); cbm_store_close(s); PASS(); } /* ── Dump to file ──────────────────────────────────────────────── */ TEST(store_dump_to_file) { cbm_store_t *s = cbm_store_open_memory(); cbm_store_upsert_project(s, "test", "/tmp/test"); cbm_node_t n = {.project = "test", .label = "Function", .name = "Hello", .qualified_name = "test.main.Hello", .file_path = "main.go", .start_line = 1, .end_line = 5, .properties_json = "{\"sig\":\"func Hello()\"}"}; int64_t id = cbm_store_upsert_node(s, &n); ASSERT_TRUE(id > 0); /* Dump to temp file */ char *td = th_mktempdir("cbm_dump"); char path[256]; snprintf(path, sizeof(path), "%s/test.db", td); int rc = cbm_store_dump_to_file(s, path); ASSERT_EQ(rc, CBM_STORE_OK); cbm_store_close(s); /* Open dumped file and verify data */ cbm_store_t *disk = cbm_store_open_path(path); ASSERT_NOT_NULL(disk); cbm_node_t found = {0}; rc = cbm_store_find_node_by_qn(disk, "test", "test.main.Hello", &found); ASSERT_EQ(rc, CBM_STORE_OK); ASSERT_STR_EQ(found.name, "Hello"); cbm_node_free_fields(&found); cbm_store_close(disk); unlink(path); PASS(); } /* ── BFS with cross-service (HTTP_CALLS) edges ─────────────────── */ TEST(store_bfs_cross_service) { cbm_store_t *s = cbm_store_open_memory(); cbm_store_upsert_project(s, "test", "/tmp/test"); cbm_node_t na = { .project = "test", .label = "Function", .name = "A", .qualified_name = "test.A"}; cbm_node_t nb = { .project = "test", .label = "Function", .name = "B", .qualified_name = "test.B"}; int64_t idA = cbm_store_upsert_node(s, &na); int64_t idB = cbm_store_upsert_node(s, &nb); cbm_edge_t e = {.project = "test", .source_id = idA, .target_id = idB, .type = "HTTP_CALLS"}; cbm_store_insert_edge(s, &e); /* BFS from A with both CALLS and HTTP_CALLS */ const char *types[] = {"CALLS", "HTTP_CALLS"}; cbm_traverse_result_t result = {0}; int rc = cbm_store_bfs(s, idA, "outbound", types, 2, 1, 200, &result); ASSERT_EQ(rc, CBM_STORE_OK); ASSERT_GTE(result.visited_count, 1); /* B */ /* Verify that we found B via HTTP_CALLS */ int found_b = 0; for (int i = 0; i < result.visited_count; i++) { if (strcmp(result.visited[i].node.name, "B") == 0) found_b = 1; } ASSERT_TRUE(found_b); /* Check edges contain HTTP_CALLS type */ int found_http = 0; for (int i = 0; i < result.edge_count; i++) { if (strcmp(result.edges[i].type, "HTTP_CALLS") == 0) found_http = 1; } ASSERT_TRUE(found_http); cbm_store_traverse_free(&result); cbm_store_close(s); PASS(); } /* ── BFS depth-limited chain ───────────────────────────────────── */ TEST(store_bfs_depth_chain) { cbm_store_t *s = cbm_store_open_memory(); cbm_store_upsert_project(s, "test", "/tmp/test"); /* Build chain: A → B → C → D */ cbm_node_t na = { .project = "test", .label = "Function", .name = "A", .qualified_name = "test.A"}; cbm_node_t nb = { .project = "test", .label = "Function", .name = "B", .qualified_name = "test.B"}; cbm_node_t nc = { .project = "test", .label = "Function", .name = "C", .qualified_name = "test.C"}; cbm_node_t nd = { .project = "test", .label = "Function", .name = "D", .qualified_name = "test.D"}; int64_t idA = cbm_store_upsert_node(s, &na); int64_t idB = cbm_store_upsert_node(s, &nb); int64_t idC = cbm_store_upsert_node(s, &nc); int64_t idD = cbm_store_upsert_node(s, &nd); cbm_edge_t e1 = {.project = "test", .source_id = idA, .target_id = idB, .type = "CALLS"}; cbm_edge_t e2 = {.project = "test", .source_id = idB, .target_id = idC, .type = "CALLS"}; cbm_edge_t e3 = {.project = "test", .source_id = idC, .target_id = idD, .type = "CALLS"}; cbm_store_insert_edge(s, &e1); cbm_store_insert_edge(s, &e2); cbm_store_insert_edge(s, &e3); /* BFS from A, depth=3 should find B(hop1), C(hop2), D(hop3) */ const char *types[] = {"CALLS"}; cbm_traverse_result_t result = {0}; int rc = cbm_store_bfs(s, idA, "outbound", types, 1, 3, 100, &result); ASSERT_EQ(rc, CBM_STORE_OK); ASSERT_EQ(result.visited_count, 3); /* Verify hop distances */ for (int i = 0; i < result.visited_count; i++) { if (strcmp(result.visited[i].node.name, "B") == 0) ASSERT_EQ(result.visited[i].hop, 1); if (strcmp(result.visited[i].node.name, "C") == 0) ASSERT_EQ(result.visited[i].hop, 2); if (strcmp(result.visited[i].node.name, "D") == 0) ASSERT_EQ(result.visited[i].hop, 3); } cbm_store_traverse_free(&result); cbm_store_close(s); PASS(); } /* ── Search case insensitive ───────────────────────────────────── */ TEST(store_search_case_insensitive) { cbm_store_t *s = cbm_store_open_memory(); cbm_store_upsert_project(s, "test", "/tmp/test"); cbm_node_t n = {.project = "test", .label = "Function", .name = "HandleRequest", .qualified_name = "test.HandleRequest"}; cbm_store_upsert_node(s, &n); /* Case-insensitive search (default) */ cbm_search_params_t params = {.project = "test", .name_pattern = ".*handlerequest.*", .min_degree = -1, .max_degree = -1, .case_sensitive = false}; cbm_search_output_t out = {0}; int rc = cbm_store_search(s, ¶ms, &out); ASSERT_EQ(rc, CBM_STORE_OK); ASSERT_EQ(out.count, 1); cbm_store_search_free(&out); /* Case-sensitive search — should NOT match */ cbm_search_params_t params2 = {.project = "test", .name_pattern = ".*handlerequest.*", .min_degree = -1, .max_degree = -1, .case_sensitive = true}; cbm_search_output_t out2 = {0}; rc = cbm_store_search(s, ¶ms2, &out2); ASSERT_EQ(rc, CBM_STORE_OK); ASSERT_EQ(out2.count, 0); cbm_store_search_free(&out2); cbm_store_close(s); PASS(); } /* ── Impact: HopToRisk ─────────────────────────────────────────── */ TEST(store_hop_to_risk) { ASSERT_STR_EQ(cbm_risk_label(cbm_hop_to_risk(1)), "CRITICAL"); ASSERT_STR_EQ(cbm_risk_label(cbm_hop_to_risk(2)), "HIGH"); ASSERT_STR_EQ(cbm_risk_label(cbm_hop_to_risk(3)), "MEDIUM"); ASSERT_STR_EQ(cbm_risk_label(cbm_hop_to_risk(4)), "LOW"); ASSERT_STR_EQ(cbm_risk_label(cbm_hop_to_risk(5)), "LOW"); ASSERT_STR_EQ(cbm_risk_label(cbm_hop_to_risk(10)), "LOW"); PASS(); } /* ── Impact: BuildImpactSummary ────────────────────────────────── */ TEST(store_build_impact_summary) { cbm_node_hop_t hops[5] = { {.node = {.id = 1}, .hop = 1}, {.node = {.id = 2}, .hop = 1}, {.node = {.id = 3}, .hop = 2}, {.node = {.id = 4}, .hop = 3}, {.node = {.id = 5}, .hop = 4}, }; cbm_edge_info_t edges[1] = { {.from_name = "A", .to_name = "B", .type = "CALLS"}, }; cbm_impact_summary_t s = cbm_build_impact_summary(hops, 5, edges, 1); ASSERT_EQ(s.critical, 2); ASSERT_EQ(s.high, 1); ASSERT_EQ(s.medium, 1); ASSERT_EQ(s.low, 1); ASSERT_EQ(s.total, 5); ASSERT_FALSE(s.has_cross_service); PASS(); } /* ── Impact: cross-service detection ──────────────────────────── */ TEST(store_cross_service_detection) { cbm_node_hop_t hops[1] = {{.node = {.id = 1}, .hop = 1}}; cbm_edge_info_t edges_http[1] = { {.from_name = "A", .to_name = "B", .type = "HTTP_CALLS"}, }; cbm_impact_summary_t s1 = cbm_build_impact_summary(hops, 1, edges_http, 1); ASSERT_TRUE(s1.has_cross_service); cbm_edge_info_t edges_async[1] = { {.from_name = "A", .to_name = "B", .type = "ASYNC_CALLS"}, }; cbm_impact_summary_t s2 = cbm_build_impact_summary(hops, 1, edges_async, 1); ASSERT_TRUE(s2.has_cross_service); PASS(); } /* ── Impact: DeduplicateHops ──────────────────────────────────── */ TEST(store_deduplicate_hops) { cbm_node_hop_t hops[4] = { {.node = {.id = 1, .name = "A"}, .hop = 2}, {.node = {.id = 1, .name = "A"}, .hop = 3}, /* duplicate at higher hop */ {.node = {.id = 2, .name = "B"}, .hop = 1}, {.node = {.id = 3, .name = "C"}, .hop = 3}, }; cbm_node_hop_t *result = NULL; int count = 0; int rc = cbm_deduplicate_hops(hops, 4, &result, &count); ASSERT_EQ(rc, CBM_STORE_OK); ASSERT_EQ(count, 3); /* Find node 1 — should have minimum hop = 2 */ int found1 = 0; for (int i = 0; i < count; i++) { if (result[i].node.id == 1) { ASSERT_EQ(result[i].hop, 2); found1 = 1; } } ASSERT_TRUE(found1); free(result); PASS(); } /* ── BFS with risk labels (from store_test.go) ─────────────────── */ TEST(store_bfs_with_risk_labels) { cbm_store_t *s = cbm_store_open_memory(); cbm_store_upsert_project(s, "test", "/tmp/test"); /* Build chain: A → B → C → D */ cbm_node_t na = { .project = "test", .label = "Function", .name = "A", .qualified_name = "test.A"}; cbm_node_t nb = { .project = "test", .label = "Function", .name = "B", .qualified_name = "test.B"}; cbm_node_t nc = { .project = "test", .label = "Function", .name = "C", .qualified_name = "test.C"}; cbm_node_t nd = { .project = "test", .label = "Function", .name = "D", .qualified_name = "test.D"}; int64_t idA = cbm_store_upsert_node(s, &na); (void)cbm_store_upsert_node(s, &nb); (void)cbm_store_upsert_node(s, &nc); (void)cbm_store_upsert_node(s, &nd); cbm_edge_t e1 = {.project = "test", .source_id = idA, .target_id = idA + 1, .type = "CALLS"}; cbm_edge_t e2 = { .project = "test", .source_id = idA + 1, .target_id = idA + 2, .type = "CALLS"}; cbm_edge_t e3 = { .project = "test", .source_id = idA + 2, .target_id = idA + 3, .type = "CALLS"}; cbm_store_insert_edge(s, &e1); cbm_store_insert_edge(s, &e2); cbm_store_insert_edge(s, &e3); const char *types[] = {"CALLS"}; cbm_traverse_result_t result = {0}; int rc = cbm_store_bfs(s, idA, "outbound", types, 1, 3, 200, &result); ASSERT_EQ(rc, CBM_STORE_OK); /* Deduplicate */ cbm_node_hop_t *deduped = NULL; int dcount = 0; cbm_deduplicate_hops(result.visited, result.visited_count, &deduped, &dcount); ASSERT_EQ(dcount, 3); /* Verify risk labels */ for (int i = 0; i < dcount; i++) { if (strcmp(deduped[i].node.name, "B") == 0) ASSERT_STR_EQ(cbm_risk_label(cbm_hop_to_risk(deduped[i].hop)), "CRITICAL"); if (strcmp(deduped[i].node.name, "C") == 0) ASSERT_STR_EQ(cbm_risk_label(cbm_hop_to_risk(deduped[i].hop)), "HIGH"); if (strcmp(deduped[i].node.name, "D") == 0) ASSERT_STR_EQ(cbm_risk_label(cbm_hop_to_risk(deduped[i].hop)), "MEDIUM"); } /* Build summary */ cbm_impact_summary_t summary = cbm_build_impact_summary(deduped, dcount, result.edges, result.edge_count); ASSERT_EQ(summary.critical, 1); ASSERT_EQ(summary.high, 1); ASSERT_EQ(summary.medium, 1); ASSERT_EQ(summary.total, 3); free(deduped); cbm_store_traverse_free(&result); cbm_store_close(s); PASS(); } /* ── BFS cross-service summary ─────────────────────────────────── */ TEST(store_bfs_cross_service_summary) { cbm_store_t *s = cbm_store_open_memory(); cbm_store_upsert_project(s, "test", "/tmp/test"); cbm_node_t na = { .project = "test", .label = "Function", .name = "A", .qualified_name = "test.A"}; cbm_node_t nb = { .project = "test", .label = "Function", .name = "B", .qualified_name = "test.B"}; int64_t idA = cbm_store_upsert_node(s, &na); int64_t idB = cbm_store_upsert_node(s, &nb); cbm_edge_t e = {.project = "test", .source_id = idA, .target_id = idB, .type = "HTTP_CALLS"}; cbm_store_insert_edge(s, &e); const char *types[] = {"CALLS", "HTTP_CALLS"}; cbm_traverse_result_t result = {0}; int rc = cbm_store_bfs(s, idA, "outbound", types, 2, 1, 200, &result); ASSERT_EQ(rc, CBM_STORE_OK); cbm_impact_summary_t summary = cbm_build_impact_summary(result.visited, result.visited_count, result.edges, result.edge_count); ASSERT_TRUE(summary.has_cross_service); cbm_store_traverse_free(&result); cbm_store_close(s); PASS(); } /* ── GlobToLike ─────────────────────────────────────────────────── */ TEST(store_glob_to_like) { struct { const char *pattern; const char *want; } tests[] = { {"**/*.py", "%%.py"}, {"**/dir/**", "%dir%"}, {"*.go", "%.go"}, {"src/**", "src%"}, {"**/test_*.py", "%test_%.py"}, {"file?.txt", "file_.txt"}, {"exact.go", "exact.go"}, {"**/custom-pip-package/**", "%custom-pip-package%"}, }; for (int i = 0; i < 8; i++) { char *got = cbm_glob_to_like(tests[i].pattern); ASSERT_NOT_NULL(got); ASSERT_STR_EQ(got, tests[i].want); free(got); } /* NULL returns NULL */ ASSERT_NULL(cbm_glob_to_like(NULL)); PASS(); } /* ── ExtractLikeHints ────────────────────────────────────────────── */ TEST(store_extract_like_hints) { char *hints[16]; int n; /* Basic: .*handler.* → ["handler"] */ n = cbm_extract_like_hints(".*handler.*", hints, 16); ASSERT_EQ(n, 1); ASSERT_STR_EQ(hints[0], "handler"); free(hints[0]); /* Multiple segments: .*Order.*Handler.* → ["Order", "Handler"] */ n = cbm_extract_like_hints(".*Order.*Handler.*", hints, 16); ASSERT_EQ(n, 2); ASSERT_STR_EQ(hints[0], "Order"); ASSERT_STR_EQ(hints[1], "Handler"); free(hints[0]); free(hints[1]); /* Plain literal: "handler" → ["handler"] */ n = cbm_extract_like_hints("handler", hints, 16); ASSERT_EQ(n, 1); ASSERT_STR_EQ(hints[0], "handler"); free(hints[0]); /* Anchored: ^handleRequest$ → ["handleRequest"] */ n = cbm_extract_like_hints("^handleRequest$", hints, 16); ASSERT_EQ(n, 1); ASSERT_STR_EQ(hints[0], "handleRequest"); free(hints[0]); /* Too generic: .* → no hints */ n = cbm_extract_like_hints(".*", hints, 16); ASSERT_EQ(n, 0); /* Short literal: .*ab.* → "ab" is only 2 chars, below threshold */ n = cbm_extract_like_hints(".*ab.*", hints, 16); ASSERT_EQ(n, 0); /* Exactly 3 chars: .*abc.* → ["abc"] */ n = cbm_extract_like_hints(".*abc.*", hints, 16); ASSERT_EQ(n, 1); ASSERT_STR_EQ(hints[0], "abc"); free(hints[0]); /* Alternation: bail out */ n = cbm_extract_like_hints(".*foo|.*bar", hints, 16); ASSERT_EQ(n, 0); n = cbm_extract_like_hints(".*Order.*|.*Handler.*", hints, 16); ASSERT_EQ(n, 0); /* Escaped dot: \\. is ".", only 1 char */ n = cbm_extract_like_hints("\\.", hints, 16); ASSERT_EQ(n, 0); /* Multi-segment with underscore: .*test_.*helper.* → ["test_", "helper"] */ n = cbm_extract_like_hints(".*test_.*helper.*", hints, 16); ASSERT_EQ(n, 2); ASSERT_STR_EQ(hints[0], "test_"); ASSERT_STR_EQ(hints[1], "helper"); free(hints[0]); free(hints[1]); /* NULL safety */ n = cbm_extract_like_hints(NULL, hints, 16); ASSERT_EQ(n, 0); PASS(); } /* ── EnsureCaseInsensitive ──────────────────────────────────────── */ TEST(store_ensure_case_insensitive) { ASSERT_STR_EQ(cbm_ensure_case_insensitive("handler"), "(?i)handler"); ASSERT_STR_EQ(cbm_ensure_case_insensitive("(?i)handler"), "(?i)handler"); ASSERT_STR_EQ(cbm_ensure_case_insensitive(".*Order.*"), "(?i).*Order.*"); ASSERT_STR_EQ(cbm_ensure_case_insensitive(""), "(?i)"); PASS(); } /* ── StripCaseFlag ──────────────────────────────────────────────── */ TEST(store_strip_case_flag) { ASSERT_STR_EQ(cbm_strip_case_flag("(?i)handler"), "handler"); ASSERT_STR_EQ(cbm_strip_case_flag("handler"), "handler"); ASSERT_STR_EQ(cbm_strip_case_flag("(?i)(?i)double"), "(?i)double"); PASS(); } TEST(store_batch_count_degrees) { cbm_store_t *s = cbm_store_open_memory(); ASSERT_NOT_NULL(s); cbm_store_upsert_project(s, "test", "/tmp/test"); /* A -> B, A -> C, B -> C (CALLS), A -> C (USAGE) */ cbm_node_t na = { .project = "test", .label = "Function", .name = "A", .qualified_name = "test.A"}; cbm_node_t nb = { .project = "test", .label = "Function", .name = "B", .qualified_name = "test.B"}; cbm_node_t nc = { .project = "test", .label = "Function", .name = "C", .qualified_name = "test.C"}; int64_t idA = cbm_store_upsert_node(s, &na); int64_t idB = cbm_store_upsert_node(s, &nb); int64_t idC = cbm_store_upsert_node(s, &nc); cbm_edge_t e1 = {.project = "test", .source_id = idA, .target_id = idB, .type = "CALLS"}; cbm_edge_t e2 = {.project = "test", .source_id = idA, .target_id = idC, .type = "CALLS"}; cbm_edge_t e3 = {.project = "test", .source_id = idB, .target_id = idC, .type = "CALLS"}; cbm_edge_t e4 = {.project = "test", .source_id = idA, .target_id = idC, .type = "USAGE"}; cbm_store_insert_edge(s, &e1); cbm_store_insert_edge(s, &e2); cbm_store_insert_edge(s, &e3); cbm_store_insert_edge(s, &e4); /* All edge types */ int64_t ids3[] = {idA, idB, idC}; int in3[3], out3[3]; int rc = cbm_store_batch_count_degrees(s, ids3, 3, NULL, in3, out3); ASSERT_EQ(rc, CBM_STORE_OK); /* A: in=0, out=3 (2 CALLS + 1 USAGE) */ ASSERT_EQ(in3[0], 0); ASSERT_EQ(out3[0], 3); /* B: in=1, out=1 */ ASSERT_EQ(in3[1], 1); ASSERT_EQ(out3[1], 1); /* C: in=3, out=0 */ ASSERT_EQ(in3[2], 3); ASSERT_EQ(out3[2], 0); /* Filtered by CALLS only */ int64_t ids2[] = {idA, idC}; int in2[2], out2[2]; rc = cbm_store_batch_count_degrees(s, ids2, 2, "CALLS", in2, out2); ASSERT_EQ(rc, CBM_STORE_OK); /* A: in=0, out=2 (CALLS only) */ ASSERT_EQ(in2[0], 0); ASSERT_EQ(out2[0], 2); /* C: in=2, out=0 (CALLS only) */ ASSERT_EQ(in2[1], 2); ASSERT_EQ(out2[1], 0); cbm_store_close(s); PASS(); } /* ── GlobToLike edge cases ──────────────────────────────────────── */ TEST(store_glob_to_like_empty) { char *got = cbm_glob_to_like(""); ASSERT_NOT_NULL(got); ASSERT_STR_EQ(got, ""); free(got); PASS(); } TEST(store_glob_to_like_only_star) { char *got = cbm_glob_to_like("*"); ASSERT_NOT_NULL(got); ASSERT_STR_EQ(got, "%"); free(got); PASS(); } TEST(store_glob_to_like_consecutive_doublestar) { /* double-star slash double-star should collapse to %% */ char *got = cbm_glob_to_like("**/**"); ASSERT_NOT_NULL(got); ASSERT_STR_EQ(got, "%%"); free(got); PASS(); } TEST(store_glob_to_like_dot_and_brackets) { /* Dots and brackets are literal in glob-to-LIKE — passed through */ char *got = cbm_glob_to_like("src/[abc]/*.ts"); ASSERT_NOT_NULL(got); ASSERT_STR_EQ(got, "src/[abc]/%.ts"); free(got); PASS(); } TEST(store_glob_to_like_question_marks) { /* Multiple ? should produce multiple _ */ char *got = cbm_glob_to_like("f???.txt"); ASSERT_NOT_NULL(got); ASSERT_STR_EQ(got, "f___.txt"); free(got); PASS(); } /* ── ExtractLikeHints edge cases ───────────────────────────────── */ TEST(store_extract_like_hints_null_out) { /* NULL out array */ int n = cbm_extract_like_hints(".*handler.*", NULL, 16); ASSERT_EQ(n, 0); PASS(); } TEST(store_extract_like_hints_zero_max) { char *hints[4]; int n = cbm_extract_like_hints(".*handler.*", hints, 0); ASSERT_EQ(n, 0); PASS(); } TEST(store_extract_like_hints_alternation_complex) { char *hints[16]; /* Alternation with multiple segments on each side */ int n = cbm_extract_like_hints("(foo|bar)baz", hints, 16); ASSERT_EQ(n, 0); PASS(); } TEST(store_extract_like_hints_short_segments) { char *hints[16]; /* All segments < 3 chars — no hints */ int n = cbm_extract_like_hints(".*ab.*cd.*", hints, 16); ASSERT_EQ(n, 0); PASS(); } TEST(store_extract_like_hints_complex_multi_segment) { char *hints[16]; /* .*Foo.*Bar.* should extract both */ int n = cbm_extract_like_hints(".*Foo.*Bar.*", hints, 16); ASSERT_EQ(n, 2); ASSERT_STR_EQ(hints[0], "Foo"); ASSERT_STR_EQ(hints[1], "Bar"); free(hints[0]); free(hints[1]); PASS(); } TEST(store_extract_like_hints_max_out_limit) { char *hints[2]; /* More segments than max_out — should stop at max */ int n = cbm_extract_like_hints(".*aaa.*bbb.*ccc.*ddd.*", hints, 2); ASSERT_EQ(n, 2); ASSERT_STR_EQ(hints[0], "aaa"); ASSERT_STR_EQ(hints[1], "bbb"); free(hints[0]); free(hints[1]); PASS(); } TEST(store_extract_like_hints_escaped_chars) { char *hints[16]; /* Backslash escaping: \. makes the dot literal, so it becomes part of the * accumulated literal string. ".handler" is the extracted hint. */ int n = cbm_extract_like_hints("\\.handler", hints, 16); ASSERT_EQ(n, 1); ASSERT_STR_EQ(hints[0], ".handler"); free(hints[0]); PASS(); } /* ── Case helper edge cases ────────────────────────────────────── */ TEST(store_ensure_case_insensitive_null) { const char *result = cbm_ensure_case_insensitive(NULL); ASSERT_STR_EQ(result, ""); PASS(); } TEST(store_ensure_case_insensitive_already_ci) { /* Already case-insensitive — should NOT double-prefix */ ASSERT_STR_EQ(cbm_ensure_case_insensitive("(?i).*Order.*"), "(?i).*Order.*"); PASS(); } TEST(store_ensure_case_insensitive_plain) { ASSERT_STR_EQ(cbm_ensure_case_insensitive("FooBar"), "(?i)FooBar"); PASS(); } TEST(store_strip_case_flag_null) { const char *result = cbm_strip_case_flag(NULL); ASSERT_STR_EQ(result, ""); PASS(); } TEST(store_strip_case_flag_no_flag) { ASSERT_STR_EQ(cbm_strip_case_flag("plain_pattern"), "plain_pattern"); PASS(); } TEST(store_strip_case_flag_empty) { ASSERT_STR_EQ(cbm_strip_case_flag(""), ""); PASS(); } /* ── Architecture helper edge cases ────────────────────────────── */ TEST(store_qn_to_package_single_segment) { /* No dots — returns empty string */ ASSERT_STR_EQ(cbm_qn_to_package("nodots"), ""); PASS(); } TEST(store_qn_to_package_two_segments) { /* project.name — returns segment[1] */ ASSERT_STR_EQ(cbm_qn_to_package("proj.name"), "name"); PASS(); } TEST(store_qn_to_package_many_segments) { /* project.dir.pkg.Func — 4+ segments returns segment[2] */ ASSERT_STR_EQ(cbm_qn_to_package("myproj.dir.pkg.Func"), "pkg"); PASS(); } TEST(store_qn_to_package_null) { ASSERT_STR_EQ(cbm_qn_to_package(NULL), ""); PASS(); } TEST(store_qn_to_package_empty) { ASSERT_STR_EQ(cbm_qn_to_package(""), ""); PASS(); } TEST(store_qn_to_top_package_single_segment) { ASSERT_STR_EQ(cbm_qn_to_top_package("nodots"), ""); PASS(); } TEST(store_qn_to_top_package_two_segments) { /* project.dir — returns "dir" */ ASSERT_STR_EQ(cbm_qn_to_top_package("proj.dir"), "dir"); PASS(); } TEST(store_qn_to_top_package_many_segments) { /* Always returns segment[1] regardless of depth */ ASSERT_STR_EQ(cbm_qn_to_top_package("proj.dir.sub.Func"), "dir"); PASS(); } TEST(store_qn_to_top_package_null) { ASSERT_STR_EQ(cbm_qn_to_top_package(NULL), ""); PASS(); } TEST(store_is_test_file_various) { /* Positive cases */ ASSERT_TRUE(cbm_is_test_file_path("test_handler.py")); ASSERT_TRUE(cbm_is_test_file_path("handler_test.go")); ASSERT_TRUE(cbm_is_test_file_path("handler.test.ts")); ASSERT_FALSE(cbm_is_test_file_path("handler.spec.ts")); /* "spec" not "test" — no match */ ASSERT_TRUE(cbm_is_test_file_path("src/__tests__/handler.js")); ASSERT_TRUE(cbm_is_test_file_path("tests/unit/handler.py")); /* Negative cases */ ASSERT_FALSE(cbm_is_test_file_path("handler.go")); ASSERT_FALSE(cbm_is_test_file_path("main.py")); ASSERT_FALSE(cbm_is_test_file_path("service.ts")); /* Edge: NULL and empty */ ASSERT_FALSE(cbm_is_test_file_path(NULL)); ASSERT_FALSE(cbm_is_test_file_path("")); PASS(); } /* ── Risk/impact edge cases ────────────────────────────────────── */ TEST(store_hop_to_risk_all_levels) { /* hop 0 hits the default case → LOW */ ASSERT_EQ(cbm_hop_to_risk(0), CBM_RISK_LOW); /* hop 1 → CRITICAL */ ASSERT_EQ(cbm_hop_to_risk(1), CBM_RISK_CRITICAL); /* hop 2 → HIGH */ ASSERT_EQ(cbm_hop_to_risk(2), CBM_RISK_HIGH); /* hop 3 → MEDIUM */ ASSERT_EQ(cbm_hop_to_risk(3), CBM_RISK_MEDIUM); /* hop 4+ → LOW */ ASSERT_EQ(cbm_hop_to_risk(4), CBM_RISK_LOW); ASSERT_EQ(cbm_hop_to_risk(100), CBM_RISK_LOW); /* negative → LOW (default) */ ASSERT_EQ(cbm_hop_to_risk(-1), CBM_RISK_LOW); PASS(); } TEST(store_risk_label_all_levels) { ASSERT_STR_EQ(cbm_risk_label(CBM_RISK_CRITICAL), "CRITICAL"); ASSERT_STR_EQ(cbm_risk_label(CBM_RISK_HIGH), "HIGH"); ASSERT_STR_EQ(cbm_risk_label(CBM_RISK_MEDIUM), "MEDIUM"); ASSERT_STR_EQ(cbm_risk_label(CBM_RISK_LOW), "LOW"); /* Out-of-range enum value falls to default → LOW */ ASSERT_STR_EQ(cbm_risk_label((cbm_risk_level_t)99), "LOW"); PASS(); } TEST(store_impact_summary_empty) { /* Zero hops and edges */ cbm_impact_summary_t s = cbm_build_impact_summary(NULL, 0, NULL, 0); ASSERT_EQ(s.total, 0); ASSERT_EQ(s.critical, 0); ASSERT_EQ(s.high, 0); ASSERT_EQ(s.medium, 0); ASSERT_EQ(s.low, 0); ASSERT_FALSE(s.has_cross_service); PASS(); } SUITE(store_search) { RUN_TEST(store_search_by_label); RUN_TEST(store_search_by_name_pattern); RUN_TEST(store_search_empty_label_ignored); RUN_TEST(store_search_by_file_pattern); RUN_TEST(store_search_file_pattern_substring_issue200); RUN_TEST(store_search_pagination); RUN_TEST(store_search_degree_filter); RUN_TEST(store_search_degree_counts_inherits); RUN_TEST(store_search_degree_calls_plus_inherits_no_double_count); RUN_TEST(store_search_min_degree_includes_inherits_only); RUN_TEST(store_search_isolated_node_zero_degree); RUN_TEST(store_search_all); RUN_TEST(store_search_exclude_labels); RUN_TEST(store_search_case_insensitive); RUN_TEST(store_bfs_outbound); RUN_TEST(store_bfs_inbound); RUN_TEST(store_bfs_cross_service); RUN_TEST(store_bfs_depth_chain); RUN_TEST(store_transaction_commit); RUN_TEST(store_transaction_rollback); RUN_TEST(store_bulk_write_mode); RUN_TEST(store_schema_info); RUN_TEST(store_dump_to_file); RUN_TEST(store_hop_to_risk); RUN_TEST(store_build_impact_summary); RUN_TEST(store_cross_service_detection); RUN_TEST(store_deduplicate_hops); RUN_TEST(store_bfs_with_risk_labels); RUN_TEST(store_bfs_cross_service_summary); RUN_TEST(store_glob_to_like); RUN_TEST(store_extract_like_hints); RUN_TEST(store_ensure_case_insensitive); RUN_TEST(store_strip_case_flag); RUN_TEST(store_batch_count_degrees); /* Edge case tests */ RUN_TEST(store_glob_to_like_empty); RUN_TEST(store_glob_to_like_only_star); RUN_TEST(store_glob_to_like_consecutive_doublestar); RUN_TEST(store_glob_to_like_dot_and_brackets); RUN_TEST(store_glob_to_like_question_marks); RUN_TEST(store_extract_like_hints_null_out); RUN_TEST(store_extract_like_hints_zero_max); RUN_TEST(store_extract_like_hints_alternation_complex); RUN_TEST(store_extract_like_hints_short_segments); RUN_TEST(store_extract_like_hints_complex_multi_segment); RUN_TEST(store_extract_like_hints_max_out_limit); RUN_TEST(store_extract_like_hints_escaped_chars); RUN_TEST(store_ensure_case_insensitive_null); RUN_TEST(store_ensure_case_insensitive_already_ci); RUN_TEST(store_ensure_case_insensitive_plain); RUN_TEST(store_strip_case_flag_null); RUN_TEST(store_strip_case_flag_no_flag); RUN_TEST(store_strip_case_flag_empty); RUN_TEST(store_qn_to_package_single_segment); RUN_TEST(store_qn_to_package_two_segments); RUN_TEST(store_qn_to_package_many_segments); RUN_TEST(store_qn_to_package_null); RUN_TEST(store_qn_to_package_empty); RUN_TEST(store_qn_to_top_package_single_segment); RUN_TEST(store_qn_to_top_package_two_segments); RUN_TEST(store_qn_to_top_package_many_segments); RUN_TEST(store_qn_to_top_package_null); RUN_TEST(store_is_test_file_various); RUN_TEST(store_hop_to_risk_all_levels); RUN_TEST(store_risk_label_all_levels); RUN_TEST(store_impact_summary_empty); }