From f4efd0b86844bf2ac965ba9ec30c8805cc774e40 Mon Sep 17 00:00:00 2001 From: BobbieBarker <4307099+BobbieBarker@users.noreply.github.com> Date: Mon, 28 Sep 2026 17:50:28 -0700 Subject: [PATCH] fix(registry): score a candidate on the extractor's test verdict, not its name cbm_registry_add discarded the caller's knowledge of whether a symbol is test code and recovered it from the qualified name: is_test_qn scanned the QN for "test", "Test", "mock", "stub", "spec", "fake" or "Fixture" anywhere in the string. REG_TEST_PENALTY then keeps non-test candidates ahead of test ones, so that scan decides which definition an unresolved call resolves to. An unanchored substring match over a path is wrong in both directions, and the false positives are not exotic. On one 970-file Elixir tree the scan calls 794 of 20,882 Function and Method nodes test code. 516 of those match on "spec", and 268 match ONLY because "inspect" contains it: forge_symphony.pipeline.impl .inspect_optional, .pr_tracker.continuation_intent.inspect_record and every other symbol under an operator_inspect module is ordinary production code that was being ranked below real test code. "latest" and "contest" match "test" the same way. In the other direction a test helper under a directory that spells none of those words is scored as production. The extractor already knows. CBMDefinition.is_test is set from the file's test-ness (cbm_is_test_file) OR the definition's own attributes, and it is the value the store filters on. cbm_registry_add now takes it and stores it beside the QN, and is_test_qn is deleted. Three things follow from the verdict no longer being recomputable: candidate_score loses its tri-state `int is_test` and its `-1: ask` fallback, because there is nothing left to ask. qn_test_flags stops returning NULL. Both filtered resolve paths have to carry the verdicts with the candidates. resolve_multi_with_imports already copied a parallel array but guarded it on `flags ?`; filter_import_reachable carried none at all, so the fuzzy path passed NULL whenever it had filtered and silently fell back to the QN scan. index_under_name drops an entry whose verdict it cannot store, rather than leaving the flag array short. A missing verdict scores as non-test, which would let an unranked candidate outrank real code; one missing candidate is the smaller error. pipeline_incremental.c seeds the registry from persisted nodes and has no CBMDefinition, so it reads the same flag out of properties_json through cbm_node_is_test, exported from pass_tests.c. Its comment requires the incremental registry to mirror the full-index one exactly, and both now answer from the same value. Measured by indexing four corpora with a binary built from this commit's parent and with this one, and diffing the `edges` rows keyed on (source QN, target QN, type): corpus moved retargeted lost (resolve) lost (similarity) Elixir 970-file 920 919 1 0 ripgrep 14.1.1 353 193 30 130 tokio 1.40.0 1,548 1,426 86 36 rust-analyzer 5,160 4,817 219 124 Almost every moved edge is the same call site landing on a different target. 336 resolution edges are lost across the four, where the new winner is refused by a check the old one passed. The similarity column is SEMANTICALLY_RELATED, which the post-passes rebuild wholesale over a drifting corpus and which moves on its own. This is a behaviour change across every indexed project, not a signature change, and the gating suite does not see it: 8,236 tests pass with the scan and without it. REG_TEST_PENALTY had no direct coverage, because none of the 95 cbm_registry_add call sites in the suite registered a symbol the scan would flag. tests/test_registry.c: three tests. registry_prefers_non_test_over_test_ candidate pins the penalty itself. registry_test_verdict_comes_from_the_caller _not_the_qn pins where the verdict comes from, using proj.latest.parse (production code the scan reads as a test) against proj.harness.parse (a test the scan reads as production); it fails with the scan restored and the signature kept. registry_sole_test_candidate_still_resolves pins that the penalty orders candidates without vetoing them. Signed-off-by: Chad <4307099+BobbieBarker@users.noreply.github.com> --- src/pipeline/pass_definitions.c | 2 +- src/pipeline/pass_parallel.c | 2 +- src/pipeline/pass_tests.c | 6 +- src/pipeline/pipeline.h | 5 +- src/pipeline/pipeline_incremental.c | 2 +- src/pipeline/pipeline_internal.h | 6 ++ src/pipeline/registry.c | 134 ++++++++---------------- tests/test_pipeline.c | 78 +++++++------- tests/test_registry.c | 157 ++++++++++++++++++---------- 9 files changed, 199 insertions(+), 193 deletions(-) diff --git a/src/pipeline/pass_definitions.c b/src/pipeline/pass_definitions.c index 7c7ed68847..c6bb872c46 100644 --- a/src/pipeline/pass_definitions.c +++ b/src/pipeline/pass_definitions.c @@ -335,7 +335,7 @@ static void process_def(cbm_pipeline_ctx_t *ctx, const CBMDefinition *def, const * (SQL FROM/JOIN lineage). pass_parallel.c and pipeline_incremental.c seed * through the same predicate, so the three registries cannot diverge. */ if (node_id > 0 && cbm_label_is_registry_symbol(def->label)) { - cbm_registry_add(ctx->registry, def->name, def->qualified_name, def->label); + cbm_registry_add(ctx->registry, def->name, def->qualified_name, def->label, def->is_test); } char *file_qn = cbm_pipeline_fqn_compute(ctx->project_name, rel, "__file__"); const cbm_gbuf_node_t *file_node = cbm_gbuf_find_by_qn(ctx->gbuf, file_qn); diff --git a/src/pipeline/pass_parallel.c b/src/pipeline/pass_parallel.c index e85ad015f1..7a760fdcb4 100644 --- a/src/pipeline/pass_parallel.c +++ b/src/pipeline/pass_parallel.c @@ -1588,7 +1588,7 @@ static int register_and_link_def(cbm_pipeline_ctx_t *ctx, const CBMDefinition *d /* Registry membership is defined ONCE by cbm_label_is_registry_symbol * (helpers.c) — see pass_definitions.c for the per-label rationale. */ if (cbm_label_is_registry_symbol(def->label)) { - cbm_registry_add(ctx->registry, def->name, def->qualified_name, def->label); + cbm_registry_add(ctx->registry, def->name, def->qualified_name, def->label, def->is_test); (*reg_entries)++; } const cbm_gbuf_node_t *def_node = cbm_gbuf_find_by_qn(ctx->gbuf, def->qualified_name); diff --git a/src/pipeline/pass_tests.c b/src/pipeline/pass_tests.c index 9cade02a0d..65c099bf08 100644 --- a/src/pipeline/pass_tests.c +++ b/src/pipeline/pass_tests.c @@ -44,7 +44,7 @@ static const char *itoa_log(int val) { } /* Check if a node has is_test:true in its properties JSON. */ -static bool node_is_test(const cbm_gbuf_node_t *n) { +bool cbm_node_is_test(const cbm_gbuf_node_t *n) { if (!n || !n->properties_json) { return false; } @@ -227,13 +227,13 @@ static int create_tests_edges(cbm_pipeline_ctx_t *ctx) { } bool src_is_test = - node_is_test(src) || (src->file_path && cbm_is_test_path(src->file_path)); + cbm_node_is_test(src) || (src->file_path && cbm_is_test_path(src->file_path)); if (!src_is_test) { continue; } bool tgt_is_test = - node_is_test(tgt) || (tgt->file_path && cbm_is_test_path(tgt->file_path)); + cbm_node_is_test(tgt) || (tgt->file_path && cbm_is_test_path(tgt->file_path)); if (tgt_is_test) { continue; } diff --git a/src/pipeline/pipeline.h b/src/pipeline/pipeline.h index c0fddc7e77..9d47e826fc 100644 --- a/src/pipeline/pipeline.h +++ b/src/pipeline/pipeline.h @@ -229,8 +229,11 @@ cbm_registry_t *cbm_registry_new(void); void cbm_registry_free(cbm_registry_t *r); /* Register a function/method/class. All strings are copied. */ +/* `is_test` is the extractor's verdict for this symbol (CBMDefinition.is_test: + * the file's test-ness OR the definition's own test attributes). The registry + * stores it and scores with it; it does not derive test-ness from the QN. */ void cbm_registry_add(cbm_registry_t *r, const char *name, const char *qualified_name, - const char *label); + const char *label, bool is_test); /* Resolve a callee name using prioritized strategies. * import_map: NULL-terminated array of {local_name, resolved_qn} pairs, or NULL. diff --git a/src/pipeline/pipeline_incremental.c b/src/pipeline/pipeline_incremental.c index 154518658d..cc90c31941 100644 --- a/src/pipeline/pipeline_incremental.c +++ b/src/pipeline/pipeline_incremental.c @@ -1041,7 +1041,7 @@ static void registry_visitor(const cbm_gbuf_node_t *node, void *userdata) { if (!incr_label_is_registry_symbol(node->label)) { return; } - cbm_registry_add(r, node->name, node->qualified_name, node->label); + cbm_registry_add(r, node->name, node->qualified_name, node->label, cbm_node_is_test(node)); } static void free_incremental_result_cache(CBMFileResult **cache, int count) { diff --git a/src/pipeline/pipeline_internal.h b/src/pipeline/pipeline_internal.h index e7782adeff..6c7ac96e92 100644 --- a/src/pipeline/pipeline_internal.h +++ b/src/pipeline/pipeline_internal.h @@ -166,6 +166,12 @@ typedef struct { typedef bool (*cbm_result_want_fn)(const CBMFileResult *header); CBMFileResult *cbm_pipeline_result_acquire(const cbm_pipeline_ctx_t *ctx, CBMFileResult **cache, int i, cbm_result_want_fn want, bool *loaded); +/* True when a graph-buffer node carries is_test:true in its properties JSON. + * The incremental registry seed has only the persisted node, not the + * CBMDefinition the definition passes hand to cbm_registry_add, and both must + * give the registry the same verdict for the same symbol. */ +bool cbm_node_is_test(const cbm_gbuf_node_t *n); + void cbm_pipeline_result_release(CBMFileResult *r, bool loaded); /* Log the store counters, close and delete the store, drop the latch. */ diff --git a/src/pipeline/registry.c b/src/pipeline/registry.c index bdf6f5944d..c0db4d71fb 100644 --- a/src/pipeline/registry.c +++ b/src/pipeline/registry.c @@ -28,6 +28,7 @@ enum { REG_MAX_CANDIDATES = 256 }; #include "cbm.h" /* cbm_label_is_relation — the resolve-time relation veto */ #include "foundation/compat.h" /* CBM_TLS */ #include "foundation/hash_table.h" +#include "foundation/log.h" #include "foundation/dyn_array.h" #include "foundation/platform.h" @@ -72,16 +73,16 @@ const char *cbm_confidence_band(double score) { /* ── Internal types ──────────────────────────────────────────────── */ -/* Array of QN strings for byName index, with the test/mock verdict of each - * name cached beside it: the scorer asked is_test_qn for every candidate of - * every unresolved call, and the answer depends only on the name, which never - * changes once registered (waste sanitizer scaling lane, 2026-09-17). Field - * order and names match CBM_DYN_ARRAY(char *) so cbm_da_push works on it. */ +/* Array of QN strings for byName index, with each entry's test verdict beside + * it. The verdict is the extractor's, handed to cbm_registry_add by the caller + * that already computed it from the file path and the definition's attributes; + * it is stored rather than derived, so every entry always has one. Field order + * and names match CBM_DYN_ARRAY(char *) so cbm_da_push works on it. */ typedef struct { char **items; int count; int cap; - uint8_t *is_test; /* is_test_qn(items[i]), one byte per entry */ + uint8_t *is_test; /* the caller's verdict for items[i], one byte per entry */ int is_test_cap; } qn_array_t; @@ -151,78 +152,19 @@ static int common_prefix_len(const char *a, const char *b) { enum { REG_TEST_PENALTY = 1000 }; -/* Check if a qualified name looks like a test/mock path: does it contain any of - * Test, test, Mock, mock, Stub, stub, Fake, fake, Fixture, spec? One pass that - * dispatches on the first character instead of ten strstr scans of the whole - * name (this ran for every candidate of every unresolved call: the scaling - * lane's x4.0, waste sanitizer 2026-09-17). */ -static bool is_test_qn(const char *qn) { - if (!qn) { - return false; - } - for (const char *p = qn; *p; p++) { - switch (*p) { - case 'T': - if (strncmp(p, "Test", 4) == 0) { - return true; - } - break; - case 't': - if (strncmp(p, "test", 4) == 0) { - return true; - } - break; - case 'M': - if (strncmp(p, "Mock", 4) == 0) { - return true; - } - break; - case 'm': - if (strncmp(p, "mock", 4) == 0) { - return true; - } - break; - case 'S': - if (strncmp(p, "Stub", 4) == 0) { - return true; - } - break; - case 's': - if (strncmp(p, "stub", 4) == 0 || strncmp(p, "spec", 4) == 0) { - return true; - } - break; - case 'F': - if (strncmp(p, "Fake", 4) == 0 || strncmp(p, "Fixture", 7) == 0) { - return true; - } - break; - case 'f': - if (strncmp(p, "fake", 4) == 0) { - return true; - } - break; - default: - break; - } - } - return false; -} - -/* The cached test verdicts, only when they cover every entry: a failed growth - * leaves the cache short, and then the scorer asks per candidate as before. */ +/* The stored test verdicts. index_under_name drops an entry it cannot record a + * verdict for, so this covers every entry of a non-empty bucket. */ static const uint8_t *qn_test_flags(const qn_array_t *arr) { - return arr->is_test && arr->is_test_cap >= arr->count ? arr->is_test : NULL; + return arr->is_test; } /* Score a candidate for tiebreaking. Higher = better. * Layer 1: Non-test code preferred over test code (+1000) * Layer 2: Namespace proximity via common prefix length (+plen) - * `is_test` is the cached verdict when the caller has one (-1: ask). */ -static int candidate_score(const char *candidate_qn, const char *module_qn, int is_test) { + * `is_test` is the verdict stored for this candidate at registration. */ +static int candidate_score(const char *candidate_qn, const char *module_qn, bool is_test) { int score = 0; - bool test = is_test >= 0 ? is_test != 0 : is_test_qn(candidate_qn); - if (!test) { + if (!is_test) { score += REG_TEST_PENALTY; } score += common_prefix_len(candidate_qn, module_qn); @@ -268,8 +210,7 @@ static const char *best_by_import_distance(const char **candidates, const uint8_ const char *best = NULL; int best_score = CBM_NOT_FOUND; for (int i = 0; i < count; i++) { - int score = - candidate_score(candidates[i], module_qn, is_test_flags ? (int)is_test_flags[i] : -1); + int score = candidate_score(candidates[i], module_qn, is_test_flags[i] != 0); if (score > best_score || (score == best_score && best && candidate_outranks_on_tie(candidates[i], best))) { best_score = score; @@ -863,7 +804,8 @@ void cbm_registry_free(cbm_registry_t *r) { * * No array dedup needed: cbm_registry_add's exact-map check guarantees the QN * is new, and it calls this at most once per distinct key. */ -static void index_under_name(cbm_registry_t *r, const char *key, const char *owned_qn) { +static void index_under_name(cbm_registry_t *r, const char *key, const char *owned_qn, + bool is_test) { qn_array_t *arr = cbm_ht_get(r->by_name, key); if (!arr) { arr = calloc(CBM_ALLOC_ONE, sizeof(qn_array_t)); @@ -872,7 +814,7 @@ static void index_under_name(cbm_registry_t *r, const char *key, const char *own int before = arr->count; cbm_da_push(arr, (char *)owned_qn); if (arr->count == before) { - return; /* the name could not be recorded: no verdict to cache */ + return; /* the name could not be recorded: no verdict to store */ } if (arr->count > arr->is_test_cap) { int want = arr->cap > 0 ? arr->cap : arr->count; @@ -883,13 +825,19 @@ static void index_under_name(cbm_registry_t *r, const char *key, const char *own arr->is_test_cap = want; } } - if (arr->count <= arr->is_test_cap) { - arr->is_test[arr->count - SKIP_ONE] = is_test_qn(owned_qn) ? 1 : 0; + if (arr->count > arr->is_test_cap) { + /* The verdict can no longer be recomputed from the QN, so an entry + * without one would be scored as non-test and outrank real code. Drop + * it instead: one missing candidate beats a wrong winner. */ + arr->count = before; + cbm_log_warn("registry.verdict_alloc_failed", "key", key); + return; } + arr->is_test[arr->count - SKIP_ONE] = is_test ? 1 : 0; } void cbm_registry_add(cbm_registry_t *r, const char *name, const char *qualified_name, - const char *label) { + const char *label, bool is_test) { if (!r || !qualified_name || !label) { return; } @@ -945,14 +893,14 @@ void cbm_registry_add(cbm_registry_t *r, const char *name, const char *qualified * `name` is NULL or empty only for callers that have no symbol name to * give; those have the derived key and nothing else. */ const char *derived = simple_name(qualified_name); - index_under_name(r, derived, owned_qn); + index_under_name(r, derived, owned_qn, is_test); /* '#' is a QN fence, and extract_defs.c's rust_cfg_qualified_name is the * only thing in the tree that mints one today. A grammar that starts * minting a '#' opts into this second key by doing so, whatever it means by * the fence: its symbols become reachable under the passed name as well, * and they share that name's bucket with everything else filed under it. */ if (name && name[0] && strchr(derived, '#') && strcmp(name, derived) != 0) { - index_under_name(r, name, owned_qn); + index_under_name(r, name, owned_qn, is_test); } } @@ -1111,9 +1059,7 @@ static cbm_resolution_t resolve_multi_with_imports(const qn_array_t *arr, const int fcount = 0; for (int i = 0; i < arr->count && fcount < CBM_SZ_256; i++) { if (is_import_reachable(arr->items[i], import_vals, import_count)) { - if (flags) { - filtered_test[fcount] = flags[i]; - } + filtered_test[fcount] = flags[i]; filtered[fcount] = arr->items[i]; fcount++; } @@ -1123,8 +1069,7 @@ static cbm_resolution_t resolve_multi_with_imports(const qn_array_t *arr, const return (cbm_resolution_t){filtered[0], "suffix_match", conf, arr->count}; } if (fcount > SKIP_ONE) { - const char *best = - best_by_import_distance(filtered, flags ? filtered_test : NULL, fcount, module_qn); + const char *best = best_by_import_distance(filtered, filtered_test, fcount, module_qn); if (best) { double conf = candidate_count_penalty(CONF_SUFFIX_MATCH, fcount); return (cbm_resolution_t){best, "suffix_match", conf, fcount}; @@ -1454,11 +1399,15 @@ cbm_resolution_t cbm_registry_resolve_lineage(const cbm_registry_t *r, const cha /* ── Fuzzy Resolve ──────────────────────────────────────────────── */ /* Filter candidates by import reachability. Returns count of reachable. */ -static int filter_import_reachable(const char **candidates, int count, const char **import_vals, - int import_count, const char **out, int max_out) { +/* Keeps `out_flags` in step with `out`: the verdicts are stored per entry and + * cannot be recomputed from the QN, so a filtered view has to carry its own. */ +static int filter_import_reachable(const char **candidates, const uint8_t *flags, int count, + const char **import_vals, int import_count, const char **out, + uint8_t *out_flags, int max_out) { int n = 0; for (int i = 0; i < count && n < max_out; i++) { if (is_import_reachable(candidates[i], import_vals, import_count)) { + out_flags[n] = flags[i]; out[n++] = candidates[i]; } } @@ -1498,13 +1447,17 @@ cbm_fuzzy_result_t cbm_registry_fuzzy_resolve(const cbm_registry_t *r, const cha /* Multiple candidates: filter by import reachability */ const char *filtered[CBM_SZ_256]; + uint8_t filtered_flags[CBM_SZ_256]; int fcount = arr->count; const char **fptr = (const char **)arr->items; + const uint8_t *fflags = qn_test_flags(arr); if (have_imports) { - fcount = filter_import_reachable((const char **)arr->items, arr->count, import_map_vals, - import_map_count, filtered, CBM_SZ_256); + fcount = filter_import_reachable((const char **)arr->items, qn_test_flags(arr), arr->count, + import_map_vals, import_map_count, filtered, + filtered_flags, CBM_SZ_256); fptr = filtered; + fflags = filtered_flags; } if (fcount == 0) { @@ -1524,8 +1477,7 @@ cbm_fuzzy_result_t cbm_registry_fuzzy_resolve(const cbm_registry_t *r, const cha {fptr[0], "fuzzy", candidate_count_penalty(CONF_FUZZY_SINGLE, arr->count), arr->count}, true}; } - const char *best = best_by_import_distance( - fptr, fptr == (const char **)arr->items ? qn_test_flags(arr) : NULL, fcount, module_qn); + const char *best = best_by_import_distance(fptr, fflags, fcount, module_qn); if (!best) { return no_match; } diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index 568740f170..f5a1ddd4e2 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -10930,8 +10930,8 @@ TEST(helm_parse_chart_no_deps_issue338) { 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"); + cbm_registry_add(reg, "CreateOrder", "svcA.handlers.CreateOrder", "Function", false); + cbm_registry_add(reg, "ValidateOrder", "svcB.validators.ValidateOrder", "Function", false); /* Normal resolve unique name */ cbm_resolution_t r = cbm_registry_resolve(reg, "CreateOrder", "svcC.caller", NULL, NULL, 0); @@ -10949,7 +10949,7 @@ TEST(registry_resolve_single_candidate) { TEST(registry_fuzzy_nonexistent) { cbm_registry_t *reg = cbm_registry_new(); - cbm_registry_add(reg, "CreateOrder", "svcA.handlers.CreateOrder", "Function"); + cbm_registry_add(reg, "CreateOrder", "svcA.handlers.CreateOrder", "Function", false); cbm_fuzzy_result_t fr = cbm_registry_fuzzy_resolve(reg, "NonExistent", "svcC.caller", NULL, NULL, 0); @@ -10961,8 +10961,8 @@ TEST(registry_fuzzy_nonexistent) { 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"); + cbm_registry_add(reg, "Process", "svcA.handlers.Process", "Function", false); + cbm_registry_add(reg, "Process", "svcB.handlers.Process", "Function", false); /* Caller in svcA → prefer svcA */ cbm_fuzzy_result_t fr = @@ -10981,7 +10981,7 @@ TEST(registry_fuzzy_multiple_best_by_distance) { TEST(registry_fuzzy_simple_name_extraction) { cbm_registry_t *reg = cbm_registry_new(); - cbm_registry_add(reg, "DoWork", "myproject.utils.DoWork", "Function"); + cbm_registry_add(reg, "DoWork", "myproject.utils.DoWork", "Function", false); /* Deeply qualified name → extract "DoWork" */ cbm_fuzzy_result_t fr = cbm_registry_fuzzy_resolve(reg, "some.deep.module.DoWork", @@ -11006,8 +11006,8 @@ TEST(registry_fuzzy_empty) { 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"); + cbm_registry_add(reg, "Foo", "pkg.module.Foo", "Function", false); + cbm_registry_add(reg, "Bar", "pkg.module.Bar", "Method", false); ASSERT_TRUE(cbm_registry_exists(reg, "pkg.module.Foo")); ASSERT_TRUE(cbm_registry_exists(reg, "pkg.module.Bar")); @@ -11020,7 +11020,7 @@ TEST(registry_exists) { TEST(registry_confidence_import_map) { cbm_registry_t *reg = cbm_registry_new(); - cbm_registry_add(reg, "Foo", "proj.other.Foo", "Function"); + cbm_registry_add(reg, "Foo", "proj.other.Foo", "Function", false); const char *keys[] = {"other"}; const char *vals[] = {"proj.other"}; @@ -11035,7 +11035,7 @@ TEST(registry_confidence_import_map) { TEST(registry_confidence_import_map_suffix) { cbm_registry_t *reg = cbm_registry_new(); - cbm_registry_add(reg, "Foo", "proj.other.sub.Foo", "Function"); + cbm_registry_add(reg, "Foo", "proj.other.sub.Foo", "Function", false); const char *keys[] = {"other"}; const char *vals[] = {"proj.other"}; @@ -11050,7 +11050,7 @@ TEST(registry_confidence_import_map_suffix) { TEST(registry_confidence_same_module) { cbm_registry_t *reg = cbm_registry_new(); - cbm_registry_add(reg, "Foo", "proj.pkg.Foo", "Function"); + cbm_registry_add(reg, "Foo", "proj.pkg.Foo", "Function", false); cbm_resolution_t r = cbm_registry_resolve(reg, "Foo", "proj.pkg", NULL, NULL, 0); ASSERT_STR_EQ(r.qualified_name, "proj.pkg.Foo"); @@ -11063,7 +11063,7 @@ TEST(registry_confidence_same_module) { TEST(registry_confidence_unique_name) { cbm_registry_t *reg = cbm_registry_new(); - cbm_registry_add(reg, "Bar", "proj.pkg.Bar", "Function"); + cbm_registry_add(reg, "Bar", "proj.pkg.Bar", "Function", false); cbm_resolution_t r = cbm_registry_resolve(reg, "Bar", "proj.unrelated", NULL, NULL, 0); ASSERT_STR_EQ(r.qualified_name, "proj.pkg.Bar"); @@ -11076,8 +11076,8 @@ TEST(registry_confidence_unique_name) { 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_registry_add(reg, "Process", "proj.svcA.Process", "Function", false); + cbm_registry_add(reg, "Process", "proj.svcB.Process", "Function", false); cbm_resolution_t r = cbm_registry_resolve(reg, "Process", "proj.svcA.caller", NULL, NULL, 0); ASSERT_STR_EQ(r.qualified_name, "proj.svcA.Process"); @@ -11093,7 +11093,7 @@ TEST(registry_confidence_suffix_match) { * wrong target — and with one candidate it won the top name-only confidence. */ TEST(registry_receiver_chain_refuses_library_unique_name_issue1893) { cbm_registry_t *reg = cbm_registry_new(); - cbm_registry_add(reg, "data", "HomeboxUI.PickedFile.data", "Variable"); + cbm_registry_add(reg, "data", "HomeboxUI.PickedFile.data", "Variable", false); cbm_resolution_t r = cbm_registry_resolve(reg, "URLSession.shared.data", "HomeboxUI.Net", NULL, NULL, 0); @@ -11107,8 +11107,8 @@ TEST(registry_receiver_chain_refuses_library_unique_name_issue1893) { * the final name and import distance picks the winner. */ TEST(registry_receiver_chain_refuses_library_suffix_match_issue1893) { cbm_registry_t *reg = cbm_registry_new(); - cbm_registry_add(reg, "data", "HomeboxUI.PickedFile.data", "Variable"); - cbm_registry_add(reg, "data", "HomeboxUI.Payload.data", "Variable"); + cbm_registry_add(reg, "data", "HomeboxUI.PickedFile.data", "Variable", false); + cbm_registry_add(reg, "data", "HomeboxUI.Payload.data", "Variable", false); cbm_resolution_t r = cbm_registry_resolve(reg, "URLSession.shared.data", "HomeboxUI.Net", NULL, NULL, 0); @@ -11122,7 +11122,7 @@ TEST(registry_receiver_chain_refuses_library_suffix_match_issue1893) { * so Calendar really is in the receiver chain. */ TEST(registry_receiver_chain_keeps_project_extension_issue1893) { cbm_registry_t *reg = cbm_registry_new(); - cbm_registry_add(reg, "startOfDayUTC", "AuthDTOs.Calendar.startOfDayUTC", "Method"); + cbm_registry_add(reg, "startOfDayUTC", "AuthDTOs.Calendar.startOfDayUTC", "Method", false); cbm_resolution_t r = cbm_registry_resolve(reg, "Calendar.utcGregorian.startOfDayUTC", "HomeboxUI.Stats", NULL, NULL, 0); @@ -11142,7 +11142,7 @@ TEST(registry_receiver_chain_keeps_project_extension_issue1893) { TEST(registry_receiver_chain_admits_factory_chain_into_nested_type) { cbm_registry_t *reg = cbm_registry_new(); cbm_registry_add(reg, "putList", "org.elasticsearch.common.settings.Settings.Builder.putList", - "Method"); + "Method", false); cbm_resolution_t r = cbm_registry_resolve( reg, "Settings.builder().putList", "org.elasticsearch.index.IndexSettings", NULL, NULL, 0); @@ -11153,7 +11153,7 @@ TEST(registry_receiver_chain_admits_factory_chain_into_nested_type) { * .toBuilder().setWarningsHandler -> RequestOptions.Builder.setWarningsHandler. */ cbm_registry_add(reg, "setWarningsHandler", "org.elasticsearch.client.RequestOptions.Builder.setWarningsHandler", - "Method"); + "Method", false); r = cbm_registry_resolve(reg, "RequestOptions.DEFAULT.toBuilder().setWarningsHandler", "org.elasticsearch.client.Rest", NULL, NULL, 0); ASSERT_STR_EQ(r.qualified_name, @@ -11169,7 +11169,7 @@ TEST(registry_receiver_chain_admits_factory_chain_into_nested_type) { TEST(registry_receiver_chain_still_refuses_foreign_root_with_ancestry_rule) { cbm_registry_t *reg = cbm_registry_new(); cbm_registry_add(reg, "getEncoder", "org.elasticsearch.index.codec.DocOffsetsCodec.getEncoder", - "Method"); + "Method", false); cbm_resolution_t r = cbm_registry_resolve( reg, "Base64.getEncoder", "org.elasticsearch.index.IndexSettings", NULL, NULL, 0); @@ -11179,7 +11179,7 @@ TEST(registry_receiver_chain_still_refuses_foreign_root_with_ancestry_rule) { * org.elasticsearch.common.Math.toIntExact through "Math" appearing as an * ancestry segment ONLY IF that segment really is the type — here it is, and * that is the correct outcome (the project's own Math.toIntExact). */ - cbm_registry_add(reg, "toIntExact", "org.elasticsearch.common.Math.toIntExact", "Method"); + cbm_registry_add(reg, "toIntExact", "org.elasticsearch.common.Math.toIntExact", "Method", false); r = cbm_registry_resolve(reg, "Math.toIntExact", "org.elasticsearch.index.IndexSettings", NULL, NULL, 0); ASSERT_STR_EQ(r.qualified_name, "org.elasticsearch.common.Math.toIntExact"); @@ -11192,7 +11192,7 @@ TEST(registry_receiver_chain_still_refuses_foreign_root_with_ancestry_rule) { * must not look at it, or every ordinary vm.load style call would be refused. */ TEST(registry_receiver_chain_ignores_lowercase_root_issue1893) { cbm_registry_t *reg = cbm_registry_new(); - cbm_registry_add(reg, "load", "HomeboxUI.EntityListViewModel.load", "Method"); + cbm_registry_add(reg, "load", "HomeboxUI.EntityListViewModel.load", "Method", false); cbm_resolution_t r = cbm_registry_resolve(reg, "vm.load", "HomeboxUI.Views", NULL, NULL, 0); ASSERT_STR_EQ(r.qualified_name, "HomeboxUI.EntityListViewModel.load"); @@ -11205,7 +11205,7 @@ TEST(registry_receiver_chain_ignores_lowercase_root_issue1893) { /* An unqualified callee has no chain at all and must pass through unchanged. */ TEST(registry_receiver_chain_ignores_bare_name_issue1893) { cbm_registry_t *reg = cbm_registry_new(); - cbm_registry_add(reg, "helper", "proj.pkg.helper", "Function"); + cbm_registry_add(reg, "helper", "proj.pkg.helper", "Function", false); cbm_resolution_t r = cbm_registry_resolve(reg, "helper", "proj.other", NULL, NULL, 0); ASSERT_STR_EQ(r.qualified_name, "proj.pkg.helper"); @@ -11217,7 +11217,7 @@ TEST(registry_receiver_chain_ignores_bare_name_issue1893) { TEST(registry_fuzzy_confidence_single) { cbm_registry_t *reg = cbm_registry_new(); - cbm_registry_add(reg, "Handler", "proj.svc.Handler", "Function"); + cbm_registry_add(reg, "Handler", "proj.svc.Handler", "Function", false); cbm_fuzzy_result_t fr = cbm_registry_fuzzy_resolve(reg, "unknownPkg.Handler", "proj.caller", NULL, NULL, 0); @@ -11231,8 +11231,8 @@ TEST(registry_fuzzy_confidence_single) { 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_registry_add(reg, "Process", "proj.svcA.Process", "Function", false); + cbm_registry_add(reg, "Process", "proj.svcB.Process", "Function", false); cbm_fuzzy_result_t fr = cbm_registry_fuzzy_resolve(reg, "unknownPkg.Process", "proj.svcA.other", NULL, NULL, 0); @@ -11246,8 +11246,8 @@ TEST(registry_fuzzy_confidence_distance) { 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"); + cbm_registry_add(reg, "Process", "proj.billing.Process", "Function", false); + cbm_registry_add(reg, "Process", "proj.handler.Process", "Function", false); /* Import only handler's module → should prefer handler */ const char *keys[] = {"handler"}; @@ -11261,7 +11261,7 @@ TEST(registry_negative_import_rejects) { TEST(registry_fuzzy_import_penalty) { cbm_registry_t *reg = cbm_registry_new(); - cbm_registry_add(reg, "Handler", "proj.billing.Handler", "Function"); + cbm_registry_add(reg, "Handler", "proj.billing.Handler", "Function", false); /* Has imports but billing not imported → confidence halved */ const char *keys[] = {"other"}; @@ -11278,7 +11278,7 @@ TEST(registry_fuzzy_import_penalty) { TEST(registry_fuzzy_no_import_map_passthrough) { cbm_registry_t *reg = cbm_registry_new(); - cbm_registry_add(reg, "Handler", "proj.billing.Handler", "Function"); + cbm_registry_add(reg, "Handler", "proj.billing.Handler", "Function", false); /* NULL import map → no penalty, full fuzzy confidence */ cbm_fuzzy_result_t fr = @@ -11292,10 +11292,10 @@ TEST(registry_fuzzy_no_import_map_passthrough) { 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"); + cbm_registry_add(reg, "Foo", "proj.pkg.Foo", "Function", false); + cbm_registry_add(reg, "Bar", "proj.pkg.Bar", "Function", false); + cbm_registry_add(reg, "Foo", "proj.other.Foo", "Function", false); + cbm_registry_add(reg, "transform", "proj.utils.DataProcessor.transform", "Method", false); /* FindByName returns all entries for "Foo" */ const char **foos = NULL; @@ -12628,10 +12628,10 @@ TEST(registry_is_import_reachable) { /* 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"); + cbm_registry_add(reg, "Foo", "proj.pkg.Foo", "Function", false); + cbm_registry_add(reg, "Bar", "proj.pkg.Bar", "Function", false); + cbm_registry_add(reg, "Foo", "proj.other.Foo", "Function", false); + cbm_registry_add(reg, "transform", "proj.utils.DataProcessor.transform", "Method", false); /* FindEndingWith "DataProcessor.transform" → 1 match */ const char **matches = NULL; diff --git a/tests/test_registry.c b/tests/test_registry.c index f77b1460bb..beb125024a 100644 --- a/tests/test_registry.c +++ b/tests/test_registry.c @@ -191,7 +191,7 @@ TEST(registry_free_null) { TEST(registry_add_and_exists) { 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", false); 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")); @@ -199,10 +199,52 @@ TEST(registry_add_and_exists) { PASS(); } +/* REG_TEST_PENALTY had no direct coverage. Of the registry call sites in this + * suite, none registered a symbol the scorer would treat as a test, so the + * layer that keeps production code ahead of test code was never exercised. + * These three pin it, and pin where the verdict comes from. */ +TEST(registry_prefers_non_test_over_test_candidate) { + cbm_registry_t *r = cbm_registry_new(); + cbm_registry_add(r, "helper", "proj.app.helper", "Function", false); + cbm_registry_add(r, "helper", "proj.harness.helper", "Function", true); + + cbm_resolution_t res = cbm_registry_resolve(r, "helper", "proj.other", NULL, NULL, 0); + ASSERT_STR_EQ(res.qualified_name, "proj.app.helper"); + cbm_registry_free(r); + PASS(); +} + +/* The verdict is the caller's, not the QN's spelling. "latest" contains "test", + * so the scan this replaces read ordinary production code as a test and pushed + * it below a real one; "harness" spells nothing, so the same scan read a test + * as production. Both verdicts here are the extractor's, and both are right. */ +TEST(registry_test_verdict_comes_from_the_caller_not_the_qn) { + cbm_registry_t *r = cbm_registry_new(); + cbm_registry_add(r, "parse", "proj.latest.parse", "Function", false); + cbm_registry_add(r, "parse", "proj.harness.parse", "Function", true); + + cbm_resolution_t res = cbm_registry_resolve(r, "parse", "proj.other", NULL, NULL, 0); + ASSERT_STR_EQ(res.qualified_name, "proj.latest.parse"); + cbm_registry_free(r); + PASS(); +} + +/* A bucket of one is returned whatever its verdict: the penalty orders + * candidates, it does not veto them. */ +TEST(registry_sole_test_candidate_still_resolves) { + cbm_registry_t *r = cbm_registry_new(); + cbm_registry_add(r, "only", "proj.harness.only", "Function", true); + + cbm_resolution_t res = cbm_registry_resolve(r, "only", "proj.other", NULL, NULL, 0); + ASSERT_STR_EQ(res.qualified_name, "proj.harness.only"); + 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"); + cbm_registry_add(r, "Foo", "proj.pkg.Foo", "Class", false); + cbm_registry_add(r, "bar", "proj.pkg.bar", "Function", false); ASSERT_STR_EQ(cbm_registry_label_of(r, "proj.pkg.Foo"), "Class"); ASSERT_STR_EQ(cbm_registry_label_of(r, "proj.pkg.bar"), "Function"); @@ -214,9 +256,9 @@ TEST(registry_label_of) { 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"); + cbm_registry_add(r, "main", "proj.cmd.main", "Function", false); + cbm_registry_add(r, "main", "proj.srv.main", "Function", false); + cbm_registry_add(r, "helper", "proj.util.helper", "Function", false); const char **out = NULL; int count = 0; @@ -235,8 +277,8 @@ TEST(registry_find_by_name) { 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 */ + cbm_registry_add(r, "main", "proj.cmd.main", "Function", false); + cbm_registry_add(r, "main", "proj.cmd.main", "Function", false); /* duplicate */ ASSERT_EQ(cbm_registry_size(r), 1); const char **out = NULL; @@ -252,7 +294,7 @@ TEST(registry_no_duplicates) { TEST(resolve_same_module) { cbm_registry_t *r = cbm_registry_new(); - cbm_registry_add(r, "helper", "proj.pkg.service.helper", "Function"); + cbm_registry_add(r, "helper", "proj.pkg.service.helper", "Function", false); /* Call "helper" from the same module → should resolve */ cbm_resolution_t res = cbm_registry_resolve(r, "helper", "proj.pkg.service", NULL, NULL, 0); @@ -270,9 +312,9 @@ TEST(resolve_same_module) { * Foo::Bar::sub()) where the same sub name exists in multiple packages. */ TEST(resolve_qualified_disambiguates_same_name) { cbm_registry_t *r = cbm_registry_new(); - cbm_registry_add(r, "save", "proj.lib.App.Alpha.save", "Function"); - cbm_registry_add(r, "save", "proj.lib.App.Beta.save", "Function"); - cbm_registry_add(r, "save", "proj.lib.App.Gamma.save", "Function"); + cbm_registry_add(r, "save", "proj.lib.App.Alpha.save", "Function", false); + cbm_registry_add(r, "save", "proj.lib.App.Beta.save", "Function", false); + cbm_registry_add(r, "save", "proj.lib.App.Gamma.save", "Function", false); /* Each fully-qualified call routes to its own package. */ cbm_resolution_t a = @@ -313,8 +355,8 @@ TEST(resolve_qualified_disambiguates_same_name) { * pick arbitrarily under the high-confidence qualified_suffix strategy. */ TEST(resolve_qualified_ambiguous_tail_falls_through) { cbm_registry_t *r = cbm_registry_new(); - cbm_registry_add(r, "run", "proj.svcA.Foo.Bar.run", "Function"); - cbm_registry_add(r, "run", "proj.svcB.Foo.Bar.run", "Function"); + cbm_registry_add(r, "run", "proj.svcA.Foo.Bar.run", "Function", false); + cbm_registry_add(r, "run", "proj.svcB.Foo.Bar.run", "Function", false); /* "Foo::Bar::run" tail matches BOTH candidates → not unique → fall through. */ cbm_resolution_t res = @@ -332,7 +374,7 @@ TEST(resolve_qualified_ambiguous_tail_falls_through) { * `add` callee could ever reach it. */ TEST(registry_indexes_by_passed_name_not_qn_tail) { cbm_registry_t *r = cbm_registry_new(); - cbm_registry_add(r, "add", "proj.lib.add#cfg(test)", "Function"); + cbm_registry_add(r, "add", "proj.lib.add#cfg(test)", "Function", false); cbm_resolution_t res = cbm_registry_resolve(r, "add", "proj.lib.caller", NULL, NULL, 0); ASSERT_STR_EQ(res.qualified_name, "proj.lib.add#cfg(test)"); @@ -357,9 +399,9 @@ TEST(registry_indexes_by_passed_name_not_qn_tail) { * lookup a bare module reference needs. */ TEST(registry_indexes_a_dotted_name_under_its_tail_too) { cbm_registry_t *r = cbm_registry_new(); - cbm_registry_add(r, "resource.aws_instance.web", "proj.main.resource.aws_instance.web", - "Class"); - cbm_registry_add(r, "helper.py", "proj.pkg.helper", "Module"); + cbm_registry_add(r, "resource.aws_instance.web", "proj.main.resource.aws_instance.web", "Class", + false); + cbm_registry_add(r, "helper.py", "proj.pkg.helper", "Module", false); cbm_resolution_t tail = cbm_registry_resolve(r, "web", "proj.main", NULL, NULL, 0); ASSERT_STR_EQ(tail.qualified_name, "proj.main.resource.aws_instance.web"); @@ -377,7 +419,7 @@ TEST(registry_indexes_a_dotted_name_under_its_tail_too) { TEST(resolve_import_map) { cbm_registry_t *r = cbm_registry_new(); - cbm_registry_add(r, "Process", "proj.pkg.worker.Process", "Function"); + cbm_registry_add(r, "Process", "proj.pkg.worker.Process", "Function", false); /* Import map: "worker" → "proj.pkg.worker" */ const char *keys[] = {"worker"}; @@ -400,9 +442,9 @@ TEST(resolve_import_map) { * 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"); + cbm_registry_add(r, "requireAdmin", "proj.lib.authorization.requireAdmin", "Function", false); /* Same name in another module — without the fix this is what gets picked. */ - cbm_registry_add(r, "requireAdmin", "proj.lib.users.requireAdmin", "Function"); + cbm_registry_add(r, "requireAdmin", "proj.lib.users.requireAdmin", "Function", false); const char *keys[] = {"requireAdmin"}; const char *vals[] = {"proj.lib.authorization"}; @@ -423,7 +465,7 @@ TEST(resolve_import_map_bare_function) { * Regression for #875. */ TEST(resolve_import_map_bare_alias) { cbm_registry_t *r = cbm_registry_new(); - cbm_registry_add(r, "scan_bash", "proj.security_scan.scan_bash", "Function"); + cbm_registry_add(r, "scan_bash", "proj.security_scan.scan_bash", "Function", false); /* Import map: alias "_scan_bash" → FULL SYMBOL QN (not the module). */ const char *keys[] = {"_scan_bash"}; const char *vals[] = {"proj.security_scan.scan_bash"}; @@ -440,10 +482,10 @@ TEST(resolve_import_map_bare_alias) { * already on main via #875/#979; this locks the Yui G1 shape. */ TEST(resolve_import_map_aliased_from_import) { cbm_registry_t *r = cbm_registry_new(); - cbm_registry_add(r, "execute", "proj.services.satori_bridge.gate.execute", "Function"); + cbm_registry_add(r, "execute", "proj.services.satori_bridge.gate.execute", "Function", false); /* Alias ghost must not win if somehow registered. */ cbm_registry_add(r, "bridge_execute", "proj.services.satori_bridge.gate.bridge_execute", - "Function"); + "Function", false); const char *keys[] = {"bridge_execute"}; const char *vals[] = {"proj.services.satori_bridge.gate.execute"}; @@ -459,7 +501,7 @@ TEST(resolve_import_map_aliased_from_import) { TEST(resolve_unique_name) { cbm_registry_t *r = cbm_registry_new(); - cbm_registry_add(r, "UniqueFunc", "proj.deep.path.UniqueFunc", "Function"); + cbm_registry_add(r, "UniqueFunc", "proj.deep.path.UniqueFunc", "Function", false); /* Call "UniqueFunc" — only one candidate project-wide */ cbm_resolution_t res = @@ -473,7 +515,7 @@ TEST(resolve_unique_name) { TEST(resolve_unresolved) { cbm_registry_t *r = cbm_registry_new(); - cbm_registry_add(r, "foo", "proj.pkg.foo", "Function"); + cbm_registry_add(r, "foo", "proj.pkg.foo", "Function", false); /* Call "nonexistent" — not in registry */ cbm_resolution_t res = cbm_registry_resolve(r, "nonexistent", "proj.other", NULL, NULL, 0); @@ -490,7 +532,7 @@ TEST(resolve_many_nodes) { 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"); + cbm_registry_add(r, name, qn, "Function", false); } ASSERT_EQ(cbm_registry_size(r), 500); @@ -528,8 +570,8 @@ TEST(confidence_band_speculative) { 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"); + cbm_registry_add(r, "Process", "proj.svcA.Process", "Function", false); + cbm_registry_add(r, "Process", "proj.svcB.Process", "Function", false); /* Caller in svcA — should prefer svcA via import distance */ cbm_resolution_t res = cbm_registry_resolve(r, "Process", "proj.svcA.caller", NULL, NULL, 0); @@ -552,7 +594,7 @@ TEST(resolve_caps_unresolvably_ambiguous_names) { for (int i = 0; i < 300; i++) { char qn[64]; snprintf(qn, sizeof(qn), "proj.mod%d.flags", i); - cbm_registry_add(r, "flags", qn, "Variable"); + cbm_registry_add(r, "flags", qn, "Variable", false); } cbm_resolution_t res = cbm_registry_resolve(r, "flags", "proj.other.caller", NULL, NULL, 0); ASSERT_TRUE(res.qualified_name == NULL || res.qualified_name[0] == '\0'); @@ -570,7 +612,7 @@ TEST(resolve_caps_unresolvably_ambiguous_names) { TEST(resolve_import_map_suffix) { cbm_registry_t *r = cbm_registry_new(); - cbm_registry_add(r, "Foo", "proj.other.sub.Foo", "Function"); + cbm_registry_add(r, "Foo", "proj.other.sub.Foo", "Function", false); const char *keys[] = {"other"}; const char *vals[] = {"proj.other"}; @@ -592,7 +634,7 @@ 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"); + cbm_registry_add(r, "Helper", "proj.shared.utils.Helper", "Function", false); /* With import covering the module → full confidence */ const char *keys1[] = {"utils"}; @@ -615,7 +657,7 @@ TEST(resolve_is_import_reachable) { 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"); + cbm_registry_add(r, "Process", "proj.handler.sub.Process", "Function", false); const char *keys[] = {"handler"}; const char *vals[] = {"proj.handler"}; @@ -669,8 +711,8 @@ TEST(reach_cache_memo_matches_uncached_across_files) { 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"); + cbm_registry_add(r, "Process", "proj.billing.Process", "Function", false); + cbm_registry_add(r, "Process", "proj.handler.Process", "Function", false); /* Import only handler's module — suffix_match should prefer handler */ const char *keys[] = {"handler"}; @@ -686,8 +728,8 @@ TEST(negative_import_rejects_unimported) { 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"); + cbm_registry_add(r, "CreateOrder", "svcA.handlers.CreateOrder", "Function", false); + cbm_registry_add(r, "ValidateOrder", "svcB.validators.ValidateOrder", "Function", false); /* FuzzyResolve should find by simple name even with unknown prefix */ cbm_fuzzy_result_t fr = @@ -701,7 +743,7 @@ TEST(fuzzy_resolve_single_candidate) { TEST(fuzzy_resolve_nonexistent) { cbm_registry_t *r = cbm_registry_new(); - cbm_registry_add(r, "CreateOrder", "svcA.handlers.CreateOrder", "Function"); + cbm_registry_add(r, "CreateOrder", "svcA.handlers.CreateOrder", "Function", false); cbm_fuzzy_result_t fr = cbm_registry_fuzzy_resolve(r, "NonExistent", "svcC.caller", NULL, NULL, 0); @@ -713,8 +755,8 @@ TEST(fuzzy_resolve_nonexistent) { 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"); + cbm_registry_add(r, "Process", "svcA.handlers.Process", "Function", false); + cbm_registry_add(r, "Process", "svcB.handlers.Process", "Function", false); /* Caller in svcA — should prefer svcA */ cbm_fuzzy_result_t fr = @@ -733,7 +775,7 @@ TEST(fuzzy_resolve_multiple_best_by_distance) { TEST(fuzzy_resolve_deep_name_extraction) { cbm_registry_t *r = cbm_registry_new(); - cbm_registry_add(r, "DoWork", "myproject.utils.DoWork", "Function"); + cbm_registry_add(r, "DoWork", "myproject.utils.DoWork", "Function", false); /* Deeply qualified callee — should extract "DoWork" */ cbm_fuzzy_result_t fr = @@ -758,7 +800,7 @@ TEST(fuzzy_resolve_empty_registry) { TEST(fuzzy_resolve_confidence_single) { cbm_registry_t *r = cbm_registry_new(); - cbm_registry_add(r, "Handler", "proj.svc.Handler", "Function"); + cbm_registry_add(r, "Handler", "proj.svc.Handler", "Function", false); cbm_fuzzy_result_t fr = cbm_registry_fuzzy_resolve(r, "unknownPkg.Handler", "proj.caller", NULL, NULL, 0); @@ -772,8 +814,8 @@ TEST(fuzzy_resolve_confidence_single) { 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_registry_add(r, "Process", "proj.svcA.Process", "Function", false); + cbm_registry_add(r, "Process", "proj.svcB.Process", "Function", false); cbm_fuzzy_result_t fr = cbm_registry_fuzzy_resolve(r, "unknownPkg.Process", "proj.svcA.other", NULL, NULL, 0); @@ -787,7 +829,7 @@ TEST(fuzzy_resolve_confidence_distance) { TEST(fuzzy_penalty_unreachable_import) { cbm_registry_t *r = cbm_registry_new(); - cbm_registry_add(r, "Handler", "proj.billing.Handler", "Function"); + cbm_registry_add(r, "Handler", "proj.billing.Handler", "Function", false); /* Import for different module → confidence halved */ const char *keys[] = {"other"}; @@ -804,7 +846,7 @@ TEST(fuzzy_penalty_unreachable_import) { TEST(fuzzy_no_import_map_passthrough) { cbm_registry_t *r = cbm_registry_new(); - cbm_registry_add(r, "Handler", "proj.billing.Handler", "Function"); + cbm_registry_add(r, "Handler", "proj.billing.Handler", "Function", false); /* nil import map → full confidence */ cbm_fuzzy_result_t fr = @@ -913,8 +955,8 @@ TEST(registry_tie_break_is_independent_of_registration_order) { cbm_registry_t *forward = cbm_registry_new(); cbm_registry_t *backward = cbm_registry_new(); for (int i = 0; i < n; i++) { - cbm_registry_add(forward, "dev_name", cands[i], labels[i]); - cbm_registry_add(backward, "dev_name", cands[n - 1 - i], labels[n - 1 - i]); + cbm_registry_add(forward, "dev_name", cands[i], labels[i], false); + cbm_registry_add(backward, "dev_name", cands[n - 1 - i], labels[n - 1 - i], false); } /* A far-away caller with no imports: every candidate scores the same. */ @@ -932,10 +974,10 @@ TEST(registry_tie_break_is_independent_of_registration_order) { /* Equal depth: the smaller QN, from either order. */ cbm_registry_t *lex_a = cbm_registry_new(); cbm_registry_t *lex_b = cbm_registry_new(); - cbm_registry_add(lex_a, "sg_next", "proj.tools.virtio.scatterlist.sg_next", "Function"); - cbm_registry_add(lex_a, "sg_next", "proj.include.linux.scatterlist.sg_next", "Function"); - cbm_registry_add(lex_b, "sg_next", "proj.include.linux.scatterlist.sg_next", "Function"); - cbm_registry_add(lex_b, "sg_next", "proj.tools.virtio.scatterlist.sg_next", "Function"); + cbm_registry_add(lex_a, "sg_next", "proj.tools.virtio.scatterlist.sg_next", "Function", false); + cbm_registry_add(lex_a, "sg_next", "proj.include.linux.scatterlist.sg_next", "Function", false); + cbm_registry_add(lex_b, "sg_next", "proj.include.linux.scatterlist.sg_next", "Function", false); + cbm_registry_add(lex_b, "sg_next", "proj.tools.virtio.scatterlist.sg_next", "Function", false); cbm_resolution_t la = cbm_registry_resolve(lex_a, "sg_next", "proj.drivers.scsi.arm_scsi", NULL, NULL, 0); cbm_resolution_t lb = @@ -945,9 +987,9 @@ TEST(registry_tie_break_is_independent_of_registration_order) { /* Proximity still outranks depth: the sibling wins over a shallower stranger. */ cbm_registry_t *near = cbm_registry_new(); - cbm_registry_add(near, "vnic_rq_free", "proj.lib.vnic_rq_free", "Function"); + cbm_registry_add(near, "vnic_rq_free", "proj.lib.vnic_rq_free", "Function", false); cbm_registry_add(near, "vnic_rq_free", "proj.drivers.scsi.fnic.vnic_rq.vnic_rq_free", - "Function"); + "Function", false); cbm_resolution_t nr = cbm_registry_resolve(near, "vnic_rq_free", "proj.drivers.scsi.fnic.fnic_res", NULL, NULL, 0); ASSERT_STR_EQ(nr.qualified_name, "proj.drivers.scsi.fnic.vnic_rq.vnic_rq_free"); @@ -1209,8 +1251,8 @@ TEST(weak_call_guards_share_one_drop_list) { * Signal.send). Regression guard for #1000. */ TEST(resolve_import_map_alias_with_suffix_hits_method) { cbm_registry_t *r = cbm_registry_new(); - cbm_registry_add(r, "user_logged_in", "proj.auth.signals.user_logged_in", "Variable"); - cbm_registry_add(r, "send", "proj.auth.signals.user_logged_in.send", "Method"); + cbm_registry_add(r, "user_logged_in", "proj.auth.signals.user_logged_in", "Variable", false); + cbm_registry_add(r, "send", "proj.auth.signals.user_logged_in.send", "Method", false); const char *keys[] = {"user_logged_in"}; const char *vals[] = {"proj.auth.signals.user_logged_in"}; cbm_resolution_t res = @@ -1244,6 +1286,9 @@ SUITE(registry) { RUN_TEST(registry_create_free); RUN_TEST(registry_free_null); RUN_TEST(registry_add_and_exists); + RUN_TEST(registry_prefers_non_test_over_test_candidate); + RUN_TEST(registry_test_verdict_comes_from_the_caller_not_the_qn); + RUN_TEST(registry_sole_test_candidate_still_resolves); RUN_TEST(registry_label_of); RUN_TEST(registry_find_by_name); RUN_TEST(registry_no_duplicates);