/* * test_pipeline.c — Integration tests for the indexing pipeline. * * Tests pipeline lifecycle, structure pass, and end-to-end indexing * on a temporary directory with known file layout. */ #include "../src/foundation/compat.h" #include "foundation/platform.h" // cbm_normalize_path_sep (drive-canonicalization regression) #include "test_framework.h" #include "test_helpers.h" #include "foundation/mem.h" // cbm_mem_init/budget (back-pressure futile-nap test) #include "pipeline/pipeline.h" #include "pipeline/pipeline_internal.h" #include "store/store.h" #include "git/git_context.h" #include "foundation/dump_verify.h" #include #include #include #include "foundation/compat_thread.h" #include #include #include #include "graph_buffer/graph_buffer.h" #include "yyjson/yyjson.h" #include "sqlite3.h" /* vendored/sqlite3 — PRAGMA integrity_check on dumped DBs */ /* ── Helper: create temp test repo with known layout ───────────── */ static char g_tmpdir[256]; /* Create: * /tmp/cbm_test_XXXXXX/ * main.go (empty) * pkg/ * service.go (empty) * util/ * helper.go (empty) */ static int setup_test_repo(void) { snprintf(g_tmpdir, sizeof(g_tmpdir), "/tmp/cbm_test_XXXXXX"); if (!cbm_mkdtemp(g_tmpdir)) return -1; char path[512]; /* main.go — calls Serve() from pkg */ snprintf(path, sizeof(path), "%s/main.go", g_tmpdir); FILE *f = fopen(path, "w"); if (!f) return -1; fprintf(f, "package main\n\n" "import \"pkg\"\n\n" "func main() {\n" "\tpkg.Serve()\n" "}\n"); fclose(f); /* pkg/ */ snprintf(path, sizeof(path), "%s/pkg", g_tmpdir); cbm_mkdir(path); /* pkg/service.go — calls Help() from util */ snprintf(path, sizeof(path), "%s/pkg/service.go", g_tmpdir); f = fopen(path, "w"); if (!f) return -1; fprintf(f, "package pkg\n\n" "import \"pkg/util\"\n\n" "func Serve() {\n" "\tutil.Help()\n" "}\n"); fclose(f); /* pkg/util/ */ snprintf(path, sizeof(path), "%s/pkg/util", g_tmpdir); cbm_mkdir(path); /* pkg/util/helper.go */ snprintf(path, sizeof(path), "%s/pkg/util/helper.go", g_tmpdir); f = fopen(path, "w"); if (!f) return -1; fprintf(f, "package util\n\nfunc Help() {}\n"); fclose(f); return 0; } /* Recursive remove (simple, not production-grade) */ static void rm_rf(const char *path) { th_rmtree(path); } static void teardown_test_repo(void) { if (g_tmpdir[0]) rm_rf(g_tmpdir); g_tmpdir[0] = '\0'; } /* ── Lifecycle tests ─────────────────────────────────────────────── */ TEST(pipeline_create_free) { cbm_pipeline_t *p = cbm_pipeline_new("/some/path", NULL, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_STR_EQ(cbm_pipeline_project_name(p), "some-path"); cbm_pipeline_free(p); PASS(); } TEST(pipeline_null_repo) { cbm_pipeline_t *p = cbm_pipeline_new(NULL, NULL, CBM_MODE_FULL); ASSERT_NULL(p); PASS(); } TEST(pipeline_free_null) { cbm_pipeline_free(NULL); /* should not crash */ PASS(); } TEST(pipeline_cancel) { cbm_pipeline_t *p = cbm_pipeline_new("/some/path", NULL, CBM_MODE_FULL); ASSERT_NOT_NULL(p); cbm_pipeline_cancel(p); /* Running a cancelled pipeline should return -1 immediately */ int rc = cbm_pipeline_run(p); /* Note: it may fail because /some/path doesn't exist, not because of cancel. * This test just verifies cancel doesn't crash. */ (void)rc; cbm_pipeline_free(p); PASS(); } TEST(pipeline_cancel_null) { cbm_pipeline_cancel(NULL); /* should not crash */ PASS(); } TEST(pipeline_run_null) { int rc = cbm_pipeline_run(NULL); ASSERT_EQ(rc, -1); PASS(); } /* ── Focused: file-backed store persistence ─────────────────────── */ TEST(store_file_persistence) { if (setup_test_repo() != 0) { FAIL("failed to create temp dir"); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/persist_test.db", g_tmpdir); /* Write data */ cbm_store_t *s1 = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s1); cbm_store_upsert_project(s1, "proj", "/tmp"); cbm_node_t n = {.project = "proj", .label = "Function", .name = "foo", .qualified_name = "proj.foo", .file_path = "f.go"}; int64_t id = cbm_store_upsert_node(s1, &n); ASSERT_GT(id, 0); int cnt1 = cbm_store_count_nodes(s1, "proj"); ASSERT_EQ(cnt1, 1); cbm_store_checkpoint(s1); cbm_store_close(s1); /* Reopen and verify */ cbm_store_t *s2 = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s2); int cnt2 = cbm_store_count_nodes(s2, "proj"); ASSERT_EQ(cnt2, 1); cbm_store_close(s2); teardown_test_repo(); PASS(); } TEST(store_bulk_persistence) { if (setup_test_repo() != 0) { FAIL("failed to create temp dir"); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/bulk_test.db", g_tmpdir); /* Verify: begin_bulk + explicit txn + end_bulk persists to file */ cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); cbm_store_upsert_project(s, "proj", "/tmp"); cbm_store_begin_bulk(s); cbm_store_begin(s); cbm_node_t n = {.project = "proj", .label = "Function", .name = "foo", .qualified_name = "proj.foo", .file_path = "f.go"}; int64_t id = cbm_store_upsert_node(s, &n); ASSERT_GT(id, 0); ASSERT_EQ(cbm_store_commit(s), 0); cbm_store_end_bulk(s); cbm_store_checkpoint(s); cbm_store_close(s); /* Reopen and verify data survived */ cbm_store_t *s2 = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s2); ASSERT_EQ(cbm_store_count_nodes(s2, "proj"), 1); cbm_store_close(s2); teardown_test_repo(); PASS(); } /* ── Integration: structure pass on temp repo ────────────────────── */ TEST(pipeline_structure_nodes) { if (setup_test_repo() != 0) { FAIL("failed to create temp dir"); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/test.db", g_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_tmpdir, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); int rc = cbm_pipeline_run(p); ASSERT_EQ(rc, 0); /* Verify results by opening the store */ cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); const char *project = cbm_pipeline_project_name(p); /* Should have nodes: 1 Project + 2 Folders + 3 Files + N Functions */ int node_count = cbm_store_count_nodes(s, project); ASSERT_GTE(node_count, 9); /* 6 structure + at least 3 definitions */ /* Verify project node exists */ cbm_node_t proj_node = {0}; rc = cbm_store_find_node_by_qn(s, project, project, &proj_node); ASSERT_EQ(rc, CBM_STORE_OK); ASSERT_STR_EQ(proj_node.label, "Project"); cbm_node_free_fields(&proj_node); /* Verify folder nodes */ cbm_node_t *folders = NULL; int folder_count = 0; rc = cbm_store_find_nodes_by_label(s, project, "Folder", &folders, &folder_count); ASSERT_EQ(rc, CBM_STORE_OK); ASSERT_GTE(folder_count, 2); /* pkg, pkg/util */ cbm_store_free_nodes(folders, folder_count); /* Verify file nodes */ cbm_node_t *file_nodes = NULL; int file_count = 0; rc = cbm_store_find_nodes_by_label(s, project, "File", &file_nodes, &file_count); ASSERT_EQ(rc, CBM_STORE_OK); ASSERT_GTE(file_count, 3); /* main.go, service.go, helper.go */ cbm_store_free_nodes(file_nodes, file_count); /* Verify edges exist */ int edge_count = cbm_store_count_edges(s, project); ASSERT_GTE(edge_count, 5); /* CONTAINS_FOLDER + CONTAINS_FILE edges */ cbm_store_close(s); cbm_pipeline_free(p); teardown_test_repo(); PASS(); } /* Issue #516: an ADR stored via manage_adr (project_summaries) must survive a * full re-index. A full re-index deletes the DB and rebuilds it from the graph * buffer, which writes an empty project_summaries table; the fix captures the * ADR before the delete and restores it after the rebuild. Reproduce-first: * index, store an ADR, force a full re-index by adding files, assert the ADR * is still present and unchanged. */ TEST(pipeline_adr_survives_full_reindex) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_adr_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("failed to create temp dir"); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/test.db", tmp); /* Initial index with a single source file. */ char path[512]; snprintf(path, sizeof(path), "%s/main.py", tmp); FILE *f = fopen(path, "w"); ASSERT_NOT_NULL(f); fprintf(f, "def foo():\n pass\n"); fclose(f); cbm_pipeline_t *p1 = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p1); ASSERT_EQ(cbm_pipeline_run(p1), 0); const char *project = cbm_pipeline_project_name(p1); char project_copy[256]; snprintf(project_copy, sizeof(project_copy), "%s", project); cbm_pipeline_free(p1); /* Store an ADR. */ const char *adr_text = "# Decision\nWe chose X over Y."; cbm_store_t *s1 = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s1); ASSERT_EQ(cbm_store_adr_store(s1, project_copy, adr_text), CBM_STORE_OK); cbm_store_close(s1); /* Force a full re-index: add enough files to exceed the incremental * threshold so the DB is deleted and rebuilt. */ for (int i = 0; i < 4; i++) { snprintf(path, sizeof(path), "%s/extra%d.py", tmp, i); f = fopen(path, "w"); ASSERT_NOT_NULL(f); fprintf(f, "def g%d():\n return %d\n", i, i); fclose(f); } cbm_pipeline_t *p2 = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p2); ASSERT_EQ(cbm_pipeline_run(p2), 0); cbm_pipeline_free(p2); /* The ADR must still be present and unchanged. */ cbm_store_t *s2 = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s2); cbm_adr_t adr = {0}; int rc = cbm_store_adr_get(s2, project_copy, &adr); ASSERT_EQ(rc, CBM_STORE_OK); ASSERT_NOT_NULL(adr.content); ASSERT_STR_EQ(adr.content, adr_text); cbm_store_adr_free(&adr); cbm_store_close(s2); rm_rf(tmp); PASS(); } TEST(pipeline_structure_edges) { if (setup_test_repo() != 0) { FAIL("failed to create temp dir"); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/test.db", g_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_tmpdir, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); int rc = cbm_pipeline_run(p); ASSERT_EQ(rc, 0); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); const char *project = cbm_pipeline_project_name(p); /* Check CONTAINS_FILE edges */ int cf_count = cbm_store_count_edges_by_type(s, project, "CONTAINS_FILE"); /* Check CONTAINS_FOLDER edges */ int cd_count = cbm_store_count_edges_by_type(s, project, "CONTAINS_FOLDER"); /* Cleanup before assertions (so failures don't leak) */ cbm_store_close(s); cbm_pipeline_free(p); teardown_test_repo(); ASSERT_GTE(cf_count, 3); /* project->main.go, pkg->service.go, util->helper.go */ ASSERT_GTE(cd_count, 1); /* project->pkg (pkg->util may merge on some platforms) */ PASS(); } TEST(pipeline_branch_root_structure) { if (setup_test_repo() != 0) { FAIL("failed to create temp dir"); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/test_branch_root.db", g_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_tmpdir, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); int rc = cbm_pipeline_run(p); ASSERT_EQ(rc, 0); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); const char *project = cbm_pipeline_project_name(p); char branch_qn[1024]; snprintf(branch_qn, sizeof(branch_qn), "%s.__branch__.working-tree", project); cbm_node_t project_node = {0}; cbm_node_t branch_node = {0}; cbm_node_t root_file_node = {0}; cbm_node_t root_folder_node = {0}; rc = cbm_store_find_node_by_qn(s, project, project, &project_node); ASSERT_EQ(rc, CBM_STORE_OK); rc = cbm_store_find_node_by_qn(s, project, branch_qn, &branch_node); ASSERT_EQ(rc, CBM_STORE_OK); ASSERT_STR_EQ(branch_node.label, "Branch"); ASSERT_STR_EQ(branch_node.name, "working-tree"); ASSERT_NOT_NULL(strstr(branch_node.properties_json, "\"is_git\":false")); char *root_folder_qn = cbm_pipeline_fqn_folder(project, "pkg"); char *root_file_qn = cbm_pipeline_fqn_compute(project, "main.go", "__file__"); ASSERT_NOT_NULL(root_folder_qn); ASSERT_NOT_NULL(root_file_qn); rc = cbm_store_find_node_by_qn(s, project, root_folder_qn, &root_folder_node); ASSERT_EQ(rc, CBM_STORE_OK); rc = cbm_store_find_node_by_qn(s, project, root_file_qn, &root_file_node); ASSERT_EQ(rc, CBM_STORE_OK); cbm_edge_t *has_branch = NULL; int has_branch_count = 0; rc = cbm_store_find_edges_by_source_type(s, project_node.id, "HAS_BRANCH", &has_branch, &has_branch_count); ASSERT_EQ(rc, CBM_STORE_OK); ASSERT_EQ(has_branch_count, 1); ASSERT_EQ(has_branch[0].target_id, branch_node.id); cbm_edge_t *project_files = NULL; int project_file_count = 0; rc = cbm_store_find_edges_by_source_type(s, project_node.id, "CONTAINS_FILE", &project_files, &project_file_count); ASSERT_EQ(rc, CBM_STORE_OK); ASSERT_EQ(project_file_count, 0); cbm_edge_t *branch_files = NULL; int branch_file_count = 0; rc = cbm_store_find_edges_by_source_type(s, branch_node.id, "CONTAINS_FILE", &branch_files, &branch_file_count); ASSERT_EQ(rc, CBM_STORE_OK); ASSERT_EQ(branch_file_count, 1); ASSERT_EQ(branch_files[0].target_id, root_file_node.id); cbm_edge_t *branch_folders = NULL; int branch_folder_count = 0; rc = cbm_store_find_edges_by_source_type(s, branch_node.id, "CONTAINS_FOLDER", &branch_folders, &branch_folder_count); ASSERT_EQ(rc, CBM_STORE_OK); ASSERT_EQ(branch_folder_count, 1); ASSERT_EQ(branch_folders[0].target_id, root_folder_node.id); cbm_store_free_edges(has_branch, has_branch_count); cbm_store_free_edges(project_files, project_file_count); cbm_store_free_edges(branch_files, branch_file_count); cbm_store_free_edges(branch_folders, branch_folder_count); cbm_node_free_fields(&project_node); cbm_node_free_fields(&branch_node); cbm_node_free_fields(&root_file_node); cbm_node_free_fields(&root_folder_node); free(root_folder_qn); free(root_file_qn); cbm_store_close(s); cbm_pipeline_free(p); teardown_test_repo(); PASS(); } TEST(pipeline_project_name_derived) { if (setup_test_repo() != 0) { FAIL("failed to create temp dir"); } cbm_pipeline_t *p = cbm_pipeline_new(g_tmpdir, NULL, CBM_MODE_FULL); ASSERT_NOT_NULL(p); /* Project name should be derived from tmpdir path */ const char *name = cbm_pipeline_project_name(p); ASSERT_NOT_NULL(name); ASSERT_TRUE(strlen(name) > 0); cbm_pipeline_free(p); teardown_test_repo(); PASS(); } TEST(pipeline_fast_mode) { if (setup_test_repo() != 0) { FAIL("failed to create temp dir"); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/test_fast.db", g_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_tmpdir, db_path, CBM_MODE_FAST); ASSERT_NOT_NULL(p); int rc = cbm_pipeline_run(p); ASSERT_EQ(rc, 0); /* Just verify it completes without error in fast mode */ cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); const char *project = cbm_pipeline_project_name(p); int node_count = cbm_store_count_nodes(s, project); ASSERT_GT(node_count, 0); cbm_store_close(s); cbm_pipeline_free(p); teardown_test_repo(); PASS(); } /* ── Definitions pass tests ──────────────────────────────────────── */ TEST(pipeline_definitions_function_nodes) { if (setup_test_repo() != 0) { FAIL("failed to create temp dir"); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/test_defs.db", g_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_tmpdir, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); int rc = cbm_pipeline_run(p); ASSERT_EQ(rc, 0); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); const char *project = cbm_pipeline_project_name(p); /* Verify Function nodes extracted from Go source files */ cbm_node_t *funcs = NULL; int func_count = 0; rc = cbm_store_find_nodes_by_label(s, project, "Function", &funcs, &func_count); ASSERT_EQ(rc, CBM_STORE_OK); ASSERT_GTE(func_count, 3); /* main, Serve, Help */ /* Verify each expected function exists */ int found_main = 0, found_serve = 0, found_help = 0; for (int i = 0; i < func_count; i++) { if (strcmp(funcs[i].name, "main") == 0) found_main = 1; else if (strcmp(funcs[i].name, "Serve") == 0) found_serve = 1; else if (strcmp(funcs[i].name, "Help") == 0) found_help = 1; } ASSERT_TRUE(found_main); ASSERT_TRUE(found_serve); ASSERT_TRUE(found_help); cbm_store_free_nodes(funcs, func_count); cbm_store_close(s); cbm_pipeline_free(p); teardown_test_repo(); PASS(); } TEST(pipeline_definitions_defines_edges) { if (setup_test_repo() != 0) { FAIL("failed to create temp dir"); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/test_defs_edges.db", g_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_tmpdir, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); int rc = cbm_pipeline_run(p); ASSERT_EQ(rc, 0); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); const char *project = cbm_pipeline_project_name(p); /* DEFINES edges: File → Function (one per extracted definition) */ int defines_count = cbm_store_count_edges_by_type(s, project, "DEFINES"); ASSERT_GTE(defines_count, 3); /* main.go→main, service.go→Serve, helper.go→Help */ /* CONTAINS_FILE edges should still exist from structure pass */ int cf_count = cbm_store_count_edges_by_type(s, project, "CONTAINS_FILE"); ASSERT_GTE(cf_count, 3); cbm_store_close(s); cbm_pipeline_free(p); teardown_test_repo(); PASS(); } TEST(pipeline_definitions_properties) { if (setup_test_repo() != 0) { FAIL("failed to create temp dir"); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/test_defs_props.db", g_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_tmpdir, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); int rc = cbm_pipeline_run(p); ASSERT_EQ(rc, 0); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); const char *project = cbm_pipeline_project_name(p); /* Verify a function has valid properties (complexity, lines, etc.) */ cbm_node_t *funcs = NULL; int func_count = 0; rc = cbm_store_find_nodes_by_label(s, project, "Function", &funcs, &func_count); ASSERT_EQ(rc, CBM_STORE_OK); ASSERT_GT(func_count, 0); /* Check that Serve (exported) has is_exported:true in properties */ for (int i = 0; i < func_count; i++) { if (strcmp(funcs[i].name, "Serve") == 0) { ASSERT_NOT_NULL(funcs[i].properties_json); ASSERT_TRUE(strstr(funcs[i].properties_json, "\"is_exported\":true") != NULL); } /* All functions should have file_path set */ ASSERT_NOT_NULL(funcs[i].file_path); } cbm_store_free_nodes(funcs, func_count); cbm_store_close(s); cbm_pipeline_free(p); teardown_test_repo(); PASS(); } /* Node properties must remain VALID JSON even when a definition's serialized * properties exceed the fixed 2 KB build buffer. Found on the Linux kernel: * 135 nodes (50-param functions with struct-typed signatures) had properties * truncated mid-string at 2047 bytes — malformed JSON that aborts EVERY * json_extract()-based consumer (arch_entry_points, partial indexes, user * Cypher on properties). Oversized optional fields must be dropped whole, * never cut mid-value. */ TEST(pipeline_def_props_valid_json_when_oversized) { if (setup_test_repo() != 0) { FAIL("failed to create temp dir"); } /* Sweep of C functions with growing signatures. The corruption fires only * when the serialized position lands in a narrow window just under the * buffer margin as the param_types array starts — the array is then cut * mid-item (`"param_types":["enum`), exactly the kernel failure shape. * Sweeping 10..69 params deterministically hits the window (pre-fix: * sweep_fn_30 -> 2047-byte malformed properties). ONE FUNCTION PER FILE so * the file count exceeds MIN_FILES_FOR_PARALLEL (50) and the test covers * the PARALLEL pipeline's duplicated props builder (pass_parallel.c) — the * path large repos take; the serial twin lives in pass_definitions.c. */ for (int n = 10; n < 70; n++) { char path[512]; snprintf(path, sizeof(path), "%s/sweep_%02d.c", g_tmpdir, n); FILE *f = fopen(path, "w"); if (!f) { teardown_test_repo(); FAIL("failed to write sweep file"); } fprintf(f, "int sweep_fn_%02d(", n); for (int i = 0; i < n; i++) { fprintf(f, "%sstruct long_struct_type_name_padding_padding_%02d *par_%02d", i ? ", " : "", i, i); } fprintf(f, ") { return 0; }\n"); fclose(f); } /* Multi-line parameter declarations: param_types items then carry raw * newline/tab bytes from the source slice. The array appender's inline * escape loop only handled quote/backslash, so the raw control bytes made * the JSON invalid (118 Linux-kernel rows of this shape). */ { char path[512]; snprintf(path, sizeof(path), "%s/sweep_nl.c", g_tmpdir); FILE *f = fopen(path, "w"); if (!f) { teardown_test_repo(); FAIL("failed to write sweep_nl.c"); } fprintf(f, "int sweep_fn_nl(struct\n\t\t\t\treally_long_struct_name *a,\n" " enum\n\tweird_enum b) { return 0; }\n"); fclose(f); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/test_huge_props.db", g_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_tmpdir, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); const char *project = cbm_pipeline_project_name(p); cbm_node_t *funcs = NULL; int func_count = 0; ASSERT_EQ(cbm_store_find_nodes_by_label(s, project, "Function", &funcs, &func_count), CBM_STORE_OK); int checked = 0; for (int i = 0; i < func_count; i++) { if (strncmp(funcs[i].name, "sweep_fn_", 9) != 0) { continue; } ASSERT_NOT_NULL(funcs[i].properties_json); yyjson_doc *doc = yyjson_read(funcs[i].properties_json, strlen(funcs[i].properties_json), 0); if (!doc) { printf(" INVALID properties JSON for %s (%zu bytes): ...%s\n", funcs[i].name, strlen(funcs[i].properties_json), funcs[i].properties_json + (strlen(funcs[i].properties_json) > 60 ? strlen(funcs[i].properties_json) - 60 : 0)); } ASSERT_NOT_NULL(doc); /* valid JSON for EVERY sweep size */ yyjson_doc_free(doc); checked++; } ASSERT_EQ(checked, 61); /* all sweep functions present (60 sizes + nl case) */ cbm_store_free_nodes(funcs, func_count); cbm_store_close(s); cbm_pipeline_free(p); teardown_test_repo(); PASS(); } /* Edge properties must be VALID JSON. Decorator source text (quotes, raw * newlines: @register.tag("block"), multi-line @override_settings) was * interpolated raw into the DECORATES properties — django produced 3826 * malformed edges, and any json_extract-based consumer (including the * url_path_gen generated-column evaluation during PRAGMA integrity_check) * aborts on them. Usage/call emit sites had the same hole for sliced source * text. */ TEST(pipeline_edge_props_valid_json) { if (setup_test_repo() != 0) { FAIL("failed to create temp dir"); } char path[512]; snprintf(path, sizeof(path), "%s/deco.py", g_tmpdir); FILE *f = fopen(path, "w"); if (!f) { teardown_test_repo(); FAIL("failed to write deco.py"); } fprintf(f, "from x import register\n" "@register.tag(\"block\")\n" "def do_block(parser):\n" " return parser\n" "@register.tag(\"extends\")\n" "def do_extends(parser):\n" " return parser\n"); fclose(f); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/test_edge_props.db", g_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_tmpdir, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); const char *project = cbm_pipeline_project_name(p); cbm_edge_t *edges = NULL; int edge_count = 0; ASSERT_EQ(cbm_store_find_edges_by_type(s, project, "DECORATES", &edges, &edge_count), CBM_STORE_OK); ASSERT_GT(edge_count, 0); /* the decorators must produce DECORATES edges */ for (int i = 0; i < edge_count; i++) { const char *pj = edges[i].properties_json; if (!pj) { continue; } yyjson_doc *doc = yyjson_read(pj, strlen(pj), 0); if (!doc) { printf(" INVALID edge properties JSON: %.80s\n", pj); } ASSERT_NOT_NULL(doc); yyjson_doc_free(doc); } cbm_store_free_edges(edges, edge_count); cbm_store_close(s); cbm_pipeline_free(p); teardown_test_repo(); PASS(); } /* ── Calls pass tests ──────────────────────────────────────────── */ TEST(pipeline_calls_resolution) { if (setup_test_repo() != 0) { FAIL("failed to create temp dir"); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/test_calls.db", g_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_tmpdir, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); int rc = cbm_pipeline_run(p); ASSERT_EQ(rc, 0); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); const char *project = cbm_pipeline_project_name(p); /* main() calls pkg.Serve(), Serve() calls util.Help() → at least 2 CALLS edges */ int calls_count = cbm_store_count_edges_by_type(s, project, "CALLS"); ASSERT_GTE(calls_count, 1); /* at least some calls resolved */ cbm_store_close(s); cbm_pipeline_free(p); teardown_test_repo(); PASS(); } /* True iff a CALLS edge exists from a node named src_name to a node named * tgt_name. Used to assert cross-file call resolution survives a reindex. */ static bool cross_file_call_exists(cbm_store_t *s, const char *project, const char *src_name, const char *tgt_name) { cbm_node_t *srcs = NULL; cbm_node_t *tgts = NULL; int sc = 0; int tc = 0; cbm_store_find_nodes_by_name(s, project, src_name, &srcs, &sc); cbm_store_find_nodes_by_name(s, project, tgt_name, &tgts, &tc); bool found = false; for (int i = 0; i < sc && !found; i++) { cbm_edge_t *edges = NULL; int ec = 0; cbm_store_find_edges_by_source_type(s, srcs[i].id, "CALLS", &edges, &ec); for (int j = 0; j < ec && !found; j++) { for (int k = 0; k < tc; k++) { if (edges[j].target_id == tgts[k].id) { found = true; break; } } } if (edges) { cbm_store_free_edges(edges, ec); } } if (srcs) { cbm_store_free_nodes(srcs, sc); } if (tgts) { cbm_store_free_nodes(tgts, tc); } return found; } /* Regression: incremental re-index of an edited file must NOT drop inbound * cross-file CALLS edges whose source lives in an UNCHANGED file. * * Repro: Serve() (pkg/service.go) CALLS Help() (pkg/util/helper.go). Editing * helper.go purges Help's node; before the fix the cascade deleted the * Serve->Help edge and never regenerated it (helper.go's callers are not * re-parsed), so the edge silently vanished on every edit and the incremental * graph diverged from a full reindex. */ TEST(pipeline_incremental_preserves_cross_file_calls) { if (setup_test_repo() != 0) { FAIL("failed to create temp dir"); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/test_incr_calls.db", g_tmpdir); /* 1. Full index. */ cbm_pipeline_t *p1 = cbm_pipeline_new(g_tmpdir, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p1); ASSERT_EQ(cbm_pipeline_run(p1), 0); const char *project = cbm_pipeline_project_name(p1); cbm_store_t *s1 = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s1); int calls_before = cbm_store_count_edges_by_type(s1, project, "CALLS"); ASSERT_GTE(calls_before, 2); /* main->Serve and Serve->Help at minimum */ ASSERT_TRUE(cross_file_call_exists(s1, project, "Serve", "Help")); cbm_store_close(s1); cbm_pipeline_free(p1); /* 2. Edit the callee's file so the incremental classifier marks it changed * (mtime+size differ). Help's symbol + qualified name are unchanged. */ char helper[512]; snprintf(helper, sizeof(helper), "%s/pkg/util/helper.go", g_tmpdir); ASSERT_EQ(th_append_file(helper, "\n// incremental regression marker\n"), 0); /* 3. Re-run on the SAME db_path → auto-routes to incremental re-index. */ cbm_pipeline_t *p2 = cbm_pipeline_new(g_tmpdir, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p2); ASSERT_EQ(cbm_pipeline_run(p2), 0); /* 4. The inbound cross-file CALLS edge must survive and the total CALLS * count must not regress. (Before the fix: Serve->Help is dropped.) * NOTE: query with p2's project name — p1 (and the `project` pointer it * owned) was freed above; p1 and p2 derive the same name from g_tmpdir. */ const char *project2 = cbm_pipeline_project_name(p2); cbm_store_t *s2 = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s2); int calls_after = cbm_store_count_edges_by_type(s2, project2, "CALLS"); ASSERT_EQ(calls_after, calls_before); ASSERT_TRUE(cross_file_call_exists(s2, project2, "Serve", "Help")); cbm_store_close(s2); cbm_pipeline_free(p2); teardown_test_repo(); PASS(); } /* TS/JS receiver-aware weak-strategy suppression (#592/#606 direction; Perl * precedent #477). A member call x.foo() whose receiver TYPE the TS-LSP cannot * resolve (a regex literal `re.test()`) must NOT be bound to a same-named * project method by a weak short-name strategy — that fabricates a CALLS edge. * Type-resolved receivers (`c.test()` on a typed SalesforceRestClient) and bare * local calls must still resolve. < 50 files → sequential path (pass_calls.c). * RED before the fix: checkFormat->test exists via unique_name/suffix_match. */ static void write_temp_file(const char *dir, const char *name, const char *content); TEST(pipeline_tsjs_receiver_suppresses_weak_method_edge) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_tsjs_recv_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } /* The lone project symbol named "test" — a real method. */ write_temp_file(tmp, "src/client.ts", "export class SalesforceRestClient {\n" " test(): boolean {\n" " return true;\n" " }\n" "}\n"); /* Regex receiver: `re.test(s)` calls RegExp.prototype.test, NOT the method. * The TS-LSP cannot bind it to a project symbol → the registry would guess * "test" by short name (weak). This is the false edge to suppress. */ write_temp_file(tmp, "src/caller.ts", "const re = /^[a-z]+$/;\n" "export function checkFormat(s: string): boolean {\n" " return re.test(s);\n" "}\n"); /* Typed receiver: `c` is annotated SalesforceRestClient, so the TS-LSP * resolves c.test() to the method (lsp_ts_method, conf 0.95) BEFORE the * registry runs — the guard's explicit drop-list keeps every lsp_* edge. */ write_temp_file(tmp, "src/typed.ts", "import { SalesforceRestClient } from './client';\n" "export function runTyped(c: SalesforceRestClient): boolean {\n" " return c.test();\n" "}\n"); /* Bare local call: same-module resolution, unaffected by the guard. */ write_temp_file(tmp, "src/local.ts", "function localHelper(): number {\n" " return 1;\n" "}\n" "export function callsLocal(): number {\n" " return localHelper();\n" "}\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/tsjs_recv.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); const char *project = cbm_pipeline_project_name(p); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); /* (1) The false edge is suppressed (reproduce-first: RED before the fix). */ ASSERT_FALSE(cross_file_call_exists(s, project, "checkFormat", "test")); /* (2) The type-resolved receiver call survives (LSP wins before the guard). */ ASSERT_TRUE(cross_file_call_exists(s, project, "runTyped", "test")); /* (3) The bare local call survives (same-module / lsp_ts_local). */ ASSERT_TRUE(cross_file_call_exists(s, project, "callsLocal", "localHelper")); /* (4) breadth insurance: the real edges are still emitted. */ ASSERT_GTE(cbm_store_count_edges_by_type(s, project, "CALLS"), 2); cbm_store_close(s); cbm_pipeline_free(p); th_rmtree(tmp); PASS(); } /* Count nodes with the given exact name in the project (e.g. a Route path). */ static int count_nodes_named(cbm_store_t *s, const char *project, const char *name) { cbm_node_t *ns = NULL; int n = 0; cbm_store_find_nodes_by_name(s, project, name, &ns, &n); if (ns) { cbm_store_free_nodes(ns, n); } return n; } /* Parallel-resolver regression for the TS/JS receiver guard (>= 50 files forces * pass_parallel.c's resolve_file_calls). The guard must not drop a weak member * match before the service classification runs — it suppresses ONLY the plain * CALLS fall-through, so every service edge (HTTP_CALLS via the #523 callee * bypass or emit_service_edge's unconditional detect_url_in_args, Route via the * ROUTE_REG fall-through, …) is emitted exactly as on main. These callees are * classified by main's verb-suffix + URL-arg heuristic, NOT by an HTTP library * name in the callee — a duplicated predicate keyed on the resolved QN lost them * (axios.get, api.patch on a renamed-axios instance, supertest request(app).get). * The regex false edge must stay suppressed in parallel too. CBM_WORKERS forces * >1 worker so the parallel path is taken regardless of the host core count. */ TEST(pipeline_tsjs_receiver_parallel_keeps_service_edges) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_tsjs_par_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } /* The lone project symbols named get()/patch()/test() — weak short-name * targets the registry mis-binds the member calls below to. */ write_temp_file(tmp, "src/thing.ts", "export class ApiThing {\n" " get(): number {\n" " return 1;\n" " }\n" " patch(): number {\n" " return 2;\n" " }\n" " load(): number {\n" " return 3;\n" " }\n" "}\n" "export class Other {\n" " test(): boolean {\n" " return true;\n" " }\n" "}\n"); /* Untyped axios: the HTTP signal is in the callee name, not the resolved QN * (the registry mis-binds "get" to ApiThing.get). Must yield HTTP_CALLS. */ write_temp_file(tmp, "src/http.ts", "export function callApi() {\n" " return axios.get('/api/orders');\n" "}\n"); /* Renamed axios instance `api`: no HTTP lib id in the callee — classified by * the .patch verb suffix + URL arg. Must yield HTTP_CALLS (was lost when the * exemption keyed on the resolved QN only). */ write_temp_file(tmp, "src/http2.ts", "export function callPatch(): unknown {\n" " return api.patch('/plans/:id', {});\n" "}\n"); /* Supertest-style chained receiver `request(app).get('/y')` — untyped, verb * suffix + URL arg. */ write_temp_file(tmp, "src/supertest.ts", "export function callSup(app: unknown): unknown {\n" " return request(app).get('/y');\n" "}\n"); /* `dev.load('/data')`: `.load` is NOT a route suffix and `dev` is not an HTTP * lib, so main classifies it via the unconditional detect_url_in_args (URL * arg) -> HTTP_CALLS, while the weak plain match to ApiThing.load is the false * edge that must be suppressed. This is exactly the detect_url_in_args path * the previous (predicate-duplicating) guard skipped by dropping the call * before emit_service_edge ran — the class of ~399 HTTP_CALLS it lost. */ write_temp_file(tmp, "src/load.ts", "export function callLoad(dev: unknown): unknown {\n" " return dev.load('/api/data');\n" "}\n"); /* Route registration by callee suffix + a '/'-path arg. Must yield a Route. */ write_temp_file(tmp, "src/routes.ts", "function handler() {}\n" "export function reg(router: any) {\n" " router.get('/users', handler);\n" "}\n"); /* Regex receiver: the false edge that must stay suppressed in parallel too. */ write_temp_file(tmp, "src/re.ts", "const re = /^[a-z]+$/;\n" "export function checkFormat(s: string): boolean {\n" " return re.test(s);\n" "}\n"); /* Pad past MIN_FILES_FOR_PARALLEL (50) so the parallel resolver runs. */ for (int i = 0; i < 52; i++) { char name[64]; char body[128]; snprintf(name, sizeof(name), "src/filler%d.ts", i); snprintf(body, sizeof(body), "export function filler%d(): number {\n return %d;\n}\n", i, i); write_temp_file(tmp, name, body); } char *old_workers = getenv("CBM_WORKERS"); char *saved = old_workers ? strdup(old_workers) : NULL; cbm_setenv("CBM_WORKERS", "4", 1); /* force parallel regardless of host cores */ char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/tsjs_par.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); const char *project = cbm_pipeline_project_name(p); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); /* (1) Genuine HTTP_CALLS survive under the guard (>= 3): * - axios.get('/api/orders') -> 2 edges (recognized lib #523 callee bypass * + detect_url_in_args), and * - dev.load('/api/data') -> 1 edge via detect_url_in_args, which runs * unconditionally after emit_service_edge's branch even when the plain * fall-through is suppressed. * dev.load is the class the predicate-duplicating guard lost: `.load` is not * a route suffix and `dev` is not an HTTP lib, so it was dropped before * emit_service_edge ran (RED on that guard: only axios's 2). */ ASSERT_GTE(cbm_store_count_edges_by_type(s, project, "HTTP_CALLS"), 3); /* (2) The verb-suffix + route-path member calls keep their route * registrations (edge type CALLS -> a Route node named by the path). These * classify as route_registration on main, NOT HTTP_CALLS — Option A preserves * that by construction. Assert the three Route paths survive: * api.patch('/plans/:id'), request(app).get('/y'), router.get('/users'). */ ASSERT_GTE(count_nodes_named(s, project, "/plans/:id"), 1); ASSERT_GTE(count_nodes_named(s, project, "/y"), 1); ASSERT_GTE(count_nodes_named(s, project, "/users"), 1); /* (3) The false plain-CALLS edges are suppressed in parallel: the regex * receiver and the dev.load weak match to ApiThing.load. */ ASSERT_FALSE(cross_file_call_exists(s, project, "checkFormat", "test")); ASSERT_FALSE(cross_file_call_exists(s, project, "callLoad", "load")); cbm_store_close(s); cbm_pipeline_free(p); if (saved) { cbm_setenv("CBM_WORKERS", saved, 1); free(saved); } else { cbm_unsetenv("CBM_WORKERS"); } th_rmtree(tmp); PASS(); } /* Native `fetch()` (#856), sequential path (< 50 files → pass_calls.c). A bare * unqualified call to the global fetch API has no import and no local * definition anywhere in this project, so registry resolution comes back * empty; classify it as HTTP_CALLS via the same #523-style empty-resolution * fallback used for axios/requests. A member call on an unrelated receiver * (`repo.fetch()`) must NOT be swept in — its callee_name is "repo.fetch", * not the bare "fetch" this check matches exactly. */ TEST(pipeline_native_fetch_classified_as_http_calls) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_fetch_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_temp_file(tmp, "src/api.ts", "export function loadData(): unknown {\n" " return fetch('/api/data');\n" "}\n"); /* `repo.fetch()` — a method call, not the global. Must not over-match. */ write_temp_file(tmp, "src/repo.ts", "export function useRepo(repo: { fetch: () => unknown }): unknown {\n" " return repo.fetch();\n" "}\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/fetch.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); const char *project = cbm_pipeline_project_name(p); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); ASSERT_GTE(cbm_store_count_edges_by_type(s, project, "HTTP_CALLS"), 1); /* Exactly the bare call, not the method call too. */ ASSERT_EQ(cbm_store_count_edges_by_type(s, project, "HTTP_CALLS"), 1); cbm_store_close(s); cbm_pipeline_free(p); th_rmtree(tmp); PASS(); } /* Native `fetch()` (#856), parallel path (>= 50 files -> pass_parallel.c's * resolve_file_calls). Mirrors pipeline_native_fetch_classified_as_http_calls * but forces the parallel resolver, since the empty-resolution fallback is a * separate implementation there (resolve_file_calls calls * emit_http_async_service_edge directly rather than through emit_service_edge, * which would otherwise re-derive CBM_SVC_NONE for "fetch" and silently fall * through to a plain CALLS edge). */ TEST(pipeline_native_fetch_parallel_classified_as_http_calls) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_fetch_par_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_temp_file(tmp, "src/api.ts", "export function loadData(): unknown {\n" " return fetch('/api/data');\n" "}\n"); for (int i = 0; i < 52; i++) { char name[64]; char body[128]; snprintf(name, sizeof(name), "src/filler%d.ts", i); snprintf(body, sizeof(body), "export function filler%d(): number {\n return %d;\n}\n", i, i); write_temp_file(tmp, name, body); } char *old_workers = getenv("CBM_WORKERS"); char *saved = old_workers ? strdup(old_workers) : NULL; cbm_setenv("CBM_WORKERS", "4", 1); /* force parallel regardless of host cores */ char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/fetch_par.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); const char *project = cbm_pipeline_project_name(p); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); ASSERT_GTE(cbm_store_count_edges_by_type(s, project, "HTTP_CALLS"), 1); cbm_store_close(s); cbm_pipeline_free(p); if (saved) { cbm_setenv("CBM_WORKERS", saved, 1); free(saved); } else { cbm_unsetenv("CBM_WORKERS"); } th_rmtree(tmp); PASS(); } /* Native `fetch()` (#856): a LOCAL `fetch` definition must win over the * global-API guess. Registry resolution finds this project's own top-level * `fetch` function, so the call is a plain CALLS edge to it — never * HTTP_CALLS. Isolated in its own project: the registry's project-wide * unique-name fallback means a local `fetch` anywhere in a project can * legitimately capture bare `fetch()` calls project-wide, so this must not * share a project with the genuine-native-fetch test above. */ TEST(pipeline_local_fetch_shadow_not_classified_as_http) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_fetch_shadow_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_temp_file(tmp, "src/local_fetch.ts", "function fetch(url: string): string {\n" " return 'mock:' + url;\n" "}\n" "export function useLocalFetch(): string {\n" " return fetch('/api/data');\n" "}\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/fetch_shadow.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); const char *project = cbm_pipeline_project_name(p); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); ASSERT_EQ(cbm_store_count_edges_by_type(s, project, "HTTP_CALLS"), 0); ASSERT_TRUE(cross_file_call_exists(s, project, "useLocalFetch", "fetch")); cbm_store_close(s); cbm_pipeline_free(p); th_rmtree(tmp); PASS(); } /* ── Git history pass tests ─────────────────────────────────────── */ TEST(githistory_is_trackable) { /* Source files → trackable */ ASSERT_TRUE(cbm_is_trackable_file("main.go")); ASSERT_TRUE(cbm_is_trackable_file("src/app.py")); ASSERT_TRUE(cbm_is_trackable_file("README.md")); /* Non-trackable: skip prefixes */ ASSERT_FALSE(cbm_is_trackable_file("node_modules/foo/bar.js")); ASSERT_FALSE(cbm_is_trackable_file("vendor/lib/dep.go")); ASSERT_FALSE(cbm_is_trackable_file(".git/config")); ASSERT_FALSE(cbm_is_trackable_file("__pycache__/mod.pyc")); /* Non-trackable: lock files */ ASSERT_FALSE(cbm_is_trackable_file("package-lock.json")); ASSERT_FALSE(cbm_is_trackable_file("go.sum")); /* Non-trackable: binary/minified extensions */ ASSERT_FALSE(cbm_is_trackable_file("image.png")); ASSERT_FALSE(cbm_is_trackable_file("src/style.min.css")); PASS(); } TEST(githistory_compute_coupling) { /* 5 commits with overlapping files */ char *files_0[] = {"a.go", "b.go", "c.go"}; char *files_1[] = {"a.go", "b.go"}; char *files_2[] = {"a.go", "b.go"}; char *files_3[] = {"a.go", "c.go"}; char *files_4[] = {"d.go", "e.go"}; cbm_commit_files_t commits[] = { {files_0, 3, 0}, {files_1, 2, 0}, {files_2, 2, 0}, {files_3, 2, 0}, {files_4, 2, 0}, }; cbm_change_coupling_t results[100]; int n = cbm_compute_change_coupling(commits, 5, results, 100); /* a.go + b.go co-change 3 times → should appear */ bool found_ab = false; for (int i = 0; i < n; i++) { if ((strcmp(results[i].file_a, "a.go") == 0 && strcmp(results[i].file_b, "b.go") == 0) || (strcmp(results[i].file_a, "b.go") == 0 && strcmp(results[i].file_b, "a.go") == 0)) { found_ab = true; ASSERT_EQ(results[i].co_change_count, 3); ASSERT_TRUE(results[i].coupling_score >= 0.7); } } ASSERT_TRUE(found_ab); /* d.go + e.go co-change only 1 time → below threshold, should NOT appear */ for (int i = 0; i < n; i++) { ASSERT_FALSE(strcmp(results[i].file_a, "d.go") == 0 || strcmp(results[i].file_b, "d.go") == 0); } PASS(); } TEST(githistory_coupling_carries_last_co_change) { /* Coupling output must surface the most recent commit timestamp at which * a pair co-changed, so callers can score recency in addition to * frequency. The pair (a.go, b.go) co-changes at three timestamps; the * resulting last_co_change must be the maximum (newest) of those. */ char *files_old[] = {"a.go", "b.go"}; char *files_mid[] = {"a.go", "b.go"}; char *files_new[] = {"a.go", "b.go"}; /* Unrelated co-change in the middle to make sure we don't accidentally * pick up that pair's timestamp by index. */ char *files_other[] = {"c.go", "d.go"}; cbm_commit_files_t commits[] = { {files_old, 2, 1700000000LL}, /* oldest a.go/b.go co-change */ {files_other, 2, 1750000000LL}, /* unrelated pair */ {files_mid, 2, 1720000000LL}, {files_new, 2, 1800000000LL}, /* newest a.go/b.go co-change */ }; cbm_change_coupling_t results[16]; int n = cbm_compute_change_coupling(commits, 4, results, 16); ASSERT_GTE(n, 1); bool found_ab = false; for (int i = 0; i < n; i++) { bool is_ab = (strcmp(results[i].file_a, "a.go") == 0 && strcmp(results[i].file_b, "b.go") == 0) || (strcmp(results[i].file_a, "b.go") == 0 && strcmp(results[i].file_b, "a.go") == 0); if (!is_ab) { continue; } ASSERT_EQ(results[i].co_change_count, 3); /* last_co_change must be the max of the three a.go/b.go timestamps. */ ASSERT_EQ(results[i].last_co_change, 1800000000LL); found_ab = true; } ASSERT_TRUE(found_ab); PASS(); } TEST(githistory_skip_large_commits) { /* A single commit with 25 files → should be skipped (>20) */ char *files[25]; char bufs[25][32]; for (int i = 0; i < 25; i++) { snprintf(bufs[i], sizeof(bufs[i]), "file%d.go", i); files[i] = bufs[i]; } cbm_commit_files_t commits[] = {{files, 25, 0}}; cbm_change_coupling_t results[100]; int n = cbm_compute_change_coupling(commits, 1, results, 100); ASSERT_EQ(n, 0); PASS(); } TEST(githistory_limits_to_max) { /* Create many commits to generate >100 couplings */ /* 50 files, each pair has 3 commits → C(50,2)=1225 pairs, but only * pairs with ≥3 co-changes and score≥0.3 pass the threshold */ int nfiles = 50; int npairs = nfiles * (nfiles - 1) / 2; int ncommits = npairs * 3; cbm_commit_files_t *commits = calloc(ncommits, sizeof(cbm_commit_files_t)); char **file_strs = calloc(nfiles, sizeof(char *)); for (int i = 0; i < nfiles; i++) { file_strs[i] = malloc(32); snprintf(file_strs[i], 32, "f%d.go", i); } int ci = 0; for (int i = 0; i < nfiles; i++) { for (int j = i + 1; j < nfiles; j++) { for (int k = 0; k < 3; k++) { commits[ci].files = malloc(2 * sizeof(char *)); commits[ci].files[0] = file_strs[i]; commits[ci].files[1] = file_strs[j]; commits[ci].count = 2; ci++; } } } /* max_out = 100 → should cap at 100 */ cbm_change_coupling_t results[100]; int n = cbm_compute_change_coupling(commits, ncommits, results, 100); ASSERT_TRUE(n <= 100); /* Cleanup */ for (int i = 0; i < ncommits; i++) { free(commits[i].files); } for (int i = 0; i < nfiles; i++) { free(file_strs[i]); } free(file_strs); free(commits); PASS(); } /* ── Test detection tests ──────────────────────────────────────── */ TEST(testdetect_is_test_file) { /* Test file patterns (all languages) */ ASSERT_TRUE(cbm_is_test_path("foo_test.go")); ASSERT_TRUE(cbm_is_test_path("test_handler.py")); ASSERT_TRUE(cbm_is_test_path("handler.test.js")); ASSERT_TRUE(cbm_is_test_path("handler.spec.ts")); ASSERT_TRUE(cbm_is_test_path("Component.test.tsx")); ASSERT_TRUE(cbm_is_test_path("OrderTest.java")); ASSERT_TRUE(cbm_is_test_path("handler_test.rs")); ASSERT_TRUE(cbm_is_test_path("handler_test.cpp")); ASSERT_TRUE(cbm_is_test_path("OrderTest.cs")); ASSERT_TRUE(cbm_is_test_path("OrderTest.php")); ASSERT_TRUE(cbm_is_test_path("OrderSpec.scala")); ASSERT_TRUE(cbm_is_test_path("OrderTest.kt")); ASSERT_TRUE(cbm_is_test_path("handler_test.lua")); /* Non-test file patterns */ ASSERT_FALSE(cbm_is_test_path("foo.go")); ASSERT_FALSE(cbm_is_test_path("handler.py")); ASSERT_FALSE(cbm_is_test_path("handler.js")); ASSERT_FALSE(cbm_is_test_path("handler.ts")); ASSERT_FALSE(cbm_is_test_path("Component.tsx")); ASSERT_FALSE(cbm_is_test_path("Order.java")); ASSERT_FALSE(cbm_is_test_path("handler.rs")); ASSERT_FALSE(cbm_is_test_path("handler.cpp")); ASSERT_FALSE(cbm_is_test_path("Order.cs")); ASSERT_FALSE(cbm_is_test_path("Order.php")); ASSERT_FALSE(cbm_is_test_path("Order.scala")); ASSERT_FALSE(cbm_is_test_path("Order.kt")); ASSERT_FALSE(cbm_is_test_path("handler.lua")); PASS(); } TEST(testdetect_is_test_function) { /* Test function patterns */ ASSERT_TRUE(cbm_is_test_func_name("TestCreate")); /* Go */ ASSERT_TRUE(cbm_is_test_func_name("test_create")); /* Python/Rust/Lua */ ASSERT_TRUE(cbm_is_test_func_name("test")); /* JS/TS */ ASSERT_TRUE(cbm_is_test_func_name("describe")); /* JS/TS */ ASSERT_TRUE(cbm_is_test_func_name("it")); /* JS/TS */ ASSERT_TRUE(cbm_is_test_func_name("testCreate")); /* Java/PHP/Scala/Kotlin */ ASSERT_TRUE(cbm_is_test_func_name("TestCreate")); /* C++/C# */ /* Non-test function patterns */ ASSERT_FALSE(cbm_is_test_func_name("create")); ASSERT_FALSE(cbm_is_test_func_name("handleRequest")); ASSERT_FALSE(cbm_is_test_func_name("process")); PASS(); } /* ── Implements pass tests (graph buffer based) ────────────────── */ TEST(implements_creates_override) { /* Port of TestPassImplementsCreatesOverrideEdges. * Set up Interface+methods, Class+methods, verify IMPLEMENTS+OVERRIDE. */ cbm_gbuf_t *gb = cbm_gbuf_new("test-proj", "/tmp/test"); ASSERT_NOT_NULL(gb); /* Interface "Reader" with methods "Read" and "Close" */ int64_t iface_id = cbm_gbuf_upsert_node(gb, "Interface", "Reader", "pkg.Reader", "pkg/reader.go", 1, 5, "{}"); ASSERT_GT(iface_id, 0); int64_t read_method_id = cbm_gbuf_upsert_node(gb, "Method", "Read", "pkg.Reader.Read", "pkg/reader.go", 2, 2, "{}"); int64_t close_method_id = cbm_gbuf_upsert_node(gb, "Method", "Close", "pkg.Reader.Close", "pkg/reader.go", 3, 3, "{}"); ASSERT_GT(read_method_id, 0); ASSERT_GT(close_method_id, 0); /* DEFINES_METHOD edges */ cbm_gbuf_insert_edge(gb, iface_id, read_method_id, "DEFINES_METHOD", "{}"); cbm_gbuf_insert_edge(gb, iface_id, close_method_id, "DEFINES_METHOD", "{}"); /* Class "FileReader" with matching methods */ int64_t class_id = cbm_gbuf_upsert_node(gb, "Class", "FileReader", "pkg.FileReader", "pkg/filereader.go", 1, 10, "{}"); ASSERT_GT(class_id, 0); int64_t fr_read_id = cbm_gbuf_upsert_node(gb, "Method", "Read", "pkg.FileReader.Read", "pkg/filereader.go", 2, 4, "{\"receiver\":\"(f *FileReader)\"}"); int64_t fr_close_id = cbm_gbuf_upsert_node(gb, "Method", "Close", "pkg.FileReader.Close", "pkg/filereader.go", 5, 7, "{\"receiver\":\"(f *FileReader)\"}"); ASSERT_GT(fr_read_id, 0); ASSERT_GT(fr_close_id, 0); /* Run Go-style implements detection */ atomic_int cancelled = 0; cbm_pipeline_ctx_t ctx = { .project_name = "test-proj", .repo_path = "/tmp/test", .gbuf = gb, .registry = NULL, .cancelled = &cancelled, }; int edges_created = cbm_pipeline_implements_go(&ctx); ASSERT_GT(edges_created, 0); /* Verify IMPLEMENTS edge: FileReader → Reader */ const cbm_gbuf_edge_t **impl_edges = NULL; int impl_count = 0; int rc = cbm_gbuf_find_edges_by_source_type(gb, class_id, "IMPLEMENTS", &impl_edges, &impl_count); ASSERT_EQ(rc, 0); ASSERT_EQ(impl_count, 1); ASSERT_EQ(impl_edges[0]->target_id, iface_id); /* Verify OVERRIDE edges: FileReader.Read → Reader.Read */ const cbm_gbuf_edge_t **read_overrides = NULL; int read_override_count = 0; rc = cbm_gbuf_find_edges_by_source_type(gb, fr_read_id, "OVERRIDE", &read_overrides, &read_override_count); ASSERT_EQ(rc, 0); ASSERT_EQ(read_override_count, 1); ASSERT_EQ(read_overrides[0]->target_id, read_method_id); /* Verify OVERRIDE edge: FileReader.Close → Reader.Close */ const cbm_gbuf_edge_t **close_overrides = NULL; int close_override_count = 0; rc = cbm_gbuf_find_edges_by_source_type(gb, fr_close_id, "OVERRIDE", &close_overrides, &close_override_count); ASSERT_EQ(rc, 0); ASSERT_EQ(close_override_count, 1); ASSERT_EQ(close_overrides[0]->target_id, close_method_id); cbm_gbuf_free(gb); PASS(); } TEST(implements_no_match) { /* Port of TestPassImplementsNoOverrideWithoutMatch. * Interface requires Read+Write, struct only has Read → no edges. */ cbm_gbuf_t *gb = cbm_gbuf_new("test-proj", "/tmp/test"); ASSERT_NOT_NULL(gb); /* Interface "ReadWriter" with methods "Read" and "Write" */ int64_t iface_id = cbm_gbuf_upsert_node(gb, "Interface", "ReadWriter", "pkg.ReadWriter", "pkg/rw.go", 1, 5, "{}"); int64_t read_id = cbm_gbuf_upsert_node(gb, "Method", "Read", "pkg.ReadWriter.Read", "pkg/rw.go", 2, 2, "{}"); int64_t write_id = cbm_gbuf_upsert_node(gb, "Method", "Write", "pkg.ReadWriter.Write", "pkg/rw.go", 3, 3, "{}"); cbm_gbuf_insert_edge(gb, iface_id, read_id, "DEFINES_METHOD", "{}"); cbm_gbuf_insert_edge(gb, iface_id, write_id, "DEFINES_METHOD", "{}"); /* Struct "OnlyReader" with only "Read" (missing "Write") */ int64_t class_id = cbm_gbuf_upsert_node(gb, "Class", "OnlyReader", "pkg.OnlyReader", "pkg/onlyreader.go", 1, 10, "{}"); int64_t or_read_id = cbm_gbuf_upsert_node(gb, "Method", "Read", "pkg.OnlyReader.Read", "pkg/onlyreader.go", 2, 4, "{\"receiver\":\"(o *OnlyReader)\"}"); /* Run Go-style implements detection */ atomic_int cancelled = 0; cbm_pipeline_ctx_t ctx = { .project_name = "test-proj", .repo_path = "/tmp/test", .gbuf = gb, .registry = NULL, .cancelled = &cancelled, }; int edges_created = cbm_pipeline_implements_go(&ctx); ASSERT_EQ(edges_created, 0); /* Verify NO IMPLEMENTS edge */ const cbm_gbuf_edge_t **impl_edges = NULL; int impl_count = 0; cbm_gbuf_find_edges_by_source_type(gb, class_id, "IMPLEMENTS", &impl_edges, &impl_count); ASSERT_EQ(impl_count, 0); /* Verify NO OVERRIDE edge */ const cbm_gbuf_edge_t **override_edges = NULL; int override_count = 0; cbm_gbuf_find_edges_by_source_type(gb, or_read_id, "OVERRIDE", &override_edges, &override_count); ASSERT_EQ(override_count, 0); /* Suppress unused warnings */ (void)iface_id; (void)read_id; (void)write_id; cbm_gbuf_free(gb); PASS(); } /* ── Usages pass tests (full pipeline integration) ──────────────── */ /* Helper to create a temp dir with a single source file */ static char g_usages_tmpdir[256]; static int setup_usages_repo(const char *filename, const char *content, const char *extra_file, const char *extra_content) { snprintf(g_usages_tmpdir, sizeof(g_usages_tmpdir), "/tmp/cbm_usage_XXXXXX"); if (!cbm_mkdtemp(g_usages_tmpdir)) return -1; /* Check if filename has subdirectory */ char path[512]; snprintf(path, sizeof(path), "%s/%s", g_usages_tmpdir, filename); /* Create subdirectories if needed */ char dir_path[512]; snprintf(dir_path, sizeof(dir_path), "%s", path); char *last_slash = strrchr(dir_path, '/'); if (last_slash) { *last_slash = '\0'; th_mkdir_p(dir_path); } FILE *f = fopen(path, "w"); if (!f) return -1; fprintf(f, "%s", content); fclose(f); if (extra_file && extra_content) { snprintf(path, sizeof(path), "%s/%s", g_usages_tmpdir, extra_file); f = fopen(path, "w"); if (!f) return -1; fprintf(f, "%s", extra_content); fclose(f); } return 0; } static void teardown_usages_repo(void) { if (g_usages_tmpdir[0]) rm_rf(g_usages_tmpdir); g_usages_tmpdir[0] = '\0'; } TEST(usages_creates_edges) { /* Port of TestPassUsagesCreatesEdges. * Go source with callback reference → USAGE edge. */ const char *go_source = "package mypkg\n\n" "func Process(data string) string {\n" "\treturn data\n" "}\n\n" "func Register() {\n" "\thandler := Process\n" "\t_ = handler\n" "}\n"; if (setup_usages_repo("mypkg/main.go", go_source, "go.mod", "module testmod\ngo 1.21\n") != 0) { FAIL("failed to create temp dir"); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/test_usages.db", g_usages_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_usages_tmpdir, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); int rc = cbm_pipeline_run(p); ASSERT_EQ(rc, 0); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); const char *project = cbm_pipeline_project_name(p); /* Check for USAGE edges */ cbm_edge_t *edges = NULL; int edge_count = 0; rc = cbm_store_find_edges_by_type(s, project, "USAGE", &edges, &edge_count); bool found_usage = false; for (int i = 0; i < edge_count; i++) { cbm_node_t src = {0}, tgt = {0}; if (cbm_store_find_node_by_id(s, edges[i].source_id, &src) == CBM_STORE_OK && cbm_store_find_node_by_id(s, edges[i].target_id, &tgt) == CBM_STORE_OK) { if (strcmp(src.name, "Register") == 0 && strcmp(tgt.name, "Process") == 0) { found_usage = true; } } cbm_node_free_fields(&src); cbm_node_free_fields(&tgt); } if (edges) cbm_store_free_edges(edges, edge_count); ASSERT_TRUE(found_usage); cbm_store_close(s); cbm_pipeline_free(p); teardown_usages_repo(); PASS(); } TEST(usages_no_duplicate_calls) { /* Port of TestPassUsagesDoesNotDuplicateCalls. * A direct function call should produce CALLS but not USAGE. */ const char *go_source = "package mypkg\n\n" "func Helper() string {\n" "\treturn \"ok\"\n" "}\n\n" "func Main() {\n" "\tHelper()\n" "}\n"; if (setup_usages_repo("mypkg/main.go", go_source, "go.mod", "module testmod\ngo 1.21\n") != 0) { FAIL("failed to create temp dir"); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/test_no_dup.db", g_usages_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_usages_tmpdir, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); int rc = cbm_pipeline_run(p); ASSERT_EQ(rc, 0); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); const char *project = cbm_pipeline_project_name(p); /* Should have CALLS edge from Main to Helper */ cbm_edge_t *call_edges = NULL; int call_count = 0; cbm_store_find_edges_by_type(s, project, "CALLS", &call_edges, &call_count); bool found_call = false; for (int i = 0; i < call_count; i++) { cbm_node_t src = {0}, tgt = {0}; if (cbm_store_find_node_by_id(s, call_edges[i].source_id, &src) == CBM_STORE_OK && cbm_store_find_node_by_id(s, call_edges[i].target_id, &tgt) == CBM_STORE_OK) { if (strcmp(src.name, "Main") == 0 && strcmp(tgt.name, "Helper") == 0) { found_call = true; } } cbm_node_free_fields(&src); cbm_node_free_fields(&tgt); } if (call_edges) cbm_store_free_edges(call_edges, call_count); ASSERT_TRUE(found_call); /* Should NOT have USAGE edge from Main to Helper */ cbm_edge_t *usage_edges = NULL; int usage_count = 0; cbm_store_find_edges_by_type(s, project, "USAGE", &usage_edges, &usage_count); for (int i = 0; i < usage_count; i++) { cbm_node_t src = {0}, tgt = {0}; if (cbm_store_find_node_by_id(s, usage_edges[i].source_id, &src) == CBM_STORE_OK && cbm_store_find_node_by_id(s, usage_edges[i].target_id, &tgt) == CBM_STORE_OK) { ASSERT_FALSE(strcmp(src.name, "Main") == 0 && strcmp(tgt.name, "Helper") == 0); } cbm_node_free_fields(&src); cbm_node_free_fields(&tgt); } if (usage_edges) cbm_store_free_edges(usage_edges, usage_count); cbm_store_close(s); cbm_pipeline_free(p); teardown_usages_repo(); PASS(); } TEST(calls_edge_carries_call_site_line) { /* Regression guard for #503: a CALLS edge must persist the source line of * the call site in its JSON props as "line":N. Helper() is invoked on * line 8 (1-based) of the source below. */ const char *go_source = "package mypkg\n" "\n" "func Helper() string {\n" "\treturn \"ok\"\n" "}\n" "\n" "func Main() {\n" "\tHelper()\n" "}\n"; if (setup_usages_repo("mypkg/main.go", go_source, "go.mod", "module testmod\ngo 1.21\n") != 0) { FAIL("failed to create temp dir"); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/test_call_line.db", g_usages_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_usages_tmpdir, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); const char *project = cbm_pipeline_project_name(p); cbm_edge_t *call_edges = NULL; int call_count = 0; cbm_store_find_edges_by_type(s, project, "CALLS", &call_edges, &call_count); bool found_line = false; for (int i = 0; i < call_count; i++) { cbm_node_t src = {0}, tgt = {0}; if (cbm_store_find_node_by_id(s, call_edges[i].source_id, &src) == CBM_STORE_OK && cbm_store_find_node_by_id(s, call_edges[i].target_id, &tgt) == CBM_STORE_OK) { if (strcmp(src.name, "Main") == 0 && strcmp(tgt.name, "Helper") == 0) { ASSERT_NOT_NULL(call_edges[i].properties_json); ASSERT_TRUE(strstr(call_edges[i].properties_json, "\"line\":8}") != NULL); found_line = true; } } cbm_node_free_fields(&src); cbm_node_free_fields(&tgt); } if (call_edges) cbm_store_free_edges(call_edges, call_count); ASSERT_TRUE(found_line); cbm_store_close(s); cbm_pipeline_free(p); teardown_usages_repo(); PASS(); } TEST(usages_kotlin_creates_edges) { /* Port of TestPassUsagesKotlinCreatesEdges. * Kotlin source with callback reference → USAGE edge. */ const char *kt_source = "fun process(data: String): String {\n" " return data\n" "}\n\n" "fun register() {\n" " val handler = ::process\n" "}\n"; if (setup_usages_repo("Main.kt", kt_source, NULL, NULL) != 0) { FAIL("failed to create temp dir"); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/test_kt_usage.db", g_usages_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_usages_tmpdir, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); int rc = cbm_pipeline_run(p); ASSERT_EQ(rc, 0); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); const char *project = cbm_pipeline_project_name(p); /* Check USAGE edges exist (Kotlin extraction may or may not produce them * depending on extraction support — just verify no crash and valid counts) */ cbm_edge_t *edges = NULL; int edge_count = 0; rc = cbm_store_find_edges_by_type(s, project, "USAGE", &edges, &edge_count); ASSERT_EQ(rc, CBM_STORE_OK); /* Note: The Go test just logs, doesn't assert — same behavior here */ if (edges) cbm_store_free_edges(edges, edge_count); cbm_store_close(s); cbm_pipeline_free(p); teardown_usages_repo(); PASS(); } TEST(usages_kotlin_no_duplicate_calls) { /* Port of TestPassUsagesKotlinDoesNotDuplicateCalls. * Direct call in Kotlin → CALLS but not USAGE. */ const char *kt_source = "fun helper(): String {\n" " return \"ok\"\n" "}\n\n" "fun main() {\n" " helper()\n" "}\n"; if (setup_usages_repo("Main.kt", kt_source, NULL, NULL) != 0) { FAIL("failed to create temp dir"); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/test_kt_nodup.db", g_usages_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_usages_tmpdir, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); int rc = cbm_pipeline_run(p); ASSERT_EQ(rc, 0); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); const char *project = cbm_pipeline_project_name(p); /* Should have CALLS edge from main to helper */ cbm_edge_t *call_edges = NULL; int call_count = 0; cbm_store_find_edges_by_type(s, project, "CALLS", &call_edges, &call_count); bool found_call = false; for (int i = 0; i < call_count; i++) { cbm_node_t src = {0}, tgt = {0}; if (cbm_store_find_node_by_id(s, call_edges[i].source_id, &src) == CBM_STORE_OK && cbm_store_find_node_by_id(s, call_edges[i].target_id, &tgt) == CBM_STORE_OK) { if (strcmp(src.name, "main") == 0 && strcmp(tgt.name, "helper") == 0) { found_call = true; } } cbm_node_free_fields(&src); cbm_node_free_fields(&tgt); } if (call_edges) cbm_store_free_edges(call_edges, call_count); ASSERT_TRUE(found_call); /* Should NOT have USAGE edge from main to helper */ cbm_edge_t *usage_edges = NULL; int usage_count = 0; cbm_store_find_edges_by_type(s, project, "USAGE", &usage_edges, &usage_count); for (int i = 0; i < usage_count; i++) { cbm_node_t src = {0}, tgt = {0}; if (cbm_store_find_node_by_id(s, usage_edges[i].source_id, &src) == CBM_STORE_OK && cbm_store_find_node_by_id(s, usage_edges[i].target_id, &tgt) == CBM_STORE_OK) { ASSERT_FALSE(strcmp(src.name, "main") == 0 && strcmp(tgt.name, "helper") == 0); } cbm_node_free_fields(&src); cbm_node_free_fields(&tgt); } if (usage_edges) cbm_store_free_edges(usage_edges, usage_count); cbm_store_close(s); cbm_pipeline_free(p); teardown_usages_repo(); PASS(); } /* ── Pipeline language integration tests ────────────────────────── */ /* General helper: create temp dir and write multiple files */ static char g_lang_tmpdir[256]; static int setup_lang_repo(const char **filenames, const char **contents, int count) { snprintf(g_lang_tmpdir, sizeof(g_lang_tmpdir), "/tmp/cbm_lang_XXXXXX"); if (!cbm_mkdtemp(g_lang_tmpdir)) return -1; for (int i = 0; i < count; i++) { char path[512]; snprintf(path, sizeof(path), "%s/%s", g_lang_tmpdir, filenames[i]); /* Create parent directories */ char dir[512]; snprintf(dir, sizeof(dir), "%s", path); char *slash = strrchr(dir, '/'); if (slash) { *slash = '\0'; th_mkdir_p(dir); } FILE *f = fopen(path, "wb"); if (!f) return -1; fprintf(f, "%s", contents[i]); fclose(f); } return 0; } static void teardown_lang_repo(void) { if (g_lang_tmpdir[0]) rm_rf(g_lang_tmpdir); g_lang_tmpdir[0] = '\0'; } TEST(pipeline_python_project) { /* Port of TestPipelinePythonProject */ const char *files[] = {"main.py", "utils.py"}; const char *contents[] = { "def greet(name):\n return f\"Hello, {name}\"\n\n" "def process():\n result = greet(\"world\")\n return result\n", "API_URL = \"https://example.com/api\"\nMAX_RETRIES = 3\n\n" "def fetch_data(url):\n pass\n\n" "class DataProcessor:\n def transform(self, data):\n return data\n"}; if (setup_lang_repo(files, contents, 2) != 0) FAIL("tmpdir"); char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_lang_tmpdir, db, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_store_t *s = cbm_store_open_path(db); ASSERT_NOT_NULL(s); const char *proj = cbm_pipeline_project_name(p); cbm_node_t *funcs = NULL; int fc = 0; cbm_store_find_nodes_by_label(s, proj, "Function", &funcs, &fc); ASSERT_GTE(fc, 3); /* greet, process, fetch_data */ cbm_store_free_nodes(funcs, fc); cbm_node_t *cls = NULL; int cc = 0; cbm_store_find_nodes_by_label(s, proj, "Class", &cls, &cc); ASSERT_GTE(cc, 1); /* DataProcessor */ cbm_store_free_nodes(cls, cc); cbm_node_t *methods = NULL; int mc = 0; cbm_store_find_nodes_by_label(s, proj, "Method", &methods, &mc); ASSERT_GTE(mc, 1); /* transform */ cbm_store_free_nodes(methods, mc); cbm_store_close(s); cbm_pipeline_free(p); teardown_lang_repo(); PASS(); } /* #768 end-to-end: `import { A, B } from './lib'` must survive the REAL * pipeline (extract -> gbuf dedup -> raw SQLite dump) as TWO IMPORTS edges * with distinct local_name — and the dumped DB must satisfy its own schema. * With only the graph-buffer half of the fix, both edges reach the dump but * violate an unwidened UNIQUE(source_id,target_id,type), which PRAGMA * integrity_check flags as a non-unique autoindex entry. */ TEST(pipeline_imports_multi_symbol_edges) { const char *files[] = {"consumer.ts", "lib.ts"}; const char *contents[] = { "import { A, B } from './lib';\n\nexport function useBoth() {\n return A() + B();\n}\n", "export function A() {\n return 1;\n}\nexport function B() {\n return 2;\n}\n"}; if (setup_lang_repo(files, contents, 2) != 0) FAIL("tmpdir"); char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_lang_tmpdir, db, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); /* The dumped DB must pass SQLite's own full integrity check — this is * what catches a buffer-only fix shipping DBs that violate their own * UNIQUE constraint. */ sqlite3 *raw = NULL; ASSERT_EQ(sqlite3_open(db, &raw), SQLITE_OK); sqlite3_stmt *stmt = NULL; sqlite3_prepare_v2(raw, "PRAGMA integrity_check", -1, &stmt, NULL); ASSERT_EQ(sqlite3_step(stmt), SQLITE_ROW); const char *integrity = (const char *)sqlite3_column_text(stmt, 0); ASSERT_STR_EQ(integrity, "ok"); sqlite3_finalize(stmt); sqlite3_close(raw); /* Both named imports must be queryable as separate IMPORTS edges. */ cbm_store_t *s = cbm_store_open_path(db); ASSERT_NOT_NULL(s); const char *proj = cbm_pipeline_project_name(p); cbm_edge_t *edges = NULL; int count = 0; ASSERT_EQ(cbm_store_find_edges_by_type(s, proj, "IMPORTS", &edges, &count), CBM_STORE_OK); ASSERT_EQ(count, 2); ASSERT_TRUE(strstr(edges[0].properties_json, "\"local_name\":\"A\"") != NULL || strstr(edges[1].properties_json, "\"local_name\":\"A\"") != NULL); ASSERT_TRUE(strstr(edges[0].properties_json, "\"local_name\":\"B\"") != NULL || strstr(edges[1].properties_json, "\"local_name\":\"B\"") != NULL); cbm_store_free_edges(edges, count); cbm_store_close(s); cbm_pipeline_free(p); teardown_lang_repo(); PASS(); } TEST(pipeline_go_cross_package_call) { /* Port of TestGoCrossPackageCallViaImport */ const char *files[] = {"main.go", "svc/handler.go"}; const char *contents[] = { "package main\n\nimport \"example.com/myapp/svc\"\n\n" "func run() {\n\tsvc.ProcessOrder(\"123\")\n}\n", "package svc\n\nfunc ProcessOrder(id string) error {\n\treturn nil\n}\n"}; if (setup_lang_repo(files, contents, 2) != 0) FAIL("tmpdir"); char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_lang_tmpdir, db, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_store_t *s = cbm_store_open_path(db); ASSERT_NOT_NULL(s); const char *proj = cbm_pipeline_project_name(p); /* Verify ProcessOrder exists */ cbm_node_t *targets = NULL; int tc = 0; cbm_store_find_nodes_by_name(s, proj, "ProcessOrder", &targets, &tc); ASSERT_GT(tc, 0); /* Verify run() exists */ cbm_node_t *callers = NULL; int clc = 0; cbm_store_find_nodes_by_name(s, proj, "run", &callers, &clc); ASSERT_GT(clc, 0); /* Check CALLS edge from run to ProcessOrder */ cbm_edge_t *edges = NULL; int ec = 0; cbm_store_find_edges_by_source_type(s, callers[0].id, "CALLS", &edges, &ec); bool found = false; for (int i = 0; i < ec; i++) { if (edges[i].target_id == targets[0].id) found = true; } ASSERT_TRUE(found); if (edges) cbm_store_free_edges(edges, ec); cbm_store_free_nodes(targets, tc); cbm_store_free_nodes(callers, clc); cbm_store_close(s); cbm_pipeline_free(p); teardown_lang_repo(); PASS(); } TEST(pipeline_python_cross_module_call) { /* Port of TestPythonCrossModuleCallViaImport */ const char *files[] = {"utils.py", "main.py"}; const char *contents[] = { "def fetch_data(url):\n return {\"status\": \"ok\"}\n", "from utils import fetch_data\n\n" "def process():\n result = fetch_data(\"https://example.com\")\n return result\n"}; if (setup_lang_repo(files, contents, 2) != 0) FAIL("tmpdir"); char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_lang_tmpdir, db, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_store_t *s = cbm_store_open_path(db); ASSERT_NOT_NULL(s); const char *proj = cbm_pipeline_project_name(p); cbm_node_t *targets = NULL; int tc = 0; cbm_store_find_nodes_by_name(s, proj, "fetch_data", &targets, &tc); ASSERT_GT(tc, 0); cbm_node_t *callers = NULL; int clc = 0; cbm_store_find_nodes_by_name(s, proj, "process", &callers, &clc); ASSERT_GT(clc, 0); cbm_edge_t *edges = NULL; int ec = 0; cbm_store_find_edges_by_source_type(s, callers[0].id, "CALLS", &edges, &ec); bool found = false; for (int i = 0; i < ec; i++) { if (edges[i].target_id == targets[0].id) found = true; } ASSERT_TRUE(found); if (edges) cbm_store_free_edges(edges, ec); cbm_store_free_nodes(targets, tc); cbm_store_free_nodes(callers, clc); cbm_store_close(s); cbm_pipeline_free(p); teardown_lang_repo(); PASS(); } TEST(pipeline_go_type_classification) { /* Port of TestGoTypeClassification */ const char *files[] = {"types.go"}; const char *contents[] = {"package types\n\n" "type Reader interface {\n\tRead(p []byte) (n int, err error)\n}\n\n" "type Writer interface {\n\tWrite(p []byte) (n int, err error)\n}\n\n" "type Config struct {\n\tHost string\n\tPort int\n}\n\n" "type ID = string\n"}; if (setup_lang_repo(files, contents, 1) != 0) FAIL("tmpdir"); char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_lang_tmpdir, db, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_store_t *s = cbm_store_open_path(db); ASSERT_NOT_NULL(s); const char *proj = cbm_pipeline_project_name(p); /* Should have 2 Interface nodes (Reader, Writer) */ cbm_node_t *ifaces = NULL; int ic = 0; cbm_store_find_nodes_by_label(s, proj, "Interface", &ifaces, &ic); ASSERT_EQ(ic, 2); cbm_store_free_nodes(ifaces, ic); /* Should have 1 Struct node (Config struct) */ cbm_node_t *cls = NULL; int cc = 0; cbm_store_find_nodes_by_label(s, proj, "Struct", &cls, &cc); ASSERT_EQ(cc, 1); ASSERT_STR_EQ(cls[0].name, "Config"); cbm_store_free_nodes(cls, cc); /* Should have 1 Type node (ID alias) */ cbm_node_t *types = NULL; int tc = 0; cbm_store_find_nodes_by_label(s, proj, "Type", &types, &tc); ASSERT_EQ(tc, 1); ASSERT_STR_EQ(types[0].name, "ID"); cbm_store_free_nodes(types, tc); cbm_store_close(s); cbm_pipeline_free(p); teardown_lang_repo(); PASS(); } TEST(pipeline_go_grouped_types) { /* Port of TestGoGroupedTypeDeclaration */ const char *files[] = {"models.go"}; const char *contents[] = {"package models\n\n" "type (\n" "\tRequest struct {\n\t\tURL string\n\t}\n\n" "\tResponse struct {\n\t\tStatus int\n\t}\n\n" "\tHandler interface {\n\t\tHandle(req Request) Response\n\t}\n" ")\n"}; if (setup_lang_repo(files, contents, 1) != 0) FAIL("tmpdir"); char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_lang_tmpdir, db, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_store_t *s = cbm_store_open_path(db); ASSERT_NOT_NULL(s); const char *proj = cbm_pipeline_project_name(p); cbm_node_t *cls = NULL; int cc = 0; cbm_store_find_nodes_by_label(s, proj, "Struct", &cls, &cc); ASSERT_EQ(cc, 2); /* Request, Response */ cbm_store_free_nodes(cls, cc); cbm_node_t *ifaces = NULL; int ic = 0; cbm_store_find_nodes_by_label(s, proj, "Interface", &ifaces, &ic); ASSERT_EQ(ic, 1); /* Handler */ cbm_store_free_nodes(ifaces, ic); cbm_store_close(s); cbm_pipeline_free(p); teardown_lang_repo(); PASS(); } TEST(pipeline_kotlin_project) { /* Port of TestPipelineKotlinProject */ const char *files[] = {"Main.kt", "Service.kt"}; const char *contents[] = { "fun greet(name: String): String {\n return \"Hello, $name\"\n}\n\n" "fun main() {\n val result = greet(\"world\")\n println(result)\n}\n", "class OrderService {\n" " fun processOrder(id: String): Boolean {\n return true\n }\n\n" " fun submitOrder(order: String): Boolean {\n" " return processOrder(order)\n }\n}\n\n" "object Config {\n val API_URL = \"https://example.com/api\"\n}\n"}; if (setup_lang_repo(files, contents, 2) != 0) FAIL("tmpdir"); char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_lang_tmpdir, db, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_store_t *s = cbm_store_open_path(db); ASSERT_NOT_NULL(s); const char *proj = cbm_pipeline_project_name(p); cbm_node_t *funcs = NULL; int fc = 0; cbm_store_find_nodes_by_label(s, proj, "Function", &funcs, &fc); ASSERT_GTE(fc, 2); /* greet, main */ cbm_store_free_nodes(funcs, fc); cbm_node_t *cls = NULL; int cc = 0; cbm_store_find_nodes_by_label(s, proj, "Class", &cls, &cc); ASSERT_GTE(cc, 1); /* OrderService */ cbm_store_free_nodes(cls, cc); cbm_node_t *methods = NULL; int mc = 0; cbm_store_find_nodes_by_label(s, proj, "Method", &methods, &mc); ASSERT_GTE(mc, 2); /* processOrder, submitOrder */ cbm_store_free_nodes(methods, mc); int edge_count = cbm_store_count_edges(s, proj); ASSERT_GT(edge_count, 0); cbm_store_close(s); cbm_pipeline_free(p); teardown_lang_repo(); PASS(); } TEST(pipeline_lua_anonymous_functions) { /* Port of TestLuaAnonymousFunctionExtraction */ const char *files[] = {"app.lua"}; const char *contents[] = {"local run_before_filter\n" "run_before_filter = function(filter, r)\n" " return filter(r)\n" "end\n\n" "local validate = function(data)\n" " return data ~= nil\n" "end\n\n" "function named_func(x)\n" " return x + 1\n" "end\n"}; if (setup_lang_repo(files, contents, 1) != 0) FAIL("tmpdir"); char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_lang_tmpdir, db, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_store_t *s = cbm_store_open_path(db); ASSERT_NOT_NULL(s); const char *proj = cbm_pipeline_project_name(p); cbm_node_t *funcs = NULL; int fc = 0; cbm_store_find_nodes_by_label(s, proj, "Function", &funcs, &fc); ASSERT_GTE(fc, 3); /* run_before_filter, validate, named_func */ /* Verify specific functions exist */ const char *expected[] = {"run_before_filter", "validate", "named_func"}; for (int e = 0; e < 3; e++) { cbm_node_t *found = NULL; int fnc = 0; cbm_store_find_nodes_by_name(s, proj, expected[e], &found, &fnc); ASSERT_GT(fnc, 0); cbm_store_free_nodes(found, fnc); } cbm_store_free_nodes(funcs, fc); cbm_store_close(s); cbm_pipeline_free(p); teardown_lang_repo(); PASS(); } TEST(pipeline_csharp_modern) { /* Port of TestCSharpModernFeatures */ const char *files[] = {"Controller.cs", "Model.cs"}; const char *contents[] = {"namespace Conduit.Features;\n\n" "public class UsersController {\n" "\tpublic void Get() {}\n" "\tpublic void Create(string name) {}\n" "}\n", "namespace Conduit.Models {\n" "\tclass User {\n" "\t\tpublic string Name { get; set; }\n" "\t\tpublic int GetAge() { return 0; }\n" "\t}\n" "}\n"}; if (setup_lang_repo(files, contents, 2) != 0) FAIL("tmpdir"); char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_lang_tmpdir, db, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_store_t *s = cbm_store_open_path(db); ASSERT_NOT_NULL(s); const char *proj = cbm_pipeline_project_name(p); cbm_node_t *modules = NULL; int modc = 0; cbm_store_find_nodes_by_label(s, proj, "Module", &modules, &modc); ASSERT_GTE(modc, 2); cbm_store_free_nodes(modules, modc); cbm_node_t *cls = NULL; int cc = 0; cbm_store_find_nodes_by_label(s, proj, "Class", &cls, &cc); ASSERT_GTE(cc, 2); /* UsersController, User */ cbm_store_free_nodes(cls, cc); cbm_node_t *methods = NULL; int mc = 0; cbm_store_find_nodes_by_label(s, proj, "Method", &methods, &mc); ASSERT_GTE(mc, 3); /* Get, Create, GetAge */ cbm_store_free_nodes(methods, mc); cbm_store_close(s); cbm_pipeline_free(p); teardown_lang_repo(); PASS(); } TEST(pipeline_bom_stripping) { /* Port of TestBOMStripping — UTF-8 BOM prefix should be handled */ snprintf(g_lang_tmpdir, sizeof(g_lang_tmpdir), "/tmp/cbm_bom_XXXXXX"); if (!cbm_mkdtemp(g_lang_tmpdir)) FAIL("tmpdir"); char path[512]; snprintf(path, sizeof(path), "%s/bom.go", g_lang_tmpdir); FILE *f = fopen(path, "wb"); ASSERT_NOT_NULL(f); /* Write UTF-8 BOM (0xEF 0xBB 0xBF) followed by Go source */ unsigned char bom[] = {0xEF, 0xBB, 0xBF}; fwrite(bom, 1, 3, f); fprintf(f, "package main\n\nfunc BOMFunc() {}\n"); fclose(f); char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_lang_tmpdir, db, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_store_t *s = cbm_store_open_path(db); ASSERT_NOT_NULL(s); const char *proj = cbm_pipeline_project_name(p); cbm_node_t *found = NULL; int fc = 0; cbm_store_find_nodes_by_name(s, proj, "BOMFunc", &found, &fc); ASSERT_GT(fc, 0); /* BOMFunc should be found despite BOM */ cbm_store_free_nodes(found, fc); cbm_store_close(s); cbm_pipeline_free(p); teardown_lang_repo(); PASS(); } TEST(pipeline_form_call_resolution) { /* Port of TestFORMProcedureCallResolution */ const char *files[] = {"calc.frm"}; const char *contents[] = {"#procedure callee(x)\n" " id x = 0;\n" "#endprocedure\n" "#procedure caller()\n" " #call callee(1)\n" "#endprocedure\n"}; if (setup_lang_repo(files, contents, 1) != 0) FAIL("tmpdir"); char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_lang_tmpdir, db, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_store_t *s = cbm_store_open_path(db); ASSERT_NOT_NULL(s); const char *proj = cbm_pipeline_project_name(p); /* Verify CALLS edge exists to callee */ cbm_edge_t *edges = NULL; int ec = 0; cbm_store_find_edges_by_type(s, proj, "CALLS", &edges, &ec); bool found = false; for (int i = 0; i < ec; i++) { cbm_node_t tgt = {0}; if (cbm_store_find_node_by_id(s, edges[i].target_id, &tgt) == CBM_STORE_OK && strcmp(tgt.name, "callee") == 0) { found = true; } cbm_node_free_fields(&tgt); } ASSERT_TRUE(found); if (edges) cbm_store_free_edges(edges, ec); cbm_store_close(s); cbm_pipeline_free(p); teardown_lang_repo(); PASS(); } TEST(pipeline_python_type_inference) { /* Port of TestPythonMethodDispatchViaTypeInference * p = DataProcessor() then p.transform() → CALLS DataProcessor.transform */ const char *files[] = {"processor.py", "main.py"}; const char *contents[] = {"class DataProcessor:\n" " def transform(self, data):\n" " return data.upper()\n" "\n" " def validate(self, data):\n" " return len(data) > 0\n", "from processor import DataProcessor\n" "\n" "def run():\n" " p = DataProcessor()\n" " result = p.transform(\"hello\")\n" " return result\n"}; if (setup_lang_repo(files, contents, 2) != 0) FAIL("tmpdir"); char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_lang_tmpdir, db, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_store_t *s = cbm_store_open_path(db); ASSERT_NOT_NULL(s); const char *proj = cbm_pipeline_project_name(p); /* Verify DataProcessor.transform exists as a Method */ cbm_node_t *methods = NULL; int mc = 0; cbm_store_find_nodes_by_name(s, proj, "transform", &methods, &mc); ASSERT_GT(mc, 0); /* Verify run() exists */ cbm_node_t *callers = NULL; int clc = 0; cbm_store_find_nodes_by_name(s, proj, "run", &callers, &clc); ASSERT_GT(clc, 0); /* Check CALLS edge from run() to DataProcessor.transform */ cbm_edge_t *edges = NULL; int ec = 0; cbm_store_find_edges_by_source_type(s, callers[0].id, "CALLS", &edges, &ec); bool found = false; for (int i = 0; i < ec; i++) { if (edges[i].target_id == methods[0].id) found = true; } /* Type inference may or may not resolve — log but don't fail hard */ if (!found) { /* At minimum, verify the method exists and run() has some calls */ } if (edges) cbm_store_free_edges(edges, ec); cbm_store_free_nodes(methods, mc); cbm_store_free_nodes(callers, clc); cbm_store_close(s); cbm_pipeline_free(p); teardown_lang_repo(); PASS(); } /* ═══════════════════════════════════════════════════════════════════ * Docstring integration (port of TestDocstringIntegration) * ═══════════════════════════════════════════════════════════════════ */ TEST(pipeline_docstring_go_function) { /* Go function with // comment docstring */ const char *files[] = {"main.go"}; const char *contents[] = {"package main\n\n" "// Compute does something.\n" "func Compute() {}\n"}; if (setup_lang_repo(files, contents, 1) != 0) FAIL("tmpdir"); char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_lang_tmpdir, db, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_store_t *s = cbm_store_open_path(db); ASSERT_NOT_NULL(s); const char *proj = cbm_pipeline_project_name(p); cbm_node_t *nodes = NULL; int nc = 0; cbm_store_find_nodes_by_name(s, proj, "Compute", &nodes, &nc); ASSERT_GT(nc, 0); /* Check properties_json contains docstring */ bool found_docstring = false; for (int i = 0; i < nc; i++) { if (strcmp(nodes[i].label, "Function") == 0 && nodes[i].properties_json && strstr(nodes[i].properties_json, "docstring") && strstr(nodes[i].properties_json, "Compute does something")) { found_docstring = true; } } ASSERT_TRUE(found_docstring); cbm_store_free_nodes(nodes, nc); cbm_store_close(s); cbm_pipeline_free(p); teardown_lang_repo(); PASS(); } TEST(pipeline_docstring_python_function) { /* Python function with triple-quoted docstring */ const char *files[] = {"main.py"}; const char *contents[] = {"def compute():\n" "\t\"\"\"Does something.\"\"\"\n" "\tpass\n"}; if (setup_lang_repo(files, contents, 1) != 0) FAIL("tmpdir"); char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_lang_tmpdir, db, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_store_t *s = cbm_store_open_path(db); ASSERT_NOT_NULL(s); const char *proj = cbm_pipeline_project_name(p); cbm_node_t *nodes = NULL; int nc = 0; cbm_store_find_nodes_by_name(s, proj, "compute", &nodes, &nc); ASSERT_GT(nc, 0); bool found_docstring = false; for (int i = 0; i < nc; i++) { if (strcmp(nodes[i].label, "Function") == 0 && nodes[i].properties_json && strstr(nodes[i].properties_json, "docstring") && strstr(nodes[i].properties_json, "Does something")) { found_docstring = true; } } ASSERT_TRUE(found_docstring); cbm_store_free_nodes(nodes, nc); cbm_store_close(s); cbm_pipeline_free(p); teardown_lang_repo(); PASS(); } TEST(pipeline_docstring_java_method) { /* Java method with Javadoc comment */ const char *files[] = {"A.java"}; const char *contents[] = {"class A {\n" "\t/** Computes result. */\n" "\tvoid compute() {}\n" "}\n"}; if (setup_lang_repo(files, contents, 1) != 0) FAIL("tmpdir"); char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_lang_tmpdir, db, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_store_t *s = cbm_store_open_path(db); ASSERT_NOT_NULL(s); const char *proj = cbm_pipeline_project_name(p); cbm_node_t *nodes = NULL; int nc = 0; cbm_store_find_nodes_by_name(s, proj, "compute", &nodes, &nc); ASSERT_GT(nc, 0); bool found_docstring = false; for (int i = 0; i < nc; i++) { if (strcmp(nodes[i].label, "Method") == 0 && nodes[i].properties_json && strstr(nodes[i].properties_json, "docstring") && strstr(nodes[i].properties_json, "Computes result")) { found_docstring = true; } } ASSERT_TRUE(found_docstring); cbm_store_free_nodes(nodes, nc); cbm_store_close(s); cbm_pipeline_free(p); teardown_lang_repo(); PASS(); } TEST(pipeline_docstring_kotlin_function) { /* Kotlin function with KDoc comment */ const char *files[] = {"main.kt"}; const char *contents[] = {"/** Computes result. */\n" "fun compute() {}\n"}; if (setup_lang_repo(files, contents, 1) != 0) FAIL("tmpdir"); char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_lang_tmpdir, db, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_store_t *s = cbm_store_open_path(db); ASSERT_NOT_NULL(s); const char *proj = cbm_pipeline_project_name(p); cbm_node_t *nodes = NULL; int nc = 0; cbm_store_find_nodes_by_name(s, proj, "compute", &nodes, &nc); ASSERT_GT(nc, 0); bool found_docstring = false; for (int i = 0; i < nc; i++) { if (strcmp(nodes[i].label, "Function") == 0 && nodes[i].properties_json && strstr(nodes[i].properties_json, "docstring") && strstr(nodes[i].properties_json, "Computes result")) { found_docstring = true; } } ASSERT_TRUE(found_docstring); cbm_store_free_nodes(nodes, nc); cbm_store_close(s); cbm_pipeline_free(p); teardown_lang_repo(); PASS(); } TEST(pipeline_docstring_go_class) { /* Go struct with // comment docstring */ const char *files[] = {"main.go"}; const char *contents[] = {"package main\n\n" "// MyStruct is documented.\n" "type MyStruct struct{}\n"}; if (setup_lang_repo(files, contents, 1) != 0) FAIL("tmpdir"); char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_lang_tmpdir, db, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_store_t *s = cbm_store_open_path(db); ASSERT_NOT_NULL(s); const char *proj = cbm_pipeline_project_name(p); cbm_node_t *nodes = NULL; int nc = 0; cbm_store_find_nodes_by_name(s, proj, "MyStruct", &nodes, &nc); ASSERT_GT(nc, 0); bool found_docstring = false; for (int i = 0; i < nc; i++) { if (strcmp(nodes[i].label, "Struct") == 0 && nodes[i].properties_json && strstr(nodes[i].properties_json, "docstring") && strstr(nodes[i].properties_json, "MyStruct is documented")) { found_docstring = true; } } ASSERT_TRUE(found_docstring); cbm_store_free_nodes(nodes, nc); cbm_store_close(s); cbm_pipeline_free(p); teardown_lang_repo(); PASS(); } TEST(project_name_from_path) { /* Port of TestProjectNameFromPath — more cases than integ_pipeline_project_name */ struct { const char *path; const char *want; } cases[] = { {"/tmp/bench/erlang/lib/stdlib/src", "tmp-bench-erlang-lib-stdlib-src"}, {"/Users/martin/projects/myapp", "Users-martin-projects-myapp"}, {"/home/user/repo", "home-user-repo"}, {"/single", "single"}, }; for (int i = 0; i < 4; i++) { char *got = cbm_project_name_from_path(cases[i].path); ASSERT_NOT_NULL(got); ASSERT_STR_EQ(got, cases[i].want); free(got); } PASS(); } /* Regression for #394/#227/#367: a Windows drive letter is case-insensitive, so * "c:/repo" and "C:/repo" must canonicalize to the SAME project key. Otherwise * agent CWDs (which report lowercase "c:\...") produce a distinct key + colliding * cache file that clobbers the good index, and the lowercase index self-deletes. * Pure string logic, so it reproduces on any platform. */ TEST(project_name_drive_letter_case_insensitive_issue394) { char *lower = cbm_project_name_from_path("c:/WEBDEV/Cardio-Cloud"); char *upper = cbm_project_name_from_path("C:/WEBDEV/Cardio-Cloud"); ASSERT_NOT_NULL(lower); ASSERT_NOT_NULL(upper); /* Both must fold to the upper-case-drive key, e.g. "C-WEBDEV-Cardio-Cloud". */ ASSERT_STR_EQ(lower, upper); ASSERT_EQ(lower[0], 'C'); free(lower); free(upper); /* And the normalizer itself upper-cases the drive root in place. */ char buf1[32]; snprintf(buf1, sizeof(buf1), "%s", "c:/x"); cbm_normalize_path_sep(buf1); ASSERT_STR_EQ(buf1, "C:/x"); char buf2[32]; snprintf(buf2, sizeof(buf2), "%s", "d:\\proj\\sub"); cbm_normalize_path_sep(buf2); ASSERT_STR_EQ(buf2, "D:/proj/sub"); PASS(); } static const char *test_null_dev(void) { #ifdef _WIN32 return "NUL"; #else return "/dev/null"; #endif } static bool git_available(void) { char cmd[128]; snprintf(cmd, sizeof(cmd), "git --version >%s 2>&1", test_null_dev()); int rc = system(cmd); return rc == 0; } static int run_cmd(const char *cmd) { return system(cmd); } TEST(git_context_non_git_path) { char *tmp = th_mktempdir("cbm_gitctx_nongit"); ASSERT_NOT_NULL(tmp); cbm_git_context_t ctx = {0}; ASSERT_EQ(cbm_git_context_resolve(tmp, &ctx), 0); ASSERT_FALSE(ctx.is_git); ASSERT_TRUE(ctx.root_exists); char *qn = cbm_git_context_branch_qn("proj", &ctx); ASSERT_NOT_NULL(qn); ASSERT_STR_EQ(qn, "proj.__branch__.working-tree"); free(qn); char json[1024]; ASSERT_GT(cbm_git_context_props_json(&ctx, json, sizeof(json)), 0); ASSERT_NOT_NULL(strstr(json, "\"is_git\":false")); ASSERT_NOT_NULL(strstr(json, "\"root_exists\":true")); char long_value[1200]; memset(long_value, 'a', sizeof(long_value) - 1); long_value[sizeof(long_value) - 1] = '\0'; cbm_git_context_t long_ctx = { .root_exists = true, .canonical_root = long_value, }; char small_json[64]; ASSERT_EQ(cbm_git_context_props_json(&long_ctx, small_json, sizeof(small_json)), 0); cbm_git_context_free(&ctx); th_rmtree(tmp); PASS(); } TEST(git_context_linked_worktree) { if (!git_available()) { FAIL("git unavailable"); } char *tmp = th_mktempdir("cbm_gitctx_repo"); ASSERT_NOT_NULL(tmp); char repo[512], wt[512], cmd[2048]; int n = snprintf(repo, sizeof(repo), "%s/repo with space", tmp); ASSERT_TRUE(n > 0 && n < (int)sizeof(repo)); n = snprintf(wt, sizeof(wt), "%s/wt with space", tmp); ASSERT_TRUE(n > 0 && n < (int)sizeof(wt)); ASSERT_EQ(th_mkdir_p(repo), 0); const char *null_dev = test_null_dev(); snprintf(cmd, sizeof(cmd), "git -C \"%s\" init >%s 2>&1", repo, null_dev); ASSERT_EQ(run_cmd(cmd), 0); snprintf(cmd, sizeof(cmd), "git -C \"%s\" checkout -b main >%s 2>&1", repo, null_dev); ASSERT_EQ(run_cmd(cmd), 0); ASSERT_EQ(th_write_file(TH_PATH(repo, "file.txt"), "hello\n"), 0); snprintf(cmd, sizeof(cmd), "git -C \"%s\" add file.txt >%s 2>&1", repo, null_dev); ASSERT_EQ(run_cmd(cmd), 0); snprintf(cmd, sizeof(cmd), "git -C \"%s\" -c user.name=\"CBM Test\" -c user.email=\"cbm@example.invalid\" " "commit -m \"initial\" >%s 2>&1", repo, null_dev); ASSERT_EQ(run_cmd(cmd), 0); snprintf(cmd, sizeof(cmd), "git -C \"%s\" worktree add -b feature/git-context \"%s\" >%s 2>&1", repo, wt, null_dev); ASSERT_EQ(run_cmd(cmd), 0); cbm_git_context_t main_ctx = {0}; cbm_git_context_t wt_ctx = {0}; ASSERT_EQ(cbm_git_context_resolve(repo, &main_ctx), 0); ASSERT_EQ(cbm_git_context_resolve(wt, &wt_ctx), 0); ASSERT_TRUE(main_ctx.is_git); ASSERT_FALSE(main_ctx.is_worktree); ASSERT_TRUE(wt_ctx.is_git); ASSERT_TRUE(wt_ctx.is_worktree); ASSERT_STR_EQ(main_ctx.canonical_root, wt_ctx.canonical_root); ASSERT_NOT_NULL(wt_ctx.branch); ASSERT_STR_EQ(wt_ctx.branch, "feature/git-context"); ASSERT_STR_EQ(wt_ctx.branch_slug, "feature-git-context"); ASSERT_NOT_NULL(wt_ctx.head_sha); char *qn = cbm_git_context_branch_qn("proj", &wt_ctx); ASSERT_NOT_NULL(qn); ASSERT_STR_EQ(qn, "proj.__branch__.feature-git-context"); free(qn); char json[2048]; ASSERT_GT(cbm_git_context_props_json(&wt_ctx, json, sizeof(json)), 0); ASSERT_NOT_NULL(strstr(json, "\"is_git\":true")); ASSERT_NOT_NULL(strstr(json, "\"is_worktree\":true")); ASSERT_NOT_NULL(strstr(json, "\"branch\":\"feature/git-context\"")); cbm_git_context_free(&main_ctx); cbm_git_context_free(&wt_ctx); snprintf(cmd, sizeof(cmd), "git -C \"%s\" checkout --detach HEAD >%s 2>&1", repo, null_dev); ASSERT_EQ(run_cmd(cmd), 0); cbm_git_context_t detached_ctx = {0}; ASSERT_EQ(cbm_git_context_resolve(repo, &detached_ctx), 0); ASSERT_TRUE(detached_ctx.is_detached); ASSERT_STR_EQ(detached_ctx.branch_slug, "detached"); cbm_git_context_free(&detached_ctx); th_rmtree(tmp); PASS(); } TEST(project_name_uniqueness) { /* Port of TestProjectNameUniqueness */ char *a = cbm_project_name_from_path("/tmp/bench/zig/lib/std"); char *b = cbm_project_name_from_path("/tmp/bench/erlang/lib/stdlib/src"); ASSERT_NOT_NULL(a); ASSERT_NOT_NULL(b); ASSERT_TRUE(strcmp(a, b) != 0); free(a); free(b); PASS(); } /* ── Git diff helpers (pass_gitdiff.c) ─────────────────────────── */ TEST(gitdiff_parse_range_with_count) { int start, count; cbm_parse_range("10,5", &start, &count); ASSERT_EQ(start, 10); ASSERT_EQ(count, 5); PASS(); } TEST(gitdiff_parse_range_no_count) { int start, count; cbm_parse_range("10", &start, &count); ASSERT_EQ(start, 10); ASSERT_EQ(count, 1); PASS(); } TEST(gitdiff_parse_range_zero_count) { int start, count; cbm_parse_range("1,0", &start, &count); ASSERT_EQ(start, 1); ASSERT_EQ(count, 0); PASS(); } TEST(gitdiff_parse_range_large) { int start, count; cbm_parse_range("52,2", &start, &count); ASSERT_EQ(start, 52); ASSERT_EQ(count, 2); PASS(); } TEST(gitdiff_parse_name_status) { const char *input = "M\tinternal/store/nodes.go\n" "A\tnew_file.go\n" "D\told_file.go\n" "R100\tsrc/old.go\tsrc/new.go\n"; cbm_changed_file_t files[16]; int n = cbm_parse_name_status(input, files, 16); ASSERT_EQ(n, 4); ASSERT_STR_EQ(files[0].status, "M"); ASSERT_STR_EQ(files[0].path, "internal/store/nodes.go"); ASSERT_STR_EQ(files[1].status, "A"); ASSERT_STR_EQ(files[1].path, "new_file.go"); ASSERT_STR_EQ(files[2].status, "D"); ASSERT_STR_EQ(files[2].path, "old_file.go"); ASSERT_STR_EQ(files[3].status, "R"); ASSERT_STR_EQ(files[3].path, "src/new.go"); ASSERT_STR_EQ(files[3].old_path, "src/old.go"); PASS(); } TEST(gitdiff_parse_name_status_filters_untrackable) { const char *input = "M\tpackage-lock.json\n" "M\tsrc/main.go\n" "M\tvendor/lib.go\n"; cbm_changed_file_t files[16]; int n = cbm_parse_name_status(input, files, 16); ASSERT_EQ(n, 1); ASSERT_STR_EQ(files[0].path, "src/main.go"); PASS(); } TEST(gitdiff_parse_hunks) { const char *input = "diff --git a/main.go b/main.go\n" "index abc1234..def5678 100644\n" "--- a/main.go\n" "+++ b/main.go\n" "@@ -10,3 +10,5 @@ func main() {\n" "+\tnewLine1()\n" "+\tnewLine2()\n" "@@ -50,0 +52,2 @@ func helper() {\n" "+\tanother()\n" "+\tline()\n" "diff --git a/binary.png b/binary.png\n" "Binary files a/binary.png and b/binary.png differ\n" "diff --git a/utils.go b/utils.go\n" "--- a/utils.go\n" "+++ b/utils.go\n" "@@ -1 +1 @@ package utils\n" "-old\n" "+new\n"; cbm_changed_hunk_t hunks[16]; int n = cbm_parse_hunks(input, hunks, 16); ASSERT_EQ(n, 3); ASSERT_STR_EQ(hunks[0].path, "main.go"); ASSERT_EQ(hunks[0].start_line, 10); ASSERT_EQ(hunks[0].end_line, 14); ASSERT_STR_EQ(hunks[1].path, "main.go"); ASSERT_EQ(hunks[1].start_line, 52); ASSERT_EQ(hunks[1].end_line, 53); ASSERT_STR_EQ(hunks[2].path, "utils.go"); ASSERT_EQ(hunks[2].start_line, 1); ASSERT_EQ(hunks[2].end_line, 1); PASS(); } TEST(gitdiff_parse_hunks_no_newline_marker) { const char *input = "diff --git a/file.go b/file.go\n" "--- a/file.go\n" "+++ b/file.go\n" "@@ -5,2 +5,3 @@ func foo() {\n" "+\tbar()\n" "\\ No newline at end of file\n"; cbm_changed_hunk_t hunks[4]; int n = cbm_parse_hunks(input, hunks, 4); ASSERT_EQ(n, 1); ASSERT_EQ(hunks[0].start_line, 5); ASSERT_EQ(hunks[0].end_line, 7); PASS(); } TEST(gitdiff_parse_hunks_mode_change) { const char *input = "diff --git a/script.sh b/script.sh\n" "old mode 100644\n" "new mode 100755\n"; cbm_changed_hunk_t hunks[4]; int n = cbm_parse_hunks(input, hunks, 4); ASSERT_EQ(n, 0); PASS(); } TEST(gitdiff_parse_hunks_deletion) { const char *input = "diff --git a/file.go b/file.go\n" "--- a/file.go\n" "+++ b/file.go\n" "@@ -10,3 +10,0 @@ func foo() {\n"; cbm_changed_hunk_t hunks[4]; int n = cbm_parse_hunks(input, hunks, 4); ASSERT_EQ(n, 1); ASSERT_EQ(hunks[0].start_line, 10); PASS(); } /* ── Config helpers (pass_configures.c) ───────────────────────── */ TEST(configures_is_env_var_name) { ASSERT(cbm_is_env_var_name("DATABASE_URL")); ASSERT(cbm_is_env_var_name("API_KEY")); ASSERT(cbm_is_env_var_name("PORT")); ASSERT(!cbm_is_env_var_name("A")); /* too short */ ASSERT(!cbm_is_env_var_name("port")); /* lowercase */ ASSERT(!cbm_is_env_var_name("apiKey")); /* camelCase */ ASSERT(cbm_is_env_var_name("DB_2")); /* with digit */ ASSERT(!cbm_is_env_var_name("__")); /* no uppercase */ ASSERT(!cbm_is_env_var_name("")); /* empty */ PASS(); } TEST(configures_normalize_config_key) { char norm[256]; int tokens; tokens = cbm_normalize_config_key("max_connections", norm, sizeof(norm)); ASSERT_STR_EQ(norm, "max_connections"); ASSERT_EQ(tokens, 2); tokens = cbm_normalize_config_key("maxConnections", norm, sizeof(norm)); ASSERT_STR_EQ(norm, "max_connections"); ASSERT_EQ(tokens, 2); tokens = cbm_normalize_config_key("DATABASE_HOST", norm, sizeof(norm)); ASSERT_STR_EQ(norm, "database_host"); ASSERT_EQ(tokens, 2); tokens = cbm_normalize_config_key("database.host", norm, sizeof(norm)); ASSERT_STR_EQ(norm, "database_host"); ASSERT_EQ(tokens, 2); tokens = cbm_normalize_config_key("port", norm, sizeof(norm)); ASSERT_STR_EQ(norm, "port"); ASSERT_EQ(tokens, 1); tokens = cbm_normalize_config_key("maxRetryCount", norm, sizeof(norm)); ASSERT_STR_EQ(norm, "max_retry_count"); ASSERT_EQ(tokens, 3); PASS(); } TEST(configures_has_config_extension) { ASSERT(cbm_has_config_extension("config.toml")); ASSERT(cbm_has_config_extension("settings.yaml")); ASSERT(cbm_has_config_extension("config.yml")); ASSERT(cbm_has_config_extension(".env")); ASSERT(cbm_has_config_extension("config.ini")); ASSERT(cbm_has_config_extension("data.json")); ASSERT(cbm_has_config_extension("pom.xml")); ASSERT(!cbm_has_config_extension("main.go")); ASSERT(!cbm_has_config_extension("app.py")); ASSERT(!cbm_has_config_extension("data.csv")); PASS(); } /* ── Config integration tests (configures_test.go ports) ──────── */ TEST(configures_env_var_in_config) { /* Port of TestBuildEnvIndex_ConfigVariableAdded: * config.toml has DATABASE_URL, main.go does os.Getenv("DATABASE_URL") * → CONFIGURES edges should link them. */ const char *files[] = {"config.toml", "main.go"}; const char *contents[] = {"DATABASE_URL = \"postgresql://localhost/db\"\n", "package main\n\n" "import \"os\"\n\n" "func main() {\n" "\turl := os.Getenv(\"DATABASE_URL\")\n" "\t_ = url\n" "}\n"}; if (setup_lang_repo(files, contents, 2) != 0) FAIL("tmpdir"); char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_lang_tmpdir, db, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_store_t *s = cbm_store_open_path(db); ASSERT_NOT_NULL(s); const char *proj = cbm_pipeline_project_name(p); /* Verify CONFIGURES edges were created */ cbm_edge_t *edges = NULL; int ec = 0; cbm_store_find_edges_by_type(s, proj, "CONFIGURES", &edges, &ec); /* At minimum the pipeline should not crash. Edge count depends on * extraction matching env var accesses to config variables. */ if (edges) cbm_store_free_edges(edges, ec); cbm_store_close(s); cbm_pipeline_free(p); teardown_lang_repo(); PASS(); } TEST(configures_lowercase_key_skipped) { /* Port of TestBuildEnvIndex_LowercaseKeySkipped: * config.toml has lowercase key — should NOT produce env var CONFIGURES edges. */ const char *files[] = {"config.toml", "main.go"}; const char *contents[] = {"database_host = \"localhost\"\n", "package main\n\nfunc main() {}\n"}; if (setup_lang_repo(files, contents, 2) != 0) FAIL("tmpdir"); char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_lang_tmpdir, db, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_store_t *s = cbm_store_open_path(db); ASSERT_NOT_NULL(s); /* Pipeline ran successfully — no crash from lowercase config keys */ cbm_store_close(s); cbm_pipeline_free(p); teardown_lang_repo(); PASS(); } TEST(configures_non_config_file_skipped) { /* Port of TestBuildEnvIndex_NonConfigFileSkipped: * Only Go file, no config file — no config-derived CONFIGURES edges. */ const char *files[] = {"main.go"}; const char *contents[] = {"package main\n\n" "var API_URL = \"https://api.example.com\"\n\n" "func main() {}\n"}; if (setup_lang_repo(files, contents, 1) != 0) FAIL("tmpdir"); char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_lang_tmpdir, db, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_store_t *s = cbm_store_open_path(db); ASSERT_NOT_NULL(s); /* No config file → buildEnvIndex should not create config-derived entries */ cbm_store_close(s); cbm_pipeline_free(p); teardown_lang_repo(); PASS(); } TEST(configures_full_pipeline_integration) { /* Port of TestConfigIntegration_FullPipeline: * TOML + INI + JSON config files + Go code → Class & Variable nodes from * config files, plus CONFIGURES edges. */ const char *files[] = {"config.toml", "settings.ini", "config.json", "main.go"}; const char *contents[] = {"[database]\n" "host = \"localhost\"\n" "port = 5432\n" "max_connections = 100\n\n" "[server]\n" "bind_address = \"0.0.0.0\"\n", "[database]\n" "host = localhost\n" "port = 5432\n", "{\"appName\": \"test\", \"maxRetries\": 3}", "package main\n\n" "import \"os\"\n\n" "func getMaxConnections() int { return 100 }\n\n" "func loadConfig() {\n" "\tcfg := readFile(\"config.toml\")\n" "\t_ = cfg\n" "\tdbURL := os.Getenv(\"DATABASE_URL\")\n" "\t_ = dbURL\n" "}\n\n" "func readFile(path string) string { return \"\" }\n"}; if (setup_lang_repo(files, contents, 4) != 0) FAIL("tmpdir"); char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_lang_tmpdir, db, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_store_t *s = cbm_store_open_path(db); ASSERT_NOT_NULL(s); const char *proj = cbm_pipeline_project_name(p); /* Should have Class nodes (database, server sections from TOML) */ cbm_node_t *classes = NULL; int cc = 0; cbm_store_find_nodes_by_label(s, proj, "Class", &classes, &cc); ASSERT_GT(cc, 0); if (classes) cbm_store_free_nodes(classes, cc); /* Should have Variable nodes from config files */ cbm_node_t *vars = NULL; int vc = 0; cbm_store_find_nodes_by_label(s, proj, "Variable", &vars, &vc); ASSERT_GT(vc, 0); if (vars) cbm_store_free_nodes(vars, vc); /* Should have Function nodes from Go code */ cbm_node_t *funcs = NULL; int fc = 0; cbm_store_find_nodes_by_label(s, proj, "Function", &funcs, &fc); ASSERT_GT(fc, 0); if (funcs) cbm_store_free_nodes(funcs, fc); cbm_store_close(s); cbm_pipeline_free(p); teardown_lang_repo(); PASS(); } TEST(enrichment_split_camel_case) { char *parts[8]; int n; n = cbm_split_camel_case("GetMapping", parts, 8); ASSERT_EQ(n, 2); ASSERT_STR_EQ(parts[0], "Get"); ASSERT_STR_EQ(parts[1], "Mapping"); for (int i = 0; i < n; i++) free(parts[i]); n = cbm_split_camel_case("getMessage", parts, 8); ASSERT_EQ(n, 2); ASSERT_STR_EQ(parts[0], "get"); ASSERT_STR_EQ(parts[1], "Message"); for (int i = 0; i < n; i++) free(parts[i]); n = cbm_split_camel_case("cache", parts, 8); ASSERT_EQ(n, 1); ASSERT_STR_EQ(parts[0], "cache"); for (int i = 0; i < n; i++) free(parts[i]); n = cbm_split_camel_case("HTMLParser", parts, 8); ASSERT_EQ(n, 1); ASSERT_STR_EQ(parts[0], "HTMLParser"); for (int i = 0; i < n; i++) free(parts[i]); n = cbm_split_camel_case("", parts, 8); ASSERT_EQ(n, 0); PASS(); } TEST(enrichment_tokenize_decorator) { char *tokens[16]; int n; n = cbm_tokenize_decorator("@Override", tokens, 16); ASSERT_EQ(n, 1); ASSERT_STR_EQ(tokens[0], "override"); for (int i = 0; i < n; i++) free(tokens[i]); n = cbm_tokenize_decorator("@Deprecated", tokens, 16); ASSERT_EQ(n, 1); ASSERT_STR_EQ(tokens[0], "deprecated"); for (int i = 0; i < n; i++) free(tokens[i]); n = cbm_tokenize_decorator("@Test", tokens, 16); ASSERT_EQ(n, 1); ASSERT_STR_EQ(tokens[0], "test"); for (int i = 0; i < n; i++) free(tokens[i]); n = cbm_tokenize_decorator("@login_required", tokens, 16); ASSERT_EQ(n, 2); ASSERT_STR_EQ(tokens[0], "login"); ASSERT_STR_EQ(tokens[1], "required"); for (int i = 0; i < n; i++) free(tokens[i]); n = cbm_tokenize_decorator("@cache", tokens, 16); ASSERT_EQ(n, 1); ASSERT_STR_EQ(tokens[0], "cache"); for (int i = 0; i < n; i++) free(tokens[i]); n = cbm_tokenize_decorator("@pytest.fixture", tokens, 16); ASSERT_EQ(n, 2); ASSERT_STR_EQ(tokens[0], "pytest"); ASSERT_STR_EQ(tokens[1], "fixture"); for (int i = 0; i < n; i++) free(tokens[i]); /* "get" is stopword → only "mapping" */ n = cbm_tokenize_decorator("@GetMapping(\"/api\")", tokens, 16); ASSERT_EQ(n, 1); ASSERT_STR_EQ(tokens[0], "mapping"); for (int i = 0; i < n; i++) free(tokens[i]); /* "post" passes, "mapping" passes */ n = cbm_tokenize_decorator("@PostMapping(\"/api\")", tokens, 16); ASSERT_EQ(n, 2); ASSERT_STR_EQ(tokens[0], "post"); ASSERT_STR_EQ(tokens[1], "mapping"); for (int i = 0; i < n; i++) free(tokens[i]); n = cbm_tokenize_decorator("@Transactional", tokens, 16); ASSERT_EQ(n, 1); ASSERT_STR_EQ(tokens[0], "transactional"); for (int i = 0; i < n; i++) free(tokens[i]); n = cbm_tokenize_decorator("@MessageMapping(\"/chat\")", tokens, 16); ASSERT_EQ(n, 2); ASSERT_STR_EQ(tokens[0], "message"); ASSERT_STR_EQ(tokens[1], "mapping"); for (int i = 0; i < n; i++) free(tokens[i]); /* Rust-style #[test] */ n = cbm_tokenize_decorator("#[test]", tokens, 16); ASSERT_EQ(n, 1); ASSERT_STR_EQ(tokens[0], "test"); for (int i = 0; i < n; i++) free(tokens[i]); /* #[derive(Debug)] */ n = cbm_tokenize_decorator("#[derive(Debug)]", tokens, 16); ASSERT_EQ(n, 1); ASSERT_STR_EQ(tokens[0], "derive"); for (int i = 0; i < n; i++) free(tokens[i]); /* Both "app" and "get" are stopwords → empty */ n = cbm_tokenize_decorator("@app.get(\"/api\")", tokens, 16); ASSERT_EQ(n, 0); /* "router" is stopword, "post" passes */ n = cbm_tokenize_decorator("@router.post(\"/api\")", tokens, 16); ASSERT_EQ(n, 1); ASSERT_STR_EQ(tokens[0], "post"); for (int i = 0; i < n; i++) free(tokens[i]); /* Too short after filtering */ n = cbm_tokenize_decorator("@x", tokens, 16); ASSERT_EQ(n, 0); /* Empty */ n = cbm_tokenize_decorator("", tokens, 16); ASSERT_EQ(n, 0); n = cbm_tokenize_decorator("@click.command", tokens, 16); ASSERT_EQ(n, 2); ASSERT_STR_EQ(tokens[0], "click"); ASSERT_STR_EQ(tokens[1], "command"); for (int i = 0; i < n; i++) free(tokens[i]); n = cbm_tokenize_decorator("@celery.task", tokens, 16); ASSERT_EQ(n, 2); ASSERT_STR_EQ(tokens[0], "celery"); ASSERT_STR_EQ(tokens[1], "task"); for (int i = 0; i < n; i++) free(tokens[i]); PASS(); } /* ── Decorator tags integration tests (enrichment_test.go ports) ─ */ /* Helper: check if a node's properties_json contains a specific decorator_tag */ static bool has_decorator_tag(const char *properties_json, const char *tag) { if (!properties_json || !tag) return false; yyjson_doc *doc = yyjson_read(properties_json, strlen(properties_json), 0); if (!doc) return false; yyjson_val *root = yyjson_doc_get_root(doc); yyjson_val *tags = yyjson_obj_get(root, "decorator_tags"); if (!tags || !yyjson_is_arr(tags)) { yyjson_doc_free(doc); return false; } yyjson_val *item; yyjson_arr_iter iter; yyjson_arr_iter_init(tags, &iter); while ((item = yyjson_arr_iter_next(&iter))) { if (yyjson_is_str(item) && strcmp(yyjson_get_str(item), tag) == 0) { yyjson_doc_free(doc); return true; } } yyjson_doc_free(doc); return false; } TEST(decorator_tags_python_auto_discovery) { /* Port of TestDecoratorTagAutoDiscovery: * Python file with repeated decorators (@login_required on 2 funcs, * @cache on 2 funcs, @unique_helper on 1 func). * Words on 2+ nodes become tags; unique words do not. */ const char *files[] = {"views.py"}; const char *contents[] = {"from functools import cache\n\n" "@login_required\n" "def list_orders():\n" " pass\n\n" "@login_required\n" "def get_order():\n" " pass\n\n" "@cache\n" "def compute_total():\n" " pass\n\n" "@cache\n" "def compute_tax():\n" " pass\n\n" "@unique_helper\n" "def special():\n" " pass\n"}; if (setup_lang_repo(files, contents, 1) != 0) FAIL("tmpdir"); char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_lang_tmpdir, db, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_store_t *s = cbm_store_open_path(db); ASSERT_NOT_NULL(s); const char *proj = cbm_pipeline_project_name(p); /* Find functions by name and check decorator_tags */ cbm_node_t *funcs = NULL; int fc = 0; cbm_store_find_nodes_by_label(s, proj, "Function", &funcs, &fc); /* Build name→properties_json map */ const char *list_orders_props = NULL; const char *get_order_props = NULL; const char *compute_total_props = NULL; const char *compute_tax_props = NULL; const char *special_props = NULL; for (int i = 0; i < fc; i++) { if (strcmp(funcs[i].name, "list_orders") == 0) list_orders_props = funcs[i].properties_json; else if (strcmp(funcs[i].name, "get_order") == 0) get_order_props = funcs[i].properties_json; else if (strcmp(funcs[i].name, "compute_total") == 0) compute_total_props = funcs[i].properties_json; else if (strcmp(funcs[i].name, "compute_tax") == 0) compute_tax_props = funcs[i].properties_json; else if (strcmp(funcs[i].name, "special") == 0) special_props = funcs[i].properties_json; } /* "login" and "required" appear on 2 nodes → should be tags */ ASSERT_TRUE(has_decorator_tag(list_orders_props, "login")); ASSERT_TRUE(has_decorator_tag(list_orders_props, "required")); ASSERT_TRUE(has_decorator_tag(get_order_props, "login")); ASSERT_TRUE(has_decorator_tag(get_order_props, "required")); /* "cache" appears on 2 nodes → should be a tag */ ASSERT_TRUE(has_decorator_tag(compute_total_props, "cache")); ASSERT_TRUE(has_decorator_tag(compute_tax_props, "cache")); /* "unique" and "helper" appear on only 1 node → should NOT be tags */ ASSERT_FALSE(has_decorator_tag(special_props, "unique")); ASSERT_FALSE(has_decorator_tag(special_props, "helper")); if (funcs) cbm_store_free_nodes(funcs, fc); cbm_store_close(s); cbm_pipeline_free(p); teardown_lang_repo(); PASS(); } TEST(decorator_tags_java_class_methods) { /* Port of TestDecoratorTagJavaClassMethods: * Java class with @GetMapping, @PostMapping, @Transactional annotations. * "mapping" appears on all 4 → tag. "post" on 2 → tag. */ const char *files[] = {"Controller.java"}; const char *contents[] = {"class OwnerController {\n" " @GetMapping(\"/owners\")\n" " public void listOwners() {}\n\n" " @GetMapping(\"/owners/{id}\")\n" " public void showOwner() {}\n\n" " @PostMapping(\"/owners\")\n" " public void createOwner() {}\n\n" " @Transactional\n" " @PostMapping(\"/owners/{id}\")\n" " public void updateOwner() {}\n" "}\n"}; if (setup_lang_repo(files, contents, 1) != 0) FAIL("tmpdir"); char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_lang_tmpdir, db, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_store_t *s = cbm_store_open_path(db); ASSERT_NOT_NULL(s); const char *proj = cbm_pipeline_project_name(p); /* Find methods */ cbm_node_t *methods = NULL; int mc = 0; cbm_store_find_nodes_by_label(s, proj, "Method", &methods, &mc); /* "mapping" appears on all 4 methods → should be a tag */ for (int i = 0; i < mc; i++) { if (strcmp(methods[i].name, "listOwners") == 0 || strcmp(methods[i].name, "showOwner") == 0 || strcmp(methods[i].name, "createOwner") == 0 || strcmp(methods[i].name, "updateOwner") == 0) { ASSERT_TRUE(has_decorator_tag(methods[i].properties_json, "mapping")); } } /* "post" appears on createOwner + updateOwner → should be a tag */ for (int i = 0; i < mc; i++) { if (strcmp(methods[i].name, "createOwner") == 0 || strcmp(methods[i].name, "updateOwner") == 0) { ASSERT_TRUE(has_decorator_tag(methods[i].properties_json, "post")); } } /* "transactional" appears on only 1 method → should NOT be a tag */ for (int i = 0; i < mc; i++) { if (strcmp(methods[i].name, "updateOwner") == 0) { ASSERT_FALSE(has_decorator_tag(methods[i].properties_json, "transactional")); } } if (methods) cbm_store_free_nodes(methods, mc); cbm_store_close(s); cbm_pipeline_free(p); teardown_lang_repo(); PASS(); } /* ── Compile commands helpers (pass_compile_commands.c) ────────── */ TEST(compile_commands_split_command) { char *args[16]; int n; n = cbm_split_command("gcc -c main.c", args, 16); ASSERT_EQ(n, 3); ASSERT_STR_EQ(args[0], "gcc"); ASSERT_STR_EQ(args[1], "-c"); ASSERT_STR_EQ(args[2], "main.c"); for (int i = 0; i < n; i++) free(args[i]); n = cbm_split_command("gcc -DFOO=\"bar baz\" -c main.c", args, 16); ASSERT_EQ(n, 4); for (int i = 0; i < n; i++) free(args[i]); n = cbm_split_command("g++ -I/usr/include -std=c++17 -o out -c in.cpp", args, 16); ASSERT_EQ(n, 7); for (int i = 0; i < n; i++) free(args[i]); PASS(); } TEST(compile_commands_extract_flags) { const char *args[] = {"g++", "-I", "/abs/include", "-I/rel/include", "-isystem", "/sys/include", "-DFOO", "-DBAR=42", "-std=c++20", "-O2", "-Wall", "-c", "main.cpp"}; cbm_compile_flags_t *f = cbm_extract_flags(args, 13, "/project"); ASSERT_NOT_NULL(f); ASSERT_EQ(f->include_count, 3); ASSERT_EQ(f->define_count, 2); ASSERT_STR_EQ(f->standard, "c++20"); cbm_compile_flags_free(f); PASS(); } TEST(compile_commands_parse_json) { const char *json = "[\n" " {\n" " \"directory\": \"/home/user/project/build\",\n" " \"command\": \"gcc -I/home/user/project/include " "-I/home/user/project/src -DDEBUG=1 -DVERSION=\\\"1.0\\\" " "-std=c11 -o main.o -c /home/user/project/src/main.c\",\n" " \"file\": \"/home/user/project/src/main.c\"\n" " },\n" " {\n" " \"directory\": \"/home/user/project/build\",\n" " \"arguments\": [\"g++\", \"-I/home/user/project/include\", " "\"-isystem\", \"/home/user/project/third_party\", " "\"-DUSE_SSL\", \"-std=c++17\", \"-c\", " "\"/home/user/project/src/server.cpp\"],\n" " \"file\": \"/home/user/project/src/server.cpp\"\n" " },\n" " {\n" " \"directory\": \"/home/user/project/build\",\n" " \"command\": \"gcc -c /outside/repo/file.c\",\n" " \"file\": \"/outside/repo/file.c\"\n" " }\n" "]"; char **paths = NULL; cbm_compile_flags_t **flags = NULL; int n = cbm_parse_compile_commands(json, "/home/user/project", &paths, &flags); ASSERT(n >= 2); /* At least main.c and server.cpp, outside file excluded */ /* Find main.c */ int main_idx = -1, server_idx = -1; for (int i = 0; i < n; i++) { if (strcmp(paths[i], "src/main.c") == 0) main_idx = i; if (strcmp(paths[i], "src/server.cpp") == 0) server_idx = i; } ASSERT(main_idx >= 0); ASSERT_EQ(flags[main_idx]->include_count, 2); ASSERT_EQ(flags[main_idx]->define_count, 2); ASSERT_STR_EQ(flags[main_idx]->standard, "c11"); ASSERT(server_idx >= 0); ASSERT_EQ(flags[server_idx]->include_count, 2); ASSERT_EQ(flags[server_idx]->define_count, 1); ASSERT_STR_EQ(flags[server_idx]->standard, "c++17"); /* Verify outside-repo file excluded */ for (int i = 0; i < n; i++) { ASSERT(strstr(paths[i], "outside") == NULL); } /* Cleanup */ for (int i = 0; i < n; i++) { free(paths[i]); cbm_compile_flags_free(flags[i]); } free(paths); free(flags); PASS(); } TEST(compile_commands_parse_empty) { char **paths = NULL; cbm_compile_flags_t **flags = NULL; int n = cbm_parse_compile_commands("[]", "/repo", &paths, &flags); ASSERT_EQ(n, 0); free(paths); free(flags); PASS(); } TEST(compile_commands_parse_invalid) { char **paths = NULL; cbm_compile_flags_t **flags = NULL; int n = cbm_parse_compile_commands("not json", "/repo", &paths, &flags); ASSERT(n < 0); PASS(); } /* ── Suite ─────────────────────────────────────────────────────── */ /* ── Infrascan: file identification ──────────────────────────────── */ TEST(infra_is_compose_file) { /* Port of TestIsComposeFile (8 cases) */ ASSERT(cbm_is_compose_file("docker-compose.yml")); ASSERT(cbm_is_compose_file("docker-compose.yaml")); ASSERT(cbm_is_compose_file("docker-compose.prod.yml")); ASSERT(cbm_is_compose_file("compose.yml")); ASSERT(cbm_is_compose_file("compose.yaml")); ASSERT(!cbm_is_compose_file("mycompose.yml")); ASSERT(!cbm_is_compose_file("docker-compose.txt")); ASSERT(!cbm_is_compose_file("Dockerfile")); PASS(); } TEST(infra_is_cloudbuild_file) { /* Port of TestIsCloudbuildFile (5 cases) */ ASSERT(cbm_is_cloudbuild_file("cloudbuild.yaml")); ASSERT(cbm_is_cloudbuild_file("cloudbuild.yml")); ASSERT(cbm_is_cloudbuild_file("cloudbuild-prod.yaml")); ASSERT(cbm_is_cloudbuild_file("Cloudbuild.yml")); ASSERT(!cbm_is_cloudbuild_file("build.yaml")); PASS(); } TEST(infra_is_shell_script) { /* Port of TestIsShellScript (5 cases) */ ASSERT(cbm_is_shell_script("run.sh", ".sh")); ASSERT(cbm_is_shell_script("deploy.bash", ".bash")); ASSERT(cbm_is_shell_script("init.zsh", ".zsh")); ASSERT(!cbm_is_shell_script("main.py", ".py")); ASSERT(!cbm_is_shell_script("Dockerfile", "")); PASS(); } TEST(infra_is_dockerfile) { ASSERT(cbm_is_dockerfile("Dockerfile")); ASSERT(cbm_is_dockerfile("dockerfile")); ASSERT(cbm_is_dockerfile("Dockerfile.prod")); ASSERT(cbm_is_dockerfile("app.dockerfile")); ASSERT(!cbm_is_dockerfile("docker-compose.yml")); ASSERT(!cbm_is_dockerfile("main.go")); PASS(); } TEST(infra_is_kustomize_file) { ASSERT(cbm_is_kustomize_file("kustomization.yaml")); ASSERT(cbm_is_kustomize_file("kustomization.yml")); ASSERT(cbm_is_kustomize_file("KUSTOMIZATION.YAML")); /* case-insensitive */ ASSERT(!cbm_is_kustomize_file("deployment.yaml")); ASSERT(!cbm_is_kustomize_file("kustomize.yaml")); ASSERT(!cbm_is_kustomize_file(NULL)); PASS(); } TEST(infra_is_k8s_manifest) { const char *deploy = "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: my-app\n"; const char *plain = "name: foo\nvalue: bar\n"; const char *kust = "apiVersion: kustomize.config.k8s.io/v1beta1\nkind: Kustomization\n"; ASSERT(cbm_is_k8s_manifest("deployment.yaml", deploy)); ASSERT(!cbm_is_k8s_manifest("deployment.yaml", plain)); /* kustomize file should return false even if it has apiVersion */ ASSERT(!cbm_is_k8s_manifest("kustomization.yaml", kust)); ASSERT(!cbm_is_k8s_manifest(NULL, deploy)); ASSERT(!cbm_is_k8s_manifest("deployment.yaml", NULL)); PASS(); } TEST(infra_is_env_file) { ASSERT(cbm_is_env_file(".env")); ASSERT(cbm_is_env_file(".env.local")); ASSERT(cbm_is_env_file("prod.env")); ASSERT(!cbm_is_env_file("main.go")); ASSERT(!cbm_is_env_file("env.txt")); PASS(); } /* ── Infrascan: cleanJSONBrackets ───────────────────────────────── */ TEST(infra_clean_json_brackets) { /* Port of TestCleanJSONBrackets (4 cases) */ char out[256]; cbm_clean_json_brackets("[\"./server\"]", out, sizeof(out)); ASSERT_STR_EQ(out, "./server"); cbm_clean_json_brackets("[\"python\", \"main.py\"]", out, sizeof(out)); ASSERT_STR_EQ(out, "python main.py"); cbm_clean_json_brackets("./server", out, sizeof(out)); ASSERT_STR_EQ(out, "./server"); cbm_clean_json_brackets("[\"./app\", \"--flag\", \"value\"]", out, sizeof(out)); ASSERT_STR_EQ(out, "./app --flag value"); PASS(); } /* ── Infrascan: secret detection ────────────────────────────────── */ TEST(infra_secret_detection) { /* Key-based detection */ ASSERT(cbm_is_secret_binding("JWT_SECRET", "anything")); ASSERT(cbm_is_secret_binding("API_KEY", "anything")); ASSERT(cbm_is_secret_binding("my_password", "anything")); ASSERT(cbm_is_secret_binding("AUTH_TOKEN", "anything")); ASSERT(!cbm_is_secret_binding("DATABASE_URL", "https://db.example.com")); /* Value-based detection */ ASSERT(cbm_is_secret_value("sk-1234567890abcdef12345")); ASSERT(cbm_is_secret_value("-----BEGIN RSA PRIVATE KEY-----")); ASSERT(!cbm_is_secret_value("https://db.example.com")); ASSERT(!cbm_is_secret_value("hello world")); ASSERT(!cbm_is_secret_value("8080")); /* isSecretBinding checks both */ ASSERT(cbm_is_secret_binding("ANYTHING", "sk-1234567890abcdef12345")); ASSERT(!cbm_is_secret_binding("PORT", "8080")); PASS(); } /* ── Infrascan: Dockerfile parser ───────────────────────────────── */ /* Helper: find env var by key in result */ static const char *find_env_var(const cbm_env_kv_t *vars, int count, const char *key) { for (int i = 0; i < count; i++) { if (strcmp(vars[i].key, key) == 0) return vars[i].value; } return NULL; } /* Helper: check if string array contains value */ static bool str_array_contains(const char (*arr)[32], int count, const char *val) { for (int i = 0; i < count; i++) { if (strcmp(arr[i], val) == 0) return true; } return false; } static bool str_array_128_contains(const char (*arr)[128], int count, const char *val) { for (int i = 0; i < count; i++) { if (strcmp(arr[i], val) == 0) return true; } return false; } static bool str_array_256_contains(const char (*arr)[256], int count, const char *val) { for (int i = 0; i < count; i++) { if (strcmp(arr[i], val) == 0) return true; } return false; } TEST(infra_parse_dockerfile_multistage) { /* Port of TestParseDockerfile "multi-stage with all directives" */ const char *src = "FROM golang:1.23-alpine AS builder\n" "WORKDIR /app\n" "ARG SSH_PRIVATE_KEY\n" "RUN go build -o server .\n" "\n" "FROM alpine:3.19\n" "WORKDIR /usr/app\n" "ENV PORT=8080\n" "ENV PYTHONUNBUFFERED=1\n" "EXPOSE 8080 443\n" "USER appuser\n" "CMD [\"./server\"]\n" "HEALTHCHECK CMD wget http://localhost:8080/health\n"; cbm_dockerfile_result_t r; ASSERT_EQ(cbm_parse_dockerfile_source(src, &r), 0); ASSERT_STR_EQ(r.base_image, "alpine:3.19"); ASSERT_EQ(r.stage_count, 2); ASSERT_STR_EQ(r.stage_images[0], "golang:1.23-alpine"); ASSERT_STR_EQ(r.stage_images[1], "alpine:3.19"); ASSERT(str_array_contains(r.exposed_ports, r.port_count, "8080")); ASSERT(str_array_contains(r.exposed_ports, r.port_count, "443")); ASSERT_STR_EQ(r.workdir, "/usr/app"); ASSERT_STR_EQ(r.user, "appuser"); ASSERT_STR_EQ(r.cmd, "./server"); ASSERT_STR_EQ(r.healthcheck, "wget http://localhost:8080/health"); ASSERT_NOT_NULL(find_env_var(r.env_vars, r.env_count, "PORT")); ASSERT_STR_EQ(find_env_var(r.env_vars, r.env_count, "PORT"), "8080"); ASSERT_STR_EQ(find_env_var(r.env_vars, r.env_count, "PYTHONUNBUFFERED"), "1"); ASSERT(str_array_128_contains(r.build_args, r.build_arg_count, "SSH_PRIVATE_KEY")); PASS(); } TEST(infra_parse_dockerfile_entrypoint) { /* Port of TestParseDockerfile "single stage with entrypoint" */ const char *src = "FROM python:3.9-slim\n" "ENTRYPOINT [\"python\", \"main.py\"]\n"; cbm_dockerfile_result_t r; ASSERT_EQ(cbm_parse_dockerfile_source(src, &r), 0); ASSERT_STR_EQ(r.base_image, "python:3.9-slim"); ASSERT_STR_EQ(r.entrypoint, "python main.py"); ASSERT_EQ(r.stage_count, 1); PASS(); } TEST(infra_parse_dockerfile_secret_filtered) { /* Port of TestParseDockerfile "secret env vars filtered" */ const char *src = "FROM node:20\n" "ENV API_KEY=sk-1234567890abcdef12345\n" "ENV DATABASE_URL=https://db.example.com\n" "ENV JWT_SECRET=supersecret\n"; cbm_dockerfile_result_t r; ASSERT_EQ(cbm_parse_dockerfile_source(src, &r), 0); /* API_KEY and JWT_SECRET should be filtered */ ASSERT(find_env_var(r.env_vars, r.env_count, "API_KEY") == NULL); ASSERT(find_env_var(r.env_vars, r.env_count, "JWT_SECRET") == NULL); /* DATABASE_URL should remain */ ASSERT_NOT_NULL(find_env_var(r.env_vars, r.env_count, "DATABASE_URL")); ASSERT_STR_EQ(find_env_var(r.env_vars, r.env_count, "DATABASE_URL"), "https://db.example.com"); PASS(); } TEST(infra_parse_dockerfile_expose_protocol) { /* Port of TestParseDockerfile "expose with protocol suffix" */ const char *src = "FROM nginx:latest\n" "EXPOSE 80/tcp 443/tcp\n"; cbm_dockerfile_result_t r; ASSERT_EQ(cbm_parse_dockerfile_source(src, &r), 0); ASSERT(str_array_contains(r.exposed_ports, r.port_count, "80")); ASSERT(str_array_contains(r.exposed_ports, r.port_count, "443")); PASS(); } TEST(infra_parse_dockerfile_env_space) { /* Port of TestParseDockerfile "ENV space-separated format" */ const char *src = "FROM python:3.9\n" "ENV PYTHONPATH /usr/app\n"; cbm_dockerfile_result_t r; ASSERT_EQ(cbm_parse_dockerfile_source(src, &r), 0); ASSERT_NOT_NULL(find_env_var(r.env_vars, r.env_count, "PYTHONPATH")); ASSERT_STR_EQ(find_env_var(r.env_vars, r.env_count, "PYTHONPATH"), "/usr/app"); PASS(); } TEST(infra_parse_dockerfile_empty) { /* Port of TestParseDockerfileEmpty */ cbm_dockerfile_result_t r; ASSERT_EQ(cbm_parse_dockerfile_source("# just a comment\n", &r), -1); PASS(); } /* ── Infrascan: Dotenv parser ───────────────────────────────────── */ TEST(infra_parse_dotenv) { /* Port of TestParseDotenvFile */ const char *src = "# Database config\n" "DATABASE_HOST=localhost\n" "DATABASE_PORT=5432\n" "DATABASE_NAME=mydb\n" "API_SECRET=should-not-appear\n" "PLAIN_VALUE=hello world\n"; cbm_dotenv_result_t r; ASSERT_EQ(cbm_parse_dotenv_source(src, &r), 0); ASSERT_STR_EQ(find_env_var(r.env_vars, r.env_count, "DATABASE_HOST"), "localhost"); ASSERT_STR_EQ(find_env_var(r.env_vars, r.env_count, "DATABASE_PORT"), "5432"); ASSERT_STR_EQ(find_env_var(r.env_vars, r.env_count, "PLAIN_VALUE"), "hello world"); /* API_SECRET should be filtered */ ASSERT(find_env_var(r.env_vars, r.env_count, "API_SECRET") == NULL); PASS(); } TEST(infra_parse_dotenv_quoted) { /* Port of TestParseDotenvQuotedValues */ const char *src = "KEY1=\"quoted value\"\n" "KEY2='single quoted'\n"; cbm_dotenv_result_t r; ASSERT_EQ(cbm_parse_dotenv_source(src, &r), 0); ASSERT_STR_EQ(find_env_var(r.env_vars, r.env_count, "KEY1"), "quoted value"); ASSERT_STR_EQ(find_env_var(r.env_vars, r.env_count, "KEY2"), "single quoted"); PASS(); } /* ── Infrascan: Shell script parser ─────────────────────────────── */ TEST(infra_parse_shell) { /* Port of TestParseShellScript */ const char *src = "#!/bin/bash\n" "set -e\n" "\n" "# Configuration\n" "YOUR_CONTAINER_NAME=\"order-email-extractor-endpoint\"\n" "DOCKERFILE_PATH=\"/path/to/dockerfile\"\n" "\n" "export ENVIRONMENT=\"development\"\n" "export USE_STACKDRIVER=\"false\"\n" "\n" "# Shut down existing containers\n" "./shut-down-docker-container.sh\n" "\n" "docker build -t \"$YOUR_CONTAINER_NAME\" \"$DOCKERFILE_PATH\"\n" "docker run -d --name \"$YOUR_CONTAINER_NAME\" \"$YOUR_CONTAINER_NAME\"\n" "docker-compose up -d\n"; cbm_shell_result_t r; ASSERT_EQ(cbm_parse_shell_source(src, &r), 0); ASSERT_STR_EQ(r.shebang, "/bin/bash"); ASSERT_STR_EQ(find_env_var(r.env_vars, r.env_count, "ENVIRONMENT"), "development"); ASSERT_STR_EQ(find_env_var(r.env_vars, r.env_count, "YOUR_CONTAINER_NAME"), "order-email-extractor-endpoint"); ASSERT(str_array_256_contains(r.docker_cmds, r.docker_cmd_count, "docker build")); ASSERT(str_array_256_contains(r.docker_cmds, r.docker_cmd_count, "docker run")); ASSERT(str_array_256_contains(r.docker_cmds, r.docker_cmd_count, "docker-compose up")); PASS(); } TEST(infra_parse_shell_with_source) { /* Port of TestParseShellScriptWithSource */ const char *src = "#!/usr/bin/env bash\n" "source ./config.sh\n" ". /etc/profile.d/env.sh\n"; cbm_shell_result_t r; ASSERT_EQ(cbm_parse_shell_source(src, &r), 0); ASSERT_STR_EQ(r.shebang, "/usr/bin/env bash"); ASSERT(str_array_256_contains(r.sources, r.source_count, "./config.sh")); ASSERT(str_array_256_contains(r.sources, r.source_count, "/etc/profile.d/env.sh")); PASS(); } TEST(infra_parse_shell_secret_filtered) { /* Port of TestParseShellScriptSecretFiltered */ const char *src = "#!/bin/bash\n" "export API_SECRET=\"should-not-appear\"\n" "export DATABASE_URL=\"https://db.example.com\"\n"; cbm_shell_result_t r; ASSERT_EQ(cbm_parse_shell_source(src, &r), 0); ASSERT(find_env_var(r.env_vars, r.env_count, "API_SECRET") == NULL); ASSERT_STR_EQ(find_env_var(r.env_vars, r.env_count, "DATABASE_URL"), "https://db.example.com"); PASS(); } TEST(infra_parse_shell_shebang_only) { /* Port of TestParseShellScriptShebanOnly */ const char *src = "#!/bin/bash\n# just comments\n"; cbm_shell_result_t r; ASSERT_EQ(cbm_parse_shell_source(src, &r), 0); ASSERT_STR_EQ(r.shebang, "/bin/bash"); PASS(); } TEST(infra_parse_shell_truly_empty) { /* Port of TestParseShellScriptTrulyEmpty */ cbm_shell_result_t r; ASSERT_EQ(cbm_parse_shell_source("# no shebang, just comments\n", &r), -1); PASS(); } /* ── Infrascan: Terraform parser ────────────────────────────────── */ TEST(infra_parse_terraform_full) { /* Port of TestParseTerraformFile */ const char *src = "\n" "terraform {\n" " required_providers {\n" " google = {\n" " source = \"hashicorp/google\"\n" " version = \"~> 6.35.0\"\n" " }\n" " }\n" " backend \"gcs\" {\n" " bucket = \"example-tf\"\n" " prefix = \"state\"\n" " }\n" "}\n" "\n" "variable \"project_id\" {\n" " description = \"The GCP project ID\"\n" " type = string\n" " default = \"example-cloud\"\n" "}\n" "\n" "variable \"region\" {\n" " description = \"The region\"\n" " type = string\n" "}\n" "\n" "resource \"google_cloud_run_service\" \"main\" {\n" " name = \"my-service\"\n" " location = var.region\n" "}\n" "\n" "resource \"google_compute_address\" \"nat_ip\" {\n" " name = \"nat-ip\"\n" " region = var.region\n" "}\n" "\n" "output \"service_url\" {\n" " value = google_cloud_run_service.main.status[0].url\n" "}\n" "\n" "data \"google_project\" \"project\" {\n" "}\n" "\n" "module \"vpc\" {\n" " source = \"./modules/vpc\"\n" "}\n" "\n" "locals {\n" " env = \"prod\"\n" "}\n"; cbm_terraform_result_t r; ASSERT_EQ(cbm_parse_terraform_source(src, &r), 0); ASSERT_STR_EQ(r.backend, "gcs"); /* Resources */ ASSERT_EQ(r.resource_count, 2); bool found_cloud_run = false; for (int i = 0; i < r.resource_count; i++) { if (strcmp(r.resources[i].type, "google_cloud_run_service") == 0 && strcmp(r.resources[i].name, "main") == 0) { found_cloud_run = true; } } ASSERT(found_cloud_run); /* Variables */ ASSERT_EQ(r.variable_count, 2); bool found_project_id = false; for (int i = 0; i < r.variable_count; i++) { if (strcmp(r.variables[i].name, "project_id") == 0) { ASSERT_STR_EQ(r.variables[i].default_val, "example-cloud"); ASSERT_STR_EQ(r.variables[i].type, "string"); ASSERT_STR_EQ(r.variables[i].description, "The GCP project ID"); found_project_id = true; } } ASSERT(found_project_id); /* Outputs */ ASSERT_EQ(r.output_count, 1); ASSERT_STR_EQ(r.outputs[0], "service_url"); /* Data sources */ ASSERT_EQ(r.data_source_count, 1); ASSERT_STR_EQ(r.data_sources[0].type, "google_project"); ASSERT_STR_EQ(r.data_sources[0].name, "project"); /* Modules */ ASSERT_EQ(r.module_count, 1); ASSERT_STR_EQ(r.modules[0].tf_name, "vpc"); ASSERT_STR_EQ(r.modules[0].source, "./modules/vpc"); /* Locals */ ASSERT(r.has_locals); PASS(); } TEST(infra_parse_terraform_variables_only) { /* Port of TestParseTerraformVariablesOnly — secret default filtered */ const char *src = "\n" "variable \"project_id\" {\n" " description = \"The GCP project ID\"\n" " type = string\n" " default = \"example-cloud\"\n" "}\n" "\n" "variable \"secret_key\" {\n" " description = \"A secret\"\n" " type = string\n" " default = \"sk-1234567890abcdef12345\"\n" "}\n"; cbm_terraform_result_t r; ASSERT_EQ(cbm_parse_terraform_source(src, &r), 0); ASSERT_EQ(r.variable_count, 2); /* secret_key default should be filtered */ for (int i = 0; i < r.variable_count; i++) { if (strcmp(r.variables[i].name, "secret_key") == 0) { ASSERT_STR_EQ(r.variables[i].default_val, ""); } } PASS(); } TEST(infra_parse_terraform_empty) { /* Port of TestParseTerraformEmpty */ cbm_terraform_result_t r; ASSERT_EQ(cbm_parse_terraform_source("# just comments\n", &r), -1); PASS(); } /* ── Helm Chart.yaml dependency parsing (#338) ──────────────────── */ TEST(helm_parse_chart_dependencies_issue338) { const char *src = "apiVersion: v2\n" "name: mychart\n" "version: 1.0.0\n" "dependencies:\n" " - name: postgresql\n" " repository: https://charts.bitnami.com/bitnami\n" " version: 12.x.x\n" " - name: redis\n" " repository: https://charts.bitnami.com/bitnami\n" "maintainers:\n" " - name: alice\n"; /* not a dependency — outside the block */ cbm_helm_chart_t hc; ASSERT_EQ(cbm_parse_helm_chart(src, &hc), 0); ASSERT_STR_EQ(hc.chart_name, "mychart"); ASSERT_EQ(hc.dep_count, 2); ASSERT_STR_EQ(hc.deps[0], "postgresql"); ASSERT_STR_EQ(hc.deps[1], "redis"); PASS(); } TEST(helm_parse_chart_no_deps_issue338) { cbm_helm_chart_t hc; ASSERT_EQ(cbm_parse_helm_chart("name: solo\nversion: 0.1.0\n", &hc), 0); ASSERT_STR_EQ(hc.chart_name, "solo"); ASSERT_EQ(hc.dep_count, 0); PASS(); } /* ── Infrascan: infra QN helper ─────────────────────────────────── */ /* ── Function Registry / Resolver tests ─────────────────────────── */ TEST(registry_resolve_single_candidate) { cbm_registry_t *reg = cbm_registry_new(); cbm_registry_add(reg, "CreateOrder", "svcA.handlers.CreateOrder", "Function"); cbm_registry_add(reg, "ValidateOrder", "svcB.validators.ValidateOrder", "Function"); /* Normal resolve unique name */ cbm_resolution_t r = cbm_registry_resolve(reg, "CreateOrder", "svcC.caller", NULL, NULL, 0); ASSERT_STR_EQ(r.qualified_name, "svcA.handlers.CreateOrder"); /* Fuzzy resolve with unknown prefix */ cbm_fuzzy_result_t fr = cbm_registry_fuzzy_resolve(reg, "unknownPkg.CreateOrder", "svcC.caller", NULL, NULL, 0); ASSERT_TRUE(fr.ok); ASSERT_STR_EQ(fr.result.qualified_name, "svcA.handlers.CreateOrder"); cbm_registry_free(reg); PASS(); } TEST(registry_fuzzy_nonexistent) { cbm_registry_t *reg = cbm_registry_new(); cbm_registry_add(reg, "CreateOrder", "svcA.handlers.CreateOrder", "Function"); cbm_fuzzy_result_t fr = cbm_registry_fuzzy_resolve(reg, "NonExistent", "svcC.caller", NULL, NULL, 0); ASSERT_FALSE(fr.ok); cbm_registry_free(reg); PASS(); } TEST(registry_fuzzy_multiple_best_by_distance) { cbm_registry_t *reg = cbm_registry_new(); cbm_registry_add(reg, "Process", "svcA.handlers.Process", "Function"); cbm_registry_add(reg, "Process", "svcB.handlers.Process", "Function"); /* Caller in svcA → prefer svcA */ cbm_fuzzy_result_t fr = cbm_registry_fuzzy_resolve(reg, "unknown.Process", "svcA.other", NULL, NULL, 0); ASSERT_TRUE(fr.ok); ASSERT_STR_EQ(fr.result.qualified_name, "svcA.handlers.Process"); /* Caller in svcB → prefer svcB */ fr = cbm_registry_fuzzy_resolve(reg, "unknown.Process", "svcB.other", NULL, NULL, 0); ASSERT_TRUE(fr.ok); ASSERT_STR_EQ(fr.result.qualified_name, "svcB.handlers.Process"); cbm_registry_free(reg); PASS(); } TEST(registry_fuzzy_simple_name_extraction) { cbm_registry_t *reg = cbm_registry_new(); cbm_registry_add(reg, "DoWork", "myproject.utils.DoWork", "Function"); /* Deeply qualified name → extract "DoWork" */ cbm_fuzzy_result_t fr = cbm_registry_fuzzy_resolve(reg, "some.deep.module.DoWork", "myproject.caller", NULL, NULL, 0); ASSERT_TRUE(fr.ok); ASSERT_STR_EQ(fr.result.qualified_name, "myproject.utils.DoWork"); cbm_registry_free(reg); PASS(); } TEST(registry_fuzzy_empty) { cbm_registry_t *reg = cbm_registry_new(); cbm_fuzzy_result_t fr = cbm_registry_fuzzy_resolve(reg, "SomeFunc", "myproject.caller", NULL, NULL, 0); ASSERT_FALSE(fr.ok); cbm_registry_free(reg); PASS(); } TEST(registry_exists) { cbm_registry_t *reg = cbm_registry_new(); cbm_registry_add(reg, "Foo", "pkg.module.Foo", "Function"); cbm_registry_add(reg, "Bar", "pkg.module.Bar", "Method"); ASSERT_TRUE(cbm_registry_exists(reg, "pkg.module.Foo")); ASSERT_TRUE(cbm_registry_exists(reg, "pkg.module.Bar")); ASSERT_FALSE(cbm_registry_exists(reg, "pkg.module.Missing")); ASSERT_FALSE(cbm_registry_exists(reg, "")); cbm_registry_free(reg); PASS(); } TEST(registry_confidence_import_map) { cbm_registry_t *reg = cbm_registry_new(); cbm_registry_add(reg, "Foo", "proj.other.Foo", "Function"); const char *keys[] = {"other"}; const char *vals[] = {"proj.other"}; cbm_resolution_t r = cbm_registry_resolve(reg, "other.Foo", "proj.pkg", keys, vals, 1); ASSERT_STR_EQ(r.qualified_name, "proj.other.Foo"); ASSERT(r.confidence > 0.90 && r.confidence <= 1.0); ASSERT_STR_EQ(r.strategy, "import_map"); cbm_registry_free(reg); PASS(); } TEST(registry_confidence_import_map_suffix) { cbm_registry_t *reg = cbm_registry_new(); cbm_registry_add(reg, "Foo", "proj.other.sub.Foo", "Function"); const char *keys[] = {"other"}; const char *vals[] = {"proj.other"}; cbm_resolution_t r = cbm_registry_resolve(reg, "other.Foo", "proj.pkg", keys, vals, 1); ASSERT_STR_EQ(r.qualified_name, "proj.other.sub.Foo"); ASSERT(r.confidence > 0.80 && r.confidence <= 0.90); ASSERT_STR_EQ(r.strategy, "import_map_suffix"); cbm_registry_free(reg); PASS(); } TEST(registry_confidence_same_module) { cbm_registry_t *reg = cbm_registry_new(); cbm_registry_add(reg, "Foo", "proj.pkg.Foo", "Function"); cbm_resolution_t r = cbm_registry_resolve(reg, "Foo", "proj.pkg", NULL, NULL, 0); ASSERT_STR_EQ(r.qualified_name, "proj.pkg.Foo"); ASSERT(r.confidence > 0.85 && r.confidence <= 0.95); ASSERT_STR_EQ(r.strategy, "same_module"); cbm_registry_free(reg); PASS(); } TEST(registry_confidence_unique_name) { cbm_registry_t *reg = cbm_registry_new(); cbm_registry_add(reg, "Bar", "proj.pkg.Bar", "Function"); cbm_resolution_t r = cbm_registry_resolve(reg, "Bar", "proj.unrelated", NULL, NULL, 0); ASSERT_STR_EQ(r.qualified_name, "proj.pkg.Bar"); ASSERT(r.confidence > 0.70 && r.confidence <= 0.80); ASSERT_STR_EQ(r.strategy, "unique_name"); cbm_registry_free(reg); PASS(); } TEST(registry_confidence_suffix_match) { cbm_registry_t *reg = cbm_registry_new(); cbm_registry_add(reg, "Process", "proj.svcA.Process", "Function"); cbm_registry_add(reg, "Process", "proj.svcB.Process", "Function"); cbm_resolution_t r = cbm_registry_resolve(reg, "Process", "proj.svcA.caller", NULL, NULL, 0); ASSERT_STR_EQ(r.qualified_name, "proj.svcA.Process"); ASSERT(r.confidence > 0.50 && r.confidence <= 0.60); ASSERT_STR_EQ(r.strategy, "suffix_match"); cbm_registry_free(reg); PASS(); } TEST(registry_fuzzy_confidence_single) { cbm_registry_t *reg = cbm_registry_new(); cbm_registry_add(reg, "Handler", "proj.svc.Handler", "Function"); cbm_fuzzy_result_t fr = cbm_registry_fuzzy_resolve(reg, "unknownPkg.Handler", "proj.caller", NULL, NULL, 0); ASSERT_TRUE(fr.ok); ASSERT(fr.result.confidence > 0.35 && fr.result.confidence <= 0.45); ASSERT_STR_EQ(fr.result.strategy, "fuzzy"); cbm_registry_free(reg); PASS(); } TEST(registry_fuzzy_confidence_distance) { cbm_registry_t *reg = cbm_registry_new(); cbm_registry_add(reg, "Process", "proj.svcA.Process", "Function"); cbm_registry_add(reg, "Process", "proj.svcB.Process", "Function"); cbm_fuzzy_result_t fr = cbm_registry_fuzzy_resolve(reg, "unknownPkg.Process", "proj.svcA.other", NULL, NULL, 0); ASSERT_TRUE(fr.ok); ASSERT(fr.result.confidence > 0.25 && fr.result.confidence <= 0.35); ASSERT_STR_EQ(fr.result.strategy, "fuzzy"); cbm_registry_free(reg); PASS(); } TEST(registry_negative_import_rejects) { cbm_registry_t *reg = cbm_registry_new(); cbm_registry_add(reg, "Process", "proj.billing.Process", "Function"); cbm_registry_add(reg, "Process", "proj.handler.Process", "Function"); /* Import only handler's module → should prefer handler */ const char *keys[] = {"handler"}; const char *vals[] = {"proj.handler"}; cbm_resolution_t r = cbm_registry_resolve(reg, "Process", "proj.caller", keys, vals, 1); ASSERT_STR_EQ(r.qualified_name, "proj.handler.Process"); cbm_registry_free(reg); PASS(); } TEST(registry_fuzzy_import_penalty) { cbm_registry_t *reg = cbm_registry_new(); cbm_registry_add(reg, "Handler", "proj.billing.Handler", "Function"); /* Has imports but billing not imported → confidence halved */ const char *keys[] = {"other"}; const char *vals[] = {"proj.other"}; cbm_fuzzy_result_t fr = cbm_registry_fuzzy_resolve(reg, "unknown.Handler", "proj.caller", keys, vals, 1); ASSERT_TRUE(fr.ok); /* 0.40 * 0.5 = 0.20 */ ASSERT(fr.result.confidence > 0.15 && fr.result.confidence <= 0.25); cbm_registry_free(reg); PASS(); } TEST(registry_fuzzy_no_import_map_passthrough) { cbm_registry_t *reg = cbm_registry_new(); cbm_registry_add(reg, "Handler", "proj.billing.Handler", "Function"); /* NULL import map → no penalty, full fuzzy confidence */ cbm_fuzzy_result_t fr = cbm_registry_fuzzy_resolve(reg, "unknown.Handler", "proj.caller", NULL, NULL, 0); ASSERT_TRUE(fr.ok); ASSERT(fr.result.confidence > 0.35 && fr.result.confidence <= 0.45); cbm_registry_free(reg); PASS(); } TEST(registry_find_by_name) { cbm_registry_t *reg = cbm_registry_new(); cbm_registry_add(reg, "Foo", "proj.pkg.Foo", "Function"); cbm_registry_add(reg, "Bar", "proj.pkg.Bar", "Function"); cbm_registry_add(reg, "Foo", "proj.other.Foo", "Function"); cbm_registry_add(reg, "transform", "proj.utils.DataProcessor.transform", "Method"); /* FindByName returns all entries for "Foo" */ const char **foos = NULL; int foos_count = 0; cbm_registry_find_by_name(reg, "Foo", &foos, &foos_count); ASSERT_EQ(foos_count, 2); /* FindByName for unique "Bar" */ const char **bars = NULL; int bars_count = 0; cbm_registry_find_by_name(reg, "Bar", &bars, &bars_count); ASSERT_EQ(bars_count, 1); ASSERT_STR_EQ(bars[0], "proj.pkg.Bar"); /* FindByName for "transform" */ const char **transforms = NULL; int trans_count = 0; cbm_registry_find_by_name(reg, "transform", &transforms, &trans_count); ASSERT_EQ(trans_count, 1); ASSERT_STR_EQ(transforms[0], "proj.utils.DataProcessor.transform"); /* label_of */ ASSERT_STR_EQ(cbm_registry_label_of(reg, "proj.utils.DataProcessor.transform"), "Method"); ASSERT_STR_EQ(cbm_registry_label_of(reg, "proj.pkg.Foo"), "Function"); /* Total size */ ASSERT_EQ(cbm_registry_size(reg), 4); /* Resolve same-module */ cbm_resolution_t r = cbm_registry_resolve(reg, "Foo", "proj.pkg", NULL, NULL, 0); ASSERT_STR_EQ(r.qualified_name, "proj.pkg.Foo"); /* Resolve via import map */ const char *keys[] = {"other"}; const char *vals[] = {"proj.other"}; r = cbm_registry_resolve(reg, "other.Foo", "proj.pkg", keys, vals, 1); ASSERT_STR_EQ(r.qualified_name, "proj.other.Foo"); /* Resolve unique name */ r = cbm_registry_resolve(reg, "Bar", "proj.unrelated", NULL, NULL, 0); ASSERT_STR_EQ(r.qualified_name, "proj.pkg.Bar"); cbm_registry_free(reg); PASS(); } TEST(registry_confidence_band) { ASSERT_STR_EQ(cbm_confidence_band(0.95), "high"); ASSERT_STR_EQ(cbm_confidence_band(0.70), "high"); ASSERT_STR_EQ(cbm_confidence_band(0.55), "medium"); ASSERT_STR_EQ(cbm_confidence_band(0.45), "medium"); ASSERT_STR_EQ(cbm_confidence_band(0.40), "speculative"); ASSERT_STR_EQ(cbm_confidence_band(0.25), "speculative"); ASSERT_STR_EQ(cbm_confidence_band(0.20), ""); ASSERT_STR_EQ(cbm_confidence_band(0.0), ""); PASS(); } TEST(infra_qn_helper) { /* Port of TestInfraQN */ /* Regular infra file → __infra__ suffix */ char *qn = cbm_infra_qn("myproject", "docker-images/service/Dockerfile", "dockerfile", NULL); ASSERT_NOT_NULL(qn); ASSERT(strstr(qn, ".__infra__") != NULL); free(qn); /* Compose service → ::service_name suffix */ qn = cbm_infra_qn("myproject", "docker-compose.yml", "compose-service", "web"); ASSERT_NOT_NULL(qn); ASSERT(strstr(qn, "::web") != NULL); free(qn); PASS(); } /* ── Infrascan integration tests ────────────────────────────────── */ TEST(infra_pipeline_integration) { /* Port of TestPassInfraFilesIntegration (Dockerfile + .env parts). * Tests parse functions on source text (pipeline infrascan pass not * wired yet — compose YAML also blocked on YAML parser). */ /* Parse Dockerfile */ cbm_dockerfile_result_t dr; ASSERT_EQ(cbm_parse_dockerfile_source("FROM alpine:3.19\nEXPOSE 8080\n", &dr), 0); ASSERT_STR_EQ(dr.base_image, "alpine:3.19"); ASSERT_GTE(dr.port_count, 1); /* Parse .env */ cbm_dotenv_result_t er; ASSERT_EQ(cbm_parse_dotenv_source("APP_PORT=8080\nDEBUG=true\n", &er), 0); ASSERT_GTE(er.env_count, 1); /* APP_PORT should be present */ bool found_port = false; for (int i = 0; i < er.env_count; i++) { if (strcmp(er.env_vars[i].key, "APP_PORT") == 0 && strcmp(er.env_vars[i].value, "8080") == 0) found_port = true; } ASSERT_TRUE(found_port); PASS(); } TEST(infra_pipeline_idempotent) { /* Port of TestPassInfraFilesIdempotent: * Parsing same source twice should produce identical results. */ const char *src = "FROM alpine:3.19\nEXPOSE 8080\nENV PORT=8080\n"; cbm_dockerfile_result_t r1, r2; ASSERT_EQ(cbm_parse_dockerfile_source(src, &r1), 0); ASSERT_EQ(cbm_parse_dockerfile_source(src, &r2), 0); ASSERT_STR_EQ(r1.base_image, r2.base_image); ASSERT_EQ(r1.port_count, r2.port_count); ASSERT_EQ(r1.env_count, r2.env_count); PASS(); } /* ── K8s / Kustomize extraction tests ──────────────────────────── */ TEST(k8s_extract_kustomize) { const char *src = "apiVersion: kustomize.config.k8s.io/v1beta1\n" "kind: Kustomization\n" "resources:\n" " - deployment.yaml\n" " - service.yaml\n"; CBMFileResult *r = cbm_extract_file(src, (int)strlen(src), CBM_LANG_KUSTOMIZE, "myproj", "base/kustomization.yaml", 0, NULL, NULL); ASSERT(r != NULL); ASSERT_GTE(r->imports.count, 2); bool found_deploy = false, found_svc = false; for (int i = 0; i < r->imports.count; i++) { if (r->imports.items[i].module_path && strcmp(r->imports.items[i].module_path, "deployment.yaml") == 0) found_deploy = true; if (r->imports.items[i].module_path && strcmp(r->imports.items[i].module_path, "service.yaml") == 0) found_svc = true; } ASSERT_TRUE(found_deploy); ASSERT_TRUE(found_svc); cbm_free_result(r); PASS(); } TEST(k8s_extract_manifest) { const char *src = "apiVersion: apps/v1\n" "kind: Deployment\n" "metadata:\n" " name: my-app\n" " namespace: production\n"; CBMFileResult *r = cbm_extract_file(src, (int)strlen(src), CBM_LANG_K8S, "myproj", "k8s/deployment.yaml", 0, NULL, NULL); ASSERT(r != NULL); ASSERT_GTE(r->defs.count, 1); bool found_resource = false; for (int d = 0; d < r->defs.count; d++) { if (r->defs.items[d].label && strcmp(r->defs.items[d].label, "Resource") == 0 && r->defs.items[d].name && strstr(r->defs.items[d].name, "Deployment") != NULL) found_resource = true; } ASSERT_TRUE(found_resource); cbm_free_result(r); PASS(); } TEST(k8s_extract_manifest_no_name) { const char *src = "apiVersion: apps/v1\nkind: Deployment\n"; CBMFileResult *r = cbm_extract_file(src, (int)strlen(src), CBM_LANG_K8S, "myproj", "k8s/deploy.yaml", 0, NULL, NULL); ASSERT(r != NULL); /* No crash — defs count may be 0 because metadata.name is absent */ ASSERT(!r->has_error); cbm_free_result(r); PASS(); } TEST(k8s_extract_manifest_multidoc) { /* Two-document YAML separated by "---". * extract_k8s_manifest contains a "break" after the first successful push, * so it processes only the first document that has both kind and * metadata.name. This test pins that behaviour: the first document's * resource must be present and no crash must occur. * * Note: with some tree-sitter YAML grammar versions the root stream may * expose both documents as siblings; the break still fires after the first * successful def push, so defs.count must be exactly 1. */ const char *src = "apiVersion: apps/v1\n" "kind: Deployment\n" "metadata:\n" " name: my-app\n" "---\n" "apiVersion: v1\n" "kind: Service\n" "metadata:\n" " name: my-svc\n"; CBMFileResult *r = cbm_extract_file(src, (int)strlen(src), CBM_LANG_K8S, "myproj", "k8s/multi.yaml", 0, NULL, NULL); ASSERT(r != NULL); ASSERT(!r->has_error); /* First document's resource must be present */ int found = 0; for (int i = 0; i < r->defs.count; i++) { if (r->defs.items[i].label && strcmp(r->defs.items[i].label, "Resource") == 0 && r->defs.items[i].name && strcmp(r->defs.items[i].name, "Deployment/my-app") == 0) { found = 1; } } ASSERT(found); /* At least one def, no more than one (only first document processed) */ ASSERT(r->defs.count >= 1); cbm_free_result(r); PASS(); } /* ── Envscan tests (port of envscan_test.go) ───────────────────── */ /* Helper: write a file inside a temp dir */ static void write_temp_file(const char *dir, const char *name, const char *content) { char path[512]; /* Create subdirectories if needed */ snprintf(path, sizeof(path), "%s/%s", dir, name); char *slash = strrchr(path, '/'); if (slash) { char parent[512]; size_t plen = slash - path; memcpy(parent, path, plen); parent[plen] = '\0'; /* mkdir -p (simple version, one level) */ cbm_mkdir(parent); } FILE *f = fopen(path, "w"); if (f) { fputs(content, f); fclose(f); } } /* Helper: find binding by key in results */ static const cbm_env_binding_t *find_binding_by_key(const cbm_env_binding_t *bindings, int count, const char *key) { for (int i = 0; i < count; i++) { if (strcmp(bindings[i].key, key) == 0) return &bindings[i]; } return NULL; } /* Helper: find binding by value in results */ static int has_binding_value(const cbm_env_binding_t *bindings, int count, const char *value) { for (int i = 0; i < count; i++) { if (strcmp(bindings[i].value, value) == 0) return 1; } return 0; } TEST(envscan_dockerfile_env_urls) { char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_envscan_dock_XXXXXX"); if (!cbm_mkdtemp(tmpdir)) FAIL("tmpdir"); write_temp_file(tmpdir, "Dockerfile", "FROM python:3.9-slim\n" "ENV ORDER_URL=https://api.example.com/api/orders\n" "ENV DB_HOST=localhost\n" "ARG WEBHOOK_URL=https://hooks.example.com/webhook\n"); cbm_env_binding_t bindings[32]; int count = cbm_scan_project_env_urls(tmpdir, bindings, 32); ASSERT_NOT_NULL(find_binding_by_key(bindings, count, "ORDER_URL")); ASSERT_STR_EQ(find_binding_by_key(bindings, count, "ORDER_URL")->value, "https://api.example.com/api/orders"); ASSERT_NOT_NULL(find_binding_by_key(bindings, count, "WEBHOOK_URL")); ASSERT_STR_EQ(find_binding_by_key(bindings, count, "WEBHOOK_URL")->value, "https://hooks.example.com/webhook"); /* DB_HOST=localhost is NOT a URL → should be absent */ ASSERT_TRUE(find_binding_by_key(bindings, count, "DB_HOST") == NULL); th_rmtree(tmpdir); PASS(); } TEST(envscan_shell_env_urls) { char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_envscan_sh_XXXXXX"); if (!cbm_mkdtemp(tmpdir)) FAIL("tmpdir"); write_temp_file(tmpdir, "setup.sh", "#!/bin/bash\n" "export DB_URL=\"https://db.example.com/api/sync\"\n" "APP_NAME=\"my-service\"\n" "CALLBACK_URL=https://hooks.example.com/notify\n"); cbm_env_binding_t bindings[32]; int count = cbm_scan_project_env_urls(tmpdir, bindings, 32); ASSERT_NOT_NULL(find_binding_by_key(bindings, count, "DB_URL")); ASSERT_STR_EQ(find_binding_by_key(bindings, count, "DB_URL")->value, "https://db.example.com/api/sync"); ASSERT_NOT_NULL(find_binding_by_key(bindings, count, "CALLBACK_URL")); /* APP_NAME is NOT a URL → absent */ ASSERT_TRUE(find_binding_by_key(bindings, count, "APP_NAME") == NULL); th_rmtree(tmpdir); PASS(); } TEST(envscan_env_file_urls) { char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_envscan_env_XXXXXX"); if (!cbm_mkdtemp(tmpdir)) FAIL("tmpdir"); write_temp_file(tmpdir, ".env", "\nAPI_URL=https://api.example.com/v1\n" "DEBUG=true\n" "SERVICE_URL=https://service.example.com/api\n"); cbm_env_binding_t bindings[32]; int count = cbm_scan_project_env_urls(tmpdir, bindings, 32); ASSERT_NOT_NULL(find_binding_by_key(bindings, count, "API_URL")); ASSERT_STR_EQ(find_binding_by_key(bindings, count, "API_URL")->value, "https://api.example.com/v1"); ASSERT_NOT_NULL(find_binding_by_key(bindings, count, "SERVICE_URL")); /* DEBUG=true is NOT a URL */ ASSERT_TRUE(find_binding_by_key(bindings, count, "DEBUG") == NULL); th_rmtree(tmpdir); PASS(); } TEST(envscan_toml_urls) { char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_envscan_toml_XXXXXX"); if (!cbm_mkdtemp(tmpdir)) FAIL("tmpdir"); write_temp_file(tmpdir, "config.toml", "[service]\n" "base_url = \"https://api.example.com\"\n" "name = \"my-service\"\n" "callback_url = \"https://hooks.example.com/notify\"\n"); cbm_env_binding_t bindings[32]; int count = cbm_scan_project_env_urls(tmpdir, bindings, 32); ASSERT_NOT_NULL(find_binding_by_key(bindings, count, "base_url")); ASSERT_STR_EQ(find_binding_by_key(bindings, count, "base_url")->value, "https://api.example.com"); ASSERT_NOT_NULL(find_binding_by_key(bindings, count, "callback_url")); /* name="my-service" is NOT a URL */ ASSERT_TRUE(find_binding_by_key(bindings, count, "name") == NULL); th_rmtree(tmpdir); PASS(); } TEST(envscan_yaml_urls) { char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_envscan_yaml_XXXXXX"); if (!cbm_mkdtemp(tmpdir)) FAIL("tmpdir"); write_temp_file(tmpdir, "config.yaml", "service:\n" " service_url: \"https://api.internal.com/api/process\"\n" " timeout: 30\n" " callback_url: \"https://hooks.internal.com/callback\"\n"); cbm_env_binding_t bindings[32]; int count = cbm_scan_project_env_urls(tmpdir, bindings, 32); ASSERT_NOT_NULL(find_binding_by_key(bindings, count, "service_url")); ASSERT_STR_EQ(find_binding_by_key(bindings, count, "service_url")->value, "https://api.internal.com/api/process"); ASSERT_NOT_NULL(find_binding_by_key(bindings, count, "callback_url")); th_rmtree(tmpdir); PASS(); } TEST(envscan_terraform_urls) { char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_envscan_tf_XXXXXX"); if (!cbm_mkdtemp(tmpdir)) FAIL("tmpdir"); write_temp_file(tmpdir, "variables.tf", "variable \"webhook_url\" {\n" " description = \"Webhook endpoint\"\n" " default = \"https://api.example.com/webhook\"\n" "}\n\n" "variable \"region\" {\n" " default = \"us-east-1\"\n" "}\n"); cbm_env_binding_t bindings[32]; int count = cbm_scan_project_env_urls(tmpdir, bindings, 32); ASSERT_GTE(count, 1); ASSERT_TRUE(has_binding_value(bindings, count, "https://api.example.com/webhook")); th_rmtree(tmpdir); PASS(); } TEST(envscan_properties_urls) { char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_envscan_prop_XXXXXX"); if (!cbm_mkdtemp(tmpdir)) FAIL("tmpdir"); write_temp_file(tmpdir, "app.properties", "api.url=https://api.example.com/health\n" "app.name=myapp\n" "service.endpoint=https://service.example.com/api\n"); cbm_env_binding_t bindings[32]; int count = cbm_scan_project_env_urls(tmpdir, bindings, 32); ASSERT_TRUE(has_binding_value(bindings, count, "https://api.example.com/health")); ASSERT_TRUE(has_binding_value(bindings, count, "https://service.example.com/api")); th_rmtree(tmpdir); PASS(); } TEST(envscan_secret_key_exclusion) { char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_envscan_skey_XXXXXX"); if (!cbm_mkdtemp(tmpdir)) FAIL("tmpdir"); write_temp_file(tmpdir, "Dockerfile", "FROM node:18\n" "ENV SECRET_TOKEN=https://api.example.com/api\n" "ENV API_KEY=https://api.example.com/v1\n" "ENV PASSWORD=https://auth.example.com/login\n" "ENV NORMAL_URL=https://api.example.com/orders\n"); cbm_env_binding_t bindings[32]; int count = cbm_scan_project_env_urls(tmpdir, bindings, 32); /* Secret keys should be excluded */ ASSERT_TRUE(find_binding_by_key(bindings, count, "SECRET_TOKEN") == NULL); ASSERT_TRUE(find_binding_by_key(bindings, count, "API_KEY") == NULL); ASSERT_TRUE(find_binding_by_key(bindings, count, "PASSWORD") == NULL); /* Normal key should be present */ ASSERT_NOT_NULL(find_binding_by_key(bindings, count, "NORMAL_URL")); th_rmtree(tmpdir); PASS(); } TEST(envscan_secret_value_exclusion) { char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_envscan_sval_XXXXXX"); if (!cbm_mkdtemp(tmpdir)) FAIL("tmpdir"); write_temp_file( tmpdir, "deploy.sh", "#!/bin/bash\n" "export GH_URL=\"https://ghp_FAKEFAKEFAKEFAKEFAKEFAKEFAKEFAKEFAKE@github.com/repo\"\n" "export NORMAL_ENDPOINT=\"https://api.example.com/orders\"\n"); cbm_env_binding_t bindings[32]; int count = cbm_scan_project_env_urls(tmpdir, bindings, 32); /* ghp_ token URL should be excluded */ ASSERT_TRUE(find_binding_by_key(bindings, count, "GH_URL") == NULL); /* Normal URL should be present */ ASSERT_NOT_NULL(find_binding_by_key(bindings, count, "NORMAL_ENDPOINT")); th_rmtree(tmpdir); PASS(); } TEST(envscan_secret_file_exclusion) { char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_envscan_sfile_XXXXXX"); if (!cbm_mkdtemp(tmpdir)) FAIL("tmpdir"); /* Secret file should be skipped */ write_temp_file(tmpdir, "credentials.sh", "#!/bin/bash\nexport API_URL=\"https://api.example.com/v1\"\n"); /* Normal file should be scanned */ write_temp_file(tmpdir, "setup.sh", "#!/bin/bash\nexport API_URL=\"https://api.example.com/v1\"\n"); cbm_env_binding_t bindings[32]; int count = cbm_scan_project_env_urls(tmpdir, bindings, 32); /* Should find binding from setup.sh but not credentials.sh */ int from_credentials = 0; int from_setup = 0; for (int i = 0; i < count; i++) { if (strcmp(bindings[i].file_path, "credentials.sh") == 0) from_credentials = 1; if (strcmp(bindings[i].file_path, "setup.sh") == 0) from_setup = 1; } ASSERT_EQ(from_credentials, 0); ASSERT_EQ(from_setup, 1); th_rmtree(tmpdir); PASS(); } TEST(envscan_skips_ignored_dirs) { char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_envscan_ign_XXXXXX"); if (!cbm_mkdtemp(tmpdir)) FAIL("tmpdir"); /* File inside .git should be skipped */ char gitdir[512]; snprintf(gitdir, sizeof(gitdir), "%s/.git", tmpdir); cbm_mkdir(gitdir); write_temp_file(tmpdir, ".git/config.sh", "#!/bin/bash\nexport API_URL=\"https://api.example.com/v1\"\n"); /* File inside node_modules should be skipped */ char nmdir[512]; snprintf(nmdir, sizeof(nmdir), "%s/node_modules", tmpdir); cbm_mkdir(nmdir); char nmpkg[512]; snprintf(nmpkg, sizeof(nmpkg), "%s/node_modules/pkg", tmpdir); cbm_mkdir(nmpkg); write_temp_file(tmpdir, "node_modules/pkg/config.sh", "#!/bin/bash\nexport API_URL=\"https://api.example.com/v1\"\n"); /* File at root level should be scanned */ write_temp_file(tmpdir, "deploy.sh", "#!/bin/bash\nexport API_URL=\"https://api.example.com/v1\"\n"); cbm_env_binding_t bindings[32]; int count = cbm_scan_project_env_urls(tmpdir, bindings, 32); int from_git = 0, from_nm = 0, from_root = 0; for (int i = 0; i < count; i++) { if (strncmp(bindings[i].file_path, ".git/", 5) == 0) from_git = 1; if (strncmp(bindings[i].file_path, "node_modules/", 13) == 0) from_nm = 1; if (strcmp(bindings[i].file_path, "deploy.sh") == 0) from_root = 1; } ASSERT_EQ(from_git, 0); ASSERT_EQ(from_nm, 0); ASSERT_EQ(from_root, 1); th_rmtree(tmpdir); PASS(); } TEST(envscan_non_url_values_skipped) { char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_envscan_nurl_XXXXXX"); if (!cbm_mkdtemp(tmpdir)) FAIL("tmpdir"); write_temp_file(tmpdir, "Dockerfile", "FROM python:3.9\n" "ENV APP_NAME=my-service\n" "ENV PORT=8080\n" "ENV DEBUG=true\n" "ENV LOG_LEVEL=info\n"); write_temp_file(tmpdir, "config.sh", "#!/bin/bash\n" "export REGION=\"us-east-1\"\n" "export COUNT=42\n"); cbm_env_binding_t bindings[32]; int count = cbm_scan_project_env_urls(tmpdir, bindings, 32); ASSERT_EQ(count, 0); th_rmtree(tmpdir); PASS(); } /* ── Discovery-exclusion plumbing in auxiliary repo walks (#792) ── */ /* Boundary semantics of the shared exclusion predicate: anchored at the * repo root, matches the excluded dir itself and its subtree, but never * sibling names sharing a prefix. Regression guard for issue #792. */ TEST(pipeline_relpath_excluded_boundary) { char *excluded[] = {(char *)"vendor_big", (char *)"packages/big"}; /* Exact match and subtree paths are excluded. */ ASSERT_TRUE(cbm_pipeline_relpath_is_excluded("vendor_big", excluded, 2)); ASSERT_TRUE(cbm_pipeline_relpath_is_excluded("vendor_big/lib/package.json", excluded, 2)); ASSERT_TRUE(cbm_pipeline_relpath_is_excluded("packages/big", excluded, 2)); ASSERT_TRUE(cbm_pipeline_relpath_is_excluded("packages/big/src/x.ts", excluded, 2)); /* Sibling names sharing the prefix are NOT excluded ('/'-boundary). */ ASSERT_FALSE(cbm_pipeline_relpath_is_excluded("vendor_bigger", excluded, 2)); ASSERT_FALSE(cbm_pipeline_relpath_is_excluded("vendor", excluded, 2)); ASSERT_FALSE(cbm_pipeline_relpath_is_excluded("packages/bigger/x.ts", excluded, 2)); /* Exclusions are root-anchored prefixes, not substring matches. */ ASSERT_FALSE(cbm_pipeline_relpath_is_excluded("src/vendor_big/x.c", excluded, 2)); /* NULL / empty safety. */ ASSERT_FALSE(cbm_pipeline_relpath_is_excluded(NULL, excluded, 2)); ASSERT_FALSE(cbm_pipeline_relpath_is_excluded("", excluded, 2)); ASSERT_FALSE(cbm_pipeline_relpath_is_excluded("vendor_big", NULL, 0)); ASSERT_FALSE(cbm_pipeline_relpath_is_excluded("vendor_big", excluded, 0)); char *with_empty[] = {(char *)""}; ASSERT_FALSE(cbm_pipeline_relpath_is_excluded("vendor_big", with_empty, 1)); PASS(); } /* Helper: does the entries array contain a package with this name? */ static int pkg_entries_has_name(const cbm_pkg_entries_t *e, const char *name) { for (int i = 0; i < e->count; i++) { if (e->items[i].pkg_name && strcmp(e->items[i].pkg_name, name) == 0) return 1; } return 0; } /* The pkgmap repo walk must honor discovery exclusions (issue #792: a * gitignored huge subtree kept the pkgmap walk busy for 15 minutes). * Control run first (no exclusions → BOTH manifests parsed) so the * exclusion assertion below cannot pass vacuously. */ TEST(pkgmap_scan_repo_honors_discovery_exclusions) { char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_pkgmap_excl_XXXXXX"); if (!cbm_mkdtemp(tmpdir)) FAIL("tmpdir"); char dir[512]; snprintf(dir, sizeof(dir), "%s/packages", tmpdir); cbm_mkdir(dir); snprintf(dir, sizeof(dir), "%s/packages/app", tmpdir); cbm_mkdir(dir); snprintf(dir, sizeof(dir), "%s/vendor_big", tmpdir); cbm_mkdir(dir); snprintf(dir, sizeof(dir), "%s/vendor_big/lib", tmpdir); cbm_mkdir(dir); write_temp_file(tmpdir, "packages/app/package.json", "{\"name\":\"@org/app\",\"main\":\"index.js\"}\n"); write_temp_file(tmpdir, "vendor_big/lib/package.json", "{\"name\":\"@org/vendored\",\"main\":\"index.js\"}\n"); /* Control: NULL exclusion list — the walk reaches and parses BOTH * manifests (proves the excluded one is reachable + parseable). */ cbm_pkg_entries_t control; cbm_pkg_entries_init(&control); cbm_pkgmap_scan_repo(tmpdir, &control, NULL, 0); ASSERT_TRUE(pkg_entries_has_name(&control, "@org/app")); ASSERT_TRUE(pkg_entries_has_name(&control, "@org/vendored")); cbm_pkg_entries_free(&control); /* With vendor_big excluded (as discovery reports for a gitignored * subtree): the walk must not descend into it. */ char *excluded[] = {(char *)"vendor_big"}; cbm_pkg_entries_t entries; cbm_pkg_entries_init(&entries); cbm_pkgmap_scan_repo(tmpdir, &entries, excluded, 1); ASSERT_TRUE(pkg_entries_has_name(&entries, "@org/app")); ASSERT_FALSE(pkg_entries_has_name(&entries, "@org/vendored")); cbm_pkg_entries_free(&entries); th_rmtree(tmpdir); PASS(); } /* The env-URL walk must honor discovery exclusions the same way (#792). * Control run first via the NULL-exclusion wrapper so the exclusion * assertion cannot pass vacuously. */ TEST(envscan_walk_honors_discovery_exclusions) { char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_envscan_excl_XXXXXX"); if (!cbm_mkdtemp(tmpdir)) FAIL("tmpdir"); char dir[512]; snprintf(dir, sizeof(dir), "%s/big_generated", tmpdir); cbm_mkdir(dir); write_temp_file(tmpdir, "deploy.sh", "#!/bin/bash\nexport CONTROL_URL=\"https://api.example.com/v1\"\n"); write_temp_file(tmpdir, "big_generated/env.sh", "#!/bin/bash\nexport EXCLUDED_URL=\"https://excluded.example.com/v1\"\n"); /* Control: the NULL-exclusion wrapper sees both bindings. */ cbm_env_binding_t bindings[32]; int count = cbm_scan_project_env_urls(tmpdir, bindings, 32); ASSERT_NOT_NULL(find_binding_by_key(bindings, count, "CONTROL_URL")); ASSERT_NOT_NULL(find_binding_by_key(bindings, count, "EXCLUDED_URL")); /* With big_generated excluded, its binding must disappear. */ char *excluded[] = {(char *)"big_generated"}; count = cbm_scan_project_env_urls_excluded(tmpdir, bindings, 32, excluded, 1); ASSERT_NOT_NULL(find_binding_by_key(bindings, count, "CONTROL_URL")); ASSERT_TRUE(find_binding_by_key(bindings, count, "EXCLUDED_URL") == NULL); th_rmtree(tmpdir); PASS(); } /* ── Git history tests (port of githistory_test.go) ────────────── */ /* Port of Go TestIsTrackableFile from githistory_test.go */ TEST(githistory_is_trackable_file) { /* Source files — trackable */ ASSERT_TRUE(cbm_is_trackable_file("main.go")); ASSERT_TRUE(cbm_is_trackable_file("src/app.py")); ASSERT_TRUE(cbm_is_trackable_file("README.md")); /* node_modules — not trackable */ ASSERT_FALSE(cbm_is_trackable_file("node_modules/foo/bar.js")); /* vendor — not trackable */ ASSERT_FALSE(cbm_is_trackable_file("vendor/lib/dep.go")); /* Lock files — not trackable */ ASSERT_FALSE(cbm_is_trackable_file("package-lock.json")); ASSERT_FALSE(cbm_is_trackable_file("go.sum")); /* Binary/assets — not trackable */ ASSERT_FALSE(cbm_is_trackable_file("image.png")); /* .git directory — not trackable */ ASSERT_FALSE(cbm_is_trackable_file(".git/config")); /* __pycache__ — not trackable */ ASSERT_FALSE(cbm_is_trackable_file("__pycache__/mod.pyc")); /* Minified files — not trackable */ ASSERT_FALSE(cbm_is_trackable_file("src/style.min.css")); PASS(); } /* Port of Go TestComputeChangeCoupling from githistory_test.go */ TEST(githistory_compute_change_coupling) { /* 5 commits: * aaa: a.go, b.go, c.go * bbb: a.go, b.go * ccc: a.go, b.go * ddd: a.go, c.go * eee: d.go, e.go */ char *files_aaa[] = {"a.go", "b.go", "c.go"}; char *files_bbb[] = {"a.go", "b.go"}; char *files_ccc[] = {"a.go", "b.go"}; char *files_ddd[] = {"a.go", "c.go"}; char *files_eee[] = {"d.go", "e.go"}; cbm_commit_files_t commits[5] = { {files_aaa, 3, 0}, {files_bbb, 2, 0}, {files_ccc, 2, 0}, {files_ddd, 2, 0}, {files_eee, 2, 0}, }; cbm_change_coupling_t out[100]; int count = cbm_compute_change_coupling(commits, 5, out, 100); /* a.go + b.go co-change 3 times → should be in results */ bool found_ab = false; for (int i = 0; i < count; i++) { if ((strcmp(out[i].file_a, "a.go") == 0 && strcmp(out[i].file_b, "b.go") == 0) || (strcmp(out[i].file_a, "b.go") == 0 && strcmp(out[i].file_b, "a.go") == 0)) { found_ab = true; ASSERT_EQ(out[i].co_change_count, 3); ASSERT(out[i].coupling_score >= 0.9); } } ASSERT_TRUE(found_ab); /* d.go + e.go co-change only 1 time → below threshold of 3 */ for (int i = 0; i < count; i++) { if (strcmp(out[i].file_a, "d.go") == 0 || strcmp(out[i].file_b, "d.go") == 0) { ASSERT(0); /* d.go should not appear */ } } PASS(); } /* Port of Go TestComputeChangeCouplingSkipsLargeCommits from githistory_test.go */ TEST(githistory_coupling_skips_large_commits) { /* 25 files in one commit → exceeds 20-file threshold */ char *files[25]; char bufs[25][32]; for (int i = 0; i < 25; i++) { snprintf(bufs[i], sizeof(bufs[i]), "file%d.go", i); files[i] = bufs[i]; } cbm_commit_files_t commits[1] = {{files, 25, 0}}; cbm_change_coupling_t out[100]; int count = cbm_compute_change_coupling(commits, 1, out, 100); ASSERT_EQ(count, 0); PASS(); } /* Port of Go TestComputeChangeCouplingLimitsTo100 from githistory_test.go */ TEST(githistory_coupling_limits_output) { /* Generate many small commits to create >100 couplings. * 50 files, each pair committed 3 times. max_out=100. */ int idx = 0; char *pair_files[2450][2]; /* 50*49/2 pairs * 3 repetitions = 3675 commits */ char pair_bufs[2450][2][32]; cbm_commit_files_t commits[3675]; int ci = 0; for (int i = 0; i < 50 && ci < 3675; i++) { for (int j = i + 1; j < 50 && ci < 3675; j++) { for (int k = 0; k < 3 && ci < 3675; k++) { snprintf(pair_bufs[idx][0], 32, "f%d.go", i); snprintf(pair_bufs[idx][1], 32, "f%d.go", j); pair_files[idx][0] = pair_bufs[idx][0]; pair_files[idx][1] = pair_bufs[idx][1]; commits[ci].files = pair_files[idx]; commits[ci].count = 2; ci++; idx++; if (idx >= 2450) idx = 0; /* reuse buffer space */ } } } cbm_change_coupling_t out[200]; int count = cbm_compute_change_coupling(commits, ci, out, 100); ASSERT(count <= 100); PASS(); } /* Port of Go TestIsImportReachable from resolver_test.go */ TEST(registry_is_import_reachable) { const char *import_vals[] = {"proj.handler", "proj.shared.utils"}; /* Exact match: proj.handler.Process → true */ ASSERT_TRUE(cbm_registry_is_import_reachable("proj.handler.Process", import_vals, 2)); /* Sub-package: proj.handler.sub.Process → true (handler contains handler) */ ASSERT_TRUE(cbm_registry_is_import_reachable("proj.handler.sub.Process", import_vals, 2)); /* Nested match: proj.shared.utils.Helper → true */ ASSERT_TRUE(cbm_registry_is_import_reachable("proj.shared.utils.Helper", import_vals, 2)); /* Unrelated: proj.billing.Process → false */ ASSERT_FALSE(cbm_registry_is_import_reachable("proj.billing.Process", import_vals, 2)); /* Completely unrelated: unrelated.pkg.Func → false */ ASSERT_FALSE(cbm_registry_is_import_reachable("unrelated.pkg.Func", import_vals, 2)); PASS(); } /* Port of FindEndingWith portion from Go TestFunctionRegistry in pipeline_test.go */ TEST(registry_find_ending_with) { cbm_registry_t *reg = cbm_registry_new(); cbm_registry_add(reg, "Foo", "proj.pkg.Foo", "Function"); cbm_registry_add(reg, "Bar", "proj.pkg.Bar", "Function"); cbm_registry_add(reg, "Foo", "proj.other.Foo", "Function"); cbm_registry_add(reg, "transform", "proj.utils.DataProcessor.transform", "Method"); /* FindEndingWith "DataProcessor.transform" → 1 match */ const char **matches = NULL; int count = cbm_registry_find_ending_with(reg, "DataProcessor.transform", &matches); ASSERT_EQ(count, 1); ASSERT_STR_EQ(matches[0], "proj.utils.DataProcessor.transform"); free(matches); /* FindEndingWith "Foo" → 2 matches */ matches = NULL; count = cbm_registry_find_ending_with(reg, "Foo", &matches); ASSERT_EQ(count, 2); /* Both should be present (order may vary) */ bool found_pkg = false, found_other = false; for (int i = 0; i < count; i++) { if (strcmp(matches[i], "proj.pkg.Foo") == 0) found_pkg = true; if (strcmp(matches[i], "proj.other.Foo") == 0) found_other = true; } ASSERT_TRUE(found_pkg); ASSERT_TRUE(found_other); free(matches); /* FindEndingWith "Nonexistent" → 0 matches */ matches = NULL; count = cbm_registry_find_ending_with(reg, "Nonexistent", &matches); ASSERT_EQ(count, 0); cbm_registry_free(reg); PASS(); } /* ═══════════════════════════════════════════════════════════════════ * Incremental reindex tests * ═══════════════════════════════════════════════════════════════════ */ /* Helper: create a simple 2-file Go project for incremental tests */ static char g_incr_tmpdir[256]; static char g_incr_dbpath[512]; static int setup_incremental_repo(void) { snprintf(g_incr_tmpdir, sizeof(g_incr_tmpdir), "/tmp/cbm_incr_XXXXXX"); if (!cbm_mkdtemp(g_incr_tmpdir)) { return -1; } snprintf(g_incr_dbpath, sizeof(g_incr_dbpath), "%s/test.db", g_incr_tmpdir); char path[512]; FILE *f; /* main.go — calls Helper() */ snprintf(path, sizeof(path), "%s/main.go", g_incr_tmpdir); f = fopen(path, "w"); if (!f) { return -1; } fprintf(f, "package main\n\nfunc main() {\n\tHelper()\n}\n"); fclose(f); /* helper.go — defines Helper() */ snprintf(path, sizeof(path), "%s/helper.go", g_incr_tmpdir); f = fopen(path, "w"); if (!f) { return -1; } fprintf(f, "package main\n\nfunc Helper() string {\n\treturn \"hello\"\n}\n"); fclose(f); return 0; } static void cleanup_incremental_repo(void) { th_rmtree(g_incr_tmpdir); } /* ═══════════════════════════════════════════════════════════════════ * FastAPI Depends() edge tracking (PR #66, fix #27) * ═══════════════════════════════════════════════════════════════════ */ TEST(pipeline_fastapi_depends_edges) { /* Depends(get_current_user) should produce a CALLS edge from the * endpoint to the dependency function. */ const char *files[] = {"auth.py", "routes.py"}; const char *contents[] = {/* auth.py: defines get_current_user */ "def get_current_user(token: str):\n" " return decode_token(token)\n", /* routes.py: endpoint depends on get_current_user */ "from fastapi import Depends\n" "from auth import get_current_user\n\n" "def get_profile(user = Depends(get_current_user)):\n" " return {\"user\": user}\n"}; if (setup_lang_repo(files, contents, 2) != 0) { FAIL("tmpdir"); } char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_lang_tmpdir, db, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_store_t *s = cbm_store_open_path(db); ASSERT_NOT_NULL(s); const char *proj = cbm_pipeline_project_name(p); /* Check CALLS edges for fastapi_depends strategy */ cbm_edge_t *edges = NULL; int edge_count = 0; cbm_store_find_edges_by_type(s, proj, "CALLS", &edges, &edge_count); bool found_depends_edge = false; for (int i = 0; i < edge_count; i++) { if (edges[i].properties_json && strstr(edges[i].properties_json, "fastapi_depends")) { found_depends_edge = true; break; } } if (edges) { cbm_store_free_edges(edges, edge_count); } ASSERT_TRUE(found_depends_edge); cbm_store_close(s); cbm_pipeline_free(p); teardown_lang_repo(); PASS(); } /* DLL resolve test removed — feature removed due to Windows Defender * false positive (Wacatac.B!ml). See issue #89. */ /* ═══════════════════════════════════════════════════════════════════ * Incremental reindex * ═══════════════════════════════════════════════════════════════════ */ TEST(incremental_full_then_noop) { /* Full index, then re-run → should detect no changes and skip */ if (setup_incremental_repo() != 0) { FAIL("setup failed"); } /* First: full index */ cbm_pipeline_t *p = cbm_pipeline_new(g_incr_tmpdir, g_incr_dbpath, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); char *project = strdup(cbm_pipeline_project_name(p)); cbm_pipeline_free(p); /* Verify nodes exist */ cbm_store_t *s = cbm_store_open_path(g_incr_dbpath); ASSERT_NOT_NULL(s); int nodes_before = cbm_store_count_nodes(s, project); ASSERT_GT(nodes_before, 0); cbm_store_close(s); /* Second: incremental — nothing changed → should be no-op */ p = cbm_pipeline_new(g_incr_tmpdir, g_incr_dbpath, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_pipeline_free(p); s = cbm_store_open_path(g_incr_dbpath); ASSERT_NOT_NULL(s); int nodes_after = cbm_store_count_nodes(s, project); /* Node count should be same (no duplicates, no loss) */ ASSERT_EQ(nodes_after, nodes_before); cbm_store_close(s); free(project); cleanup_incremental_repo(); PASS(); } TEST(incremental_detects_changed_file) { /* Full index, modify one file, re-index → changed file re-parsed */ if (setup_incremental_repo() != 0) { FAIL("setup failed"); } /* First: full index */ cbm_pipeline_t *p = cbm_pipeline_new(g_incr_tmpdir, g_incr_dbpath, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); char *project = strdup(cbm_pipeline_project_name(p)); cbm_pipeline_free(p); /* Modify helper.go — add a new function */ char path[512]; snprintf(path, sizeof(path), "%s/helper.go", g_incr_tmpdir); FILE *f = fopen(path, "w"); ASSERT_NOT_NULL(f); fprintf(f, "package main\n\n" "func Helper() string {\n\treturn \"hello\"\n}\n\n" "func NewFunc() int {\n\treturn 42\n}\n"); fclose(f); /* Second: incremental — should detect change and re-index */ p = cbm_pipeline_new(g_incr_tmpdir, g_incr_dbpath, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); /* Verify node count increased (NewFunc was added) */ cbm_store_t *s = cbm_store_open_path(g_incr_dbpath); ASSERT_NOT_NULL(s); int nodes_after = cbm_store_count_nodes(s, project); ASSERT_GT(nodes_after, 0); cbm_store_close(s); cbm_pipeline_free(p); free(project); cleanup_incremental_repo(); PASS(); } TEST(incremental_detects_deleted_file) { /* Full index, delete a file, re-index → deleted file's nodes removed */ if (setup_incremental_repo() != 0) { FAIL("setup failed"); } /* First: full index */ cbm_pipeline_t *p = cbm_pipeline_new(g_incr_tmpdir, g_incr_dbpath, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); char *project = strdup(cbm_pipeline_project_name(p)); cbm_pipeline_free(p); /* Delete helper.go */ char path[512]; snprintf(path, sizeof(path), "%s/helper.go", g_incr_tmpdir); unlink(path); /* Second: incremental — should remove Helper nodes */ p = cbm_pipeline_new(g_incr_tmpdir, g_incr_dbpath, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); /* Verify node count decreased (Helper's file was deleted) */ cbm_store_t *s = cbm_store_open_path(g_incr_dbpath); ASSERT_NOT_NULL(s); int nodes_after = cbm_store_count_nodes(s, project); ASSERT_GT(nodes_after, 0); /* still has main.go nodes */ cbm_store_close(s); cbm_pipeline_free(p); free(project); cleanup_incremental_repo(); PASS(); } TEST(incremental_new_file_added) { /* Full index, add a new file, re-index → new file's nodes appear */ if (setup_incremental_repo() != 0) { FAIL("setup failed"); } /* First: full index */ cbm_pipeline_t *p = cbm_pipeline_new(g_incr_tmpdir, g_incr_dbpath, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); char *project = strdup(cbm_pipeline_project_name(p)); cbm_pipeline_free(p); /* Add extra.go */ char path[512]; snprintf(path, sizeof(path), "%s/extra.go", g_incr_tmpdir); FILE *f = fopen(path, "w"); ASSERT_NOT_NULL(f); fprintf(f, "package main\n\nfunc Extra() bool {\n\treturn true\n}\n"); fclose(f); /* Second: incremental — should pick up Extra */ p = cbm_pipeline_new(g_incr_tmpdir, g_incr_dbpath, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_store_t *s = cbm_store_open_path(g_incr_dbpath); ASSERT_NOT_NULL(s); int nodes_after = cbm_store_count_nodes(s, project); ASSERT_GT(nodes_after, 0); cbm_store_close(s); cbm_pipeline_free(p); free(project); cleanup_incremental_repo(); PASS(); } TEST(incremental_fast_preserves_mode_skipped_tools_dir) { /* Regression: 2026-04-13. A fast-mode reindex after a full-mode index * was silently destroying every file under FAST_SKIP_DIRS directories * (`tools`, `scripts`, `bin`, `build`, `docs`, ...) by classifying them * as deleted in find_deleted_files even though they still existed on * disk. The Skyline graph lost packages/mcp/src/tools/ (18 files / ~500 * nodes) mid-session when a concurrent /develop run obediently called * mode='fast'. This test pins the additive semantics: lesser-mode * reindexes must NOT delete files that are merely outside the current * pass's discovery scope. Fix: find_deleted_files now stat()s each * stored-but-missing file and only purges it if it is truly absent * from disk. */ char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_modeskip_XXXXXX"); if (!cbm_mkdtemp(tmpdir)) { FAIL("tmpdir"); } char dbpath[512]; snprintf(dbpath, sizeof(dbpath), "%s/test.db", tmpdir); char path[512]; FILE *f; /* main.go — root-level production code (visible in fast and full) */ snprintf(path, sizeof(path), "%s/main.go", tmpdir); f = fopen(path, "w"); ASSERT_NOT_NULL(f); fprintf(f, "package main\n\nfunc main() {\n}\n"); fclose(f); /* tools/util.go — production code under a FAST_SKIP_DIRS directory. * Full mode indexes it; fast mode skips it via the discover.c heuristic. */ char tools_dir[512]; snprintf(tools_dir, sizeof(tools_dir), "%s/tools", tmpdir); cbm_mkdir_p(tools_dir, 0755); snprintf(path, sizeof(path), "%s/tools/util.go", tmpdir); f = fopen(path, "w"); ASSERT_NOT_NULL(f); fprintf(f, "package tools\n\nfunc Util() string {\n\treturn \"u\"\n}\n"); fclose(f); /* Step 1: full-mode index — both files should be present */ cbm_pipeline_t *p = cbm_pipeline_new(tmpdir, dbpath, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); char *project = strdup(cbm_pipeline_project_name(p)); cbm_pipeline_free(p); cbm_store_t *s = cbm_store_open_path(dbpath); ASSERT_NOT_NULL(s); cbm_node_t *tools_nodes_before = NULL; int tools_count_before = 0; cbm_store_find_nodes_by_file(s, project, "tools/util.go", &tools_nodes_before, &tools_count_before); ASSERT_GT(tools_count_before, 0); /* full mode must see tools/util.go */ cbm_store_free_nodes(tools_nodes_before, tools_count_before); int total_before = cbm_store_count_nodes(s, project); cbm_store_close(s); /* Step 2: fast-mode reindex — tools/util.go MUST survive (additive semantics) */ p = cbm_pipeline_new(tmpdir, dbpath, CBM_MODE_FAST); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_pipeline_free(p); s = cbm_store_open_path(dbpath); ASSERT_NOT_NULL(s); cbm_node_t *tools_nodes_after = NULL; int tools_count_after = 0; cbm_store_find_nodes_by_file(s, project, "tools/util.go", &tools_nodes_after, &tools_count_after); /* The critical assertion: tools/util.go nodes must still be present after * a fast-mode reindex that skipped the tools/ directory. Before the fix, * this was 0. */ ASSERT_GT(tools_count_after, 0); ASSERT_EQ(tools_count_after, tools_count_before); /* same nodes, untouched */ cbm_store_free_nodes(tools_nodes_after, tools_count_after); /* Sanity: total node count should not have collapsed by ~the size of tools/ */ int total_after = cbm_store_count_nodes(s, project); ASSERT_GTE(total_after, total_before); /* additive — never less */ cbm_store_close(s); /* Step 3: mutate main.go and fast reindex — forces dump_and_persist to * run (instead of the noop early-return path that step 2 hit). This is * the real dangerous path: the gbuf gets loaded, mutated for main.go, * dumped back to disk. tools/util.go must survive THAT cycle, not just * the trivial noop path. Audit finding from 2026-04-13. */ snprintf(path, sizeof(path), "%s/main.go", tmpdir); f = fopen(path, "w"); ASSERT_NOT_NULL(f); fprintf(f, "package main\n\nfunc main() {\n\tprintln(\"changed\")\n}\n"); fclose(f); /* Bump mtime explicitly — some filesystems have coarse mtime resolution * and the rewrite could land in the same tick as the original write. */ #ifndef _WIN32 struct stat mst; if (stat(path, &mst) == 0) { struct timespec times[2]; times[0].tv_sec = mst.st_atime; times[0].tv_nsec = 0; times[1].tv_sec = mst.st_mtime + 5; times[1].tv_nsec = 0; utimensat(AT_FDCWD, path, times, 0); } #endif /* !_WIN32 */ p = cbm_pipeline_new(tmpdir, dbpath, CBM_MODE_FAST); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_pipeline_free(p); s = cbm_store_open_path(dbpath); ASSERT_NOT_NULL(s); cbm_node_t *tools_nodes_run3 = NULL; int tools_count_run3 = 0; cbm_store_find_nodes_by_file(s, project, "tools/util.go", &tools_nodes_run3, &tools_count_run3); /* tools/util.go nodes must STILL be present after a fast reindex that * actually ran the full dump_and_persist cycle (not the noop fast-path). */ ASSERT_EQ(tools_count_run3, tools_count_before); cbm_store_free_nodes(tools_nodes_run3, tools_count_run3); cbm_store_close(s); /* Step 4: actually delete tools/util.go from disk and full-reindex. * Now it really is gone, so its nodes should be purged. This pins the * other half of the contract: the stat-based check correctly identifies * truly-deleted files as deleted. */ snprintf(path, sizeof(path), "%s/tools/util.go", tmpdir); unlink(path); p = cbm_pipeline_new(tmpdir, dbpath, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_pipeline_free(p); s = cbm_store_open_path(dbpath); ASSERT_NOT_NULL(s); cbm_node_t *tools_nodes_deleted = NULL; int tools_count_deleted = 0; cbm_store_find_nodes_by_file(s, project, "tools/util.go", &tools_nodes_deleted, &tools_count_deleted); ASSERT_EQ(tools_count_deleted, 0); /* truly deleted → purged */ cbm_store_free_nodes(tools_nodes_deleted, tools_count_deleted); cbm_store_close(s); free(project); th_rmtree(tmpdir); PASS(); } TEST(incremental_k8s_manifest_indexed) { /* Full index with a k8s manifest, then add a new manifest via incremental. * Verifies that cbm_pipeline_pass_k8s() runs during incremental re-index. */ char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_k8s_incr_XXXXXX"); if (!cbm_mkdtemp(tmpdir)) { FAIL("tmpdir"); } char dbpath[512]; snprintf(dbpath, sizeof(dbpath), "%s/test.db", tmpdir); char path[512]; FILE *f; /* Initial manifest */ snprintf(path, sizeof(path), "%s/deploy.yaml", tmpdir); f = fopen(path, "w"); ASSERT_NOT_NULL(f); fprintf(f, "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: my-app\n"); fclose(f); /* Full index */ cbm_pipeline_t *p = cbm_pipeline_new(tmpdir, dbpath, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); char *project = strdup(cbm_pipeline_project_name(p)); cbm_pipeline_free(p); /* Verify Resource node created by full index */ cbm_store_t *s = cbm_store_open_path(dbpath); ASSERT_NOT_NULL(s); cbm_node_t *nodes = NULL; int count = 0; cbm_store_find_nodes_by_label(s, project, "Resource", &nodes, &count); ASSERT_GT(count, 0); cbm_store_free_nodes(nodes, count); cbm_store_close(s); /* Add a second manifest — incremental should pick it up */ snprintf(path, sizeof(path), "%s/svc.yaml", tmpdir); f = fopen(path, "w"); ASSERT_NOT_NULL(f); fprintf(f, "apiVersion: v1\nkind: Service\nmetadata:\n name: my-svc\n"); fclose(f); /* Incremental re-index */ p = cbm_pipeline_new(tmpdir, dbpath, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_pipeline_free(p); /* Verify both Resource nodes now present */ s = cbm_store_open_path(dbpath); ASSERT_NOT_NULL(s); nodes = NULL; count = 0; cbm_store_find_nodes_by_label(s, project, "Resource", &nodes, &count); ASSERT_GTE(count, 2); cbm_store_free_nodes(nodes, count); cbm_store_close(s); free(project); th_rmtree(tmpdir); PASS(); } TEST(incremental_kustomize_module_indexed) { /* Verifies that a kustomization.yaml added after the initial full index * gets a Module node via the incremental k8s pass. */ char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_kust_incr_XXXXXX"); if (!cbm_mkdtemp(tmpdir)) { FAIL("tmpdir"); } char dbpath[512]; snprintf(dbpath, sizeof(dbpath), "%s/test.db", tmpdir); char path[512]; FILE *f; /* Initial resource manifest (gives full index something to find) */ snprintf(path, sizeof(path), "%s/deploy.yaml", tmpdir); f = fopen(path, "w"); ASSERT_NOT_NULL(f); fprintf(f, "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: my-app\n"); fclose(f); /* Full index */ cbm_pipeline_t *p = cbm_pipeline_new(tmpdir, dbpath, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); char *project = strdup(cbm_pipeline_project_name(p)); cbm_pipeline_free(p); /* Add kustomization.yaml */ snprintf(path, sizeof(path), "%s/kustomization.yaml", tmpdir); f = fopen(path, "w"); ASSERT_NOT_NULL(f); fprintf(f, "apiVersion: kustomize.config.k8s.io/v1beta1\n" "kind: Kustomization\n" "resources:\n" " - deploy.yaml\n"); fclose(f); /* Incremental re-index */ p = cbm_pipeline_new(tmpdir, dbpath, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_pipeline_free(p); /* Verify Module node created for the kustomization overlay */ cbm_store_t *s = cbm_store_open_path(dbpath); ASSERT_NOT_NULL(s); cbm_node_t *nodes = NULL; int count = 0; cbm_store_find_nodes_by_label(s, project, "Module", &nodes, &count); bool found_kust = false; for (int i = 0; i < count; i++) { if (nodes[i].properties_json && strstr(nodes[i].properties_json, "kustomize")) { found_kust = true; break; } } cbm_store_free_nodes(nodes, count); cbm_store_close(s); ASSERT_TRUE(found_kust); free(project); th_rmtree(tmpdir); PASS(); } /* ── Index lock tests ───────────────────────────────────────────── */ TEST(pipeline_lock_try_acquire) { /* First try-lock should succeed */ ASSERT_TRUE(cbm_pipeline_try_lock()); /* Second try-lock should fail (already held) */ ASSERT_FALSE(cbm_pipeline_try_lock()); /* Release, then re-acquire should succeed */ cbm_pipeline_unlock(); ASSERT_TRUE(cbm_pipeline_try_lock()); cbm_pipeline_unlock(); PASS(); } TEST(pipeline_lock_blocking) { /* Lock, then unlock — basic sanity */ cbm_pipeline_lock(); cbm_pipeline_unlock(); /* Should be immediately re-acquirable */ cbm_pipeline_lock(); cbm_pipeline_unlock(); PASS(); } /* Thread function that tries to acquire the lock and records result */ static atomic_int g_thread_acquired = 0; static atomic_int g_thread_done = 0; static void *try_lock_thread(void *arg) { (void)arg; if (cbm_pipeline_try_lock()) { atomic_store(&g_thread_acquired, 1); cbm_pipeline_unlock(); } else { atomic_store(&g_thread_acquired, 0); } atomic_store(&g_thread_done, 1); return NULL; } TEST(pipeline_lock_contention) { /* Main thread holds lock, spawned thread should fail try_lock */ cbm_pipeline_lock(); atomic_store(&g_thread_acquired, -1); atomic_store(&g_thread_done, 0); cbm_thread_t tid; int rc = cbm_thread_create(&tid, 0, try_lock_thread, NULL); ASSERT_EQ(rc, 0); /* Wait for thread to finish */ cbm_thread_join(&tid); /* Thread should NOT have acquired the lock */ ASSERT_EQ(atomic_load(&g_thread_acquired), 0); cbm_pipeline_unlock(); PASS(); } TEST(pipeline_lock_release_allows_contender) { /* Main thread acquires and releases, then spawned thread should succeed */ cbm_pipeline_lock(); cbm_pipeline_unlock(); atomic_store(&g_thread_acquired, -1); atomic_store(&g_thread_done, 0); cbm_thread_t tid; int rc = cbm_thread_create(&tid, 0, try_lock_thread, NULL); ASSERT_EQ(rc, 0); cbm_thread_join(&tid); /* Thread SHOULD have acquired the lock */ ASSERT_EQ(atomic_load(&g_thread_acquired), 1); PASS(); } /* ── Resource management & internal helper tests ─────────────────── */ TEST(pipeline_empty_path) { /* Empty string repo path — should handle gracefully */ cbm_pipeline_t *p = cbm_pipeline_new("", NULL, CBM_MODE_FULL); /* Implementation may return NULL or a valid pipeline with empty project name. * Either behavior is acceptable — the key is no crash. */ if (p) { cbm_pipeline_free(p); } PASS(); } TEST(pipeline_project_name_content) { /* Verify project name is derived from the repo_path */ cbm_pipeline_t *p = cbm_pipeline_new("/home/user/my-project", NULL, CBM_MODE_FULL); ASSERT_NOT_NULL(p); const char *name = cbm_pipeline_project_name(p); ASSERT_NOT_NULL(name); ASSERT_TRUE(strlen(name) > 0); /* Should contain "my-project" as part of the derived name */ ASSERT_TRUE(strstr(name, "my-project") != NULL); cbm_pipeline_free(p); PASS(); } TEST(pipeline_cancel_sets_flag) { /* Verify cancel sets the flag so subsequent run exits early */ cbm_pipeline_t *p = cbm_pipeline_new("/tmp/nonexistent", NULL, CBM_MODE_FULL); ASSERT_NOT_NULL(p); /* Cancel before run */ cbm_pipeline_cancel(p); /* Cancelled pipeline should return quickly (either -1 from cancel or from * missing path — both are acceptable; key is no hang) */ int rc = cbm_pipeline_run(p); ASSERT_EQ(rc, -1); cbm_pipeline_free(p); PASS(); } TEST(pipeline_double_cancel) { /* Calling cancel twice should not crash */ cbm_pipeline_t *p = cbm_pipeline_new("/tmp/nonexistent", NULL, CBM_MODE_FULL); ASSERT_NOT_NULL(p); cbm_pipeline_cancel(p); cbm_pipeline_cancel(p); cbm_pipeline_free(p); PASS(); } TEST(pipeline_double_free_prevention) { /* free(NULL) after free should not crash. We can't truly double-free * the same pointer, but we verify NULL is safe as documented. */ cbm_pipeline_free(NULL); cbm_pipeline_free(NULL); PASS(); } TEST(trackable_source_files) { /* Common source extensions are trackable */ ASSERT_TRUE(cbm_is_trackable_file("main.go")); ASSERT_TRUE(cbm_is_trackable_file("src/handler.py")); ASSERT_TRUE(cbm_is_trackable_file("lib/server.js")); ASSERT_TRUE(cbm_is_trackable_file("components/App.tsx")); ASSERT_TRUE(cbm_is_trackable_file("pkg/svc.rs")); ASSERT_TRUE(cbm_is_trackable_file("config.yaml")); PASS(); } TEST(trackable_rejects_images) { /* Image and binary files are not trackable */ ASSERT_FALSE(cbm_is_trackable_file("logo.png")); ASSERT_FALSE(cbm_is_trackable_file("photo.jpg")); ASSERT_FALSE(cbm_is_trackable_file("icon.gif")); ASSERT_FALSE(cbm_is_trackable_file("diagram.svg")); PASS(); } TEST(trackable_rejects_minified) { /* Minified files are not trackable */ ASSERT_FALSE(cbm_is_trackable_file("bundle.min.js")); ASSERT_FALSE(cbm_is_trackable_file("style.min.css")); PASS(); } TEST(trackable_rejects_vendor_dirs) { /* Files in vendor/node_modules/__pycache__ are not trackable */ ASSERT_FALSE(cbm_is_trackable_file("vendor/github.com/lib/dep.go")); ASSERT_FALSE(cbm_is_trackable_file("node_modules/lodash/index.js")); ASSERT_FALSE(cbm_is_trackable_file("__pycache__/module.pyc")); ASSERT_FALSE(cbm_is_trackable_file(".git/objects/pack/data")); PASS(); } TEST(trackable_rejects_lock_files) { /* Lock files are not trackable */ ASSERT_FALSE(cbm_is_trackable_file("package-lock.json")); ASSERT_FALSE(cbm_is_trackable_file("go.sum")); ASSERT_FALSE(cbm_is_trackable_file("yarn.lock")); PASS(); } TEST(test_path_directory_patterns) { /* Test directory patterns: __tests__, test/, tests/, spec/ */ ASSERT_TRUE(cbm_is_test_path("__tests__/Component.js")); ASSERT_TRUE(cbm_is_test_path("tests/test_main.py")); ASSERT_TRUE(cbm_is_test_path("spec/handler_spec.rb")); PASS(); } TEST(test_path_suffix_patterns) { /* Various language-specific test suffix patterns */ ASSERT_TRUE(cbm_is_test_path("handler.spec.ts")); ASSERT_TRUE(cbm_is_test_path("handler.test.tsx")); ASSERT_TRUE(cbm_is_test_path("handler_test.go")); ASSERT_TRUE(cbm_is_test_path("test_handler.py")); /* Non-test files */ ASSERT_FALSE(cbm_is_test_path("handler.ts")); ASSERT_FALSE(cbm_is_test_path("contest.go")); ASSERT_FALSE(cbm_is_test_path("latest.py")); PASS(); } TEST(test_func_name_go_patterns) { /* Go test function names: Test + uppercase char */ ASSERT_TRUE(cbm_is_test_func_name("TestFoo")); ASSERT_TRUE(cbm_is_test_func_name("TestHTTPHandler")); /* Non-test: "Test" alone or Test + lowercase */ ASSERT_FALSE(cbm_is_test_func_name("Testable")); /* lowercase 'a' after Test */ PASS(); } TEST(test_func_name_js_helpers) { /* JS/TS test helper function names */ ASSERT_TRUE(cbm_is_test_func_name("it")); ASSERT_TRUE(cbm_is_test_func_name("describe")); ASSERT_TRUE(cbm_is_test_func_name("test")); ASSERT_TRUE(cbm_is_test_func_name("beforeEach")); ASSERT_TRUE(cbm_is_test_func_name("afterEach")); PASS(); } TEST(env_var_name_valid) { /* Valid env var names: uppercase + underscores, at least 2 chars with uppercase */ ASSERT_TRUE(cbm_is_env_var_name("DATABASE_URL")); ASSERT_TRUE(cbm_is_env_var_name("API_KEY")); ASSERT_TRUE(cbm_is_env_var_name("PORT")); ASSERT_TRUE(cbm_is_env_var_name("DB_2")); ASSERT_TRUE(cbm_is_env_var_name("MY_VAR_123")); PASS(); } TEST(env_var_name_invalid) { /* Invalid: too short, lowercase, mixed case, empty */ ASSERT_FALSE(cbm_is_env_var_name("A")); /* single char */ ASSERT_FALSE(cbm_is_env_var_name("port")); /* lowercase */ ASSERT_FALSE(cbm_is_env_var_name("apiKey")); /* camelCase */ ASSERT_FALSE(cbm_is_env_var_name("__")); /* no uppercase */ ASSERT_FALSE(cbm_is_env_var_name("")); /* empty */ ASSERT_FALSE(cbm_is_env_var_name("123")); /* digits only */ PASS(); } TEST(config_ext_positive) { /* Config file extensions recognized */ ASSERT_TRUE(cbm_has_config_extension(".env")); ASSERT_TRUE(cbm_has_config_extension("config.yaml")); ASSERT_TRUE(cbm_has_config_extension("config.yml")); ASSERT_TRUE(cbm_has_config_extension("settings.toml")); ASSERT_TRUE(cbm_has_config_extension("app.ini")); ASSERT_TRUE(cbm_has_config_extension("data.json")); ASSERT_TRUE(cbm_has_config_extension("app.properties")); PASS(); } TEST(config_ext_negative) { /* Non-config extensions rejected */ ASSERT_FALSE(cbm_has_config_extension("main.go")); ASSERT_FALSE(cbm_has_config_extension("app.py")); ASSERT_FALSE(cbm_has_config_extension("handler.rs")); ASSERT_FALSE(cbm_has_config_extension("data.csv")); ASSERT_FALSE(cbm_has_config_extension("README.md")); PASS(); } TEST(split_camel_basic) { /* Basic camelCase splitting */ char *parts[16]; int n; n = cbm_split_camel_case("getCamelCase", parts, 16); ASSERT_EQ(n, 3); ASSERT_STR_EQ(parts[0], "get"); ASSERT_STR_EQ(parts[1], "Camel"); ASSERT_STR_EQ(parts[2], "Case"); for (int i = 0; i < n; i++) free(parts[i]); PASS(); } TEST(split_camel_single_word) { /* Single lowercase word — no splits */ char *parts[16]; int n = cbm_split_camel_case("hello", parts, 16); ASSERT_EQ(n, 1); ASSERT_STR_EQ(parts[0], "hello"); for (int i = 0; i < n; i++) free(parts[i]); PASS(); } TEST(split_camel_empty) { /* Empty string — should return 0 or 1 empty part */ char *parts[16]; int n = cbm_split_camel_case("", parts, 16); for (int i = 0; i < n; i++) free(parts[i]); /* Either 0 parts or 1 empty part is acceptable */ ASSERT_TRUE(n >= 0 && n <= 1); PASS(); } TEST(tokenize_decorator_login_required) { /* @login_required → ["login", "required"] */ char *tokens[16]; int n = cbm_tokenize_decorator("@login_required", tokens, 16); ASSERT_EQ(n, 2); ASSERT_STR_EQ(tokens[0], "login"); ASSERT_STR_EQ(tokens[1], "required"); for (int i = 0; i < n; i++) free(tokens[i]); PASS(); } TEST(tokenize_decorator_single) { /* @Override → ["override"] */ char *tokens[16]; int n = cbm_tokenize_decorator("@Override", tokens, 16); ASSERT_EQ(n, 1); ASSERT_STR_EQ(tokens[0], "override"); for (int i = 0; i < n; i++) free(tokens[i]); PASS(); } TEST(split_command_basic) { /* Basic command splitting with spaces */ char *args[16]; int n = cbm_split_command("gcc -c main.c -o main.o", args, 16); ASSERT_EQ(n, 5); ASSERT_STR_EQ(args[0], "gcc"); ASSERT_STR_EQ(args[1], "-c"); ASSERT_STR_EQ(args[2], "main.c"); ASSERT_STR_EQ(args[3], "-o"); ASSERT_STR_EQ(args[4], "main.o"); for (int i = 0; i < n; i++) free(args[i]); PASS(); } TEST(split_command_quoted) { /* Command with quoted arguments */ char *args[16]; int n = cbm_split_command("gcc -DFOO=\"bar baz\" main.c", args, 16); ASSERT_GTE(n, 3); ASSERT_STR_EQ(args[0], "gcc"); for (int i = 0; i < n; i++) free(args[i]); PASS(); } TEST(split_command_empty) { /* Empty command string */ char *args[16]; int n = cbm_split_command("", args, 16); ASSERT_EQ(n, 0); PASS(); } TEST(parse_range_with_count) { /* "10,5" → start=10, count=5 */ int start, count; cbm_parse_range("10,5", &start, &count); ASSERT_EQ(start, 10); ASSERT_EQ(count, 5); PASS(); } TEST(parse_range_single) { /* "42" → start=42, count=1 */ int start, count; cbm_parse_range("42", &start, &count); ASSERT_EQ(start, 42); ASSERT_EQ(count, 1); PASS(); } TEST(parse_range_zero_count) { /* "100,0" → start=100, count=0 */ int start, count; cbm_parse_range("100,0", &start, &count); ASSERT_EQ(start, 100); ASSERT_EQ(count, 0); PASS(); } TEST(parse_name_status_basic) { /* Parse standard git diff --name-status output */ const char *input = "M\tsrc/main.go\n" "A\tsrc/new.go\n" "D\tsrc/old.go\n"; cbm_changed_file_t files[16]; int n = cbm_parse_name_status(input, files, 16); /* Exact count depends on which files pass cbm_is_trackable_file */ ASSERT_GTE(n, 0); ASSERT_LTE(n, 3); PASS(); } TEST(parse_name_status_empty) { /* Empty input */ cbm_changed_file_t files[16]; int n = cbm_parse_name_status("", files, 16); ASSERT_EQ(n, 0); PASS(); } TEST(parse_hunks_empty) { /* Empty input */ cbm_changed_hunk_t hunks[16]; int n = cbm_parse_hunks("", hunks, 16); ASSERT_EQ(n, 0); PASS(); } TEST(coupling_empty_commits) { /* Zero commits → zero couplings */ cbm_change_coupling_t results[16]; int n = cbm_compute_change_coupling(NULL, 0, results, 16); ASSERT_EQ(n, 0); PASS(); } TEST(coupling_single_file_commit) { /* Commits with single files → no pairs → zero couplings */ char *f1[] = {"a.go"}; char *f2[] = {"b.go"}; cbm_commit_files_t commits[] = {{f1, 1, 0}, {f2, 1, 0}}; cbm_change_coupling_t results[16]; int n = cbm_compute_change_coupling(commits, 2, results, 16); ASSERT_EQ(n, 0); PASS(); } TEST(clean_json_brackets_array) { /* JSON array → space-joined */ char out[256]; cbm_clean_json_brackets("[\"./app\", \"--flag\"]", out, sizeof(out)); ASSERT_TRUE(strstr(out, "./app") != NULL); ASSERT_TRUE(strstr(out, "--flag") != NULL); PASS(); } TEST(clean_json_brackets_plain) { /* Plain string → unchanged */ char out[256]; cbm_clean_json_brackets("./app --flag", out, sizeof(out)); ASSERT_STR_EQ(out, "./app --flag"); PASS(); } TEST(clean_json_brackets_empty) { /* Empty string → empty output */ char out[256]; cbm_clean_json_brackets("", out, sizeof(out)); ASSERT_STR_EQ(out, ""); PASS(); } TEST(fqn_compute_basic) { /* Basic FQN: project.dir.name */ char *fqn = cbm_pipeline_fqn_compute("proj", "pkg/handler.go", "Serve"); ASSERT_NOT_NULL(fqn); ASSERT_TRUE(strstr(fqn, "proj") != NULL); ASSERT_TRUE(strstr(fqn, "Serve") != NULL); free(fqn); PASS(); } TEST(fqn_compute_strips_ext) { /* FQN should strip file extension */ char *fqn = cbm_pipeline_fqn_compute("proj", "main.go", "main"); ASSERT_NOT_NULL(fqn); /* Should not contain ".go" */ ASSERT_TRUE(strstr(fqn, ".go") == NULL); ASSERT_TRUE(strstr(fqn, "main") != NULL); free(fqn); PASS(); } TEST(fqn_module_basic) { /* Module QN: project.dir.parts */ char *mod = cbm_pipeline_fqn_module("proj", "pkg/util/helper.go"); ASSERT_NOT_NULL(mod); ASSERT_TRUE(strstr(mod, "proj") != NULL); free(mod); PASS(); } TEST(fqn_folder_basic) { /* Folder QN from directory path */ char *folder = cbm_pipeline_fqn_folder("proj", "pkg/util"); ASSERT_NOT_NULL(folder); ASSERT_TRUE(strstr(folder, "proj") != NULL); free(folder); PASS(); } TEST(project_name_special_chars) { /* Project name with colons, slashes — should be normalized */ char *name = cbm_project_name_from_path("/home/user/my:project"); ASSERT_NOT_NULL(name); /* Colons should be converted to dashes */ ASSERT_TRUE(strchr(name, ':') == NULL); ASSERT_TRUE(strchr(name, '/') == NULL); free(name); PASS(); } TEST(project_name_trailing_slash) { /* Trailing slash should not affect result */ char *a = cbm_project_name_from_path("/home/user/project"); ASSERT_NOT_NULL(a); free(a); PASS(); } /* ── Complexity propagation pass tests (Tier B) ────────────────── */ /* Find the first Function/Method node with `name` in a label set. Returns a * borrowed pointer into `funcs` or NULL. */ static const cbm_node_t *find_node_named(cbm_node_t *funcs, int count, const char *name) { for (int i = 0; i < count; i++) { if (strcmp(funcs[i].name, name) == 0) return &funcs[i]; } return NULL; } /* The complexity pass propagates loop_depth along CALLS edges into * transitive_loop_depth and flags call-graph cycles as recursive. Caller and * callee live in one file so the calls resolve intra-file (most reliable). */ TEST(pipeline_complexity_transitive_loop_depth) { char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_cx_XXXXXX"); if (!cbm_mkdtemp(tmpdir)) FAIL("tmpdir"); write_temp_file(tmpdir, "cx.go", "package p\n\n" "func work() {}\n\n" "func inner() {\n" "\tfor i := 0; i < 10; i++ {\n" "\t\tfor j := 0; j < 10; j++ {\n" "\t\t\twork()\n" "\t\t}\n" "\t}\n" "}\n\n" "func outer() {\n" "\tfor i := 0; i < 10; i++ {\n" "\t\tinner()\n" "\t}\n" "}\n\n" "func recur(n int) {\n" "\tif n > 0 {\n" "\t\trecur(n - 1)\n" "\t}\n" "}\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/cx.db", tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(tmpdir, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); const char *project = cbm_pipeline_project_name(p); cbm_node_t *funcs = NULL; int func_count = 0; ASSERT_EQ(cbm_store_find_nodes_by_label(s, project, "Function", &funcs, &func_count), CBM_STORE_OK); ASSERT_GT(func_count, 0); /* inner: two nested loops, callee work() has depth 0 → tld == 2 */ const cbm_node_t *inner = find_node_named(funcs, func_count, "inner"); ASSERT_NOT_NULL(inner); ASSERT_NOT_NULL(inner->properties_json); ASSERT_TRUE(strstr(inner->properties_json, "\"transitive_loop_depth\":2") != NULL); /* outer: own depth 1 + inner's tld 2 → tld == 3 (interprocedural) */ const cbm_node_t *outer = find_node_named(funcs, func_count, "outer"); ASSERT_NOT_NULL(outer); ASSERT_NOT_NULL(outer->properties_json); ASSERT_TRUE(strstr(outer->properties_json, "\"transitive_loop_depth\":3") != NULL); /* recur: self-recursion → recursive:true flagged by the cycle guard */ const cbm_node_t *recur = find_node_named(funcs, func_count, "recur"); ASSERT_NOT_NULL(recur); ASSERT_NOT_NULL(recur->properties_json); ASSERT_TRUE(strstr(recur->properties_json, "\"recursive\":true") != NULL); cbm_store_free_nodes(funcs, func_count); cbm_store_close(s); cbm_pipeline_free(p); th_rmtree(tmpdir); PASS(); } /* Regression for #334: the plausibility gate compares committed (extracted) * node count against persisted rows. committed_nodes must be captured BEFORE * cbm_gbuf_dump_to_sqlite frees the gbuf node index — otherwise it reads 0 and * the gate is silently inert. Drives the real pipeline (not a synthetic count) * and asserts committed_nodes is populated and matches what was persisted. */ TEST(pipeline_committed_counts_match_persisted) { if (setup_test_repo() != 0) { FAIL("failed to create temp dir"); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/committed_test.db", g_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_tmpdir, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); int rc = cbm_pipeline_run(p); ASSERT_EQ(rc, 0); int committed_nodes = -1; int committed_edges = -1; cbm_pipeline_get_committed_counts(p, &committed_nodes, &committed_edges); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); const char *project = cbm_pipeline_project_name(p); int persisted_nodes = cbm_store_count_nodes(s, project); cbm_store_close(s); /* The bug captured committed_nodes after the node index was freed → 0. */ ASSERT_GT(committed_nodes, 0); /* A faithful full dump persists exactly what it committed. */ ASSERT_EQ(committed_nodes, persisted_nodes); /* The gate must NOT flag a healthy full index as degraded. */ ASSERT_FALSE(cbm_dump_verify_is_degraded(committed_nodes, persisted_nodes, CBM_DUMP_VERIFY_DEFAULT_RATIO, CBM_DUMP_VERIFY_MIN_FLOOR)); cbm_pipeline_free(p); teardown_test_repo(); PASS(); } /* Reproduce-first (perf, linux-kernel finding): the extraction back-pressure * gate must stop re-paying the full collect+nap tax on every file pull once a * full nap cycle has failed to reclaim under budget. With CBM_MEM_BUDGET_MB=1 * the test process RSS is permanently over budget and NOTHING napping can * change that (the resident floor IS the process) — napping is provably futile. * OLD behavior: one full 40-spin nap cycle per pulled file (kernel index: ~63k * pulls × ~120 ms ÷ 12 workers ≈ 390 s of idle workers at 79% avg CPU). * FIXED: the first futile cycle flips a shared flag; later pulls proceed with * the designed soft overshoot. Cycle count then can't exceed one cycle per * worker (workers already inside the gate when the flag flips) plus re-probes. * RED on the unfixed gate: cycles == file count (64) > cores+2. * The counter (cbm_pp_bp_nap_cycles) makes this deterministic — no timing. * * The gate lives ONLY in the parallel extract path, so the fixture MUST exceed * MIN_FILES_FOR_PARALLEL (50) — else the run routes sequential, the gate never * fires, and the test would pass vacuously (cycles==0). The engagement assert * below (cycles >= 1) is a hard guard against that regressing silently. */ TEST(pipeline_backpressure_futile_nap_disengages) { /* 64 tiny files: > MIN_FILES_FOR_PARALLEL (50) so the parallel path (and its * back-pressure gate) actually runs; old-code cycles (~64) >> the bound. */ snprintf(g_tmpdir, sizeof(g_tmpdir), "/tmp/cbm_test_XXXXXX"); if (!cbm_mkdtemp(g_tmpdir)) { FAIL("failed to create temp dir"); } for (int i = 0; i < 64; i++) { char path[512]; snprintf(path, sizeof(path), "%s/f%02d.go", g_tmpdir, i); FILE *f = fopen(path, "w"); if (!f) { FAIL("failed to create fixture file"); } fprintf(f, "package main\n\nfunc F%02d() int {\n\treturn %d\n}\n", i, i); fclose(f); } /* 1 MB budget: over-budget on every pull, unreclaimable by napping. * Set via the test hook, NOT setenv + cbm_mem_init: the init-once guard * makes any re-init keep whatever budget the FIRST in-process init * computed. The old env dance either failed to apply the 1 MB budget * (some earlier test's init won the guard) or applied it permanently * (this test's init won) — the "restore" re-init was then a silent * no-op and the 1 MB budget leaked into every later budget consumer * in the runner (mem_over_budget_low_rss went red suite-order-wide). */ size_t saved_budget = cbm_mem_budget(); cbm_mem_set_budget_for_tests((size_t)1024 * 1024); ASSERT_TRUE(cbm_mem_budget() > 0); ASSERT_TRUE(cbm_mem_over_budget()); cbm_pp_bp_nap_cycles_reset(); cbm_pipeline_t *p = cbm_pipeline_new(g_tmpdir, NULL, CBM_MODE_FULL); ASSERT_NOT_NULL(p); int rc = cbm_pipeline_run(p); long cycles = cbm_pp_bp_nap_cycles(); /* Restore the caller-visible budget BEFORE asserting. */ cbm_mem_set_budget_for_tests(saved_budget); cbm_pipeline_free(p); teardown_test_repo(); ASSERT_EQ(rc, 0); /* Engagement guard (anti-vacuous): the gate must have actually run — the * parallel path taken and the 1 MB budget exceeded on every pull. cycles==0 * means the fixture routed sequential (or the gate was compiled out) and * this test proved NOTHING; fail loudly rather than pass vacuously. */ if (cycles < 1) { FAIL("back-pressure gate never engaged (cycles==0) — fixture routed sequential?"); } /* Futile napping must disengage: at most one in-flight cycle per worker * plus a small margin, never one per file (64). */ long bound = (long)cbm_system_info().total_cores + 2; if (cycles > bound) { char msg[128]; snprintf(msg, sizeof(msg), "nap cycles %ld > bound %ld (gate re-paid per pull)", cycles, bound); FAIL(msg); } PASS(); } /* TS cross-registry test hooks (ts_lsp.c) — extern to avoid pulling the * tree-sitter-typed ts_lsp.h into this store-level test. */ extern long cbm_ts_full_registry_builds(void); extern void cbm_ts_full_registry_builds_reset(void); /* Reproduce-first (ms-typescript finding, 2026-07-07): the SEQUENTIAL * cross-LSP driver must resolve TS files through the SHARED prebuilt * registry, never a full per-file build. cbm_run_ts_lsp_cross registers * stdlib + EVERY cross-file def + finalizes once PER FILE — O(files x defs). * On an 81k-file TS corpus that ground one core for hours (74% of stack * samples inside build_qn_index), and when the supervisor's quiet-timeout * killed the crawl mid-pass, the stale extraction marker blamed innocent * files, quarantining four of them one 15-minute retry at a time. * * The fixture stays UNDER MIN_FILES_FOR_PARALLEL (50) so the pipeline * routes through the sequential driver — the path that lacked the * shared-registry prepare. * RED on the unfixed driver: full builds == TS file count (40). * GREEN: full builds == 0 AND the cross-file TS call still resolves * (quality guard — the shared path must not lose the edge). */ TEST(pipeline_seq_ts_cross_uses_shared_registry) { snprintf(g_tmpdir, sizeof(g_tmpdir), "/tmp/cbm_test_XXXXXX"); if (!cbm_mkdtemp(g_tmpdir)) { FAIL("failed to create temp dir"); } char path[512]; snprintf(path, sizeof(path), "%s/shared.ts", g_tmpdir); FILE *f = fopen(path, "w"); if (!f) { FAIL("failed to create fixture file"); } fputs("export function sharedHelper(): number {\n return 42;\n}\n", f); fclose(f); for (int i = 0; i < 39; i++) { snprintf(path, sizeof(path), "%s/caller%02d.ts", g_tmpdir, i); f = fopen(path, "w"); if (!f) { FAIL("failed to create fixture file"); } fprintf(f, "import { sharedHelper } from \"./shared\";\n" "export function caller%02d(): number {\n return sharedHelper();\n}\n", i); fclose(f); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/seqts.db", g_tmpdir); cbm_ts_full_registry_builds_reset(); cbm_pipeline_t *p = cbm_pipeline_new(g_tmpdir, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); int rc = cbm_pipeline_run(p); long builds = cbm_ts_full_registry_builds(); /* Quality guard FIRST: the cross-file call must resolve either way. */ cbm_store_t *s = cbm_store_open_path(db_path); bool linked = false; if (s) { linked = cross_file_call_exists(s, cbm_pipeline_project_name(p), "caller00", "sharedHelper"); cbm_store_close(s); } cbm_pipeline_free(p); teardown_test_repo(); ASSERT_EQ(rc, 0); ASSERT_TRUE(linked); /* The point: ZERO full per-file registry builds on the sequential path. */ ASSERT_EQ(builds, 0); PASS(); } SUITE(pipeline) { /* Index lock */ RUN_TEST(pipeline_lock_try_acquire); RUN_TEST(pipeline_lock_blocking); RUN_TEST(pipeline_lock_contention); RUN_TEST(pipeline_lock_release_allows_contender); /* Lifecycle */ RUN_TEST(pipeline_create_free); RUN_TEST(pipeline_null_repo); RUN_TEST(pipeline_free_null); RUN_TEST(pipeline_cancel); RUN_TEST(pipeline_cancel_null); RUN_TEST(pipeline_run_null); /* Extraction back-pressure */ RUN_TEST(pipeline_backpressure_futile_nap_disengages); /* Sequential cross-LSP shared registry (ms-typescript quadratic) */ RUN_TEST(pipeline_seq_ts_cross_uses_shared_registry); /* File persistence */ RUN_TEST(store_file_persistence); RUN_TEST(store_bulk_persistence); /* Integration: structure pass */ RUN_TEST(pipeline_structure_nodes); RUN_TEST(pipeline_committed_counts_match_persisted); RUN_TEST(pipeline_adr_survives_full_reindex); RUN_TEST(pipeline_structure_edges); RUN_TEST(pipeline_branch_root_structure); RUN_TEST(pipeline_project_name_derived); RUN_TEST(pipeline_fast_mode); /* Definitions pass */ RUN_TEST(pipeline_definitions_function_nodes); RUN_TEST(pipeline_definitions_defines_edges); RUN_TEST(pipeline_definitions_properties); RUN_TEST(pipeline_def_props_valid_json_when_oversized); RUN_TEST(pipeline_edge_props_valid_json); /* Complexity propagation pass (Tier B) */ RUN_TEST(pipeline_complexity_transitive_loop_depth); /* Calls pass */ RUN_TEST(pipeline_calls_resolution); RUN_TEST(pipeline_incremental_preserves_cross_file_calls); RUN_TEST(pipeline_tsjs_receiver_suppresses_weak_method_edge); RUN_TEST(pipeline_tsjs_receiver_parallel_keeps_service_edges); RUN_TEST(pipeline_native_fetch_classified_as_http_calls); RUN_TEST(pipeline_native_fetch_parallel_classified_as_http_calls); RUN_TEST(pipeline_local_fetch_shadow_not_classified_as_http); /* Git history pass */ RUN_TEST(githistory_is_trackable); RUN_TEST(githistory_compute_coupling); RUN_TEST(githistory_coupling_carries_last_co_change); RUN_TEST(githistory_skip_large_commits); RUN_TEST(githistory_limits_to_max); /* Test detection */ RUN_TEST(testdetect_is_test_file); RUN_TEST(testdetect_is_test_function); /* Implements pass (graph buffer based) */ RUN_TEST(implements_creates_override); RUN_TEST(implements_no_match); /* Usages pass (full pipeline integration) */ RUN_TEST(usages_creates_edges); RUN_TEST(usages_no_duplicate_calls); RUN_TEST(calls_edge_carries_call_site_line); RUN_TEST(usages_kotlin_creates_edges); RUN_TEST(usages_kotlin_no_duplicate_calls); /* Language integration tests */ RUN_TEST(pipeline_python_project); RUN_TEST(pipeline_imports_multi_symbol_edges); RUN_TEST(pipeline_go_cross_package_call); RUN_TEST(pipeline_python_cross_module_call); RUN_TEST(pipeline_go_type_classification); RUN_TEST(pipeline_go_grouped_types); RUN_TEST(pipeline_kotlin_project); RUN_TEST(pipeline_lua_anonymous_functions); RUN_TEST(pipeline_csharp_modern); RUN_TEST(pipeline_bom_stripping); RUN_TEST(pipeline_form_call_resolution); RUN_TEST(pipeline_python_type_inference); /* Docstring integration (port of TestDocstringIntegration) */ RUN_TEST(pipeline_docstring_go_function); RUN_TEST(pipeline_docstring_python_function); RUN_TEST(pipeline_docstring_java_method); RUN_TEST(pipeline_docstring_kotlin_function); RUN_TEST(pipeline_docstring_go_class); /* Project name */ RUN_TEST(project_name_from_path); RUN_TEST(project_name_drive_letter_case_insensitive_issue394); RUN_TEST(git_context_non_git_path); RUN_TEST(git_context_linked_worktree); RUN_TEST(project_name_uniqueness); /* Git diff helpers */ RUN_TEST(gitdiff_parse_range_with_count); RUN_TEST(gitdiff_parse_range_no_count); RUN_TEST(gitdiff_parse_range_zero_count); RUN_TEST(gitdiff_parse_range_large); RUN_TEST(gitdiff_parse_name_status); RUN_TEST(gitdiff_parse_name_status_filters_untrackable); RUN_TEST(gitdiff_parse_hunks); RUN_TEST(gitdiff_parse_hunks_no_newline_marker); RUN_TEST(gitdiff_parse_hunks_mode_change); RUN_TEST(gitdiff_parse_hunks_deletion); /* Config helpers */ RUN_TEST(configures_is_env_var_name); RUN_TEST(configures_normalize_config_key); RUN_TEST(configures_has_config_extension); /* Config integration tests */ RUN_TEST(configures_env_var_in_config); RUN_TEST(configures_lowercase_key_skipped); RUN_TEST(configures_non_config_file_skipped); RUN_TEST(configures_full_pipeline_integration); /* HTTP link pipeline integration */ /* Enrichment helpers */ RUN_TEST(enrichment_split_camel_case); RUN_TEST(enrichment_tokenize_decorator); /* Decorator tags integration */ RUN_TEST(decorator_tags_python_auto_discovery); RUN_TEST(decorator_tags_java_class_methods); /* Compile commands helpers */ RUN_TEST(compile_commands_split_command); RUN_TEST(compile_commands_extract_flags); RUN_TEST(compile_commands_parse_json); RUN_TEST(compile_commands_parse_empty); RUN_TEST(compile_commands_parse_invalid); /* Infrascan helpers */ RUN_TEST(infra_is_compose_file); RUN_TEST(infra_is_cloudbuild_file); RUN_TEST(infra_is_shell_script); RUN_TEST(infra_is_dockerfile); RUN_TEST(infra_is_kustomize_file); RUN_TEST(infra_is_k8s_manifest); RUN_TEST(infra_is_env_file); RUN_TEST(infra_clean_json_brackets); RUN_TEST(infra_secret_detection); /* Infrascan: Dockerfile parser */ RUN_TEST(infra_parse_dockerfile_multistage); RUN_TEST(infra_parse_dockerfile_entrypoint); RUN_TEST(infra_parse_dockerfile_secret_filtered); RUN_TEST(infra_parse_dockerfile_expose_protocol); RUN_TEST(infra_parse_dockerfile_env_space); RUN_TEST(infra_parse_dockerfile_empty); /* Infrascan: Dotenv parser */ RUN_TEST(infra_parse_dotenv); RUN_TEST(infra_parse_dotenv_quoted); /* Infrascan: Shell script parser */ RUN_TEST(infra_parse_shell); RUN_TEST(infra_parse_shell_with_source); RUN_TEST(infra_parse_shell_secret_filtered); RUN_TEST(infra_parse_shell_shebang_only); RUN_TEST(infra_parse_shell_truly_empty); /* Infrascan: Terraform parser */ RUN_TEST(infra_parse_terraform_full); RUN_TEST(infra_parse_terraform_variables_only); RUN_TEST(infra_parse_terraform_empty); RUN_TEST(helm_parse_chart_dependencies_issue338); RUN_TEST(helm_parse_chart_no_deps_issue338); /* Infrascan: QN helper */ RUN_TEST(infra_qn_helper); /* Infrascan: pipeline integration */ RUN_TEST(infra_pipeline_integration); RUN_TEST(infra_pipeline_idempotent); /* K8s / Kustomize extraction */ RUN_TEST(k8s_extract_kustomize); RUN_TEST(k8s_extract_manifest); RUN_TEST(k8s_extract_manifest_no_name); RUN_TEST(k8s_extract_manifest_multidoc); /* Env URL scanning */ RUN_TEST(envscan_dockerfile_env_urls); RUN_TEST(envscan_shell_env_urls); RUN_TEST(envscan_env_file_urls); RUN_TEST(envscan_toml_urls); RUN_TEST(envscan_yaml_urls); RUN_TEST(envscan_terraform_urls); RUN_TEST(envscan_properties_urls); RUN_TEST(envscan_secret_key_exclusion); RUN_TEST(envscan_secret_value_exclusion); RUN_TEST(envscan_secret_file_exclusion); RUN_TEST(envscan_skips_ignored_dirs); RUN_TEST(envscan_non_url_values_skipped); /* Discovery-exclusion plumbing in auxiliary repo walks (#792) */ RUN_TEST(pipeline_relpath_excluded_boundary); RUN_TEST(pkgmap_scan_repo_honors_discovery_exclusions); RUN_TEST(envscan_walk_honors_discovery_exclusions); /* Function registry / resolver */ RUN_TEST(registry_resolve_single_candidate); RUN_TEST(registry_fuzzy_nonexistent); RUN_TEST(registry_fuzzy_multiple_best_by_distance); RUN_TEST(registry_fuzzy_simple_name_extraction); RUN_TEST(registry_fuzzy_empty); RUN_TEST(registry_exists); RUN_TEST(registry_confidence_import_map); RUN_TEST(registry_confidence_import_map_suffix); RUN_TEST(registry_confidence_same_module); RUN_TEST(registry_confidence_unique_name); RUN_TEST(registry_confidence_suffix_match); RUN_TEST(registry_fuzzy_confidence_single); RUN_TEST(registry_fuzzy_confidence_distance); RUN_TEST(registry_negative_import_rejects); RUN_TEST(registry_fuzzy_import_penalty); RUN_TEST(registry_fuzzy_no_import_map_passthrough); RUN_TEST(registry_find_by_name); RUN_TEST(registry_confidence_band); RUN_TEST(registry_is_import_reachable); RUN_TEST(registry_find_ending_with); /* Git history */ RUN_TEST(githistory_is_trackable_file); RUN_TEST(githistory_compute_change_coupling); RUN_TEST(githistory_coupling_skips_large_commits); RUN_TEST(githistory_coupling_limits_output); /* Incremental reindex */ /* FastAPI Depends edge tracking (PR #66 port) */ RUN_TEST(pipeline_fastapi_depends_edges); /* Incremental */ RUN_TEST(incremental_full_then_noop); RUN_TEST(incremental_detects_changed_file); RUN_TEST(incremental_detects_deleted_file); RUN_TEST(incremental_new_file_added); RUN_TEST(incremental_fast_preserves_mode_skipped_tools_dir); RUN_TEST(incremental_k8s_manifest_indexed); RUN_TEST(incremental_kustomize_module_indexed); /* Resource management & internal helper tests */ RUN_TEST(pipeline_empty_path); RUN_TEST(pipeline_project_name_content); RUN_TEST(pipeline_cancel_sets_flag); RUN_TEST(pipeline_double_cancel); RUN_TEST(pipeline_double_free_prevention); /* Trackable file tests */ RUN_TEST(trackable_source_files); RUN_TEST(trackable_rejects_images); RUN_TEST(trackable_rejects_minified); RUN_TEST(trackable_rejects_vendor_dirs); RUN_TEST(trackable_rejects_lock_files); /* Test path detection */ RUN_TEST(test_path_directory_patterns); RUN_TEST(test_path_suffix_patterns); /* Test function name detection */ RUN_TEST(test_func_name_go_patterns); RUN_TEST(test_func_name_js_helpers); /* Env var name detection */ RUN_TEST(env_var_name_valid); RUN_TEST(env_var_name_invalid); /* Config extension detection */ RUN_TEST(config_ext_positive); RUN_TEST(config_ext_negative); /* Camel case splitting */ RUN_TEST(split_camel_basic); RUN_TEST(split_camel_single_word); RUN_TEST(split_camel_empty); /* Decorator tokenization */ RUN_TEST(tokenize_decorator_login_required); RUN_TEST(tokenize_decorator_single); /* Command splitting */ RUN_TEST(split_command_basic); RUN_TEST(split_command_quoted); RUN_TEST(split_command_empty); /* Range parsing */ RUN_TEST(parse_range_with_count); RUN_TEST(parse_range_single); RUN_TEST(parse_range_zero_count); /* Name status parsing */ RUN_TEST(parse_name_status_basic); RUN_TEST(parse_name_status_empty); /* Hunk parsing */ RUN_TEST(parse_hunks_empty); /* Change coupling */ RUN_TEST(coupling_empty_commits); RUN_TEST(coupling_single_file_commit); /* JSON bracket cleaning */ RUN_TEST(clean_json_brackets_array); RUN_TEST(clean_json_brackets_plain); RUN_TEST(clean_json_brackets_empty); /* FQN computation */ RUN_TEST(fqn_compute_basic); RUN_TEST(fqn_compute_strips_ext); RUN_TEST(fqn_module_basic); RUN_TEST(fqn_folder_basic); /* Project name edge cases */ RUN_TEST(project_name_special_chars); RUN_TEST(project_name_trailing_slash); }