mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-06 08:35:21 +08:00
CALLS edges for cross-file function calls were mis-wired or missing in
TypeScript projects that use path aliases (e.g. @/lib/auth → src/lib/auth):
1. resolve_relative_import only handled "./relative" paths, so
"@/lib/auth"-style imports never reached IMPORTS edges. The import
map stayed empty, Strategy 1 in cbm_registry_resolve failed, and
resolution fell through to name-based lookups that pick the wrong
target when functions share names across files.
2. resolve_import_map built the candidate QN as the bare module QN
when the callee had no dot ("requireAdmin" vs "pkg.Func"), instead
of module_qn.requireAdmin. Even with a populated import map, bare
calls still mis-resolved.
Fix:
- New src/pipeline/path_alias.{h,c}: language-agnostic build-tool
path-alias subsystem. Currently parses tsconfig.json and
jsconfig.json compilerOptions.paths + baseUrl via the existing
yyjson dependency (with ALLOW_COMMENTS|ALLOW_TRAILING_COMMAS for
JSONC). The data model (cbm_path_alias_t / map / scope /
collection) is generic; adding Vite/Webpack/Python loaders means
writing a new file-parsing helper and registering it in
find_alias_files — the resolver, collection, and pipeline
integration do not change.
- O(n log n) ordering: scope-by-dir-specificity and entry-by-
prefix-specificity are sorted with qsort, replacing the O(n²)
bubble sorts that the original draft used. With caps of 256
entries per config and 256 configs per repo, the sort is
effectively flat.
- Resource ceilings with logging: directory recursion capped at
32 levels; total config files capped at 256; per-config size
capped at 64 KB. Cap hits emit cbm_log_warn so silent truncation
on pathological monorepos shows up in the index log.
- Walk hygiene: skips node_modules, .git, dist, build, .next,
coverage, target.
- Single integration point: cbm_pipeline_resolve_module gets a
Strategy 1b between relative-import resolution and pkgmap so both
the sequential and parallel pipelines pick up alias resolution
automatically. Non-TS projects pay nothing — the loader returns
NULL when no usable configs are found.
- Layer 2 fix in registry.c: resolve_import_map now builds the
candidate as resolved.prefix when suffix is NULL (one-line fix,
matching comment).
- Tests: 10 path_alias tests (basic wildcard, specificity, exact,
extension stripping, baseUrl, NULL safety, nearest-ancestor scope
selection, end-to-end loader against a tmp monorepo, no-configs
case) plus a registry test for the bare-call fix. Full suite:
2835 passed, 0 failed.
Cherry-picked and generalized from #243 (originally TypeScript-only
under tsconfig-named types). Renamed types (cbm_path_alias_scope_t /
collection_t in place of cbm_tsconfig_*) and centralised the load
into cbm_load_path_aliases so future loaders for other build tools
slot in without touching extraction or pipeline code.
Closes #243.
Co-authored-by: Peter Cox <info@petercox.ie>
662 lines
23 KiB
C
662 lines
23 KiB
C
/*
|
|
* test_registry.c — Tests for function registry and FQN helpers.
|
|
*
|
|
* RED phase: Define expected behavior for symbol registration,
|
|
* resolution strategies, and qualified name computation.
|
|
*/
|
|
#include "test_framework.h"
|
|
#include "pipeline/pipeline.h"
|
|
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
/* ── FQN computation ──────────────────────────────────────────────── */
|
|
|
|
TEST(fqn_simple) {
|
|
char *qn = cbm_pipeline_fqn_compute("myproj", "cmd/server/main.go", "HandleRequest");
|
|
ASSERT_NOT_NULL(qn);
|
|
ASSERT_STR_EQ(qn, "myproj.cmd.server.main.HandleRequest");
|
|
free(qn);
|
|
PASS();
|
|
}
|
|
|
|
TEST(fqn_no_name) {
|
|
char *qn = cbm_pipeline_fqn_compute("myproj", "pkg/service.go", NULL);
|
|
ASSERT_NOT_NULL(qn);
|
|
ASSERT_STR_EQ(qn, "myproj.pkg.service");
|
|
free(qn);
|
|
PASS();
|
|
}
|
|
|
|
TEST(fqn_python_init) {
|
|
/* __init__.py should be stripped */
|
|
char *qn = cbm_pipeline_fqn_compute("myproj", "pkg/__init__.py", "Foo");
|
|
ASSERT_NOT_NULL(qn);
|
|
ASSERT_STR_EQ(qn, "myproj.pkg.Foo");
|
|
free(qn);
|
|
PASS();
|
|
}
|
|
|
|
TEST(fqn_js_index) {
|
|
/* index.js should be stripped */
|
|
char *qn = cbm_pipeline_fqn_compute("myproj", "src/index.ts", "App");
|
|
ASSERT_NOT_NULL(qn);
|
|
ASSERT_STR_EQ(qn, "myproj.src.App");
|
|
free(qn);
|
|
PASS();
|
|
}
|
|
|
|
TEST(fqn_module) {
|
|
char *qn = cbm_pipeline_fqn_module("myproj", "cmd/server/main.go");
|
|
ASSERT_NOT_NULL(qn);
|
|
ASSERT_STR_EQ(qn, "myproj.cmd.server.main");
|
|
free(qn);
|
|
PASS();
|
|
}
|
|
|
|
TEST(fqn_folder) {
|
|
char *qn = cbm_pipeline_fqn_folder("myproj", "cmd/server");
|
|
ASSERT_NOT_NULL(qn);
|
|
ASSERT_STR_EQ(qn, "myproj.cmd.server");
|
|
free(qn);
|
|
PASS();
|
|
}
|
|
|
|
TEST(fqn_root_file) {
|
|
char *qn = cbm_pipeline_fqn_compute("proj", "main.go", "main");
|
|
ASSERT_NOT_NULL(qn);
|
|
ASSERT_STR_EQ(qn, "proj.main.main");
|
|
free(qn);
|
|
PASS();
|
|
}
|
|
|
|
/* ── FQN collision regression tests ──────────────────────────────── */
|
|
/* Bug: __init__.py Module QN collided with Folder QN, causing Folder
|
|
* nodes to be overwritten during extraction. Symbols inside __init__.py
|
|
* must still get clean package QNs (no __init__ in their QN). */
|
|
|
|
TEST(fqn_init_module_distinct_from_folder) {
|
|
/* Module QN for __init__.py must differ from Folder QN for same dir */
|
|
char *mod_qn = cbm_pipeline_fqn_module("proj", "pkg/__init__.py");
|
|
char *folder_qn = cbm_pipeline_fqn_folder("proj", "pkg");
|
|
ASSERT_NOT_NULL(mod_qn);
|
|
ASSERT_NOT_NULL(folder_qn);
|
|
/* These MUST be different — the old bug was they were both "proj.pkg" */
|
|
ASSERT_STR_NEQ(mod_qn, folder_qn);
|
|
/* Module should contain __init__ as disambiguator */
|
|
ASSERT_NOT_NULL(strstr(mod_qn, "__init__"));
|
|
/* Folder should NOT contain __init__ */
|
|
ASSERT_EQ(strstr(folder_qn, "__init__"), NULL);
|
|
free(mod_qn);
|
|
free(folder_qn);
|
|
PASS();
|
|
}
|
|
|
|
TEST(fqn_init_nested_module_distinct) {
|
|
/* Same collision test for deeply nested __init__.py */
|
|
char *mod_qn =
|
|
cbm_pipeline_fqn_module("proj", "docker-images/cloud-runs/bq-sync-api/__init__.py");
|
|
char *folder_qn = cbm_pipeline_fqn_folder("proj", "docker-images/cloud-runs/bq-sync-api");
|
|
ASSERT_NOT_NULL(mod_qn);
|
|
ASSERT_NOT_NULL(folder_qn);
|
|
ASSERT_STR_NEQ(mod_qn, folder_qn);
|
|
free(mod_qn);
|
|
free(folder_qn);
|
|
PASS();
|
|
}
|
|
|
|
TEST(fqn_index_ts_module_distinct_from_folder) {
|
|
/* Same collision for JS/TS index.ts */
|
|
char *mod_qn = cbm_pipeline_fqn_module("proj", "src/components/index.ts");
|
|
char *folder_qn = cbm_pipeline_fqn_folder("proj", "src/components");
|
|
ASSERT_NOT_NULL(mod_qn);
|
|
ASSERT_NOT_NULL(folder_qn);
|
|
ASSERT_STR_NEQ(mod_qn, folder_qn);
|
|
free(mod_qn);
|
|
free(folder_qn);
|
|
PASS();
|
|
}
|
|
|
|
TEST(fqn_init_symbols_get_clean_package_qn) {
|
|
/* Symbols inside __init__.py must NOT have __init__ in their QN.
|
|
* "proj.pkg.Foo" not "proj.pkg.__init__.Foo" */
|
|
char *sym_qn = cbm_pipeline_fqn_compute("proj", "pkg/__init__.py", "Foo");
|
|
ASSERT_NOT_NULL(sym_qn);
|
|
ASSERT_STR_EQ(sym_qn, "proj.pkg.Foo");
|
|
ASSERT_EQ(strstr(sym_qn, "__init__"), NULL);
|
|
free(sym_qn);
|
|
PASS();
|
|
}
|
|
|
|
TEST(fqn_index_symbols_get_clean_qn) {
|
|
/* Symbols inside index.ts must NOT have index in their QN */
|
|
char *sym_qn = cbm_pipeline_fqn_compute("proj", "src/index.ts", "App");
|
|
ASSERT_NOT_NULL(sym_qn);
|
|
ASSERT_STR_EQ(sym_qn, "proj.src.App");
|
|
free(sym_qn);
|
|
PASS();
|
|
}
|
|
|
|
TEST(fqn_init_file_node_distinct) {
|
|
/* File node QN (name="__file__") for __init__.py must be distinct from Folder */
|
|
char *file_qn = cbm_pipeline_fqn_compute("proj", "pkg/__init__.py", "__file__");
|
|
char *folder_qn = cbm_pipeline_fqn_folder("proj", "pkg");
|
|
ASSERT_NOT_NULL(file_qn);
|
|
ASSERT_NOT_NULL(folder_qn);
|
|
ASSERT_STR_NEQ(file_qn, folder_qn);
|
|
free(file_qn);
|
|
free(folder_qn);
|
|
PASS();
|
|
}
|
|
|
|
TEST(fqn_regular_module_unchanged) {
|
|
/* Non-init modules should be unaffected by the fix */
|
|
char *qn = cbm_pipeline_fqn_module("proj", "pkg/utils.py");
|
|
ASSERT_NOT_NULL(qn);
|
|
ASSERT_STR_EQ(qn, "proj.pkg.utils");
|
|
free(qn);
|
|
PASS();
|
|
}
|
|
|
|
TEST(project_name_from_path) {
|
|
char *name = cbm_project_name_from_path("/Users/dev/project");
|
|
ASSERT_NOT_NULL(name);
|
|
ASSERT_STR_EQ(name, "Users-dev-project");
|
|
free(name);
|
|
PASS();
|
|
}
|
|
|
|
TEST(project_name_from_root) {
|
|
char *name = cbm_project_name_from_path("/");
|
|
ASSERT_NOT_NULL(name);
|
|
ASSERT_STR_EQ(name, "root");
|
|
free(name);
|
|
PASS();
|
|
}
|
|
|
|
/* ── Registry lifecycle ───────────────────────────────────────────── */
|
|
|
|
TEST(registry_create_free) {
|
|
cbm_registry_t *r = cbm_registry_new();
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_EQ(cbm_registry_size(r), 0);
|
|
cbm_registry_free(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(registry_free_null) {
|
|
cbm_registry_free(NULL); /* should not crash */
|
|
PASS();
|
|
}
|
|
|
|
TEST(registry_add_and_exists) {
|
|
cbm_registry_t *r = cbm_registry_new();
|
|
cbm_registry_add(r, "main", "proj.cmd.main", "Function");
|
|
ASSERT_EQ(cbm_registry_size(r), 1);
|
|
ASSERT_TRUE(cbm_registry_exists(r, "proj.cmd.main"));
|
|
ASSERT_FALSE(cbm_registry_exists(r, "proj.cmd.other"));
|
|
cbm_registry_free(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(registry_label_of) {
|
|
cbm_registry_t *r = cbm_registry_new();
|
|
cbm_registry_add(r, "Foo", "proj.pkg.Foo", "Class");
|
|
cbm_registry_add(r, "bar", "proj.pkg.bar", "Function");
|
|
|
|
ASSERT_STR_EQ(cbm_registry_label_of(r, "proj.pkg.Foo"), "Class");
|
|
ASSERT_STR_EQ(cbm_registry_label_of(r, "proj.pkg.bar"), "Function");
|
|
ASSERT_NULL(cbm_registry_label_of(r, "proj.pkg.baz"));
|
|
|
|
cbm_registry_free(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(registry_find_by_name) {
|
|
cbm_registry_t *r = cbm_registry_new();
|
|
cbm_registry_add(r, "main", "proj.cmd.main", "Function");
|
|
cbm_registry_add(r, "main", "proj.srv.main", "Function");
|
|
cbm_registry_add(r, "helper", "proj.util.helper", "Function");
|
|
|
|
const char **out = NULL;
|
|
int count = 0;
|
|
cbm_registry_find_by_name(r, "main", &out, &count);
|
|
ASSERT_EQ(count, 2);
|
|
|
|
cbm_registry_find_by_name(r, "helper", &out, &count);
|
|
ASSERT_EQ(count, 1);
|
|
|
|
cbm_registry_find_by_name(r, "nonexistent", &out, &count);
|
|
ASSERT_EQ(count, 0);
|
|
|
|
cbm_registry_free(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(registry_no_duplicates) {
|
|
cbm_registry_t *r = cbm_registry_new();
|
|
cbm_registry_add(r, "main", "proj.cmd.main", "Function");
|
|
cbm_registry_add(r, "main", "proj.cmd.main", "Function"); /* duplicate */
|
|
ASSERT_EQ(cbm_registry_size(r), 1);
|
|
|
|
const char **out = NULL;
|
|
int count = 0;
|
|
cbm_registry_find_by_name(r, "main", &out, &count);
|
|
ASSERT_EQ(count, 1); /* no duplicate in byName list */
|
|
|
|
cbm_registry_free(r);
|
|
PASS();
|
|
}
|
|
|
|
/* ── Resolution strategies ────────────────────────────────────────── */
|
|
|
|
TEST(resolve_same_module) {
|
|
cbm_registry_t *r = cbm_registry_new();
|
|
cbm_registry_add(r, "helper", "proj.pkg.service.helper", "Function");
|
|
|
|
/* Call "helper" from the same module → should resolve */
|
|
cbm_resolution_t res = cbm_registry_resolve(r, "helper", "proj.pkg.service", NULL, NULL, 0);
|
|
ASSERT_STR_EQ(res.qualified_name, "proj.pkg.service.helper");
|
|
ASSERT_STR_EQ(res.strategy, "same_module");
|
|
ASSERT_TRUE(res.confidence >= 0.85);
|
|
|
|
cbm_registry_free(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(resolve_import_map) {
|
|
cbm_registry_t *r = cbm_registry_new();
|
|
cbm_registry_add(r, "Process", "proj.pkg.worker.Process", "Function");
|
|
|
|
/* Import map: "worker" → "proj.pkg.worker" */
|
|
const char *keys[] = {"worker"};
|
|
const char *vals[] = {"proj.pkg.worker"};
|
|
|
|
/* Call "worker.Process" → should resolve via import map */
|
|
cbm_resolution_t res =
|
|
cbm_registry_resolve(r, "worker.Process", "proj.cmd.main", keys, vals, 1);
|
|
ASSERT_STR_EQ(res.qualified_name, "proj.pkg.worker.Process");
|
|
ASSERT_STR_EQ(res.strategy, "import_map");
|
|
ASSERT_TRUE(res.confidence >= 0.90);
|
|
|
|
cbm_registry_free(r);
|
|
PASS();
|
|
}
|
|
|
|
/* Bare function call (no dot) routed through import_map. The candidate QN
|
|
* must be module_qn.callee, not module_qn — otherwise lookups fall through
|
|
* to name-based resolution and pick a same-named function from a different
|
|
* file. Regression for the @/lib/auth-style import case. */
|
|
TEST(resolve_import_map_bare_function) {
|
|
cbm_registry_t *r = cbm_registry_new();
|
|
cbm_registry_add(r, "requireAdmin", "proj.lib.authorization.requireAdmin", "Function");
|
|
/* Same name in another module — without the fix this is what gets picked. */
|
|
cbm_registry_add(r, "requireAdmin", "proj.lib.users.requireAdmin", "Function");
|
|
|
|
const char *keys[] = {"requireAdmin"};
|
|
const char *vals[] = {"proj.lib.authorization"};
|
|
|
|
cbm_resolution_t res =
|
|
cbm_registry_resolve(r, "requireAdmin", "proj.actions.settings", keys, vals, 1);
|
|
ASSERT_STR_EQ(res.qualified_name, "proj.lib.authorization.requireAdmin");
|
|
ASSERT_STR_EQ(res.strategy, "import_map");
|
|
|
|
cbm_registry_free(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(resolve_unique_name) {
|
|
cbm_registry_t *r = cbm_registry_new();
|
|
cbm_registry_add(r, "UniqueFunc", "proj.deep.path.UniqueFunc", "Function");
|
|
|
|
/* Call "UniqueFunc" — only one candidate project-wide */
|
|
cbm_resolution_t res =
|
|
cbm_registry_resolve(r, "UniqueFunc", "proj.other.module", NULL, NULL, 0);
|
|
ASSERT_STR_EQ(res.qualified_name, "proj.deep.path.UniqueFunc");
|
|
ASSERT_STR_EQ(res.strategy, "unique_name");
|
|
|
|
cbm_registry_free(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(resolve_unresolved) {
|
|
cbm_registry_t *r = cbm_registry_new();
|
|
cbm_registry_add(r, "foo", "proj.pkg.foo", "Function");
|
|
|
|
/* Call "nonexistent" — not in registry */
|
|
cbm_resolution_t res = cbm_registry_resolve(r, "nonexistent", "proj.other", NULL, NULL, 0);
|
|
ASSERT_TRUE(res.qualified_name == NULL || res.qualified_name[0] == '\0');
|
|
|
|
cbm_registry_free(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(resolve_many_nodes) {
|
|
cbm_registry_t *r = cbm_registry_new();
|
|
/* Add 500 functions */
|
|
for (int i = 0; i < 500; i++) {
|
|
char name[32], qn[64];
|
|
snprintf(name, sizeof(name), "func_%d", i);
|
|
snprintf(qn, sizeof(qn), "proj.pkg.func_%d", i);
|
|
cbm_registry_add(r, name, qn, "Function");
|
|
}
|
|
ASSERT_EQ(cbm_registry_size(r), 500);
|
|
|
|
/* Resolve one */
|
|
cbm_resolution_t res = cbm_registry_resolve(r, "func_250", "proj.pkg", NULL, NULL, 0);
|
|
ASSERT_STR_EQ(res.qualified_name, "proj.pkg.func_250");
|
|
|
|
cbm_registry_free(r);
|
|
PASS();
|
|
}
|
|
|
|
/* ── Confidence band ───────────────────────────────────────────── */
|
|
|
|
TEST(confidence_band_high) {
|
|
ASSERT_STR_EQ(cbm_confidence_band(0.95), "high");
|
|
ASSERT_STR_EQ(cbm_confidence_band(0.70), "high");
|
|
PASS();
|
|
}
|
|
|
|
TEST(confidence_band_medium) {
|
|
ASSERT_STR_EQ(cbm_confidence_band(0.55), "medium");
|
|
ASSERT_STR_EQ(cbm_confidence_band(0.45), "medium");
|
|
PASS();
|
|
}
|
|
|
|
TEST(confidence_band_speculative) {
|
|
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();
|
|
}
|
|
|
|
/* ── Suffix match resolution ──────────────────────────────────── */
|
|
|
|
TEST(resolve_suffix_match) {
|
|
cbm_registry_t *r = cbm_registry_new();
|
|
cbm_registry_add(r, "Process", "proj.svcA.Process", "Function");
|
|
cbm_registry_add(r, "Process", "proj.svcB.Process", "Function");
|
|
|
|
/* Caller in svcA — should prefer svcA via import distance */
|
|
cbm_resolution_t res = cbm_registry_resolve(r, "Process", "proj.svcA.caller", NULL, NULL, 0);
|
|
ASSERT_STR_EQ(res.qualified_name, "proj.svcA.Process");
|
|
ASSERT_STR_EQ(res.strategy, "suffix_match");
|
|
ASSERT_TRUE(res.confidence >= 0.50 && res.confidence <= 0.60);
|
|
|
|
cbm_registry_free(r);
|
|
PASS();
|
|
}
|
|
|
|
/* ── Import map suffix resolution ─────────────────────────────── */
|
|
|
|
TEST(resolve_import_map_suffix) {
|
|
cbm_registry_t *r = cbm_registry_new();
|
|
cbm_registry_add(r, "Foo", "proj.other.sub.Foo", "Function");
|
|
|
|
const char *keys[] = {"other"};
|
|
const char *vals[] = {"proj.other"};
|
|
|
|
/* "other.Foo" → exact "proj.other.Foo" not found →
|
|
* suffix scan finds "proj.other.sub.Foo" */
|
|
cbm_resolution_t res = cbm_registry_resolve(r, "other.Foo", "proj.pkg", keys, vals, 1);
|
|
ASSERT_STR_EQ(res.qualified_name, "proj.other.sub.Foo");
|
|
ASSERT_STR_EQ(res.strategy, "import_map_suffix");
|
|
ASSERT_TRUE(res.confidence >= 0.80 && res.confidence <= 0.90);
|
|
|
|
cbm_registry_free(r);
|
|
PASS();
|
|
}
|
|
|
|
/* ── Import reachability tests ────────────────────────────────── */
|
|
|
|
TEST(resolve_is_import_reachable) {
|
|
/* Test import reachability through unique_name confidence penalty.
|
|
* is_import_reachable is static, so we test it indirectly. */
|
|
cbm_registry_t *r = cbm_registry_new();
|
|
cbm_registry_add(r, "Helper", "proj.shared.utils.Helper", "Function");
|
|
|
|
/* With import covering the module → full confidence */
|
|
const char *keys1[] = {"utils"};
|
|
const char *vals1[] = {"proj.shared.utils"};
|
|
cbm_resolution_t res = cbm_registry_resolve(r, "Helper", "proj.caller", keys1, vals1, 1);
|
|
ASSERT_STR_EQ(res.strategy, "unique_name");
|
|
ASSERT_TRUE(res.confidence >= 0.70); /* 0.75, not halved */
|
|
|
|
/* With import NOT covering the module → halved */
|
|
const char *keys2[] = {"other"};
|
|
const char *vals2[] = {"proj.other"};
|
|
res = cbm_registry_resolve(r, "Helper", "proj.caller", keys2, vals2, 1);
|
|
ASSERT_STR_EQ(res.strategy, "unique_name");
|
|
ASSERT_TRUE(res.confidence <= 0.40); /* 0.75 * 0.5 = 0.375 */
|
|
|
|
cbm_registry_free(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(resolve_import_reachable_prefix) {
|
|
/* "proj.handler.sub.Process" should be reachable via import "proj.handler" */
|
|
cbm_registry_t *r = cbm_registry_new();
|
|
cbm_registry_add(r, "Process", "proj.handler.sub.Process", "Function");
|
|
|
|
const char *keys[] = {"handler"};
|
|
const char *vals[] = {"proj.handler"};
|
|
cbm_resolution_t res = cbm_registry_resolve(r, "Process", "proj.caller", keys, vals, 1);
|
|
ASSERT_STR_EQ(res.strategy, "unique_name");
|
|
ASSERT_TRUE(res.confidence >= 0.70); /* reachable → no penalty */
|
|
|
|
cbm_registry_free(r);
|
|
PASS();
|
|
}
|
|
|
|
/* ── Negative import evidence ─────────────────────────────────── */
|
|
|
|
TEST(negative_import_rejects_unimported) {
|
|
cbm_registry_t *r = cbm_registry_new();
|
|
cbm_registry_add(r, "Process", "proj.billing.Process", "Function");
|
|
cbm_registry_add(r, "Process", "proj.handler.Process", "Function");
|
|
|
|
/* Import only handler's module — suffix_match should prefer handler */
|
|
const char *keys[] = {"handler"};
|
|
const char *vals[] = {"proj.handler"};
|
|
cbm_resolution_t res = cbm_registry_resolve(r, "Process", "proj.caller", keys, vals, 1);
|
|
ASSERT_STR_EQ(res.qualified_name, "proj.handler.Process");
|
|
|
|
cbm_registry_free(r);
|
|
PASS();
|
|
}
|
|
|
|
/* ── Fuzzy resolve ────────────────────────────────────────────── */
|
|
|
|
TEST(fuzzy_resolve_single_candidate) {
|
|
cbm_registry_t *r = cbm_registry_new();
|
|
cbm_registry_add(r, "CreateOrder", "svcA.handlers.CreateOrder", "Function");
|
|
cbm_registry_add(r, "ValidateOrder", "svcB.validators.ValidateOrder", "Function");
|
|
|
|
/* FuzzyResolve should find by simple name even with unknown prefix */
|
|
cbm_fuzzy_result_t fr =
|
|
cbm_registry_fuzzy_resolve(r, "unknownPkg.CreateOrder", "svcC.caller", NULL, NULL, 0);
|
|
ASSERT_TRUE(fr.ok);
|
|
ASSERT_STR_EQ(fr.result.qualified_name, "svcA.handlers.CreateOrder");
|
|
|
|
cbm_registry_free(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(fuzzy_resolve_nonexistent) {
|
|
cbm_registry_t *r = cbm_registry_new();
|
|
cbm_registry_add(r, "CreateOrder", "svcA.handlers.CreateOrder", "Function");
|
|
|
|
cbm_fuzzy_result_t fr =
|
|
cbm_registry_fuzzy_resolve(r, "NonExistent", "svcC.caller", NULL, NULL, 0);
|
|
ASSERT_FALSE(fr.ok);
|
|
|
|
cbm_registry_free(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(fuzzy_resolve_multiple_best_by_distance) {
|
|
cbm_registry_t *r = cbm_registry_new();
|
|
cbm_registry_add(r, "Process", "svcA.handlers.Process", "Function");
|
|
cbm_registry_add(r, "Process", "svcB.handlers.Process", "Function");
|
|
|
|
/* Caller in svcA — should prefer svcA */
|
|
cbm_fuzzy_result_t fr =
|
|
cbm_registry_fuzzy_resolve(r, "unknown.Process", "svcA.other", NULL, NULL, 0);
|
|
ASSERT_TRUE(fr.ok);
|
|
ASSERT_STR_EQ(fr.result.qualified_name, "svcA.handlers.Process");
|
|
|
|
/* Caller in svcB — should prefer svcB */
|
|
fr = cbm_registry_fuzzy_resolve(r, "unknown.Process", "svcB.other", NULL, NULL, 0);
|
|
ASSERT_TRUE(fr.ok);
|
|
ASSERT_STR_EQ(fr.result.qualified_name, "svcB.handlers.Process");
|
|
|
|
cbm_registry_free(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(fuzzy_resolve_deep_name_extraction) {
|
|
cbm_registry_t *r = cbm_registry_new();
|
|
cbm_registry_add(r, "DoWork", "myproject.utils.DoWork", "Function");
|
|
|
|
/* Deeply qualified callee — should extract "DoWork" */
|
|
cbm_fuzzy_result_t fr =
|
|
cbm_registry_fuzzy_resolve(r, "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(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(fuzzy_resolve_empty_registry) {
|
|
cbm_registry_t *r = cbm_registry_new();
|
|
|
|
cbm_fuzzy_result_t fr =
|
|
cbm_registry_fuzzy_resolve(r, "SomeFunc", "myproject.caller", NULL, NULL, 0);
|
|
ASSERT_FALSE(fr.ok);
|
|
|
|
cbm_registry_free(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(fuzzy_resolve_confidence_single) {
|
|
cbm_registry_t *r = cbm_registry_new();
|
|
cbm_registry_add(r, "Handler", "proj.svc.Handler", "Function");
|
|
|
|
cbm_fuzzy_result_t fr =
|
|
cbm_registry_fuzzy_resolve(r, "unknownPkg.Handler", "proj.caller", NULL, NULL, 0);
|
|
ASSERT_TRUE(fr.ok);
|
|
ASSERT_TRUE(fr.result.confidence >= 0.35 && fr.result.confidence <= 0.45);
|
|
ASSERT_STR_EQ(fr.result.strategy, "fuzzy");
|
|
|
|
cbm_registry_free(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(fuzzy_resolve_confidence_distance) {
|
|
cbm_registry_t *r = cbm_registry_new();
|
|
cbm_registry_add(r, "Process", "proj.svcA.Process", "Function");
|
|
cbm_registry_add(r, "Process", "proj.svcB.Process", "Function");
|
|
|
|
cbm_fuzzy_result_t fr =
|
|
cbm_registry_fuzzy_resolve(r, "unknownPkg.Process", "proj.svcA.other", NULL, NULL, 0);
|
|
ASSERT_TRUE(fr.ok);
|
|
ASSERT_TRUE(fr.result.confidence >= 0.25 && fr.result.confidence <= 0.35);
|
|
ASSERT_STR_EQ(fr.result.strategy, "fuzzy");
|
|
|
|
cbm_registry_free(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(fuzzy_penalty_unreachable_import) {
|
|
cbm_registry_t *r = cbm_registry_new();
|
|
cbm_registry_add(r, "Handler", "proj.billing.Handler", "Function");
|
|
|
|
/* Import for different module → confidence halved */
|
|
const char *keys[] = {"other"};
|
|
const char *vals[] = {"proj.other"};
|
|
cbm_fuzzy_result_t fr =
|
|
cbm_registry_fuzzy_resolve(r, "unknown.Handler", "proj.caller", keys, vals, 1);
|
|
ASSERT_TRUE(fr.ok);
|
|
/* 0.40 * 0.5 = 0.20 */
|
|
ASSERT_TRUE(fr.result.confidence >= 0.15 && fr.result.confidence <= 0.25);
|
|
|
|
cbm_registry_free(r);
|
|
PASS();
|
|
}
|
|
|
|
TEST(fuzzy_no_import_map_passthrough) {
|
|
cbm_registry_t *r = cbm_registry_new();
|
|
cbm_registry_add(r, "Handler", "proj.billing.Handler", "Function");
|
|
|
|
/* nil import map → full confidence */
|
|
cbm_fuzzy_result_t fr =
|
|
cbm_registry_fuzzy_resolve(r, "unknown.Handler", "proj.caller", NULL, NULL, 0);
|
|
ASSERT_TRUE(fr.ok);
|
|
ASSERT_TRUE(fr.result.confidence >= 0.35 && fr.result.confidence <= 0.45);
|
|
|
|
cbm_registry_free(r);
|
|
PASS();
|
|
}
|
|
|
|
/* ── Suite ─────────────────────────────────────────────────────── */
|
|
|
|
SUITE(registry) {
|
|
/* FQN */
|
|
RUN_TEST(fqn_simple);
|
|
RUN_TEST(fqn_no_name);
|
|
RUN_TEST(fqn_python_init);
|
|
RUN_TEST(fqn_js_index);
|
|
RUN_TEST(fqn_module);
|
|
RUN_TEST(fqn_folder);
|
|
RUN_TEST(fqn_root_file);
|
|
/* FQN collision regression (Folder vs __init__.py Module) */
|
|
RUN_TEST(fqn_init_module_distinct_from_folder);
|
|
RUN_TEST(fqn_init_nested_module_distinct);
|
|
RUN_TEST(fqn_index_ts_module_distinct_from_folder);
|
|
RUN_TEST(fqn_init_symbols_get_clean_package_qn);
|
|
RUN_TEST(fqn_index_symbols_get_clean_qn);
|
|
RUN_TEST(fqn_init_file_node_distinct);
|
|
RUN_TEST(fqn_regular_module_unchanged);
|
|
RUN_TEST(project_name_from_path);
|
|
RUN_TEST(project_name_from_root);
|
|
/* Registry lifecycle */
|
|
RUN_TEST(registry_create_free);
|
|
RUN_TEST(registry_free_null);
|
|
RUN_TEST(registry_add_and_exists);
|
|
RUN_TEST(registry_label_of);
|
|
RUN_TEST(registry_find_by_name);
|
|
RUN_TEST(registry_no_duplicates);
|
|
/* Resolution */
|
|
RUN_TEST(resolve_same_module);
|
|
RUN_TEST(resolve_import_map);
|
|
RUN_TEST(resolve_import_map_bare_function);
|
|
RUN_TEST(resolve_unique_name);
|
|
RUN_TEST(resolve_unresolved);
|
|
RUN_TEST(resolve_many_nodes);
|
|
/* Confidence band */
|
|
RUN_TEST(confidence_band_high);
|
|
RUN_TEST(confidence_band_medium);
|
|
RUN_TEST(confidence_band_speculative);
|
|
/* Suffix match + import map suffix */
|
|
RUN_TEST(resolve_suffix_match);
|
|
RUN_TEST(resolve_import_map_suffix);
|
|
/* Import reachability */
|
|
RUN_TEST(resolve_is_import_reachable);
|
|
RUN_TEST(resolve_import_reachable_prefix);
|
|
/* Negative import evidence */
|
|
RUN_TEST(negative_import_rejects_unimported);
|
|
/* Fuzzy resolve */
|
|
RUN_TEST(fuzzy_resolve_single_candidate);
|
|
RUN_TEST(fuzzy_resolve_nonexistent);
|
|
RUN_TEST(fuzzy_resolve_multiple_best_by_distance);
|
|
RUN_TEST(fuzzy_resolve_deep_name_extraction);
|
|
RUN_TEST(fuzzy_resolve_empty_registry);
|
|
RUN_TEST(fuzzy_resolve_confidence_single);
|
|
RUN_TEST(fuzzy_resolve_confidence_distance);
|
|
RUN_TEST(fuzzy_penalty_unreachable_import);
|
|
RUN_TEST(fuzzy_no_import_map_passthrough);
|
|
}
|