diff --git a/.github/workflows/_test.yml b/.github/workflows/_test.yml index 0eddc1754..e1db65e32 100644 --- a/.github/workflows/_test.yml +++ b/.github/workflows/_test.yml @@ -460,7 +460,14 @@ jobs: run: scripts/ci/msan-lane.sh build - name: MSan suite (exclusions per scripts/msan.sh) - run: scripts/ci/msan-lane.sh run + # Reduce mmap ASLR entropy on the host first, as the TSan leg does: with + # the runner kernel's higher default the MSan runtime tries to re-exec + # itself with ADDR_NO_RANDOMIZE, which the container's seccomp profile + # refuses, and it aborts ("CHECK failed: msan_linux.cpp:193 ... + # personality") before any test runs. The container shares this kernel. + run: | + sudo sysctl -w vm.mmap_rnd_bits=28 || true + scripts/ci/msan-lane.sh run # macOS leak coverage. LeakSanitizer runs by default under ASan on Linux, so # test-unix's ubuntu legs have always had it; on macOS it is off by default diff --git a/scripts/test-windows.ps1 b/scripts/test-windows.ps1 index 8c1a8358a..28739163d 100644 --- a/scripts/test-windows.ps1 +++ b/scripts/test-windows.ps1 @@ -18,6 +18,7 @@ * test_hook_augment.py guards #618 (fixed by #619) * test_ui_drive_listing.py guards #548 (roots field) * test_cli_non_ascii_arg.py guards #423/#20 (wide-argv main()) + * test_mcp_stdio.py guards #654 (clean stdio stderr) * test_daemon_stability.py guards the daemon parameter surface, crash recovery, busy-stop refusal, and churn * test_windows_update_handoff.py guards that `update` @@ -166,6 +167,7 @@ $guards = @( "tests\windows\test_hook_augment.py", "tests\windows\test_ui_drive_listing.py", "tests\windows\test_cli_non_ascii_arg.py", + "tests\windows\test_mcp_stdio.py", "tests\windows\test_windows_update_handoff.py" ) diff --git a/src/cli/hook_augment.c b/src/cli/hook_augment.c index 58e75c6d2..ac0b37eb3 100644 --- a/src/cli/hook_augment.c +++ b/src/cli/hook_augment.c @@ -65,8 +65,8 @@ * Observability (#858): a fired deadline is otherwise indistinguishable from * "no matches", so the handler first write()s a pre-formatted breadcrumb to * /logs/hook-augment-timeouts.log (by default - * ~/.cache/codebase-memory-mcp/logs; fd and message prepared at arm time — - * only async-signal-safe write/_exit in the handler). */ + * ~/.cache/codebase-memory-mcp/logs) and to stderr (fd and message prepared at + * arm time — only async-signal-safe write/_exit in the handler). */ #ifndef _WIN32 #define HA_DEADLINE_DEFAULT_MS 2000 /* in-process budget; see ha_deadline_ms() */ #define HA_DEADLINE_MIN_MS 50 @@ -102,9 +102,16 @@ static size_t g_ha_crumb_len = 0; static void ha_deadline_exit(int sig) { (void)sig; - if (g_ha_crumb_fd >= 0 && g_ha_crumb_len > 0) { - ssize_t w = write(g_ha_crumb_fd, g_ha_crumb_msg, g_ha_crumb_len); - (void)w; + /* async-signal-safe only: write() the prepared breadcrumb to the timeouts + * log (when it opened) and ALWAYS to stderr, so a fired deadline is a + * visible event, never a silent 0-byte exit(0). */ + if (g_ha_crumb_len > 0) { + if (g_ha_crumb_fd >= 0) { + ssize_t w = write(g_ha_crumb_fd, g_ha_crumb_msg, g_ha_crumb_len); + (void)w; + } + ssize_t e = write(STDERR_FILENO, g_ha_crumb_msg, g_ha_crumb_len); + (void)e; } _exit(0); } @@ -115,6 +122,16 @@ static bool ha_fits(int written, size_t cap) { } static void ha_open_crumb_log(int deadline_ms) { + /* Pre-format the breadcrumb unconditionally: the handler may only write() a + * prepared buffer, and formatting it before the log path is resolved means + * stderr still gets the line even when the file cannot be opened (no HOME, + * unwritable cache dir) — the "never silent" guarantee holds regardless. */ + int n = snprintf(g_ha_crumb_msg, sizeof(g_ha_crumb_msg), + "hook-augment: deadline_exceeded ms=%d pid=%ld (raise via " + "CBM_HOOK_DEADLINE_MS)\n", + deadline_ms, (long)getpid()); + g_ha_crumb_len = (n > 0 && n < (int)sizeof(g_ha_crumb_msg)) ? (size_t)n : 0; + const char *override = getenv("CBM_HOOK_TIMEOUT_LOG"); /* tests + power users */ char path[CBM_SZ_1K]; /* A path cut off at the buffer names some other file, and O_CREAT would @@ -136,18 +153,10 @@ static void ha_open_crumb_log(int deadline_ms) { cbm_mkdir_p_ex(dir, 0755, CBM_MKDIR_FOLLOW_OWNED); } /* Startup may re-arm after resolving saved settings. Keep the original - * diagnostic descriptor and refresh only the deadline text below. */ + * diagnostic descriptor; the deadline text above is refreshed on every arm. */ if (g_ha_crumb_fd < 0) { g_ha_crumb_fd = open(path, O_WRONLY | O_CREAT | O_APPEND, 0644); } - if (g_ha_crumb_fd < 0) { - return; - } - int n = snprintf(g_ha_crumb_msg, sizeof(g_ha_crumb_msg), - "hook-augment: deadline_exceeded ms=%d pid=%ld (raise via " - "CBM_HOOK_DEADLINE_MS)\n", - deadline_ms, (long)getpid()); - g_ha_crumb_len = (n > 0 && n < (int)sizeof(g_ha_crumb_msg)) ? (size_t)n : 0; } int cbm_hook_augment_deadline_ms_for_testing(void) { diff --git a/src/daemon/frontend.c b/src/daemon/frontend.c index e76c920b6..fa81cc06d 100644 --- a/src/daemon/frontend.c +++ b/src/daemon/frontend.c @@ -121,9 +121,11 @@ static atomic_uint_fast64_t frontend_test_worker_observation_count = ATOMIC_VAR_ static atomic_uint_fast64_t frontend_test_worker_idle_count = ATOMIC_VAR_INIT(0); static atomic_uint_fast64_t frontend_test_routed_cancellation_count = ATOMIC_VAR_INIT(0); static atomic_uint_fast32_t frontend_test_replay_budget_ms = ATOMIC_VAR_INIT(0); +static atomic_uint_fast32_t frontend_test_monitor_idle_sleep = ATOMIC_VAR_INIT(0); void cbm_daemon_frontend_test_observer_reset(bool hold_monitor) { atomic_store_explicit(&frontend_test_monitor_observation_count, 0, memory_order_release); + atomic_store_explicit(&frontend_test_monitor_idle_sleep, 0, memory_order_release); atomic_store_explicit(&frontend_test_worker_observation_count, 0, memory_order_release); atomic_store_explicit(&frontend_test_worker_idle_count, 0, memory_order_release); atomic_store_explicit(&frontend_test_monitor_is_waiting, false, memory_order_release); @@ -157,6 +159,10 @@ uint64_t cbm_daemon_frontend_test_routed_cancellations(void) { uint32_t cbm_daemon_frontend_test_replay_timeout_ms(void) { return (uint32_t)atomic_load_explicit(&frontend_test_replay_budget_ms, memory_order_acquire); } + +uint32_t cbm_daemon_frontend_test_monitor_idle_sleep_ms(void) { + return (uint32_t)atomic_load_explicit(&frontend_test_monitor_idle_sleep, memory_order_acquire); +} #endif static cbm_version_cohort_maintenance_presence_t frontend_observe_maintenance( @@ -187,7 +193,11 @@ static void *frontend_maintenance_monitor_worker(void *opaque) { cbm_version_cohort_maintenance_presence_t presence = frontend_observe_maintenance(monitor->manager, true); if (presence == CBM_VERSION_COHORT_MAINTENANCE_ABSENT) { - cbm_usleep(FRONTEND_MAINTENANCE_IDLE_POLL_MS * 1000U); + uint32_t idle_ms = FRONTEND_MAINTENANCE_IDLE_POLL_MS; +#if defined(CBM_ENABLE_TEST_SEAMS) && CBM_ENABLE_TEST_SEAMS + atomic_store_explicit(&frontend_test_monitor_idle_sleep, idle_ms, memory_order_release); +#endif + cbm_usleep(idle_ms * 1000U); continue; } diff --git a/src/daemon/frontend.h b/src/daemon/frontend.h index 8ee33d276..4629644bb 100644 --- a/src/daemon/frontend.h +++ b/src/daemon/frontend.h @@ -83,6 +83,10 @@ uint64_t cbm_daemon_frontend_test_routed_cancellations(void); * settings requests. Recovery tests use distinct connect and startup budgets * and compare. */ uint32_t cbm_daemon_frontend_test_replay_timeout_ms(void); +/* Milliseconds the maintenance monitor last slept in its no-maintenance + * (ABSENT) branch, recorded from the exact value passed to the sleep; zero + * until the first idle cycle after reset. Pins the idle probe cadence. */ +uint32_t cbm_daemon_frontend_test_monitor_idle_sleep_ms(void); #endif /* Takes ownership of client and borrows cohort_manager for the complete call. diff --git a/src/daemon/runtime.c b/src/daemon/runtime.c index 64f6a05f4..eb2eddf70 100644 --- a/src/daemon/runtime.c +++ b/src/daemon/runtime.c @@ -798,6 +798,302 @@ static bool runtime_posix_stat_same_image(const struct stat *first, const struct #endif +/* ── Executable-image fingerprint cache ────────────────────────────────────── + * The build-identity fingerprint is a SHA-256 over the entire executable + * (cbm_daemon_build_fingerprint_native_file), a full read of a ~300 MB image. + * A daemon process otherwise pays it twice for its own image at start (the + * supervisor's startup capture, then the runtime service's active-image check), + * so the digest of this process's OWN image is cached, keyed by the file + * identity the acquire path verifies as stable across the hash: device, inode, + * size, mtime and change time (on Windows FILE_BASIC_INFO.ChangeTime). A peer's + * image is never cached: it is hashed on every fingerprinting acquire + * (re-hashing of a verified peer copy is avoided separately, #1955). + * + * Limiting the cache to the own image is what makes that key sufficient. On + * POSIX the change time moves on every write and cannot be set back. A Windows + * change time can be set through SetFileInformationByHandle, but Windows keeps + * a running image locked against modification for as long as the process, and + * so this cache, exists. A rebuilt or replaced binary rolls the key and misses, + * so the cache never returns a stale digest and build-cohort admission keeps + * its exact meaning. One entry suffices: a process has one own image. + * + * The cache is process-local: a fresh process (a hook, a CLI call) still hashes + * its own image once. A hit does not weaken the identity proof: the acquire + * chain still brackets the held image with its before/after stat and + * process-maps checks, so the returned digest is bound to that same verified + * image; only the re-read of bytes whose identity is unchanged is skipped. A + * freshly hashed digest is committed only after that post-hash check has + * passed, under the identity observed after the hash, so an image that changed + * under the hash is never cached. */ +#if defined(_WIN32) || defined(__APPLE__) || defined(__linux__) || defined(__FreeBSD__) || \ + defined(__NetBSD__) + +/* POSIX carries timespec fields; Windows stores FILETIME ticks in the seconds + * fields and leaves the nanoseconds fields zero. */ +typedef struct { + bool valid; + uint64_t device; + uint64_t inode; + uint64_t size; + int64_t mtime_seconds; + int64_t mtime_nanoseconds; + int64_t ctime_seconds; + int64_t ctime_nanoseconds; +} runtime_fingerprint_key_t; + +/* The single entry: this process's own image. */ +static atomic_flag runtime_fingerprint_cache_lock = ATOMIC_FLAG_INIT; +static runtime_fingerprint_key_t runtime_fingerprint_cache_key; +static char runtime_fingerprint_cache_digest[CBM_DAEMON_BUILD_FINGERPRINT_SIZE]; + +#ifdef CBM_ENABLE_TEST_SEAMS +static atomic_int runtime_fingerprint_hash_call_count; +static atomic_bool runtime_fingerprint_hash_stub_active; +static char runtime_fingerprint_hash_stub_digest[CBM_DAEMON_BUILD_FINGERPRINT_SIZE]; +#endif + +static bool runtime_fingerprint_key_equal(const runtime_fingerprint_key_t *a, + const runtime_fingerprint_key_t *b) { + return a->valid && b->valid && a->device == b->device && a->inode == b->inode && + a->size == b->size && a->mtime_seconds == b->mtime_seconds && + a->mtime_nanoseconds == b->mtime_nanoseconds && a->ctime_seconds == b->ctime_seconds && + a->ctime_nanoseconds == b->ctime_nanoseconds; +} + +static void runtime_fingerprint_cache_enter(void) { + while ( + atomic_flag_test_and_set_explicit(&runtime_fingerprint_cache_lock, memory_order_acquire)) {} +} + +static void runtime_fingerprint_cache_leave(void) { + atomic_flag_clear_explicit(&runtime_fingerprint_cache_lock, memory_order_release); +} + +static bool runtime_fingerprint_cache_lookup(const runtime_fingerprint_key_t *key, + char out[CBM_DAEMON_BUILD_FINGERPRINT_SIZE]) { + if (!key->valid) { + return false; + } + runtime_fingerprint_cache_enter(); + bool hit = runtime_fingerprint_key_equal(&runtime_fingerprint_cache_key, key); + if (hit) { + memcpy(out, runtime_fingerprint_cache_digest, CBM_DAEMON_BUILD_FINGERPRINT_SIZE); + } + runtime_fingerprint_cache_leave(); + return hit; +} + +static void runtime_fingerprint_cache_store( + const runtime_fingerprint_key_t *key, + const char fingerprint[CBM_DAEMON_BUILD_FINGERPRINT_SIZE]) { + if (!key->valid || fingerprint[0] == '\0') { + return; + } + runtime_fingerprint_cache_enter(); + runtime_fingerprint_cache_key = *key; + memcpy(runtime_fingerprint_cache_digest, fingerprint, CBM_DAEMON_BUILD_FINGERPRINT_SIZE); + runtime_fingerprint_cache_leave(); +} + +/* The single point where the expensive image hash is paid; a test seam counts + * calls and can substitute a stub digest so the cache's hit/miss behaviour is + * observable without a real full-image read. */ +static bool runtime_fingerprint_hash_native_file(uintptr_t native_file, + char out[CBM_DAEMON_BUILD_FINGERPRINT_SIZE]) { +#ifdef CBM_ENABLE_TEST_SEAMS + atomic_fetch_add_explicit(&runtime_fingerprint_hash_call_count, 1, memory_order_relaxed); + if (atomic_load_explicit(&runtime_fingerprint_hash_stub_active, memory_order_acquire)) { + memcpy(out, runtime_fingerprint_hash_stub_digest, CBM_DAEMON_BUILD_FINGERPRINT_SIZE); + return true; + } +#endif + return cbm_daemon_build_fingerprint_native_file(native_file, out); +} + +/* Resolve the digest for the image identity `key` of process `process_id`: + * from the cache when that process is this one and the cache already holds + * that identity, otherwise by hashing the held native image. `cached` reports + * which. Every acquire site resolves through here, so this is where the cache + * is limited to the own image: a peer's key is marked invalid, which is never + * looked up and never committed. A freshly hashed digest is deliberately not + * stored here: the acquire site commits it (runtime_fingerprint_cache_commit) + * only once its post-hash identity check has proven the hashed bytes belong to + * the identity they were keyed on. */ +static bool runtime_build_fingerprint_resolve(uintptr_t native_file, uint64_t process_id, + runtime_fingerprint_key_t *key, + char fingerprint[CBM_DAEMON_BUILD_FINGERPRINT_SIZE], + bool *cached) { + if (process_id != runtime_current_process_id()) { + key->valid = false; + } + *cached = runtime_fingerprint_cache_lookup(key, fingerprint); + return *cached || runtime_fingerprint_hash_native_file(native_file, fingerprint); +} + +/* Commit a freshly hashed digest after the acquire chain has passed. The + * identity observed after the hash must equal the one the digest was resolved + * under; otherwise the bytes read belong to no single identity and are not + * cached. A cache hit has nothing to commit, and neither has a peer, whose + * resolved key is invalid. */ +static void runtime_fingerprint_cache_commit( + const runtime_fingerprint_key_t *before, const runtime_fingerprint_key_t *after, bool cached, + const char fingerprint[CBM_DAEMON_BUILD_FINGERPRINT_SIZE]) { + if (!cached && runtime_fingerprint_key_equal(before, after)) { + runtime_fingerprint_cache_store(after, fingerprint); + } +} + +#ifdef _WIN32 +static int64_t runtime_fingerprint_filetime(FILETIME value) { + return (int64_t)(((uint64_t)value.dwHighDateTime << 32) | value.dwLowDateTime); +} + +/* Key the held image by the snapshot identity the acquire path compares plus + * the kernel-maintained change time. An unreadable change time yields an + * invalid key, which never hits and is never stored: the image is simply + * hashed. */ +static void runtime_fingerprint_key_windows(HANDLE file, + const BY_HANDLE_FILE_INFORMATION *information, + const LARGE_INTEGER *size, + runtime_fingerprint_key_t *key) { + memset(key, 0, sizeof(*key)); + FILE_BASIC_INFO basic; + memset(&basic, 0, sizeof(basic)); + key->valid = information != NULL && size != NULL && size->QuadPart >= 0 && + file != INVALID_HANDLE_VALUE && + GetFileInformationByHandleEx(file, FileBasicInfo, &basic, sizeof(basic)) != 0; + if (key->valid) { + key->device = information->dwVolumeSerialNumber; + key->inode = ((uint64_t)information->nFileIndexHigh << 32) | information->nFileIndexLow; + key->size = (uint64_t)size->QuadPart; + key->mtime_seconds = runtime_fingerprint_filetime(information->ftLastWriteTime); + key->ctime_seconds = basic.ChangeTime.QuadPart; + } +} + +static bool runtime_build_fingerprint_resolve_windows( + HANDLE file, uint64_t process_id, const BY_HANDLE_FILE_INFORMATION *information, + const LARGE_INTEGER *size, char fingerprint[CBM_DAEMON_BUILD_FINGERPRINT_SIZE], + runtime_fingerprint_key_t *key, bool *cached) { + runtime_fingerprint_key_windows(file, information, size, key); + return runtime_build_fingerprint_resolve((uintptr_t)file, process_id, key, fingerprint, cached); +} + +static void runtime_fingerprint_cache_commit_windows( + HANDLE file, const BY_HANDLE_FILE_INFORMATION *information, const LARGE_INTEGER *size, + const runtime_fingerprint_key_t *key, bool cached, + const char fingerprint[CBM_DAEMON_BUILD_FINGERPRINT_SIZE]) { + runtime_fingerprint_key_t key_after; + runtime_fingerprint_key_windows(file, information, size, &key_after); + runtime_fingerprint_cache_commit(key, &key_after, cached, fingerprint); +} +#else +static void runtime_fingerprint_key_posix(const struct stat *status, + runtime_fingerprint_key_t *key) { + memset(key, 0, sizeof(*key)); + key->valid = status != NULL && S_ISREG(status->st_mode); + if (key->valid) { + key->device = (uint64_t)status->st_dev; + key->inode = (uint64_t)status->st_ino; + key->size = (uint64_t)status->st_size; +#ifdef __APPLE__ + key->mtime_seconds = status->st_mtimespec.tv_sec; + key->mtime_nanoseconds = status->st_mtimespec.tv_nsec; + key->ctime_seconds = status->st_ctimespec.tv_sec; + key->ctime_nanoseconds = status->st_ctimespec.tv_nsec; +#else + key->mtime_seconds = status->st_mtim.tv_sec; + key->mtime_nanoseconds = status->st_mtim.tv_nsec; + key->ctime_seconds = status->st_ctim.tv_sec; + key->ctime_nanoseconds = status->st_ctim.tv_nsec; +#endif + } +} + +static bool runtime_build_fingerprint_resolve_posix( + uintptr_t native_file, uint64_t process_id, const struct stat *status, + char fingerprint[CBM_DAEMON_BUILD_FINGERPRINT_SIZE], runtime_fingerprint_key_t *key, + bool *cached) { + runtime_fingerprint_key_posix(status, key); + return runtime_build_fingerprint_resolve(native_file, process_id, key, fingerprint, cached); +} + +static void runtime_fingerprint_cache_commit_posix( + const struct stat *status, const runtime_fingerprint_key_t *key, bool cached, + const char fingerprint[CBM_DAEMON_BUILD_FINGERPRINT_SIZE]) { + runtime_fingerprint_key_t key_after; + runtime_fingerprint_key_posix(status, &key_after); + runtime_fingerprint_cache_commit(key, &key_after, cached, fingerprint); +} +#endif + +#ifdef CBM_ENABLE_TEST_SEAMS +void cbm_daemon_runtime_fingerprint_cache_reset_for_testing(void) { + runtime_fingerprint_cache_enter(); + memset(&runtime_fingerprint_cache_key, 0, sizeof(runtime_fingerprint_cache_key)); + memset(runtime_fingerprint_cache_digest, 0, sizeof(runtime_fingerprint_cache_digest)); + runtime_fingerprint_cache_leave(); + atomic_store_explicit(&runtime_fingerprint_hash_call_count, 0, memory_order_relaxed); + atomic_store_explicit(&runtime_fingerprint_hash_stub_active, false, memory_order_release); + memset(runtime_fingerprint_hash_stub_digest, 0, sizeof(runtime_fingerprint_hash_stub_digest)); +} + +void cbm_daemon_runtime_fingerprint_cache_set_hash_stub_for_testing(const char *digest) { + if (!digest) { + atomic_store_explicit(&runtime_fingerprint_hash_stub_active, false, memory_order_release); + return; + } + memset(runtime_fingerprint_hash_stub_digest, 0, sizeof(runtime_fingerprint_hash_stub_digest)); + size_t length = strlen(digest); + if (length >= CBM_DAEMON_BUILD_FINGERPRINT_SIZE) { + length = CBM_DAEMON_BUILD_FINGERPRINT_SIZE - 1; + } + memcpy(runtime_fingerprint_hash_stub_digest, digest, length); + atomic_store_explicit(&runtime_fingerprint_hash_stub_active, true, memory_order_release); +} + +int cbm_daemon_runtime_fingerprint_hash_call_count_for_testing(void) { + return atomic_load_explicit(&runtime_fingerprint_hash_call_count, memory_order_relaxed); +} + +static runtime_fingerprint_key_t runtime_fingerprint_key_for_testing( + const cbm_daemon_runtime_fingerprint_identity_for_testing_t *identity) { + runtime_fingerprint_key_t key = {.valid = true, + .device = identity->device, + .inode = identity->inode, + .size = identity->size, + .mtime_seconds = identity->mtime_seconds, + .mtime_nanoseconds = identity->mtime_nanoseconds, + .ctime_seconds = identity->ctime_seconds, + .ctime_nanoseconds = identity->ctime_nanoseconds}; + return key; +} + +bool cbm_daemon_runtime_fingerprint_cache_resolve_for_testing( + const cbm_daemon_runtime_fingerprint_identity_for_testing_t *before, + const cbm_daemon_runtime_fingerprint_identity_for_testing_t *after, + char out[CBM_DAEMON_BUILD_FINGERPRINT_SIZE]) { + /* There is no native image behind a synthetic identity: only the stubbed + * hash may ever be reached from here. The identity stands for this + * process's own image, the only one the cache serves. */ + if (!before || !after || !out || + !atomic_load_explicit(&runtime_fingerprint_hash_stub_active, memory_order_acquire)) { + return false; + } + runtime_fingerprint_key_t key_before = runtime_fingerprint_key_for_testing(before); + runtime_fingerprint_key_t key_after = runtime_fingerprint_key_for_testing(after); + bool cached = false; + if (!runtime_build_fingerprint_resolve((uintptr_t)0, runtime_current_process_id(), &key_before, + out, &cached)) { + return false; + } + runtime_fingerprint_cache_commit(&key_before, &key_after, cached, out); + return true; +} +#endif + +#endif /* supported platform: executable-image fingerprint cache */ + /* Acquire one process instance's mapped image as a native object. Supplying a * fingerprint hashes that same held object; NULL performs metadata-only * acquisition for the HELLO fast path. Every platform brackets the process @@ -826,6 +1122,8 @@ static bool runtime_process_image_reference_acquire( BY_HANDLE_FILE_INFORMATION file_after; LARGE_INTEGER size_before; LARGE_INTEGER size_after; + runtime_fingerprint_key_t fingerprint_key = {0}; + bool fingerprint_cached = false; bool ok = runtime_windows_process_image_snapshot(process, &process_before); /* QueryFullProcessImageNameW returns a stable identity spelling for the * before/after comparison, but its Win32/DOS form may exceed MAX_PATH. @@ -839,7 +1137,9 @@ static bool runtime_process_image_reference_acquire( : INVALID_HANDLE_VALUE; free(open_path); ok = ok && runtime_windows_file_snapshot(file, &file_before, &size_before) && - (!fingerprint || cbm_daemon_build_fingerprint_native_file((uintptr_t)file, fingerprint)) && + (!fingerprint || runtime_build_fingerprint_resolve_windows( + file, process_id, &file_before, &size_before, fingerprint, + &fingerprint_key, &fingerprint_cached)) && runtime_windows_file_snapshot(file, &file_after, &size_after) && runtime_windows_file_snapshot_same(&file_before, &size_before, &file_after, &size_after) && runtime_windows_process_image_snapshot(process, &process_after) && @@ -848,6 +1148,10 @@ static bool runtime_process_image_reference_acquire( ok = false; } if (ok) { + if (fingerprint) { + runtime_fingerprint_cache_commit_windows( + file, &file_after, &size_after, &fingerprint_key, fingerprint_cached, fingerprint); + } reference->held = true; reference->file = file; reference->information = file_after; @@ -873,14 +1177,22 @@ static bool runtime_process_image_reference_acquire( int fd = ok ? open(path, O_RDONLY | O_CLOEXEC | O_NOFOLLOW | O_NONBLOCK) : -1; struct stat file_before; struct stat file_after; + runtime_fingerprint_key_t fingerprint_key = {0}; + bool fingerprint_cached = false; ok = ok && fd >= 0 && fstat(fd, &file_before) == 0 && S_ISREG(file_before.st_mode) && runtime_mac_process_maps_file_executable(pid, &file_before) && - (!fingerprint || cbm_daemon_build_fingerprint_native_file((uintptr_t)fd, fingerprint)) && + (!fingerprint || runtime_build_fingerprint_resolve_posix( + (uintptr_t)fd, process_id, &file_before, fingerprint, + &fingerprint_key, &fingerprint_cached)) && fstat(fd, &file_after) == 0 && runtime_mac_stat_same(&file_before, &file_after) && runtime_mac_process_maps_file_executable(pid, &file_after) && runtime_mac_process_instance(pid, &process_after) && runtime_mac_process_instance_same(&process_before, &process_after); if (ok) { + if (fingerprint) { + runtime_fingerprint_cache_commit_posix(&file_after, &fingerprint_key, + fingerprint_cached, fingerprint); + } reference->held = true; reference->fd = fd; reference->status = file_after; @@ -898,10 +1210,13 @@ static bool runtime_process_image_reference_acquire( int image_fd = process_fd >= 0 ? openat(process_fd, "exe", O_RDONLY | O_CLOEXEC) : -1; struct stat image_before; struct stat image_after; + runtime_fingerprint_key_t fingerprint_key = {0}; + bool fingerprint_cached = false; bool ok = image_fd >= 0 && fstat(image_fd, &image_before) == 0 && S_ISREG(image_before.st_mode) && - (!fingerprint || - cbm_daemon_build_fingerprint_native_file((uintptr_t)image_fd, fingerprint)) && + (!fingerprint || runtime_build_fingerprint_resolve_posix( + (uintptr_t)image_fd, process_id, &image_before, fingerprint, + &fingerprint_key, &fingerprint_cached)) && fstat(image_fd, &image_after) == 0 && runtime_posix_stat_same_image(&image_before, &image_after); int verify_fd = ok ? openat(process_fd, "exe", O_RDONLY | O_CLOEXEC) : -1; @@ -915,6 +1230,10 @@ static bool runtime_process_image_reference_acquire( ok = false; } if (ok) { + if (fingerprint) { + runtime_fingerprint_cache_commit_posix(&image_after, &fingerprint_key, + fingerprint_cached, fingerprint); + } reference->held = true; reference->fd = image_fd; reference->status = image_after; @@ -940,12 +1259,19 @@ static bool runtime_process_image_reference_acquire( int image_fd = ok ? open(path, O_RDONLY | O_CLOEXEC | O_NOFOLLOW | O_NONBLOCK) : -1; struct stat image_before; struct stat image_after; + runtime_fingerprint_key_t fingerprint_key = {0}; + bool fingerprint_cached = false; ok = image_fd >= 0 && fstat(image_fd, &image_before) == 0 && S_ISREG(image_before.st_mode) && - (!fingerprint || - cbm_daemon_build_fingerprint_native_file((uintptr_t)image_fd, fingerprint)) && + (!fingerprint || runtime_build_fingerprint_resolve_posix( + (uintptr_t)image_fd, process_id, &image_before, fingerprint, + &fingerprint_key, &fingerprint_cached)) && fstat(image_fd, &image_after) == 0 && runtime_posix_stat_same_image(&image_before, &image_after); if (ok) { + if (fingerprint) { + runtime_fingerprint_cache_commit_posix(&image_after, &fingerprint_key, + fingerprint_cached, fingerprint); + } reference->held = true; reference->fd = image_fd; reference->status = image_after; diff --git a/src/daemon/runtime.h b/src/daemon/runtime.h index 76d168c73..361a7e781 100644 --- a/src/daemon/runtime.h +++ b/src/daemon/runtime.h @@ -499,6 +499,31 @@ typedef enum { } cbm_daemon_runtime_hello_point_t; typedef void (*cbm_daemon_runtime_hello_hook_t)(cbm_daemon_runtime_hello_point_t point); void cbm_daemon_runtime_set_hello_hook_for_testing(cbm_daemon_runtime_hello_hook_t hook); + +/* Executable-image fingerprint-cache seams. reset empties the cache, zeroes + * the hash counter and disarms the stub; a test that arms the stub must reset + * before its first assertion, because an armed stub replaces every later + * fingerprint in the process. resolve drives a synthetic file identity through + * the exact resolve-then-commit path the acquire sites use: `before` is the + * identity the digest is keyed on, `after` the identity observed after the + * hash, and the digest is committed only when the two agree. It refuses unless + * the stub is armed, since no native image backs a synthetic identity. */ +typedef struct { + uint64_t device; + uint64_t inode; + uint64_t size; + int64_t mtime_seconds; + int64_t mtime_nanoseconds; + int64_t ctime_seconds; + int64_t ctime_nanoseconds; +} cbm_daemon_runtime_fingerprint_identity_for_testing_t; +void cbm_daemon_runtime_fingerprint_cache_reset_for_testing(void); +void cbm_daemon_runtime_fingerprint_cache_set_hash_stub_for_testing(const char *digest); +int cbm_daemon_runtime_fingerprint_hash_call_count_for_testing(void); +bool cbm_daemon_runtime_fingerprint_cache_resolve_for_testing( + const cbm_daemon_runtime_fingerprint_identity_for_testing_t *before, + const cbm_daemon_runtime_fingerprint_identity_for_testing_t *after, + char out[CBM_DAEMON_BUILD_FINGERPRINT_SIZE]); #endif #endif /* CBM_DAEMON_RUNTIME_H */ diff --git a/src/pipeline/pass_definitions.c b/src/pipeline/pass_definitions.c index bb3954008..44678b76c 100644 --- a/src/pipeline/pass_definitions.c +++ b/src/pipeline/pass_definitions.c @@ -521,13 +521,16 @@ static void process_def(cbm_pipeline_ctx_t *ctx, const CBMDefinition *def, const * field resolution), Variable/Field (READS/WRITES resolution), and Table/View * (SQL FROM/JOIN lineage). pass_parallel.c and pipeline_incremental.c seed * through the same predicate, so the three registries cannot diverge. - * A generated member is registered by qualified name only, at all three. */ + * A generated member is registered by qualified name only, at all three. + * The registry's test verdict is the extractor's OR the test-double path + * rule (cbm_registry_test_double_path) on the node's file, at all three. */ if (node_id > 0 && cbm_label_is_registry_symbol(def->label)) { if (cbm_def_props_generated(def->extra_props)) { cbm_registry_add_qn_only(ctx->registry, def->qualified_name, def->label); } else { cbm_registry_add_lang_in_module(ctx->registry, def->name, def->qualified_name, - def->label, lang, module_prefix_len); + def->label, lang, module_prefix_len, + def->is_test || cbm_registry_test_double_path(def_rel)); } if (def->qn_sig_off) { cbm_registry_set_swift_signature(ctx->registry, def->qualified_name, diff --git a/src/pipeline/pass_parallel.c b/src/pipeline/pass_parallel.c index 670347352..4ae2ea88f 100644 --- a/src/pipeline/pass_parallel.c +++ b/src/pipeline/pass_parallel.c @@ -1816,7 +1816,7 @@ int cbm_parallel_extract(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files, /* `file_node_id` is the defining file's node id (0 when it has none), looked up * once per file by the caller: computing the file QN and finding its node for * every definition was 700 k allocations and lookups on the Go corpus. */ -static int register_and_link_def(cbm_pipeline_ctx_t *ctx, const CBMDefinition *def, +static int register_and_link_def(cbm_pipeline_ctx_t *ctx, const CBMDefinition *def, const char *rel, int64_t file_node_id, CBMLanguage lang, size_t module_prefix_len, int *reg_entries) { int edges = 0; @@ -1824,14 +1824,17 @@ static int register_and_link_def(cbm_pipeline_ctx_t *ctx, const CBMDefinition *d return 0; } /* Registry membership is defined ONCE by cbm_label_is_registry_symbol - * (helpers.c) — see pass_definitions.c for the per-label rationale, and - * for the generated member registered by qualified name only. */ + * (helpers.c) — see pass_definitions.c for the per-label rationale, for + * the generated member registered by qualified name only, and for the + * test-double path rule, judged on the path the def's node stores. */ if (cbm_label_is_registry_symbol(def->label)) { if (cbm_def_props_generated(def->extra_props)) { cbm_registry_add_qn_only(ctx->registry, def->qualified_name, def->label); } else { + const char *def_rel = def->file_path ? def->file_path : rel; cbm_registry_add_lang_in_module(ctx->registry, def->name, def->qualified_name, - def->label, lang, module_prefix_len); + def->label, lang, module_prefix_len, + def->is_test || cbm_registry_test_double_path(def_rel)); } if (def->qn_sig_off) { cbm_registry_set_swift_signature(ctx->registry, def->qualified_name, @@ -2050,7 +2053,7 @@ int cbm_build_registry_from_cache(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t : 0; for (int d = 0; d < result->defs.count; d++) { defines_edges += - register_and_link_def(ctx, &result->defs.items[d], file_node_id, + register_and_link_def(ctx, &result->defs.items[d], rel, file_node_id, files[i].language, py_prefix_len, ®_entries); } } diff --git a/src/pipeline/pass_tests.c b/src/pipeline/pass_tests.c index a763016fb..9972086ee 100644 --- a/src/pipeline/pass_tests.c +++ b/src/pipeline/pass_tests.c @@ -45,7 +45,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; } @@ -270,13 +270,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 f877cdecc..83907ae4b 100644 --- a/src/pipeline/pipeline.h +++ b/src/pipeline/pipeline.h @@ -283,9 +283,24 @@ cbm_registry_t *cbm_registry_new(void); void cbm_registry_free(cbm_registry_t *r); /* Register a function/method/class. All strings are copied. The entry's - * language is unknown, so name-based resolution never filters it out. */ + * language is unknown, so name-based resolution never filters it out. + * `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); + +/* The test-double path rule, for the registry's ranking only: true when the + * repo-relative path of a symbol's defining file has a directory segment named + * exactly `fake`, `mock`, `mocks`, `testing` or `testutil`, or its file name + * matches `mock_*.go` or `fake_*.go`. Go projects keep generated fake + * clientsets and mock packages in ordinary files the extractor rightly does + * not call tests; a call should still prefer real code over them. The three + * registry seeders (pass_definitions.c, pass_parallel.c, + * pipeline_incremental.c) pass the extractor's verdict OR this, so full and + * incremental indexes rank alike. It never decides what a node is: the stored + * is_test property, TESTS edges and importance stay with cbm_is_test_path. */ +bool cbm_registry_test_double_path(const char *rel_path); /* Swift-only metadata and label-compatible overload selection. The returned * QNs are borrowed from the registry and all belong to one base callable. @@ -306,7 +321,7 @@ int cbm_registry_swift_candidates_in_file(const cbm_registry_t *r, const CBMCall * function-local symbols: that needs the defining file's module QN, see * cbm_registry_add_lang_in_module. */ void cbm_registry_add_lang(cbm_registry_t *r, const char *name, const char *qualified_name, - const char *label, CBMLanguage lang); + const char *label, CBMLanguage lang, bool is_test); /* As cbm_registry_add_lang. A Python symbol declared inside a function body of * its own file (cbm_registry_qn_is_function_local, bounded by @@ -318,7 +333,7 @@ void cbm_registry_add_lang(cbm_registry_t *r, const char *name, const char *qual * indexes agree. 0 = no local detection. */ void cbm_registry_add_lang_in_module(cbm_registry_t *r, const char *name, const char *qualified_name, const char *label, - CBMLanguage lang, size_t module_prefix_len); + CBMLanguage lang, size_t module_prefix_len, bool is_test); /* Register a symbol for exact (qualified-name) lookups only: it is never * offered to the short-name strategies (unique_name, suffix_match, diff --git a/src/pipeline/pipeline_delta.c b/src/pipeline/pipeline_delta.c index 63c5ef1d1..22573f8c8 100644 --- a/src/pipeline/pipeline_delta.c +++ b/src/pipeline/pipeline_delta.c @@ -623,6 +623,28 @@ static char *delta_generated_props(const char *props, const char *generated) { return out; } +/* `props` (a JSON object) with the extractor's test verdict added as + * `"is_test":true` (CBM_MEM_CLASS_OTHER; the caller frees), NULL on allocation + * failure. The incremental registry seed reads the verdict back + * (cbm_node_is_test) so a re-resolved caller ranks a test definition below + * real code exactly as the full build does; an absent key reads as false. */ +static char *delta_test_props(const char *props) { + static const char member[] = "\"is_test\":true"; + size_t pl = props ? strlen(props) : 0; + bool has_members = pl > PAIR_LEN && props[0] == '{' && props[pl - SKIP_ONE] == '}'; + size_t cap = pl + sizeof(member) + CBM_SZ_4; + char *out = (char *)cbm_alloc(CBM_MEM_CLASS_OTHER, cap); + if (!out) { + return NULL; + } + if (has_members) { + snprintf(out, cap, "%.*s,%s}", (int)(pl - SKIP_ONE), props, member); + } else { + snprintf(out, cap, "{%s}", member); + } + return out; +} + int64_t cbm_delta_preseed(cbm_store_t *store, const char *project, cbm_gbuf_t *gbuf) { sqlite3 *db = cbm_store_get_db(store); if (!db) { @@ -673,7 +695,13 @@ int64_t cbm_delta_preseed(cbm_store_t *store, const char *project, cbm_gbuf_t *g * And a generated member's mark (a Java record's implicit accessor, a * Method node): the registry seed reads it to register the member by * qualified name only, as the full build does (cbm_node_props_generated); - * without it the proxy came back into the short-name index. */ + * without it the proxy came back into the short-name index. + * + * And the extractor's test verdict: the registry seed reads it back + * (cbm_node_is_test) to rank a test definition below real code, as the + * full build does with CBMDefinition.is_test; without it every proxy + * scored as production and a re-resolved caller could bind a test helper + * the full build ranks last. */ sqlite3_stmt *stmt = NULL; if (sqlite3_prepare_v2( db, @@ -702,7 +730,8 @@ int64_t cbm_delta_preseed(cbm_store_t *store, const char *project, cbm_gbuf_t *g " THEN json_extract(properties, '$.parent_class') END," " CASE WHEN label = 'Method'" " AND instr(properties, '\"" CBM_PROP_GENERATED "\"') > 0" - " THEN json_extract(properties, '$." CBM_PROP_GENERATED "') END" + " THEN json_extract(properties, '$." CBM_PROP_GENERATED "') END," + " instr(properties, '\"is_test\":true') > 0" " FROM nodes" " WHERE project = ?1 AND label NOT IN" " ('Macro','Comment','Section','Branch','Commit','Tag')" @@ -727,14 +756,27 @@ int64_t cbm_delta_preseed(cbm_store_t *store, const char *project, cbm_gbuf_t *g int end_line = sqlite3_column_int(stmt, 7); const char *parent = (const char *)sqlite3_column_text(stmt, 8); const char *generated = (const char *)sqlite3_column_text(stmt, 9); + bool is_test = sqlite3_column_int(stmt, 10) != 0; char *owner_props = parent ? delta_owner_props(project, name, qn, parent, client_props) : NULL; const char *props = owner_props ? owner_props : (client_props ? client_props : "{}"); char *generated_props = delta_generated_props(props, generated); + const char *seed_props = generated_props ? generated_props : props; + char *test_props = is_test ? delta_test_props(seed_props) : NULL; + if (is_test && !test_props) { + /* A proxy without its verdict would rank as production code; + * the run cannot be trusted, as with an id mismatch below. */ + cbm_free(CBM_MEM_CLASS_OTHER, generated_props); + cbm_free(CBM_MEM_CLASS_OTHER, owner_props); + sqlite3_finalize(stmt); + cbm_log_error("delta.preseed_verdict_alloc_failed", "qn", qn ? qn : ""); + return -1; + } /* Pin the gbuf id to the database id: proxies ARE their rows. */ cbm_gbuf_set_next_id(gbuf, id); int64_t got = cbm_gbuf_upsert_node(gbuf, label, name, qn, fp ? fp : "", start_line, - end_line, generated_props ? generated_props : props); + end_line, test_props ? test_props : seed_props); + cbm_free(CBM_MEM_CLASS_OTHER, test_props); cbm_free(CBM_MEM_CLASS_OTHER, generated_props); cbm_free(CBM_MEM_CLASS_OTHER, owner_props); if (got != id) { diff --git a/src/pipeline/pipeline_incremental.c b/src/pipeline/pipeline_incremental.c index 58ac46d1a..9324314cd 100644 --- a/src/pipeline/pipeline_incremental.c +++ b/src/pipeline/pipeline_incremental.c @@ -1396,8 +1396,11 @@ static void registry_visitor(const cbm_gbuf_node_t *node, void *userdata) { size_t prefix_len = lang == CBM_LANG_PYTHON ? cbm_pipeline_fqn_symbol_prefix_len(seed->project, node->file_path) : 0; + /* The full index's verdict: the extractor's (stored as is_test) OR the + * test-double path rule on the node's file (pass_definitions.c). */ + bool is_test = cbm_node_is_test(node) || cbm_registry_test_double_path(node->file_path); cbm_registry_add_lang_in_module(seed->registry, node->name, node->qualified_name, node->label, - lang, prefix_len); + lang, prefix_len, is_test); if (node->properties_json) { const char *mask = strstr(node->properties_json, "\"param_default_mask\":\""); const char *count = strstr(node->properties_json, "\"selector_param_count\":"); diff --git a/src/pipeline/pipeline_internal.h b/src/pipeline/pipeline_internal.h index 930a2fe7a..cb8af8f02 100644 --- a/src/pipeline/pipeline_internal.h +++ b/src/pipeline/pipeline_internal.h @@ -260,7 +260,8 @@ cbm_resolution_t cbm_registry_resolve_lang(const cbm_registry_t *r, const char * CBMLanguage lang); /* The CALLS variant: as cbm_registry_resolve_lang, except that a bare C call * (no `.` / `->`) does not consider Field or Method nodes, which it can never - * call. */ + * call, and that in a C# file a built-in type keyword root (`int.TryParse`) + * names a type for the receiver chain check (#2121). */ cbm_resolution_t cbm_registry_resolve_call_lang(const cbm_registry_t *r, const char *callee_name, const char *module_qn, const char **import_map_keys, const char **import_map_vals, int import_map_count, @@ -656,6 +657,12 @@ static inline bool cbm_pipeline_test_force_def_modules_null(const cbm_pipeline_c 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 93b4a16c1..ac1b8ce61 100644 --- a/src/pipeline/registry.c +++ b/src/pipeline/registry.c @@ -30,6 +30,7 @@ enum { REG_MAX_CANDIDATES = 256 }; #include "foundation/compat.h" /* CBM_TLS */ #include "foundation/hash_table.h" #include "foundation/arena.h" +#include "foundation/log.h" #include "foundation/dyn_array.h" #include "foundation/mem_core.h" #include "foundation/platform.h" @@ -81,16 +82,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; /* Language of the file that defined items[i] (a CBMLanguage; CBM_LANG_COUNT * = unknown, compatible with every caller), one byte per entry. Grown in @@ -204,84 +205,64 @@ 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; +/* A directory segment that, by name alone, holds test doubles: Go's generated + * fake clientsets and listers (pkg/client/.../fake), mock packages, and the + * testing / testutil helper packages. Whole segments only, compared exactly. */ +static bool test_double_dir_segment(const char *seg, size_t len) { + static const char *const dirs[] = {"fake", "mock", "mocks", "testing", "testutil"}; + for (size_t i = 0; i < sizeof(dirs) / sizeof(dirs[0]); i++) { + if (strlen(dirs[i]) == len && memcmp(seg, dirs[i], len) == 0) { + return true; } } 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. */ -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; +/* `base` is a Go file named `prefix`*.go. */ +static bool go_file_with_prefix(const char *base, const char *prefix) { + static const char go_ext[] = ".go"; + size_t blen = strlen(base); + size_t plen = strlen(prefix); + size_t elen = sizeof(go_ext) - SKIP_ONE; + return blen >= plen + elen && strncmp(base, prefix, plen) == 0 && + strcmp(base + blen - elen, go_ext) == 0; } -/* The cached candidate languages, under the same coverage rule: both side - * arrays grow together and share is_test_cap. */ +bool cbm_registry_test_double_path(const char *rel_path) { + if (!rel_path) { + return false; + } + const char *seg = rel_path; + for (const char *slash = strchr(seg, '/'); slash; slash = strchr(seg, '/')) { + if (test_double_dir_segment(seg, (size_t)(slash - seg))) { + return true; + } + seg = slash + SKIP_ONE; + } + /* `seg` is the file name now. */ + return go_file_with_prefix(seg, "mock_") || go_file_with_prefix(seg, "fake_"); +} + +/* 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; +} + +/* The stored candidate languages. Both side arrays grow together and share + * is_test_cap, and index_under_name drops an entry that does not fit both, so + * this covers every entry of a non-empty bucket too. */ static const uint8_t *qn_lang_flags(const qn_array_t *arr) { - return arr->lang && arr->is_test_cap >= arr->count ? arr->lang : NULL; + return arr->lang; } /* 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); @@ -351,9 +332,10 @@ static bool candidate_outranks_on_tie(const cbm_registry_t *r, const char *candi } /* Pick candidate with highest composite score (test-deprioritization + namespace - * proximity), ties settled by candidate_outranks_on_tie. `is_test_flags` and - * `lang_flags` are the candidates' cached test verdicts and languages, or NULL - * when the caller has none (a filtered subset carries its own copies). */ + * proximity), ties settled by candidate_outranks_on_tie. `is_test_flags` are + * the candidates' stored test verdicts, always given: every by-name entry has + * one, and a filtered subset carries its own copies. `lang_flags` are their + * languages, or NULL when the caller has none. */ static const char *best_by_import_distance(const cbm_registry_t *r, const char **candidates, const uint8_t *is_test_flags, const uint8_t *lang_flags, int count, const char *module_qn) { @@ -361,8 +343,7 @@ static const char *best_by_import_distance(const cbm_registry_t *r, const char * uint8_t best_lang = (uint8_t)CBM_LANG_COUNT; 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); uint8_t lang = lang_flags ? lang_flags[i] : (uint8_t)CBM_LANG_COUNT; if (score > best_score || (score == best_score && best && @@ -485,6 +466,7 @@ typedef struct { cbm_resolution_t res; size_t scope_len; /* raw and Python-scoped requests may share one file cache */ bool skip_members; /* a C call and a C reference to the same name share it too */ + bool cs_keywords; /* a C# call and a C# reference to the same name share it too */ } resolve_cache_entry_t; static CBM_TLS CBMHashTable *_resolve_cache = NULL; @@ -1718,11 +1700,11 @@ void cbm_registry_free(cbm_registry_t *r) { /* ── Registration ────────────────────────────────────────────────── */ /* Record `owned_qn` in the by-name bucket for `key`, keeping the bucket's - * is_test flags in step with its entries. + * is_test and lang side arrays in step with its entries. * * 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, CBMLanguage lang) { qn_array_t *arr = cbm_ht_get(r->by_name, key); if (!arr) { @@ -1732,7 +1714,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; @@ -1743,19 +1725,25 @@ static void index_under_name(cbm_registry_t *r, const char *key, const char *own uint8_t *grown_lang = cbm_realloc(CBM_MEM_CLASS_DYN_ARRAY, arr->lang, (size_t)want * sizeof(uint8_t)); /* Both side arrays share is_test_cap: on failure it stays below - * count, so this name serves neither cached test verdicts nor - * language filtering, exactly the unfiltered answer. */ + * count, and the entry is dropped below. */ if (grown_lang) { arr->lang = grown_lang; 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; - arr->lang[arr->count - SKIP_ONE] = - (uint8_t)(lang >= 0 && lang < CBM_LANG_COUNT ? lang : CBM_LANG_COUNT); + 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. The language + * shares is_test_cap, so the drop covers it as well. */ + arr->count = before; + cbm_log_warn("registry.verdict_alloc_failed", "key", key); + return; } + arr->is_test[arr->count - SKIP_ONE] = is_test ? 1 : 0; + arr->lang[arr->count - SKIP_ONE] = + (uint8_t)(lang >= 0 && lang < CBM_LANG_COUNT ? lang : CBM_LANG_COUNT); } static bool registry_label_is_callable(const char *label) { @@ -1843,13 +1831,13 @@ static const char *registry_add_exact(cbm_registry_t *r, const char *qualified_n } void cbm_registry_add(cbm_registry_t *r, const char *name, const char *qualified_name, - const char *label) { - cbm_registry_add_lang_in_module(r, name, qualified_name, label, CBM_LANG_COUNT, 0); + const char *label, bool is_test) { + cbm_registry_add_lang_in_module(r, name, qualified_name, label, CBM_LANG_COUNT, 0, is_test); } void cbm_registry_add_lang(cbm_registry_t *r, const char *name, const char *qualified_name, - const char *label, CBMLanguage lang) { - cbm_registry_add_lang_in_module(r, name, qualified_name, label, lang, 0); + const char *label, CBMLanguage lang, bool is_test) { + cbm_registry_add_lang_in_module(r, name, qualified_name, label, lang, 0, is_test); } /* Python nests defs: a def or class declared in a function body -- a closure, @@ -1868,7 +1856,7 @@ void cbm_registry_add_lang(cbm_registry_t *r, const char *name, const char *qual * index seconds for nothing. */ void cbm_registry_add_lang_in_module(cbm_registry_t *r, const char *name, const char *qualified_name, const char *label, - CBMLanguage lang, size_t module_prefix_len) { + CBMLanguage lang, size_t module_prefix_len, bool is_test) { if (!r || !qualified_name || !label) { return; } @@ -1912,7 +1900,7 @@ void cbm_registry_add_lang_in_module(cbm_registry_t *r, const char *name, const char *derived = simple_name(qualified_name); const char *primary = name && cbm_qn_callable_base_len_named(owned_qn, name) < strlen(owned_qn) ? name : derived; - index_under_name(r, primary, owned_qn, lang); + index_under_name(r, primary, owned_qn, is_test, lang); /* '#' is a QN fence; no extractor mints one for a REGISTERED definition * today (Rust cfg twins are variants of one plain QN; a C macro's QN ends in * "#macro", but Macro is no registry label, so it never gets here). A @@ -1924,7 +1912,7 @@ void cbm_registry_add_lang_in_module(cbm_registry_t *r, const char *name, * signature-qualified callable is filed under `name` already, and its * capped suffix may carry a '#' of its own. */ if (name && name[0] && strchr(derived, '#') && strcmp(name, primary) != 0) { - index_under_name(r, name, owned_qn, lang); + index_under_name(r, name, owned_qn, is_test, lang); } } @@ -2295,12 +2283,8 @@ static cbm_resolution_t resolve_multi_with_imports(const cbm_registry_t *r, cons 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]; - } - if (langs) { - filtered_lang[fcount] = langs[i]; - } + filtered_test[fcount] = flags[i]; + filtered_lang[fcount] = langs[i]; filtered[fcount] = arr->items[i]; fcount++; } @@ -2310,8 +2294,8 @@ static cbm_resolution_t resolve_multi_with_imports(const cbm_registry_t *r, cons return (cbm_resolution_t){filtered[0], "suffix_match", conf, arr->count}; } if (fcount > SKIP_ONE) { - const char *best = best_by_import_distance(r, filtered, flags ? filtered_test : NULL, - langs ? filtered_lang : NULL, fcount, module_qn); + const char *best = + best_by_import_distance(r, filtered, filtered_test, filtered_lang, fcount, module_qn); if (best) { double conf = candidate_count_penalty(CONF_SUFFIX_MATCH, fcount); return (cbm_resolution_t){best, "suffix_match", conf, fcount}; @@ -2384,6 +2368,26 @@ static const char *qualified_suffix_match(const qn_array_t *arr, const char *cal return match; } +/* True when the first segment of the dotted chain `dotted` is one of C#'s + * built-in type keywords (int, string, bool, ...; nint and nuint since C# 9). + * Closed list: these words are grammar, not a naming heuristic. `dynamic` is + * not on it: the type has no static members, so `dynamic.X` is a variable + * named `dynamic` and stays an ordinary lower-case value root. */ +static bool cs_type_keyword_root(const char *dotted) { + static const char *const keywords[] = { + "bool", "byte", "char", "decimal", "double", "float", "int", "long", "nint", + "nuint", "object", "sbyte", "short", "string", "uint", "ulong", "ushort", "void", + }; + const char *dot = strchr(dotted, '.'); + size_t len = dot ? (size_t)(dot - dotted) : strlen(dotted); + for (size_t i = 0; i < sizeof(keywords) / sizeof(keywords[0]); i++) { + if (strlen(keywords[i]) == len && strncmp(dotted, keywords[i], len) == 0) { + return true; + } + } + return false; +} + /* A dotted callee whose FIRST segment starts upper-case names a type — URLSession, * Calendar, JSONEncoder. That receiver chain is evidence the bare-name scorers * throw away, and throwing it away binds Foundation's URLSession.shared.data to @@ -2397,9 +2401,17 @@ static const char *qualified_suffix_match(const qn_array_t *arr, const char *cal * show, so the chain proves nothing there and the call passes through * unchanged. A callee with no separator passes through as well. * - * Language agnostic by design: the registry holds no language, and every - * language that writes receiver chains gains the same protection. */ -static bool receiver_chain_admits(const char *callee_name, const char *candidate_qn) { + * One exception, for C# CALLS only (#2121): C# spells its built-in types as + * lower-case keywords, so in `int.TryParse` or `string.Join` the root names a + * type and gets the same check (cs_type_keyword_root). `lang` is the calling + * file's language on the CALLS path (cbm_registry_resolve_call_lang) and + * CBM_LANG_COUNT on every other resolve. In Python, JS or Go these words are + * ordinary identifiers, so a variable named `object` stays a value root there. + * + * Otherwise language agnostic by design: every language that writes receiver + * chains gains the same protection. */ +static bool receiver_chain_admits(const char *callee_name, const char *candidate_qn, + CBMLanguage lang) { /* Normalize "::" -> "." so the chain composes with dotted candidate QNs, * the same way qualified_suffix_match does. */ char dotted[CBM_SZ_512]; @@ -2418,7 +2430,8 @@ static bool receiver_chain_admits(const char *callee_name, const char *candidate if (!last_dot) { return true; /* bare name — no receiver chain to judge */ } - if (dotted[0] < 'A' || dotted[0] > 'Z') { + if ((dotted[0] < 'A' || dotted[0] > 'Z') && + !(lang == CBM_LANG_CSHARP && cs_type_keyword_root(dotted))) { return true; /* lower-case root names a value, not a type */ } /* A name written in capitals with underscores is a constant holding a @@ -2477,14 +2490,13 @@ static bool receiver_chain_admits(const char *callee_name, const char *candidate /* The same-name candidates without the members -- Field and Method nodes -- * in `view` (backed by the caller's buffers, at most REG_MAX_CANDIDATES - * entries). The side arrays are carried over only when the bucket has them - * for every entry. */ + * entries). Every entry of a non-empty bucket has its stored verdict and + * language (index_under_name), and the view carries them over. */ static void candidates_without_members(const cbm_registry_t *r, const qn_array_t *arr, qn_array_t *view, char **items, uint8_t *is_test, uint8_t *lang) { const uint8_t *flags = qn_test_flags(arr); const uint8_t *langs = qn_lang_flags(arr); - bool side = flags && langs; int n = 0; for (int i = 0; i < arr->count; i++) { const char *label = cbm_registry_label_of(r, arr->items[i]); @@ -2492,24 +2504,20 @@ static void candidates_without_members(const cbm_registry_t *r, const qn_array_t continue; } items[n] = arr->items[i]; - if (side) { - is_test[n] = flags[i]; - lang[n] = langs[i]; - } + is_test[n] = flags[i]; + lang[n] = langs[i]; n++; } - *view = (qn_array_t){.items = items, - .count = n, - .cap = n, - .is_test = side ? is_test : NULL, - .is_test_cap = side ? n : 0, - .lang = side ? lang : NULL}; + *view = (qn_array_t){ + .items = items, .count = n, .cap = n, .is_test = is_test, .is_test_cap = n, .lang = lang}; } -/* Strategies 3.5, 3 and 4 over one same-name candidate set. */ +/* Strategies 3.5, 3 and 4 over one same-name candidate set. `lang` reaches + * receiver_chain_admits only (see there). */ static cbm_resolution_t resolve_name_candidates(const cbm_registry_t *r, const qn_array_t *arr, const char *callee_name, const char *module_qn, - const char **import_vals, int import_count) { + const char **import_vals, int import_count, + CBMLanguage lang) { /* Strategy 3.5: a qualified callee disambiguates among multiple same-name * candidates by full qualified tail, before bare-name scoring collapses * them onto a single winner. */ @@ -2522,7 +2530,7 @@ static cbm_resolution_t resolve_name_candidates(const cbm_registry_t *r, const q /* Strategy 3: unique name */ if (arr->count == SKIP_ONE) { - if (!receiver_chain_admits(callee_name, arr->items[0])) { + if (!receiver_chain_admits(callee_name, arr->items[0], lang)) { return empty_result(); } double conf = CONF_UNIQUE_NAME; @@ -2540,7 +2548,7 @@ static cbm_resolution_t resolve_name_candidates(const cbm_registry_t *r, const q const char *best = best_by_import_distance(r, (const char **)arr->items, qn_test_flags(arr), qn_lang_flags(arr), arr->count, module_qn); if (best) { - if (!receiver_chain_admits(callee_name, best)) { + if (!receiver_chain_admits(callee_name, best, lang)) { return empty_result(); } double conf = candidate_count_penalty(CONF_SUFFIX_MATCH, arr->count); @@ -2558,7 +2566,7 @@ static cbm_resolution_t resolve_name_candidates(const cbm_registry_t *r, const q * libc `read()` the unique name of a C++ adapter's Method instead. */ static cbm_resolution_t resolve_name_lookup(const cbm_registry_t *r, const char *callee_name, const char *module_qn, const char **import_vals, - int import_count, bool skip_members) { + int import_count, bool skip_members, CBMLanguage lang) { const char *lookup = simple_name(callee_name); const qn_array_t *arr = cbm_ht_get(r->by_name, lookup); if (!arr || arr->count == 0) { @@ -2576,17 +2584,20 @@ static cbm_resolution_t resolve_name_lookup(const cbm_registry_t *r, const char if (view.count == 0) { return empty_result(); } - return resolve_name_candidates(r, &view, callee_name, module_qn, import_vals, import_count); + return resolve_name_candidates(r, &view, callee_name, module_qn, import_vals, import_count, + lang); } - return resolve_name_candidates(r, arr, callee_name, module_qn, import_vals, import_count); + return resolve_name_candidates(r, arr, callee_name, module_qn, import_vals, import_count, lang); } /* The strategy chain shared by both public resolve variants (no caching here — - * cbm_registry_resolve owns the per-file cache). */ + * cbm_registry_resolve owns the per-file cache). `lang`: see + * receiver_chain_admits. */ static cbm_resolution_t registry_resolve_chain(const cbm_registry_t *r, const char *callee_name, const char *module_qn, const char **import_map_keys, const char **import_map_vals, int import_map_count, - size_t scope_len, bool skip_members) { + size_t scope_len, bool skip_members, + CBMLanguage lang) { /* Split callee at the first path separator: "pkg.Func" → prefix="pkg", * suffix="Func". Rust/C++ use "::" rather than ".", so honor whichever * separator appears first ("lib::square" → prefix="lib", suffix="square"). @@ -2624,7 +2635,7 @@ static cbm_resolution_t registry_resolve_chain(const cbm_registry_t *r, const ch if (!(res.qualified_name && res.qualified_name[0])) { /* Strategy 3+4: name lookup */ res = resolve_name_lookup(r, callee_name, module_qn, import_map_vals, import_map_count, - skip_members); + skip_members, lang); } return res; } @@ -2634,7 +2645,7 @@ static cbm_resolution_t registry_resolve_chain(const cbm_registry_t *r, const ch * at most REG_MAX_CANDIDATES long whenever a name strategy produced `qn`. */ static CBMLanguage registry_entry_language(const cbm_registry_t *r, const char *qn) { const qn_array_t *arr = cbm_ht_get(r->by_name, simple_name(qn)); - if (!arr || !arr->lang || arr->is_test_cap < arr->count) { + if (!arr) { return CBM_LANG_COUNT; } for (int i = 0; i < arr->count; i++) { @@ -2672,25 +2683,31 @@ bool cbm_registry_name_guess_vetoed(const cbm_registry_t *r, CBMLanguage caller_ registry_entry_language(r, res->qualified_name)); } +/* call_lang: the calling file's language for a CALLS resolve, CBM_LANG_COUNT + * for every other one (see receiver_chain_admits). */ static cbm_resolution_t registry_resolve_scoped(const cbm_registry_t *r, const char *callee_name, const char *module_qn, const char **import_map_keys, const char **import_map_vals, int import_map_count, - size_t scope_len, bool skip_members) { + size_t scope_len, bool skip_members, + CBMLanguage call_lang) { if (!r || !callee_name) { return empty_result(); } /* Per-file cache: same callee_name in N call sites → 1 chain walk * + N-1 O(1) hash hits. module_qn is constant per file so the - * cache key only needs callee_name. */ + * cache key only needs callee_name. The chain reads call_lang only as + * "is it C#", so that one bit is all the entry has to record. */ + bool cs_keywords = call_lang == CBM_LANG_CSHARP; resolve_cache_entry_t *cached = _resolve_cache ? (resolve_cache_entry_t *)cbm_ht_get(_resolve_cache, callee_name) : NULL; - if (cached && cached->scope_len == scope_len && cached->skip_members == skip_members) + if (cached && cached->scope_len == scope_len && cached->skip_members == skip_members && + cached->cs_keywords == cs_keywords) return cached->res; cbm_resolution_t res = registry_resolve_chain(r, callee_name, module_qn, import_map_keys, import_map_vals, - import_map_count, scope_len, skip_members); + import_map_count, scope_len, skip_members, call_lang); /* Data relations (Table/View) are lineage-only registry members: common * table names (users, orders, config) collide with code identifiers across @@ -2711,12 +2728,14 @@ static cbm_resolution_t registry_resolve_scoped(const cbm_registry_t *r, const c cached->res = res; cached->scope_len = scope_len; cached->skip_members = skip_members; + cached->cs_keywords = cs_keywords; } else if (_resolve_cache) { resolve_cache_entry_t *e = (resolve_cache_entry_t *)malloc(sizeof(*e)); if (e) { e->res = res; e->scope_len = scope_len; e->skip_members = skip_members; + e->cs_keywords = cs_keywords; char *kdup = strdup(callee_name); if (kdup) { cbm_ht_set(_resolve_cache, kdup, e); @@ -3028,22 +3047,28 @@ static bool swift_signature_matches(const swift_signature_t *meta, const CBMCall } /* Keep receiver choice ahead of general import/name choice. Without a module, - * this path still uses the deterministic distance tie-break, as before. */ -static const char *swift_choose_receiver(const cbm_registry_t *r, const char **matches, int matched, - const char *name, const CBMCall *call, - const char *module_qn, const char **import_vals, - int import_count, const char **scratch) { + * this path still uses the deterministic distance tie-break, as before. + * `match_test` holds the stored test verdict of each of `matches`, and + * `scratch_test` is the matching scratch, so every filtered list the scorer + * sees carries its own verdicts. */ +static const char *swift_choose_receiver(const cbm_registry_t *r, const char **matches, + const uint8_t *match_test, int matched, const char *name, + const CBMCall *call, const char *module_qn, + const char **import_vals, int import_count, + const char **scratch, uint8_t *scratch_test) { if (!strchr(call->callee_name, '.')) { return NULL; } size_t callee_len = strlen(call->callee_name); const char **receiver_matches = scratch; + uint8_t *receiver_test = scratch_test; int receiver_count = 0; for (int i = 0; i < matched; i++) { size_t base = cbm_qn_callable_base_len_named(matches[i], name); if (base >= callee_len && memcmp(matches[i] + base - callee_len, call->callee_name, callee_len) == 0 && (base == callee_len || matches[i][base - callee_len - SKIP_ONE] == '.')) { + receiver_test[receiver_count] = match_test[i]; receiver_matches[receiver_count++] = matches[i]; } } @@ -3051,45 +3076,53 @@ static const char *swift_choose_receiver(const cbm_registry_t *r, const char **m return NULL; } const char **reachable = scratch + matched; + uint8_t *reachable_test = scratch_test + matched; int reachable_count = 0; if (import_vals) { for (int i = 0; i < receiver_count; i++) { if (is_import_reachable(receiver_matches[i], import_vals, import_count)) { + reachable_test[reachable_count] = receiver_test[i]; reachable[reachable_count++] = receiver_matches[i]; } } } const char **preferred = reachable_count > 0 ? reachable : receiver_matches; + const uint8_t *preferred_test = reachable_count > 0 ? reachable_test : receiver_test; int preferred_count = reachable_count > 0 ? reachable_count : receiver_count; - return best_by_import_distance(r, preferred, NULL, NULL, preferred_count, module_qn); + return best_by_import_distance(r, preferred, preferred_test, NULL, preferred_count, module_qn); } static const char *swift_choose_candidate(const cbm_registry_t *r, const char **matches, - int matched, const char *name, const CBMCall *call, - const char *module_qn, const char **import_vals, - int import_count, const char **scratch) { - const char *chosen = swift_choose_receiver(r, matches, matched, name, call, module_qn, - import_vals, import_count, scratch); + const uint8_t *match_test, int matched, const char *name, + const CBMCall *call, const char *module_qn, + const char **import_vals, int import_count, + const char **scratch, uint8_t *scratch_test) { + const char *chosen = + swift_choose_receiver(r, matches, match_test, matched, name, call, module_qn, import_vals, + import_count, scratch, scratch_test); if (chosen) { return chosen; } const char **reachable = scratch; + uint8_t *reachable_test = scratch_test; int reachable_count = 0; if (import_vals) { for (int i = 0; i < matched; i++) { if (is_import_reachable(matches[i], import_vals, import_count)) { + reachable_test[reachable_count] = match_test[i]; reachable[reachable_count++] = matches[i]; } } } if (reachable_count > 0) { - chosen = module_qn - ? best_by_import_distance(r, reachable, NULL, NULL, reachable_count, module_qn) - : reachable[0]; + chosen = module_qn ? best_by_import_distance(r, reachable, reachable_test, NULL, + reachable_count, module_qn) + : reachable[0]; } if (!chosen) { - chosen = module_qn ? best_by_import_distance(r, matches, NULL, NULL, matched, module_qn) - : matches[0]; + chosen = module_qn + ? best_by_import_distance(r, matches, match_test, NULL, matched, module_qn) + : matches[0]; } return chosen; } @@ -3719,23 +3752,29 @@ int cbm_registry_swift_candidates(const cbm_registry_t *r, const CBMCall *call, if (!bucket) { return 0; } + /* Three QN lists (the matches and the choosers' two scratch lists), each + * with a parallel byte per entry for its stored test verdict, in one block. */ enum { SWIFT_MATCH_BUFFERS = 3 }; - if ((size_t)bucket->count > SIZE_MAX / sizeof(const char *) / SWIFT_MATCH_BUFFERS) { + const size_t slot = sizeof(const char *) + sizeof(uint8_t); + if ((size_t)bucket->count > SIZE_MAX / slot / SWIFT_MATCH_BUFFERS) { return SWIFT_INCOMPATIBLE; } - const char **matches = cbm_alloc( - CBM_MEM_CLASS_OTHER, (size_t)bucket->count * SWIFT_MATCH_BUFFERS * sizeof(*matches)); + size_t slots = (size_t)bucket->count * SWIFT_MATCH_BUFFERS; + const char **matches = cbm_alloc(CBM_MEM_CLASS_OTHER, slots * slot); if (!matches) { return SWIFT_INCOMPATIBLE; } + uint8_t *match_test = (uint8_t *)(matches + slots); + const uint8_t *bucket_test = qn_test_flags(bucket); int matched = 0; bool has_swift = false; for (int i = 0; i < bucket->count; i++) { const char *qn = bucket->items[i]; swift_signature_t *meta = cbm_ht_get(r->swift_signatures, qn); has_swift = has_swift || meta != NULL; - if (meta && receiver_chain_admits(call->callee_name, qn) && + if (meta && receiver_chain_admits(call->callee_name, qn, CBM_LANG_SWIFT) && swift_signature_matches(meta, call)) { + match_test[matched] = bucket_test[i]; matches[matched++] = qn; } } @@ -3743,8 +3782,9 @@ int cbm_registry_swift_candidates(const cbm_registry_t *r, const CBMCall *call, cbm_free(CBM_MEM_CLASS_OTHER, matches); return has_swift ? SWIFT_INCOMPATIBLE : 0; } - const char *chosen = swift_choose_candidate(r, matches, matched, name, call, module_qn, - import_vals, import_count, matches + bucket->count); + const char *chosen = + swift_choose_candidate(r, matches, match_test, matched, name, call, module_qn, import_vals, + import_count, matches + bucket->count, match_test + bucket->count); if (!chosen) { cbm_free(CBM_MEM_CLASS_OTHER, matches); return 0; @@ -4445,7 +4485,8 @@ cbm_resolution_t cbm_registry_resolve(const cbm_registry_t *r, const char *calle const char *module_qn, const char **import_map_keys, const char **import_map_vals, int import_map_count) { return registry_resolve_scoped(r, callee_name, module_qn, import_map_keys, import_map_vals, - import_map_count, module_qn ? strlen(module_qn) : 0, false); + import_map_count, module_qn ? strlen(module_qn) : 0, false, + CBM_LANG_COUNT); } static size_t lang_scope_len(const char *module_qn, CBMLanguage lang) { @@ -4458,7 +4499,8 @@ cbm_resolution_t cbm_registry_resolve_lang(const cbm_registry_t *r, const char * const char **import_map_vals, int import_map_count, CBMLanguage lang) { return registry_resolve_scoped(r, callee_name, module_qn, import_map_keys, import_map_vals, - import_map_count, lang_scope_len(module_qn, lang), false); + import_map_count, lang_scope_len(module_qn, lang), false, + CBM_LANG_COUNT); } cbm_resolution_t cbm_registry_resolve_call_lang(const cbm_registry_t *r, const char *callee_name, @@ -4472,7 +4514,8 @@ cbm_resolution_t cbm_registry_resolve_call_lang(const cbm_registry_t *r, const c bool skip_members = lang == CBM_LANG_C && callee_name && !strchr(callee_name, '.') && !strstr(callee_name, "->"); return registry_resolve_scoped(r, callee_name, module_qn, import_map_keys, import_map_vals, - import_map_count, lang_scope_len(module_qn, lang), skip_members); + import_map_count, lang_scope_len(module_qn, lang), skip_members, + lang); } /* ── Class-qualified route handlers (PHP, #1146) ─────────────────── */ @@ -4640,7 +4683,8 @@ cbm_resolution_t cbm_registry_resolve_lineage(const cbm_registry_t *r, const cha * it would poison one variant with the other's semantics. SQL files hold * few distinct relation refs, so the chain walk stays cheap. */ return registry_resolve_chain(r, callee_name, module_qn, import_map_keys, import_map_vals, - import_map_count, module_qn ? strlen(module_qn) : 0, false); + import_map_count, module_qn ? strlen(module_qn) : 0, false, + CBM_LANG_COUNT); } /* Python looks a bare name up in the calling def's own scope, then in each @@ -4713,11 +4757,18 @@ cbm_resolution_t cbm_registry_resolve_call_scoped(const cbm_registry_t *r, const /* ── 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` and `out_langs` 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, and the languages travel with them. */ +static int filter_import_reachable(const char **candidates, const uint8_t *flags, + const uint8_t *langs, int count, const char **import_vals, + int import_count, const char **out, uint8_t *out_flags, + uint8_t *out_langs, 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_langs[n] = langs[i]; out[n++] = candidates[i]; } } @@ -4757,13 +4808,21 @@ 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]; + uint8_t filtered_langs[CBM_SZ_256]; int fcount = arr->count; const char **fptr = (const char **)arr->items; + const uint8_t *fflags = qn_test_flags(arr); + const uint8_t *flangs = qn_lang_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), + qn_lang_flags(arr), arr->count, import_map_vals, + import_map_count, filtered, filtered_flags, filtered_langs, + CBM_SZ_256); fptr = filtered; + fflags = filtered_flags; + flangs = filtered_langs; } if (fcount == 0) { @@ -4783,9 +4842,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 bool all = fptr == (const char **)arr->items; - const char *best = best_by_import_distance(r, fptr, all ? qn_test_flags(arr) : NULL, - all ? qn_lang_flags(arr) : NULL, fcount, module_qn); + const char *best = best_by_import_distance(r, fptr, fflags, flangs, fcount, module_qn); if (!best) { return no_match; } diff --git a/tests/test_callable_sig.c b/tests/test_callable_sig.c index 9f09e55d0..e981aa7ff 100644 --- a/tests/test_callable_sig.c +++ b/tests/test_callable_sig.c @@ -434,9 +434,9 @@ TEST(callable_sig_leaf_splitters_skip_suffix) { * keeps its historical key (last '.' or "::" segment). */ TEST(callable_sig_registry_by_name_uses_base_leaf) { cbm_registry_t *r = cbm_registry_new(); - cbm_registry_add(r, "upload", "p.Session.upload(_:Data,to:URL)", "Method"); - cbm_registry_add(r, "upload", "p.Session.upload(_:Data,with:URLRequest)", "Method"); - cbm_registry_add(r, "square", "p.Math::square", "Function"); + cbm_registry_add(r, "upload", "p.Session.upload(_:Data,to:URL)", "Method", false); + cbm_registry_add(r, "upload", "p.Session.upload(_:Data,with:URLRequest)", "Method", false); + cbm_registry_add(r, "square", "p.Math::square", "Function", false); const char **out = NULL; int count = 0; cbm_registry_find_by_name(r, "upload", &out, &count); diff --git a/tests/test_cli.c b/tests/test_cli.c index a337cce4d..2be1371ed 100644 --- a/tests/test_cli.c +++ b/tests/test_cli.c @@ -18664,19 +18664,31 @@ TEST(cli_hook_augment_deadline_breadcrumb_issue858) { snprintf(logpath, sizeof(logpath), "%s/timeouts.log", tmpdir); int fds[2]; + int errfds[2]; if (pipe(fds) != 0) { test_rmdir_r(tmpdir); FAIL("pipe failed"); } + if (pipe(errfds) != 0) { + close(fds[0]); + close(fds[1]); + test_rmdir_r(tmpdir); + FAIL("stderr pipe failed"); + } fflush(NULL); pid_t pid = fork(); if (pid == 0) { /* Child: hook-augment with a 60ms deadline and stdin that blocks - * forever (parent keeps the write end open, sends nothing). */ + * forever (parent keeps the write end open, sends nothing). Its stderr + * is redirected so the parent can prove the deadline is also announced + * there, not only in the timeouts log. */ close(fds[1]); dup2(fds[0], 0); close(fds[0]); + close(errfds[0]); + dup2(errfds[1], 2); + close(errfds[1]); setenv("CBM_HOOK_DEADLINE_MS", "60", 1); setenv("CBM_HOOK_TIMEOUT_LOG", logpath, 1); alarm(10); /* backstop: never hang the suite */ @@ -18684,6 +18696,19 @@ TEST(cli_hook_augment_deadline_breadcrumb_issue858) { } ASSERT_GT(pid, 0); close(fds[0]); + close(errfds[1]); + + /* Drain the child's stderr before reaping, so a full pipe can never wedge + * it. EOF arrives once the child's deadline handler has run and _exit()ed. */ + char errbuf[4096] = ""; + size_t errlen = 0; + ssize_t rd; + while (errlen + 1 < sizeof(errbuf) && + (rd = read(errfds[0], errbuf + errlen, sizeof(errbuf) - 1 - errlen)) > 0) { + errlen += (size_t)rd; + } + errbuf[errlen] = '\0'; + close(errfds[0]); int status = 0; waitpid(pid, &status, 0); @@ -18694,8 +18719,8 @@ TEST(cli_hook_augment_deadline_breadcrumb_issue858) { ASSERT_EQ(WEXITSTATUS(status), 0); /* RED before the fix: no breadcrumb existed — a fired deadline was - * indistinguishable from a no-match run. GREEN: the log names the - * deadline and the knob. */ + * indistinguishable from a no-match run. GREEN: it is announced on BOTH + * the timeouts log and stderr, each naming the deadline and the knob. */ FILE *f = fopen(logpath, "r"); if (!f) { fprintf(stderr, " [858] FAIL no timeout breadcrumb written to %s\n", logpath); @@ -18708,6 +18733,9 @@ TEST(cli_hook_augment_deadline_breadcrumb_issue858) { ASSERT(strstr(line, "deadline_exceeded") != NULL); ASSERT(strstr(line, "CBM_HOOK_DEADLINE_MS") != NULL); + ASSERT(strstr(errbuf, "deadline_exceeded") != NULL); + ASSERT(strstr(errbuf, "CBM_HOOK_DEADLINE_MS") != NULL); + cbm_unsetenv("CBM_HOOK_DEADLINE_MS"); cbm_unsetenv("CBM_HOOK_TIMEOUT_LOG"); test_rmdir_r(tmpdir); diff --git a/tests/test_daemon_frontend.c b/tests/test_daemon_frontend.c index aa49bf4c7..7e5c540ba 100644 --- a/tests/test_daemon_frontend.c +++ b/tests/test_daemon_frontend.c @@ -1641,6 +1641,32 @@ TEST(daemon_frontend_idle_uses_one_maintenance_observer) { ASSERT_TRUE(fixture_closed); PASS(); } + +/* An idle monitor must not repeatedly perform the expensive presence probe + * while no maintenance request exists (#2093). The monitor records the exact + * value it sleeps on in its no-maintenance branch; once recorded that value is + * stable, so the verdict depends on neither elapsed time nor scheduler cadence. + * The deadline below is only a hang guard. */ +TEST(daemon_frontend_idle_maintenance_probe_is_bounded) { + frontend_maintenance_fixture_t fixture; + ASSERT_TRUE(frontend_maintenance_fixture_start(&fixture, "idle-cadence")); + + cbm_daemon_frontend_test_observer_reset(false); + cbm_daemon_maintenance_monitor_t *monitor = cbm_daemon_maintenance_monitor_start( + fixture.manager, NULL, NULL, EXIT_SUCCESS, "idle cadence test"); + ASSERT_TRUE(monitor != NULL); + uint64_t deadline = cbm_now_ms() + 10000U; + while (cbm_now_ms() < deadline && cbm_daemon_frontend_test_monitor_idle_sleep_ms() == 0) { + cbm_usleep(1000); + } + uint32_t idle_sleep_ms = cbm_daemon_frontend_test_monitor_idle_sleep_ms(); + ASSERT_TRUE(cbm_daemon_maintenance_monitor_stop(&monitor)); + frontend_maintenance_fixture_finish(&fixture); + + ASSERT_TRUE(idle_sleep_ms != 0); + ASSERT_GTE(idle_sleep_ms, 250); + PASS(); +} #endif #ifndef _WIN32 @@ -1982,6 +2008,7 @@ SUITE(daemon_frontend) { RUN_TEST(daemon_frontend_rejects_non_notification_cancellation_shapes); #if defined(CBM_ENABLE_TEST_SEAMS) && CBM_ENABLE_TEST_SEAMS RUN_TEST(daemon_frontend_idle_uses_one_maintenance_observer); + RUN_TEST(daemon_frontend_idle_maintenance_probe_is_bounded); #endif #ifndef _WIN32 RUN_TEST(daemon_frontend_maintenance_exits_while_stdio_reader_is_blocked); diff --git a/tests/test_daemon_runtime.c b/tests/test_daemon_runtime.c index 61488358c..c34c45f5b 100644 --- a/tests/test_daemon_runtime.c +++ b/tests/test_daemon_runtime.c @@ -5237,6 +5237,196 @@ TEST(daemon_runtime_process_fingerprint_never_hashes_replacement_path) { } #endif +/* The executable-image fingerprint cache short-circuits the full-image hash for + * a repeated file identity, and rolls its key when the binary changes so a + * rebuild or atomic replacement never returns a stale digest. Results are + * gathered first and asserted only after the stub is disarmed: an assertion + * returns early, and an armed stub would replace every later fingerprint in + * this process. */ +TEST(daemon_runtime_fingerprint_cache_hit_miss_key_roll) { + /* 64-hex placeholders, distinct enough to tell "cached" from "recomputed". */ + static const char digest_a[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] = + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + static const char digest_b[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] = + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; + const cbm_daemon_runtime_fingerprint_identity_for_testing_t image = {1, 2, 3, 100, + 200, 300, 400}; + cbm_daemon_runtime_fingerprint_identity_for_testing_t rolled = image; + char miss[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] = {0}; + char hit[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] = {0}; + char mtime_rolled[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] = {0}; + char ctime_rolled[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] = {0}; + char inode_rolled[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] = {0}; + + cbm_daemon_runtime_fingerprint_cache_reset_for_testing(); + /* Miss: first sight of this identity pays the hash and stores digest_a. */ + cbm_daemon_runtime_fingerprint_cache_set_hash_stub_for_testing(digest_a); + bool miss_ok = cbm_daemon_runtime_fingerprint_cache_resolve_for_testing(&image, &image, miss); + int hashes_after_miss = cbm_daemon_runtime_fingerprint_hash_call_count_for_testing(); + /* Hit: the same identity returns the cached digest without calling the + * hash, even though the stub would now yield a different value. */ + cbm_daemon_runtime_fingerprint_cache_set_hash_stub_for_testing(digest_b); + bool hit_ok = cbm_daemon_runtime_fingerprint_cache_resolve_for_testing(&image, &image, hit); + int hashes_after_hit = cbm_daemon_runtime_fingerprint_hash_call_count_for_testing(); + /* Key roll: a changed mtime (a rebuilt binary) misses and recomputes. */ + rolled = image; + rolled.mtime_seconds = 101; + bool mtime_ok = + cbm_daemon_runtime_fingerprint_cache_resolve_for_testing(&rolled, &rolled, mtime_rolled); + int hashes_after_mtime_roll = cbm_daemon_runtime_fingerprint_hash_call_count_for_testing(); + /* A changed change time (an in-place write, a metadata edit) rolls it too. */ + rolled = image; + rolled.ctime_seconds = 301; + bool ctime_ok = + cbm_daemon_runtime_fingerprint_cache_resolve_for_testing(&rolled, &rolled, ctime_rolled); + int hashes_after_ctime_roll = cbm_daemon_runtime_fingerprint_hash_call_count_for_testing(); + /* A changed inode (atomic replacement) likewise rolls the key. */ + rolled = image; + rolled.inode = 9; + bool inode_ok = + cbm_daemon_runtime_fingerprint_cache_resolve_for_testing(&rolled, &rolled, inode_rolled); + int hashes_after_inode_roll = cbm_daemon_runtime_fingerprint_hash_call_count_for_testing(); + cbm_daemon_runtime_fingerprint_cache_reset_for_testing(); + + ASSERT_TRUE(miss_ok); + ASSERT_STR_EQ(miss, digest_a); + ASSERT_EQ(hashes_after_miss, 1); + ASSERT_TRUE(hit_ok); + ASSERT_STR_EQ(hit, digest_a); + ASSERT_EQ(hashes_after_hit, 1); + ASSERT_TRUE(mtime_ok); + ASSERT_STR_EQ(mtime_rolled, digest_b); + ASSERT_EQ(hashes_after_mtime_roll, 2); + ASSERT_TRUE(ctime_ok); + ASSERT_STR_EQ(ctime_rolled, digest_b); + ASSERT_EQ(hashes_after_ctime_roll, 3); + ASSERT_TRUE(inode_ok); + ASSERT_STR_EQ(inode_rolled, digest_b); + ASSERT_EQ(hashes_after_inode_roll, 4); + PASS(); +} + +/* A digest is committed only when the identity observed after the hash is the + * one it was keyed on. An image that changed under the hash still yields the + * bytes read for that acquisition (the acquire chain's own post-hash stat check + * is what rejects it) but is never cached, so the next sight of the original + * identity hashes again. */ +TEST(daemon_runtime_fingerprint_cache_skips_image_changed_under_hash) { + static const char digest[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] = + "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"; + const cbm_daemon_runtime_fingerprint_identity_for_testing_t image = {1, 2, 3, 100, + 200, 300, 400}; + cbm_daemon_runtime_fingerprint_identity_for_testing_t moved = image; + moved.ctime_seconds = 301; + char first[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] = {0}; + char second[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] = {0}; + + cbm_daemon_runtime_fingerprint_cache_reset_for_testing(); + cbm_daemon_runtime_fingerprint_cache_set_hash_stub_for_testing(digest); + bool first_ok = cbm_daemon_runtime_fingerprint_cache_resolve_for_testing(&image, &moved, first); + int hashes_after_first = cbm_daemon_runtime_fingerprint_hash_call_count_for_testing(); + bool second_ok = + cbm_daemon_runtime_fingerprint_cache_resolve_for_testing(&image, &image, second); + int hashes_after_second = cbm_daemon_runtime_fingerprint_hash_call_count_for_testing(); + cbm_daemon_runtime_fingerprint_cache_reset_for_testing(); + + ASSERT_TRUE(first_ok); + ASSERT_STR_EQ(first, digest); + ASSERT_EQ(hashes_after_first, 1); + ASSERT_TRUE(second_ok); + ASSERT_STR_EQ(second, digest); + ASSERT_EQ(hashes_after_second, 2); + PASS(); +} + +/* The real acquire path: fingerprinting this process twice pays the image hash + * once, and the second call is served from the cache inside the same bracketed + * identity checks. This drives cbm_daemon_runtime_process_build_fingerprint, + * not the seam, so a cache wired into the seam but not into the platform's + * acquire site fails here. */ +TEST(daemon_runtime_process_fingerprint_hashes_own_image_once_per_process) { + static const char digest[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] = + "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"; + char first[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] = {0}; + char second[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] = {0}; + + cbm_daemon_runtime_fingerprint_cache_reset_for_testing(); + cbm_daemon_runtime_fingerprint_cache_set_hash_stub_for_testing(digest); + bool first_ok = cbm_daemon_runtime_process_build_fingerprint(runtime_test_process_id(), first); + int hashes_after_first = cbm_daemon_runtime_fingerprint_hash_call_count_for_testing(); + bool second_ok = + cbm_daemon_runtime_process_build_fingerprint(runtime_test_process_id(), second); + int hashes_after_second = cbm_daemon_runtime_fingerprint_hash_call_count_for_testing(); + cbm_daemon_runtime_fingerprint_cache_reset_for_testing(); + + ASSERT_TRUE(first_ok); + ASSERT_TRUE(second_ok); + ASSERT_STR_EQ(first, digest); + ASSERT_STR_EQ(second, digest); + ASSERT_EQ(hashes_after_first, 1); + ASSERT_EQ(hashes_after_second, 1); + PASS(); +} + +#if defined(__APPLE__) || defined(__linux__) +/* Only this process's own image is cached. The peer here is this very runner + * spawned in holder mode, so its image has exactly the file identity the own + * image is cached under: a cache keyed on that identity alone would answer for + * the peer. A peer must be hashed on every fingerprint, before and after the + * own image is cached, while the own image still costs one hash. */ +TEST(daemon_runtime_process_fingerprint_caches_only_own_image) { + static const char digest[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] = + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; + char image_path[RUNTIME_TEST_PATH_CAP] = {0}; + char peer_first[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] = {0}; + char peer_second[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] = {0}; + char own_first[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] = {0}; + char own_second[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] = {0}; + char peer_after_own[CBM_DAEMON_BUILD_FINGERPRINT_SIZE] = {0}; + int release_fd = -1; + bool setup = runtime_test_self_image_path(image_path); + pid_t child = setup ? runtime_test_spawn_blocked_executable(image_path, &release_fd) : -1; + setup = setup && child > 0; + + cbm_daemon_runtime_fingerprint_cache_reset_for_testing(); + cbm_daemon_runtime_fingerprint_cache_set_hash_stub_for_testing(digest); + bool peer_first_ok = + setup && cbm_daemon_runtime_process_build_fingerprint((uint64_t)child, peer_first); + bool peer_second_ok = + setup && cbm_daemon_runtime_process_build_fingerprint((uint64_t)child, peer_second); + int hashes_after_peer = cbm_daemon_runtime_fingerprint_hash_call_count_for_testing(); + bool own_first_ok = + cbm_daemon_runtime_process_build_fingerprint(runtime_test_process_id(), own_first); + bool own_second_ok = + cbm_daemon_runtime_process_build_fingerprint(runtime_test_process_id(), own_second); + int hashes_after_own = cbm_daemon_runtime_fingerprint_hash_call_count_for_testing(); + bool peer_after_own_ok = + setup && cbm_daemon_runtime_process_build_fingerprint((uint64_t)child, peer_after_own); + int hashes_after_peer_again = cbm_daemon_runtime_fingerprint_hash_call_count_for_testing(); + cbm_daemon_runtime_fingerprint_cache_reset_for_testing(); + runtime_test_stop_blocked_executable(child, release_fd); + + ASSERT_TRUE(setup); + ASSERT_TRUE(peer_first_ok); + ASSERT_TRUE(peer_second_ok); + ASSERT_TRUE(own_first_ok); + ASSERT_TRUE(own_second_ok); + ASSERT_TRUE(peer_after_own_ok); + ASSERT_STR_EQ(peer_first, digest); + ASSERT_STR_EQ(peer_second, digest); + ASSERT_STR_EQ(own_first, digest); + ASSERT_STR_EQ(own_second, digest); + ASSERT_STR_EQ(peer_after_own, digest); + /* Each peer fingerprint pays its own hash: a peer is never cached. */ + ASSERT_EQ(hashes_after_peer, 2); + /* The own image is hashed once, then served from the cache. */ + ASSERT_EQ(hashes_after_own, 3); + /* The cached own entry never answers for the peer's identical file. */ + ASSERT_EQ(hashes_after_peer_again, 4); + PASS(); +} +#endif + TEST(daemon_runtime_close_begin_releases_admission_with_inflight_request) { static const uint8_t request[] = {'b', 'l', 'o', 'c', 'k'}; cbm_daemon_build_identity_t identity = @@ -5430,6 +5620,12 @@ SUITE(daemon_runtime) { RUN_TEST(daemon_runtime_copied_image_fallback_accepts_identical_and_rejects_changed); #if defined(_WIN32) || defined(__APPLE__) || defined(__linux__) RUN_TEST(daemon_runtime_process_fingerprint_never_hashes_replacement_path); +#endif + RUN_TEST(daemon_runtime_fingerprint_cache_hit_miss_key_roll); + RUN_TEST(daemon_runtime_fingerprint_cache_skips_image_changed_under_hash); + RUN_TEST(daemon_runtime_process_fingerprint_hashes_own_image_once_per_process); +#if defined(__APPLE__) || defined(__linux__) + RUN_TEST(daemon_runtime_process_fingerprint_caches_only_own_image); #endif RUN_TEST(daemon_runtime_convenience_service_owns_participant_guard); RUN_TEST(daemon_runtime_host_loop_stops_on_coordination_file_loss); diff --git a/tests/test_fqn.c b/tests/test_fqn.c index 013bea6a9..d2734df0d 100644 --- a/tests/test_fqn.c +++ b/tests/test_fqn.c @@ -630,6 +630,23 @@ TEST(project_name_encodes_unicode_segments_issue571) { PASS(); } +TEST(project_name_unicode_segments_do_not_collide_issue571) { + char *first = cbm_project_name_from_path("/Users/dev/\xe5\xbc\x80\xe5\x8f\x91"); + char *second = cbm_project_name_from_path("/Users/dev/\xe5\x90\x8e\xe7\xab\xaf"); + ASSERT_NOT_NULL(first); + ASSERT_NOT_NULL(second); + ASSERT_STR_NEQ(first, second); + /* Distinct is not enough: each name must carry the hex image of its own + * non-ASCII segment, so the segment is preserved rather than replaced. */ + ASSERT_NOT_NULL(strstr(first, "e5bc80e58f91")); + ASSERT_NOT_NULL(strstr(second, "e5908ee7abaf")); + ASSERT_TRUE(cbm_validate_project_name(first)); + ASSERT_TRUE(cbm_validate_project_name(second)); + free(first); + free(second); + PASS(); +} + /* issue #624: #571 preserves non-ASCII path segments by hex-encoding each byte * (1 byte -> 2 hex chars), so a DEEP non-ASCII path triples in length and can * blow past the filesystem's 255-byte filename-component limit. Then @@ -802,6 +819,7 @@ SUITE(fqn) { RUN_TEST(project_name_deep_path); RUN_TEST(project_name_always_validator_safe_issue349); RUN_TEST(project_name_encodes_unicode_segments_issue571); + RUN_TEST(project_name_unicode_segments_do_not_collide_issue571); RUN_TEST(project_name_length_capped_issue624); RUN_TEST(project_name_colon_only); RUN_TEST(project_name_backslash_only); diff --git a/tests/test_parallel.c b/tests/test_parallel.c index 09815518a..59c8b9130 100644 --- a/tests/test_parallel.c +++ b/tests/test_parallel.c @@ -3331,6 +3331,100 @@ TEST(parallel_csharp_parse_damaged_namespace_is_not_global) { PASS(); } +/* #2121, the issue's own two files: C# spells its built-in types as lower-case + * keywords, so `int.TryParse(...)` calls the BCL, never the project's only + * TryParse. The LSP's answer (System.Int32.TryParse) has no graph node, so the + * call falls through to the registry, whose receiver chain check used to skip + * every lower-case root. Both resolvers must refuse the name guess (also for + * the C# 9 `nint`), and a real call to the project type must still bind. */ +TEST(parallel_csharp_keyword_receiver_no_name_guess_issue2121) { + static const char decoys_src[] = + "namespace Contoso.App;\n" + "\n" + "public class TsidNode\n" + "{\n" + " public static bool TryParse(string text, out TsidNode n)\n" + " {\n" + " n = null;\n" + " return false;\n" + " }\n" + "}\n"; + static const char demo_src[] = "using Microsoft.Extensions.Options;\n" + "\n" + "namespace Contoso.App;\n" + "\n" + "public class Demo\n" + "{\n" + " public string M(string input)\n" + " {\n" + " _ = int.TryParse(input, out _);\n" + " _ = nint.TryParse(input, out _);\n" + " return input;\n" + " }\n" + "\n" + " public string Control(string input)\n" + " {\n" + " _ = TsidNode.TryParse(input, out _);\n" + " return input;\n" + " }\n" + "}\n"; + const char *project = "cbm_cs_2121"; + static const char *rels[] = {"src/Decoys.cs", "src/Demo.cs"}; + const char *srcs[] = {decoys_src, demo_src}; + enum { N_FILES = 2 }; + + char tmpdir[256]; + snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_par_cs_2121_XXXXXX"); + if (!cbm_mkdtemp(tmpdir)) { + FAIL("mkdtemp failed"); + } + char paths[N_FILES][512]; + cbm_file_info_t files[N_FILES] = {0}; + for (int i = 0; i < N_FILES; i++) { + snprintf(paths[i], sizeof(paths[i]), "%s/%s", tmpdir, rels[i]); + if (th_write_file(paths[i], srcs[i]) != 0) { + th_rmtree(tmpdir); + FAIL("failed to write #2121 fixture"); + } + files[i].path = paths[i]; + files[i].rel_path = (char *)rels[i]; + files[i].language = CBM_LANG_CSHARP; + } + + cbm_gbuf_t *graphs[2] = {run_sequential_with_lsp_cross(project, tmpdir, files, N_FILES), NULL}; + g_harness_cs_registry = true; + graphs[1] = run_parallel(project, tmpdir, files, N_FILES, 2); + g_harness_cs_registry = false; + ASSERT_NOT_NULL(graphs[0]); + ASSERT_NOT_NULL(graphs[1]); + + const char *tsid = "cbm_cs_2121.src.Decoys.TsidNode.TryParse"; + bool ok[2][4]; + bool all = true; + for (int g = 0; g < 2; g++) { + cbm_gbuf_t *gb = graphs[g]; + /* The negative below must not pass because a node went missing. */ + ok[g][0] = cbm_gbuf_find_by_qn(gb, tsid) != NULL; + ok[g][1] = find_unique_callable_node_by_tail(gb, "Demo.M") != NULL; + ok[g][2] = !has_edge_from_callable_to_qn(gb, "Demo.M", tsid, "CALLS"); + ok[g][3] = has_edge_from_callable_to_qn(gb, "Demo.Control", tsid, "CALLS"); + for (int k = 0; k < 4; k++) { + all = all && ok[g][k]; + } + } + if (!all) { + for (int g = 0; g < 2; g++) { + printf(" #2121 diagnostic %s: target=%d caller=%d no_keyword_guess=%d control=%d\n", + g == 0 ? "sequential" : "parallel", ok[g][0], ok[g][1], ok[g][2], ok[g][3]); + } + } + cbm_gbuf_free(graphs[0]); + cbm_gbuf_free(graphs[1]); + th_rmtree(tmpdir); + ASSERT_TRUE(all); + PASS(); +} + /* External Kotlin protocol targets must not borrow a project method merely * because the final Class.method segments agree. Raw assertions prove that the * iterator/destructuring carriers and their stdlib semantic rows really exist; @@ -6031,6 +6125,7 @@ SUITE(parallel) { RUN_TEST(parallel_csharp_same_name_class_binds_by_namespace); RUN_TEST(parallel_csharp_nested_type_and_global_namespace_binding); RUN_TEST(parallel_csharp_parse_damaged_namespace_is_not_global); + RUN_TEST(parallel_csharp_keyword_receiver_no_name_guess_issue2121); RUN_TEST(parallel_kotlin_external_protocol_does_not_use_project_class_method_tail); RUN_TEST(parallel_kotlin_nonbinary_operator_carriers_reach_graph); RUN_TEST(parallel_rust_cross_crate_worker_receives_workspace_manifest); diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index 71d423f50..baf2e34b7 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -6619,6 +6619,132 @@ TEST(pipeline_incremental_cross_file_call_reference_matches_fresh_full) { PASS(); } +/* #2427 rank probes: one bare call, two same-named candidates, and the + * registry's test ranking alone decides which one the call binds. Three runs + * on one database: a full index, then a comment-only edit of the caller on + * the closure-repair route, then another on the forced legacy-partial route. + * Both incremental routes re-resolve the caller against a registry seeded from + * stored nodes rather than the extractor's definitions, which is where a + * seeder that loses the verdict shows. */ +typedef struct { + int want[3]; + int other[3]; + cbm_incremental_route_t route[3]; +} rank_probe_t; + +static int rank_probe_count(const char *db, const char *project, const char *source, + const char *target, const char *file) { + cbm_store_t *s = cbm_store_open_path(db); + if (!s) { + return -1; + } + int n = named_edge_to_file_count(s, project, "CALLS", source, target, file); + cbm_store_close(s); + return n; +} + +static void rank_probe(const char *tmp, const char *caller_path, const char *caller_src, + const char *source, const char *target, const char *want_file, + const char *other_file, rank_probe_t *out) { + char db[512]; + snprintf(db, sizeof(db), "%s/rank.db", tmp); + for (int run = 0; run < 3; run++) { + out->want[run] = -1; + out->other[run] = -1; + out->route[run] = CBM_INCREMENTAL_ROUTE_NONE; + if (run > 0) { + char edited[512]; + snprintf(edited, sizeof(edited), "%s// edit %d\n", caller_src, run); + write_temp_file(tmp, caller_path, edited); + } + cbm_pipeline_incremental_test_reset_faults(); +#if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API + if (run == 2) { + cbm_pipeline_incremental_test_force_legacy_partial_once(); + } +#endif + cbm_pipeline_t *p = cbm_pipeline_new(tmp, db, CBM_MODE_FULL); + if (!p) { + return; + } + int rc = cbm_pipeline_run(p); + out->route[run] = cbm_pipeline_incremental_test_last_route(); + char project[256]; + snprintf(project, sizeof(project), "%s", cbm_pipeline_project_name(p)); + cbm_pipeline_free(p); + if (rc != 0) { + return; + } + out->want[run] = rank_probe_count(db, project, source, target, want_file); + out->other[run] = rank_probe_count(db, project, source, target, other_file); + } +} + +/* "latest" spells "test", so the QN scan the extractor's verdict replaced + * ranked the production Parse below a helper in a _test.go file, whose Go QN + * names only its package and so spells nothing. The verdict ranks them the + * right way round, and both incremental routes must read the same verdict + * back from the stored nodes: the closure-repair proxies once carried no + * is_test, every candidate scored as production, and the smaller QN -- the + * test helper -- won. */ +TEST(pipeline_registry_ranks_by_extractor_verdict_full_and_incremental) { + char tmp[256]; + snprintf(tmp, sizeof(tmp), "/tmp/cbm_rank_verdict_XXXXXX"); + ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); + static const char caller[] = "package app\n\nfunc Run() int { return Parse() }\n"; + write_temp_file(tmp, "go.mod", "module example.com/rank\n\ngo 1.22\n"); + write_temp_file(tmp, "latest/parse.go", "package latest\n\nfunc Parse() int { return 1 }\n"); + write_temp_file(tmp, "harness/parse_test.go", + "package harness\n\nfunc Parse() int { return 2 }\n"); + write_temp_file(tmp, "app/run.go", caller); + rank_probe_t r; + rank_probe(tmp, "app/run.go", caller, "Run", "Parse", "latest/parse.go", + "harness/parse_test.go", &r); + th_rmtree(tmp); + ASSERT_EQ(r.want[0], 1); /* full */ + ASSERT_EQ(r.other[0], 0); + ASSERT_EQ(r.route[1], CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR); + ASSERT_EQ(r.want[1], 1); + ASSERT_EQ(r.other[1], 0); +#if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API + ASSERT_EQ(r.route[2], CBM_INCREMENTAL_ROUTE_LEGACY_PARTIAL); +#endif + ASSERT_EQ(r.want[2], 1); + ASSERT_EQ(r.other[2], 0); + PASS(); +} + +/* A generated fake beside its callers: client/fake shares more of the caller's + * package path than the real store does, so on the extractor's verdict alone + * (a fake is not a test file) the fake wins on proximity. The test-double path + * rule ranks it last in the full index and, through the incremental seed's own + * use of the rule, on both incremental routes. */ +TEST(pipeline_registry_ranks_test_doubles_last_full_and_incremental) { + char tmp[256]; + snprintf(tmp, sizeof(tmp), "/tmp/cbm_rank_double_XXXXXX"); + ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); + static const char caller[] = "package client\n\nfunc Run() int { return Load() }\n"; + write_temp_file(tmp, "go.mod", "module example.com/rank\n\ngo 1.22\n"); + write_temp_file(tmp, "store/load.go", "package store\n\nfunc Load() int { return 1 }\n"); + write_temp_file(tmp, "client/fake/load.go", "package fake\n\nfunc Load() int { return 2 }\n"); + write_temp_file(tmp, "client/run.go", caller); + rank_probe_t r; + rank_probe(tmp, "client/run.go", caller, "Run", "Load", "store/load.go", "client/fake/load.go", + &r); + th_rmtree(tmp); + ASSERT_EQ(r.want[0], 1); /* full */ + ASSERT_EQ(r.other[0], 0); + ASSERT_EQ(r.route[1], CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR); + ASSERT_EQ(r.want[1], 1); + ASSERT_EQ(r.other[1], 0); +#if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API + ASSERT_EQ(r.route[2], CBM_INCREMENTAL_ROUTE_LEGACY_PARTIAL); +#endif + ASSERT_EQ(r.want[2], 1); + ASSERT_EQ(r.other[2], 0); + PASS(); +} + /* A semantic control file can change an unchanged source file's exact target. * Both target modules deliberately export the same short name: without the * tsconfig path mapping, project-wide unique-name fallback is ambiguous and @@ -21834,7 +21960,7 @@ static int python_typed_field_scope_case(bool external) { if (result && reg && gbuf && result->field_types.count == 1) { for (int i = 0; i < result->defs.count; i++) { const CBMDefinition *def = &result->defs.items[i]; - cbm_registry_add(reg, def->name, def->qualified_name, def->label); + cbm_registry_add(reg, def->name, def->qualified_name, def->label, false); } const char **keys = NULL; const char **vals = NULL; @@ -27801,8 +27927,8 @@ TEST(helm_parse_chart_many_dependencies) { 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); @@ -27820,7 +27946,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); @@ -27832,8 +27958,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 = @@ -27852,7 +27978,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", @@ -27877,8 +28003,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")); @@ -27891,7 +28017,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"}; @@ -27906,7 +28032,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"}; @@ -27921,7 +28047,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"); @@ -27934,7 +28060,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"); @@ -27947,8 +28073,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"); @@ -27964,7 +28090,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); @@ -27978,8 +28104,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); @@ -27993,7 +28119,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); @@ -28013,7 +28139,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); @@ -28024,7 +28150,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, @@ -28040,7 +28166,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); @@ -28050,7 +28176,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"); @@ -28063,7 +28189,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"); @@ -28073,10 +28199,99 @@ TEST(registry_receiver_chain_ignores_lowercase_root_issue1893) { PASS(); } +/* Issue #2121: unlike vm.load's lower-case value root above, C#'s built-in + * type keywords (int, string, bool, nint, ...) DO name a type and must get the + * same chain-consistency check an upper-case root gets. The check belongs to + * the CALLS entry point both resolvers share, for a C# file. */ +TEST(registry_receiver_chain_refuses_lowercase_type_keyword_issue2121) { + cbm_registry_t *reg = cbm_registry_new(); + cbm_registry_add(reg, "TryParse", "tmp.Decoys.TsidNode.TryParse", "Method", false); + + cbm_resolution_t r = cbm_registry_resolve_call_lang(reg, "int.TryParse", "tmp.Demo", NULL, NULL, + 0, CBM_LANG_CSHARP); + ASSERT_NULL(r.qualified_name); + /* C# 9 native-sized integers are keywords in receiver position too. */ + r = cbm_registry_resolve_call_lang(reg, "nint.TryParse", "tmp.Demo", NULL, NULL, 0, + CBM_LANG_CSHARP); + ASSERT_NULL(r.qualified_name); + + cbm_registry_free(reg); + PASS(); +} + +/* The true positive the new keyword check must not eat: the project really + * does define a type literally named "int" (or similar) and the chain shows + * it, so the match must still go through. Resolved as a C# call, so this + * exercises the keyword branch's ancestry walk, not the non-C# early return + * (which would pass through for the wrong reason). */ +TEST(registry_receiver_chain_keeps_type_keyword_when_chain_matches_issue2121) { + cbm_registry_t *reg = cbm_registry_new(); + cbm_registry_add(reg, "TryParse", "tmp.int.TryParse", "Method", false); + + cbm_resolution_t r = cbm_registry_resolve_call_lang(reg, "int.TryParse", "tmp.Demo", NULL, NULL, + 0, CBM_LANG_CSHARP); + ASSERT_STR_EQ(r.qualified_name, "tmp.int.TryParse"); + + cbm_registry_free(reg); + PASS(); +} + +/* The type-keyword branch above is a C#-only rule (closed list, gated on the + * calling file's language) and must not trade away recall in a language where + * the same spellings are ordinary identifiers. `object` and `string` are + * ordinary lower-case variable names in Python/Go; a variable named `object` + * calling a method (`object.TryParse`, shaped exactly like the refusal test + * above) must still resolve as a value root there: the negative control + * pinning the other side of the gate. */ +TEST(registry_receiver_chain_keyword_list_gated_to_csharp_issue2121) { + cbm_registry_t *reg = cbm_registry_new(); + cbm_registry_add(reg, "TryParse", "tmp.Decoys.TsidNode.TryParse", "Method", false); + + cbm_resolution_t r = cbm_registry_resolve_call_lang(reg, "object.TryParse", "tmp.Demo", NULL, + NULL, 0, CBM_LANG_PYTHON); + ASSERT_STR_EQ(r.qualified_name, "tmp.Decoys.TsidNode.TryParse"); + ASSERT_STR_EQ(r.strategy, "unique_name"); + + /* A Go call and the language-blind cbm_registry_resolve read a second + * keyword spelling as a value root as well. */ + cbm_registry_t *reg2 = cbm_registry_new(); + cbm_registry_add(reg2, "Join", "tmp.Decoys.Joiner.Join", "Method", false); + cbm_resolution_t r2 = + cbm_registry_resolve_call_lang(reg2, "string.Join", "tmp.Demo", NULL, NULL, 0, CBM_LANG_GO); + ASSERT_STR_EQ(r2.qualified_name, "tmp.Decoys.Joiner.Join"); + ASSERT_STR_EQ(r2.strategy, "unique_name"); + r2 = cbm_registry_resolve(reg2, "string.Join", "tmp.Demo", NULL, NULL, 0); + ASSERT_STR_EQ(r2.qualified_name, "tmp.Decoys.Joiner.Join"); + ASSERT_STR_EQ(r2.strategy, "unique_name"); + + cbm_registry_free(reg); + cbm_registry_free(reg2); + PASS(); +} + +/* `dynamic` is a contextual keyword in C#, not a reserved type keyword like + * `int`: in receiver position (`dynamic.Invoke(...)`) it is a variable that + * happens to be called `dynamic`, whose type the chain does not show. In a C# + * file it must stay an ordinary lower-case value root, like vm.load above, so + * the call still binds: the control that keeps the keyword list closed to + * words that can only name a type. */ +TEST(registry_receiver_chain_csharp_dynamic_variable_still_binds_issue2121) { + cbm_registry_t *reg = cbm_registry_new(); + cbm_registry_add(reg, "Invoke", "tmp.Handlers.Handler.Invoke", "Method", false); + + cbm_resolution_t r = cbm_registry_resolve_call_lang(reg, "dynamic.Invoke", "tmp.Demo", NULL, + NULL, 0, CBM_LANG_CSHARP); + ASSERT_STR_EQ(r.qualified_name, "tmp.Handlers.Handler.Invoke"); + ASSERT_STR_EQ(r.strategy, "unique_name"); + + cbm_registry_free(reg); + PASS(); +} + /* 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"); @@ -28088,7 +28303,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); @@ -28102,8 +28317,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); @@ -28117,8 +28332,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"}; @@ -28132,7 +28347,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"}; @@ -28149,7 +28364,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 = @@ -28163,10 +28378,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; @@ -29126,10 +29341,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; @@ -34397,6 +34612,8 @@ SUITE(pipeline) { RUN_TEST(pipeline_call_reference_sequential_parallel_edge_set_parity); RUN_TEST(pipeline_complexity_props_independent_of_worker_order); RUN_TEST(pipeline_incremental_cross_file_call_reference_matches_fresh_full); + RUN_TEST(pipeline_registry_ranks_by_extractor_verdict_full_and_incremental); + RUN_TEST(pipeline_registry_ranks_test_doubles_last_full_and_incremental); RUN_TEST(pipeline_incremental_changed_target_invalidates_stale_inbound_call_reference); RUN_TEST(pipeline_incremental_parallel_registry_nodes_advance_shared_ids); #if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API @@ -34687,6 +34904,10 @@ SUITE(pipeline) { RUN_TEST(registry_receiver_chain_admits_factory_chain_into_nested_type); RUN_TEST(registry_receiver_chain_still_refuses_foreign_root_with_ancestry_rule); RUN_TEST(registry_receiver_chain_ignores_lowercase_root_issue1893); + RUN_TEST(registry_receiver_chain_refuses_lowercase_type_keyword_issue2121); + RUN_TEST(registry_receiver_chain_keeps_type_keyword_when_chain_matches_issue2121); + RUN_TEST(registry_receiver_chain_keyword_list_gated_to_csharp_issue2121); + RUN_TEST(registry_receiver_chain_csharp_dynamic_variable_still_binds_issue2121); RUN_TEST(registry_receiver_chain_ignores_bare_name_issue1893); RUN_TEST(registry_fuzzy_confidence_single); RUN_TEST(registry_fuzzy_confidence_distance); diff --git a/tests/test_registry.c b/tests/test_registry.c index 6e0e18f3a..10c9ef8e5 100644 --- a/tests/test_registry.c +++ b/tests/test_registry.c @@ -193,7 +193,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")); @@ -201,10 +201,114 @@ 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(); +} + +/* The test-double path rule: whole directory segments named fake, mock, mocks, + * testing or testutil, and Go files named mock_*.go or fake_*.go. Anchored, so + * the substrings the old QN scan tripped on (inspect, latest, contest) and + * near-miss names (fakeroot, mockery, testingutil) stay ordinary code. */ +TEST(registry_test_double_path_table) { + static const char *const doubles[] = { + "pkg/client/fake/clientset.go", + "fake/x.go", + "internal/mocks/store.go", + "pkg/mock/store.go", + "pkg/testing/fixtures.go", + "internal/testutil/helpers.go", + "pkg/mock_store.go", + "pkg/fake_clock.go", + "mock_store.go", + "src/fake/deep/nested/util.py", + }; + static const char *const ordinary[] = { + "pkg/inspect/x.go", "pkg/latest/x.go", "pkg/contest/x.go", "pkg/fakeroot/x.go", + "pkg/mockery/x.go", "pkg/testingutil/x.go", "pkg/mocking.go", "pkg/mock_store.py", + "pkg/fake_clock.golang", "pkg/fake.go", "pkg/x/fake", "pkg/Fake/x.go", + "pkg/myfake/x.go", "pkg/fake-client/x.go", "testing.go", "", + }; + char why[256]; + for (size_t i = 0; i < sizeof(doubles) / sizeof(doubles[0]); i++) { + if (!cbm_registry_test_double_path(doubles[i])) { + snprintf(why, sizeof(why), "expected a test-double path: %s", doubles[i]); + FAIL(why); + } + } + for (size_t i = 0; i < sizeof(ordinary) / sizeof(ordinary[0]); i++) { + if (cbm_registry_test_double_path(ordinary[i])) { + snprintf(why, sizeof(why), "expected an ordinary path: %s", ordinary[i]); + FAIL(why); + } + } + ASSERT_FALSE(cbm_registry_test_double_path(NULL)); + PASS(); +} + +/* The Swift overload choosers score with the verdicts stored at registration, + * carried through their own filtered lists. Both overloads fit the call and tie + * on proximity; without the verdicts the smaller QN (the test double) would win + * the tie. */ +TEST(registry_swift_overload_choice_uses_stored_verdicts) { + cbm_registry_t *r = cbm_registry_new(); + const char *double_qn = "proj.Fixtures.Store.load(path:String)"; + const char *real_qn = "proj.Zeta.Store.load(path:String)"; + cbm_registry_add(r, "load", double_qn, "Method", true); + cbm_registry_add(r, "load", real_qn, "Method", false); + cbm_registry_set_swift_signature(r, double_qn, 0, 1, NULL); + cbm_registry_set_swift_signature(r, real_qn, 0, 1, NULL); + + CBMCallArg arg = {.keyword = "path"}; + CBMCall call = {.callee_name = "load", .args = &arg, .arg_count = 1}; + const char *out[4] = {0}; + ASSERT_EQ(cbm_registry_swift_candidates(r, &call, "proj.Caller", NULL, 0, out, 4), 1); + ASSERT_STR_EQ(out[0], real_qn); + 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"); @@ -218,10 +322,10 @@ TEST(registry_label_of) { * method's fenced `#field` twin when the class has one; nothing else moves. */ TEST(registry_value_target_field_twin) { cbm_registry_t *r = cbm_registry_new(); - cbm_registry_add(r, "count", "p.C.count", "Method"); - cbm_registry_add(r, "count", "p.C.count#field", "Field"); - cbm_registry_add(r, "size", "p.C.size", "Method"); - cbm_registry_add(r, "name", "p.C.name", "Field"); + cbm_registry_add(r, "count", "p.C.count", "Method", false); + cbm_registry_add(r, "count", "p.C.count#field", "Field", false); + cbm_registry_add(r, "size", "p.C.size", "Method", false); + cbm_registry_add(r, "name", "p.C.name", "Field", false); ASSERT_STR_EQ(cbm_registry_value_target(r, "p.C.count"), "p.C.count#field"); ASSERT_STR_EQ(cbm_registry_value_target(r, "p.C.size"), "p.C.size"); ASSERT_STR_EQ(cbm_registry_value_target(r, "p.C.name"), "p.C.name"); @@ -237,9 +341,9 @@ TEST(registry_value_target_field_twin) { 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; @@ -258,8 +362,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; @@ -275,7 +379,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); @@ -297,7 +401,7 @@ TEST(resolve_same_module) { * (false only for a this/super receiver). */ TEST(resolve_same_module_only_on_self_receiver) { cbm_registry_t *r = cbm_registry_new(); - cbm_registry_add(r, "get", "proj.pkg.service.get", "Function"); + cbm_registry_add(r, "get", "proj.pkg.service.get", "Function", false); const CBMLanguage js = CBM_LANG_JAVASCRIPT; const char *mod = "proj.pkg.service"; CBMImport axios_import = {.local_name = "axios", .module_path = "axios", .is_default = true}; @@ -457,11 +561,11 @@ TEST(resolve_qualified_receiver_awareness) { const CBMLanguage js = CBM_LANG_JAVASCRIPT; /* 1. Register a Method candidate under Class Service: proj.pkg.Service.get */ - cbm_registry_add(r, "Service", "proj.pkg.Service", "Class"); - cbm_registry_add(r, "get", "proj.pkg.Service.get", "Method"); + cbm_registry_add(r, "Service", "proj.pkg.Service", "Class", false); + cbm_registry_add(r, "get", "proj.pkg.Service.get", "Method", false); /* 2. Register a Module axios */ - cbm_registry_add(r, "axios", "proj.axios", "Module"); + cbm_registry_add(r, "axios", "proj.axios", "Module", false); /* Test Case A: Unrelated receiver that is a known module/type -> name-only * guess, dropped by the member guard */ @@ -513,7 +617,7 @@ TEST(resolve_qualified_receiver_awareness) { /* Test Case F: Function candidate behind an unrelated receiver -> name-only * guess, dropped by the member guard */ /* Register unique Function helper.compute */ - cbm_registry_add(r, "compute", "proj.pkg.helper.compute", "Function"); + cbm_registry_add(r, "compute", "proj.pkg.helper.compute", "Function", false); { cbm_resolution_t res = cbm_registry_resolve(r, "unrelated.compute", "proj.app", NULL, NULL, 0); @@ -530,9 +634,9 @@ TEST(resolve_qualified_receiver_awareness) { * 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 = @@ -573,8 +677,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 = @@ -592,7 +696,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)"); @@ -617,9 +721,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"); @@ -637,7 +741,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"}; @@ -660,9 +764,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"}; @@ -683,7 +787,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"}; @@ -700,10 +804,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"}; @@ -719,7 +823,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 = @@ -733,7 +837,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); @@ -750,7 +854,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); @@ -788,8 +892,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); @@ -812,7 +916,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'); @@ -830,7 +934,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"}; @@ -852,7 +956,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"}; @@ -875,7 +979,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"}; @@ -929,8 +1033,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"}; @@ -946,8 +1050,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 = @@ -961,7 +1065,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); @@ -973,8 +1077,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 = @@ -993,7 +1097,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 = @@ -1018,7 +1122,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); @@ -1032,8 +1136,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); @@ -1047,7 +1151,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"}; @@ -1064,7 +1168,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 = @@ -1173,8 +1277,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. */ @@ -1192,10 +1296,10 @@ TEST(registry_tie_break_is_independent_of_registration_order) { /* Equal kind: 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 = @@ -1205,9 +1309,9 @@ TEST(registry_tie_break_is_independent_of_registration_order) { /* Proximity outranks the tie order: the sibling wins over a shallower stranger. */ cbm_registry_t *proximity = cbm_registry_new(); - cbm_registry_add(proximity, "vnic_rq_free", "proj.lib.vnic_rq_free", "Function"); + cbm_registry_add(proximity, "vnic_rq_free", "proj.lib.vnic_rq_free", "Function", false); cbm_registry_add(proximity, "vnic_rq_free", "proj.drivers.scsi.fnic.vnic_rq.vnic_rq_free", - "Function"); + "Function", false); cbm_resolution_t nr = cbm_registry_resolve(proximity, "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"); @@ -1231,13 +1335,14 @@ TEST(registry_tie_prefers_callable_over_property) { cbm_registry_add( r, "where", "proj.exposed-core.src.main.kotlin.org.jetbrains.exposed.v1.core.AbstractQuery.where", - "Variable"); + "Variable", false); cbm_registry_add(r, "where", "proj.exposed-jdbc.src.main.kotlin.org.jetbrains.exposed.v1.jdbc.Query.where", - "Method"); + "Method", false); /* A deeper method still beats a shallower field: kind decides before order. */ - cbm_registry_add(r, "limit", "proj.exposed-core.Table.limit", "Field"); - cbm_registry_add(r, "limit", "proj.exposed-jdbc.src.main.kotlin.jdbc.Query.limit", "Method"); + cbm_registry_add(r, "limit", "proj.exposed-core.Table.limit", "Field", false); + cbm_registry_add(r, "limit", "proj.exposed-jdbc.src.main.kotlin.jdbc.Query.limit", "Method", + false); const char *caller = "proj.exposed-tests.src.test.kotlin.org.jetbrains.exposed.v1.tests.SelectTests"; @@ -1258,8 +1363,8 @@ TEST(registry_tie_prefers_callable_over_property) { * order alone, which is still a pure function of the candidate set (O9). */ TEST(registry_tie_ignores_nesting_depth) { cbm_registry_t *r = cbm_registry_new(); - cbm_registry_add(r, "filter", "proj.django.template.library.Library.filter", "Method"); - cbm_registry_add(r, "filter", "proj.django.db.models.query.QuerySet.filter", "Method"); + cbm_registry_add(r, "filter", "proj.django.template.library.Library.filter", "Method", false); + cbm_registry_add(r, "filter", "proj.django.db.models.query.QuerySet.filter", "Method", false); cbm_resolution_t f = cbm_registry_resolve(r, "filter", "proj.tests.lookup.tests", NULL, NULL, 0); ASSERT_STR_EQ(f.qualified_name, "proj.django.db.models.query.QuerySet.filter"); @@ -1281,23 +1386,25 @@ TEST(registry_tie_prefers_csharp_implementation_over_ref_stub) { cbm_registry_add_lang( r, "MemoryStream", "proj.src.libraries.System.Runtime.ref.System.Runtime.MemoryStream.MemoryStream", "Method", - CBM_LANG_CSHARP); + CBM_LANG_CSHARP, false); cbm_registry_add_lang( r, "MemoryStream", "proj.src.libraries.System.Private.CoreLib.src.System.IO.MemoryStream.MemoryStream", - "Method", CBM_LANG_CSHARP); + "Method", CBM_LANG_CSHARP, false); /* Equal depth, "ref" sorts before "src" (WithCancellation). */ cbm_registry_add_lang(r, "WithCancellation", "proj.src.libraries.System.Linq.Parallel.ref.System.Linq." "ParallelEnumerable.WithCancellation", - "Method", CBM_LANG_CSHARP); + "Method", CBM_LANG_CSHARP, false); cbm_registry_add_lang(r, "WithCancellation", "proj.src.libraries.System.Linq.Parallel.src.System.Linq." "ParallelEnumerable.WithCancellation", - "Method", CBM_LANG_CSHARP); + "Method", CBM_LANG_CSHARP, false); /* Not C#: a `ref` directory gets no special treatment. */ - cbm_registry_add_lang(r, "resolve", "proj.lib.ref.Resolver.resolve", "Method", CBM_LANG_JAVA); - cbm_registry_add_lang(r, "resolve", "proj.lib.src.Resolver.resolve", "Method", CBM_LANG_JAVA); + cbm_registry_add_lang(r, "resolve", "proj.lib.ref.Resolver.resolve", "Method", CBM_LANG_JAVA, + false); + cbm_registry_add_lang(r, "resolve", "proj.lib.src.Resolver.resolve", "Method", CBM_LANG_JAVA, + false); const char *caller = "proj.src.libraries.System.Security.Cryptography.src.CapiHelper"; cbm_resolution_t ms = cbm_registry_resolve(r, "MemoryStream", caller, NULL, NULL, 0); @@ -1353,10 +1460,10 @@ static cbm_resolution_t xl_resolve(const cbm_registry_t *r, CBMLanguage lang, co TEST(name_guess_vetoed_across_languages) { cbm_registry_t *r = cbm_registry_new(); - cbm_registry_add_lang(r, "get", "builtins.dict.get", "Method", CBM_LANG_PYTHON); - cbm_registry_add_lang(r, "patch", "proj.web.Panel.patch", "Function", CBM_LANG_TSX); - cbm_registry_add_lang(r, "patch", "proj.api.client.patch", "Function", CBM_LANG_PYTHON); - cbm_registry_add(r, "legacy", "proj.x.legacy", "Function"); /* unknown language */ + cbm_registry_add_lang(r, "get", "builtins.dict.get", "Method", CBM_LANG_PYTHON, false); + cbm_registry_add_lang(r, "patch", "proj.web.Panel.patch", "Function", CBM_LANG_TSX, false); + cbm_registry_add_lang(r, "patch", "proj.api.client.patch", "Function", CBM_LANG_PYTHON, false); + cbm_registry_add(r, "legacy", "proj.x.legacy", "Function", false); /* unknown language */ /* JS `app.get`: the only "get" is Python's -> EMPTY, not unique_name. */ cbm_resolution_t res = xl_resolve(r, CBM_LANG_JAVASCRIPT, "app.get", "proj.app"); @@ -1402,10 +1509,11 @@ TEST(name_guess_vetoed_across_languages) { TEST(same_module_vetoed_across_languages) { cbm_registry_t *r = cbm_registry_new(); cbm_registry_add_lang(r, "reducer_worker", "proj.tests.cov.reducer_worker", "Function", - CBM_LANG_PYTHON); + CBM_LANG_PYTHON, false); cbm_registry_add_lang(r, "reducer_worker", "proj.other.reducer_worker", "Function", - CBM_LANG_PYTHON); - cbm_registry_add_lang(r, "stat_child", "proj.tests.cov.stat_child", "Function", CBM_LANG_C); + CBM_LANG_PYTHON, false); + cbm_registry_add_lang(r, "stat_child", "proj.tests.cov.stat_child", "Function", CBM_LANG_C, + false); cbm_resolution_t res = cbm_registry_resolve(r, "reducer_worker", "proj.tests.cov", NULL, NULL, 0); @@ -1562,11 +1670,11 @@ TEST(c_bare_call_does_not_weigh_field_namesakes) { * A bare C call never names a member, so its Field namesakes do not * compete: the function is the unique name it was in v0.11.0. */ cbm_registry_t *r = cbm_registry_new(); - cbm_registry_add_lang(r, "put_page", "proj.include.linux.mm.put_page", "Function", - CBM_LANG_CPP); + cbm_registry_add_lang(r, "put_page", "proj.include.linux.mm.put_page", "Function", CBM_LANG_CPP, + false); cbm_registry_add_lang(r, "put_page", "proj.arch.arm64.include.asm.kvm_pgtable.kvm_pgtable_mm_ops.put_page", - "Field", CBM_LANG_CPP); + "Field", CBM_LANG_CPP, false); const char *mod = "proj.arch.arm64.kernel.elfcore"; cbm_resolution_t res = cbm_registry_resolve_call_lang(r, "put_page", mod, NULL, NULL, 0, CBM_LANG_C); @@ -1581,13 +1689,14 @@ TEST(c_bare_call_does_not_weigh_field_namesakes) { /* A name only members carry is no bare C call target at all -- neither a * struct member nor a C++ method: redis's libc `read()` became the unique * name of a Qt adapter's `read` Method once only Fields were set aside. */ - cbm_registry_add_lang(r, "on_close", "proj.net.sock.sock_ops.on_close", "Field", CBM_LANG_C); + cbm_registry_add_lang(r, "on_close", "proj.net.sock.sock_ops.on_close", "Field", CBM_LANG_C, + false); res = cbm_registry_resolve_call_lang(r, "on_close", mod, NULL, NULL, 0, CBM_LANG_C); ASSERT_TRUE(res.qualified_name == NULL || res.qualified_name[0] == '\0'); cbm_registry_add_lang(r, "read", "proj.deps.hiredis.adapters.qt.RedisQtAdapter.read", "Method", - CBM_LANG_CPP); - cbm_registry_add_lang(r, "read", "proj.src.connection.ConnectionType.read", "Field", - CBM_LANG_C); + CBM_LANG_CPP, false); + cbm_registry_add_lang(r, "read", "proj.src.connection.ConnectionType.read", "Field", CBM_LANG_C, + false); res = cbm_registry_resolve_call_lang(r, "read", mod, NULL, NULL, 0, CBM_LANG_C); ASSERT_TRUE(res.qualified_name == NULL || res.qualified_name[0] == '\0'); /* C++ still reaches the method by name. */ @@ -1652,12 +1761,12 @@ TEST(unique_field_qn_needs_exactly_one_field_of_that_name) { * the same name is not a candidate for a member at all. */ cbm_registry_t *r = cbm_registry_new(); ASSERT_NOT_NULL(r); - cbm_registry_add(r, "only_here", "proj.a.Alpha.only_here", "Field"); - cbm_registry_add(r, "count", "proj.a.Alpha.count", "Field"); - cbm_registry_add(r, "count", "proj.b.Beta.count", "Field"); - cbm_registry_add(r, "close", "proj.cb.Callbacks.close", "Field"); - cbm_registry_add(r, "close", "proj.io.close", "Function"); - cbm_registry_add(r, "lonely_fn", "proj.io.lonely_fn", "Function"); + cbm_registry_add(r, "only_here", "proj.a.Alpha.only_here", "Field", false); + cbm_registry_add(r, "count", "proj.a.Alpha.count", "Field", false); + cbm_registry_add(r, "count", "proj.b.Beta.count", "Field", false); + cbm_registry_add(r, "close", "proj.cb.Callbacks.close", "Field", false); + cbm_registry_add(r, "close", "proj.io.close", "Function", false); + cbm_registry_add(r, "lonely_fn", "proj.io.lonely_fn", "Function", false); const char *only = cbm_registry_unique_field_qn(r, "only_here"); ASSERT_NOT_NULL(only); @@ -1746,7 +1855,7 @@ TEST(generated_member_registers_by_qn_only) { ASSERT_EQ(n, 0); cbm_resolution_t res = cbm_registry_resolve(r, "v.y", "proj.app.Caller", NULL, NULL, 0); ASSERT_NULL(res.qualified_name); - cbm_registry_add(r, "x", "proj.geo.Point.Point.x", "Method"); + cbm_registry_add(r, "x", "proj.geo.Point.Point.x", "Method", false); res = cbm_registry_resolve(r, "v.x", "proj.app.Caller", NULL, NULL, 0); ASSERT_STR_EQ(res.qualified_name, "proj.geo.Point.Point.x"); /* Degenerate input registers nothing. */ @@ -2403,8 +2512,8 @@ TEST(go_suppress_keeps_typed_and_import_aware_matches) { * 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 = @@ -2418,7 +2527,7 @@ TEST(resolve_import_map_alias_with_suffix_hits_method) { TEST(swift_overload_labels_defaults_and_trailing_closure) { cbm_registry_t *r = cbm_registry_new(); ASSERT_NOT_NULL(r); - cbm_registry_add(r, "Service", "proj.Service", "Class"); + cbm_registry_add(r, "Service", "proj.Service", "Class", false); const char *qns[] = { "proj.Service.work(flag:Bool)", "proj.Service.work(name:String)", @@ -2449,7 +2558,7 @@ TEST(swift_overload_labels_defaults_and_trailing_closure) { "combine", "repeated", "repeated", "unlabeled"}; const uint8_t counts[] = {1, 1, 1, 1, 2, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 2, 2, 2, 2}; for (int i = 0; i < 22; i++) { - cbm_registry_add(r, names[i], qns[i], "Method"); + cbm_registry_add(r, names[i], qns[i], "Method", false); uint64_t defaults = (i == 5 || i == 15) ? UINT64_C(1) << 1 : i == 17 || i >= 19 ? UINT64_C(1) : 0; @@ -2460,7 +2569,7 @@ TEST(swift_overload_labels_defaults_and_trailing_closure) { const char *generic_qns[] = {"proj.Service.constrainedVoid>(_:T)", "proj.Service.constrained(_:T)async"}; for (int i = 0; i < 2; i++) { - cbm_registry_add(r, "constrained", generic_qns[i], "Method"); + cbm_registry_add(r, "constrained", generic_qns[i], "Method", false); cbm_registry_set_swift_signature(r, generic_qns[i], 0, 1, NULL); } CBMCallArg generic_arg = {0}; @@ -2468,7 +2577,7 @@ TEST(swift_overload_labels_defaults_and_trailing_closure) { .callee_name = "Service.constrained", .args = &generic_arg, .arg_count = 1}; ASSERT_EQ(cbm_registry_swift_candidates(r, &generic_call, "proj.Caller", NULL, 0, out, 8), 2); const char *nested = "proj.Service.nested,U==()=>Void>(_:T)=>Int"; - cbm_registry_add(r, "nested", nested, "Method"); + cbm_registry_add(r, "nested", nested, "Method", false); cbm_registry_set_swift_signature(r, nested, 0, 1, NULL); generic_call.callee_name = "Service.nested"; ASSERT_EQ(cbm_registry_swift_candidates(r, &generic_call, "proj.Caller", NULL, 0, out, 8), 1); @@ -2476,7 +2585,7 @@ TEST(swift_overload_labels_defaults_and_trailing_closure) { const char *returns[] = {"proj.Service.pick()=>Int", "proj.Service.pick()async=>(A/Item)=>B/Item"}; for (int i = 0; i < 2; i++) { - cbm_registry_add(r, "pick", returns[i], "Method"); + cbm_registry_add(r, "pick", returns[i], "Method", false); cbm_registry_set_swift_signature(r, returns[i], 0, 0, NULL); } CBMCall returned = {.callee_name = "Service.pick"}; @@ -2484,7 +2593,7 @@ TEST(swift_overload_labels_defaults_and_trailing_closure) { const char *malformed[] = {"proj.Service.bad()=>", "proj.Service.bad()=>[Int", "proj.Service.bad()=>Int)"}; for (int i = 0; i < 3; i++) { - cbm_registry_add(r, "bad", malformed[i], "Method"); + cbm_registry_add(r, "bad", malformed[i], "Method", false); cbm_registry_set_swift_signature(r, malformed[i], 0, 0, NULL); } returned.callee_name = "Service.bad"; @@ -2493,7 +2602,7 @@ TEST(swift_overload_labels_defaults_and_trailing_closure) { for (int i = 0; i < 2; i++) { char qn[128]; snprintf(qn, sizeof(qn), "proj.Service.%s(_:A,_:A)=>Bool", operators[i]); - cbm_registry_add(r, operators[i], qn, "Method"); + cbm_registry_add(r, operators[i], qn, "Method", false); cbm_registry_set_swift_signature(r, qn, 0, 2, NULL); CBMCallArg operands[2] = {{0}}; CBMCall operation = {.callee_name = operators[i], .args = operands, .arg_count = 2}; @@ -2657,14 +2766,14 @@ TEST(swift_manifest_root_targets_respect_nested_package_boundaries) { const char *methods[] = {"proj.Root.Ext.Service.run()", "proj.Nested.Ext.Service.run()", "proj.Unknown.Ext.Service.run()"}; for (int i = 0; i < 5; i++) { - cbm_registry_add(r, "Service", qns[i], "Class"); + cbm_registry_add(r, "Service", qns[i], "Class", false); ASSERT_GT(cbm_gbuf_upsert_node(graph, "Class", "Service", qns[i], files[i], 1, 3, i == 0 || i == 2 ? "{\"swift_type_path\":\"Service\"}" : "{\"swift_extension\":\"Service\"}"), 0); } for (int i = 0; i < 3; i++) { - cbm_registry_add(r, "run", methods[i], "Method"); + cbm_registry_add(r, "run", methods[i], "Method", false); cbm_registry_set_swift_signature(r, methods[i], 0, 0, "()"); } cbm_registry_build_swift_owners(r, graph, root, NULL, 0); @@ -2701,12 +2810,12 @@ TEST(swift_receiver_resolution_is_independent_of_registration_order) { for (int reverse = 0; reverse < 2; reverse++) { cbm_registry_t *r = cbm_registry_new(); ASSERT_NOT_NULL(r); - cbm_registry_add(r, "Service", "proj.alpha.Service", "Class"); - cbm_registry_add(r, "Service", "proj.beta.Service", "Class"); + cbm_registry_add(r, "Service", "proj.alpha.Service", "Class", false); + cbm_registry_add(r, "Service", "proj.beta.Service", "Class", false); const char *first = reverse ? beta : alpha; const char *second = reverse ? alpha : beta; - cbm_registry_add(r, "work", first, "Method"); - cbm_registry_add(r, "work", second, "Method"); + cbm_registry_add(r, "work", first, "Method", false); + cbm_registry_add(r, "work", second, "Method", false); cbm_registry_set_swift_signature(r, first, 0, 1, NULL); cbm_registry_set_swift_signature(r, second, 0, 1, NULL); @@ -2725,11 +2834,11 @@ TEST(swift_receiver_resolution_is_independent_of_registration_order) { TEST(swift_multiple_trailing_closures_match_labels) { cbm_registry_t *r = cbm_registry_new(); ASSERT_NOT_NULL(r); - cbm_registry_add(r, "Service", "proj.Service", "Class"); + cbm_registry_add(r, "Service", "proj.Service", "Class", false); const char *one_qn = "proj.Service.handle(completion:()=>Void)"; const char *two_qn = "proj.Service.handle(completion:()=>Void,onError:()=>Void)"; - cbm_registry_add(r, "handle", one_qn, "Method"); - cbm_registry_add(r, "handle", two_qn, "Method"); + cbm_registry_add(r, "handle", one_qn, "Method", false); + cbm_registry_add(r, "handle", two_qn, "Method", false); cbm_registry_set_swift_signature(r, one_qn, 0, 1, NULL); cbm_registry_set_swift_signature(r, two_qn, 0, 2, NULL); @@ -2762,7 +2871,7 @@ TEST(swift_multiple_trailing_closures_match_labels) { * closure can skip an earlier defaulted closure and bind the required one. */ const char *delayed_qn = "proj.Service.delayed(onDismiss:(()=>Void)?,content:()=>Void,onError:(()=>Void)?)"; - cbm_registry_add(r, "delayed", delayed_qn, "Method"); + cbm_registry_add(r, "delayed", delayed_qn, "Method", false); cbm_registry_set_swift_signature(r, delayed_qn, (UINT64_C(1) << 0) | (UINT64_C(1) << 2), 3, NULL); call.callee_name = "Service.delayed"; @@ -2779,9 +2888,9 @@ TEST(swift_multiple_trailing_closures_match_labels) { TEST(swift_repeated_labels_skip_default_before_next_trailing_label) { cbm_registry_t *r = cbm_registry_new(); ASSERT_NOT_NULL(r); - cbm_registry_add(r, "Service", "proj.Service", "Class"); + cbm_registry_add(r, "Service", "proj.Service", "Class", false); const char *qn = "proj.Service.repeated(a:()=>Void,b:()=>Void,a:()=>Void,b:()=>Void)"; - cbm_registry_add(r, "repeated", qn, "Method"); + cbm_registry_add(r, "repeated", qn, "Method", false); cbm_registry_set_swift_signature(r, qn, (UINT64_C(1) << 0) | (UINT64_C(1) << 3), 4, NULL); const char *labels[3] = {NULL, "a", "b"}; CBMCall call = {.callee_name = "Service.repeated", @@ -2800,9 +2909,9 @@ TEST(swift_repeated_labels_skip_default_before_next_trailing_label) { TEST(swift_repeated_labels_do_not_skip_default_past_next_trailing_label) { cbm_registry_t *r = cbm_registry_new(); ASSERT_NOT_NULL(r); - cbm_registry_add(r, "Service", "proj.Service", "Class"); + cbm_registry_add(r, "Service", "proj.Service", "Class", false); const char *qn = "proj.Service.repeated(a:()=>Void,b:()=>Void,a:()=>Void,b:()=>Void)"; - cbm_registry_add(r, "repeated", qn, "Method"); + cbm_registry_add(r, "repeated", qn, "Method", false); cbm_registry_set_swift_signature(r, qn, (UINT64_C(1) << 0) | (UINT64_C(1) << 1), 4, NULL); const char *labels[2] = {NULL, "b"}; CBMCall call = {.callee_name = "Service.repeated", @@ -2822,9 +2931,9 @@ TEST(swift_repeated_labels_do_not_skip_default_past_next_trailing_label) { TEST(swift_variadic_trailing_closures_do_not_precede_parenthesized_argument) { cbm_registry_t *r = cbm_registry_new(); ASSERT_NOT_NULL(r); - cbm_registry_add(r, "Service", "proj.Service", "Class"); + cbm_registry_add(r, "Service", "proj.Service", "Class", false); const char *qn = "proj.Service.f(callbacks:(()=>Void)~,flag:Bool,done:()=>Void)"; - cbm_registry_add(r, "f", qn, "Method"); + cbm_registry_add(r, "f", qn, "Method", false); cbm_registry_set_swift_signature(r, qn, UINT64_C(1) << 1, 3, NULL); CBMCallArg arg = {.keyword = "flag"}; const char *labels[2] = {NULL, "done"}; @@ -2845,17 +2954,17 @@ TEST(swift_variadic_trailing_closures_do_not_precede_parenthesized_argument) { TEST(swift_overloaded_bucket_fails_closed) { cbm_registry_t *r = cbm_registry_new(); ASSERT_NOT_NULL(r); - cbm_registry_add(r, "Service", "proj.Service", "Class"); + cbm_registry_add(r, "Service", "proj.Service", "Class", false); const char *first_qn = "proj.Service.work(flag:Bool)"; - cbm_registry_add(r, "work", first_qn, "Method"); + cbm_registry_add(r, "work", first_qn, "Method", false); cbm_registry_set_swift_signature(r, first_qn, 0, 1, NULL); const char *other = "proj.Other.work(flag:Bool)"; - cbm_registry_add(r, "work", other, "Method"); + cbm_registry_add(r, "work", other, "Method", false); cbm_registry_set_swift_signature(r, other, 0, 1, NULL); char qns[256][64]; for (int i = 1; i < 256; i++) { snprintf(qns[i], sizeof(qns[i]), "proj.Other%d.work(flag:Bool)", i); - cbm_registry_add(r, "work", qns[i], "Method"); + cbm_registry_add(r, "work", qns[i], "Method", false); cbm_registry_set_swift_signature(r, qns[i], 0, 1, NULL); } CBMCall call = {.callee_name = "Service.work"}; @@ -2921,11 +3030,11 @@ TEST(python_symbol_scope_len_preserves_file_identity) { TEST(registry_python_package_scope_is_explicit) { cbm_registry_t *r = cbm_registry_new(); ASSERT_NOT_NULL(r); - cbm_registry_add(r, "Field", "proj.forms.fields.Field", "Class"); - cbm_registry_add(r, "Field", "proj.db.fields.Field", "Class"); - cbm_registry_add(r, "Field", "proj.db.fields.__init__.Field", "Class"); - cbm_registry_add(r, "Field", "proj.db.plain.Field", "Class"); - cbm_registry_add(r, "Field", "proj.db.index.Field", "Class"); + cbm_registry_add(r, "Field", "proj.forms.fields.Field", "Class", false); + cbm_registry_add(r, "Field", "proj.db.fields.Field", "Class", false); + cbm_registry_add(r, "Field", "proj.db.fields.__init__.Field", "Class", false); + cbm_registry_add(r, "Field", "proj.db.plain.Field", "Class", false); + cbm_registry_add(r, "Field", "proj.db.index.Field", "Class", false); const char *keys[] = {"forms"}; const char *vals[] = {"proj.forms"}; const char *module = "proj.db.fields.__init__"; @@ -2957,11 +3066,11 @@ TEST(registry_python_package_scope_is_explicit) { TEST(registry_python_scope_keeps_import_precedence_and_raw_scoring) { cbm_registry_t *r = cbm_registry_new(); ASSERT_NOT_NULL(r); - cbm_registry_add(r, "Field", "proj.vendor.Field", "Class"); - cbm_registry_add(r, "Field", "proj.pkg.Field", "Class"); - cbm_registry_add(r, "Field", "proj.pkg.__init__.Field", "Class"); - cbm_registry_add(r, "choose", "proj.pkg.__init__.child.choose", "Function"); - cbm_registry_add(r, "choose", "proj.pkg.peer.choose", "Function"); + cbm_registry_add(r, "Field", "proj.vendor.Field", "Class", false); + cbm_registry_add(r, "Field", "proj.pkg.Field", "Class", false); + cbm_registry_add(r, "Field", "proj.pkg.__init__.Field", "Class", false); + cbm_registry_add(r, "choose", "proj.pkg.__init__.child.choose", "Function", false); + cbm_registry_add(r, "choose", "proj.pkg.peer.choose", "Function", false); const char *keys[] = {"Field"}; const char *vals[] = {"proj.vendor.Field"}; bool seeded = cbm_registry_size(r) == 5; @@ -2985,14 +3094,14 @@ TEST(registry_python_scope_keeps_import_precedence_and_raw_scoring) { TEST(registry_python_scope_cache_modes_and_raw_lineage) { cbm_registry_t *r = cbm_registry_new(); ASSERT_NOT_NULL(r); - cbm_registry_add(r, "Entry", "proj.pkg.Entry", "Function"); - cbm_registry_add(r, "Entry", "proj.pkg.__init__.Entry", "Function"); - cbm_registry_add(r, "Reject", "proj.pkg.Reject", "Table"); - cbm_registry_add(r, "Reject", "proj.pkg.__init__.Reject", "Function"); - cbm_registry_add(r, "Accept", "proj.pkg.Accept", "Function"); - cbm_registry_add(r, "Accept", "proj.pkg.__init__.Accept", "Table"); - cbm_registry_add(r, "Relation", "proj.pkg.Relation", "View"); - cbm_registry_add(r, "Relation", "proj.pkg.__init__.Relation", "Table"); + cbm_registry_add(r, "Entry", "proj.pkg.Entry", "Function", false); + cbm_registry_add(r, "Entry", "proj.pkg.__init__.Entry", "Function", false); + cbm_registry_add(r, "Reject", "proj.pkg.Reject", "Table", false); + cbm_registry_add(r, "Reject", "proj.pkg.__init__.Reject", "Function", false); + cbm_registry_add(r, "Accept", "proj.pkg.Accept", "Function", false); + cbm_registry_add(r, "Accept", "proj.pkg.__init__.Accept", "Table", false); + cbm_registry_add(r, "Relation", "proj.pkg.Relation", "View", false); + cbm_registry_add(r, "Relation", "proj.pkg.__init__.Relation", "Table", false); bool seeded = cbm_registry_size(r) == 8; bool correct = true; /* Each cache lifetime has one fixed module/import map. Alternate modes in @@ -3070,12 +3179,12 @@ TEST(registry_python_scope_complete_candidate_boundaries) { int tn = snprintf(target, sizeof(target), "%s.%s", scope, name); bool shape = mn > 0 && (size_t)mn < sizeof(module) && tn > 0 && (size_t)tn == lengths[i] && (size_t)tn < sizeof(target); - cbm_registry_add(r, name, target, "Function"); + cbm_registry_add(r, name, target, "Function", false); if (lengths[i] > 511) { char truncated[512]; memcpy(truncated, target, sizeof(truncated) - 1); truncated[sizeof(truncated) - 1] = '\0'; - cbm_registry_add(r, "truncated_decoy", truncated, "Function"); + cbm_registry_add(r, "truncated_decoy", truncated, "Function", false); } const char *canonical = registry_scope_canonical(r, name, target); cbm_resolution_t python = @@ -3102,7 +3211,7 @@ TEST(registry_python_scope_qualified_terminal_bucket_uncapped) { char qn[80]; int n = snprintf(qn, sizeof(qn), "proj.noise%d.perform", i); shape = n > 0 && (size_t)n < sizeof(qn) && shape; - cbm_registry_add(r, "perform", qn, "Method"); + cbm_registry_add(r, "perform", qn, "Method", false); } char scope[1024]; memcpy(scope, "proj.", 5); @@ -3120,7 +3229,7 @@ TEST(registry_python_scope_qualified_terminal_bucket_uncapped) { int pn = snprintf(prefixed, sizeof(prefixed), "receiver.%s", names[i]); shape = tn > 0 && (size_t)tn < sizeof(target) && pn > 0 && (size_t)pn < sizeof(prefixed) && shape; - cbm_registry_add(r, "perform", target, "Method"); + cbm_registry_add(r, "perform", target, "Method", false); const char *canonical = registry_scope_canonical(r, "perform", target); cbm_resolution_t direct = cbm_registry_resolve_lang(r, names[i], module, NULL, NULL, 0, CBM_LANG_PYTHON); @@ -3154,13 +3263,13 @@ TEST(registry_python_function_local_is_exact_only_issue1548) { {"Inner", "proj.web.Outer.Inner", "Class"}, }; for (size_t i = 0; i < sizeof(defs) / sizeof(defs[0]); i++) { - cbm_registry_add_lang_in_module(r, defs[i][0], defs[i][1], defs[i][2], CBM_LANG_PYTHON, - web); + cbm_registry_add_lang_in_module(r, defs[i][0], defs[i][1], defs[i][2], CBM_LANG_PYTHON, web, + false); } cbm_registry_add_lang_in_module(r, "f", "proj.ts.f", "Function", CBM_LANG_TYPESCRIPT, - strlen("proj.ts")); + strlen("proj.ts"), false); cbm_registry_add_lang_in_module(r, "g", "proj.ts.f.g", "Function", CBM_LANG_TYPESCRIPT, - strlen("proj.ts")); + strlen("proj.ts"), false); static const char *const local[] = {"proj.web.create_app.index", "proj.web.create_app.Settings", "proj.web.create_app.Settings.value"}; @@ -3212,7 +3321,7 @@ TEST(registry_resolve_lexical_walks_enclosing_defs_issue1548) { }; for (size_t i = 0; i < sizeof(defs) / sizeof(defs[0]); i++) { cbm_registry_add_lang_in_module(r, defs[i][0], defs[i][1], defs[i][2], CBM_LANG_PYTHON, - strlen("proj.m")); + strlen("proj.m"), false); } /* In method Tree.visit, `visit` is its nested def; so it is inside that @@ -3254,17 +3363,17 @@ TEST(registry_python_local_bound_at_own_file_issue1548) { cbm_registry_t *r = cbm_registry_new(); if (order == 0) { cbm_registry_add_lang_in_module(r, "layout", "proj.pkg.layout", "Function", - CBM_LANG_PYTHON, init_prefix); + CBM_LANG_PYTHON, init_prefix, false); } cbm_registry_add_lang_in_module(r, "Layout", "proj.pkg.layout.Layout", "Class", - CBM_LANG_PYTHON, mod_prefix); + CBM_LANG_PYTHON, mod_prefix, false); cbm_registry_add_lang_in_module(r, "create", "proj.pkg.layout.Layout.create", "Method", - CBM_LANG_PYTHON, mod_prefix); + CBM_LANG_PYTHON, mod_prefix, false); cbm_registry_add_lang_in_module(r, "make", "proj.pkg.layout.make", "Function", - CBM_LANG_PYTHON, mod_prefix); + CBM_LANG_PYTHON, mod_prefix, false); if (order == 1) { cbm_registry_add_lang_in_module(r, "layout", "proj.pkg.layout", "Function", - CBM_LANG_PYTHON, init_prefix); + CBM_LANG_PYTHON, init_prefix, false); } static const char *const nameable[][2] = {{"Layout", "proj.pkg.layout.Layout"}, {"create", "proj.pkg.layout.Layout.create"}, @@ -3332,6 +3441,11 @@ 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_test_double_path_table); + RUN_TEST(registry_swift_overload_choice_uses_stored_verdicts); RUN_TEST(registry_label_of); RUN_TEST(registry_value_target_field_twin); RUN_TEST(registry_find_by_name); diff --git a/tests/windows/mcp_stdio.py b/tests/windows/mcp_stdio.py index 218e9a27f..d582d4c10 100644 --- a/tests/windows/mcp_stdio.py +++ b/tests/windows/mcp_stdio.py @@ -25,6 +25,8 @@ class McpServer: self._id = 0 self.proc = None self._stderr = [] + self._stderr_thread = None + self._stderr_eof = False env = dict(os.environ) if cache_dir: env["CBM_CACHE_DIR"] = cache_dir # isolate the graph DB location @@ -52,12 +54,27 @@ class McpServer: try: for line in self.proc.stderr: self._stderr.append(line.decode("utf-8", "replace")) + self._stderr_eof = True except Exception: pass def stderr_text(self): return "".join(self._stderr) + def stderr_complete(self, timeout): + """Wait for the stderr reader to reach EOF; True once it has. + + Only then is stderr_text() the whole capture. False means the reader + is still running (some process still holds the pipe) or stopped on a + read error, so the text may be partial: a caller that asserts on + stderr must treat False as a setup failure, never as a verdict. The + timeout is a liveness backstop, not part of any verdict. + """ + if self._stderr_thread is None: + return False + self._stderr_thread.join(timeout) + return not self._stderr_thread.is_alive() and self._stderr_eof + def _send(self, obj): data = json.dumps(obj, ensure_ascii=False).encode("utf-8") self.proc.stdin.write(data + b"\n") @@ -148,6 +165,7 @@ class McpServer: self.proc.kill() except Exception: pass + self._stderr_thread.join(timeout=10) def wait_projects_with_stats(server, timeout=90.0, poll=1.0): diff --git a/tests/windows/test_mcp_stdio.py b/tests/windows/test_mcp_stdio.py new file mode 100644 index 000000000..7b7eee617 --- /dev/null +++ b/tests/windows/test_mcp_stdio.py @@ -0,0 +1,127 @@ +"""Regression guard for clean Windows MCP stdio startup (#654). + +A successful initialize + tools/list exchange must leave nothing on stderr but +the server's own structured log lines. #654 reported the OS "path not found" +message leaking to stderr on startup, in English and on a Spanish Windows. Those +two messages are named cases; any other non-empty line that is not a structured +`level= msg=` record fails as well, so the same leak on a machine +in any other language is caught without listing its translation. + +The verdict is read only after the stderr reader has reached EOF. A reader that +is still running after the server exited means the capture may be partial, and +that is a setup failure, never a pass. + +Exit codes follow the guard contract of scripts/test-windows.ps1: +0 = GREEN, 1 = RED (regression), 2 = SETUP / precondition. +""" + +import os +import re +import subprocess +import sys +import tempfile + +from mcp_stdio import McpServer + +# The OS path-not-found message as reported in #654 (English and Spanish +# Windows). Named so a RED says exactly which known leak came back. +KNOWN_PATH_ERRORS = ( + "The system cannot find the path specified.", + "El sistema no puede encontrar la ruta especificada.", +) + +# The text-format record cbm_log writes (src/foundation/log.c): +# "level= msg= key=value ...". +STRUCTURED_LOG_LINE = re.compile(r"level=[a-z]+ msg=\S") + +# Liveness backstop for the stderr reader. The server has already exited when +# this is waited on, so the reader is at EOF unless another process still holds +# the pipe. Expiry is a SETUP failure; it never decides PASS or FAIL. +STDERR_EOF_BACKSTOP_S = 60 + + +def classify_stderr(text): + """Return (known, raw): the named #654 messages found, and every other + non-empty stderr line that is not a structured log record.""" + known = [message for message in KNOWN_PATH_ERRORS if message in text] + raw = [] + for line in text.splitlines(): + line = line.strip() + if not line or STRUCTURED_LOG_LINE.match(line): + continue + if any(message in line for message in KNOWN_PATH_ERRORS): + continue + raw.append(line) + return known, raw + + +def main(): + if os.name != "nt": + print("SKIP: Windows-only MCP stdio guard") + return 2 + + binary = sys.argv[1] if len(sys.argv) > 1 else os.environ.get("CBM_TEST_BINARY") + if not binary: + print("SETUP FAIL: CBM_TEST_BINARY is required", file=sys.stderr) + return 2 + + with tempfile.TemporaryDirectory(prefix="cbm-mcp-stdio-") as work: + cache = os.path.join(work, "cache") + runtime = os.path.join(work, "runtime") + os.makedirs(cache) + os.makedirs(runtime) + # The verdict reads the text log shape, so select it explicitly: an + # inherited CBM_LOG_FORMAT=json must not turn every record into a raw + # line. + server = McpServer( + binary, + cache_dir=cache, + extra_env={"CBM_RUNTIME_DIR": runtime, "CBM_LOG_FORMAT": "text"}, + ) + try: + with server: + server.initialize(timeout=30) + server.tools_list(timeout=30) + finally: + stop = subprocess.run( + [binary, "daemon", "stop"], + env=server.env, + capture_output=True, + timeout=30, + ) + if stop.returncode != 0: + detail = (stop.stdout + stop.stderr).decode("utf-8", "replace") + raise RuntimeError( + "daemon cleanup failed (%d): %s" % (stop.returncode, detail.strip()) + ) + + if not server.stderr_complete(STDERR_EOF_BACKSTOP_S): + print( + "SETUP FAIL: the stderr reader did not reach EOF after the server " + "exited; refusing to judge a possibly partial capture", + file=sys.stderr, + ) + return 2 + + known, raw = classify_stderr(server.stderr_text()) + if known or raw: + if known: + print( + "FAIL: startup leaked an OS path error to stderr: " + ", ".join(known), + file=sys.stderr, + ) + if raw: + print( + "FAIL: startup wrote %d stderr line(s) that are not structured " + "level= log records:" % len(raw), + file=sys.stderr, + ) + for line in raw: + print(" " + line, file=sys.stderr) + return 1 + print("PASS: successful MCP startup emitted only structured log lines on stderr") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())