Files
wehub-resource-sync 41cb1c0170
OpenSSF Scorecard / scorecard (push) Failing after 0s
DCO / dco (push) Failing after 0s
CodeQL SAST / analyze (push) Failing after 1s
Deploy Pages / deploy (push) Failing after 1s
chore: import upstream snapshot with attribution
2026-07-13 12:28:05 +08:00

1427 lines
50 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* test_store_arch.c — Tests for architecture, ADR, Louvain, and helper functions.
*
* Ports from internal/store/architecture_test.go:
* TestGetArchitectureAll, TestArchEntryPointsExcludeTests,
* TestArchHotspotsExcludeTests, TestGetArchitectureSpecificAspects,
* TestGetArchitectureEmpty, TestArchLanguages, TestArchLayers,
* TestArchClusters, TestArchFileTree, TestArchRoutes, TestArchHotspots,
* TestArchBoundaries, TestSearchCaseInsensitiveDefault,
* TestSearchCaseSensitiveExplicit, TestEnsureCaseInsensitive,
* TestStripCaseFlag, TestStoreAndRetrieveADR, TestStoreADRUpsert,
* TestDeleteADR, TestDeleteADRNotFound, TestParseADRSections,
* TestRenderADR, TestParseRenderRoundTrip, TestUpdateADRSections,
* TestUpdateADRSectionsOverflow, TestUpdateADRSectionsNoExisting,
* TestValidateADRContentAllSections, TestValidateADRContentMissingSections,
* TestValidateADRContentEmpty, TestValidateADRSectionKeysValid,
* TestValidateADRSectionKeysInvalid, TestValidateADRSectionKeysEmpty,
* TestLouvainBasic, TestLouvainEmpty, TestLouvainSingleNode,
* TestLouvainConverges, TestQnToPackage, TestQnToTopPackage,
* TestFindArchitectureDocs, TestFindArchitectureDocsEmpty,
* TestIsTestFilePath
*/
#include "test_framework.h"
#include <store/store.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include <time.h>
/* ── Helper: create architecture test store ──────────────────────── */
static cbm_store_t *setup_arch_test_store(void) {
cbm_store_t *s = cbm_store_open_memory();
if (!s)
return NULL;
cbm_store_upsert_project(s, "test", "/tmp/test");
/* Files */
const char *files[] = {"main.go", "handler.go", "service.go", "model.py", "utils.js"};
for (int i = 0; i < 5; i++) {
char qn[64];
snprintf(qn, sizeof(qn), "test.%s", files[i]);
cbm_node_t n = {.project = "test",
.label = "File",
.name = files[i],
.qualified_name = qn,
.file_path = files[i]};
cbm_store_upsert_node(s, &n);
}
/* Packages */
cbm_node_t pkg1 = {
.project = "test", .label = "Package", .name = "cmd", .qualified_name = "test.cmd"};
cbm_node_t pkg2 = {
.project = "test", .label = "Package", .name = "handler", .qualified_name = "test.handler"};
cbm_node_t pkg3 = {
.project = "test", .label = "Package", .name = "service", .qualified_name = "test.service"};
cbm_store_upsert_node(s, &pkg1);
cbm_store_upsert_node(s, &pkg2);
cbm_store_upsert_node(s, &pkg3);
/* Functions with 4-segment QNs */
cbm_node_t fn_main = {.project = "test",
.label = "Function",
.name = "main",
.qualified_name = "test.cmd.server.main",
.file_path = "cmd/server/main.go",
.properties_json = "{\"is_entry_point\":true}"};
int64_t id_main = cbm_store_upsert_node(s, &fn_main);
cbm_node_t fn_handle = {.project = "test",
.label = "Function",
.name = "HandleRequest",
.qualified_name = "test.internal.handler.HandleRequest",
.file_path = "internal/handler/handler.go",
.properties_json = "{\"is_entry_point\":true}"};
int64_t id_handle = cbm_store_upsert_node(s, &fn_handle);
cbm_node_t fn_process = {.project = "test",
.label = "Function",
.name = "ProcessOrder",
.qualified_name = "test.internal.service.ProcessOrder",
.file_path = "internal/service/service.go"};
int64_t id_process = cbm_store_upsert_node(s, &fn_process);
cbm_node_t fn_validate = {.project = "test",
.label = "Function",
.name = "ValidateOrder",
.qualified_name = "test.internal.service.ValidateOrder",
.file_path = "internal/service/service.go"};
int64_t id_validate = cbm_store_upsert_node(s, &fn_validate);
cbm_node_t fn_helper = {.project = "test",
.label = "Function",
.name = "formatDate",
.qualified_name = "test.internal.service.formatDate",
.file_path = "internal/service/service.go"};
int64_t id_helper = cbm_store_upsert_node(s, &fn_helper);
/* Test function (should be excluded) */
cbm_node_t fn_test = {.project = "test",
.label = "Function",
.name = "TestHandleRequest",
.qualified_name = "test.internal.handler.handler_test.TestHandleRequest",
.file_path = "internal/handler/handler_test.go",
.properties_json = "{\"is_entry_point\":true}"};
int64_t id_test = cbm_store_upsert_node(s, &fn_test);
/* Route */
cbm_node_t route = {
.project = "test",
.label = "Route",
.name = "/api/orders",
.qualified_name = "test.internal.handler.route./api/orders",
.properties_json =
"{\"method\":\"POST\",\"path\":\"/api/orders\",\"handler\":\"HandleRequest\"}"};
cbm_store_upsert_node(s, &route);
/* Edges: main → HandleRequest → ProcessOrder → ValidateOrder
* ProcessOrder → formatDate
* TestHandleRequest → HandleRequest */
cbm_edge_t e1 = {
.project = "test", .source_id = id_main, .target_id = id_handle, .type = "CALLS"};
cbm_edge_t e2 = {
.project = "test", .source_id = id_handle, .target_id = id_process, .type = "CALLS"};
cbm_edge_t e3 = {
.project = "test", .source_id = id_process, .target_id = id_validate, .type = "CALLS"};
cbm_edge_t e4 = {
.project = "test", .source_id = id_process, .target_id = id_helper, .type = "CALLS"};
cbm_edge_t e5 = {
.project = "test", .source_id = id_test, .target_id = id_handle, .type = "CALLS"};
cbm_store_insert_edge(s, &e1);
cbm_store_insert_edge(s, &e2);
cbm_store_insert_edge(s, &e3);
cbm_store_insert_edge(s, &e4);
cbm_store_insert_edge(s, &e5);
return s;
}
/* ── Architecture tests ──────────────────────────────────────────── */
TEST(arch_get_all) {
cbm_store_t *s = setup_arch_test_store();
cbm_architecture_info_t info;
ASSERT_EQ(cbm_store_get_architecture(s, "test", NULL, NULL, 0, &info), CBM_STORE_OK);
ASSERT_TRUE(info.language_count > 0);
ASSERT_TRUE(info.package_count > 0);
ASSERT_TRUE(info.entry_point_count > 0);
ASSERT_TRUE(info.route_count > 0);
ASSERT_TRUE(info.hotspot_count > 0);
ASSERT_TRUE(info.boundary_count > 0);
cbm_store_architecture_free(&info);
cbm_store_close(s);
PASS();
}
TEST(arch_entry_points_exclude_tests) {
cbm_store_t *s = setup_arch_test_store();
cbm_architecture_info_t info;
memset(&info, 0, sizeof(info));
const char *aspects[] = {"entry_points"};
ASSERT_EQ(cbm_store_get_architecture(s, "test", NULL, aspects, 1, &info), CBM_STORE_OK);
for (int i = 0; i < info.entry_point_count; i++) {
ASSERT_TRUE(strstr(info.entry_points[i].file, "test") == NULL);
}
ASSERT_EQ(info.entry_point_count, 2); /* main, HandleRequest */
cbm_store_architecture_free(&info);
cbm_store_close(s);
PASS();
}
TEST(arch_hotspots_exclude_tests) {
cbm_store_t *s = setup_arch_test_store();
cbm_architecture_info_t info;
memset(&info, 0, sizeof(info));
const char *aspects[] = {"hotspots"};
ASSERT_EQ(cbm_store_get_architecture(s, "test", NULL, aspects, 1, &info), CBM_STORE_OK);
for (int i = 0; i < info.hotspot_count; i++) {
ASSERT_TRUE(strstr(info.hotspots[i].name, "Test") == NULL);
}
cbm_store_architecture_free(&info);
cbm_store_close(s);
PASS();
}
TEST(arch_specific_aspects) {
cbm_store_t *s = setup_arch_test_store();
cbm_architecture_info_t info;
const char *aspects[] = {"languages", "hotspots"};
ASSERT_EQ(cbm_store_get_architecture(s, "test", NULL, aspects, 2, &info), CBM_STORE_OK);
ASSERT_TRUE(info.language_count > 0);
ASSERT_TRUE(info.hotspot_count > 0);
/* Not requested: should be zero */
ASSERT_EQ(info.package_count, 0);
ASSERT_EQ(info.entry_point_count, 0);
ASSERT_EQ(info.route_count, 0);
cbm_store_architecture_free(&info);
cbm_store_close(s);
PASS();
}
TEST(arch_path_scoping) {
cbm_store_t *s = cbm_store_open_memory();
ASSERT_NOT_NULL(s);
ASSERT_EQ(cbm_store_upsert_project(s, "pscope", "/tmp/pscope"), CBM_STORE_OK);
cbm_node_t f1 = {.project = "pscope",
.label = "File",
.name = "a.go",
.qualified_name = "pscope.apps.foo.a.go",
.file_path = "apps/foo/a.go"};
cbm_node_t f2 = {.project = "pscope",
.label = "File",
.name = "b.go",
.qualified_name = "pscope.other.b.go",
.file_path = "other/b.go"};
cbm_store_upsert_node(s, &f1);
cbm_store_upsert_node(s, &f2);
cbm_node_t fn_foo = {.project = "pscope",
.label = "Function",
.name = "Foo",
.qualified_name = "pscope.apps.foo.Foo",
.file_path = "apps/foo/a.go"};
cbm_node_t fn_other = {.project = "pscope",
.label = "Function",
.name = "Bar",
.qualified_name = "pscope.other.Bar",
.file_path = "other/b.go"};
cbm_store_upsert_node(s, &fn_foo);
cbm_store_upsert_node(s, &fn_other);
const char *aspects[] = {"languages", "packages"};
cbm_architecture_info_t whole;
memset(&whole, 0, sizeof(whole));
ASSERT_EQ(cbm_store_get_architecture(s, "pscope", NULL, aspects, 2, &whole), CBM_STORE_OK);
cbm_architecture_info_t scoped;
memset(&scoped, 0, sizeof(scoped));
ASSERT_EQ(cbm_store_get_architecture(s, "pscope", "apps/foo", aspects, 2, &scoped),
CBM_STORE_OK);
int whole_go = 0;
int scoped_go = 0;
for (int i = 0; i < whole.language_count; i++) {
if (strcmp(whole.languages[i].language, "Go") == 0) {
whole_go = whole.languages[i].file_count;
}
}
for (int i = 0; i < scoped.language_count; i++) {
if (strcmp(scoped.languages[i].language, "Go") == 0) {
scoped_go = scoped.languages[i].file_count;
}
}
ASSERT_TRUE(whole_go > scoped_go);
ASSERT_EQ(scoped_go, 1);
int whole_pkg_nodes = 0;
for (int i = 0; i < whole.package_count; i++) {
whole_pkg_nodes += whole.packages[i].node_count;
}
int scoped_pkg_nodes = 0;
for (int i = 0; i < scoped.package_count; i++) {
scoped_pkg_nodes += scoped.packages[i].node_count;
}
ASSERT_TRUE(whole_pkg_nodes > scoped_pkg_nodes);
ASSERT_EQ(scoped_pkg_nodes, 1);
ASSERT_TRUE(cbm_store_count_nodes(s, "pscope") > cbm_store_count_nodes_scoped(s, "pscope", "apps/foo"));
cbm_architecture_info_t scoped_slash;
memset(&scoped_slash, 0, sizeof(scoped_slash));
ASSERT_EQ(cbm_store_get_architecture(s, "pscope", "apps/foo/", aspects, 2, &scoped_slash),
CBM_STORE_OK);
int slash_go = 0;
for (int i = 0; i < scoped_slash.language_count; i++) {
if (strcmp(scoped_slash.languages[i].language, "Go") == 0) {
slash_go = scoped_slash.languages[i].file_count;
}
}
ASSERT_EQ(slash_go, scoped_go);
cbm_store_architecture_free(&scoped_slash);
cbm_store_architecture_free(&whole);
cbm_store_architecture_free(&scoped);
cbm_store_close(s);
PASS();
}
TEST(arch_empty_project) {
cbm_store_t *s = cbm_store_open_memory();
ASSERT_NOT_NULL(s);
ASSERT_EQ(cbm_store_upsert_project(s, "empty", "/tmp/empty"), CBM_STORE_OK);
cbm_architecture_info_t info;
const char *aspects[] = {"all"};
ASSERT_EQ(cbm_store_get_architecture(s, "empty", NULL, aspects, 1, &info), CBM_STORE_OK);
/* All should be empty but no errors */
cbm_store_architecture_free(&info);
cbm_store_close(s);
PASS();
}
TEST(arch_languages) {
cbm_store_t *s = setup_arch_test_store();
cbm_architecture_info_t info;
memset(&info, 0, sizeof(info));
const char *aspects[] = {"languages"};
ASSERT_EQ(cbm_store_get_architecture(s, "test", NULL, aspects, 1, &info), CBM_STORE_OK);
/* Check Go=3, Python=1, JavaScript=1 */
int go_count = 0, py_count = 0, js_count = 0;
for (int i = 0; i < info.language_count; i++) {
if (strcmp(info.languages[i].language, "Go") == 0)
go_count = info.languages[i].file_count;
if (strcmp(info.languages[i].language, "Python") == 0)
py_count = info.languages[i].file_count;
if (strcmp(info.languages[i].language, "JavaScript") == 0)
js_count = info.languages[i].file_count;
}
ASSERT_EQ(go_count, 3);
ASSERT_EQ(py_count, 1);
ASSERT_EQ(js_count, 1);
cbm_store_architecture_free(&info);
cbm_store_close(s);
PASS();
}
TEST(arch_routes) {
cbm_store_t *s = setup_arch_test_store();
cbm_architecture_info_t info;
memset(&info, 0, sizeof(info));
const char *aspects[] = {"routes"};
ASSERT_EQ(cbm_store_get_architecture(s, "test", NULL, aspects, 1, &info), CBM_STORE_OK);
ASSERT_EQ(info.route_count, 1);
ASSERT_STR_EQ(info.routes[0].method, "POST");
ASSERT_STR_EQ(info.routes[0].path, "/api/orders");
ASSERT_STR_EQ(info.routes[0].handler, "HandleRequest");
cbm_store_architecture_free(&info);
cbm_store_close(s);
PASS();
}
TEST(arch_hotspots) {
cbm_store_t *s = setup_arch_test_store();
cbm_architecture_info_t info;
memset(&info, 0, sizeof(info));
const char *aspects[] = {"hotspots"};
ASSERT_EQ(cbm_store_get_architecture(s, "test", NULL, aspects, 1, &info), CBM_STORE_OK);
ASSERT_TRUE(info.hotspot_count > 0);
/* ProcessOrder should be a hotspot (called by HandleRequest) */
bool found = false;
for (int i = 0; i < info.hotspot_count; i++) {
if (strcmp(info.hotspots[i].name, "ProcessOrder") == 0) {
found = true;
ASSERT_TRUE(info.hotspots[i].fan_in >= 1);
}
}
/* May not be found with few edges — just log */
(void)found;
cbm_store_architecture_free(&info);
cbm_store_close(s);
PASS();
}
TEST(arch_boundaries) {
cbm_store_t *s = setup_arch_test_store();
cbm_architecture_info_t info;
memset(&info, 0, sizeof(info));
const char *aspects[] = {"boundaries"};
ASSERT_EQ(cbm_store_get_architecture(s, "test", NULL, aspects, 1, &info), CBM_STORE_OK);
ASSERT_TRUE(info.boundary_count > 0);
/* server → handler and handler → service should be present */
bool found_sh = false, found_hs = false;
for (int i = 0; i < info.boundary_count; i++) {
if (strcmp(info.boundaries[i].from, "server") == 0 &&
strcmp(info.boundaries[i].to, "handler") == 0)
found_sh = true;
if (strcmp(info.boundaries[i].from, "handler") == 0 &&
strcmp(info.boundaries[i].to, "service") == 0)
found_hs = true;
}
ASSERT_TRUE(found_sh);
ASSERT_TRUE(found_hs);
cbm_store_architecture_free(&info);
cbm_store_close(s);
PASS();
}
/* Build a synthetic graph (nodes Functions across packages, random CALLS
* edges) and return the wall ms of the "boundaries" aspect. Returns -1 on
* setup/query failure. */
static double timed_boundaries_ms(int n_nodes, int n_edges, int n_pkgs) {
cbm_store_t *s = cbm_store_open_memory();
if (!s) {
return -1;
}
cbm_store_upsert_project(s, "perf", "/tmp/perf");
cbm_store_begin(s);
int64_t *ids = malloc((size_t)n_nodes * sizeof(int64_t));
if (!ids) {
cbm_store_close(s);
return -1;
}
for (int i = 0; i < n_nodes; i++) {
char name[32], qn[64];
snprintf(name, sizeof(name), "fn%d", i);
snprintf(qn, sizeof(qn), "perf.pkg%d.fn%d", i % n_pkgs, i);
cbm_node_t n = {.project = "perf",
.label = "Function",
.name = name,
.qualified_name = qn,
.file_path = "f.c"};
ids[i] = cbm_store_upsert_node(s, &n);
}
uint64_t rng = 42;
for (int i = 0; i < n_edges; i++) {
rng = rng * 6364136223846793005ULL + 1442695040888963407ULL;
int a = (int)((rng >> 33) % (uint64_t)n_nodes);
rng = rng * 6364136223846793005ULL + 1442695040888963407ULL;
int b = (int)((rng >> 33) % (uint64_t)n_nodes);
cbm_edge_t e = {
.project = "perf", .source_id = ids[a], .target_id = ids[b], .type = "CALLS"};
cbm_store_insert_edge(s, &e);
}
cbm_store_commit(s);
free(ids);
cbm_architecture_info_t info;
memset(&info, 0, sizeof(info));
const char *aspects[] = {"boundaries"};
struct timespec t0, t1;
clock_gettime(CLOCK_MONOTONIC, &t0);
int rc = cbm_store_get_architecture(s, "perf", NULL, aspects, 1, &info);
clock_gettime(CLOCK_MONOTONIC, &t1);
double ms =
(double)(t1.tv_sec - t0.tv_sec) * 1000.0 + (double)(t1.tv_nsec - t0.tv_nsec) / 1000000.0;
int bcount = info.boundary_count;
cbm_store_architecture_free(&info);
cbm_store_close(s);
if (rc != CBM_STORE_OK || bcount <= 0) {
return -1;
}
return ms;
}
/* arch_boundaries used a LINEAR SCAN over all def nodes for EVERY CALLS edge
* (lookup_pkg over parallel arrays) — O(E×N). On the Linux kernel graph
* (~1.4M defs × ~1.4M CALLS) get_architecture spun >10 min at 100% CPU. The
* bug was latent while C call extraction was broken (few CALLS edges) and
* surfaced when extraction was fixed.
*
* Guard is SELF-RELATIVE because absolute wall bounds do not survive CI's
* machine spread (207 ms locally vs ~26 s on the shared ubuntu-arm UBSan
* runner for the same work): doubling nodes AND edges must scale the aspect
* by ~2x (linear; allow 3x for noise), while the quadratic scales by ~4x.
* A fast absolute result short-circuits (pre-fix the small size alone took
* >4 s on an M3 Pro, so 2 s means the scan is certainly gone). */
TEST(arch_boundaries_no_quadratic_scan) {
enum { BN_NODES = 40000, BN_EDGES = 80000, BN_PKGS = 200, BN_FAST_MS = 2000 };
double t_small = timed_boundaries_ms(BN_NODES, BN_EDGES, BN_PKGS);
ASSERT_TRUE(t_small >= 0);
if (t_small < (double)BN_FAST_MS) {
printf(" boundaries %dk/%dk: %.0f ms — fast path, linear\n", BN_NODES / 1000,
BN_EDGES / 1000, t_small);
PASS();
}
double t_big = timed_boundaries_ms(BN_NODES * 2, BN_EDGES * 2, BN_PKGS);
ASSERT_TRUE(t_big >= 0);
double ratio = t_big / t_small;
printf(" boundaries %.0f ms -> %.0f ms at 2x size (ratio %.2f, quadratic ~4)\n", t_small,
t_big, ratio);
ASSERT_TRUE(ratio < 3.0);
PASS();
}
TEST(arch_layers) {
cbm_store_t *s = setup_arch_test_store();
cbm_architecture_info_t info;
memset(&info, 0, sizeof(info));
const char *aspects[] = {"layers"};
ASSERT_EQ(cbm_store_get_architecture(s, "test", NULL, aspects, 1, &info), CBM_STORE_OK);
ASSERT_TRUE(info.layer_count > 0);
/* Handler package has routes, should be "api" */
for (int i = 0; i < info.layer_count; i++) {
if (strcmp(info.layers[i].name, "handler") == 0) {
ASSERT_STR_EQ(info.layers[i].layer, "api");
}
}
cbm_store_architecture_free(&info);
cbm_store_close(s);
PASS();
}
TEST(arch_file_tree) {
cbm_store_t *s = setup_arch_test_store();
cbm_architecture_info_t info;
memset(&info, 0, sizeof(info));
const char *aspects[] = {"file_tree"};
ASSERT_EQ(cbm_store_get_architecture(s, "test", NULL, aspects, 1, &info), CBM_STORE_OK);
ASSERT_TRUE(info.file_tree_count > 0);
/* Check that entries have valid types */
for (int i = 0; i < info.file_tree_count; i++) {
ASSERT_TRUE(strcmp(info.file_tree[i].type, "dir") == 0 ||
strcmp(info.file_tree[i].type, "file") == 0);
}
cbm_store_architecture_free(&info);
cbm_store_close(s);
PASS();
}
TEST(arch_clusters) {
cbm_store_t *s = setup_arch_test_store();
cbm_architecture_info_t info;
memset(&info, 0, sizeof(info));
const char *aspects[] = {"clusters"};
ASSERT_EQ(cbm_store_get_architecture(s, "test", NULL, aspects, 1, &info), CBM_STORE_OK);
/* With 5 functions and 4 edges, Louvain should find at least 1 cluster */
if (info.cluster_count == 0) {
/* May need more nodes for meaningful clustering — just log */
cbm_store_architecture_free(&info);
cbm_store_close(s);
PASS();
}
for (int i = 0; i < info.cluster_count; i++) {
ASSERT_TRUE(info.clusters[i].members >= 2);
ASSERT_NOT_NULL(info.clusters[i].label);
ASSERT_TRUE(info.clusters[i].label[0] != '\0');
}
cbm_store_architecture_free(&info);
cbm_store_close(s);
PASS();
}
/* ── ADR tests ──────────────────────────────────────────────────── */
TEST(adr_store_and_retrieve) {
cbm_store_t *s = cbm_store_open_memory();
ASSERT_NOT_NULL(s);
ASSERT_EQ(cbm_store_upsert_project(s, "test", "/tmp/test"), CBM_STORE_OK);
const char *content = "## PURPOSE\nTest project for unit tests.\n\n## STACK\n- Go: speed";
ASSERT_EQ(cbm_store_adr_store(s, "test", content), CBM_STORE_OK);
cbm_adr_t adr;
ASSERT_EQ(cbm_store_adr_get(s, "test", &adr), CBM_STORE_OK);
ASSERT_STR_EQ(adr.content, content);
ASSERT_STR_EQ(adr.project, "test");
ASSERT_TRUE(adr.created_at != NULL && adr.created_at[0] != '\0');
ASSERT_TRUE(adr.updated_at != NULL && adr.updated_at[0] != '\0');
cbm_store_adr_free(&adr);
cbm_store_close(s);
PASS();
}
TEST(adr_upsert) {
cbm_store_t *s = cbm_store_open_memory();
ASSERT_NOT_NULL(s);
ASSERT_EQ(cbm_store_upsert_project(s, "test", "/tmp/test"), CBM_STORE_OK);
ASSERT_EQ(cbm_store_adr_store(s, "test", "v1"), CBM_STORE_OK);
ASSERT_EQ(cbm_store_adr_store(s, "test", "v2"), CBM_STORE_OK);
cbm_adr_t adr;
ASSERT_EQ(cbm_store_adr_get(s, "test", &adr), CBM_STORE_OK);
ASSERT_STR_EQ(adr.content, "v2");
cbm_store_adr_free(&adr);
cbm_store_close(s);
PASS();
}
TEST(adr_delete) {
cbm_store_t *s = cbm_store_open_memory();
ASSERT_NOT_NULL(s);
ASSERT_EQ(cbm_store_upsert_project(s, "test", "/tmp/test"), CBM_STORE_OK);
ASSERT_EQ(cbm_store_adr_store(s, "test", "## PURPOSE\nTest"), CBM_STORE_OK);
ASSERT_EQ(cbm_store_adr_delete(s, "test"), CBM_STORE_OK);
cbm_adr_t adr;
ASSERT_TRUE(cbm_store_adr_get(s, "test", &adr) != CBM_STORE_OK);
cbm_store_close(s);
PASS();
}
TEST(adr_delete_not_found) {
cbm_store_t *s = cbm_store_open_memory();
ASSERT_NOT_NULL(s);
ASSERT_TRUE(cbm_store_adr_delete(s, "nonexistent") != CBM_STORE_OK);
cbm_store_close(s);
PASS();
}
TEST(adr_parse_sections_basic) {
cbm_adr_sections_t sec = cbm_adr_parse_sections("## PURPOSE\nFoo\n\n## STACK\nBar");
ASSERT_EQ(sec.count, 2);
ASSERT_STR_EQ(sec.keys[0], "PURPOSE");
ASSERT_STR_EQ(sec.values[0], "Foo");
ASSERT_STR_EQ(sec.keys[1], "STACK");
ASSERT_STR_EQ(sec.values[1], "Bar");
cbm_adr_sections_free(&sec);
PASS();
}
TEST(adr_parse_sections_all_six) {
cbm_adr_sections_t sec =
cbm_adr_parse_sections("## PURPOSE\nA\n\n## STACK\nB\n\n## ARCHITECTURE\nC\n\n## "
"PATTERNS\nD\n\n## TRADEOFFS\nE\n\n## PHILOSOPHY\nF");
ASSERT_EQ(sec.count, 6);
ASSERT_STR_EQ(sec.keys[0], "PURPOSE");
ASSERT_STR_EQ(sec.values[0], "A");
ASSERT_STR_EQ(sec.keys[5], "PHILOSOPHY");
ASSERT_STR_EQ(sec.values[5], "F");
cbm_adr_sections_free(&sec);
PASS();
}
TEST(adr_parse_sections_non_canonical) {
cbm_adr_sections_t sec =
cbm_adr_parse_sections("## PURPOSE\nFoo\n## CUSTOM\nStill in PURPOSE\n\n## STACK\nBar");
ASSERT_EQ(sec.count, 2);
ASSERT_STR_EQ(sec.keys[0], "PURPOSE");
ASSERT_STR_EQ(sec.values[0], "Foo\n## CUSTOM\nStill in PURPOSE");
ASSERT_STR_EQ(sec.keys[1], "STACK");
ASSERT_STR_EQ(sec.values[1], "Bar");
cbm_adr_sections_free(&sec);
PASS();
}
TEST(adr_parse_sections_empty) {
cbm_adr_sections_t sec = cbm_adr_parse_sections("");
ASSERT_EQ(sec.count, 0);
cbm_adr_sections_free(&sec);
PASS();
}
TEST(adr_parse_sections_preamble) {
cbm_adr_sections_t sec = cbm_adr_parse_sections("preamble\n## PURPOSE\nFoo");
ASSERT_EQ(sec.count, 1);
ASSERT_STR_EQ(sec.keys[0], "PURPOSE");
ASSERT_STR_EQ(sec.values[0], "Foo");
cbm_adr_sections_free(&sec);
PASS();
}
TEST(adr_parse_sections_multiline) {
cbm_adr_sections_t sec =
cbm_adr_parse_sections("## PURPOSE\nLine 1\nLine 2\nLine 3\n\n## STACK\n- Go\n- SQLite");
ASSERT_EQ(sec.count, 2);
ASSERT_STR_EQ(sec.values[0], "Line 1\nLine 2\nLine 3");
ASSERT_STR_EQ(sec.values[1], "- Go\n- SQLite");
cbm_adr_sections_free(&sec);
PASS();
}
TEST(adr_render_canonical_order) {
cbm_adr_sections_t sec = {.count = 2};
sec.keys[0] = strdup("STACK");
sec.values[0] = strdup("Bar");
sec.keys[1] = strdup("PURPOSE");
sec.values[1] = strdup("Foo");
char *rendered = cbm_adr_render(&sec);
ASSERT_STR_EQ(rendered, "## PURPOSE\nFoo\n\n## STACK\nBar");
free(rendered);
cbm_adr_sections_free(&sec);
PASS();
}
TEST(adr_render_all_sections) {
cbm_adr_sections_t sec = {.count = 6};
sec.keys[0] = strdup("PHILOSOPHY");
sec.values[0] = strdup("F");
sec.keys[1] = strdup("PURPOSE");
sec.values[1] = strdup("A");
sec.keys[2] = strdup("STACK");
sec.values[2] = strdup("B");
sec.keys[3] = strdup("ARCHITECTURE");
sec.values[3] = strdup("C");
sec.keys[4] = strdup("PATTERNS");
sec.values[4] = strdup("D");
sec.keys[5] = strdup("TRADEOFFS");
sec.values[5] = strdup("E");
char *rendered = cbm_adr_render(&sec);
ASSERT_STR_EQ(rendered, "## PURPOSE\nA\n\n## STACK\nB\n\n## ARCHITECTURE\nC\n\n## "
"PATTERNS\nD\n\n## TRADEOFFS\nE\n\n## PHILOSOPHY\nF");
free(rendered);
cbm_adr_sections_free(&sec);
PASS();
}
TEST(adr_render_non_canonical) {
cbm_adr_sections_t sec = {.count = 3};
sec.keys[0] = strdup("PURPOSE");
sec.values[0] = strdup("Foo");
sec.keys[1] = strdup("ZEBRA");
sec.values[1] = strdup("Z");
sec.keys[2] = strdup("ALPHA");
sec.values[2] = strdup("A");
char *rendered = cbm_adr_render(&sec);
ASSERT_STR_EQ(rendered, "## PURPOSE\nFoo\n\n## ALPHA\nA\n\n## ZEBRA\nZ");
free(rendered);
cbm_adr_sections_free(&sec);
PASS();
}
TEST(adr_render_empty) {
cbm_adr_sections_t sec = {.count = 0};
char *rendered = cbm_adr_render(&sec);
ASSERT_STR_EQ(rendered, "");
free(rendered);
PASS();
}
TEST(adr_parse_render_roundtrip) {
const char *original =
"## PURPOSE\nTest project\n\n## STACK\n- Go: speed\n- SQLite: embedded\n\n"
"## ARCHITECTURE\nPipeline pattern\n\n## PATTERNS\n- Convention over config\n\n"
"## TRADEOFFS\n- Speed over features\n\n## PHILOSOPHY\n- Keep it simple";
cbm_adr_sections_t sec = cbm_adr_parse_sections(original);
char *rendered = cbm_adr_render(&sec);
ASSERT_STR_EQ(rendered, original);
free(rendered);
cbm_adr_sections_free(&sec);
PASS();
}
TEST(adr_update_sections) {
cbm_store_t *s = cbm_store_open_memory();
ASSERT_NOT_NULL(s);
ASSERT_EQ(cbm_store_upsert_project(s, "test", "/tmp/test"), CBM_STORE_OK);
ASSERT_EQ(cbm_store_adr_store(s, "test", "## PURPOSE\nOriginal purpose\n\n## STACK\n- Go"),
CBM_STORE_OK);
const char *keys[] = {"PATTERNS"};
const char *values[] = {"- Pipeline pattern"};
cbm_adr_t updated;
ASSERT_EQ(cbm_store_adr_update_sections(s, "test", keys, values, 1, &updated), CBM_STORE_OK);
/* Verify all sections preserved */
cbm_adr_sections_t sec = cbm_adr_parse_sections(updated.content);
bool found_purpose = false, found_stack = false, found_patterns = false;
for (int i = 0; i < sec.count; i++) {
if (strcmp(sec.keys[i], "PURPOSE") == 0) {
ASSERT_STR_EQ(sec.values[i], "Original purpose");
found_purpose = true;
}
if (strcmp(sec.keys[i], "STACK") == 0) {
ASSERT_STR_EQ(sec.values[i], "- Go");
found_stack = true;
}
if (strcmp(sec.keys[i], "PATTERNS") == 0) {
ASSERT_STR_EQ(sec.values[i], "- Pipeline pattern");
found_patterns = true;
}
}
ASSERT_TRUE(found_purpose);
ASSERT_TRUE(found_stack);
ASSERT_TRUE(found_patterns);
cbm_adr_sections_free(&sec);
cbm_store_adr_free(&updated);
cbm_store_close(s);
PASS();
}
TEST(adr_update_overflow) {
cbm_store_t *s = cbm_store_open_memory();
ASSERT_NOT_NULL(s);
ASSERT_EQ(cbm_store_upsert_project(s, "test", "/tmp/test"), CBM_STORE_OK);
ASSERT_EQ(cbm_store_adr_store(s, "test", "## PURPOSE\nShort"), CBM_STORE_OK);
/* Create huge content */
char *huge = malloc(CBM_ADR_MAX_LENGTH + 2);
memset(huge, 'x', CBM_ADR_MAX_LENGTH + 1);
huge[CBM_ADR_MAX_LENGTH + 1] = '\0';
const char *keys[] = {"STACK"};
const char *values[] = {huge};
cbm_adr_t out;
ASSERT_TRUE(cbm_store_adr_update_sections(s, "test", keys, values, 1, &out) != CBM_STORE_OK);
free(huge);
cbm_store_close(s);
PASS();
}
TEST(adr_update_no_existing) {
cbm_store_t *s = cbm_store_open_memory();
ASSERT_NOT_NULL(s);
ASSERT_EQ(cbm_store_upsert_project(s, "test", "/tmp/test"), CBM_STORE_OK);
const char *keys[] = {"PURPOSE"};
const char *values[] = {"New purpose"};
cbm_adr_t out;
ASSERT_TRUE(cbm_store_adr_update_sections(s, "test", keys, values, 1, &out) != CBM_STORE_OK);
cbm_store_close(s);
PASS();
}
TEST(adr_validate_all_sections) {
const char *content = "## PURPOSE\nA\n\n## STACK\nB\n\n## ARCHITECTURE\nC\n\n## "
"PATTERNS\nD\n\n## TRADEOFFS\nE\n\n## PHILOSOPHY\nF";
char errbuf[256];
ASSERT_EQ(cbm_adr_validate_content(content, errbuf, sizeof(errbuf)), CBM_STORE_OK);
PASS();
}
TEST(adr_validate_missing_sections) {
const char *content = "## PURPOSE\nA\n\n## STACK\nB";
char errbuf[512];
ASSERT_TRUE(cbm_adr_validate_content(content, errbuf, sizeof(errbuf)) != CBM_STORE_OK);
/* Error should mention missing sections */
ASSERT_TRUE(strstr(errbuf, "ARCHITECTURE") != NULL);
ASSERT_TRUE(strstr(errbuf, "PATTERNS") != NULL);
ASSERT_TRUE(strstr(errbuf, "TRADEOFFS") != NULL);
ASSERT_TRUE(strstr(errbuf, "PHILOSOPHY") != NULL);
PASS();
}
TEST(adr_validate_empty) {
char errbuf[256];
ASSERT_TRUE(cbm_adr_validate_content("", errbuf, sizeof(errbuf)) != CBM_STORE_OK);
PASS();
}
TEST(adr_validate_keys_valid) {
const char *keys[] = {"PURPOSE", "STACK"};
char errbuf[256];
ASSERT_EQ(cbm_adr_validate_section_keys(keys, 2, errbuf, sizeof(errbuf)), CBM_STORE_OK);
PASS();
}
TEST(adr_validate_keys_invalid) {
const char *keys[] = {"PURPOSE", "STACKS", "CUSTOM"};
char errbuf[256];
ASSERT_TRUE(cbm_adr_validate_section_keys(keys, 3, errbuf, sizeof(errbuf)) != CBM_STORE_OK);
ASSERT_TRUE(strstr(errbuf, "STACKS") != NULL);
ASSERT_TRUE(strstr(errbuf, "CUSTOM") != NULL);
PASS();
}
TEST(adr_validate_keys_empty) {
char errbuf[256];
ASSERT_EQ(cbm_adr_validate_section_keys(NULL, 0, errbuf, sizeof(errbuf)), CBM_STORE_OK);
PASS();
}
/* ── Louvain tests ──────────────────────────────────────────────── */
TEST(louvain_basic) {
/* Triangle: 1-2, 2-3, 1-3. Pair: 4-5. */
int64_t nodes[] = {1, 2, 3, 4, 5};
cbm_louvain_edge_t edges[] = {{1, 2}, {2, 3}, {1, 3}, {4, 5}};
cbm_louvain_result_t *result = NULL;
int count = 0;
ASSERT_EQ(cbm_louvain(nodes, 5, edges, 4, &result, &count), CBM_STORE_OK);
ASSERT_EQ(count, 5);
/* Build node→community map */
int comm[6] = {0};
for (int i = 0; i < count; i++) {
comm[result[i].node_id] = result[i].community;
}
/* Triangle should be same community */
ASSERT_EQ(comm[1], comm[2]);
ASSERT_EQ(comm[2], comm[3]);
/* Pair should be same community */
ASSERT_EQ(comm[4], comm[5]);
/* Triangle and pair different */
ASSERT_TRUE(comm[1] != comm[4]);
free(result);
PASS();
}
TEST(louvain_empty) {
cbm_louvain_result_t *result = NULL;
int count = 0;
ASSERT_EQ(cbm_louvain(NULL, 0, NULL, 0, &result, &count), CBM_STORE_OK);
ASSERT_EQ(count, 0);
free(result);
PASS();
}
TEST(louvain_single_node) {
int64_t nodes[] = {42};
cbm_louvain_result_t *result = NULL;
int count = 0;
ASSERT_EQ(cbm_louvain(nodes, 1, NULL, 0, &result, &count), CBM_STORE_OK);
ASSERT_EQ(count, 1);
ASSERT_EQ(result[0].node_id, 42);
free(result);
PASS();
}
TEST(louvain_converges) {
/* Two fully connected clusters of 10 nodes each, bridged by one edge */
int64_t nodes[20];
cbm_louvain_edge_t edges[200];
int nedges = 0;
for (int i = 0; i < 20; i++)
nodes[i] = i + 1;
/* Cluster 1: nodes 1-10, fully connected */
for (int i = 1; i <= 10; i++) {
for (int j = i + 1; j <= 10; j++) {
edges[nedges++] = (cbm_louvain_edge_t){i, j};
}
}
/* Cluster 2: nodes 11-20, fully connected */
for (int i = 11; i <= 20; i++) {
for (int j = i + 1; j <= 20; j++) {
edges[nedges++] = (cbm_louvain_edge_t){i, j};
}
}
/* Bridge */
edges[nedges++] = (cbm_louvain_edge_t){5, 15};
cbm_louvain_result_t *result = NULL;
int count = 0;
ASSERT_EQ(cbm_louvain(nodes, 20, edges, nedges, &result, &count), CBM_STORE_OK);
ASSERT_EQ(count, 20);
/* Count communities */
int comm_map[21] = {0};
for (int i = 0; i < count; i++) {
comm_map[result[i].node_id] = result[i].community;
}
/* Count distinct communities */
int distinct[20];
int ndistinct = 0;
for (int i = 0; i < count; i++) {
bool found = false;
for (int j = 0; j < ndistinct; j++) {
if (distinct[j] == result[i].community) {
found = true;
break;
}
}
if (!found)
distinct[ndistinct++] = result[i].community;
}
ASSERT_TRUE(ndistinct >= 2);
/* Nodes 1-10 mostly in same community */
int same_count = 0;
for (int i = 1; i <= 10; i++) {
if (comm_map[i] == comm_map[1])
same_count++;
}
ASSERT_TRUE(same_count >= 8);
free(result);
PASS();
}
/* ── Leiden multi-level / refinement tests ──────────────────────── */
/* Count distinct community labels in a result. */
static int leiden_count_communities(const cbm_louvain_result_t *r, int n) {
int *seen = malloc((size_t)n * sizeof(int));
int nd = 0;
for (int i = 0; i < n; i++) {
bool found = false;
for (int j = 0; j < nd; j++) {
if (seen[j] == r[i].community) {
found = true;
break;
}
}
if (!found) {
seen[nd++] = r[i].community;
}
}
free(seen);
return nd;
}
/* Verify every community induces a connected subgraph under `edges` — the
* property Leiden's refinement guarantees and single-level Louvain does not. */
static bool leiden_all_communities_connected(const cbm_louvain_result_t *r, int n,
const cbm_louvain_edge_t *edges, int ne) {
bool *vis = calloc((size_t)n, sizeof(bool));
int *stack = malloc((size_t)n * sizeof(int));
bool ok = true;
for (int s = 0; s < n && ok; s++) {
int comm = r[s].community;
bool seeded = false; /* community already verified from an earlier seed? */
for (int t = 0; t < s; t++) {
if (r[t].community == comm) {
seeded = true;
break;
}
}
if (seeded) {
continue;
}
for (int i = 0; i < n; i++) {
vis[i] = false;
}
int sp = 0;
stack[sp++] = s;
vis[s] = true;
while (sp > 0) {
int cur = stack[--sp];
int64_t cid = r[cur].node_id;
for (int e = 0; e < ne; e++) {
int64_t o = 0;
bool inc = false;
if (edges[e].src == cid) {
o = edges[e].dst;
inc = true;
} else if (edges[e].dst == cid) {
o = edges[e].src;
inc = true;
}
if (!inc) {
continue;
}
for (int j = 0; j < n; j++) {
if (r[j].node_id == o) {
if (!vis[j] && r[j].community == comm) {
vis[j] = true;
stack[sp++] = j;
}
break;
}
}
}
}
for (int j = 0; j < n; j++) {
if (r[j].community == comm && !vis[j]) {
ok = false; /* a member was unreachable → disconnected community */
break;
}
}
}
free(vis);
free(stack);
return ok;
}
TEST(leiden_multilevel_collapses_noise) {
/* Four 8-node cliques chained by single bridge edges. Single-level Louvain
* tends to leave many small clusters; multi-level Leiden collapses this to
* roughly four well-separated, internally-connected communities. */
enum { CL = 4, SZ = 8, N = CL * SZ };
int64_t nodes[N];
for (int i = 0; i < N; i++) {
nodes[i] = i + 1;
}
cbm_louvain_edge_t edges[CL * (SZ * (SZ - 1) / 2) + CL];
int ne = 0;
for (int c = 0; c < CL; c++) {
int base = c * SZ + 1;
for (int i = 0; i < SZ; i++) {
for (int j = i + 1; j < SZ; j++) {
edges[ne++] = (cbm_louvain_edge_t){base + i, base + j};
}
}
}
for (int c = 0; c + 1 < CL; c++) {
edges[ne++] = (cbm_louvain_edge_t){c * SZ + 1, (c + 1) * SZ + 1};
}
cbm_louvain_result_t *result = NULL;
int count = 0;
ASSERT_EQ(cbm_louvain(nodes, N, edges, ne, &result, &count), CBM_STORE_OK);
ASSERT_EQ(count, N);
int nc = leiden_count_communities(result, N);
ASSERT_TRUE(nc >= 2);
ASSERT_TRUE(nc <= CL + 1); /* far below N=32: the noise collapsed */
ASSERT_TRUE(leiden_all_communities_connected(result, N, edges, ne));
/* Each clique should be (almost) entirely in one community. */
for (int c = 0; c < CL; c++) {
int base_idx = c * SZ;
int same = 0;
for (int i = 0; i < SZ; i++) {
if (result[base_idx + i].community == result[base_idx].community) {
same++;
}
}
ASSERT_TRUE(same >= SZ - 1);
}
free(result);
PASS();
}
TEST(leiden_resolution_controls_granularity) {
/* Path graph of 30 nodes. Low resolution favours one large community; high
* resolution fragments it into more, smaller communities. */
enum { N = 30 };
int64_t nodes[N];
for (int i = 0; i < N; i++) {
nodes[i] = i + 1;
}
cbm_louvain_edge_t edges[N - 1];
int ne = 0;
for (int i = 0; i + 1 < N; i++) {
edges[ne++] = (cbm_louvain_edge_t){i + 1, i + 2};
}
cbm_louvain_result_t *lo = NULL;
cbm_louvain_result_t *hi = NULL;
int lc = 0;
int hc = 0;
ASSERT_EQ(cbm_leiden(nodes, N, edges, ne, 0.1, &lo, &lc), CBM_STORE_OK);
ASSERT_EQ(cbm_leiden(nodes, N, edges, ne, 5.0, &hi, &hc), CBM_STORE_OK);
ASSERT_EQ(lc, N);
ASSERT_EQ(hc, N);
int n_lo = leiden_count_communities(lo, N);
int n_hi = leiden_count_communities(hi, N);
ASSERT_TRUE(n_hi > n_lo);
ASSERT_TRUE(leiden_all_communities_connected(lo, N, edges, ne));
ASSERT_TRUE(leiden_all_communities_connected(hi, N, edges, ne));
free(lo);
free(hi);
PASS();
}
/* get_architecture "clusters" aspect: Leiden communities surfaced compactly. */
TEST(arch_clusters_basic) {
cbm_store_t *s = cbm_store_open_memory();
cbm_store_upsert_project(s, "test", "/tmp/test");
/* Two 4-function cliques in two packages, one bridge between them. */
int64_t id[8];
for (int i = 0; i < 8; i++) {
char nm[32];
char qn[64];
int grp = i / 4;
snprintf(nm, sizeof(nm), "fn%d", i);
snprintf(qn, sizeof(qn), "test.pkg%d.mod.fn%d", grp, i);
cbm_node_t node = {.project = "test",
.label = "Function",
.name = nm,
.qualified_name = qn,
.file_path = "f.go"};
id[i] = cbm_store_upsert_node(s, &node);
}
for (int g = 0; g < 2; g++) {
for (int a = 0; a < 4; a++) {
for (int b = a + 1; b < 4; b++) {
cbm_edge_t e = {.project = "test",
.source_id = id[(g * 4) + a],
.target_id = id[(g * 4) + b],
.type = "CALLS"};
cbm_store_insert_edge(s, &e);
}
}
}
cbm_edge_t bridge = {
.project = "test", .source_id = id[0], .target_id = id[4], .type = "CALLS"};
cbm_store_insert_edge(s, &bridge);
cbm_architecture_info_t info;
memset(&info, 0, sizeof(info));
const char *aspects[] = {"clusters"};
ASSERT_EQ(cbm_store_get_architecture(s, "test", NULL, aspects, 1, &info), CBM_STORE_OK);
ASSERT_TRUE(info.cluster_count >= 2); /* two dense communities */
for (int i = 0; i < info.cluster_count; i++) {
ASSERT_TRUE(info.clusters[i].members >= 2);
ASSERT_NOT_NULL(info.clusters[i].label);
ASSERT_TRUE(info.clusters[i].cohesion >= 0.0 && info.clusters[i].cohesion <= 1.0);
ASSERT_EQ(info.clusters[i].edge_type_count, 1);
ASSERT_TRUE(info.clusters[i].top_node_count > 0);
}
cbm_store_architecture_free(&info);
cbm_store_close(s);
PASS();
}
/* ── Helper function tests ──────────────────────────────────────── */
TEST(qn_to_package) {
/* 4+ segments: returns segment[2] */
ASSERT_STR_EQ(cbm_qn_to_package("project.internal.store.search.Search"), "store");
ASSERT_STR_EQ(cbm_qn_to_package("project.src.utils.helper.foo"), "utils");
ASSERT_STR_EQ(cbm_qn_to_package("project.src.components.Button.render"), "components");
ASSERT_STR_EQ(cbm_qn_to_package("project.cmd.server.main"), "server");
/* 3 segments: falls back to segment[1] */
ASSERT_STR_EQ(cbm_qn_to_package("project.main.foo"), "main");
ASSERT_STR_EQ(cbm_qn_to_package("project.cmd"), "cmd");
/* Edge cases */
ASSERT_STR_EQ(cbm_qn_to_package("standalone"), "");
ASSERT_STR_EQ(cbm_qn_to_package(""), "");
PASS();
}
TEST(qn_to_top_package) {
ASSERT_STR_EQ(cbm_qn_to_top_package("project.internal.store.search.Search"), "internal");
ASSERT_STR_EQ(cbm_qn_to_top_package("project.src.components.Button"), "src");
ASSERT_STR_EQ(cbm_qn_to_top_package("project.cmd"), "cmd");
ASSERT_STR_EQ(cbm_qn_to_top_package("standalone"), "");
PASS();
}
TEST(is_test_file_path) {
ASSERT_TRUE(!cbm_is_test_file_path("internal/handler/handler.go"));
ASSERT_TRUE(cbm_is_test_file_path("src/__tests__/handler.test.ts"));
ASSERT_TRUE(cbm_is_test_file_path("src/test/java/com/example/Test.java"));
ASSERT_TRUE(cbm_is_test_file_path("tests/test_handler.py"));
ASSERT_TRUE(cbm_is_test_file_path("testdata/fixture.json"));
ASSERT_TRUE(!cbm_is_test_file_path(""));
PASS();
}
TEST(find_architecture_docs) {
cbm_store_t *s = cbm_store_open_memory();
ASSERT_NOT_NULL(s);
ASSERT_EQ(cbm_store_upsert_project(s, "test", "/tmp/test"), CBM_STORE_OK);
const char *fps[] = {"main.go", "ARCHITECTURE.md", "docs/adr/001-use-sqlite.md", "README.md"};
for (int i = 0; i < 4; i++) {
char qn[64];
snprintf(qn, sizeof(qn), "test.%s", fps[i]);
cbm_node_t n = {.project = "test",
.label = "File",
.name = fps[i],
.qualified_name = qn,
.file_path = fps[i]};
cbm_store_upsert_node(s, &n);
}
char **docs = NULL;
int count = 0;
ASSERT_EQ(cbm_store_find_architecture_docs(s, "test", &docs, &count), CBM_STORE_OK);
ASSERT_EQ(count, 2);
bool found_arch = false, found_adr = false;
for (int i = 0; i < count; i++) {
if (strcmp(docs[i], "ARCHITECTURE.md") == 0)
found_arch = true;
if (strcmp(docs[i], "docs/adr/001-use-sqlite.md") == 0)
found_adr = true;
free(docs[i]);
}
free(docs);
ASSERT_TRUE(found_arch);
ASSERT_TRUE(found_adr);
cbm_store_close(s);
PASS();
}
TEST(find_architecture_docs_empty) {
cbm_store_t *s = cbm_store_open_memory();
ASSERT_NOT_NULL(s);
ASSERT_EQ(cbm_store_upsert_project(s, "test", "/tmp/test"), CBM_STORE_OK);
char **docs = NULL;
int count = 0;
ASSERT_EQ(cbm_store_find_architecture_docs(s, "test", &docs, &count), CBM_STORE_OK);
ASSERT_EQ(count, 0);
free(docs);
cbm_store_close(s);
PASS();
}
/* ── Case-insensitive search tests ───────────────────────────────── */
TEST(search_case_insensitive_default) {
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 = "FooBar", .qualified_name = "test.FooBar"};
cbm_node_t n2 = {
.project = "test", .label = "Function", .name = "foobar", .qualified_name = "test.foobar"};
cbm_node_t n3 = {
.project = "test", .label = "Function", .name = "FOOBAR", .qualified_name = "test.FOOBAR"};
cbm_store_upsert_node(s, &n1);
cbm_store_upsert_node(s, &n2);
cbm_store_upsert_node(s, &n3);
/* Default (case_sensitive=false) should match all 3 */
cbm_search_params_t params = {.project = "test",
.name_pattern = "foobar",
.min_degree = -1,
.max_degree = -1,
.case_sensitive = false};
cbm_search_output_t out = {0};
ASSERT_EQ(cbm_store_search(s, &params, &out), CBM_STORE_OK);
ASSERT_EQ(out.count, 3);
cbm_store_search_free(&out);
cbm_store_close(s);
PASS();
}
TEST(search_case_sensitive_explicit) {
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 = "FooBar", .qualified_name = "test.FooBar"};
cbm_node_t n2 = {
.project = "test", .label = "Function", .name = "foobar", .qualified_name = "test.foobar"};
cbm_node_t n3 = {
.project = "test", .label = "Function", .name = "FOOBAR", .qualified_name = "test.FOOBAR"};
cbm_store_upsert_node(s, &n1);
cbm_store_upsert_node(s, &n2);
cbm_store_upsert_node(s, &n3);
/* Explicit case-sensitive should match only "foobar" */
cbm_search_params_t params = {.project = "test",
.name_pattern = "foobar",
.min_degree = -1,
.max_degree = -1,
.case_sensitive = true};
cbm_search_output_t out = {0};
ASSERT_EQ(cbm_store_search(s, &params, &out), CBM_STORE_OK);
ASSERT_EQ(out.count, 1);
if (out.count > 0) {
ASSERT_STR_EQ(out.results[0].node.name, "foobar");
}
cbm_store_search_free(&out);
cbm_store_close(s);
PASS();
}
/* ── Suite ─────────────────────────────────────────────────────── */
SUITE(store_arch) {
/* Architecture */
RUN_TEST(arch_get_all);
RUN_TEST(arch_entry_points_exclude_tests);
RUN_TEST(arch_hotspots_exclude_tests);
RUN_TEST(arch_specific_aspects);
RUN_TEST(arch_path_scoping);
RUN_TEST(arch_empty_project);
RUN_TEST(arch_languages);
RUN_TEST(arch_routes);
RUN_TEST(arch_hotspots);
RUN_TEST(arch_boundaries);
RUN_TEST(arch_boundaries_no_quadratic_scan);
RUN_TEST(arch_layers);
RUN_TEST(arch_file_tree);
RUN_TEST(arch_clusters);
/* ADR */
RUN_TEST(adr_store_and_retrieve);
RUN_TEST(adr_upsert);
RUN_TEST(adr_delete);
RUN_TEST(adr_delete_not_found);
RUN_TEST(adr_parse_sections_basic);
RUN_TEST(adr_parse_sections_all_six);
RUN_TEST(adr_parse_sections_non_canonical);
RUN_TEST(adr_parse_sections_empty);
RUN_TEST(adr_parse_sections_preamble);
RUN_TEST(adr_parse_sections_multiline);
RUN_TEST(adr_render_canonical_order);
RUN_TEST(adr_render_all_sections);
RUN_TEST(adr_render_non_canonical);
RUN_TEST(adr_render_empty);
RUN_TEST(adr_parse_render_roundtrip);
RUN_TEST(adr_update_sections);
RUN_TEST(adr_update_overflow);
RUN_TEST(adr_update_no_existing);
RUN_TEST(adr_validate_all_sections);
RUN_TEST(adr_validate_missing_sections);
RUN_TEST(adr_validate_empty);
RUN_TEST(adr_validate_keys_valid);
RUN_TEST(adr_validate_keys_invalid);
RUN_TEST(adr_validate_keys_empty);
/* Louvain */
RUN_TEST(louvain_basic);
RUN_TEST(louvain_empty);
RUN_TEST(louvain_single_node);
RUN_TEST(louvain_converges);
RUN_TEST(leiden_multilevel_collapses_noise);
RUN_TEST(leiden_resolution_controls_granularity);
RUN_TEST(arch_clusters_basic);
/* Helpers */
RUN_TEST(qn_to_package);
RUN_TEST(qn_to_top_package);
RUN_TEST(is_test_file_path);
RUN_TEST(find_architecture_docs);
RUN_TEST(find_architecture_docs_empty);
/* Case-insensitive search */
RUN_TEST(search_case_insensitive_default);
RUN_TEST(search_case_sensitive_explicit);
}