/* * test_parallel.c — Tests for the three-phase parallel pipeline. * * Validates parity between sequential (4-pass) and parallel (3-phase) * pipeline modes on a small Go test fixture. * * Suite: suite_parallel */ #include "../src/foundation/compat.h" #include "test_framework.h" #include "test_helpers.h" #include "pipeline/pipeline.h" #include "pipeline/pipeline_internal.h" #include "pipeline/lsp_resolve.h" #include "pipeline/pass_lsp_cross.h" #include "pipeline/lsp_resolve.h" #include "pipeline/worker_pool.h" #include "graph_buffer/graph_buffer.h" #include "discover/discover.h" #include "foundation/platform.h" #include "foundation/log.h" #include "cbm.h" #include #include #include #include #include #include #include /* ── Helper: create temp test repo ───────────────────────────────── */ static char g_par_tmpdir[256]; static int setup_parallel_repo(void) { snprintf(g_par_tmpdir, sizeof(g_par_tmpdir), "/tmp/cbm_par_XXXXXX"); if (!cbm_mkdtemp(g_par_tmpdir)) return -1; char path[512]; /* main.go */ snprintf(path, sizeof(path), "%s/main.go", g_par_tmpdir); FILE *f = fopen(path, "w"); if (!f) return -1; fprintf(f, "package main\n\nimport \"pkg\"\n\n" "func main() {\n\tpkg.Serve()\n}\n"); fclose(f); /* pkg/ */ snprintf(path, sizeof(path), "%s/pkg", g_par_tmpdir); cbm_mkdir(path); /* pkg/service.go */ snprintf(path, sizeof(path), "%s/pkg/service.go", g_par_tmpdir); f = fopen(path, "w"); if (!f) return -1; fprintf(f, "package pkg\n\nimport \"pkg/util\"\n\n" "func Serve() {\n\tutil.Help()\n}\n"); fclose(f); /* pkg/util/ */ snprintf(path, sizeof(path), "%s/pkg/util", g_par_tmpdir); cbm_mkdir(path); /* pkg/util/helper.go */ snprintf(path, sizeof(path), "%s/pkg/util/helper.go", g_par_tmpdir); f = fopen(path, "w"); if (!f) return -1; fprintf(f, "package util\n\nfunc Help() {}\n"); fclose(f); return 0; } static void rm_rf(const char *path) { th_rmtree(path); } static void teardown_parallel_repo(void) { if (g_par_tmpdir[0]) rm_rf(g_par_tmpdir); g_par_tmpdir[0] = '\0'; } static void cbm_init_parallel_worker(int idx, void *ctx_ptr) { int *rcs = (int *)ctx_ptr; rcs[idx] = cbm_init(); } TEST(parallel_cbm_init_concurrent_idempotent) { enum { INIT_CALLS = 32, INIT_WORKERS = 4 }; int rcs[INIT_CALLS]; memset(rcs, 0x7f, sizeof(rcs)); cbm_parallel_for_opts_t opts = {.max_workers = INIT_WORKERS, .force_pthreads = false}; cbm_parallel_for(INIT_CALLS, cbm_init_parallel_worker, rcs, opts); for (int i = 0; i < INIT_CALLS; i++) { ASSERT_EQ(rcs[i], 0); } PASS(); } /* ── Run sequential pipeline on files, returning gbuf ─────────────── */ static cbm_gbuf_t *run_sequential(const char *project, const char *repo_path, cbm_file_info_t *files, int file_count) { cbm_gbuf_t *gbuf = cbm_gbuf_new(project, repo_path); cbm_registry_t *reg = cbm_registry_new(); atomic_int cancelled; atomic_init(&cancelled, 0); cbm_pipeline_ctx_t ctx = { .project_name = project, .repo_path = repo_path, .gbuf = gbuf, .registry = reg, .cancelled = &cancelled, }; cbm_init(); cbm_pipeline_pass_definitions(&ctx, files, file_count); cbm_pipeline_pass_calls(&ctx, files, file_count); cbm_pipeline_pass_usages(&ctx, files, file_count); cbm_pipeline_pass_semantic(&ctx, files, file_count); cbm_registry_free(reg); return gbuf; } /* ── Run parallel pipeline on files, returning gbuf ───────────────── */ static cbm_gbuf_t *run_parallel_with_extract_opts(const char *project, const char *repo_path, cbm_file_info_t *files, int file_count, int worker_count, const cbm_parallel_extract_opts_t *extract_opts) { cbm_gbuf_t *gbuf = cbm_gbuf_new(project, repo_path); cbm_registry_t *reg = cbm_registry_new(); atomic_int cancelled; atomic_init(&cancelled, 0); cbm_pipeline_ctx_t ctx = { .project_name = project, .repo_path = repo_path, .gbuf = gbuf, .registry = reg, .cancelled = &cancelled, }; _Atomic int64_t shared_ids; int64_t gbuf_next = cbm_gbuf_next_id(gbuf); atomic_init(&shared_ids, gbuf_next); CBMFileResult **result_cache = calloc((size_t)file_count, sizeof(CBMFileResult *)); cbm_init(); if (extract_opts) { cbm_parallel_extract_ex(&ctx, files, file_count, result_cache, &shared_ids, worker_count, extract_opts); } else { cbm_parallel_extract(&ctx, files, file_count, result_cache, &shared_ids, worker_count); } cbm_gbuf_set_next_id(gbuf, atomic_load(&shared_ids)); cbm_build_registry_from_cache(&ctx, files, file_count, result_cache); /* Cross-file LSP — mirrors run_parallel_pipeline ordering in pipeline.c. * Build the project-wide all_defs[] precondition, then feed it into * cbm_parallel_resolve where the fused resolve_worker invokes * cbm_pxc_run_one(_ts) per file BEFORE materializing CALLS edges. */ char **def_modules = (char **)calloc((size_t)file_count, sizeof(char *)); int def_count = 0; CBMLSPDef *all_defs = def_modules ? cbm_pxc_collect_all_defs(result_cache, files, file_count, ctx.project_name, def_modules, &def_count) : NULL; CBMModuleDefIndex *module_def_index = all_defs ? cbm_pxc_build_module_def_index(all_defs, def_count) : NULL; cbm_parallel_resolve(&ctx, files, file_count, result_cache, &shared_ids, worker_count, all_defs, def_count, def_modules, module_def_index, NULL /* cross_registries — tests use per-file path */); cbm_gbuf_set_next_id(gbuf, atomic_load(&shared_ids)); cbm_pxc_free_module_def_index(module_def_index); free(all_defs); if (def_modules) { for (int i = 0; i < file_count; i++) { free(def_modules[i]); } free(def_modules); } for (int i = 0; i < file_count; i++) if (result_cache[i]) cbm_free_result(result_cache[i]); free(result_cache); cbm_registry_free(reg); return gbuf; } static cbm_gbuf_t *run_parallel(const char *project, const char *repo_path, cbm_file_info_t *files, int file_count, int worker_count) { return run_parallel_with_extract_opts(project, repo_path, files, file_count, worker_count, NULL); } /* ── Parity Tests ─────────────────────────────────────────────────── */ static cbm_gbuf_t *g_seq_gbuf = NULL; static cbm_gbuf_t *g_par_gbuf = NULL; static int g_parity_setup_done = 0; static int ensure_parity_setup(void) { if (g_parity_setup_done) return 0; if (setup_parallel_repo() != 0) return -1; /* Discover files */ cbm_discover_opts_t opts = {.mode = CBM_MODE_FULL}; cbm_file_info_t *files = NULL; int file_count = 0; if (cbm_discover(g_par_tmpdir, &opts, &files, &file_count) != 0) return -1; const char *project = "par-test"; /* Build structure for both (need File/Folder nodes before definitions) */ /* For parity, we need the structure pass too. Let's just compare * definition/call/usage/semantic edge counts. */ /* Run both modes */ g_seq_gbuf = run_sequential(project, g_par_tmpdir, files, file_count); g_par_gbuf = run_parallel(project, g_par_tmpdir, files, file_count, 2); cbm_discover_free(files, file_count); g_parity_setup_done = 1; return 0; } static void parity_teardown(void) { if (g_seq_gbuf) { cbm_gbuf_free(g_seq_gbuf); g_seq_gbuf = NULL; } if (g_par_gbuf) { cbm_gbuf_free(g_par_gbuf); g_par_gbuf = NULL; } teardown_parallel_repo(); g_parity_setup_done = 0; } /* Node count parity */ TEST(parallel_node_count) { if (ensure_parity_setup() != 0) FAIL("setup failed"); int seq = cbm_gbuf_node_count(g_seq_gbuf); int par = cbm_gbuf_node_count(g_par_gbuf); ASSERT_GT(seq, 0); ASSERT_EQ(seq, par); PASS(); } /* Edge type parity tests */ static int assert_edge_type_parity(const char *type) { if (ensure_parity_setup() != 0) return -1; int seq = cbm_gbuf_edge_count_by_type(g_seq_gbuf, type); int par = cbm_gbuf_edge_count_by_type(g_par_gbuf, type); if (seq != par) { printf(" FAIL: %s edges: seq=%d par=%d\n", type, seq, par); return 1; } return 0; } TEST(parallel_calls_parity) { int rc = assert_edge_type_parity("CALLS"); if (rc == -1) FAIL("setup failed"); ASSERT_EQ(rc, 0); PASS(); } TEST(parallel_defines_parity) { int rc = assert_edge_type_parity("DEFINES"); if (rc == -1) FAIL("setup failed"); ASSERT_EQ(rc, 0); PASS(); } TEST(parallel_defines_method_parity) { int rc = assert_edge_type_parity("DEFINES_METHOD"); if (rc == -1) FAIL("setup failed"); ASSERT_EQ(rc, 0); PASS(); } TEST(parallel_imports_parity) { int rc = assert_edge_type_parity("IMPORTS"); if (rc == -1) FAIL("setup failed"); ASSERT_EQ(rc, 0); PASS(); } TEST(parallel_usage_parity) { int rc = assert_edge_type_parity("USAGE"); if (rc == -1) FAIL("setup failed"); ASSERT_EQ(rc, 0); PASS(); } TEST(parallel_inherits_parity) { int rc = assert_edge_type_parity("INHERITS"); if (rc == -1) FAIL("setup failed"); ASSERT_EQ(rc, 0); PASS(); } TEST(parallel_implements_parity) { int rc = assert_edge_type_parity("IMPLEMENTS"); if (rc == -1) FAIL("setup failed"); ASSERT_EQ(rc, 0); PASS(); } TEST(parallel_total_edges) { if (ensure_parity_setup() != 0) FAIL("setup failed"); int seq = cbm_gbuf_edge_count(g_seq_gbuf); int par = cbm_gbuf_edge_count(g_par_gbuf); ASSERT_GT(seq, 0); ASSERT_EQ(seq, par); PASS(); } /* ── Empty file list ──────────────────────────────────────────────── */ TEST(parallel_empty_files) { cbm_gbuf_t *gbuf = cbm_gbuf_new("empty-proj", "/tmp"); cbm_registry_t *reg = cbm_registry_new(); atomic_int cancelled; atomic_init(&cancelled, 0); cbm_pipeline_ctx_t ctx = { .project_name = "empty-proj", .repo_path = "/tmp", .gbuf = gbuf, .registry = reg, .cancelled = &cancelled, }; _Atomic int64_t shared_ids; atomic_init(&shared_ids, 1); CBMFileResult **cache = NULL; int rc = cbm_parallel_extract(&ctx, NULL, 0, cache, &shared_ids, 2); ASSERT_EQ(rc, 0); ASSERT_EQ(cbm_gbuf_node_count(gbuf), 0); cbm_registry_free(reg); cbm_gbuf_free(gbuf); PASS(); } TEST(extraction_errors_are_nonfatal_in_parallel_and_sequential_paths) { char dir[256]; snprintf(dir, sizeof(dir), "/tmp/cbm_extract_error_XXXXXX"); ASSERT_TRUE(cbm_mkdtemp(dir) != NULL); char path[512]; snprintf(path, sizeof(path), "%s/input.txt", dir); FILE *f = fopen(path, "w"); ASSERT_NOT_NULL(f); fclose(f); cbm_file_info_t file = { .path = path, .rel_path = (char *)"input.txt", .language = CBM_LANG_PYTHON, }; atomic_int cancelled; atomic_init(&cancelled, 0); cbm_gbuf_t *gbuf = cbm_gbuf_new("extract-error", dir); cbm_registry_t *registry = cbm_registry_new(); ASSERT_NOT_NULL(gbuf); ASSERT_NOT_NULL(registry); cbm_pipeline_ctx_t ctx = { .project_name = "extract-error", .repo_path = dir, .gbuf = gbuf, .registry = registry, .cancelled = &cancelled, .pkgmap_preseeded = true, }; CBMFileResult *cache[1] = {NULL}; _Atomic int64_t shared_ids; atomic_init(&shared_ids, 1); ASSERT_EQ(cbm_parallel_extract(&ctx, &file, 1, cache, &shared_ids, 1), 0); ASSERT_NOT_NULL(cache[0]); ASSERT_FALSE(cache[0]->has_error); int nodes_after_empty = cbm_gbuf_node_count(gbuf); cbm_free_result(cache[0]); cache[0] = NULL; f = fopen(path, "w"); ASSERT_NOT_NULL(f); fputs("content\n", f); fclose(f); file.language = (CBMLanguage)-1; /* Unsupported-language extraction is a per-file skip, not a run-level * failure. Both paths must leave the graph unchanged and allow the caller * to publish the successfully indexed subset. The production pipeline * supplies ctx.pipeline and records this file in skipped[]. */ ASSERT_EQ(cbm_parallel_extract(&ctx, &file, 1, cache, &shared_ids, 1), 0); ASSERT_NULL(cache[0]); ASSERT_EQ(cbm_gbuf_node_count(gbuf), nodes_after_empty); ASSERT_EQ(cbm_pipeline_pass_definitions(&ctx, &file, 1), 0); ASSERT_EQ(cbm_gbuf_node_count(gbuf), nodes_after_empty); cbm_registry_free(registry); cbm_gbuf_free(gbuf); unlink(path); rmdir(dir); PASS(); } /* ── Regression: args JSON must not overflow the props buffer ──────── */ /* A call with many long string arguments makes append_args_json()'s running * position exceed the fixed CBM_SZ_2K `props` stack buffer in * emit_normal_calls_edge(): format_call_arg() returns snprintf's UNtruncated * length, so pos += n could run past the buffer and the trailing * buf[pos]='\0' wrote out of bounds (stack-buffer-overflow; caught by the * stack canary as a SIGABRT on real repos). This indexes a fixture whose * single call carries enough long args to drive pos past 2 KB; under the * ASan test build a regression aborts here. */ TEST(parallel_args_json_no_overflow) { char dir[256]; snprintf(dir, sizeof(dir), "/tmp/cbm_argov_XXXXXX"); ASSERT_TRUE(cbm_mkdtemp(dir) != NULL); char path[512]; snprintf(path, sizeof(path), "%s/app.ts", dir); FILE *f = fopen(path, "w"); ASSERT_TRUE(f != NULL); fputs("function sink(...xs: string[]) { return xs; }\n", f); fputs("function caller() {\n sink(\n", f); for (int i = 0; i < 60; i++) { /* 100-char string literal per arg; 60 args => args JSON well past the * 2 KB props buffer, forcing the pre-fix overshoot. */ fputs(" \"", f); for (int j = 0; j < 100; j++) fputc('a' + (i % 26), f); fputs(i < 59 ? "\",\n" : "\"\n", f); } fputs(" );\n}\n", f); fclose(f); cbm_discover_opts_t opts = {.mode = CBM_MODE_FULL}; cbm_file_info_t *files = NULL; int file_count = 0; ASSERT_EQ(cbm_discover(dir, &opts, &files, &file_count), 0); ASSERT_GT(file_count, 0); cbm_gbuf_t *gbuf = run_parallel("argov-test", dir, files, file_count, 4); ASSERT_TRUE(gbuf != NULL); ASSERT_GT(cbm_gbuf_edge_count(gbuf), 0); cbm_gbuf_free(gbuf); cbm_discover_free(files, file_count); th_rmtree(dir); PASS(); } typedef struct { int self_get_calls; } self_get_call_ctx_t; static void count_self_get_call_edges(const cbm_gbuf_edge_t *edge, void *ud) { self_get_call_ctx_t *c = ud; if (!edge || !edge->type || strcmp(edge->type, "CALLS") != 0) { return; } if (edge->source_id != edge->target_id) { return; } if (edge->properties_json && strstr(edge->properties_json, "\"callee\":\"ec.get\"")) { c->self_get_calls++; } } static int count_extracted_calls_named(const CBMFileResult *result, const char *callee_name) { int count = 0; if (!result || !callee_name) { return 0; } for (int i = 0; i < result->calls.count; i++) { const char *got = result->calls.items[i].callee_name; if (got && strcmp(got, callee_name) == 0) { count++; } } return count; } TEST(parallel_unresolved_route_suffix_does_not_emit_self_call) { char dir[256]; snprintf(dir, sizeof(dir), "/tmp/cbm_route_suffix_XXXXXX"); ASSERT_TRUE(cbm_mkdtemp(dir) != NULL); const char *source = "def FilterPanel(ec, e):\n" " return ec.get(e.type)\n"; CBMFileResult *extracted = cbm_extract_file(source, (int)strlen(source), CBM_LANG_PYTHON, "cbm_route_suffix", "app.py", 0, NULL, NULL); ASSERT_NOT_NULL(extracted); ASSERT_GT(count_extracted_calls_named(extracted, "ec.get"), 0); cbm_free_result(extracted); char path[512]; snprintf(path, sizeof(path), "%s/app.py", dir); ASSERT_EQ(th_write_file(path, source), 0); cbm_file_info_t files[1] = {0}; files[0].path = path; files[0].rel_path = (char *)"app.py"; files[0].language = CBM_LANG_PYTHON; cbm_gbuf_t *gbuf = run_parallel("cbm_route_suffix", dir, files, 1, 1); ASSERT_NOT_NULL(gbuf); self_get_call_ctx_t c = {0}; cbm_gbuf_foreach_edge(gbuf, count_self_get_call_edges, &c); ASSERT_EQ(c.self_get_calls, 0); cbm_gbuf_free(gbuf); th_rmtree(dir); PASS(); } typedef struct { const char *url_path; int route_registration_calls; } route_registration_count_ctx_t; static void count_route_registration_edges(const cbm_gbuf_edge_t *edge, void *ud) { route_registration_count_ctx_t *c = ud; if (!edge || !edge->type || strcmp(edge->type, "CALLS") != 0 || !edge->properties_json) { return; } if (strstr(edge->properties_json, "\"via\":\"route_registration\"") && strstr(edge->properties_json, c->url_path)) { c->route_registration_calls++; } } static int count_route_registration_for_path(cbm_gbuf_t *gbuf, const char *url_path) { route_registration_count_ctx_t c = {.url_path = url_path, .route_registration_calls = 0}; cbm_gbuf_foreach_edge(gbuf, count_route_registration_edges, &c); return c.route_registration_calls; } static void count_exception_edges(const cbm_gbuf_edge_t *edge, void *ud) { int *count = (int *)ud; if (!edge || !edge->type || !count) { return; } if (strcmp(edge->type, "THROWS") == 0 || strcmp(edge->type, "RAISES") == 0) { (*count)++; } } static int exception_edge_count(cbm_gbuf_t *gbuf) { int count = 0; cbm_gbuf_foreach_edge(gbuf, count_exception_edges, &count); return count; } TEST(parallel_top_level_raise_matches_sequential_no_file_fallback) { char dir[CBM_PATH_MAX]; int n = snprintf(dir, sizeof(dir), "%s/cbm_top_raise_XXXXXX", cbm_tmpdir()); ASSERT_GT(n, 0); ASSERT_LT((size_t)n, sizeof(dir)); ASSERT_TRUE(cbm_mkdtemp(dir) != NULL); const char *source = "class HTTPException(Exception):\n" " pass\n\n" "raise HTTPException()\n"; char path[CBM_PATH_MAX]; n = snprintf(path, sizeof(path), "%s/app.py", dir); ASSERT_GT(n, 0); ASSERT_LT((size_t)n, sizeof(path)); ASSERT_EQ(th_write_file(path, source), 0); cbm_file_info_t files[1] = {0}; files[0].path = path; files[0].rel_path = (char *)"app.py"; files[0].language = CBM_LANG_PYTHON; cbm_gbuf_t *seq = run_sequential("cbm_top_raise", dir, files, 1); cbm_gbuf_t *par = run_parallel("cbm_top_raise", dir, files, 1, 1); ASSERT_NOT_NULL(seq); ASSERT_NOT_NULL(par); ASSERT_EQ(exception_edge_count(seq), 0); ASSERT_EQ(exception_edge_count(par), 0); cbm_gbuf_free(seq); cbm_gbuf_free(par); th_rmtree(dir); PASS(); } TEST(parallel_fastapi_websocket_route_registration_matches_sequential) { char dir[256]; snprintf(dir, sizeof(dir), "/tmp/cbm_ws_routes_XXXXXX"); ASSERT_TRUE(cbm_mkdtemp(dir) != NULL); const char *source = "from fastapi import APIRouter, WebSocket\n\n" "router = APIRouter()\n\n" "@router.websocket('/custom_error/')\n" "async def router_ws_custom_error(websocket: WebSocket):\n" " raise RuntimeError('boom')\n\n" "@router.websocket_route('/router')\n" "async def routerindex(websocket: WebSocket):\n" " await websocket.accept()\n"; char path[512]; snprintf(path, sizeof(path), "%s/app.py", dir); ASSERT_EQ(th_write_file(path, source), 0); cbm_file_info_t files[1] = {0}; files[0].path = path; files[0].rel_path = (char *)"app.py"; files[0].language = CBM_LANG_PYTHON; cbm_gbuf_t *seq = run_sequential("cbm_ws_routes", dir, files, 1); cbm_gbuf_t *par = run_parallel("cbm_ws_routes", dir, files, 1, 1); ASSERT_NOT_NULL(seq); ASSERT_NOT_NULL(par); ASSERT_EQ(count_route_registration_for_path(seq, "/custom_error/"), 1); ASSERT_EQ(count_route_registration_for_path(seq, "/router"), 1); ASSERT_EQ(count_route_registration_for_path(par, "/custom_error/"), 1); ASSERT_EQ(count_route_registration_for_path(par, "/router"), 1); cbm_gbuf_free(seq); cbm_gbuf_free(par); th_rmtree(dir); PASS(); } /* ── Production pipeline worker-count parity ─────────────────────── */ enum { PARITY_REPO_FILE_COUNT = 64, PARITY_EXPECTED_FILE_HASHES = PARITY_REPO_FILE_COUNT + 2, PARITY_DB_PATH_COUNT = 4, PARITY_PATH_BUF = CBM_SZ_512, PARITY_SOURCE_BUF = CBM_SZ_4K, PARITY_REP_QN_COUNT = 4, }; typedef struct { int nodes; int edges; int file_hashes; int calls; int imports; int usage; int semantic; int representative_qns; } pipeline_db_counts_t; static int parity_format(char *dst, size_t dst_sz, const char *fmt, ...) { if (!dst || dst_sz == 0 || !fmt) { return CBM_NOT_FOUND; } va_list ap; va_start(ap, fmt); int n = vsnprintf(dst, dst_sz, fmt, ap); va_end(ap); return n >= 0 && (size_t)n < dst_sz ? 0 : CBM_NOT_FOUND; } static void remove_sqlite_family(const char *db_path) { if (!db_path || !db_path[0]) { return; } cbm_unlink(db_path); char sidecar[PARITY_PATH_BUF]; if (parity_format(sidecar, sizeof(sidecar), "%s-wal", db_path) == 0) { cbm_unlink(sidecar); } if (parity_format(sidecar, sizeof(sidecar), "%s-shm", db_path) == 0) { cbm_unlink(sidecar); } } static int sqlite_integrity_ok(const char *db_path) { sqlite3 *db = NULL; sqlite3_stmt *stmt = NULL; int ok = 0; if (sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READONLY, NULL) == SQLITE_OK && db && sqlite3_prepare_v2(db, "PRAGMA integrity_check;", CBM_NOT_FOUND, &stmt, NULL) == SQLITE_OK && sqlite3_step(stmt) == SQLITE_ROW) { const char *msg = (const char *)sqlite3_column_text(stmt, 0); ok = msg && strcmp(msg, "ok") == 0; } if (stmt) { sqlite3_finalize(stmt); } if (db) { sqlite3_close(db); } return ok; } static int write_worker_parity_repo(char *repo_dir, size_t repo_dir_sz) { if (!repo_dir || repo_dir_sz == 0) { return CBM_NOT_FOUND; } if (parity_format(repo_dir, repo_dir_sz, "%s/cbm_pipe_parity_XXXXXX", cbm_tmpdir()) != 0) { return CBM_NOT_FOUND; } if (!cbm_mkdtemp(repo_dir)) { return CBM_NOT_FOUND; } char path[PARITY_PATH_BUF]; char src[PARITY_SOURCE_BUF]; if (parity_format(path, sizeof(path), "%s/common.py", repo_dir) != 0) { return CBM_NOT_FOUND; } if (th_write_file(path, "def shared(value):\n" " return value + 1\n" "\n" "class Shared:\n" " def touch(self, value):\n" " return shared(value)\n") != 0) { return CBM_NOT_FOUND; } for (int i = 0; i < PARITY_REPO_FILE_COUNT; i++) { if (parity_format(path, sizeof(path), "%s/mod_%02d.py", repo_dir, i) != 0) { return CBM_NOT_FOUND; } int prev = (i + PARITY_REPO_FILE_COUNT - 1) % PARITY_REPO_FILE_COUNT; if (parity_format(src, sizeof(src), "from common import Shared, shared\n" "from mod_%02d import func_%02d\n" "\n" "class Worker%02d:\n" " def method_%02d(self, value):\n" " helper = Shared()\n" " return helper.touch(shared(value))\n" "\n" "def func_%02d(value):\n" " item = Worker%02d()\n" " return item.method_%02d(value)\n" "\n" "def chain_%02d(value):\n" " return func_%02d(value) + func_%02d(value) + shared(value)\n", prev, prev, i, i, i, i, i, i, i, prev) != 0) { return CBM_NOT_FOUND; } if (th_write_file(path, src) != 0) { return CBM_NOT_FOUND; } } if (parity_format(path, sizeof(path), "%s/app.py", repo_dir) != 0) { return CBM_NOT_FOUND; } FILE *f = fopen(path, "w"); if (!f) { return CBM_NOT_FOUND; } int write_ok = 1; for (int i = 0; i < PARITY_REPO_FILE_COUNT; i++) { write_ok = write_ok && fprintf(f, "from mod_%02d import chain_%02d\n", i, i) >= 0; } write_ok = write_ok && fputs("\ndef main():\n total = 0\n", f) >= 0; for (int i = 0; i < PARITY_REPO_FILE_COUNT; i++) { write_ok = write_ok && fprintf(f, " total += chain_%02d(%d)\n", i, i) >= 0; } write_ok = write_ok && fputs(" return total\n", f) >= 0; if (fclose(f) != 0) { write_ok = 0; } return write_ok ? 0 : CBM_NOT_FOUND; } static int count_representative_qns(cbm_store_t *store) { static const char *qns[PARITY_REP_QN_COUNT] = { "pipe-parity.common.shared", "pipe-parity.common.Shared.touch", "pipe-parity.mod_00.func_00", "pipe-parity.app.main", }; int found = 0; for (int i = 0; i < PARITY_REP_QN_COUNT; i++) { cbm_node_t node = {0}; if (cbm_store_find_node_by_qn(store, "pipe-parity", qns[i], &node) == CBM_STORE_OK) { found++; cbm_node_free_fields(&node); } } return found; } static int run_pipeline_worker_case(const char *repo_dir, const char *db_path, int workers, pipeline_db_counts_t *out) { if (!repo_dir || !db_path || !out) { return CBM_NOT_FOUND; } char worker_buf[CBM_SZ_32]; if (parity_format(worker_buf, sizeof(worker_buf), "%d", workers) != 0) { return CBM_NOT_FOUND; } if (workers > 0) { cbm_setenv("CBM_WORKERS", worker_buf, 1); } else { cbm_unsetenv("CBM_WORKERS"); } remove_sqlite_family(db_path); cbm_pipeline_t *p = cbm_pipeline_new(repo_dir, db_path, CBM_MODE_FULL); if (!p) { return CBM_NOT_FOUND; } cbm_pipeline_set_project_name(p, "pipe-parity"); int rc = cbm_pipeline_run(p); cbm_pipeline_free(p); if (rc != 0 || !sqlite_integrity_ok(db_path)) { return CBM_NOT_FOUND; } cbm_store_t *store = cbm_store_open_path_query(db_path); if (!store) { return CBM_NOT_FOUND; } cbm_file_hash_t *hashes = NULL; int hash_count = 0; int hash_rc = cbm_store_get_file_hashes(store, "pipe-parity", &hashes, &hash_count); out->nodes = cbm_store_count_nodes(store, "pipe-parity"); out->edges = cbm_store_count_edges(store, "pipe-parity"); out->calls = cbm_store_count_edges_by_type(store, "pipe-parity", "CALLS"); out->imports = cbm_store_count_edges_by_type(store, "pipe-parity", "IMPORTS"); out->usage = cbm_store_count_edges_by_type(store, "pipe-parity", "USAGE"); out->semantic = cbm_store_count_edges_by_type(store, "pipe-parity", "SEMANTICALLY_RELATED"); out->file_hashes = hash_rc == CBM_STORE_OK ? hash_count : CBM_STORE_ERR; out->representative_qns = count_representative_qns(store); cbm_store_free_file_hashes(hashes, hash_count); cbm_project_t project = {0}; int project_rc = cbm_store_get_project(store, "pipe-parity", &project); int project_root_ok = project_rc == CBM_STORE_OK && project.root_path && strcmp(project.root_path, repo_dir) == 0; cbm_project_free_fields(&project); cbm_store_close(store); return (project_root_ok && out->nodes > 0 && out->edges > 0 && out->file_hashes == PARITY_EXPECTED_FILE_HASHES && out->calls > 0 && out->imports > 0 && out->representative_qns == PARITY_REP_QN_COUNT) ? 0 : CBM_NOT_FOUND; } static int assert_pipeline_counts_equal(const pipeline_db_counts_t *want, const pipeline_db_counts_t *got) { if (!want || !got) { return CBM_NOT_FOUND; } return want->nodes == got->nodes && want->edges == got->edges && want->file_hashes == got->file_hashes && want->calls == got->calls && want->imports == got->imports && want->usage == got->usage && want->semantic == got->semantic && want->representative_qns == got->representative_qns ? 0 : CBM_NOT_FOUND; } TEST(parallel_full_pipeline_worker_count_parity_64_files) { char saved_workers[CBM_SZ_32] = {0}; bool had_workers = cbm_safe_getenv("CBM_WORKERS", saved_workers, sizeof(saved_workers), NULL) != NULL; char repo_dir[PARITY_PATH_BUF] = {0}; int rc = write_worker_parity_repo(repo_dir, sizeof(repo_dir)); const int workers[PARITY_DB_PATH_COUNT] = {1, 2, 4, 0}; char db_paths[PARITY_DB_PATH_COUNT][PARITY_PATH_BUF] = {{0}}; pipeline_db_counts_t counts[PARITY_DB_PATH_COUNT] = {{0}}; if (rc == 0) { for (int i = 0; i < PARITY_DB_PATH_COUNT; i++) { if (parity_format(db_paths[i], sizeof(db_paths[i]), "%s/pipe-parity-%d.db", repo_dir, i) != 0) { rc = CBM_NOT_FOUND; break; } if (run_pipeline_worker_case(repo_dir, db_paths[i], workers[i], &counts[i]) != 0) { rc = CBM_NOT_FOUND; break; } } } if (rc == 0) { for (int i = 1; i < PARITY_DB_PATH_COUNT; i++) { if (assert_pipeline_counts_equal(&counts[0], &counts[i]) != 0) { rc = CBM_NOT_FOUND; break; } } } for (int i = 0; i < PARITY_DB_PATH_COUNT; i++) { remove_sqlite_family(db_paths[i]); } if (repo_dir[0]) { th_rmtree(repo_dir); } if (had_workers) { cbm_setenv("CBM_WORKERS", saved_workers, 1); } else { cbm_unsetenv("CBM_WORKERS"); } ASSERT_EQ(rc, 0); PASS(); } /* ── Graph buffer merge tests ─────────────────────────────────────── */ TEST(gbuf_shared_ids_unique) { _Atomic int64_t shared = 1; cbm_gbuf_t *ga = cbm_gbuf_new_shared_ids("proj", "/", &shared); cbm_gbuf_t *gb = cbm_gbuf_new_shared_ids("proj", "/", &shared); int64_t id1 = cbm_gbuf_upsert_node(ga, "Function", "foo", "proj.foo", "a.go", 1, 5, "{}"); int64_t id2 = cbm_gbuf_upsert_node(gb, "Function", "bar", "proj.bar", "b.go", 1, 3, "{}"); ASSERT_GT(id1, 0); ASSERT_GT(id2, 0); ASSERT_NEQ(id1, id2); cbm_gbuf_free(ga); cbm_gbuf_free(gb); PASS(); } TEST(gbuf_merge_nodes) { _Atomic int64_t shared = 1; cbm_gbuf_t *dst = cbm_gbuf_new_shared_ids("proj", "/", &shared); cbm_gbuf_t *src = cbm_gbuf_new_shared_ids("proj", "/", &shared); cbm_gbuf_upsert_node(dst, "Function", "a", "proj.a", "a.go", 1, 5, "{}"); cbm_gbuf_upsert_node(dst, "Function", "b", "proj.b", "a.go", 6, 10, "{}"); cbm_gbuf_upsert_node(src, "Function", "c", "proj.c", "b.go", 1, 5, "{}"); cbm_gbuf_upsert_node(src, "Function", "d", "proj.d", "b.go", 6, 10, "{}"); ASSERT_EQ(cbm_gbuf_node_count(dst), 2); cbm_gbuf_merge(dst, src); ASSERT_EQ(cbm_gbuf_node_count(dst), 4); ASSERT_NOT_NULL(cbm_gbuf_find_by_qn(dst, "proj.c")); ASSERT_NOT_NULL(cbm_gbuf_find_by_qn(dst, "proj.d")); /* dst originals still there */ ASSERT_NOT_NULL(cbm_gbuf_find_by_qn(dst, "proj.a")); ASSERT_NOT_NULL(cbm_gbuf_find_by_qn(dst, "proj.b")); cbm_gbuf_free(src); cbm_gbuf_free(dst); PASS(); } TEST(gbuf_merge_edges) { _Atomic int64_t shared = 1; cbm_gbuf_t *dst = cbm_gbuf_new_shared_ids("proj", "/", &shared); cbm_gbuf_t *src = cbm_gbuf_new_shared_ids("proj", "/", &shared); int64_t a = cbm_gbuf_upsert_node(dst, "Function", "a", "proj.a", "a.go", 1, 5, "{}"); int64_t b = cbm_gbuf_upsert_node(dst, "Function", "b", "proj.b", "a.go", 6, 10, "{}"); /* Put an edge in src that references dst nodes (by ID) */ cbm_gbuf_insert_edge(src, a, b, "CALLS", "{}"); cbm_gbuf_merge(dst, src); ASSERT_GT(cbm_gbuf_edge_count(dst), 0); const cbm_gbuf_edge_t **edges = NULL; int count = 0; cbm_gbuf_find_edges_by_source_type(dst, a, "CALLS", &edges, &count); ASSERT_EQ(count, 1); ASSERT_EQ(edges[0]->target_id, b); cbm_gbuf_free(src); cbm_gbuf_free(dst); PASS(); } TEST(gbuf_merge_empty_src) { _Atomic int64_t shared = 1; cbm_gbuf_t *dst = cbm_gbuf_new_shared_ids("proj", "/", &shared); cbm_gbuf_t *src = cbm_gbuf_new_shared_ids("proj", "/", &shared); cbm_gbuf_upsert_node(dst, "Function", "a", "proj.a", "a.go", 1, 5, "{}"); int before = cbm_gbuf_node_count(dst); cbm_gbuf_merge(dst, src); ASSERT_EQ(cbm_gbuf_node_count(dst), before); cbm_gbuf_free(src); cbm_gbuf_free(dst); PASS(); } TEST(gbuf_merge_src_free_safe) { _Atomic int64_t shared = 1; cbm_gbuf_t *dst = cbm_gbuf_new_shared_ids("proj", "/", &shared); cbm_gbuf_t *src = cbm_gbuf_new_shared_ids("proj", "/", &shared); cbm_gbuf_upsert_node(src, "Function", "x", "proj.x", "x.go", 1, 5, "{}"); cbm_gbuf_merge(dst, src); cbm_gbuf_free(src); /* must not crash */ /* dst node still accessible */ ASSERT_NOT_NULL(cbm_gbuf_find_by_qn(dst, "proj.x")); cbm_gbuf_free(dst); PASS(); } TEST(gbuf_next_id_accessors) { cbm_gbuf_t *gb = cbm_gbuf_new("proj", "/"); ASSERT_EQ(cbm_gbuf_next_id(gb), 1); cbm_gbuf_upsert_node(gb, "Function", "foo", "proj.foo", "f.go", 1, 5, "{}"); ASSERT_GT(cbm_gbuf_next_id(gb), 1); cbm_gbuf_set_next_id(gb, 100); int64_t id = cbm_gbuf_upsert_node(gb, "Function", "bar", "proj.bar", "f.go", 6, 10, "{}"); ASSERT_GTE(id, 100); cbm_gbuf_free(gb); PASS(); } TEST(lsp_resolution_matches_cpp_segments_and_reason_joins) { CBMResolvedCall items[] = { {.caller_qn = "proj.C.run", .callee_qn = "proj.C.doWork", .strategy = "lsp_type_dispatch", .confidence = 0.90f, .reason = NULL}, {.caller_qn = "proj.C.run", .callee_qn = "proj.target", .strategy = "lsp_func_ptr", .confidence = 0.85f, .reason = "fp"}, {.caller_qn = "proj.C.run", .callee_qn = "proj.C.~C", .strategy = "lsp_destructor", .confidence = 0.90f, .reason = "ptr"}, }; CBMResolvedCallArray arr = {.items = items, .count = 3, .cap = 3}; CBMCall member_call = {.enclosing_func_qn = "proj.C.run", .callee_name = "obj->doWork"}; ASSERT(cbm_pipeline_find_lsp_resolution(&arr, &member_call, false) == &items[0]); CBMCall scoped_call = {.enclosing_func_qn = "proj.C.run", .callee_name = "ns::doWork"}; ASSERT(cbm_pipeline_find_lsp_resolution(&arr, &scoped_call, false) == &items[0]); CBMCall fp_call = {.enclosing_func_qn = "proj.C.run", .callee_name = "fp"}; ASSERT(cbm_pipeline_find_lsp_resolution(&arr, &fp_call, false) == &items[1]); CBMCall dtor_call = {.enclosing_func_qn = "proj.C.run", .callee_name = "ptr"}; ASSERT(cbm_pipeline_find_lsp_resolution(&arr, &dtor_call, false) == &items[2]); PASS(); } TEST(lsp_resolution_index_matches_linear_cpp_semantics) { CBMResolvedCall items[] = { {.caller_qn = "proj.C.run", .callee_qn = "proj.C.doWork", .strategy = "lsp_type_dispatch", .confidence = 0.90f, .reason = NULL}, {.caller_qn = "proj.C.run", .callee_qn = "proj.target", .strategy = "lsp_func_ptr", .confidence = 0.85f, .reason = "fp"}, }; CBMResolvedCallArray arr = {.items = items, .count = 2, .cap = 2}; cbm_lsp_resolution_index_t idx = {0}; cbm_lsp_resolution_index_build(&idx, &arr, 2, 0.0); ASSERT_TRUE(idx.complete); CBMCall member_call = {.enclosing_func_qn = "proj.C.run", .callee_name = "obj->doWork"}; ASSERT(cbm_lsp_resolution_index_find(&idx, &arr, &member_call, 0.0, false) == &items[0]); CBMCall fp_call = {.enclosing_func_qn = "proj.C.run", .callee_name = "fp"}; ASSERT(cbm_lsp_resolution_index_find(&idx, &arr, &fp_call, 0.0, false) == &items[1]); cbm_lsp_resolution_index_free(&idx); PASS(); } TEST(lsp_resolution_index_overlong_key_falls_back_to_linear) { char caller[CBM_SZ_1K + CBM_SZ_128]; memset(caller, 'a', sizeof(caller) - 1); caller[sizeof(caller) - 1] = '\0'; CBMResolvedCall item = {.caller_qn = caller, .callee_qn = "proj.target", .strategy = "lsp_direct", .confidence = 0.95f, .reason = NULL}; CBMResolvedCallArray arr = {.items = &item, .count = 1, .cap = 1}; cbm_lsp_resolution_index_t idx = {0}; cbm_lsp_resolution_index_build(&idx, &arr, 1, 0.0); ASSERT_FALSE(idx.complete); CBMCall call = {.enclosing_func_qn = caller, .callee_name = "target"}; ASSERT(cbm_lsp_resolution_index_find(&idx, &arr, &call, 0.0, false) == &item); cbm_lsp_resolution_index_free(&idx); PASS(); } /* ── Parallel-pipeline LSP-override regression ────────────────────── */ /* Pin the wiring fix that unified pass_calls.c (sequential) and * pass_parallel.c (parallel) on cbm_pipeline_find_lsp_resolution + * CBM_LSP_CONFIDENCE_FLOOR (lsp_resolve.h). Before the unification, the * parallel path carried its own lsp_override_resolution_pp at floor 0.5 * while the sequential path used find_lsp_resolution at floor 0.6, so a * project produced different CALLS edge attributions depending on which * pipeline mode kicked in. This test indexes a small Python repo via * the parallel pipeline and asserts at least one resulting CALLS edge * carries an "lsp_*" strategy — proof the parallel path consults * result->resolved_calls and emits LSP-attributed edges. */ typedef struct { int lsp_strategy_count; int total_calls; } lsp_edge_count_ctx_t; static void count_lsp_call_edges(const cbm_gbuf_edge_t *edge, void *ud) { lsp_edge_count_ctx_t *c = ud; if (!edge || !edge->type || strcmp(edge->type, "CALLS") != 0) { return; } c->total_calls++; if (edge->properties_json && strstr(edge->properties_json, "\"strategy\":\"lsp")) { c->lsp_strategy_count++; } } static bool resolved_call_contains(const CBMResolvedCallArray *arr, const char *caller_sub, const char *callee_sub) { if (!arr || !caller_sub || !callee_sub) { return false; } for (int i = 0; i < arr->count; i++) { const CBMResolvedCall *rc = &arr->items[i]; if (rc->caller_qn && strstr(rc->caller_qn, caller_sub) && rc->callee_qn && strstr(rc->callee_qn, callee_sub)) { return true; } } return false; } typedef struct { const cbm_gbuf_t *gbuf; const char *source_sub; const char *target_sub; const char *props_sub; bool found; } call_edge_contains_ctx_t; static void call_edge_contains_visit(const cbm_gbuf_edge_t *edge, void *ud) { call_edge_contains_ctx_t *ctx = ud; if (!ctx || ctx->found || !edge || !edge->type || strcmp(edge->type, "CALLS") != 0) { return; } const cbm_gbuf_node_t *source = cbm_gbuf_find_by_id(ctx->gbuf, edge->source_id); const cbm_gbuf_node_t *target = cbm_gbuf_find_by_id(ctx->gbuf, edge->target_id); if (!source || !target || !source->qualified_name || !target->qualified_name) { return; } if (strstr(source->qualified_name, ctx->source_sub) && strstr(target->qualified_name, ctx->target_sub) && (!ctx->props_sub || (edge->properties_json && strstr(edge->properties_json, ctx->props_sub)))) { ctx->found = true; } } static bool call_edge_contains(const cbm_gbuf_t *gbuf, const char *source_sub, const char *target_sub, const char *props_sub) { if (!gbuf || !source_sub || !target_sub) { return false; } call_edge_contains_ctx_t ctx = { .gbuf = gbuf, .source_sub = source_sub, .target_sub = target_sub, .props_sub = props_sub, }; cbm_gbuf_foreach_edge(gbuf, call_edge_contains_visit, &ctx); return ctx.found; } static const char *class_method_tail(const char *qn) { if (!qn) { return NULL; } const char *last = strrchr(qn, '.'); if (!last || last == qn) { return NULL; } const char *second = last; while (second > qn) { second--; if (*second == '.') { return second == qn ? qn : second + 1; } } return qn; } static const cbm_gbuf_node_t *find_unique_callable_node_by_tail(const cbm_gbuf_t *gbuf, const char *tail) { const char *method = tail ? strrchr(tail, '.') : NULL; method = method ? method + 1 : tail; if (!gbuf || !tail || !method) { return NULL; } const cbm_gbuf_node_t **nodes = NULL; int count = 0; if (cbm_gbuf_find_by_name(gbuf, method, &nodes, &count) != 0) { return NULL; } const cbm_gbuf_node_t *match = NULL; for (int i = 0; i < count; i++) { const cbm_gbuf_node_t *node = nodes[i]; if (!node || !node->label || !node->qualified_name) { continue; } if (strcmp(node->label, "Method") != 0 && strcmp(node->label, "Function") != 0) { continue; } const char *node_tail = class_method_tail(node->qualified_name); if (!node_tail || strcmp(node_tail, tail) != 0) { continue; } if (match) { return NULL; } match = node; } return match; } static const cbm_gbuf_edge_t *find_calls_edge_by_tails(const cbm_gbuf_t *gbuf, const char *source_tail, const char *target_tail) { const cbm_gbuf_node_t *source = find_unique_callable_node_by_tail(gbuf, source_tail); const cbm_gbuf_node_t *target = find_unique_callable_node_by_tail(gbuf, target_tail); if (!source || !target) { return NULL; } const cbm_gbuf_edge_t **edges = NULL; int count = 0; if (cbm_gbuf_find_edges_by_source_type(gbuf, source->id, "CALLS", &edges, &count) != 0) { return NULL; } for (int i = 0; i < count; i++) { if (edges[i] && edges[i]->target_id == target->id) { return edges[i]; } } return NULL; } TEST(parallel_java_kotlin_lsp_override_cross_file_emits_lsp_strategy_edges) { char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_par_jvm_XXXXXX"); if (!cbm_mkdtemp(tmpdir)) { FAIL("mkdtemp failed"); } char jpath[512]; snprintf(jpath, sizeof(jpath), "%s/src/main/java/com/example/Example.java", tmpdir); char jdir[512]; snprintf(jdir, sizeof(jdir), "%s/src/main/java/com/example", tmpdir); cbm_mkdir_p(jdir, 0755); FILE *jf = fopen(jpath, "w"); if (!jf) { FAIL("fopen example.java failed"); } fprintf(jf, "package com.example;\n" "\n" "class JavaCaller {\n" " String call(KotlinService kotlinService) {\n" " return kotlinService.ping(new JavaService());\n" " }\n" "}\n" "\n" "class JavaService {\n" " String pong() {\n" " return \"pong\";\n" " }\n" "}\n"); fclose(jf); char kpath[512]; snprintf(kpath, sizeof(kpath), "%s/src/main/kotlin/com/example/KotlinService.kt", tmpdir); char kdir[512]; snprintf(kdir, sizeof(kdir), "%s/src/main/kotlin/com/example", tmpdir); cbm_mkdir_p(kdir, 0755); FILE *kf = fopen(kpath, "w"); if (!kf) { unlink(jpath); rmdir(tmpdir); FAIL("fopen example.kt failed"); } fprintf(kf, "package com.example\n" "\n" "class KotlinService {\n" " fun ping(javaService: JavaService): String {\n" " return javaService.pong()\n" " }\n" "}\n"); fclose(kf); cbm_file_info_t files[2] = {0}; files[0].path = jpath; files[0].rel_path = (char *)"src/main/java/com/example/Example.java"; files[0].language = CBM_LANG_JAVA; files[1].path = kpath; files[1].rel_path = (char *)"src/main/kotlin/com/example/KotlinService.kt"; files[1].language = CBM_LANG_KOTLIN; cbm_gbuf_t *gbuf = run_parallel("com", tmpdir, files, 2, 2); ASSERT_NOT_NULL(gbuf); const cbm_gbuf_edge_t *java_to_kotlin = find_calls_edge_by_tails(gbuf, "JavaCaller.call", "KotlinService.ping"); const cbm_gbuf_edge_t *kotlin_to_java = find_calls_edge_by_tails(gbuf, "KotlinService.ping", "JavaService.pong"); ASSERT_NOT_NULL(java_to_kotlin); ASSERT_NOT_NULL(kotlin_to_java); ASSERT_NOT_NULL(java_to_kotlin->properties_json); ASSERT_NOT_NULL(kotlin_to_java->properties_json); ASSERT_NOT_NULL(strstr(java_to_kotlin->properties_json, "\"strategy\":\"lsp")); ASSERT_NOT_NULL(strstr(kotlin_to_java->properties_json, "\"strategy\":\"lsp")); ASSERT_TRUE(strstr(java_to_kotlin->properties_json, "\"strategy\":\"callee_suffix\"") == NULL); ASSERT_TRUE(strstr(kotlin_to_java->properties_json, "\"strategy\":\"callee_suffix\"") == NULL); cbm_gbuf_free(gbuf); unlink(kpath); unlink(jpath); rmdir(tmpdir); PASS(); } /* Gate guard for the JVM-only unique-tail fallbacks (lsp_resolve.h). * * The tail fallbacks join LSP overrides across QN drift by unique * "Class.method" leaf. That is only sound where class-per-file package * semantics hold (Java/Kotlin); in any other language a single * wrong-module coincidence would fabricate a CALLS edge, so * cbm_pipeline_lsp_allow_tail_match must keep the fallbacks OFF there. * * NOTE: a natural end-to-end non-JVM coincidence fixture is impractical: * reaching the fallbacks requires the LSP and the textual extraction to * disagree on QN prefixes, which path-derived single-root languages do * not produce in a small fixture (that drift is precisely the JVM * mixed-source-root symptom the fallback exists for). So this test * exercises the gated branches directly: the SAME wrong-module * coincidence must resolve with the gate open (JVM) and must NOT with * the gate closed. If the gate were removed — fallbacks made * unconditional again — the gate-closed assertions below would fail. */ TEST(parallel_lsp_tail_match_fallbacks_gated_to_jvm) { /* Policy: exactly the JVM languages. */ ASSERT_TRUE(cbm_pipeline_lsp_allow_tail_match(CBM_LANG_JAVA)); ASSERT_TRUE(cbm_pipeline_lsp_allow_tail_match(CBM_LANG_KOTLIN)); ASSERT_TRUE(!cbm_pipeline_lsp_allow_tail_match(CBM_LANG_PYTHON)); ASSERT_TRUE(!cbm_pipeline_lsp_allow_tail_match(CBM_LANG_GO)); ASSERT_TRUE(!cbm_pipeline_lsp_allow_tail_match(CBM_LANG_TYPESCRIPT)); ASSERT_TRUE(!cbm_pipeline_lsp_allow_tail_match(CBM_LANG_CPP)); /* Wrong-module coincidence: the resolved entry's caller shares only * the "Service.handle" tail with the textual call's enclosing * function, so the exact caller_qn pass misses and only the tail * fallback could join them. */ CBMResolvedCall rc_item = {0}; rc_item.caller_qn = "com.example.pkg.Service.handle"; rc_item.callee_qn = "com.example.pkg.Helper.run"; rc_item.strategy = "lsp"; rc_item.confidence = 0.9f; CBMResolvedCallArray arr = {0}; arr.items = &rc_item; arr.count = 1; arr.cap = 1; CBMCall call = {0}; call.enclosing_func_qn = "proj.other_mod.Service.handle"; call.callee_name = "helper.run"; ASSERT_TRUE(cbm_pipeline_find_lsp_resolution(&arr, &call, false) == NULL); ASSERT_TRUE(cbm_pipeline_find_lsp_resolution(&arr, &call, true) == &rc_item); /* Target-node fallback: callee_qn misses both as-is and * project-prefixed; exactly one node coincidentally shares the * "Helper.run" tail in an unrelated module. */ cbm_gbuf_t *tgbuf = cbm_gbuf_new("proj", "/tmp"); ASSERT_NOT_NULL(tgbuf); int64_t nid = cbm_gbuf_upsert_node(tgbuf, "Method", "run", "proj.zeta.Helper.run", "zeta/helper.py", 1, 3, NULL); ASSERT_TRUE(nid != 0); ASSERT_TRUE(cbm_pipeline_lsp_target_node(tgbuf, "proj", "com.other.Helper.run", false) == NULL); const cbm_gbuf_node_t *jvm_hit = cbm_pipeline_lsp_target_node(tgbuf, "proj", "com.other.Helper.run", true); ASSERT_NOT_NULL(jvm_hit); ASSERT_TRUE(strcmp(jvm_hit->qualified_name, "proj.zeta.Helper.run") == 0); cbm_gbuf_free(tgbuf); PASS(); } TEST(parallel_python_lsp_override_emits_lsp_strategy_edges) { char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_par_pylsp_XXXXXX"); if (!cbm_mkdtemp(tmpdir)) { FAIL("mkdtemp failed"); } /* Single-file scenario: pins the in-file LSP path where py_lsp * registers Greeter from the file's own defs, types `g = Greeter()` * as NAMED("…Greeter"), and resolves `g.hello()` to Greeter.hello * via attribute lookup. callee_qn matches the gbuf QN directly. The * cross-file equivalent is covered by * parallel_python_lsp_override_cross_file_emits_lsp_strategy_edges, * which exercises the project-prefix fallback in * cbm_pipeline_lsp_target_node. */ char fpath0[512]; snprintf(fpath0, sizeof(fpath0), "%s/app.py", tmpdir); FILE *f = fopen(fpath0, "w"); if (!f) { FAIL("fopen app.py failed"); } fprintf(f, "class Greeter:\n" " def hello(self):\n" " return 'hi'\n" "\n" "def main():\n" " g = Greeter()\n" " g.hello()\n"); fclose(f); cbm_file_info_t files[1] = {0}; files[0].path = fpath0; files[0].rel_path = (char *)"app.py"; files[0].language = CBM_LANG_PYTHON; cbm_gbuf_t *gbuf = run_parallel("cbm_par_pylsp", tmpdir, files, 1, 1); ASSERT_NOT_NULL(gbuf); lsp_edge_count_ctx_t c = {0}; cbm_gbuf_foreach_edge(gbuf, count_lsp_call_edges, &c); /* Sanity: extraction produced at least one call edge. */ ASSERT_GT(c.total_calls, 0); /* The parallel pipeline must surface at least one LSP-attributed * CALLS edge. This proves the unified cbm_pipeline_find_lsp_resolution * (shared with pass_calls.c at floor 0.6) is actually consulted in * the parallel pipeline, and that the resulting edge is emitted with * the LSP strategy intact rather than overwritten by the registry * fallback. */ ASSERT_GT(c.lsp_strategy_count, 0); cbm_gbuf_free(gbuf); unlink(fpath0); rmdir(tmpdir); PASS(); } /* Cross-file regression for the QN-mismatch bug: py_lsp's per-file mode * emits resolved_calls.callee_qn as the raw import-module path (e.g. * `greeter.Greeter` from `from greeter import Greeter`) rather than the * project-qualified QN the gbuf stores (`.greeter.Greeter`). * Before cbm_pipeline_lsp_target_node added the project-prefix fallback, * the LSP match succeeded (lsp_overrides counter incremented) but the * downstream cbm_gbuf_find_by_qn lookup missed silently, dropping the * edge. With the fallback in place, the cross-file `g.hello()` call is * attributed to .greeter.Greeter.hello with an lsp_* strategy. * * Two-file scenario: greeter.py defines Greeter; app.py imports it and * calls hello() — same shape as the original failing reproduction. */ TEST(parallel_python_lsp_override_cross_file_emits_lsp_strategy_edges) { char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_par_pylsp_xf_XXXXXX"); if (!cbm_mkdtemp(tmpdir)) { FAIL("mkdtemp failed"); } char gpath[512]; snprintf(gpath, sizeof(gpath), "%s/greeter.py", tmpdir); FILE *gf = fopen(gpath, "w"); if (!gf) { FAIL("fopen greeter.py failed"); } fprintf(gf, "class Greeter:\n" " def hello(self):\n" " return 'hi'\n"); fclose(gf); char apath[512]; snprintf(apath, sizeof(apath), "%s/app.py", tmpdir); FILE *af = fopen(apath, "w"); if (!af) { unlink(gpath); rmdir(tmpdir); FAIL("fopen app.py failed"); } fprintf(af, "from greeter import Greeter\n" "\n" "def main():\n" " g = Greeter()\n" " g.hello()\n"); fclose(af); cbm_file_info_t files[2] = {0}; files[0].path = gpath; files[0].rel_path = (char *)"greeter.py"; files[0].language = CBM_LANG_PYTHON; files[1].path = apath; files[1].rel_path = (char *)"app.py"; files[1].language = CBM_LANG_PYTHON; cbm_gbuf_t *gbuf = run_parallel("cbm_par_pylsp_xf", tmpdir, files, 2, 2); ASSERT_NOT_NULL(gbuf); lsp_edge_count_ctx_t c = {0}; cbm_gbuf_foreach_edge(gbuf, count_lsp_call_edges, &c); ASSERT_GT(c.total_calls, 0); /* The cross-file LSP override must produce at least one lsp_* * CALLS edge. Without the project-prefix fallback in * cbm_pipeline_lsp_target_node this assertion would fail because the * raw module-path callee_qn doesn't match the project-qualified * gbuf node QN. */ ASSERT_GT(c.lsp_strategy_count, 0); cbm_gbuf_free(gbuf); unlink(apath); unlink(gpath); rmdir(tmpdir); PASS(); } TEST(parallel_cross_lsp_pruning_requires_matching_call_resolution) { char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_par_pylsp_prune_XXXXXX"); if (!cbm_mkdtemp(tmpdir)) { FAIL("mkdtemp failed"); } char rpath[512]; snprintf(rpath, sizeof(rpath), "%s/routing.py", tmpdir); FILE *rf = fopen(rpath, "w"); if (!rf) { rmdir(tmpdir); FAIL("fopen routing.py failed"); } fprintf(rf, "class APIRouter:\n" " def add_api_route(self):\n" " return None\n" " def include_router(self):\n" " self.add_api_route()\n"); fclose(rf); cbm_file_info_t files[1] = {0}; files[0].path = rpath; files[0].rel_path = (char *)"routing.py"; files[0].language = CBM_LANG_PYTHON; cbm_gbuf_t *gbuf = cbm_gbuf_new("cbm_par_pylsp_prune", tmpdir); cbm_registry_t *reg = cbm_registry_new(); CBMFileResult **result_cache = calloc(1, sizeof(*result_cache)); ASSERT_NOT_NULL(gbuf); ASSERT_NOT_NULL(reg); ASSERT_NOT_NULL(result_cache); atomic_int cancelled; atomic_init(&cancelled, 0); cbm_pipeline_ctx_t ctx = {.project_name = "cbm_par_pylsp_prune", .repo_path = tmpdir, .gbuf = gbuf, .registry = reg, .cancelled = &cancelled}; _Atomic int64_t shared_ids; atomic_init(&shared_ids, cbm_gbuf_next_id(gbuf)); cbm_init(); ASSERT_EQ(cbm_parallel_extract(&ctx, files, 1, result_cache, &shared_ids, 1), 0); cbm_gbuf_set_next_id(gbuf, atomic_load(&shared_ids)); ASSERT_NOT_NULL(result_cache[0]); ASSERT_GT(result_cache[0]->calls.count, 0); result_cache[0]->resolved_calls.count = 0; CBMResolvedCall unrelated = {.caller_qn = "cbm_par_pylsp_prune.routing.unrelated", .callee_qn = "cbm_par_pylsp_prune.routing.APIRouter.unrelated", .strategy = "lsp_method", .confidence = 0.95f}; cbm_resolvedcall_push(&result_cache[0]->resolved_calls, &result_cache[0]->arena, unrelated); ASSERT_EQ(result_cache[0]->resolved_calls.count, result_cache[0]->calls.count); ASSERT_EQ(cbm_build_registry_from_cache(&ctx, files, 1, result_cache), 0); char **def_modules = calloc(1, sizeof(*def_modules)); int def_count = 0; CBMLSPDef *all_defs = cbm_pxc_collect_all_defs(result_cache, files, 1, ctx.project_name, def_modules, &def_count); CBMModuleDefIndex *module_def_index = all_defs ? cbm_pxc_build_module_def_index(all_defs, def_count) : NULL; ASSERT_NOT_NULL(all_defs); ASSERT_EQ(cbm_parallel_resolve(&ctx, files, 1, result_cache, &shared_ids, 1, all_defs, def_count, def_modules, module_def_index, NULL /* cross_registries */), 0); cbm_gbuf_set_next_id(gbuf, atomic_load(&shared_ids)); ASSERT_TRUE(resolved_call_contains(&result_cache[0]->resolved_calls, "include_router", "add_api_route")); lsp_edge_count_ctx_t lsp_edges = {0}; cbm_gbuf_foreach_edge(gbuf, count_lsp_call_edges, &lsp_edges); ASSERT_GT(lsp_edges.total_calls, 0); ASSERT_GT(lsp_edges.lsp_strategy_count, 0); ASSERT_TRUE(call_edge_contains(gbuf, "APIRouter.include_router", "APIRouter.add_api_route", "\"strategy\":\"lsp_method\"")); cbm_pxc_free_module_def_index(module_def_index); free(all_defs); if (def_modules) { free(def_modules[0]); free(def_modules); } cbm_free_result(result_cache[0]); free(result_cache); cbm_registry_free(reg); cbm_gbuf_free(gbuf); unlink(rpath); rmdir(tmpdir); PASS(); } /* RED/GREEN A — the graph-quality guarantee behind the low-RAM retention cap. * * The fused cross-file LSP step re-parses each file's source to resolve calls * whose receiver type lives in ANOTHER file (the per-file pass cannot). When * the retention cap drops a file's source, that resolution MUST still happen * via a bounded on-demand re-read; otherwise the cross-file CALLS edge is LOST. * * Fixture: a Java<->Kotlin pair with genuinely cross-language calls that only * the cross-file LSP resolves — JavaCaller.call -> KotlinService.ping (Java -> * Kotlin) and KotlinService.ping -> JavaService.pong (Kotlin -> Java). These * carry the "lsp" strategy and do NOT exist without the cross-file source, * unlike same-file or import-local Python calls which the per-file pass already * resolves (so counting lsp edges on those cannot detect the fallback). * * Three scenarios asserted GREEN with the re-read fallback in place: * 1. CONTROL — default retention: both cross-file edges present. Proves the * fixture genuinely produces them (non-vacuity guard). * 2. NO-RETAIN — retain_sources=false: nothing retained -> edges survive only * via the re-read fallback. * 3. OVER-CAP — per-file cap = 1 byte: every file dropped by the SIZE cap -> * edges survive only via the re-read fallback. * On main (no fallback) scenarios 2 and 3 LOSE both edges = RED; scenario 1 * stays present = the non-vacuity control. */ TEST(parallel_cross_file_reread_preserves_unretained_edges) { char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_par_xf_reread_XXXXXX"); if (!cbm_mkdtemp(tmpdir)) { FAIL("mkdtemp failed"); } char jpath[512]; snprintf(jpath, sizeof(jpath), "%s/src/main/java/com/example/Example.java", tmpdir); char jdir[512]; snprintf(jdir, sizeof(jdir), "%s/src/main/java/com/example", tmpdir); cbm_mkdir_p(jdir, 0755); FILE *jf = fopen(jpath, "w"); if (!jf) { FAIL("fopen Example.java failed"); } fprintf(jf, "package com.example;\n" "\n" "class JavaCaller {\n" " String call(KotlinService kotlinService) {\n" " return kotlinService.ping(new JavaService());\n" " }\n" "}\n" "\n" "class JavaService {\n" " String pong() {\n" " return \"pong\";\n" " }\n" "}\n"); fclose(jf); char kpath[512]; snprintf(kpath, sizeof(kpath), "%s/src/main/kotlin/com/example/KotlinService.kt", tmpdir); char kdir[512]; snprintf(kdir, sizeof(kdir), "%s/src/main/kotlin/com/example", tmpdir); cbm_mkdir_p(kdir, 0755); FILE *kf = fopen(kpath, "w"); if (!kf) { FAIL("fopen KotlinService.kt failed"); } fprintf(kf, "package com.example\n" "\n" "class KotlinService {\n" " fun ping(javaService: JavaService): String {\n" " return javaService.pong()\n" " }\n" "}\n"); fclose(kf); cbm_file_info_t files[2] = {0}; files[0].path = jpath; files[0].rel_path = (char *)"src/main/java/com/example/Example.java"; files[0].language = CBM_LANG_JAVA; files[1].path = kpath; files[1].rel_path = (char *)"src/main/kotlin/com/example/KotlinService.kt"; files[1].language = CBM_LANG_KOTLIN; /* CONTROL (retained) + two drop scenarios that reach the cross-file edge * only via the on-demand re-read: NO-RETAIN disables retention entirely; * OVER-CAP sets a 1-byte per-file cap so every file is dropped by size. */ const cbm_parallel_extract_opts_t no_retain = { .retain_sources = false, .retain_sources_set = true, }; const cbm_parallel_extract_opts_t over_cap = { .retain_sources = true, .retain_sources_set = true, .retain_per_file_max_bytes = 1, /* 1 byte → every file dropped by the size cap */ }; const cbm_parallel_extract_opts_t *scenarios[3] = {NULL, &no_retain, &over_cap}; for (int s = 0; s < 3; s++) { cbm_gbuf_t *gbuf = run_parallel_with_extract_opts("com", tmpdir, files, 2, 2, scenarios[s]); ASSERT_NOT_NULL(gbuf); const cbm_gbuf_edge_t *java_to_kotlin = find_calls_edge_by_tails(gbuf, "JavaCaller.call", "KotlinService.ping"); const cbm_gbuf_edge_t *kotlin_to_java = find_calls_edge_by_tails(gbuf, "KotlinService.ping", "JavaService.pong"); /* Both cross-file (Java↔Kotlin) CALLS edges must be present in EVERY * scenario. In the drop scenarios (s=1,2) the caller's source is NOT * retained, so these edges exist ONLY because resolve_worker re-reads * the source on demand. Without that fallback (main) they are LOST. */ ASSERT_NOT_NULL(java_to_kotlin); ASSERT_NOT_NULL(kotlin_to_java); /* And they must come from the source-dependent cross-file LSP, not a * source-free suffix heuristic — proving the re-read actually ran. */ ASSERT_NOT_NULL(java_to_kotlin->properties_json); ASSERT_NOT_NULL(strstr(java_to_kotlin->properties_json, "\"strategy\":\"lsp")); ASSERT_NOT_NULL(kotlin_to_java->properties_json); ASSERT_NOT_NULL(strstr(kotlin_to_java->properties_json, "\"strategy\":\"lsp")); cbm_gbuf_free(gbuf); } th_rmtree(tmpdir); PASS(); } /* issue #294: gRPC service-name extraction must (a) preserve the canonical * proto service name (FooServiceClient → FooService, not Foo) and (b) only * match real stub/client types — ordinary receiver vars must NOT produce * phantom __grpc__ Routes. */ TEST(grpc_service_name_preserves_service_suffix_issue294) { char svc[256]; char meth[256]; /* Generated client class keeps the "Service" part of the name. */ ASSERT_TRUE(extract_grpc_service_method("pb.NewFooServiceClient.GetBar", svc, sizeof(svc), meth, sizeof(meth))); ASSERT_STR_EQ(svc, "FooService"); ASSERT_STR_EQ(meth, "GetBar"); /* Java-style ...ServiceGrpc strips only "Grpc". */ ASSERT_TRUE(extract_grpc_service_method("CartServiceGrpc.getCart", svc, sizeof(svc), meth, sizeof(meth))); ASSERT_STR_EQ(svc, "CartService"); /* BlockingStub wins over Stub (longest-suffix-first). */ ASSERT_TRUE(extract_grpc_service_method("CartServiceBlockingStub.getCart", svc, sizeof(svc), meth, sizeof(meth))); ASSERT_STR_EQ(svc, "CartService"); PASS(); } TEST(grpc_no_phantom_route_from_plain_var_issue294) { char svc[256]; char meth[256]; /* Ordinary receiver vars carry no gRPC stub suffix → must NOT match, * so no phantom __grpc__provider/... or __grpc__builder/... Route. */ ASSERT_FALSE( extract_grpc_service_method("_provider.GetGroup", svc, sizeof(svc), meth, sizeof(meth))); ASSERT_FALSE(extract_grpc_service_method("_builder.AddSomeService", svc, sizeof(svc), meth, sizeof(meth))); ASSERT_FALSE(extract_grpc_service_method("logger.Info", svc, sizeof(svc), meth, sizeof(meth))); PASS(); } /* ── Shared "::" normalization in cbm_pipeline_find_lsp_resolution (QA F3) ─ * * The last-"::"-segment normalization in lsp_resolve.h widens matching for * qualified static callees (Perl `Pkg::sub`, C++ `Ns::fn`, etc.) across ALL * languages, not just Perl. These tests lock the intended behavior directly * against cbm_pipeline_find_lsp_resolution: (1) a qualified static call still * resolves to the right resolved entry, and (2) the theoretical * mis-attribution edge case (two same-named subs from different namespaces) is * bounded by caller-QN equality + the confidence floor. */ static CBMResolvedCall make_rc(const char *caller, const char *callee, float conf) { CBMResolvedCall rc; memset(&rc, 0, sizeof(rc)); rc.caller_qn = caller; rc.callee_qn = callee; rc.strategy = "test"; rc.confidence = conf; return rc; } static CBMCall make_call(const char *enclosing, const char *callee_name) { CBMCall c; memset(&c, 0, sizeof(c)); c.enclosing_func_qn = enclosing; c.callee_name = callee_name; return c; } TEST(lsp_resolve_qualified_static_call_normalizes_colons) { /* A qualified static call `Pkg::sub` (callee_name keeps the package * prefix) must still match a resolved entry whose callee_qn short-name is * the bare `sub`. This is the cross-language "::"-normalization contract. */ CBMResolvedCall items[] = { make_rc("proj.mod.caller", "proj.Pkg.sub", 0.9f), }; CBMResolvedCallArray arr = {items, 1, 1}; CBMCall call = make_call("proj.mod.caller", "Pkg::sub"); const CBMResolvedCall *hit = cbm_pipeline_find_lsp_resolution(&arr, &call, false); ASSERT(hit != NULL); ASSERT(strcmp(hit->callee_qn, "proj.Pkg.sub") == 0); /* A bare call (no "::") to the same short name resolves identically — * normalization must not regress the common case. */ CBMCall bare = make_call("proj.mod.caller", "sub"); const CBMResolvedCall *bare_hit = cbm_pipeline_find_lsp_resolution(&arr, &bare, false); ASSERT(bare_hit != NULL); ASSERT(strcmp(bare_hit->callee_qn, "proj.Pkg.sub") == 0); PASS(); } TEST(lsp_resolve_misattribution_is_bounded) { /* Two same-named subs from different namespaces (A::foo, B::foo) resolved * within the same enclosing function. Both resolved short-names normalize * to `foo`, so a textual `B::foo` matches both by short-name — the * theoretical mis-attribution. The function bounds this: it returns the * highest-confidence match (deterministic, never both), and the bound is * enforced by caller-QN equality + the confidence floor. */ CBMResolvedCall items[] = { make_rc("proj.mod.caller", "proj.A.foo", 0.7f), make_rc("proj.mod.caller", "proj.B.foo", 0.9f), /* Below the confidence floor: must be ignored entirely. */ make_rc("proj.mod.caller", "proj.C.foo", 0.3f), /* Different caller: must never match regardless of short-name. */ make_rc("proj.mod.other", "proj.D.foo", 0.95f), }; CBMResolvedCallArray arr = {items, 4, 4}; CBMCall call = make_call("proj.mod.caller", "B::foo"); const CBMResolvedCall *hit = cbm_pipeline_find_lsp_resolution(&arr, &call, false); ASSERT(hit != NULL); /* Highest-confidence qualifying entry wins; the cross-caller 0.95 entry is * excluded by caller-QN equality, the 0.3 entry by the floor. */ ASSERT(strcmp(hit->callee_qn, "proj.B.foo") == 0); /* The cross-caller high-confidence entry only matches its own caller. */ CBMCall other = make_call("proj.mod.other", "D::foo"); const CBMResolvedCall *other_hit = cbm_pipeline_find_lsp_resolution(&arr, &other, false); ASSERT(other_hit != NULL); ASSERT(strcmp(other_hit->callee_qn, "proj.D.foo") == 0); /* A caller with no qualifying entry resolves to nothing (no widening can * manufacture an edge across callers). */ CBMCall absent = make_call("proj.mod.absent", "foo"); ASSERT(cbm_pipeline_find_lsp_resolution(&arr, &absent, false) == NULL); PASS(); } /* ── Suite Registration ──────────────────────────────────────────── */ SUITE(parallel) { RUN_TEST(parallel_cbm_init_concurrent_idempotent); RUN_TEST(lsp_resolve_qualified_static_call_normalizes_colons); RUN_TEST(lsp_resolve_misattribution_is_bounded); RUN_TEST(grpc_service_name_preserves_service_suffix_issue294); RUN_TEST(grpc_no_phantom_route_from_plain_var_issue294); /* Graph buffer merge/shared-ID tests */ RUN_TEST(gbuf_shared_ids_unique); RUN_TEST(gbuf_merge_nodes); RUN_TEST(gbuf_merge_edges); RUN_TEST(gbuf_merge_empty_src); RUN_TEST(gbuf_merge_src_free_safe); RUN_TEST(gbuf_next_id_accessors); RUN_TEST(lsp_resolution_matches_cpp_segments_and_reason_joins); RUN_TEST(lsp_resolution_index_matches_linear_cpp_semantics); RUN_TEST(lsp_resolution_index_overlong_key_falls_back_to_linear); /* Parallel pipeline parity tests */ RUN_TEST(parallel_node_count); RUN_TEST(parallel_python_lsp_override_emits_lsp_strategy_edges); RUN_TEST(parallel_python_lsp_override_cross_file_emits_lsp_strategy_edges); RUN_TEST(parallel_cross_lsp_pruning_requires_matching_call_resolution); RUN_TEST(parallel_cross_file_reread_preserves_unretained_edges); RUN_TEST(parallel_java_kotlin_lsp_override_cross_file_emits_lsp_strategy_edges); RUN_TEST(parallel_lsp_tail_match_fallbacks_gated_to_jvm); RUN_TEST(parallel_calls_parity); RUN_TEST(parallel_defines_parity); RUN_TEST(parallel_defines_method_parity); RUN_TEST(parallel_imports_parity); RUN_TEST(parallel_usage_parity); RUN_TEST(parallel_inherits_parity); RUN_TEST(parallel_implements_parity); RUN_TEST(parallel_total_edges); RUN_TEST(parallel_full_pipeline_worker_count_parity_64_files); RUN_TEST(parallel_empty_files); RUN_TEST(extraction_errors_are_nonfatal_in_parallel_and_sequential_paths); RUN_TEST(parallel_args_json_no_overflow); RUN_TEST(parallel_unresolved_route_suffix_does_not_emit_self_call); RUN_TEST(parallel_top_level_raise_matches_sequential_no_file_fallback); RUN_TEST(parallel_fastapi_websocket_route_registration_matches_sequential); /* Cleanup shared state */ parity_teardown(); }