mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-11 09:25:35 +08:00
Why: the test runner isolated HOME and CBM_CACHE_DIR but not
CBM_RUNTIME_DIR, and neither of the first two moves the daemon
rendezvous. Its key is a constant product hash (cbm_daemon_rendezvous_key,
"c888acc3ae367a1e"), and with no explicit parent and CBM_RUNTIME_DIR unset
cbm_daemon_bootstrap_endpoint_new falls back to the account-wide default
parent (/private/tmp on macOS, /tmp on Linux, LocalAppData on Windows).
Every endpoint a test resolved without an explicit parent was therefore
byte-identical to the developer's live daemon socket
(/private/tmp/cbm-daemon-<uid>/cbm-c888acc3ae367a1e.sock), whatever HOME
said.
The only in-runner product caller is CLI activation
(cli_activation_production_context_init, reached from cbm_cmd_install,
cbm_cmd_uninstall and update). Only a process-global seam kept it off the
default: SUITE(cli) sets that seam on entry and clears it on exit. On the
default endpoint, activation admits into the live daemon's cohort. When
the cohort is ours or busy, or the active cache fingerprint is unreadable,
activation prints "Stopping active CBM sessions" and drains them. That is
the same class of failure as an old binary's install, which stopped a
user daemon and six MCP sessions. One test outside SUITE(cli), or one
fixture that drops the variable, was enough to reach it.
daemon_bootstrap_runtime_dir_env_relocates_rendezvous did exactly that:
it restored by unsetting CBM_RUNTIME_DIR.
Fix:
- tests/test_main.c: tf_setup_cache_sentinel creates a fresh per-run
runtime parent (th_secure_runtime_parent_new "run", under /tmp on POSIX
and LocalAppData on Windows) and sets CBM_RUNTIME_DIR to it for the
whole run. It replaces any inherited value, the rule
scripts/test-runtime.sh applies to the shell harness. The existing
atexit cleanup removes the directory.
- src/daemon/bootstrap.c, CBM_ENABLE_TEST_SEAMS only: once a test runner
arms the guard, an endpoint request with no explicit parent and no
CBM_RUNTIME_DIR is refused with a stderr diagnostic instead of
resolving the default. No product binary arms it.
- tests/repro/repro_main.c arms the same guard. No reproduction needs the
rendezvous.
- tests/test_daemon_bootstrap.c: the relocation test restores the value
it found instead of unsetting it.
- tests/test_helpers.h: th_secure_runtime_base() names the anchor that
th_secure_runtime_parent_new already used.
Proof:
- Before, on origin/main 1f9b4db1: runner_isolation is RED 3/3 at its
first assertion (CBM_RUNTIME_DIR is NULL during the run).
- After: green 3/3. The endpoint resolved through the product resolver
lies inside /private/tmp/cbm-runtime-run-XXXXXX/, never at
/private/tmp/cbm-daemon-<uid>. All 43 activation-guard scopes logged
by the suites below are fixture or cli-suite directories, and the
guard never fired. The per-run directory is gone after exit, and the
live daemon and its runtime directory are unchanged.
- Revert: dropping the setenv gives RED (value is NULL). Disarming the
guard gives RED (refused_without).
Tests:
- runner_isolation: runner_isolation_daemon_runtime_dir_is_private,
runner_isolation_refuses_default_daemon_runtime
- Green on macOS: cli (328), mcp + mcp_mutation_guard (344),
daemon_runtime + daemon_application (106), daemon + daemon_version +
daemon_frontend + daemon_bootstrap + daemon_ipc (117), runner_isolation
+ activation_transaction + version_cohort + project_lock +
index_supervisor + private_file_lock + lock_registry (73). All skips
are existing Windows- or Linux-only platform skips.
- make -f Makefile.cbm lint-ci passes. test-repro-runner builds.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
230 lines
7.2 KiB
C
230 lines
7.2 KiB
C
/*
|
|
* test_helpers.h — Cross-platform test fixture helpers.
|
|
*
|
|
* Replaces system("rm -rf"), system("mkdir -p && echo >"), chmod(),
|
|
* and hardcoded /tmp/ paths with portable C implementations.
|
|
* Uses compat.h/compat_fs.h underneath — same abstraction layer
|
|
* as production code.
|
|
*
|
|
* All functions are static inline to avoid linker issues when
|
|
* included from multiple test files.
|
|
*/
|
|
#ifndef TEST_HELPERS_H
|
|
#define TEST_HELPERS_H
|
|
|
|
#include "../src/foundation/compat.h"
|
|
#include "../src/foundation/compat_fs.h"
|
|
#include "../src/foundation/platform.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/stat.h>
|
|
#ifdef _WIN32
|
|
#include "../src/foundation/win_utf8.h"
|
|
#endif
|
|
|
|
/* ── Path building ────────────────────────────────────────────── */
|
|
|
|
/* Build a path from base + relative. Uses a static buffer — not reentrant.
|
|
* Usage: th_write_file(TH_PATH(base, "src/main.go"), "content"); */
|
|
#define TH_PATH(base, rel) th_path_join(base, rel)
|
|
|
|
static inline const char *th_path_join(const char *base, const char *rel) {
|
|
static char buf[4][1024];
|
|
static int idx = 0;
|
|
int i = idx;
|
|
idx = (idx + 1) & 3;
|
|
snprintf(buf[i], sizeof(buf[i]), "%s/%s", base, rel);
|
|
return buf[i];
|
|
}
|
|
|
|
/* ── File writing ─────────────────────────────────────────────── */
|
|
|
|
/* Write content to a file, creating parent directories as needed. */
|
|
static inline int th_write_file(const char *path, const char *content) {
|
|
/* Create parent directories */
|
|
char dir[1024];
|
|
snprintf(dir, sizeof(dir), "%s", path);
|
|
char *last_slash = strrchr(dir, '/');
|
|
#ifdef _WIN32
|
|
char *last_bslash = strrchr(dir, '\\');
|
|
if (last_bslash && (!last_slash || last_bslash > last_slash)) {
|
|
last_slash = last_bslash;
|
|
}
|
|
#endif
|
|
if (last_slash) {
|
|
*last_slash = '\0';
|
|
cbm_mkdir_p(dir, 0755);
|
|
}
|
|
|
|
FILE *f = cbm_fopen(path, "wb");
|
|
if (!f) {
|
|
return -1;
|
|
}
|
|
if (content && content[0]) {
|
|
fputs(content, f);
|
|
}
|
|
fclose(f);
|
|
return 0;
|
|
}
|
|
|
|
/* Append content to a file. */
|
|
static inline int th_append_file(const char *path, const char *content) {
|
|
FILE *f = cbm_fopen(path, "ab");
|
|
if (!f) {
|
|
return -1;
|
|
}
|
|
if (content && content[0]) {
|
|
fputs(content, f);
|
|
}
|
|
fclose(f);
|
|
return 0;
|
|
}
|
|
|
|
/* ── Directory creation ───────────────────────────────────────── */
|
|
|
|
/* Create a directory and all parents. Returns 0 on success. */
|
|
static inline int th_mkdir_p(const char *path) {
|
|
return cbm_mkdir_p(path, 0755) ? 0 : -1;
|
|
}
|
|
|
|
/* ── Recursive directory removal ──────────────────────────────── */
|
|
|
|
/* Git for Windows marks loose objects read-only. Match rm -rf semantics in
|
|
* test cleanup without broadening production cbm_unlink behavior. */
|
|
static inline int th_unlink_force(const char *path) {
|
|
int result = cbm_unlink(path);
|
|
#ifdef _WIN32
|
|
if (result != 0) {
|
|
wchar_t *wide = cbm_path_to_wide(path);
|
|
DWORD attributes = wide ? GetFileAttributesW(wide) : INVALID_FILE_ATTRIBUTES;
|
|
if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_READONLY) != 0U &&
|
|
SetFileAttributesW(wide, attributes & ~((DWORD)FILE_ATTRIBUTE_READONLY))) {
|
|
result = cbm_unlink(path);
|
|
}
|
|
free(wide);
|
|
}
|
|
#endif
|
|
return result;
|
|
}
|
|
|
|
/* Remove a file or directory tree recursively. Cross-platform rm -rf. */
|
|
static inline int th_rmtree(const char *path) {
|
|
/* The platform wrappers preserve UTF-8 and add the extended-length prefix
|
|
* on Windows; narrow CRT stat() silently treats paths beyond MAX_PATH as
|
|
* absent and leaves their parents nonempty. */
|
|
if (!cbm_file_exists(path)) {
|
|
return 0; /* doesn't exist — success */
|
|
}
|
|
|
|
if (!cbm_is_dir(path)) {
|
|
return th_unlink_force(path);
|
|
}
|
|
|
|
/* Directory — recurse into children */
|
|
cbm_dir_t *d = cbm_opendir(path);
|
|
if (!d) {
|
|
return -1;
|
|
}
|
|
|
|
cbm_dirent_t *entry;
|
|
int rc = 0;
|
|
while ((entry = cbm_readdir(d)) != NULL) {
|
|
if (strcmp(entry->name, ".") == 0 || strcmp(entry->name, "..") == 0) {
|
|
continue;
|
|
}
|
|
char child[1024];
|
|
snprintf(child, sizeof(child), "%s/%s", path, entry->name);
|
|
if (entry->is_dir) {
|
|
if (th_rmtree(child) != 0) {
|
|
rc = -1;
|
|
}
|
|
} else {
|
|
if (th_unlink_force(child) != 0) {
|
|
rc = -1;
|
|
}
|
|
}
|
|
}
|
|
cbm_closedir(d);
|
|
if (cbm_rmdir(path) != 0) {
|
|
rc = -1;
|
|
}
|
|
return rc;
|
|
}
|
|
|
|
/* ── Temp directory creation ──────────────────────────────────── */
|
|
|
|
/* Create a temporary directory. Returns static buffer with path.
|
|
* Pattern: prefix is used as part of the dirname. */
|
|
static inline char *th_mktempdir(const char *prefix) {
|
|
static char buf[256];
|
|
#ifdef _WIN32
|
|
const char *tmp = getenv("TEMP");
|
|
if (!tmp)
|
|
tmp = getenv("TMP");
|
|
if (!tmp)
|
|
tmp = "C:\\Temp";
|
|
snprintf(buf, sizeof(buf), "%s\\%s_XXXXXX", tmp, prefix);
|
|
#else
|
|
snprintf(buf, sizeof(buf), "/tmp/%s_XXXXXX", prefix);
|
|
#endif
|
|
if (!cbm_mkdtemp(buf)) {
|
|
return NULL;
|
|
}
|
|
return buf;
|
|
}
|
|
|
|
/* Runtime IPC validates the complete Windows directory ancestry, so an MSYS2
|
|
* TEMP rooted under C:/msys64/tmp is intentionally unsuitable even when the
|
|
* leaf made by cbm_mkdtemp() has a private DACL. Keep these security-sensitive
|
|
* fixtures under LocalAppData on Windows; preserve the ordinary temporary-root
|
|
* behavior on POSIX. */
|
|
static inline const char *th_secure_runtime_base(void) {
|
|
#ifdef _WIN32
|
|
return cbm_app_local_dir();
|
|
#else
|
|
return cbm_tmpdir();
|
|
#endif
|
|
}
|
|
|
|
static inline bool th_secure_runtime_parent_new(char *out, size_t out_cap, const char *tag) {
|
|
if (!out || out_cap == 0 || !tag || !tag[0]) {
|
|
return false;
|
|
}
|
|
const char *base = th_secure_runtime_base();
|
|
if (!base || !base[0]) {
|
|
out[0] = '\0';
|
|
return false;
|
|
}
|
|
int written = snprintf(out, out_cap, "%s/cbm-runtime-%s-XXXXXX", base, tag);
|
|
if (written <= 0 || (size_t)written >= out_cap) {
|
|
out[0] = '\0';
|
|
return false;
|
|
}
|
|
return cbm_mkdtemp(out) != NULL;
|
|
}
|
|
|
|
/* ── File permissions (no-op on Windows) ──────────────────────── */
|
|
|
|
/* Make a file executable. On Windows this is a no-op (all files are
|
|
* "executable" if they have the right extension). */
|
|
static inline void th_make_executable(const char *path) {
|
|
#ifndef _WIN32
|
|
chmod(path, 0755);
|
|
#else
|
|
(void)path;
|
|
#endif
|
|
}
|
|
|
|
/* ── Cleanup helper ───────────────────────────────────────────── */
|
|
|
|
/* Remove a temp directory tree. Safe to call with NULL. */
|
|
static inline void th_cleanup(const char *path) {
|
|
if (path && path[0]) {
|
|
th_rmtree(path);
|
|
}
|
|
}
|
|
|
|
#endif /* TEST_HELPERS_H */
|