项目文件夹

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

7112 行
256 KiB
C

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
/*
* 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 <stdlib.h>
#include <string.h>
#include <stdatomic.h>
#include "foundation/compat_thread.h"
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#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);
}