/* * 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 #include #include #include #include /* ── 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, ¶ms, &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, ¶ms, &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); }