Merge origin/main into feat/graph-gate-contract

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel committed 2026-11-01 07:17:42 +01:00
commit 9aa44426f7
36 files changed
+4003 -129

No files matched your search

+20
View File
@@ -696,6 +696,26 @@ MIMALLOC_CFLAGS_TEST = -std=c11 -g -O1 -w \
-Ivendored/mimalloc/include \
-Ivendored/mimalloc/src \
-DMI_OVERRIDE=0 $(MIMALLOC_WIN_DEFINE)
# MI_WIN_INIT_USE_FLS (MinGW TEST build only) is an EXPERIMENT (R32) for the
# Windows test leg's nondeterministic crash: 1-3 suites die in their first
# test with 0xC0000005 and no sanitizer report, seen only on trees with
# mimalloc 3.5.3. 3.5.3 is the first version whose loader hooks are live on
# MinGW: TLS callbacks in .CRT$XLB/.CRT$XLY run _mi_auto_process_init under the
# loader lock and _mi_thread_done on every thread detach. With FLS,
# src/prim/windows/prim.c compiles none of those hooks; process init runs once,
# from the constructor in src/prim/prim.c (no double init), and thread-done
# goes through a fiber-local destructor. MI_DEBUG=1 is what FLS needs to run at
# all: the first thread init (src/init.c) associates the main theap before the
# FLS key exists, which trips an internal assertion in prim.c and aborts every
# MI_DEBUG=2 process at startup. Level 1 keeps the fill patterns, padding and
# double-free checks and drops only the internal assertions. The production
# allocator (MIMALLOC_CFLAGS) is unchanged. Drop or promote this when the
# experiment ends.
MIMALLOC_WIN_TEST_DEFINE :=
ifeq ($(IS_MINGW),yes)
MIMALLOC_WIN_TEST_DEFINE := -DMI_WIN_INIT_USE_FLS=1 -DMI_DEBUG=1
MIMALLOC_CFLAGS_TEST += $(MIMALLOC_WIN_TEST_DEFINE)
endif
# sqlite3 (vendored amalgamation — compiled ourselves for ASan instrumentation)
# SQLITE_ENABLE_FTS5: enables the FTS5 full-text search extension used by the
+16 -1
View File
@@ -719,7 +719,7 @@ overwrite user-modified agents.
| Agent | Activation | MCP config | Durable context / augmentation |
|-------|------------|------------|--------------------------------|
| Claude Code | Detected | `~/.claude.json` | Skill + three exact-tool graph agents; `SessionStart`, `SubagentStart`, non-blocking `PreToolUse` for `Grep`/`Glob`/`Bash`, post-`Read` coverage, and a drift note after repeated searches without a graph call |
| Codex CLI | Detected | `$CODEX_HOME/config.toml` | Managed `AGENTS.md` activation pointer, skill, three read-only agents; `SessionStart` + `SubagentStart`, `PreToolUse` graph lookup for `Bash` searches |
| Codex CLI | Detected | `$CODEX_HOME/config.toml` | Managed `AGENTS.md` activation pointer, `~/.agents/skills/codebase-memory/SKILL.md`, three read-only agents; `SessionStart` + `SubagentStart`, `PreToolUse` graph lookup for `Bash` searches |
| Gemini CLI | Detected | `.gemini/settings.json` | `GEMINI.md`, three explicit read/graph-tool subagents; `BeforeTool`, `AfterTool` `read_file` coverage, and `SessionStart` |
| Zed | Detected | platform `settings.json` (JSONC) | `AGENTS.md` + shared skill |
| OpenCode | Detected | `$OPENCODE_CONFIG` or resolved global config | `AGENTS.md`, skill, three deny-by-default read-only agents; plugin adds grep/glob graph lookup, post-`read` coverage, first-tool-result session context, and post-compaction reinjection |
@@ -768,6 +768,11 @@ For Codex, install keeps only a tiny managed activation pointer in global
`$CODEX_HOME/AGENTS.md`; all detailed behavior lives in the installed `codebase-memory` skill.
Fresh installs create the pointer, upgrades replace the legacy full managed block while preserving
all user-owned bytes, and uninstall removes only the managed pointer.
Skill placement follows the cross-client user skill root and does not follow
`CODEX_HOME`. On upgrade, the installer migrates an installer-owned legacy
`$CODEX_HOME/skills/codebase-memory/SKILL.md` only after verifying the canonical
copy. Modified or foreign files are preserved; `--force` explicitly authorizes
replacement before migration.
### Sessions, compaction, and subagents
@@ -958,6 +963,16 @@ mode. `index_status` keeps describing the published graph and its freshness.
`manage_adr` query modes (`get` and `sections`) use the server's cached query store so they can proceed while a same-project reindex is running. If another process publishes a replacement store during reindexing, they can return the pre-publication ADR until idle eviction refreshes that cache. Updates remain serialized through the project mutation guard.
`trace_path` exposes the start nodes' stored `self_recursive` (direct recursion)
and `recursive` (direct recursion or a call-graph cycle) signals in both tree and
JSON output. Across multiple start nodes, a flag is `true` if any records true,
`false` if all record false, and `null` otherwise (including missing legacy
metadata). These are static-analysis signals, not counts of resolved self-calls.
Static `CALLS` indexing omits direct self-call edges; traces that include `CALLS` state
`self_call_policy: omitted`. Caller/callee totals still count graph-reachable
nodes, so a recursive function can have zero callees. Recursion metadata remains
visible on continuation pages and when an output budget prevents rows from fitting.
## Graph Data Model
### Node Labels
+3 -2
View File
@@ -289,8 +289,9 @@ typedef struct {
* qualified_name (base QN = the first qn_sig_off bytes); 0 = no suffix.
* Always 0 until a language enables its callable_identity mode. */
uint32_t qn_sig_off;
/* C# only: declared namespace of a TOP-LEVEL type (NULL for nested types,
* the global namespace and every other language). Per type, because the
/* C# only: declared namespace of a TOP-LEVEL type, "" for the global
* namespace (proven only by a clean parse), NULL for nested types, an
* unknown namespace and every other language. Per type, because the
* file-level namespace_name records only a file's first namespace. */
const char *decl_namespace;
CBMTestDefinitionRole test_role; /* NONE preserves legacy helper/test-file semantics */
+13 -4
View File
@@ -8340,9 +8340,15 @@ static const char *cs_namespace_decl_name(CBMArena *a, TSNode ns, const char *so
/* Declared namespace of a C# type declaration: every enclosing namespace
* block (outer to inner) under an optional file-scoped namespace, which the
* grammar may attach either as an ancestor or as a preceding sibling of the
* compilation unit's members. NULL = global namespace. The file-level
* namespace_name keeps only the FIRST namespace of a file, which mislabels
* every type of a multi-namespace file (#2120). */
* compilation unit's members. "" = the global namespace, NULL = unknown. The
* file-level namespace_name keeps only the FIRST namespace of a file, which
* mislabels every type of a multi-namespace file (#2120).
*
* "No namespace found" proves the global namespace only for a clean parse:
* error recovery can dissolve a namespace declaration around a construct the
* grammar does not know (a `ref partial struct` with members does it), leaving
* the types after it parented straight under the compilation unit. Calling
* those global would let them hide every imported type of the same name. */
static const char *cs_type_decl_namespace(CBMArena *a, TSNode node, const char *source) {
const char *acc = NULL;
bool file_scoped = false;
@@ -8376,7 +8382,10 @@ static const char *cs_type_decl_namespace(CBMArena *a, TSNode node, const char *
}
}
}
return acc;
if (acc) {
return acc;
}
return ts_node_has_error(top) ? NULL : "";
}
static void extract_class_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec) {
+95 -22
View File
@@ -51,6 +51,10 @@
#define CS_USING_INITIAL_CAP 16
#define CS_NAMESPACE_INITIAL_CAP 8
#define CS_LSP_PARENT_WALK_MAX 32
/* Nested-type qualifier walk depth: one level per type nesting step of `A.B.C`
* (a namespace-qualified top-level type binds without recursing), so 8 covers
* real nesting while a cyclic alias such as `using A = A.B;` stops after 8. */
#define CS_QUALIFIER_MAX_DEPTH 8
extern const TSLanguage *tree_sitter_c_sharp(void);
@@ -257,11 +261,15 @@ void cs_lsp_init(CSLSPContext *ctx, CBMArena *arena, const char *source, int sou
/* ── using management ───────────────────────────────────────────── */
void cs_lsp_add_using(CSLSPContext *ctx, CBMCSUsingKind kind, const char *local_name,
const char *target_qn, bool is_global) {
/* Append a using directive; `in_namespace` marks one declared inside a
* namespace body (CBMCSUsing.in_namespace). */
static void cs_add_using_scoped(CSLSPContext *ctx, CBMCSUsingKind kind, const char *local_name,
const char *target_qn, bool is_global, bool in_namespace) {
if (!target_qn)
return;
/* Dedupe by (kind, local_name, target). */
/* Dedupe by (kind, local_name, target). The cross-file entry pre-loads
* the pipeline's import list without placement; the AST walk then finds
* the same directive and is the authority on where it was declared. */
for (int i = 0; i < ctx->using_count; i++) {
CBMCSUsing *u = &ctx->usings[i];
if (u->kind != kind)
@@ -272,6 +280,7 @@ void cs_lsp_add_using(CSLSPContext *ctx, CBMCSUsingKind kind, const char *local_
const char *b = u->local_name ? u->local_name : "";
if (strcmp(a, b) != 0)
continue;
u->in_namespace = u->in_namespace || in_namespace;
return;
}
if (ctx->using_count >= ctx->using_cap) {
@@ -289,6 +298,12 @@ void cs_lsp_add_using(CSLSPContext *ctx, CBMCSUsingKind kind, const char *local_
slot->local_name = local_name ? cbm_arena_strdup(ctx->arena, local_name) : "";
slot->target_qn = cbm_arena_strdup(ctx->arena, target_qn);
slot->is_global = is_global;
slot->in_namespace = in_namespace;
}
void cs_lsp_add_using(CSLSPContext *ctx, CBMCSUsingKind kind, const char *local_name,
const char *target_qn, bool is_global) {
cs_add_using_scoped(ctx, kind, local_name, target_qn, is_global, false);
}
/* ── namespace stack ────────────────────────────────────────────── */
@@ -358,12 +373,29 @@ static const char *cs_try_type_qn(CSLSPContext *ctx, const char *qn) {
return NULL;
}
/* Append the file's using-namespace targets declared inside (`in_namespace`)
* or outside a namespace body. */
static int cs_append_using_namespaces(const CSLSPContext *ctx, bool in_namespace, const char **out,
int n, int cap) {
for (int i = 0; i < ctx->using_count && n < cap; i++) {
const CBMCSUsing *u = &ctx->usings[i];
if (u->kind == CBM_CS_USING_NAMESPACE && u->in_namespace == in_namespace && u->target_qn &&
u->target_qn[0])
out[n++] = u->target_qn;
}
return n;
}
/* Namespaces through which `qualifier` (the part of a type reference before
* its simple name, NULL for a simple name) can reach a top-level type, in C#
* lookup order: each enclosing namespace innermost outward, then the global
* namespace (qualified references only) or the file's `using` namespaces
* (simple names only -- a using-namespace directive does not import nested
* namespaces). Returns the count written to `out` (at most `cap`). */
* namespace (qualified references only) or, for a simple name, the usings
* of the enclosing namespace bodies, the global namespace "" and last the
* compilation-unit usings (a using-namespace directive does not import nested
* namespaces). C# consults the members of the global namespace BEFORE the
* compilation unit's usings: a top-level type declared without a namespace
* hides an imported type of the same name. Returns the count written to
* `out` (at most `cap`). */
static int cs_visible_namespaces(CSLSPContext *ctx, const char *qualifier, const char **out,
int cap) {
int n = 0;
@@ -378,12 +410,13 @@ static int cs_visible_namespaces(CSLSPContext *ctx, const char *qualifier, const
out[n++] = qualifier;
return n;
}
for (int i = 0; i < ctx->using_count && n < cap; i++) {
const CBMCSUsing *u = &ctx->usings[i];
if (u->kind == CBM_CS_USING_NAMESPACE && u->target_qn && u->target_qn[0])
out[n++] = u->target_qn;
}
return n;
n = cs_append_using_namespaces(ctx, true, out, n, cap);
/* The global namespace (#2120 residual: such types used to be invisible
* to step 8b) ranks after every enclosing namespace and namespace-body
* using, and before the compilation-unit usings. */
if (n < cap)
out[n++] = "";
return cs_append_using_namespaces(ctx, false, out, n, cap);
}
/* Bind a (possibly namespace-qualified) type reference to the project type
@@ -421,6 +454,24 @@ static const char *cs_resolve_by_declared_namespace(CSLSPContext *ctx, const cha
}
}
}
/* No namespace-qualified hit: a qualified `A.B` may name a NESTED type —
* A a type in scope (resolved recursively, one segment shorter), B a
* member type of it. Nested QNs are the parent QN plus the simple name,
* which step 8b's namespace scan can never produce because nested types
* carry no namespace_qn (#2120 residual: `new Command.Result(...)`,
* `new Grain.Handler(dep).Handle(...)`). The walk is depth-bounded: an
* alias whose target names the alias itself (`using A = A.B;`, or a
* mutual pair) re-enters this path through step 8 with no end. */
if (!best && qualifier && ctx->qualifier_depth < CS_QUALIFIER_MAX_DEPTH) {
ctx->qualifier_depth++;
const char *qtype = cs_resolve_type_name(ctx, qualifier);
ctx->qualifier_depth--;
if (qtype) {
const char *nested = cbm_arena_sprintf(ctx->arena, "%s.%s", qtype, simple);
if (cs_lookup_type_qn(ctx, nested))
return nested;
}
}
return best ? best->qualified_name : NULL;
}
@@ -610,10 +661,14 @@ const char *cs_resolve_type_name(CSLSPContext *ctx, const char *raw) {
}
/* Other top-level declarations of the type `t` names: same simple name, same
* declared namespace, different entry. Registration order; at most `cap`. */
* declared namespace, different entry. Registration order; at most `cap`.
* A type in the global namespace ("") has no twins: without a namespace the
* same name across a repository is a different type per assembly (every test
* project's own `Program`), so pooling them would bind a member of one to an
* unrelated other, ahead of the type's own base classes. */
static int cs_namespace_twins(CSLSPContext *ctx, const CBMRegisteredType *t,
const CBMRegisteredType **out, int cap) {
if (!ctx->registry || !t || !t->namespace_qn || !t->short_name)
if (!ctx->registry || !t || !t->namespace_qn || !t->namespace_qn[0] || !t->short_name)
return 0;
int n = 0;
CBMTypeShortIter it;
@@ -2797,12 +2852,19 @@ static void cs_process_type_decl(CSLSPContext *ctx, TSNode node) {
static void cs_collect_imports(CSLSPContext *ctx, TSNode root) {
/* Walk the entire tree once at top — using directives can appear at file
* scope or inside namespace blocks. */
TSNode stack[256];
* scope or inside namespace blocks. `in_ns` records per stacked node
* whether it sits inside a namespace declaration: a block namespace's
* body, or the compilation-unit siblings after a file-scoped namespace
* (the grammar may attach those either way). */
enum { CS_IMPORT_WALK_MAX = 256 };
TSNode stack[CS_IMPORT_WALK_MAX];
bool in_ns[CS_IMPORT_WALK_MAX];
int top = 0;
in_ns[top] = false;
stack[top++] = root;
while (top > 0) {
TSNode n = stack[--top];
bool n_in_ns = in_ns[top];
if (ts_node_is_null(n))
continue;
const char *k = ts_node_type(n);
@@ -2886,20 +2948,23 @@ static void cs_collect_imports(CSLSPContext *ctx, TSNode root) {
}
if (target) {
if (is_alias) {
cs_lsp_add_using(ctx, CBM_CS_USING_ALIAS, alias_name ? alias_name : "", target,
is_global);
cs_add_using_scoped(ctx, CBM_CS_USING_ALIAS, alias_name ? alias_name : "",
target, is_global, n_in_ns);
} else if (is_static) {
cs_lsp_add_using(ctx, CBM_CS_USING_STATIC, "", target, is_global);
cs_add_using_scoped(ctx, CBM_CS_USING_STATIC, "", target, is_global, n_in_ns);
} else {
cs_lsp_add_using(ctx, CBM_CS_USING_NAMESPACE, "", target, is_global);
cs_add_using_scoped(ctx, CBM_CS_USING_NAMESPACE, "", target, is_global,
n_in_ns);
}
}
}
/* Push children for traversal. We don't recurse into method bodies
* (using directives can't appear there), but namespace bodies may
* contain more usings. */
bool child_in_ns = n_in_ns || strcmp(k, "namespace_declaration") == 0 ||
strcmp(k, "file_scoped_namespace_declaration") == 0;
uint32_t cnc = ts_node_child_count(n);
for (uint32_t i = 0; i < cnc && top < 256; i++) {
for (uint32_t i = 0; i < cnc && top < CS_IMPORT_WALK_MAX; i++) {
TSNode c = ts_node_child(n, i);
if (ts_node_is_null(c))
continue;
@@ -2908,7 +2973,11 @@ static void cs_collect_imports(CSLSPContext *ctx, TSNode root) {
strcmp(ck, "constructor_declaration") == 0 || strcmp(ck, "block") == 0) {
continue;
}
in_ns[top] = child_in_ns;
stack[top++] = c;
/* A file-scoped namespace covers the siblings that follow it. */
if (strcmp(ck, "file_scoped_namespace_declaration") == 0)
child_in_ns = true;
}
}
}
@@ -3749,12 +3818,16 @@ void cbm_run_cs_lsp(CBMArena *arena, CBMFileResult *result, const char *source,
* type is not reachable by its simple name through a namespace). */
static const char *cs_top_level_namespace(const char *type_qn, const char *short_name,
const char *module_qn, const char *namespace_name) {
if (!type_qn || !short_name || !module_qn || !namespace_name || !namespace_name[0])
if (!type_qn || !short_name || !module_qn)
return NULL;
size_t mlen = strlen(module_qn);
if (strncmp(type_qn, module_qn, mlen) != 0 || type_qn[mlen] != '.' ||
strcmp(type_qn + mlen + 1, short_name) != 0)
return NULL;
/* Top-level C# type: its declared namespace, "" for the global namespace
* so step 8b can bind it, NULL when extraction could not tell (a parse
* error dissolved the namespace; see cs_type_decl_namespace). NULL stays
* invisible to step 8b, as do nested types. */
return namespace_name;
}
+10
View File
@@ -61,6 +61,11 @@ typedef struct {
const char *local_name; /* alias name; "" for non-alias */
const char *target_qn; /* dotted target QN */
bool is_global; /* `global using` */
/* Declared inside a namespace body rather than at compilation-unit level.
* C# consults a namespace declaration's usings right after that
* namespace's own members, but compilation-unit usings only after the
* global namespace's members, so the two rank on opposite sides of it. */
bool in_namespace;
} CBMCSUsing;
/* CSLSPContext — per-file type-evaluation state. */
@@ -111,6 +116,11 @@ typedef struct {
/* Recursion guard for cs_eval_expr_type. */
int eval_depth;
/* Recursion guard for the nested-type qualifier walk of
* cs_resolve_type_name: resolving `A.B` resolves the qualifier `A` as a
* type first, and a `using A = A.B;` alias cycles back into it. */
int qualifier_depth;
/* Debug mode (CBM_LSP_DEBUG env). */
bool debug;
} CSLSPContext;
+19
View File
@@ -46,6 +46,25 @@ static const lib_pattern_t http_libraries[] = {
* case-sensitive, so without this entry `Axios.get(url)` fell through to
* the `.get` route-suffix fallback and became a route registration. */
{"Axios", CBM_SVC_HTTP, NULL},
/* Angular HttpClient (#2768). The receiver rows end in a dot, like "gin.":
* a bare "this.http" / "$http" would also match this.httpAdapter.get
* (NestJS route registration) and $httpBackend.expectGET (an AngularJS test
* double), because an uppercase letter after an id counts as CamelCase
* glue. A chain wrapped before the verb keeps its line break in the callee
* text ("this.http\n .post", prettier's style), so each receiver also
* has a line-break twin: LF, and CR for CRLF files. Java/C# HttpClient is
* already listed below. */
{"@angular/common/http", CBM_SVC_HTTP, NULL},
{"angular.common.http", CBM_SVC_HTTP, NULL},
{"this.http.", CBM_SVC_HTTP, NULL},
{"this.http\n", CBM_SVC_HTTP, NULL},
{"this.http\r", CBM_SVC_HTTP, NULL},
{"httpClient.", CBM_SVC_HTTP, NULL},
{"httpClient\n", CBM_SVC_HTTP, NULL},
{"httpClient\r", CBM_SVC_HTTP, NULL},
{"$http.", CBM_SVC_HTTP, NULL},
{"$http\n", CBM_SVC_HTTP, NULL},
{"$http\r", CBM_SVC_HTTP, NULL},
{"superagent", CBM_SVC_HTTP, NULL},
{"needle", CBM_SVC_HTTP, NULL},
{"node-fetch", CBM_SVC_HTTP, NULL},
+120 -3
View File
@@ -1367,6 +1367,118 @@ fi
echo ""
echo "=== Phase 8: agent config install E2E ==="
# Keep Codex skill placement independent from the broad multi-client fixture:
# Warp and OpenHands share the canonical root and could otherwise mask a Codex
# regression by creating the expected file themselves.
CODEX_ONLY_HOME=$(smoke_mktemp_dir)
CODEX_ONLY_CONFIG="$CODEX_ONLY_HOME/vendor-codex"
CODEX_ONLY_INSTALL="$CODEX_ONLY_HOME/bin"
CODEX_ONLY_SKILL="$CODEX_ONLY_HOME/.agents/skills/codebase-memory/SKILL.md"
CODEX_ONLY_LEGACY="$CODEX_ONLY_CONFIG/skills/codebase-memory/SKILL.md"
mkdir -p "$CODEX_ONLY_CONFIG" "$CODEX_ONLY_INSTALL"
if [[ "$BINARY" == *.exe ]]; then
CODEX_ONLY_BIN="$CODEX_ONLY_INSTALL/codebase-memory-mcp.exe"
else
CODEX_ONLY_BIN="$CODEX_ONLY_INSTALL/codebase-memory-mcp"
fi
CODEX_ONLY_LOG=$(smoke_mktemp_file)
run_codex_only() {
local executable="$1"
shift
HOME="$CODEX_ONLY_HOME" \
XDG_CONFIG_HOME="$CODEX_ONLY_HOME/.config" \
APPDATA="$CODEX_ONLY_HOME/AppData/Roaming" \
LOCALAPPDATA="$CODEX_ONLY_HOME/AppData/Local" \
CODEX_HOME="$CODEX_ONLY_CONFIG" \
"$executable" "$@" > "$CODEX_ONLY_LOG" 2>&1
}
if ! run_codex_only "$BINARY" install -y --clients=codex --dir="$CODEX_ONLY_INSTALL"; then
echo "FAIL 8-codex-1: isolated Codex install failed"
cat "$CODEX_ONLY_LOG"
exit 1
fi
if ! grep -q '^name: codebase-memory$' "$CODEX_ONLY_SKILL" 2>/dev/null ||
! grep -q 'trace_path' "$CODEX_ONLY_SKILL" 2>/dev/null ||
[ -e "$CODEX_ONLY_LEGACY" ]; then
echo "FAIL 8-codex-1: Codex skill was not installed exclusively at the canonical path"
cat "$CODEX_ONLY_LOG"
exit 1
fi
CODEX_ONLY_HASH=$(smoke_file_sha256 "$CODEX_ONLY_SKILL")
if ! run_codex_only "$CODEX_ONLY_BIN" install --plan --clients=codex \
--dir="$CODEX_ONLY_INSTALL" ||
[ "$(smoke_file_sha256 "$CODEX_ONLY_SKILL")" != "$CODEX_ONLY_HASH" ]; then
echo "FAIL 8-codex-2: Codex install plan mutated the canonical skill"
cat "$CODEX_ONLY_LOG"
exit 1
fi
if ! run_codex_only "$CODEX_ONLY_BIN" install --dry-run -y --clients=codex \
--dir="$CODEX_ONLY_INSTALL" ||
[ "$(smoke_file_sha256 "$CODEX_ONLY_SKILL")" != "$CODEX_ONLY_HASH" ]; then
echo "FAIL 8-codex-2: Codex dry-run mutated the canonical skill"
cat "$CODEX_ONLY_LOG"
exit 1
fi
mkdir -p "$(dirname "$CODEX_ONLY_LEGACY")"
mv "$CODEX_ONLY_SKILL" "$CODEX_ONLY_LEGACY"
if ! run_codex_only "$CODEX_ONLY_BIN" install -y --clients=codex \
--dir="$CODEX_ONLY_INSTALL"; then
echo "FAIL 8-codex-3: isolated Codex legacy migration failed"
cat "$CODEX_ONLY_LOG"
exit 1
fi
if ! grep -q '^name: codebase-memory$' "$CODEX_ONLY_SKILL" 2>/dev/null ||
[ -e "$CODEX_ONLY_LEGACY" ]; then
echo "FAIL 8-codex-3: Codex legacy skill was not migrated to the canonical path"
cat "$CODEX_ONLY_LOG"
exit 1
fi
rm -f "$CODEX_ONLY_SKILL"
mkdir -p "$(dirname "$CODEX_ONLY_LEGACY")"
printf '%s\n' '---' 'name: codebase-memory' '---' 'User-owned legacy skill.' \
> "$CODEX_ONLY_LEGACY"
CODEX_ONLY_FOREIGN_HASH=$(smoke_file_sha256 "$CODEX_ONLY_LEGACY")
if ! run_codex_only "$CODEX_ONLY_BIN" install -y --clients=codex \
--dir="$CODEX_ONLY_INSTALL" ||
[ -e "$CODEX_ONLY_SKILL" ] ||
[ "$(smoke_file_sha256 "$CODEX_ONLY_LEGACY")" != "$CODEX_ONLY_FOREIGN_HASH" ]; then
echo "FAIL 8-codex-4: non-force install did not preserve a foreign legacy skill"
cat "$CODEX_ONLY_LOG"
exit 1
fi
mkdir -p "$CODEX_ONLY_HOME/.openhands"
if ! run_codex_only "$CODEX_ONLY_BIN" install -y --clients=codex,openhands \
--dir="$CODEX_ONLY_INSTALL" ||
! grep -q '^name: codebase-memory$' "$CODEX_ONLY_SKILL" 2>/dev/null ||
[ "$(smoke_file_sha256 "$CODEX_ONLY_LEGACY")" != "$CODEX_ONLY_FOREIGN_HASH" ]; then
echo "FAIL 8-codex-5: a foreign Codex legacy skill blocked a shared-client install"
cat "$CODEX_ONLY_LOG"
exit 1
fi
rm -f "$CODEX_ONLY_SKILL"
if ! run_codex_only "$CODEX_ONLY_BIN" install --force -y --clients=codex \
--dir="$CODEX_ONLY_INSTALL" ||
! grep -q '^name: codebase-memory$' "$CODEX_ONLY_SKILL" 2>/dev/null ||
[ -e "$CODEX_ONLY_LEGACY" ]; then
echo "FAIL 8-codex-6: force did not converge a foreign legacy skill"
cat "$CODEX_ONLY_LOG"
exit 1
fi
CODEX_ONLY_HASH=$(smoke_file_sha256 "$CODEX_ONLY_SKILL")
if ! run_codex_only "$CODEX_ONLY_BIN" install -y --clients=codex \
--dir="$CODEX_ONLY_INSTALL" ||
[ "$(smoke_file_sha256 "$CODEX_ONLY_SKILL")" != "$CODEX_ONLY_HASH" ]; then
echo "FAIL 8-codex-7: repeated Codex install was not idempotent"
cat "$CODEX_ONLY_LOG"
exit 1
fi
echo "OK: Codex skill placement, previews, migration, force, and idempotence"
smoke_rmtree "$CODEX_ONLY_HOME"
rm -f "$CODEX_ONLY_LOG"
# Set up an isolated HOME. Directory-only agents get only the root required for
# detection; CLI-detected agents use stubs below so install must create their
# config parents from scratch.
@@ -1780,6 +1892,8 @@ fi
CODEX_LIFECYCLE_HOME=$(smoke_mktemp_dir)
CODEX_LIFECYCLE_ROOT="$CODEX_LIFECYCLE_HOME/.codex"
CODEX_INSTRUCTIONS="$CODEX_LIFECYCLE_ROOT/AGENTS.md"
CODEX_LIFECYCLE_SKILL="$CODEX_LIFECYCLE_HOME/.agents/skills/codebase-memory/SKILL.md"
CODEX_LIFECYCLE_LEGACY_SKILL="$CODEX_LIFECYCLE_ROOT/skills/codebase-memory/SKILL.md"
mkdir -p "$CODEX_LIFECYCLE_ROOT"
CODEX_FRESH_LOG=$(smoke_mktemp_file)
@@ -1793,7 +1907,8 @@ if [ ! -f "$CODEX_INSTRUCTIONS" ] ||
[ "$(smoke_file_sha256 "$CODEX_INSTRUCTIONS")" != \
"$(smoke_file_sha256 "$CODEX_POINTER_EXPECTED")" ] ||
! grep -q '\[mcp_servers.codebase-memory-mcp\]' "$CODEX_LIFECYCLE_ROOT/config.toml" ||
! grep -q 'search_graph' "$CODEX_LIFECYCLE_ROOT/skills/codebase-memory/SKILL.md" ||
! grep -q 'search_graph' "$CODEX_LIFECYCLE_SKILL" ||
[ -e "$CODEX_LIFECYCLE_LEGACY_SKILL" ] ||
[ ! -s "$CODEX_LIFECYCLE_ROOT/agents/codebase-memory-scout.toml" ] ||
[ ! -s "$CODEX_LIFECYCLE_ROOT/agents/codebase-memory.toml" ] ||
[ ! -s "$CODEX_LIFECYCLE_ROOT/agents/codebase-memory-auditor.toml" ] ||
@@ -1890,8 +2005,10 @@ if ! HOME="$CODEX_LIFECYCLE_HOME" \
fi
if [ ! -f "$CODEX_INSTRUCTIONS" ] ||
[ "$(smoke_file_sha256 "$CODEX_INSTRUCTIONS")" != \
"$(smoke_file_sha256 "$CODEX_EXPECTED_USER")" ]; then
echo "FAIL 8i: Codex uninstall did not remove only the activation pointer"
"$(smoke_file_sha256 "$CODEX_EXPECTED_USER")" ] ||
[ -e "$CODEX_LIFECYCLE_SKILL" ] ||
[ -e "$CODEX_LIFECYCLE_LEGACY_SKILL" ]; then
echo "FAIL 8i: Codex uninstall did not remove only installer-owned surfaces"
exit 1
fi
echo "OK 8i: isolated Codex activation-pointer lifecycle"
+609 -8
View File
@@ -1601,6 +1601,232 @@ int cbm_replace_binary(const char *path, const unsigned char *data, int len, int
/* Consolidated from 4 separate skills into 1 with progressive disclosure.
* This embedded version is the single source of truth for the CLI installer.
* Based on PR #81 by @gdilla — factual corrections applied. */
static const char released_skill_content_v0_6_0[] =
"---\n"
"name: codebase-memory\n"
"description: Use the codebase knowledge graph for structural code queries. "
"Triggers on: explore the codebase, understand the architecture, what functions exist, "
"show me the structure, who calls this function, what does X call, trace the call chain, "
"find callers of, show dependencies, impact analysis, dead code, unused functions, "
"high fan-out, refactor candidates, code quality audit, graph query syntax, "
"Cypher query examples, edge types, how to use search_graph.\n"
"---\n"
"\n"
"# Codebase Memory — Knowledge Graph Tools\n"
"\n"
"Graph tools return precise structural results in ~500 tokens vs ~80K for grep.\n"
"\n"
"## Quick Decision Matrix\n"
"\n"
"| Question | Tool call |\n"
"|----------|----------|\n"
"| Who calls X? | `trace_path(direction=\"inbound\")` |\n"
"| What does X call? | `trace_path(direction=\"outbound\")` |\n"
"| Full call context | `trace_path(direction=\"both\")` |\n"
"| Find by name pattern | `search_graph(name_pattern=\"...\")` |\n"
"| Dead code | `search_graph(max_degree=0, exclude_entry_points=true)` |\n"
"| Cross-service edges | `query_graph` with Cypher |\n"
"| Impact of local changes | `detect_changes()` |\n"
"| Risk-classified trace | `trace_path(risk_labels=true)` |\n"
"| Text search | `search_code` or Grep |\n"
"\n"
"## Exploration Workflow\n"
"1. `list_projects` — check if project is indexed\n"
"2. `get_graph_schema` — understand node/edge types\n"
"3. `search_graph(label=\"Function\", name_pattern=\".*Pattern.*\")` — find code\n"
"4. `get_code_snippet(qualified_name=\"project.path.FuncName\")` — read source\n"
"\n"
"## Tracing Workflow\n"
"1. `search_graph(name_pattern=\".*FuncName.*\")` — discover exact name\n"
"2. `trace_path(function_name=\"FuncName\", direction=\"both\", depth=3)` — trace\n"
"3. `detect_changes()` — map git diff to affected symbols\n"
"\n"
"## Quality Analysis\n"
"- Dead code: `search_graph(max_degree=0, exclude_entry_points=true)`\n"
"- High fan-out: `search_graph(min_degree=10, relationship=\"CALLS\", "
"direction=\"outbound\")`\n"
"- High fan-in: `search_graph(min_degree=10, relationship=\"CALLS\", "
"direction=\"inbound\")`\n"
"\n"
"## 14 MCP Tools\n"
"`index_repository`, `index_status`, `list_projects`, `delete_project`,\n"
"`search_graph`, `search_code`, `trace_path`, `detect_changes`,\n"
"`query_graph`, `get_graph_schema`, `get_code_snippet`, `get_architecture`,\n"
"`manage_adr`, `ingest_traces`\n"
"\n"
"## Edge Types\n"
"CALLS, HTTP_CALLS, ASYNC_CALLS, IMPORTS, DEFINES, DEFINES_METHOD,\n"
"HANDLES, IMPLEMENTS, OVERRIDE, USAGE, FILE_CHANGES_WITH,\n"
"CONTAINS_FILE, CONTAINS_FOLDER, CONTAINS_PACKAGE\n"
"\n"
"## Cypher Examples (for query_graph)\n"
"```\n"
"MATCH (a)-[r:HTTP_CALLS]->(b) RETURN a.name, b.name, r.url_path, "
"r.confidence LIMIT 20\n"
"MATCH (f:Function) WHERE f.name =~ '.*Handler.*' RETURN f.name, f.file_path\n"
"MATCH (a)-[r:CALLS]->(b) WHERE a.name = 'main' RETURN b.name\n"
"```\n"
"\n"
"## Gotchas\n"
"1. `search_graph(relationship=\"HTTP_CALLS\")` filters nodes by degree — "
"use `query_graph` with Cypher to see actual edges.\n"
"2. `query_graph` has a 200-row cap — use `search_graph` with degree filters "
"for counting.\n"
"3. `trace_path` needs exact names — use `search_graph(name_pattern=...)` first.\n"
"4. `direction=\"outbound\"` misses cross-service callers — use "
"`direction=\"both\"`.\n"
"5. Results default to 10 per page — check `has_more` and use `offset`.\n";
#define CBM_RELEASED_SKILL_HEADER_UNQUOTED \
"---\n" \
"name: codebase-memory\n" \
"description: Use the codebase knowledge graph for structural code queries. " \
"Triggers on: explore the codebase, understand the architecture, what functions exist, " \
"show me the structure, who calls this function, what does X call, trace the call chain, " \
"find callers of, show dependencies, impact analysis, dead code, unused functions, " \
"high fan-out, refactor candidates, code quality audit, graph query syntax, " \
"Cypher query examples, edge types, how to use search_graph.\n" \
"---\n"
#define CBM_RELEASED_SKILL_HEADER_QUOTED \
"---\n" \
"name: codebase-memory\n" \
"description: \"Use the codebase knowledge graph for structural code queries. " \
"Triggers on: explore the codebase, understand the architecture, what functions exist, " \
"show me the structure, who calls this function, what does X call, trace the call chain, " \
"find callers of, show dependencies, impact analysis, dead code, unused functions, " \
"high fan-out, refactor candidates, code quality audit, graph query syntax, " \
"Cypher query examples, edge types, how to use search_graph.\"\n" \
"---\n"
#define CBM_RELEASED_SKILL_BODY_BEFORE_EDGE_TYPES \
"\n" \
"# Codebase Memory — Knowledge Graph Tools\n" \
"\n" \
"Graph tools return precise structural results in ~500 tokens vs ~80K for grep.\n" \
"\n" \
"## Quick Decision Matrix\n" \
"\n" \
"| Question | Tool call |\n" \
"|----------|----------|\n" \
"| Who calls X? | `trace_path(direction=\"inbound\")` |\n" \
"| What does X call? | `trace_path(direction=\"outbound\")` |\n" \
"| Full call context | `trace_path(direction=\"both\")` |\n" \
"| Find by name pattern | `search_graph(name_pattern=\"...\")` |\n" \
"| Dead code | `search_graph(max_degree=0, exclude_entry_points=true)` |\n" \
"| Cross-service edges | `query_graph` with Cypher |\n" \
"| Impact of local changes | `detect_changes()` |\n" \
"| Risk-classified trace | `trace_path(risk_labels=true)` |\n" \
"| Text search | `search_code` or Grep |\n" \
"\n" \
"## Exploration Workflow\n" \
"1. `list_projects` — check if project is indexed\n" \
"2. `get_graph_schema` — understand node/edge types\n" \
"3. `search_graph(label=\"Function\", name_pattern=\".*Pattern.*\")` — find code\n" \
"4. `get_code_snippet(qualified_name=\"project.path.FuncName\")` — read source\n" \
"\n" \
"## Tracing Workflow\n" \
"1. `search_graph(name_pattern=\".*FuncName.*\")` — discover exact name\n" \
"2. `trace_path(function_name=\"FuncName\", direction=\"both\", depth=3)` — trace\n" \
"3. `detect_changes()` — map git diff to affected symbols\n" \
"\n" \
"## Evidence Tiers\n" \
"- **Scout (Tier 1):** fast positive lookup with few graph calls and targeted source checks. " \
"Treat results as provisional; never make absence, exhaustive, dead-code, or complete-impact " \
"claims.\n" \
"- **Verify (Tier 2, default):** task-directed searches, relevant trace directions, exact " \
"snippets for material claims, and all relevant result pages.\n" \
"- **Auditor (Tier 3):** bounded-scope full verification with a current graph generation, " \
"complete relevant pagination, both call directions and broader relationships when material, " \
"plus explicit unresolved limitations.\n" \
"- **Every tier:** after candidate paths are known, call `check_index_coverage` once with " \
"every " \
"evidence path. For negative or exhaustive claims also include the relevant scopes. A clean " \
"result means no recorded gap, not proof of completeness. For partial, skipped, excluded, " \
"stale, pending, or unknown coverage, read/grep the reported ranges or scope before relying " \
"on " \
"the graph.\n" \
"\n" \
"## Sessions and Subagents\n" \
"- At session start or after compaction, call `list_projects`/`index_status` before " \
"structural exploration, then choose Scout, Verify, or Auditor for the task.\n" \
"- Before delegating, query the graph and coverage in the parent. Pass the tier, exact " \
"project, " \
"generation/freshness, bounded scope, queries and pagination state, qualified symbols, " \
"paths, " \
"call-chain findings, coverage ranges/reasons, source fallback already performed, and " \
"unresolved " \
"questions to the child.\n" \
"- Runtimes such as Hermes isolate child context: put those graph findings in the " \
"`context` argument to `delegate_task`; do not assume the child inherits MCP access or " \
"the parent's conversation.\n" \
"- A child without MCP tools must not call or claim MCP access. It should work from the " \
"supplied " \
"evidence and use read/grep on exact source, especially every reported missed-coverage " \
"range.\n" \
"\n" \
"## Quality Analysis\n" \
"- Dead code: `search_graph(max_degree=0, exclude_entry_points=true)`\n" \
"- High fan-out: `search_graph(min_degree=10, relationship=\"CALLS\", " \
"direction=\"outbound\")`\n" \
"- High fan-in: `search_graph(min_degree=10, relationship=\"CALLS\", " \
"direction=\"inbound\")`\n" \
"\n" \
"## 15 MCP Tools\n" \
"`index_repository`, `index_status`, `list_projects`, `delete_project`,\n" \
"`search_graph`, `search_code`, `trace_path`, `detect_changes`,\n" \
"`query_graph`, `get_graph_schema`, `get_code_snippet`, `get_architecture`,\n" \
"`check_index_coverage`, `manage_adr`, `ingest_traces`\n" \
"\n" \
"## Edge Types\n" \
"CALLS, HTTP_CALLS, ASYNC_CALLS, DATA_FLOWS, IMPORTS, DEFINES, DEFINES_METHOD,\n"
#define CBM_RELEASED_SKILL_EDGE_TYPES_PRE_V0_10 \
"HANDLES, IMPLEMENTS, OVERRIDE, USAGE, CONFIGURES, FILE_CHANGES_WITH,\n"
#define CBM_RELEASED_SKILL_EDGE_TYPES_V0_10 \
"HANDLES, IMPLEMENTS, OVERRIDE, USAGE, CALL_REFERENCE, CONFIGURES, FILE_CHANGES_WITH,\n"
#define CBM_RELEASED_SKILL_BODY_AFTER_EDGE_TYPES \
"SIMILAR_TO, SEMANTICALLY_RELATED, CONTAINS_FILE, CONTAINS_FOLDER,\n" \
"CONTAINS_PACKAGE\n" \
"\n" \
"## Cypher Examples (for query_graph)\n" \
"```\n" \
"MATCH (a)-[r:HTTP_CALLS]->(b) RETURN a.name, b.name, r.url_path, " \
"r.confidence LIMIT 20\n" \
"MATCH (f:Function) WHERE f.name =~ '.*Handler.*' RETURN f.name, f.file_path\n" \
"MATCH (a)-[r:CALLS]->(b) WHERE a.name = 'main' RETURN b.name\n" \
"```\n" \
"\n" \
"## Gotchas\n" \
"1. `search_graph(relationship=\"HTTP_CALLS\")` filters nodes by degree — " \
"use `query_graph` with Cypher to see actual edges.\n" \
"2. `query_graph` has a 100k row ceiling — add a Cypher `LIMIT` for broad queries " \
"or use `search_graph` pagination.\n" \
"3. `trace_path` needs exact names — use `search_graph(name_pattern=...)` first.\n" \
"4. `direction=\"outbound\"` misses cross-service callers — use " \
"`direction=\"both\"`.\n" \
"5. `search_graph` results default to 50 per page — check `has_more` and use `offset`.\n"
/* Exact payload generations shipped before the current skill text. */
static const char released_skill_content_v0_9_1[] =
CBM_RELEASED_SKILL_HEADER_UNQUOTED CBM_RELEASED_SKILL_BODY_BEFORE_EDGE_TYPES
CBM_RELEASED_SKILL_EDGE_TYPES_PRE_V0_10 CBM_RELEASED_SKILL_BODY_AFTER_EDGE_TYPES;
static const char released_skill_content_v0_10_0[] =
CBM_RELEASED_SKILL_HEADER_UNQUOTED CBM_RELEASED_SKILL_BODY_BEFORE_EDGE_TYPES
CBM_RELEASED_SKILL_EDGE_TYPES_V0_10 CBM_RELEASED_SKILL_BODY_AFTER_EDGE_TYPES;
static const char released_skill_content_v0_10_3[] =
CBM_RELEASED_SKILL_HEADER_QUOTED CBM_RELEASED_SKILL_BODY_BEFORE_EDGE_TYPES
CBM_RELEASED_SKILL_EDGE_TYPES_V0_10 CBM_RELEASED_SKILL_BODY_AFTER_EDGE_TYPES;
#undef CBM_RELEASED_SKILL_BODY_AFTER_EDGE_TYPES
#undef CBM_RELEASED_SKILL_EDGE_TYPES_V0_10
#undef CBM_RELEASED_SKILL_EDGE_TYPES_PRE_V0_10
#undef CBM_RELEASED_SKILL_BODY_BEFORE_EDGE_TYPES
#undef CBM_RELEASED_SKILL_HEADER_QUOTED
#undef CBM_RELEASED_SKILL_HEADER_UNQUOTED
static const char skill_content[] =
"---\n"
"name: codebase-memory\n"
@@ -1722,6 +1948,19 @@ static const char skill_content[] =
"`direction=\"both\"`.\n"
"5. `search_graph` results default to 50 per page — check `has_more` and use `offset`.\n";
/* Byte-exact consolidated skill payloads shipped by each release range. */
static const char *const released_skill_contents[] = {
/* 0.6.0 through v0.9.0 */
released_skill_content_v0_6_0,
/* v0.9.1-rc.1 */
released_skill_content_v0_9_1,
/* v0.10.0 through v0.10.2 */
released_skill_content_v0_10_0,
/* v0.10.3 through v0.10.8 */
released_skill_content_v0_10_3,
};
enum { RELEASED_SKILL_CONTENT_COUNT = 4 };
static const char codex_instructions_content[] =
"For structural codebase exploration, use the installed `codebase-memory` skill.\n";
@@ -1742,6 +1981,16 @@ const cbm_skill_t *cbm_get_skills(void) {
return skills;
}
#ifdef CBM_CLI_ENABLE_TEST_API
size_t cbm_cli_released_skill_content_count(void) {
return RELEASED_SKILL_CONTENT_COUNT;
}
const char *cbm_cli_released_skill_content_at(size_t index) {
return index < RELEASED_SKILL_CONTENT_COUNT ? released_skill_contents[index] : NULL;
}
#endif
const char *cbm_get_codex_instructions(void) {
return codex_instructions_content;
}
@@ -2788,7 +3037,26 @@ int cbm_remove_zed_mcp_owned(const char *binary_path, const char *config_path) {
static bool dir_exists(const char *path) {
cbm_path_info_t info = {0};
return cbm_path_info_utf8(path, &info) == CBM_PATH_INFO_OK && info.is_directory;
if (cbm_path_info_utf8(path, &info) != CBM_PATH_INFO_OK) {
return false;
}
if (info.is_directory) {
return true;
}
#ifndef _WIN32
if (info.is_symlink) {
uid_t self = geteuid();
struct stat link_state;
if (lstat(path, &link_state) != 0 || !S_ISLNK(link_state.st_mode) ||
link_state.st_uid != self) {
return false;
}
struct stat target_state;
return stat(path, &target_state) == 0 && S_ISDIR(target_state.st_mode) &&
target_state.st_uid == self;
}
#endif
return false;
}
/* Resolve the Claude Code config dir.
@@ -8547,7 +8815,7 @@ static void print_detected_agents(const cbm_detected_agents_t *a, const char *ho
* behavior (it is the same code path with mutations disabled). */
typedef struct {
char agent[CLI_BUF_32];
char kind[CLI_BUF_32]; /* mcp_config | instructions | skills | hook */
char kind[CLI_BUF_32]; /* mcp_config | instructions | skill | cleanup_skill | hook */
char path[CLI_BUF_1K];
} cbm_plan_entry_t;
@@ -9053,6 +9321,270 @@ static void install_agent_skill(const char *label, const char *skills_dir, bool
printf(" skill: %s (%d installed)\n", skill_path, installed);
}
typedef enum {
CBM_CODEX_SKILL_ABSENT,
CBM_CODEX_SKILL_MANAGED,
CBM_CODEX_SKILL_UNOWNED,
CBM_CODEX_SKILL_UNSAFE,
} cbm_codex_skill_state_t;
typedef struct {
char canonical_dir[CLI_BUF_1K];
char legacy_dir[CLI_BUF_1K];
char canonical_file[CLI_BUF_1K];
char legacy_file[CLI_BUF_1K];
bool same_dir;
} cbm_codex_skill_paths_t;
static bool cbm_codex_skill_paths_init(const char *home, const char *config_dir,
cbm_codex_skill_paths_t *paths) {
if (!home || !home[0] || !config_dir || !config_dir[0] || !paths) {
return false;
}
memset(paths, 0, sizeof(*paths));
int canonical_dir =
snprintf(paths->canonical_dir, sizeof(paths->canonical_dir), "%s/.agents/skills", home);
int legacy_dir =
snprintf(paths->legacy_dir, sizeof(paths->legacy_dir), "%s/skills", config_dir);
int canonical_file = snprintf(paths->canonical_file, sizeof(paths->canonical_file),
"%s/codebase-memory/SKILL.md", paths->canonical_dir);
int legacy_file = snprintf(paths->legacy_file, sizeof(paths->legacy_file),
"%s/codebase-memory/SKILL.md", paths->legacy_dir);
if (canonical_dir < 0 || (size_t)canonical_dir >= sizeof(paths->canonical_dir) ||
legacy_dir < 0 || (size_t)legacy_dir >= sizeof(paths->legacy_dir) || canonical_file < 0 ||
(size_t)canonical_file >= sizeof(paths->canonical_file) || legacy_file < 0 ||
(size_t)legacy_file >= sizeof(paths->legacy_file)) {
return false;
}
paths->same_dir = cbm_json_mcp_paths_equal(paths->canonical_dir, paths->legacy_dir) ||
cbm_same_file(paths->canonical_dir, paths->legacy_dir);
return true;
}
static cbm_codex_skill_state_t cbm_codex_skill_state(const char *skills_dir,
const char *skill_file) {
char skill_dir[CLI_BUF_1K];
int written = snprintf(skill_dir, sizeof(skill_dir), "%s/codebase-memory", skills_dir);
if (written < 0 || (size_t)written >= sizeof(skill_dir)) {
return CBM_CODEX_SKILL_UNSAFE;
}
struct stat state;
errno = 0;
#ifndef _WIN32
int result = lstat(skill_dir, &state);
#else
int result = stat(skill_dir, &state);
#endif
if (result != 0) {
return errno == ENOENT ? CBM_CODEX_SKILL_ABSENT : CBM_CODEX_SKILL_UNSAFE;
}
if (!S_ISDIR(state.st_mode)) {
return CBM_CODEX_SKILL_UNSAFE;
}
errno = 0;
#ifndef _WIN32
result = lstat(skill_file, &state);
#else
result = stat(skill_file, &state);
#endif
if (result != 0) {
return errno == ENOENT ? CBM_CODEX_SKILL_ABSENT : CBM_CODEX_SKILL_UNSAFE;
}
if (!S_ISREG(state.st_mode)) {
return CBM_CODEX_SKILL_UNSAFE;
}
int ownership = cbm_text_owned_document_status(
skill_file, skill_content, released_skill_contents, RELEASED_SKILL_CONTENT_COUNT);
if (ownership == CLI_OK) {
return CBM_CODEX_SKILL_MANAGED;
}
return ownership == CLI_TRUE ? CBM_CODEX_SKILL_UNOWNED : CBM_CODEX_SKILL_UNSAFE;
}
static void cbm_warn_codex_skill_conflict(const cbm_codex_skill_paths_t *paths,
const char *reason) {
(void)fprintf(stderr,
"warning: Codex skill migration skipped: %s; documented_location=%s legacy=%s\n",
reason, paths->canonical_file, paths->legacy_file);
}
static bool cbm_refresh_codex_managed_skill(const cbm_codex_skill_paths_t *paths, bool dry_run) {
if (dry_run) {
return true;
}
int result =
cbm_text_migrate_owned_document(paths->canonical_file, skill_content,
released_skill_contents, RELEASED_SKILL_CONTENT_COUNT);
if (result == CLI_OK) {
return true;
}
record_agent_config_error(false, "Codex CLI", "skill_install", paths->canonical_file);
return false;
}
static bool cbm_remove_codex_managed_skill(const char *skills_dir, const char *skill_file,
bool dry_run, bool uninstalling, const char *operation) {
cbm_codex_skill_state_t state = cbm_codex_skill_state(skills_dir, skill_file);
if (state == CBM_CODEX_SKILL_ABSENT) {
return true;
}
if (state != CBM_CODEX_SKILL_MANAGED) {
if (state == CBM_CODEX_SKILL_UNOWNED) {
printf(" preserved non-owned Codex skill: %s\n", skill_file);
} else if (dry_run) {
printf(" preserved unsafe Codex skill path: %s\n", skill_file);
} else {
record_agent_config_error(uninstalling, "Codex CLI", operation, skill_file);
}
return state == CBM_CODEX_SKILL_UNOWNED;
}
if (dry_run) {
printf(" Codex skill: %s (would remove)\n", skill_file);
return true;
}
int removed = cbm_text_remove_owned_document_any(
skill_file, skill_content, released_skill_contents, RELEASED_SKILL_CONTENT_COUNT);
if (removed == CLI_OK &&
cbm_codex_skill_state(skills_dir, skill_file) == CBM_CODEX_SKILL_ABSENT) {
char skill_dir[CLI_BUF_1K];
int written = snprintf(skill_dir, sizeof(skill_dir), "%s/codebase-memory", skills_dir);
if (written > 0 && (size_t)written < sizeof(skill_dir)) {
(void)cbm_remove_empty_directory(skill_dir, false);
}
return true;
}
record_agent_config_error(uninstalling, "Codex CLI", operation, skill_file);
return false;
}
static void install_codex_skill(const char *home, const char *config_dir, bool force,
bool dry_run) {
cbm_codex_skill_paths_t paths;
if (!cbm_codex_skill_paths_init(home, config_dir, &paths)) {
if (!g_install_plan) {
record_agent_config_error(false, "Codex CLI", "skill_path", config_dir);
}
return;
}
cbm_codex_skill_state_t canonical =
cbm_codex_skill_state(paths.canonical_dir, paths.canonical_file);
cbm_codex_skill_state_t legacy =
paths.same_dir ? canonical : cbm_codex_skill_state(paths.legacy_dir, paths.legacy_file);
if (g_install_plan) {
if (canonical == CBM_CODEX_SKILL_ABSENT &&
(paths.same_dir || legacy == CBM_CODEX_SKILL_ABSENT ||
legacy == CBM_CODEX_SKILL_MANAGED)) {
plan_record("Codex CLI", "skill", paths.canonical_file);
}
if (!paths.same_dir && legacy == CBM_CODEX_SKILL_MANAGED &&
(canonical == CBM_CODEX_SKILL_ABSENT || canonical == CBM_CODEX_SKILL_MANAGED)) {
plan_record("Codex CLI", "cleanup_skill", paths.legacy_file);
}
return;
}
if (paths.same_dir) {
if (canonical == CBM_CODEX_SKILL_UNSAFE) {
record_agent_config_error(false, "Codex CLI", "skill_install", paths.canonical_file);
return;
}
if (!force && canonical == CBM_CODEX_SKILL_UNOWNED) {
printf(" preserved non-owned Codex skill: %s\n", paths.canonical_file);
return;
}
install_agent_skill("Codex CLI", paths.canonical_dir, force, dry_run);
if (!cbm_refresh_codex_managed_skill(&paths, dry_run)) {
return;
}
if (!dry_run && cbm_codex_skill_state(paths.canonical_dir, paths.canonical_file) !=
CBM_CODEX_SKILL_MANAGED) {
record_agent_config_error(false, "Codex CLI", "skill_install", paths.canonical_file);
}
return;
}
if (canonical == CBM_CODEX_SKILL_UNSAFE) {
if (dry_run) {
cbm_warn_codex_skill_conflict(&paths, "the canonical skill path is unsafe");
} else {
record_agent_config_error(false, "Codex CLI", "skill_install", paths.canonical_file);
}
return;
}
if (legacy == CBM_CODEX_SKILL_UNSAFE) {
if (force && !dry_run) {
record_agent_config_error(false, "Codex CLI", "legacy_skill_migration",
paths.legacy_file);
} else {
cbm_warn_codex_skill_conflict(&paths, "the legacy skill path is unsafe");
}
return;
}
if (!force && legacy == CBM_CODEX_SKILL_UNOWNED) {
cbm_warn_codex_skill_conflict(&paths, "the legacy skill is not installer-owned");
return;
}
if (!force && canonical == CBM_CODEX_SKILL_UNOWNED) {
if (legacy == CBM_CODEX_SKILL_MANAGED) {
cbm_warn_codex_skill_conflict(&paths, "the canonical skill is not installer-owned");
} else {
printf(" preserved non-owned Codex skill: %s\n", paths.canonical_file);
}
return;
}
bool remove_legacy = legacy == CBM_CODEX_SKILL_MANAGED;
if (force && legacy == CBM_CODEX_SKILL_UNOWNED) {
remove_legacy = true;
if (dry_run) {
printf(" legacy Codex skill: %s (would replace for migration)\n", paths.legacy_file);
} else {
int replaced = cbm_install_skills(paths.legacy_dir, true, false);
printf(" legacy Codex skill: %s (%d updated for migration)\n", paths.legacy_file,
replaced);
if (cbm_codex_skill_state(paths.legacy_dir, paths.legacy_file) !=
CBM_CODEX_SKILL_MANAGED) {
record_agent_config_error(false, "Codex CLI", "legacy_skill_migration",
paths.legacy_file);
return;
}
}
}
bool install_canonical = force || canonical == CBM_CODEX_SKILL_ABSENT;
if (install_canonical) {
install_agent_skill("Codex CLI", paths.canonical_dir, force, dry_run);
} else {
printf(" skill: %s (0 installed)\n", paths.canonical_file);
}
if (dry_run) {
if (remove_legacy) {
printf(" legacy Codex skill: %s (would remove after canonical install)\n",
paths.legacy_file);
}
return;
}
if (!cbm_refresh_codex_managed_skill(&paths, false)) {
return;
}
if (cbm_codex_skill_state(paths.canonical_dir, paths.canonical_file) !=
CBM_CODEX_SKILL_MANAGED) {
record_agent_config_error(false, "Codex CLI", "skill_install", paths.canonical_file);
return;
}
if (remove_legacy && cbm_remove_codex_managed_skill(paths.legacy_dir, paths.legacy_file, false,
false, "legacy_skill_cleanup")) {
printf(" legacy Codex skill: %s (removed)\n", paths.legacy_file);
}
}
/* Derive tier siblings only from the exact shipped Verify basename. This keeps
* vendor-specific suffixes such as .agent.md, .toml, and .json intact. */
static int cbm_tiered_profile_path(const char *verify_path, cbm_graph_tier_t tier, char *output,
@@ -9635,6 +10167,35 @@ static void install_devin_durable_context(const cbm_agent_registry_context_t *re
static void install_generated_client_extension(const char *label, const char *path,
const char *binary_path,
char *(*generate)(const char *), bool dry_run) {
/* #2204: a module that already carries content but none of our markers is
* the user's. The generated header promises "remove the markers to take
* ownership", and appending our prelude to a foreign module produces a file
* the client cannot even parse — the prelude redeclares the user's own
* import and const names, and both sides declare a default export. Probe
* ownership before generating so the preview names the refusal instead of
* promising a write the run will not make. A probe failure (-1) falls
* through to the write, which reports it through the usual error path. */
bool foreign =
cbm_text_managed_block_status(path, CBM_ADAPTER_MARKER_START, CBM_ADAPTER_MARKER_END) == 1;
if (foreign) {
/* The --plan receipt (#388) lists only writes the run will make, and
* its stdout carries the receipt JSON, so plan mode records nothing
* and prints nothing here. The dry run names the skip instead. */
if (g_install_plan) {
return;
}
if (dry_run) {
printf(" extension: %s would be skipped — the file there is not ours (no "
"codebase-memory-mcp markers), so it is left untouched; delete it or add the "
"markers to let the installer manage it\n",
path);
} else {
printf(" extension: %s left untouched — the file there is not ours (no "
"codebase-memory-mcp markers)\n",
path);
}
return;
}
if (g_install_plan) {
plan_record(label, "extension", path);
return;
@@ -9952,12 +10513,10 @@ static void install_cli_agent_configs(const cbm_detected_agents_t *agents, const
char config_dir[CLI_BUF_1K];
char cp[CLI_BUF_1K];
char ip[CLI_BUF_1K];
char skills_dir[CLI_BUF_1K];
char ap[CLI_BUF_1K];
cbm_codex_config_dir(home, config_dir, sizeof(config_dir));
snprintf(cp, sizeof(cp), "%s/config.toml", config_dir);
snprintf(ip, sizeof(ip), "%s/AGENTS.md", config_dir);
snprintf(skills_dir, sizeof(skills_dir), "%s/skills", config_dir);
snprintf(ap, sizeof(ap), "%s/agents/codebase-memory.toml", config_dir);
/* A broken hook config must not keep legacy full guidance active. */
bool pointer_installed = install_codex_activation_pointer(ip, dry_run);
@@ -9998,7 +10557,7 @@ static void install_cli_agent_configs(const cbm_detected_agents_t *agents, const
if (!g_install_plan) {
report_codex_activation_pointer_install(ip, pointer_installed);
}
install_agent_skill("Codex CLI", skills_dir, force, dry_run);
install_codex_skill(home, config_dir, force, dry_run);
install_tiered_agent_profiles(
(cbm_tiered_profile_set_t){
.label = "Codex CLI",
@@ -11310,6 +11869,7 @@ static char *cbm_build_install_plan_json_options(const char *home, const char *b
yyjson_mut_val *agent_files = yyjson_mut_arr(doc);
yyjson_mut_val *prompt_files = yyjson_mut_arr(doc);
yyjson_mut_val *hooks = yyjson_mut_arr(doc);
yyjson_mut_val *cleanup_actions = yyjson_mut_arr(doc);
for (int i = 0; i < plan.count; i++) {
cbm_plan_entry_t *e = &plan.items[i];
if (strcmp(e->kind, "mcp_config") == 0) {
@@ -11328,6 +11888,14 @@ static char *cbm_build_install_plan_json_options(const char *home, const char *b
} else if (strcmp(e->kind, "prompt") == 0) {
yyjson_mut_arr_add_strcpy(doc, prompt_files, e->path);
yyjson_mut_arr_add_strcpy(doc, instrs, e->path);
} else if (strcmp(e->kind, "cleanup_skill") == 0) {
yyjson_mut_val *cleanup = yyjson_mut_obj(doc);
yyjson_mut_obj_add_strcpy(doc, cleanup, "agent", e->agent);
yyjson_mut_obj_add_str(doc, cleanup, "kind", "skill");
yyjson_mut_obj_add_str(doc, cleanup, "operation",
"remove_owned_legacy_copy_if_migrated");
yyjson_mut_obj_add_strcpy(doc, cleanup, "path", e->path);
yyjson_mut_arr_add_val(cleanup_actions, cleanup);
} else {
yyjson_mut_arr_add_strcpy(doc, instrs, e->path);
}
@@ -11338,6 +11906,7 @@ static char *cbm_build_install_plan_json_options(const char *home, const char *b
yyjson_mut_obj_add_val(doc, root, "agent_files_planned", agent_files);
yyjson_mut_obj_add_val(doc, root, "prompt_files_planned", prompt_files);
yyjson_mut_obj_add_val(doc, root, "hooks_planned", hooks);
yyjson_mut_obj_add_val(doc, root, "cleanup_actions_planned", cleanup_actions);
yyjson_mut_obj_add_bool(doc, root, "writes_started", false);
yyjson_mut_obj_add_bool(doc, root, "network_after_install", false);
yyjson_mut_obj_add_str(doc, root, "next_safe_command", "codebase-memory-mcp install -y");
@@ -12063,6 +12632,40 @@ static void uninstall_agent_skill(const char *label, const char *skills_dir, boo
printf(" %s skill: %d removed\n", label, removed);
}
static void uninstall_codex_skills(const char *home, const char *config_dir, bool dry_run) {
cbm_codex_skill_paths_t paths;
if (!cbm_codex_skill_paths_init(home, config_dir, &paths)) {
if (!dry_run) {
record_agent_config_error(true, "Codex CLI", "skill_path", config_dir);
}
return;
}
int removed = 0;
if (cbm_codex_skill_state(paths.canonical_dir, paths.canonical_file) ==
CBM_CODEX_SKILL_MANAGED) {
if (cbm_remove_codex_managed_skill(paths.canonical_dir, paths.canonical_file, dry_run, true,
"skill_uninstall")) {
removed++;
}
} else {
(void)cbm_remove_codex_managed_skill(paths.canonical_dir, paths.canonical_file, dry_run,
true, "skill_uninstall");
}
if (!paths.same_dir &&
cbm_codex_skill_state(paths.legacy_dir, paths.legacy_file) == CBM_CODEX_SKILL_MANAGED) {
if (cbm_remove_codex_managed_skill(paths.legacy_dir, paths.legacy_file, dry_run, true,
"legacy_skill_uninstall")) {
removed++;
}
} else if (!paths.same_dir) {
(void)cbm_remove_codex_managed_skill(paths.legacy_dir, paths.legacy_file, dry_run, true,
"legacy_skill_uninstall");
}
printf(" Codex CLI skills: %d removed\n", removed);
}
static void uninstall_copilot_durable_context(const char *home, bool dry_run) {
char config_dir[CLI_BUF_1K];
char hook_path[CLI_BUF_1K];
@@ -12484,13 +13087,11 @@ static void uninstall_cli_agents(const cbm_detected_agents_t *agents, const char
char config_dir[CLI_BUF_1K];
char cp[CLI_BUF_1K];
char ip[CLI_BUF_1K];
char skills_dir[CLI_BUF_1K];
char ap[CLI_BUF_1K];
char installed_binary[CLI_BUF_1K];
cbm_codex_config_dir(home, config_dir, sizeof(config_dir));
snprintf(cp, sizeof(cp), "%s/config.toml", config_dir);
snprintf(ip, sizeof(ip), "%s/AGENTS.md", config_dir);
snprintf(skills_dir, sizeof(skills_dir), "%s/skills", config_dir);
snprintf(ap, sizeof(ap), "%s/agents/codebase-memory.toml", config_dir);
bool pointer_removed = uninstall_codex_activation_pointer(ip, dry_run);
cbm_agent_installed_binary_path(home, installed_binary, sizeof(installed_binary));
@@ -12525,7 +13126,7 @@ static void uninstall_cli_agents(const cbm_detected_agents_t *agents, const char
record_agent_config_error(true, "Codex CLI", "hook_uninstall", cp);
}
codex_toml_done:
uninstall_agent_skill("Codex CLI", skills_dir, dry_run);
uninstall_codex_skills(home, config_dir, dry_run);
uninstall_tiered_agent_profiles(
(cbm_tiered_profile_set_t){
.label = "Codex CLI",
+5
View File
@@ -182,6 +182,11 @@ typedef struct {
/* Get the array of skill definitions. */
const cbm_skill_t *cbm_get_skills(void);
#ifdef CBM_CLI_ENABLE_TEST_API
size_t cbm_cli_released_skill_content_count(void);
const char *cbm_cli_released_skill_content_at(size_t index);
#endif
/* Install skills to skills_dir (e.g. ~/.claude/skills/).
* If force is true, overwrite existing skills.
* Returns count of skills written. */
+73 -13
View File
@@ -1020,6 +1020,42 @@ static int text_validate_markers(const char *begin_marker, const char *end_marke
return TEXT_OK;
}
/* A managed document as read from disk, with our block located in it. */
typedef struct {
char *data;
size_t len;
size_t bom_len;
text_file_snapshot_t snapshot;
text_managed_region_t region;
} text_managed_document_t;
/* The read-validate-scan step shared by the upsert and the read-only ownership
* probe, so both judge a file by the same rules. An absent file reads as an
* empty document. On success the caller owns doc->data and releases it; on
* failure nothing is left allocated. */
static int text_read_managed_document(const char *file_path, const char *begin_marker,
size_t begin_len, const char *end_marker, size_t end_len,
text_managed_document_t *doc) {
doc->data = NULL;
doc->len = 0U;
doc->bom_len = 0U;
int status = text_read_file(file_path, &doc->data, &doc->len, &doc->snapshot);
if (status == TEXT_OK) {
status = text_validate_bytes(doc->data, doc->len, 1);
}
if (status == TEXT_OK) {
doc->bom_len = text_bom_length(doc->data, doc->len);
status = text_scan_managed_region(doc->data, doc->len, doc->bom_len, begin_marker,
begin_len, end_marker, end_len, &doc->region);
}
if (status != TEXT_OK) {
free(doc->data);
doc->data = NULL;
return TEXT_ERROR;
}
return TEXT_OK;
}
static int text_upsert_managed_block(const char *file_path, const char *begin_marker,
const char *end_marker, const char *owned_content,
size_t max_document_bytes) {
@@ -1036,21 +1072,16 @@ static int text_upsert_managed_block(const char *file_path, const char *begin_ma
return TEXT_ERROR;
}
char *old_data = NULL;
size_t old_len = 0U;
text_file_snapshot_t snapshot;
if (text_read_file(file_path, &old_data, &old_len, &snapshot) != TEXT_OK ||
text_validate_bytes(old_data, old_len, 1) != TEXT_OK) {
free(old_data);
return TEXT_ERROR;
}
size_t bom_len = text_bom_length(old_data, old_len);
text_managed_region_t region;
if (text_scan_managed_region(old_data, old_len, bom_len, begin_marker, begin_len, end_marker,
end_len, &region) != TEXT_OK) {
free(old_data);
text_managed_document_t doc;
int read_status =
text_read_managed_document(file_path, begin_marker, begin_len, end_marker, end_len, &doc);
if (read_status != TEXT_OK) {
return TEXT_ERROR;
}
char *old_data = doc.data;
size_t old_len = doc.len;
text_file_snapshot_t snapshot = doc.snapshot;
text_managed_region_t region = doc.region;
text_buffer_t updated = {0};
int result = TEXT_ERROR;
@@ -1070,6 +1101,7 @@ static int text_upsert_managed_block(const char *file_path, const char *begin_ma
&snapshot);
}
} else {
size_t bom_len = doc.bom_len;
const char *eol = NULL;
size_t eol_len = 0U;
text_detect_eol(old_data + bom_len, old_len - bom_len, &eol, &eol_len);
@@ -1105,6 +1137,34 @@ int cbm_text_upsert_managed_block_limited(const char *file_path, const char *beg
max_document_bytes);
}
int cbm_text_managed_block_status(const char *file_path, const char *begin_marker,
const char *end_marker) {
size_t begin_len = 0U;
size_t end_len = 0U;
if (!text_valid_path(file_path) ||
text_validate_markers(begin_marker, end_marker, &begin_len, &end_len) != TEXT_OK) {
return TEXT_ERROR;
}
text_managed_document_t doc;
int read_status =
text_read_managed_document(file_path, begin_marker, begin_len, end_marker, end_len, &doc);
if (read_status != TEXT_OK) {
return TEXT_ERROR;
}
int result = TEXT_OK;
if (!doc.region.present) {
for (size_t pos = doc.bom_len; pos < doc.len; pos++) {
char byte = doc.data[pos];
if (byte != ' ' && byte != '\t' && byte != '\r' && byte != '\n') {
result = TEXT_UNOWNED;
break;
}
}
}
free(doc.data);
return result;
}
int cbm_text_remove_managed_block(const char *file_path, const char *begin_marker,
const char *end_marker) {
size_t begin_len = 0U;
+10
View File
@@ -11,6 +11,16 @@ int cbm_text_upsert_managed_block(const char *file_path, const char *begin_marke
int cbm_text_upsert_managed_block_limited(const char *file_path, const char *begin_marker,
const char *end_marker, const char *owned_content,
size_t max_document_bytes);
/* Read-only ownership probe for a managed block, used before writing a file a
* client auto-loads as a module. Returns 0 when the block may be written (no
* file, a file whose content is only whitespace, or a file already carrying our
* markers), 1 when the file carries content but none of our markers — the
* caller must leave the user's file alone and say so rather than append to it,
* because the generated block redeclares module-level names a hand-ported
* module also declares (#2204) — and -1 for unsafe state or I/O failure. Never
* writes, so a dry run may call it. */
int cbm_text_managed_block_status(const char *file_path, const char *begin_marker,
const char *end_marker);
int cbm_text_remove_managed_block(const char *file_path, const char *begin_marker,
const char *end_marker);
+9 -5
View File
@@ -2146,6 +2146,9 @@ static bool application_index_args_normalize_defaults(yyjson_mut_val *root) {
if (name && (!yyjson_mut_is_str(name) || yyjson_mut_get_len(name) == 0)) {
(void)yyjson_mut_obj_remove_key(root, "name");
}
/* Automatic jobs carry _cbm_background, which picks the worker count and
* never what is indexed, so an explicit request joins them. */
(void)yyjson_mut_obj_remove_key(root, "_cbm_background");
return true;
}
@@ -2559,10 +2562,10 @@ static char *application_auto_index_args(cbm_daemon_application_t *application,
return NULL;
}
yyjson_mut_doc_set_root(document, root);
char *args = yyjson_mut_obj_add_strcpy(document, root, "repo_path", root_path) &&
application_index_args_add_policy(application, document, root)
? yyjson_mut_write(document, 0, NULL)
: NULL;
bool encoded = yyjson_mut_obj_add_strcpy(document, root, "repo_path", root_path) &&
application_index_args_add_policy(application, document, root) &&
yyjson_mut_obj_add_bool(document, root, "_cbm_background", true);
char *args = encoded ? yyjson_mut_write(document, 0, NULL) : NULL;
yyjson_mut_doc_free(document);
return args;
}
@@ -4423,7 +4426,8 @@ static int application_background_index(cbm_daemon_application_t *application,
}
yyjson_mut_doc_set_root(document, root);
bool encoded = yyjson_mut_obj_add_strcpy(document, root, "repo_path", canonical_root) &&
application_index_args_add_policy(application, document, root);
application_index_args_add_policy(application, document, root) &&
yyjson_mut_obj_add_bool(document, root, "_cbm_background", true);
char *default_project = cbm_project_name_from_path(canonical_root);
bool custom_project =
project_name[0] && (!default_project || strcmp(default_project, project_name) != 0);
+95 -1
View File
@@ -10368,6 +10368,72 @@ static void trace_call_coverage(cbm_store_t *store, const char *project,
cbm_ht_free(match.targets);
}
enum { TRACE_RECURSION_FLAG_COUNT = 2 };
typedef enum {
TRACE_RECURSION_UNKNOWN = -1,
TRACE_RECURSION_FALSE,
TRACE_RECURSION_TRUE,
} trace_recursion_flag_t;
static const char *const trace_recursion_keys[TRACE_RECURSION_FLAG_COUNT] = {"self_recursive",
"recursive"};
typedef struct {
/* -1: unknown, 0: all roots record false, 1: at least one records true. */
trace_recursion_flag_t flags[TRACE_RECURSION_FLAG_COUNT];
} trace_recursion_t;
static trace_recursion_t trace_recursion_metadata(const cbm_node_t *nodes, int node_count) {
trace_recursion_t metadata = {0};
/* Use the same roots as bfs_union_same_name, rather than borrowing a
* callee's flag or selecting just one definition/stub. These are stored
* analysis signals, not evidence that a resolved self-call was dropped. */
for (int i = 0; i < node_count; i++) {
const char *properties = nodes[i].properties_json;
yyjson_doc *doc = properties ? yyjson_read(properties, strlen(properties), 0) : NULL;
yyjson_val *root = doc ? yyjson_doc_get_root(doc) : NULL;
for (int j = 0; j < TRACE_RECURSION_FLAG_COUNT; j++) {
yyjson_val *value = yyjson_obj_get(root, trace_recursion_keys[j]);
if (yyjson_is_bool(value) && yyjson_get_bool(value)) {
metadata.flags[j] = TRACE_RECURSION_TRUE;
} else if (!yyjson_is_bool(value) && metadata.flags[j] != TRACE_RECURSION_TRUE) {
metadata.flags[j] = TRACE_RECURSION_UNKNOWN;
}
}
yyjson_doc_free(doc);
}
return metadata;
}
static void trace_recursion_tree(cbm_sb_t *sb, const trace_recursion_t *metadata,
bool includes_calls) {
for (int i = 0; i < TRACE_RECURSION_FLAG_COUNT; i++) {
if (metadata->flags[i] == TRACE_RECURSION_UNKNOWN) {
cbm_sb_append(sb, trace_recursion_keys[i]);
cbm_sb_append(sb, ": null\n");
} else {
cbm_tree_scalar_bool(sb, trace_recursion_keys[i],
metadata->flags[i] == TRACE_RECURSION_TRUE);
}
}
if (includes_calls) {
cbm_tree_scalar_str(sb, "self_call_policy", "omitted");
}
}
static void trace_recursion_json(yyjson_mut_doc *doc, yyjson_mut_val *root,
const trace_recursion_t *metadata, bool includes_calls) {
for (int i = 0; i < TRACE_RECURSION_FLAG_COUNT; i++) {
if (metadata->flags[i] == TRACE_RECURSION_UNKNOWN) {
yyjson_mut_obj_add_null(doc, root, trace_recursion_keys[i]);
} else {
yyjson_mut_obj_add_bool(doc, root, trace_recursion_keys[i],
metadata->flags[i] == TRACE_RECURSION_TRUE);
}
}
if (includes_calls) {
yyjson_mut_obj_add_str(doc, root, "self_call_policy", "omitted");
}
}
static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
char *func_name = cbm_mcp_get_string_arg(args, "function_name");
char *project = get_project_arg(args);
@@ -10722,6 +10788,7 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
.do_inbound = do_inbound};
trace_call_coverage(store, project, &coverage_scope, trace_includes_calls, &unresolved_out,
&unresolved_in);
trace_recursion_t recursion = trace_recursion_metadata(nodes, node_count);
const char *out_relation = unresolved_out ? "unknown" : (tr_out.truncated ? "gte" : "eq");
const char *in_relation = unresolved_in ? "unknown" : (tr_in.truncated ? "gte" : "eq");
@@ -10805,6 +10872,7 @@ render_trace_output:;
if (mode) {
cbm_tree_scalar_str(&sb, "mode", mode);
}
trace_recursion_tree(&sb, &recursion, trace_includes_calls);
/* Grouped tree is THE default; risk_labels/data_flow keep the flat
* table (extra columns) in the same tree syntax. */
bool render_risk = risk_labels && emit_optional_fields;
@@ -10888,6 +10956,7 @@ render_trace_output:;
if (mode) {
yyjson_mut_obj_add_str(doc, root, "mode", mode);
}
trace_recursion_json(doc, root, &recursion, trace_includes_calls);
if (do_outbound) {
yyjson_mut_obj_add_int(doc, root, "callees_total", out_total);
yyjson_mut_obj_add_str(doc, root, "callees_total_relation", out_relation);
@@ -10955,6 +11024,7 @@ render_trace_output:;
if (!trace_legacy_json) {
cbm_sb_t floor;
cbm_sb_init(&floor);
trace_recursion_tree(&floor, &recursion, trace_includes_calls);
if (do_outbound) {
cbm_tree_scalar_int(&floor, "callees_total", out_total);
cbm_tree_scalar_str(&floor, "callees_total_relation", out_relation);
@@ -10983,6 +11053,7 @@ render_trace_output:;
yyjson_mut_doc *floor_doc = yyjson_mut_doc_new(NULL);
yyjson_mut_val *floor = yyjson_mut_obj(floor_doc);
yyjson_mut_doc_set_root(floor_doc, floor);
trace_recursion_json(floor_doc, floor, &recursion, trace_includes_calls);
if (do_outbound) {
yyjson_mut_obj_add_int(floor_doc, floor, "callees_total", out_total);
yyjson_mut_obj_add_str(floor_doc, floor, "callees_total_relation",
@@ -12492,7 +12563,8 @@ static char *index_run_supervised_path(cbm_mcp_server_t *srv, const char *root_p
* worker reads no config); unset, the worker's index mode decides. */
if (!yyjson_mut_obj_add_strcpy(doc, root, "repo_path", root_path) ||
!cbm_mcp_index_policy_add_to_args(doc, root, &policy) ||
!cbm_mcp_index_doc_candidates_add_to_args(doc, root, srv ? srv->config : NULL)) {
!cbm_mcp_index_doc_candidates_add_to_args(doc, root, srv ? srv->config : NULL) ||
!yyjson_mut_obj_add_bool(doc, root, "_cbm_background", true)) {
yyjson_mut_doc_free(doc);
return NULL;
}
@@ -12795,6 +12867,22 @@ static bool index_root_owner_resolve(const char *repo_path, char **owner_out, ch
return false;
}
/* Background CPU headroom is for a refresh of a graph that already serves.
* A project's first index keeps every core, even when it was started
* automatically: the caller is waiting on a graph that does not exist yet.
* An empty or unreadable file is not a committed index. Both sites that
* honour automatic indexing — handle_index_repository's _cbm_background
* flag and the in-process autoindex_thread — go through this so they
* cannot drift. */
static bool project_has_committed_index(const char *project) {
if (!project || !project[0]) {
return false;
}
char db_path[CBM_SZ_1K];
project_db_path(project, db_path, sizeof(db_path));
return db_path[0] && project_db_is_servable(project, db_path);
}
/* The three heap strings handle_index_repository owns from
* cbm_mcp_get_string_arg / resolved_repo_path_from_project_arg. One release
* point keeps the dozen early-return paths in step; free(NULL) is a no-op, so
@@ -12924,6 +13012,7 @@ static char *handle_index_repository(cbm_mcp_server_t *srv, const char *args) {
char *repo_path = cbm_mcp_get_string_arg(args, "repo_path");
char *mode_str = cbm_mcp_get_string_arg(args, "mode");
char *name_override = cbm_mcp_get_string_arg(args, "name");
bool background = cbm_mcp_get_bool_arg(args, "_cbm_background");
cbm_normalize_path_sep(repo_path);
if (!repo_path) {
@@ -13096,6 +13185,10 @@ static char *handle_index_repository(cbm_mcp_server_t *srv, const char *args) {
return cbm_mcp_text_result("invalid project name", true);
}
free(name_override);
/* Headroom follows the final project name, decided before artifact
* bootstrap can create the first local copy of this index. */
cbm_pipeline_set_background(p, background &&
project_has_committed_index(cbm_pipeline_project_name(p)));
cbm_pipeline_set_persistence(p, persistence);
cbm_pipeline_set_resource_policy(p, &resource_policy);
index_doc_candidates_apply(p, doc_candidates);
@@ -21516,6 +21609,7 @@ static void *autoindex_thread(void *arg) {
return NULL;
}
index_doc_candidates_apply(p, index_doc_candidates_choice(srv, NULL));
cbm_pipeline_set_background(p, project_has_committed_index(cbm_pipeline_project_name(p)));
/* Block until any concurrent pipeline finishes */
cbm_pipeline_lock();
+23 -10
View File
@@ -1130,6 +1130,28 @@ int cbm_artifact_export(const char *db_path, const char *repo_path, const char *
/* ── Import ──────────────────────────────────────────────────────── */
/* Restore the imported ADR (carried in the graph DB's project_summaries) into
* the per-project sidecar so it arrives with the shared artifact — even for a
* teammate who already indexed the project (a marked sidecar with no ADR),
* which the marker-gated migrate-once would skip. INSERT OR IGNORE means a
* local ADR is never overwritten. Best-effort: the graph DB is already
* installed when this runs, so a failed restore never fails the import, but it
* is logged with the store's reason so a team ADR that did not arrive is
* diagnosable instead of silently missing. */
static void artifact_import_restore_adr(const char *cache_db_path) {
cbm_store_t *restore = cbm_store_open_path_existing(cache_db_path);
if (!restore) {
cbm_log_warn("artifact.import", "err", "adr_restore", "detail", "reopen_imported_db",
"path", cache_db_path);
return;
}
if (cbm_store_adr_restore_from_graph_if_absent(restore) != CBM_STORE_OK) {
cbm_log_warn("artifact.import", "err", "adr_restore", "detail", cbm_store_error(restore),
"path", cache_db_path);
}
cbm_store_close(restore);
}
int cbm_artifact_import(const char *repo_path, const char *cache_db_path) {
if (!repo_path || !cache_db_path) {
return CBM_NOT_FOUND;
@@ -1257,16 +1279,7 @@ int cbm_artifact_import(const char *repo_path, const char *cache_db_path) {
cbm_unlink(wal);
cbm_unlink(shm);
/* Restore the imported ADR (carried in the graph DB's project_summaries) into
* the per-project sidecar so it arrives with the shared artifact — even for a
* teammate who already indexed the project (a marked sidecar with no ADR),
* which the marker-gated migrate-once would skip. INSERT OR IGNORE means a
* local ADR is never overwritten. Best-effort. */
cbm_store_t *restore = cbm_store_open_path_existing(cache_db_path);
if (restore) {
(void)cbm_store_adr_restore_from_graph_if_absent(restore);
cbm_store_close(restore);
}
artifact_import_restore_adr(cache_db_path);
cbm_log_info("artifact.import", "db", cache_db_path, "size_mb",
itoa_buf((int)((size_t)dlen / ART_BYTES_PER_MB)));
+50 -3
View File
@@ -486,10 +486,12 @@ typedef struct {
const CBMLSPDef *all_defs;
const int *def_starts;
cbm_lsp_surface_row_t *rows;
int file_count;
_Atomic int next_file;
_Atomic bool failed;
} surface_job_t;
static void surface_row_one(int i, void *arg) {
static void surface_row_build(int i, void *arg) {
surface_job_t *job = (surface_job_t *)arg;
if (atomic_load_explicit(&job->failed, memory_order_relaxed)) {
return;
@@ -529,6 +531,45 @@ static void surface_row_one(int i, void *arg) {
}
}
#if defined(CBM_ENABLE_TEST_SEAMS) && CBM_ENABLE_TEST_SEAMS
static cbm_lsp_surface_row_observer_fn g_row_observer;
static void *g_row_observer_ctx;
void cbm_lsp_surface_test_set_row_observer(cbm_lsp_surface_row_observer_fn observer, void *ctx) {
g_row_observer = observer;
g_row_observer_ctx = ctx;
}
#endif
static void surface_row_one(int i, void *arg) {
#if defined(CBM_ENABLE_TEST_SEAMS) && CBM_ENABLE_TEST_SEAMS
if (g_row_observer) {
g_row_observer(i, true, g_row_observer_ctx);
}
#endif
surface_row_build(i, arg);
#if defined(CBM_ENABLE_TEST_SEAMS) && CBM_ENABLE_TEST_SEAMS
if (g_row_observer) {
g_row_observer(i, false, g_row_observer_ctx);
}
#endif
}
/* The pool also runs work on its caller. Dispatch at most one coarse job per
* allowed participant, each stealing files, so the total active row builders
* cannot exceed the pipeline budget even when the caller takes a job. */
static void surface_rows_worker(int worker, void *arg) {
(void)worker;
surface_job_t *job = arg;
for (;;) {
int i = atomic_fetch_add_explicit(&job->next_file, 1, memory_order_relaxed);
if (i >= job->file_count) {
break;
}
surface_row_one(i, job);
}
}
int cbm_lsp_surface_build_rows(const cbm_pipeline_ctx_t *ctx, const char *project,
CBMFileResult **cache, const cbm_file_info_t *files, int file_count,
const CBMLSPDef *all_defs, const int *def_starts,
@@ -550,10 +591,16 @@ int cbm_lsp_surface_build_rows(const cbm_pipeline_ctx_t *ctx, const char *projec
.all_defs = all_defs,
.def_starts = def_starts,
.rows = rows,
.file_count = file_count,
};
atomic_init(&job.next_file, 0);
atomic_init(&job.failed, false);
cbm_parallel_for(file_count, surface_row_one, &job,
(cbm_parallel_for_opts_t){.max_workers = 0, .force_pthreads = false});
int workers = cbm_pipeline_worker_count(ctx ? ctx->pipeline : NULL);
if (workers > file_count) {
workers = file_count;
}
cbm_parallel_for(workers, surface_rows_worker, &job,
(cbm_parallel_for_opts_t){.max_workers = workers, .force_pthreads = false});
if (atomic_load_explicit(&job.failed, memory_order_relaxed)) {
cbm_store_free_lsp_surfaces(rows, file_count); /* untouched rows are all NULL */
return -1;
+7
View File
@@ -68,4 +68,11 @@ int cbm_lsp_surface_python_namespace_from_json(CBMArena *arena, const char *json
*/
int cbm_lsp_surface_python_change(const char *old_json, const char *new_json);
#if defined(CBM_ENABLE_TEST_SEAMS) && CBM_ENABLE_TEST_SEAMS
/* Bracket each file callback, including skipped/error rows. Install only with
* no surface build running; clear after the observed build returns. */
typedef void (*cbm_lsp_surface_row_observer_fn)(int index, bool entering, void *ctx);
void cbm_lsp_surface_test_set_row_observer(cbm_lsp_surface_row_observer_fn observer, void *ctx);
#endif
#endif /* CBM_PIPELINE_LSP_SURFACE_H */
+10
View File
@@ -720,6 +720,16 @@ static cr_match_result_t match_http_routes(cbm_store_t *src_store, const char *s
char method[CBM_SZ_32] = {0};
json_str_prop(props, "url_path", url_path, sizeof(url_path));
json_str_prop(props, "method", method, sizeof(method));
if (!method[0]) {
char callee[CBM_SZ_256] = {0};
json_str_prop(props, "callee", callee, sizeof(callee));
if (callee[0]) {
const char *m = cbm_service_pattern_http_method(callee);
if (m) {
snprintf(method, sizeof(method), "%s", m);
}
}
}
if (!url_path[0]) {
continue;
}
+49 -7
View File
@@ -2621,19 +2621,37 @@ static void detect_url_in_args(cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *source,
if (!cbm_service_pattern_is_http_route_literal(norm, call->callee_name)) {
continue;
}
const char *method = cbm_service_pattern_http_method(call->callee_name);
const char *prefix = method ? method : "ANY";
char route_qn[CBM_ROUTE_QN_SIZE];
char cpath[CBM_SZ_256];
snprintf(route_qn, sizeof(route_qn), "__route__ANY__%s",
snprintf(route_qn, sizeof(route_qn), "__route__%s__%s", prefix,
cbm_route_canon_path(norm, cpath, sizeof(cpath)));
int64_t route_id = cbm_gbuf_upsert_node(gbuf, "Route", norm, route_qn, "", 0, 0,
"{\"source\":\"arg_url\"}");
char route_props[CBM_SZ_256];
if (method) {
snprintf(route_props, sizeof(route_props), "{\"source\":\"arg_url\",\"method\":\"%s\"}",
method);
} else {
snprintf(route_props, sizeof(route_props), "{\"source\":\"arg_url\"}");
}
int64_t route_id =
cbm_gbuf_upsert_node(gbuf, "Route", norm, route_qn, "", 0, 0, route_props);
char esc_c[CBM_SZ_256];
char esc_n[CBM_SZ_256];
cbm_json_escape(esc_c, sizeof(esc_c), call->callee_name);
cbm_json_escape(esc_n, sizeof(esc_n), norm);
char eprops[CBM_SZ_512];
snprintf(eprops, sizeof(eprops),
"{\"callee\":\"%s\",\"url_path\":\"%s\",\"via\":\"arg_url\"}", esc_c, esc_n);
/* Two escaped 255-byte strings plus the fixed keys and the verb: 1K
* never truncates the JSON (512 could, once the method was added). */
char eprops[CBM_SZ_1K];
if (method) {
snprintf(
eprops, sizeof(eprops),
"{\"callee\":\"%s\",\"url_path\":\"%s\",\"via\":\"arg_url\",\"method\":\"%s\"}",
esc_c, esc_n, method);
} else {
snprintf(eprops, sizeof(eprops),
"{\"callee\":\"%s\",\"url_path\":\"%s\",\"via\":\"arg_url\"}", esc_c, esc_n);
}
cbm_gbuf_insert_edge(gbuf, source->id, route_id, "HTTP_CALLS", eprops);
break;
}
@@ -2985,6 +3003,29 @@ static const cbm_gbuf_node_t *find_source_node(const cbm_gbuf_t *gbuf, const cha
/* Field type hint resolution for obj.Method() with multiple candidates.
* Strips C# field prefixes (_ / m_), capitalizes to get type name, and
* checks if TypeName.Method or ITypeName.Method exists among candidates. */
/* #1927: the hint may only bind a candidate whose OWNING dot-segment — the
* segment immediately before the method name — EQUALS the hinted type. The
* previous raw strstr() accepted the hinted name anywhere in the QN, so a
* single-letter receiver (`f.Close()` → hint "F") matched nearly every
* candidate ("F" ⊂ "…FileStore.Close", the method name included) and rebound
* a call whose resolution had already failed to an arbitrary project symbol
* at PP_FIELD_HINT_CONF. Segment equality keeps the intended
* variable-named-after-its-type case (`repo.Find()` → `Repo.Find`,
* lrp_go_s8_field_type_hint) and picks the candidate whose owner actually
* carries the name instead of whichever QN contains the letters. */
static bool fth_owner_segment_is(const char *candidate_qn, const char *type_name) {
const char *last_dot = strrchr(candidate_qn, '.');
if (!last_dot || last_dot == candidate_qn) {
return false;
}
const char *seg_start = last_dot;
while (seg_start > candidate_qn && seg_start[-1] != '.') {
seg_start--;
}
size_t seg_len = (size_t)(last_dot - seg_start);
return seg_len > 0 && strncmp(seg_start, type_name, seg_len) == 0 && type_name[seg_len] == '\0';
}
static void try_field_type_hint(resolve_ctx_t *rc, cbm_resolution_t *res, const char *callee_name,
int64_t source_id) {
if (!res->qualified_name || res->candidate_count <= SKIP_ONE) {
@@ -3024,7 +3065,8 @@ static void try_field_type_hint(resolve_ctx_t *rc, cbm_resolution_t *res, const
int cand_count = 0;
cbm_registry_find_by_name(rc->registry, method, &cands, &cand_count);
for (int ci = 0; ci < cand_count; ci++) {
if (strstr(cands[ci], type_name) || strstr(cands[ci], iface_name)) {
if (fth_owner_segment_is(cands[ci], type_name) ||
fth_owner_segment_is(cands[ci], iface_name)) {
const cbm_gbuf_node_t *better = cbm_gbuf_find_by_qn(rc->main_gbuf, cands[ci]);
if (better && better->id != source_id) {
res->qualified_name = cands[ci];
+50 -6
View File
@@ -81,6 +81,18 @@ static inline void *intptr_to_ptr(intptr_t v) {
* Atomic spinlock: 0 = free, 1 = locked. */
static atomic_int g_pipeline_busy = 0;
#if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API
static atomic_int g_last_worker_count = 0;
void cbm_pipeline_worker_count_test_reset(void) {
atomic_store(&g_last_worker_count, 0);
}
int cbm_pipeline_worker_count_test_last(void) {
return atomic_load(&g_last_worker_count);
}
#endif
#if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API
static atomic_bool g_persist_test_fail_after_stage_dump = false;
static atomic_bool g_persist_test_cancel_after_predump = false;
@@ -204,6 +216,7 @@ struct cbm_pipeline {
bool persistence; /* write .codebase-memory/graph.db.zst after indexing */
cbm_index_resource_policy_t resource_policy;
cbm_index_resource_violation_t resource_violation;
bool background; /* reserve CPU headroom when no user is waiting */
/* Snapshot of the artifact export failure of THIS run (set only by
* export_after_publish failure, zeroed at run start, cleared on success).
@@ -955,6 +968,12 @@ const char *cbm_pipeline_export_error(const cbm_pipeline_t *p) {
return p ? p->export_error : "";
}
void cbm_pipeline_set_background(cbm_pipeline_t *p, bool background) {
if (p) {
p->background = background;
}
}
bool cbm_pipeline_set_project_name(cbm_pipeline_t *p, const char *name) {
if (!p || !name || !name[0]) {
return false;
@@ -1816,17 +1835,42 @@ void cbm_pipeline_set_committed_counts(cbm_pipeline_t *p, int nodes, int edges)
}
}
/* Two workers is the smallest width that takes the parallel route. */
enum { PL_PARALLEL_MIN_WORKERS = 2 };
/* Background refreshes leave CPU headroom, but never drop below two workers
* while the foreground width is two or more. WHY: one worker sends the indexer
* down the sequential route, which still builds a different graph than the
* parallel route (fewer Route and HANDLES edges, churned CALLS). Until the two
* routes agree, an automatic refresh must not change the graph only because
* the host has two or fewer performance cores. */
int cbm_pipeline_background_worker_width(int foreground, int headroom) {
if (foreground >= PL_PARALLEL_MIN_WORKERS && headroom < PL_PARALLEL_MIN_WORKERS) {
return PL_PARALLEL_MIN_WORKERS;
}
return headroom;
}
/* Effective worker count. The crash supervisor re-runs its worker single-
* threaded (CBM_INDEX_SINGLE_THREAD=1) so a per-file marker can pin the EXACT
* crasher; a parallel re-run would race the marker. Honour that override
* everywhere the worker count drives the parallel/sequential decision, so the
* whole extraction phase collapses to the deterministic sequential path. */
static int effective_worker_count(bool initial) {
int cbm_pipeline_worker_count(const cbm_pipeline_t *p) {
const char *st = getenv("CBM_INDEX_SINGLE_THREAD");
int workers;
if (st && st[0] == '1') {
return 1;
workers = 1;
} else if (p && p->background) {
workers = cbm_pipeline_background_worker_width(cbm_default_worker_count(true),
cbm_default_worker_count(false));
} else {
workers = cbm_default_worker_count(true);
}
return cbm_default_worker_count(initial);
#if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API
atomic_store(&g_last_worker_count, workers);
#endif
return workers;
}
/* Resolve the DB path for this pipeline. Caller must free(). */
@@ -4181,7 +4225,7 @@ static int run_githistory(cbm_pipeline_t *p, cbm_pipeline_ctx_t *ctx) {
gh_compute_arg_t gh_arg = {.repo_path = ctx->repo_path, .result = &gh_result};
if (p->mode != CBM_MODE_FAST) {
if (effective_worker_count(true) > SKIP_ONE) {
if (cbm_pipeline_worker_count(p) > SKIP_ONE) {
if (cbm_thread_create(&gh_thread, 0, gh_compute_thread_fn, &gh_arg) == 0) {
gh_threaded = true;
}
@@ -4281,7 +4325,7 @@ static int run_structure_and_extraction(cbm_pipeline_t *p, cbm_pipeline_ctx_t *c
return CBM_NOT_FOUND;
}
int worker_count = p->frozen ? 1 : effective_worker_count(true);
int worker_count = p->frozen ? 1 : cbm_pipeline_worker_count(p);
if (p->frozen)
frozen_observe(p, 0, 1);
CBM_PROF_START(t_extract_total);
@@ -4412,7 +4456,7 @@ static int cbm_pipeline_run_staged(cbm_pipeline_t *p) {
: mode_promoted
? cbm_pipeline_build_fresh_semantic_manifest(p, p->project_name, &baseline_manifest,
&baseline_count)
: cbm_pipeline_build_semantic_manifest(p->project_name, p->repo_path, files,
: cbm_pipeline_build_semantic_manifest(p, p->project_name, p->repo_path, files,
file_count, p->excluded_dirs, p->excluded_count,
&p->git_ctx, p->userconfig, &baseline_manifest,
&baseline_count);
+17
View File
@@ -71,6 +71,23 @@ bool cbm_pipeline_doc_candidates_enabled(const cbm_pipeline_t *p);
/* Copy the exact discovery violation from the most recent run. */
void cbm_pipeline_get_resource_violation(const cbm_pipeline_t *p,
cbm_index_resource_violation_t *violation);
/* Mark work that runs without a waiting user. Background pipelines reserve
* CPU headroom; foreground pipelines retain the initial-index all-core policy. */
void cbm_pipeline_set_background(cbm_pipeline_t *p, bool background);
/* Resolve the worker policy for this pipeline, including environment and
* crash-recovery overrides. */
int cbm_pipeline_worker_count(const cbm_pipeline_t *p);
/* Background width from the foreground width and the headroom width: the
* headroom, floored at two whenever the foreground width is two or more so an
* automatic refresh keeps the parallel route. */
int cbm_pipeline_background_worker_width(int foreground, int headroom);
#if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API
void cbm_pipeline_worker_count_test_reset(void);
int cbm_pipeline_worker_count_test_last(void);
#endif
/* Snapshot of the artifact export failure of the last cbm_pipeline_run, or ""
* when the run succeeded / did not reach post-publish export. Used to
+19 -14
View File
@@ -479,9 +479,9 @@ static void *manifest_hash_worker(void *arg) {
enum { MANIFEST_PARALLEL_MIN_FILES = 64 };
int cbm_pipeline_build_semantic_manifest(const char *project, const char *repo_path,
const cbm_file_info_t *files, int file_count,
char **excluded_dirs, int excluded_count,
int cbm_pipeline_build_semantic_manifest(const cbm_pipeline_t *p, const char *project,
const char *repo_path, const cbm_file_info_t *files,
int file_count, char **excluded_dirs, int excluded_count,
const cbm_git_context_t *git_ctx,
const cbm_userconfig_t *userconfig, cbm_file_hash_t **out,
int *out_count) {
@@ -522,7 +522,7 @@ int cbm_pipeline_build_semantic_manifest(const char *project, const char *repo_p
}
struct timespec t_hash;
cbm_clock_gettime(CLOCK_MONOTONIC, &t_hash);
int hash_workers = cbm_default_worker_count(true);
int hash_workers = cbm_pipeline_worker_count(p);
if (rc == 0 && file_count >= MANIFEST_PARALLEL_MIN_FILES && hash_workers > SKIP_ONE) {
char (*shas)[CBM_SHA256_HEX_LEN + 1] = malloc((size_t)file_count * sizeof(*shas));
int64_t *mtimes = calloc((size_t)file_count, sizeof(int64_t));
@@ -869,9 +869,9 @@ int cbm_pipeline_build_fresh_semantic_manifest(cbm_pipeline_t *p, const char *pr
&fresh_ignored_total);
}
if (rc == 0) {
rc = cbm_pipeline_build_semantic_manifest(project, repo_path, fresh_files, fresh_file_count,
fresh_excluded, fresh_excluded_count,
&fresh_git_ctx, fresh_userconfig, out, out_count);
rc = cbm_pipeline_build_semantic_manifest(
p, project, repo_path, fresh_files, fresh_file_count, fresh_excluded,
fresh_excluded_count, &fresh_git_ctx, fresh_userconfig, out, out_count);
}
cbm_set_user_lang_config(previous_userconfig);
cbm_git_context_free(&fresh_git_ctx);
@@ -1855,7 +1855,8 @@ static int run_extract_resolve(cbm_pipeline_ctx_t *ctx, cbm_file_info_t *changed
* a full build takes, which is what makes its output converge. */
#define MIN_FILES_FOR_PARALLEL_INCR 50
int worker_count = cbm_pipeline_is_frozen(ctx->pipeline) ? 1 : cbm_default_worker_count(true);
int worker_count =
cbm_pipeline_is_frozen(ctx->pipeline) ? 1 : cbm_pipeline_worker_count(ctx->pipeline);
bool use_parallel =
closure != NULL || (worker_count > SKIP_ONE && ci > MIN_FILES_FOR_PARALLEL_INCR);
@@ -2339,7 +2340,7 @@ static int closure_probe_surfaces(cbm_pipeline_t *p, const char *project,
_Atomic int64_t probe_ids;
atomic_init(&probe_ids, cbm_gbuf_next_id(probe_gbuf));
rc = cbm_parallel_extract(&probe_ctx, probe_files, probe_count, cache, &probe_ids,
cbm_default_worker_count(true));
cbm_pipeline_worker_count(p));
}
if (rc == 0) {
char **def_modules =
@@ -2353,10 +2354,14 @@ static int closure_probe_surfaces(cbm_pipeline_t *p, const char *project,
if (def_modules && def_starts) {
defs = cbm_pxc_collect_all_defs(NULL, &probe_arena, cache, probe_files, probe_count,
project, def_modules, &def_count, def_starts);
rc = def_count < 0
? CBM_PIPELINE_ABORT_PRESERVE_DB
: cbm_lsp_surface_build_rows(NULL, project, cache, probe_files, probe_count,
defs, def_starts, out_rows, out_count);
/* Extract left pipeline NULL so file errors are not recorded twice.
* Surface rows only borrow it for the background worker policy. */
probe_ctx.pipeline = p;
rc = def_count < 0 ? CBM_PIPELINE_ABORT_PRESERVE_DB
: cbm_lsp_surface_build_rows(&probe_ctx, project, cache, probe_files,
probe_count, defs, def_starts, out_rows,
out_count);
probe_ctx.pipeline = NULL;
} else {
rc = -1;
}
@@ -3482,7 +3487,7 @@ static int run_closure_delta(cbm_pipeline_t *p, const char *db_path, const char
elig[elig_count++] = row;
}
}
int workers = cbm_default_worker_count(true);
int workers = cbm_pipeline_worker_count(p);
if (workers < 1) {
workers = 1;
}
+3 -3
View File
@@ -1587,9 +1587,9 @@ bool cbm_pipeline_incremental_generation_exact(cbm_pipeline_t *p, cbm_store_t *s
#define CBM_SEMANTIC_INPUT_GLOBAL_CONFIG CBM_SEMANTIC_INPUT_PREFIX "global-extension-config-v1"
#define CBM_SEMANTIC_INPUT_PROJECT_CONFIG CBM_SEMANTIC_INPUT_PREFIX "project-extension-config-v1"
int cbm_pipeline_build_semantic_manifest(const char *project, const char *repo_path,
const cbm_file_info_t *files, int file_count,
char **excluded_dirs, int excluded_count,
int cbm_pipeline_build_semantic_manifest(const cbm_pipeline_t *p, const char *project,
const char *repo_path, const cbm_file_info_t *files,
int file_count, char **excluded_dirs, int excluded_count,
const cbm_git_context_t *git_ctx,
const cbm_userconfig_t *userconfig, cbm_file_hash_t **out,
int *out_count);
+21
View File
@@ -22,9 +22,28 @@ enum { WP_TRUE = 1, WP_MIN = 1, WP_STEP = 1 };
/* CBM_WORKER_STACK_SIZE (8 MB per worker) lives in worker_pool.h. */
#if defined(CBM_ENABLE_TEST_SEAMS) && CBM_ENABLE_TEST_SEAMS
static cbm_parallel_spawn_observer_fn g_spawn_observer;
static void *g_spawn_observer_ctx;
void cbm_parallel_test_set_spawn_observer(cbm_parallel_spawn_observer_fn observer, void *ctx) {
g_spawn_observer = observer;
g_spawn_observer_ctx = ctx;
}
static void observe_spawn(int count, int helpers) {
if (g_spawn_observer) {
g_spawn_observer(count, helpers, g_spawn_observer_ctx);
}
}
#else
#define observe_spawn(count, helpers) ((void)0)
#endif
/* ── Serial fallback ─────────────────────────────────────────────── */
static void run_serial(int count, cbm_parallel_fn fn, void *ctx) {
observe_spawn(count, 0);
for (int i = 0; i < count; i++) {
fn(i, ctx);
}
@@ -103,6 +122,7 @@ static void run_pthreads_counted(int count, cbm_parallel_fn fn, void *ctx, int n
}
started++;
}
observe_spawn(count, started);
int main_done = 0;
uint64_t main_ops_before = cbm_memev_thread_ops();
while (WP_TRUE) {
@@ -164,6 +184,7 @@ static void run_pthreads(int count, cbm_parallel_fn fn, void *ctx, int nworkers)
}
}
observe_spawn(count, nworkers);
/* Main thread also participates */
while (WP_TRUE) {
int idx = atomic_fetch_add_explicit(&next_idx, WP_STEP, memory_order_relaxed);
+7
View File
@@ -36,4 +36,11 @@ typedef struct {
* If count <= 1 or workers <= 1, runs single-threaded. */
void cbm_parallel_for(int count, cbm_parallel_fn fn, void *ctx, cbm_parallel_for_opts_t opts);
#if defined(CBM_ENABLE_TEST_SEAMS) && CBM_ENABLE_TEST_SEAMS
/* Observe the completed spawn attempt before the caller takes work. Install
* only while no dispatch is running; clear after the observed dispatch joins. */
typedef void (*cbm_parallel_spawn_observer_fn)(int count, int helpers, void *ctx);
void cbm_parallel_test_set_spawn_observer(cbm_parallel_spawn_observer_fn observer, void *ctx);
#endif
#endif /* CBM_WORKER_POOL_H */
+14 -11
View File
@@ -12010,13 +12010,6 @@ static bool adr_migrated(sqlite3 *adb) {
return done;
}
/* Copy any legacy ADR rows from the graph DB's project_summaries into the
* sidecar (INSERT OR IGNORE, so a local row is never clobbered), then set the
* one-time marker. The marker check, copy and marker insert run in ONE
* BEGIN IMMEDIATE transaction on the sidecar, so two processes migrating at
* once cannot re-copy a row the other just deleted. Returns CBM_STORE_ERR on
* any failure (transaction rolled back) so callers can fail closed and preserve
* the old generation. No-op once the marker is set. */
/* Copy every ADR row from the graph DB's project_summaries into the sidecar with
* INSERT OR IGNORE (so a row already present locally is never overwritten). Must
* run inside a transaction on the sidecar connection. Returns OK/ERR; does not
@@ -12070,6 +12063,13 @@ static int adr_copy_rows(cbm_store_t *s, sqlite3 *adb) {
return (rc == SQLITE_DONE) ? CBM_STORE_OK : CBM_STORE_ERR;
}
/* Copy any legacy ADR rows from the graph DB's project_summaries into the
* sidecar (INSERT OR IGNORE, so a local row is never clobbered), then set the
* one-time marker. The marker check, copy and marker insert run in ONE
* BEGIN IMMEDIATE transaction on the sidecar, so two processes migrating at
* once cannot re-copy a row the other just deleted. Returns CBM_STORE_ERR on
* any failure (transaction rolled back) so callers can fail closed and preserve
* the old generation. No-op once the marker is set. */
static int adr_copy_legacy_once(cbm_store_t *s, sqlite3 *adb) {
if (adr_migrated(adb)) {
return CBM_STORE_OK; /* fast path: already migrated, no lock needed */
@@ -12106,19 +12106,22 @@ fail:
* DB's project_summaries, and a teammate who already indexed the project (so the
* sidecar is marked, but has no ADR) must still receive it. INSERT OR IGNORE
* guarantees a local ADR is never clobbered; running ungated by the marker is
* what distinguishes this from the one-time legacy migration. */
* what distinguishes this from the one-time legacy migration. It never writes
* the marker: `adb` comes from adr_conn_write, which hands out a sidecar handle
* only after adr_copy_legacy_once committed it, and nothing deletes it. On
* failure the store error names the step (the import logs it). */
static int adr_restore_if_absent(cbm_store_t *s, sqlite3 *adb) {
if (sqlite3_exec(adb, "BEGIN IMMEDIATE;", NULL, NULL, NULL) != SQLITE_OK) {
store_set_error_sqlite_on(s, adb, "adr restore begin");
return CBM_STORE_ERR;
}
if (adr_copy_rows(s, adb) != CBM_STORE_OK) {
store_set_error(s, "adr restore copy failed");
(void)sqlite3_exec(adb, "ROLLBACK;", NULL, NULL, NULL);
return CBM_STORE_ERR;
}
/* Mark migrated too, so a later reindex's before-delete migration is a no-op. */
(void)sqlite3_exec(adb, "INSERT OR IGNORE INTO adr_meta (key, value) VALUES ('migrated','1');",
NULL, NULL, NULL);
if (sqlite3_exec(adb, "COMMIT;", NULL, NULL, NULL) != SQLITE_OK) {
store_set_error_sqlite_on(s, adb, "adr restore commit");
(void)sqlite3_exec(adb, "ROLLBACK;", NULL, NULL, NULL);
return CBM_STORE_ERR;
}
+52
View File
@@ -252,6 +252,21 @@ TEST(artifact_export_roundtrip_keeps_adr) {
cbm_store_adr_free(&adr);
cbm_store_close(dst);
/* The fresh destination sidecar is marked migrated after the import, so a
* later reindex's migrate-before-delete is a no-op. Opening the sidecar for
* the restore already commits the marker; the restore itself adds none. */
char side[1100];
snprintf(side, sizeof(side), "%s.adr.db", import_db);
sqlite3 *raw = NULL;
ASSERT_EQ(sqlite3_open_v2(side, &raw, SQLITE_OPEN_READONLY, NULL), SQLITE_OK);
sqlite3_stmt *st = NULL;
ASSERT_EQ(
sqlite3_prepare_v2(raw, "SELECT value FROM adr_meta WHERE key='migrated';", -1, &st, NULL),
SQLITE_OK);
ASSERT_EQ(sqlite3_step(st), SQLITE_ROW);
sqlite3_finalize(st);
sqlite3_close(raw);
cleanup_dir(g_tmpdir);
PASS();
}
@@ -391,6 +406,42 @@ TEST(artifact_import_restores_adr_when_local_absent) {
PASS();
}
/* A failed ADR restore must not be silent (#2315 follow-up). A DIRECTORY at
* "<db>.adr.db" makes the sidecar open fail deterministically (no hook). The
* graph DB is already installed when the restore runs, so the import still
* succeeds (best-effort, unchanged), but the team ADR that did not arrive is
* logged with the store's reason instead of being dropped without a trace. */
TEST(artifact_import_logs_failed_adr_restore) {
setup_artifact_test();
create_test_db(g_db);
cbm_store_t *src = cbm_store_open_path(g_db);
ASSERT_NOT_NULL(src);
ASSERT_EQ(cbm_store_adr_store(src, "test-proj", "# Decision\nshared A"), CBM_STORE_OK);
cbm_store_close(src);
ASSERT_EQ(cbm_artifact_export(g_db, g_repo, "test-proj", CBM_ARTIFACT_FAST), 0);
char import_db[1024];
snprintf(import_db, sizeof(import_db), "%s/imported.db", g_tmpdir);
char adr_dir[1100];
snprintf(adr_dir, sizeof(adr_dir), "%s.adr.db", import_db);
ASSERT_EQ(cbm_mkdir(adr_dir), 0);
capture_logs_start();
int rc = cbm_artifact_import(g_repo, import_db);
const char *logs = capture_logs_end();
ASSERT_EQ(rc, 0); /* the graph DB is installed; the restore stays best-effort */
ASSERT_TRUE(cbm_file_exists(import_db));
const char *line = strstr(logs, "level=warn msg=artifact.import err=adr_restore");
ASSERT_NOT_NULL(line);
/* The store's reason is carried (the text log format maps spaces to '_'). */
ASSERT(strstr(line, "detail=adr_sidecar_open") != NULL);
cleanup_dir(g_tmpdir);
PASS();
}
TEST(artifact_export_best_roundtrip) {
setup_artifact_test();
create_test_db(g_db);
@@ -1361,6 +1412,7 @@ SUITE(artifact) {
RUN_TEST(artifact_export_without_adr_does_not_resurrect_legacy_row);
RUN_TEST(artifact_import_does_not_clobber_local_adr);
RUN_TEST(artifact_import_restores_adr_when_local_absent);
RUN_TEST(artifact_import_logs_failed_adr_restore);
RUN_TEST(artifact_export_best_roundtrip);
RUN_TEST(artifact_exists_check);
RUN_TEST(artifact_commit_hash);
+711 -9
View File
@@ -6180,6 +6180,149 @@ TEST(cli_zed_mcp_jsonc_comments) {
PASS();
}
#ifndef _WIN32
TEST(cli_install_agent_config_symlinks_issue2671) {
/* Refs #2671: dotfiles checkout pattern where agent config directory is a
* user-owned symlink into dotfiles under the user's home directory. */
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm-symlink-test-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
FAIL("cbm_mkdtemp failed");
char dotfiles_zed[512];
snprintf(dotfiles_zed, sizeof(dotfiles_zed), "%s/dotfiles/zed", tmpdir);
test_mkdirp(dotfiles_zed);
char dotfiles_settings[512];
snprintf(dotfiles_settings, sizeof(dotfiles_settings), "%s/settings.json", dotfiles_zed);
write_test_file(dotfiles_settings, "{\"theme\": \"Ayu Dark\"}\n");
#ifdef __APPLE__
char parent_dir[512];
snprintf(parent_dir, sizeof(parent_dir), "%s/Library/Application Support", tmpdir);
test_mkdirp(parent_dir);
char zed_link[512];
snprintf(zed_link, sizeof(zed_link), "%s/Zed", parent_dir);
#else
char parent_dir[512];
snprintf(parent_dir, sizeof(parent_dir), "%s/.config", tmpdir);
test_mkdirp(parent_dir);
char zed_link[512];
snprintf(zed_link, sizeof(zed_link), "%s/zed", parent_dir);
#endif
char *saved_xdg = save_test_env("XDG_CONFIG_HOME");
char xdg_dir[640];
snprintf(xdg_dir, sizeof(xdg_dir), "%s/.config", tmpdir);
cbm_setenv("XDG_CONFIG_HOME", xdg_dir, 1); /* Linux resolver prefers XDG */
/* Dangling symlink and symlink to a regular file must remain undetected. */
char dangling_target[512];
snprintf(dangling_target, sizeof(dangling_target), "%s/dotfiles/missing-zed", tmpdir);
ASSERT_EQ(symlink(dangling_target, zed_link), 0);
bool dangling_detected = cbm_detect_agents(tmpdir).zed;
ASSERT_EQ(cbm_unlink(zed_link), 0);
ASSERT_EQ(symlink(dotfiles_settings, zed_link), 0);
bool file_link_detected = cbm_detect_agents(tmpdir).zed;
ASSERT_EQ(cbm_unlink(zed_link), 0);
/* Symlink resolving to a directory not owned by the invoking user must be refused. */
char foreign_dir[512];
const char *unowned_target = "/";
if (geteuid() == 0) {
snprintf(foreign_dir, sizeof(foreign_dir), "%s/dotfiles/foreign-zed", tmpdir);
test_mkdirp(foreign_dir);
ASSERT_EQ(chown(foreign_dir, 65534, 65534), 0);
unowned_target = foreign_dir;
}
ASSERT_EQ(symlink(unowned_target, zed_link), 0);
bool unowned_target_detected = cbm_detect_agents(tmpdir).zed;
ASSERT_EQ(cbm_unlink(zed_link), 0);
ASSERT_EQ(symlink(dotfiles_zed, zed_link), 0);
cbm_cli_set_client_selection_for_testing("zed");
int rc = cbm_install_agent_configs(tmpdir, "/usr/local/bin/codebase-memory-mcp", false, false);
cbm_cli_set_client_selection_for_testing(NULL);
restore_test_env("XDG_CONFIG_HOME", saved_xdg);
char *after = read_test_file_alloc(dotfiles_settings);
bool has_cbm = after && strstr(after, "codebase-memory-mcp") != NULL;
bool has_original = after && strstr(after, "Ayu Dark") != NULL;
free(after);
struct stat st;
ASSERT_EQ(lstat(zed_link, &st), 0);
bool still_symlink = S_ISLNK(st.st_mode);
test_rmdir_r(tmpdir);
ASSERT(!dangling_detected);
ASSERT(!file_link_detected);
ASSERT(!unowned_target_detected);
ASSERT_EQ(rc, 0);
ASSERT(has_cbm);
ASSERT(has_original);
ASSERT(still_symlink);
PASS();
}
TEST(cli_install_agent_config_file_symlink_issue2671) {
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm-file-symlink-test-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
FAIL("cbm_mkdtemp failed");
char dotfiles_zed[512];
snprintf(dotfiles_zed, sizeof(dotfiles_zed), "%s/dotfiles/zed", tmpdir);
test_mkdirp(dotfiles_zed);
char dotfiles_settings[512];
snprintf(dotfiles_settings, sizeof(dotfiles_settings), "%s/settings.json", dotfiles_zed);
write_test_file(dotfiles_settings, "{\"theme\": \"Ayu Dark\"}\n");
#ifdef __APPLE__
char zed_dir[512];
snprintf(zed_dir, sizeof(zed_dir), "%s/Library/Application Support/Zed", tmpdir);
test_mkdirp(zed_dir);
char zed_settings_link[512];
snprintf(zed_settings_link, sizeof(zed_settings_link), "%s/settings.json", zed_dir);
ASSERT_EQ(symlink("../../../dotfiles/zed/settings.json", zed_settings_link), 0);
#else
char zed_dir[512];
snprintf(zed_dir, sizeof(zed_dir), "%s/.config/zed", tmpdir);
test_mkdirp(zed_dir);
char zed_settings_link[512];
snprintf(zed_settings_link, sizeof(zed_settings_link), "%s/settings.json", zed_dir);
ASSERT_EQ(symlink("../../dotfiles/zed/settings.json", zed_settings_link), 0);
#endif
char *saved_xdg = save_test_env("XDG_CONFIG_HOME");
char xdg_dir[640];
snprintf(xdg_dir, sizeof(xdg_dir), "%s/.config", tmpdir);
cbm_setenv("XDG_CONFIG_HOME", xdg_dir, 1); /* Linux resolver prefers XDG */
cbm_cli_set_client_selection_for_testing("zed");
int rc = cbm_install_agent_configs(tmpdir, "/usr/local/bin/codebase-memory-mcp", false, false);
cbm_cli_set_client_selection_for_testing(NULL);
restore_test_env("XDG_CONFIG_HOME", saved_xdg);
char *after = read_test_file_alloc(dotfiles_settings);
bool has_cbm = after && strstr(after, "codebase-memory-mcp") != NULL;
bool has_original = after && strstr(after, "Ayu Dark") != NULL;
free(after);
struct stat st;
ASSERT_EQ(lstat(zed_settings_link, &st), 0);
bool still_symlink = S_ISLNK(st.st_mode);
test_rmdir_r(tmpdir);
ASSERT_EQ(rc, 0);
ASSERT(has_cbm);
ASSERT(has_original);
ASSERT(still_symlink);
PASS();
}
#endif
/* ═══════════════════════════════════════════════════════════════════
* PATH management tests (port of TestCLI_InstallPATHAppend)
* ═══════════════════════════════════════════════════════════════════ */
@@ -8151,7 +8294,6 @@ TEST(cli_durable_profiles_follow_current_vendor_paths) {
strstr(plan, "\"instruction_files_planned\"");
const char *const planned[] = {
"/.claude/agents/codebase-memory.md",
"/vendor-codex/skills/codebase-memory/SKILL.md",
"/vendor-codex/agents/codebase-memory.toml",
"/.cursor/skills/codebase-memory/SKILL.md",
"/.cursor/agents/codebase-memory.md",
@@ -8314,13 +8456,12 @@ TEST(cli_durable_profiles_follow_current_vendor_paths) {
free(profile);
const char *const skill_files[] = {
"/skills/codebase-memory/SKILL.md",
"/.cursor/skills/codebase-memory/SKILL.md",
"/.config/opencode/skills/codebase-memory/SKILL.md",
"/.factory/skills/codebase-memory/SKILL.md",
"/.agents/skills/codebase-memory/SKILL.md",
};
const char *const skill_roots[] = {codex_home, tmpdir, tmpdir, tmpdir, tmpdir};
const char *const skill_roots[] = {tmpdir, tmpdir, tmpdir, tmpdir};
for (size_t i = 0U; files_ok && i < sizeof(skill_files) / sizeof(skill_files[0]); i++) {
snprintf(path, sizeof(path), "%s%s", skill_roots[i], skill_files[i]);
files_ok = test_file_contains_all(path, graph_terms, 3);
@@ -9431,6 +9572,127 @@ TEST(cli_dry_run_predicts_refused_hook_script_issue1387) {
FAIL("dry-run must predict a refused hook-script rewrite (#1387)");
PASS();
}
/* #2204: the OpenCode adapter is an auto-loaded module, and the generated block
* tells readers to "remove the markers to take ownership". A marker-less file
* with content is the user's: appending the block to a hand-ported module
* produces a file the client cannot even parse (the prelude redeclares the
* user's own import and const names, and both sides declare a default export).
* The installer must leave that file alone, and the dry run must say so instead
* of promising a write it will not make. A dry run never writes, so the byte
* check below follows a REAL install: that is the run that appended. */
TEST(cli_extension_leaves_user_owned_module_untouched_issue2204) {
char home[256];
char bin[320];
if (!client_detect_fixture(home, sizeof(home), bin, sizeof(bin)))
FAIL("cbm_mkdtemp failed");
/* An `opencode` CLI on the isolated PATH marks OpenCode as installed. */
char exe[512];
snprintf(exe, sizeof(exe), "%s/opencode", bin);
if (write_test_file(exe, "#!/bin/sh\nexit 0\n") != 0 || chmod(exe, 0755) != 0)
FAIL("opencode fixture failed");
char plugins_dir[512];
snprintf(plugins_dir, sizeof(plugins_dir), "%s/.config/opencode/plugins", home);
test_mkdirp(plugins_dir);
char plugin_path[768];
snprintf(plugin_path, sizeof(plugin_path), "%s/cbm-augment.ts", plugins_dir);
const char foreign[] = "import { spawn } from 'node:child_process';\n"
"const BIN = '/opt/other/codebase-memory-mcp';\n"
"export default { id: 'codebase-memory-augment', setup() {} };\n";
if (write_test_file(plugin_path, foreign) != 0)
FAIL("foreign module fixture failed");
client_detect_env_t saved = client_detect_env_isolate(bin);
FILE *capture = tmpfile();
int saved_stdout = capture ? dup(STDOUT_FILENO) : -1;
bool redirected = false;
if (capture && saved_stdout >= 0) {
fflush(stdout);
redirected = dup2(fileno(capture), STDOUT_FILENO) >= 0;
}
long plan_output_start = -1;
long plan_output_end = -1;
char *plan = NULL;
if (redirected) {
(void)cbm_install_agent_configs(home, "/opt/other/codebase-memory-mcp", false, true);
fflush(stdout);
/* The --plan receipt (#388) replays the same path with writes off. It
* must not list a module the run leaves alone, and it must print
* nothing: stdout carries the receipt JSON. */
plan_output_start = (long)lseek(STDOUT_FILENO, 0, SEEK_CUR);
plan = cbm_build_install_plan_json(home, "/opt/other/codebase-memory-mcp");
fflush(stdout);
/* The real install, the run #2204 is about. */
plan_output_end = (long)lseek(STDOUT_FILENO, 0, SEEK_CUR);
(void)cbm_install_agent_configs(home, "/opt/other/codebase-memory-mcp", false, false);
fflush(stdout);
(void)dup2(saved_stdout, STDOUT_FILENO);
}
if (saved_stdout >= 0)
close(saved_stdout);
char bytes[512] = {0};
FILE *file = cbm_fopen(plugin_path, "rb");
size_t got = file ? fread(bytes, 1U, sizeof(bytes) - 1U, file) : 0U;
if (file)
fclose(file);
/* Control: with the user's module gone the receipt does list the path, so
* the omission above cannot pass just because OpenCode went undetected. */
char *control_plan = NULL;
if (unlink(plugin_path) == 0)
control_plan = cbm_build_install_plan_json(home, "/opt/other/codebase-memory-mcp");
client_detect_env_restore(&saved);
char output[16384] = {0};
if (capture) {
rewind(capture);
size_t count = fread(output, 1, sizeof(output) - 1U, capture);
output[count] = '\0';
fclose(capture);
}
/* Each phase owns its slice of stdout: [0, start) dry run, [start, end)
* plan mode, [end, len) real install, so no phase passes on another's
* line. Checked back to front, cutting the buffer at each boundary. */
bool sliced = plan_output_start >= 0 && plan_output_end >= plan_output_start &&
(size_t)plan_output_end <= strlen(output);
bool reported =
sliced && strstr(output + plan_output_end, "cbm-augment.ts left untouched") != NULL;
if (sliced)
output[plan_output_end] = '\0';
bool plan_silent = sliced && strstr(output + plan_output_start, "cbm-augment.ts") == NULL;
if (sliced)
output[plan_output_start] = '\0';
bool warned =
sliced && strstr(output, "cbm-augment.ts") != NULL && strstr(output, "not ours") != NULL;
bool untouched = got == sizeof(foreign) - 1U && memcmp(bytes, foreign, got) == 0;
bool plan_omits = plan && strstr(plan, "cbm-augment.ts") == NULL;
bool control_lists = control_plan && strstr(control_plan, "cbm-augment.ts") != NULL;
free(plan);
free(control_plan);
test_rmdir_r(home);
if (!redirected)
FAIL("stdout capture failed");
if (!sliced)
FAIL("stdout capture lost a phase boundary");
if (!warned)
FAIL("dry-run must predict an untouched user-owned OpenCode module (#2204)");
if (!untouched)
FAIL("a real install must leave the user-owned OpenCode module byte-identical (#2204)");
if (!reported)
FAIL("a real install must report the user-owned OpenCode module as left untouched (#2204)");
if (!plan_omits)
FAIL("the install plan must not list a module the run leaves untouched (#2204)");
if (!plan_silent)
FAIL("plan mode must keep stdout for the receipt JSON (#2204)");
if (!control_lists)
FAIL("control: the install plan must list the module once it is not user-owned");
PASS();
}
#endif
TEST(cli_agent_client_registry_routes_plan_install_and_uninstall) {
@@ -10840,12 +11102,21 @@ TEST(cli_codex_respects_codex_home) {
char *json = cbm_build_install_plan_json(tmpdir, "/usr/local/bin/codebase-memory-mcp");
char expected_config[640];
snprintf(expected_config, sizeof(expected_config), "%s/config.toml", codex_home);
char expected_skill[640];
char legacy_skill[640];
snprintf(expected_skill, sizeof(expected_skill), "%s/.agents/skills/codebase-memory/SKILL.md",
tmpdir);
snprintf(legacy_skill, sizeof(legacy_skill), "%s/skills/codebase-memory/SKILL.md", codex_home);
yyjson_doc *plan_doc = json ? yyjson_read(json, strlen(json), 0) : NULL;
yyjson_val *plan_root = plan_doc ? yyjson_doc_get_root(plan_doc) : NULL;
bool plans_config =
test_json_string_array_contains(plan_root, "config_files_planned", expected_config);
bool plans_instructions = test_json_string_array_contains(
plan_root, "instruction_files_planned", expected_instructions);
bool plans_canonical_skill =
test_json_string_array_contains(plan_root, "skill_files_planned", expected_skill);
bool plans_legacy_skill =
test_json_string_array_contains(plan_root, "skill_files_planned", legacy_skill);
bool plans_cleanup = json && strstr(json, "remove_managed_block_if_present") != NULL;
char *instructions_after = read_test_file_alloc(expected_instructions);
bool plan_preserved_user_file =
@@ -10859,9 +11130,10 @@ TEST(cli_codex_respects_codex_home) {
if (!agents.codex)
FAIL("Codex detection must honor CODEX_HOME");
if (!plans_config || !plans_instructions || plans_cleanup || !plan_preserved_user_file)
if (!plans_config || !plans_instructions || !plans_canonical_skill || plans_legacy_skill ||
plans_cleanup || !plan_preserved_user_file)
FAIL("Codex plan must include the managed activation pointer under CODEX_HOME without "
"mutating existing user content");
"mutating existing user content, and use HOME/.agents for the skill");
PASS();
}
@@ -10880,7 +11152,7 @@ TEST(cli_codex_install_uses_global_activation_pointer_issue1689) {
snprintf(codex_home, sizeof(codex_home), "%s/.codex", tmpdir);
snprintf(agents_path, sizeof(agents_path), "%s/AGENTS.md", codex_home);
snprintf(config_path, sizeof(config_path), "%s/config.toml", codex_home);
snprintf(skill_path, sizeof(skill_path), "%s/skills/codebase-memory/SKILL.md", codex_home);
snprintf(skill_path, sizeof(skill_path), "%s/.agents/skills/codebase-memory/SKILL.md", tmpdir);
snprintf(profile_path, sizeof(profile_path), "%s/agents/codebase-memory.toml", codex_home);
#ifdef _WIN32
snprintf(binary_path, sizeof(binary_path), "%s/.local/bin/codebase-memory-mcp.exe", tmpdir);
@@ -10985,7 +11257,6 @@ TEST(cli_codex_install_uses_global_activation_pointer_issue1689) {
restore_test_env("PATH", saved_path);
restore_test_env("CODEX_HOME", saved_codex);
test_rmdir_r(tmpdir);
if (!fresh_pointer_installed || !other_surfaces_installed || !unowned_preserved ||
!managed_migrated_once || !marker_only_migrated || !malformed_preserved ||
!uninstall_preserved_foreign)
@@ -11037,6 +11308,408 @@ TEST(cli_grok_respects_grok_home) {
PASS();
}
static bool cli_test_file_equals(const char *path, const char *expected) {
char *data = read_test_file_alloc(path);
bool equal = data && strcmp(data, expected) == 0;
free(data);
return equal;
}
TEST(cli_codex_skill_migrates_owned_legacy_to_agents_root) {
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-codex-skill-migrate-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
FAIL("cbm_mkdtemp failed");
char codex_home[512];
char legacy_root[640];
char legacy_file[768];
char sibling_file[768];
char canonical_file[768];
snprintf(codex_home, sizeof(codex_home), "%s/vendor-codex", tmpdir);
snprintf(legacy_root, sizeof(legacy_root), "%s/skills", codex_home);
snprintf(legacy_file, sizeof(legacy_file), "%s/codebase-memory/SKILL.md", legacy_root);
snprintf(sibling_file, sizeof(sibling_file), "%s/codebase-memory/notes.md", legacy_root);
snprintf(canonical_file, sizeof(canonical_file), "%s/.agents/skills/codebase-memory/SKILL.md",
tmpdir);
test_mkdirp(codex_home);
ASSERT_EQ(cbm_install_skills(legacy_root, false, false), CBM_SKILL_COUNT);
write_test_file(sibling_file, "user notes\n");
char *saved_home = save_test_env("HOME");
char *saved_path = save_test_env("PATH");
char *saved_codex = save_test_env("CODEX_HOME");
cbm_setenv("HOME", tmpdir, 1);
cbm_setenv("PATH", tmpdir, 1);
cbm_setenv("CODEX_HOME", codex_home, 1);
int dry_rc = cbm_install_agent_configs(tmpdir, "/opt/codebase-memory-mcp", false, true);
struct stat state;
bool dry_safe = dry_rc == 0 && stat(canonical_file, &state) != 0 &&
cli_test_file_equals(legacy_file, cbm_get_skills()[0].content);
char *plan = cbm_build_install_plan_json(tmpdir, "/opt/codebase-memory-mcp");
bool plan_ok = plan && strstr(plan, canonical_file) && strstr(plan, legacy_file) &&
strstr(plan, "cleanup_actions_planned") &&
strstr(plan, "remove_owned_legacy_copy_if_migrated");
bool plan_safe = stat(canonical_file, &state) != 0 &&
cli_test_file_equals(legacy_file, cbm_get_skills()[0].content);
free(plan);
int install_rc = cbm_install_agent_configs(tmpdir, "/opt/codebase-memory-mcp", false, false);
bool migrated =
install_rc == 0 && cli_test_file_equals(canonical_file, cbm_get_skills()[0].content) &&
stat(legacy_file, &state) != 0 && cli_test_file_equals(sibling_file, "user notes\n");
restore_test_env("HOME", saved_home);
restore_test_env("PATH", saved_path);
restore_test_env("CODEX_HOME", saved_codex);
test_rmdir_r(tmpdir);
if (!dry_safe || !plan_ok || !plan_safe || !migrated)
FAIL("Codex owned legacy skill migration must be previewable, destination-first, and "
"sibling-safe");
PASS();
}
TEST(cli_codex_skill_migrates_every_released_legacy_payload) {
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-codex-skill-released-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
FAIL("cbm_mkdtemp failed");
char codex_home[512];
char legacy_dir[768];
char legacy_file[768];
char canonical_file[768];
snprintf(codex_home, sizeof(codex_home), "%s/.codex", tmpdir);
snprintf(legacy_dir, sizeof(legacy_dir), "%s/skills/codebase-memory", codex_home);
snprintf(legacy_file, sizeof(legacy_file), "%s/SKILL.md", legacy_dir);
snprintf(canonical_file, sizeof(canonical_file), "%s/.agents/skills/codebase-memory/SKILL.md",
tmpdir);
test_mkdirp(codex_home);
char *saved_home = save_test_env("HOME");
char *saved_path = save_test_env("PATH");
char *saved_codex = save_test_env("CODEX_HOME");
cbm_setenv("HOME", tmpdir, 1);
cbm_setenv("PATH", tmpdir, 1);
cbm_setenv("CODEX_HOME", codex_home, 1);
cbm_cli_set_client_selection_for_testing("codex");
size_t released_count = cbm_cli_released_skill_content_count();
bool migrated_all = released_count == 4U;
for (size_t i = 0U; migrated_all && i < released_count; i++) {
const char *released = cbm_cli_released_skill_content_at(i);
test_mkdirp(legacy_dir);
if (!released || write_test_file(legacy_file, released) != 0) {
migrated_all = false;
break;
}
(void)remove(canonical_file);
int rc = cbm_install_agent_configs(tmpdir, "/opt/codebase-memory-mcp", false, false);
struct stat state;
migrated_all = rc == 0 &&
cli_test_file_equals(canonical_file, cbm_get_skills()[0].content) &&
stat(legacy_file, &state) != 0;
if (!migrated_all || write_test_file(canonical_file, released) != 0) {
migrated_all = false;
break;
}
rc = cbm_install_agent_configs(tmpdir, "/opt/codebase-memory-mcp", false, false);
migrated_all = rc == 0 && cli_test_file_equals(canonical_file, cbm_get_skills()[0].content);
}
cbm_cli_set_client_selection_for_testing(NULL);
restore_test_env("HOME", saved_home);
restore_test_env("PATH", saved_path);
restore_test_env("CODEX_HOME", saved_codex);
test_rmdir_r(tmpdir);
if (!migrated_all)
FAIL("Codex must migrate every released skill payload from legacy and canonical paths");
PASS();
}
TEST(cli_codex_foreign_legacy_does_not_block_shared_clients) {
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-codex-skill-shared-client-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
FAIL("cbm_mkdtemp failed");
const char *foreign = "---\nname: codebase-memory\n---\nUser-owned legacy skill.\n";
char codex_home[512];
char legacy_dir[768];
char legacy_file[768];
char openhands_dir[512];
char canonical_file[768];
snprintf(codex_home, sizeof(codex_home), "%s/.codex", tmpdir);
snprintf(legacy_dir, sizeof(legacy_dir), "%s/skills/codebase-memory", codex_home);
snprintf(legacy_file, sizeof(legacy_file), "%s/SKILL.md", legacy_dir);
snprintf(openhands_dir, sizeof(openhands_dir), "%s/.openhands", tmpdir);
snprintf(canonical_file, sizeof(canonical_file), "%s/.agents/skills/codebase-memory/SKILL.md",
tmpdir);
test_mkdirp(legacy_dir);
test_mkdirp(openhands_dir);
write_test_file(legacy_file, foreign);
char *saved_home = save_test_env("HOME");
char *saved_path = save_test_env("PATH");
char *saved_codex = save_test_env("CODEX_HOME");
cbm_setenv("HOME", tmpdir, 1);
cbm_setenv("PATH", tmpdir, 1);
cbm_setenv("CODEX_HOME", codex_home, 1);
char *plan = cbm_build_install_plan_json(tmpdir, "/opt/codebase-memory-mcp");
bool plan_keeps_shared_install = plan && strstr(plan, canonical_file) &&
!strstr(plan, "remove_owned_legacy_copy_if_migrated");
free(plan);
int install_rc = cbm_install_agent_configs(tmpdir, "/opt/codebase-memory-mcp", false, false);
int repeat_rc = cbm_install_agent_configs(tmpdir, "/opt/codebase-memory-mcp", false, false);
bool installed = install_rc == 0 && repeat_rc == 0 &&
cli_test_file_equals(legacy_file, foreign) &&
cli_test_file_equals(canonical_file, cbm_get_skills()[0].content);
restore_test_env("HOME", saved_home);
restore_test_env("PATH", saved_path);
restore_test_env("CODEX_HOME", saved_codex);
test_rmdir_r(tmpdir);
if (!plan_keeps_shared_install || !installed)
FAIL("A foreign Codex legacy skill must not suppress another client's shared skill");
PASS();
}
TEST(cli_codex_skill_preserves_foreign_legacy_until_force) {
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-codex-skill-foreign-legacy-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
FAIL("cbm_mkdtemp failed");
const char *foreign = "---\nname: codebase-memory\n---\nUser-owned legacy skill.\n";
char codex_home[512];
char legacy_dir[768];
char legacy_file[768];
char sibling_file[768];
char canonical_file[768];
snprintf(codex_home, sizeof(codex_home), "%s/vendor-codex", tmpdir);
snprintf(legacy_dir, sizeof(legacy_dir), "%s/skills/codebase-memory", codex_home);
snprintf(legacy_file, sizeof(legacy_file), "%s/SKILL.md", legacy_dir);
snprintf(sibling_file, sizeof(sibling_file), "%s/user.txt", legacy_dir);
snprintf(canonical_file, sizeof(canonical_file), "%s/.agents/skills/codebase-memory/SKILL.md",
tmpdir);
test_mkdirp(legacy_dir);
write_test_file(legacy_file, foreign);
write_test_file(sibling_file, "keep\n");
char *saved_home = save_test_env("HOME");
char *saved_path = save_test_env("PATH");
char *saved_codex = save_test_env("CODEX_HOME");
cbm_setenv("HOME", tmpdir, 1);
cbm_setenv("PATH", tmpdir, 1);
cbm_setenv("CODEX_HOME", codex_home, 1);
cbm_cli_set_client_selection_for_testing("codex");
int normal_rc = cbm_install_agent_configs(tmpdir, "/opt/codebase-memory-mcp", false, false);
struct stat state;
bool preserved = normal_rc == 0 && cli_test_file_equals(legacy_file, foreign) &&
stat(canonical_file, &state) != 0;
int force_rc = cbm_install_agent_configs(tmpdir, "/opt/codebase-memory-mcp", true, false);
bool forced = force_rc == 0 &&
cli_test_file_equals(canonical_file, cbm_get_skills()[0].content) &&
stat(legacy_file, &state) != 0 && cli_test_file_equals(sibling_file, "keep\n");
cbm_cli_set_client_selection_for_testing(NULL);
restore_test_env("HOME", saved_home);
restore_test_env("PATH", saved_path);
restore_test_env("CODEX_HOME", saved_codex);
test_rmdir_r(tmpdir);
if (!preserved || !forced)
FAIL("Codex must preserve foreign legacy content unless --force authorizes replacement");
PASS();
}
TEST(cli_codex_skill_preserves_foreign_canonical_and_owned_legacy) {
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-codex-skill-foreign-canonical-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
FAIL("cbm_mkdtemp failed");
const char *foreign = "---\nname: codebase-memory\n---\nUser-owned canonical skill.\n";
char codex_home[512];
char legacy_root[640];
char legacy_file[768];
char canonical_dir[768];
char canonical_file[768];
snprintf(codex_home, sizeof(codex_home), "%s/vendor-codex", tmpdir);
snprintf(legacy_root, sizeof(legacy_root), "%s/skills", codex_home);
snprintf(legacy_file, sizeof(legacy_file), "%s/codebase-memory/SKILL.md", legacy_root);
snprintf(canonical_dir, sizeof(canonical_dir), "%s/.agents/skills/codebase-memory", tmpdir);
snprintf(canonical_file, sizeof(canonical_file), "%s/SKILL.md", canonical_dir);
test_mkdirp(codex_home);
ASSERT_EQ(cbm_install_skills(legacy_root, false, false), CBM_SKILL_COUNT);
test_mkdirp(canonical_dir);
write_test_file(canonical_file, foreign);
char *saved_home = save_test_env("HOME");
char *saved_path = save_test_env("PATH");
char *saved_codex = save_test_env("CODEX_HOME");
cbm_setenv("HOME", tmpdir, 1);
cbm_setenv("PATH", tmpdir, 1);
cbm_setenv("CODEX_HOME", codex_home, 1);
int normal_rc = cbm_install_agent_configs(tmpdir, "/opt/codebase-memory-mcp", false, false);
bool preserved = normal_rc == 0 && cli_test_file_equals(canonical_file, foreign) &&
cli_test_file_equals(legacy_file, cbm_get_skills()[0].content);
int force_rc = cbm_install_agent_configs(tmpdir, "/opt/codebase-memory-mcp", true, false);
struct stat state;
bool forced = force_rc == 0 &&
cli_test_file_equals(canonical_file, cbm_get_skills()[0].content) &&
stat(legacy_file, &state) != 0;
restore_test_env("HOME", saved_home);
restore_test_env("PATH", saved_path);
restore_test_env("CODEX_HOME", saved_codex);
test_rmdir_r(tmpdir);
if (!preserved || !forced)
FAIL("Codex must preserve conflicting canonical bytes and its owned legacy copy until "
"--force");
PASS();
}
TEST(cli_codex_skill_failed_destination_preserves_owned_legacy) {
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-codex-skill-failed-destination-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
FAIL("cbm_mkdtemp failed");
char codex_home[512];
char legacy_root[640];
char legacy_file[768];
char canonical_root[640];
char blocked_path[768];
snprintf(codex_home, sizeof(codex_home), "%s/vendor-codex", tmpdir);
snprintf(legacy_root, sizeof(legacy_root), "%s/skills", codex_home);
snprintf(legacy_file, sizeof(legacy_file), "%s/codebase-memory/SKILL.md", legacy_root);
snprintf(canonical_root, sizeof(canonical_root), "%s/.agents/skills", tmpdir);
snprintf(blocked_path, sizeof(blocked_path), "%s/codebase-memory", canonical_root);
test_mkdirp(codex_home);
ASSERT_EQ(cbm_install_skills(legacy_root, false, false), CBM_SKILL_COUNT);
test_mkdirp(canonical_root);
write_test_file(blocked_path, "not a directory\n");
char *saved_home = save_test_env("HOME");
char *saved_path = save_test_env("PATH");
char *saved_codex = save_test_env("CODEX_HOME");
cbm_setenv("HOME", tmpdir, 1);
cbm_setenv("PATH", tmpdir, 1);
cbm_setenv("CODEX_HOME", codex_home, 1);
int rc = cbm_install_agent_configs(tmpdir, "/opt/codebase-memory-mcp", false, false);
bool safe = rc != 0 && cli_test_file_equals(blocked_path, "not a directory\n") &&
cli_test_file_equals(legacy_file, cbm_get_skills()[0].content);
restore_test_env("HOME", saved_home);
restore_test_env("PATH", saved_path);
restore_test_env("CODEX_HOME", saved_codex);
test_rmdir_r(tmpdir);
if (!safe)
FAIL("Codex migration must retain the owned legacy copy when canonical publication fails");
PASS();
}
TEST(cli_codex_skill_uninstall_cleans_both_owned_locations) {
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-codex-skill-uninstall-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
FAIL("cbm_mkdtemp failed");
char codex_home[512];
char canonical_root[640];
char legacy_root[640];
char canonical_file[768];
char legacy_file[768];
char binary_dir[640];
char binary_path[768];
snprintf(codex_home, sizeof(codex_home), "%s/.codex", tmpdir);
snprintf(canonical_root, sizeof(canonical_root), "%s/.agents/skills", tmpdir);
snprintf(legacy_root, sizeof(legacy_root), "%s/skills", codex_home);
snprintf(canonical_file, sizeof(canonical_file), "%s/codebase-memory/SKILL.md", canonical_root);
snprintf(legacy_file, sizeof(legacy_file), "%s/codebase-memory/SKILL.md", legacy_root);
snprintf(binary_dir, sizeof(binary_dir), "%s/.local/bin", tmpdir);
#ifdef _WIN32
snprintf(binary_path, sizeof(binary_path), "%s/codebase-memory-mcp.exe", binary_dir);
#else
snprintf(binary_path, sizeof(binary_path), "%s/codebase-memory-mcp", binary_dir);
#endif
test_mkdirp(codex_home);
test_mkdirp(binary_dir);
write_test_file(binary_path, "installed binary\n");
ASSERT_EQ(cbm_install_skills(canonical_root, false, false), CBM_SKILL_COUNT);
ASSERT_EQ(cbm_install_skills(legacy_root, false, false), CBM_SKILL_COUNT);
char *saved_home = save_test_env("HOME");
char *saved_path = save_test_env("PATH");
char *saved_codex = save_test_env("CODEX_HOME");
cbm_setenv("HOME", tmpdir, 1);
cbm_setenv("PATH", tmpdir, 1);
cbm_unsetenv("CODEX_HOME");
char *argv[] = {"uninstall", "--yes"};
int owned_rc = cli_test_cmd_uninstall(2, argv);
struct stat state;
bool owned_removed =
owned_rc == 0 && stat(canonical_file, &state) != 0 && stat(legacy_file, &state) != 0;
const char *foreign = "---\nname: codebase-memory\n---\nUser-owned canonical skill.\n";
write_test_file(binary_path, "installed binary\n");
bool mixed_seeded = cbm_install_skills(canonical_root, false, false) == CBM_SKILL_COUNT &&
cbm_install_skills(legacy_root, false, false) == CBM_SKILL_COUNT;
if (mixed_seeded) {
write_test_file(canonical_file, foreign);
}
int mixed_rc = mixed_seeded ? cli_test_cmd_uninstall(2, argv) : 1;
bool mixed_safe = mixed_rc == 0 && cli_test_file_equals(canonical_file, foreign) &&
stat(legacy_file, &state) != 0;
restore_test_env("HOME", saved_home);
restore_test_env("PATH", saved_path);
restore_test_env("CODEX_HOME", saved_codex);
test_rmdir_r(tmpdir);
if (!owned_removed || !mixed_safe)
FAIL("Codex uninstall must clean both owned roots while preserving foreign skill bytes");
PASS();
}
TEST(cli_codex_skill_deduplicates_agents_codex_home) {
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-codex-skill-same-root-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
FAIL("cbm_mkdtemp failed");
char codex_home[512];
char skill_file[768];
snprintf(codex_home, sizeof(codex_home), "%s/.agents", tmpdir);
snprintf(skill_file, sizeof(skill_file), "%s/skills/codebase-memory/SKILL.md", codex_home);
test_mkdirp(codex_home);
char *saved_home = save_test_env("HOME");
char *saved_path = save_test_env("PATH");
char *saved_codex = save_test_env("CODEX_HOME");
cbm_setenv("HOME", tmpdir, 1);
cbm_setenv("PATH", tmpdir, 1);
cbm_setenv("CODEX_HOME", codex_home, 1);
int install_rc = cbm_install_agent_configs(tmpdir, "/opt/codebase-memory-mcp", false, false);
char *plan = cbm_build_install_plan_json(tmpdir, "/opt/codebase-memory-mcp");
bool deduplicated = install_rc == 0 &&
cli_test_file_equals(skill_file, cbm_get_skills()[0].content) && plan &&
!strstr(plan, "remove_owned_legacy_copy_if_migrated");
free(plan);
restore_test_env("HOME", saved_home);
restore_test_env("PATH", saved_path);
restore_test_env("CODEX_HOME", saved_codex);
test_rmdir_r(tmpdir);
if (!deduplicated)
FAIL("Codex must treat HOME/.agents and CODEX_HOME as one skill root when they coincide");
PASS();
}
TEST(cli_gemini_session_hook_uses_json_for_all_sources) {
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-gemini-session-XXXXXX");
@@ -13663,7 +14336,7 @@ TEST(cli_codex_migrates_to_single_hook_representation) {
int uninstall_rc = cli_test_cmd_uninstall(1, uninstall_argv);
char skill_path[768];
char agent_path[768];
snprintf(skill_path, sizeof(skill_path), "%s/skills/codebase-memory/SKILL.md", codex_dir);
snprintf(skill_path, sizeof(skill_path), "%s/.agents/skills/codebase-memory/SKILL.md", tmpdir);
snprintf(agent_path, sizeof(agent_path), "%s/agents/codebase-memory.toml", codex_dir);
struct stat state;
hooks = read_test_file_alloc(hooks_path);
@@ -13807,9 +14480,16 @@ TEST(cli_codex_preflight_reports_heading_and_reason) {
char codex_dir[512];
char config_path[640];
char agents_path[640];
char legacy_root[640];
char legacy_file[768];
char canonical_file[768];
snprintf(codex_dir, sizeof(codex_dir), "%s/.codex", tmpdir);
snprintf(config_path, sizeof(config_path), "%s/config.toml", codex_dir);
snprintf(agents_path, sizeof(agents_path), "%s/AGENTS.md", codex_dir);
snprintf(legacy_root, sizeof(legacy_root), "%s/skills", codex_dir);
snprintf(legacy_file, sizeof(legacy_file), "%s/codebase-memory/SKILL.md", legacy_root);
snprintf(canonical_file, sizeof(canonical_file), "%s/.agents/skills/codebase-memory/SKILL.md",
tmpdir);
if (test_mkdirp(codex_dir) != 0) {
test_rmdir_r(tmpdir);
FAIL("failed to create Codex preflight fixture directory");
@@ -13822,6 +14502,10 @@ TEST(cli_codex_preflight_reports_heading_and_reason) {
test_rmdir_r(tmpdir);
FAIL("failed to write Codex preflight fixture config");
}
if (cbm_install_skills(legacy_root, false, false) != CBM_SKILL_COUNT) {
test_rmdir_r(tmpdir);
FAIL("failed to seed owned legacy Codex skill");
}
char *saved_home = save_test_env("HOME");
char *saved_path = save_test_env("PATH");
@@ -13829,6 +14513,7 @@ TEST(cli_codex_preflight_reports_heading_and_reason) {
cbm_setenv("HOME", tmpdir, 1);
cbm_setenv("PATH", tmpdir, 1);
cbm_unsetenv("CODEX_HOME");
cbm_cli_set_client_selection_for_testing("codex");
FILE *capture = tmpfile();
int saved_stdout = capture ? dup(STDOUT_FILENO) : -1;
@@ -13863,8 +14548,11 @@ TEST(cli_codex_preflight_reports_heading_and_reason) {
}
char *after = read_test_file_alloc(config_path);
char *agents_after = read_test_file_alloc(agents_path);
struct stat state;
bool unchanged = after && strcmp(after, ambiguous) == 0 && agents_after &&
strcmp(agents_after, test_codex_activation_block) == 0;
strcmp(agents_after, test_codex_activation_block) == 0 &&
stat(canonical_file, &state) != 0 &&
cli_test_file_equals(legacy_file, cbm_get_skills()[0].content);
bool diagnostic =
strstr(output, "Codex CLI:\nerror: agent_config agent=Codex CLI op=hook_preflight path=") !=
NULL &&
@@ -13872,6 +14560,7 @@ TEST(cli_codex_preflight_reports_heading_and_reason) {
free(after);
free(agents_after);
cbm_cli_set_client_selection_for_testing(NULL);
restore_test_env("HOME", saved_home);
restore_test_env("PATH", saved_path);
restore_test_env("CODEX_HOME", saved_codex);
@@ -19579,6 +20268,10 @@ SUITE(cli) {
RUN_TEST(cli_zed_mcp_preserves_settings);
RUN_TEST(cli_zed_mcp_uninstall);
RUN_TEST(cli_zed_mcp_jsonc_comments);
#ifndef _WIN32
RUN_TEST(cli_install_agent_config_symlinks_issue2671);
RUN_TEST(cli_install_agent_config_file_symlink_issue2671);
#endif
/* PATH management (3 tests) */
RUN_TEST(cli_ensure_path_append);
@@ -19663,6 +20356,7 @@ SUITE(cli) {
#endif
#ifndef _WIN32
RUN_TEST(cli_dry_run_predicts_refused_hook_script_issue1387);
RUN_TEST(cli_extension_leaves_user_owned_module_untouched_issue2204);
#endif
RUN_TEST(cli_agent_client_registry_routes_plan_install_and_uninstall);
RUN_TEST(cli_registry_installs_kimi_rovo_amp_durable_context);
@@ -19680,6 +20374,14 @@ SUITE(cli) {
RUN_TEST(cli_codex_respects_codex_home);
RUN_TEST(cli_grok_respects_grok_home);
RUN_TEST(cli_codex_install_uses_global_activation_pointer_issue1689);
RUN_TEST(cli_codex_skill_migrates_owned_legacy_to_agents_root);
RUN_TEST(cli_codex_skill_migrates_every_released_legacy_payload);
RUN_TEST(cli_codex_foreign_legacy_does_not_block_shared_clients);
RUN_TEST(cli_codex_skill_preserves_foreign_legacy_until_force);
RUN_TEST(cli_codex_skill_preserves_foreign_canonical_and_owned_legacy);
RUN_TEST(cli_codex_skill_failed_destination_preserves_owned_legacy);
RUN_TEST(cli_codex_skill_uninstall_cleans_both_owned_locations);
RUN_TEST(cli_codex_skill_deduplicates_agents_codex_home);
RUN_TEST(cli_gemini_session_hook_uses_json_for_all_sources);
RUN_TEST(cli_gemini_installs_dedicated_graph_subagent);
RUN_TEST(cli_antigravity_does_not_imply_gemini);
+39
View File
@@ -725,6 +725,44 @@ TEST(config_text_write_owned_document_if_unchanged_rejects_stale_snapshot) {
PASS();
}
/* #2204: the generated OpenCode module must not be appended to a file the user
* owns. The probe separates "may be written" (absent, whitespace-only, or
* already carrying our markers) from "the user's file" (content, no markers) so
* the installer can leave it alone instead of producing an unparseable hybrid. */
TEST(config_text_managed_block_status_reports_foreign_module) {
char dir[CTE_PATH_CAP];
char path[CTE_PATH_CAP];
const char whitespace[] = " \n\t\n";
const char foreign[] = "import { spawn } from 'node:child_process';\n"
"export default { id: 'codebase-memory-augment', setup() {} };\n";
const char unbalanced[] = CTE_BEGIN "\nowned\n";
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
/* No file yet, an empty file, and a whitespace-only file are all writable. */
ASSERT_EQ(cbm_text_managed_block_status(path, CTE_BEGIN, CTE_END), 0);
ASSERT_EQ(cte_write_bytes(path, "", 0U), 0);
ASSERT_EQ(cbm_text_managed_block_status(path, CTE_BEGIN, CTE_END), 0);
ASSERT_EQ(cte_write_bytes(path, whitespace, sizeof(whitespace) - 1U), 0);
ASSERT_EQ(cbm_text_managed_block_status(path, CTE_BEGIN, CTE_END), 0);
/* Our own block refreshes in place, so it is writable too. */
ASSERT_EQ(cbm_text_upsert_managed_block(path, CTE_BEGIN, CTE_END, "owned"), 0);
ASSERT_EQ(cbm_text_managed_block_status(path, CTE_BEGIN, CTE_END), 0);
/* A hand-ported module with no markers is the user's: refuse, never append. */
ASSERT_EQ(cte_write_bytes(path, foreign, sizeof(foreign) - 1U), 0);
ASSERT_EQ(cbm_text_managed_block_status(path, CTE_BEGIN, CTE_END), 1);
/* Unbalanced markers and malformed marker arguments are unsafe states. */
ASSERT_EQ(cte_write_bytes(path, unbalanced, sizeof(unbalanced) - 1U), 0);
ASSERT_EQ(cbm_text_managed_block_status(path, CTE_BEGIN, CTE_END), -1);
ASSERT_EQ(cbm_text_managed_block_status(path, "same", "same"), -1);
ASSERT_EQ(cbm_text_managed_block_status(NULL, CTE_BEGIN, CTE_END), -1);
th_cleanup(dir);
PASS();
}
SUITE(config_text_edit) {
RUN_TEST(config_text_managed_insert_preserves_bom_comments_and_is_idempotent);
RUN_TEST(config_text_managed_replace_preserves_crlf_surroundings);
@@ -732,6 +770,7 @@ SUITE(config_text_edit) {
RUN_TEST(config_text_managed_remove_preserves_user_bytes);
RUN_TEST(config_text_managed_malformed_markers_fail_closed);
RUN_TEST(config_text_managed_rejects_unsafe_markers_and_owned_content);
RUN_TEST(config_text_managed_block_status_reports_foreign_module);
RUN_TEST(config_text_owned_document_write_remove_exact_only);
RUN_TEST(config_text_owned_document_migrates_exact_releases_only);
RUN_TEST(config_text_rejects_invalid_utf8_and_controls);
+191
View File
@@ -944,10 +944,201 @@ TEST(cross_repo_caller_without_local_handler_keeps_cross_http_issue1459) {
PASS();
}
static bool cross_repo_seed_server_route(const cross_repo_fixture_t *fixture,
const char *target_project, const char *method,
const char *route_path, const char *handler_name) {
char target_path[512];
if (!cross_repo_project_path(fixture, target_project, target_path, sizeof(target_path))) {
return false;
}
cbm_store_t *target = cbm_store_open_path(target_path);
if (!target) {
return false;
}
bool ok = cbm_store_upsert_project(target, target_project, fixture->cache) == CBM_STORE_OK;
char target_route_qn[256];
char handler_qn[256];
char route_name[128];
snprintf(target_route_qn, sizeof(target_route_qn), "__route__%s__%s", method, route_path);
snprintf(handler_qn, sizeof(handler_qn), "%s.%s", target_project, handler_name);
snprintf(route_name, sizeof(route_name), "%s %s", method, route_path);
cbm_node_t target_route = {.project = target_project,
.label = "Route",
.name = route_name,
.qualified_name = target_route_qn,
.file_path = "server.java"};
cbm_node_t handler = {.project = target_project,
.label = "Function",
.name = handler_name,
.qualified_name = handler_qn,
.file_path = "server.java"};
int64_t target_route_id = ok ? cbm_store_upsert_node(target, &target_route) : 0;
int64_t handler_id = ok ? cbm_store_upsert_node(target, &handler) : 0;
cbm_edge_t handles = {.project = target_project,
.source_id = handler_id,
.target_id = target_route_id,
.type = "HANDLES"};
ok = ok && target_route_id > 0 && handler_id > 0 && cbm_store_insert_edge(target, &handles) > 0;
cbm_store_close(target);
return ok;
}
static bool cross_repo_seed_client_call(const cross_repo_fixture_t *fixture,
const char *source_project, const char *caller_name,
const char *callee_name, const char *url_path,
const char *method) {
char source_path[512];
if (!cross_repo_project_path(fixture, source_project, source_path, sizeof(source_path))) {
return false;
}
cbm_store_t *source = cbm_store_open_path(source_path);
if (!source) {
return false;
}
bool ok = cbm_store_upsert_project(source, source_project, fixture->cache) == CBM_STORE_OK;
char caller_qn[256];
char local_route_qn[256];
char edge_props[512];
snprintf(caller_qn, sizeof(caller_qn), "%s.%s", source_project, caller_name);
snprintf(local_route_qn, sizeof(local_route_qn), "__route__%s__%s", method ? method : "ANY",
url_path);
if (method) {
snprintf(edge_props, sizeof(edge_props),
"{\"callee\":\"%s\",\"url_path\":\"%s\",\"method\":\"%s\"}", callee_name,
url_path, method);
} else {
snprintf(edge_props, sizeof(edge_props),
"{\"callee\":\"%s\",\"url_path\":\"%s\"}", callee_name, url_path);
}
cbm_node_t caller = {.project = source_project,
.label = "Function",
.name = caller_name,
.qualified_name = caller_qn,
.file_path = "item.service.ts"};
cbm_node_t local_route = {.project = source_project,
.label = "Route",
.name = url_path,
.qualified_name = local_route_qn,
.file_path = "item.service.ts"};
int64_t caller_id = ok ? cbm_store_upsert_node(source, &caller) : 0;
int64_t local_route_id = ok ? cbm_store_upsert_node(source, &local_route) : 0;
cbm_edge_t http_call = {.project = source_project,
.source_id = caller_id,
.target_id = local_route_id,
.type = "HTTP_CALLS",
.properties_json = edge_props};
ok = ok && caller_id > 0 && local_route_id > 0 && cbm_store_insert_edge(source, &http_call) > 0;
cbm_store_close(source);
return ok;
}
static int cross_repo_count_edges_like(const cross_repo_fixture_t *fixture, const char *project,
const char *edge_type, const char *pattern) {
char path[512];
if (!cross_repo_project_path(fixture, project, path, sizeof(path))) {
return -1;
}
cbm_store_t *store = cbm_store_open_path_query(path);
if (!store) {
return -1;
}
sqlite3_stmt *s = NULL;
int count = -1;
if (sqlite3_prepare_v2(
cbm_store_get_db(store),
"SELECT COUNT(*) FROM edges WHERE project = ?1 AND type = ?2 AND properties LIKE ?3",
-1, &s, NULL) == SQLITE_OK) {
sqlite3_bind_text(s, 1, project, -1, SQLITE_STATIC);
sqlite3_bind_text(s, 2, edge_type, -1, SQLITE_STATIC);
sqlite3_bind_text(s, 3, pattern, -1, SQLITE_STATIC);
if (sqlite3_step(s) == SQLITE_ROW) {
count = sqlite3_column_int(s, 0);
}
sqlite3_finalize(s);
}
cbm_store_close(store);
return count;
}
/* #2768: Angular HttpClient calls must match cross-repo routes with exact HTTP verbs.
* When client makes GET requests to /items/list, /items/42, and /items/job/10,
* and the server defines overlapping routes:
* DELETE /items/{id} (seeded first in DB)
* GET /items/{id} (seeded second in DB)
* POST /items/{parentId}/{id} (seeded first in DB)
* GET /items/job/{jobId} (seeded second in DB)
* the fuzzy route matcher must enforce method compatibility, linking the GET calls
* to their respective GET endpoints and never to DELETE or POST endpoints.
* getItem and getJob are seeded WITHOUT a method property (an edge written by an
* index that did not record the verb): the matcher must infer GET from the
* callee's verb suffix, or the method-less fuzzy match lands on the DELETE / POST
* route seeded first. getItems keeps its explicit "GET". */
TEST(cross_repo_angular_http_client_exact_verb_matching_issue2768) {
cross_repo_fixture_t fixture;
bool setup = cross_repo_fixture_begin(&fixture) &&
/* Target server with overlapping endpoints */
cross_repo_seed_server_route(&fixture, "api-2768", "GET", "/items/list",
"handle_list_items") &&
cross_repo_seed_server_route(&fixture, "api-2768", "DELETE", "/items/{}",
"handle_delete_item") &&
cross_repo_seed_server_route(&fixture, "api-2768", "GET", "/items/{}",
"handle_get_item") &&
cross_repo_seed_server_route(&fixture, "api-2768", "POST", "/items/{}/{}",
"handle_post_nested_item") &&
cross_repo_seed_server_route(&fixture, "api-2768", "GET", "/items/job/{}",
"handle_get_job") &&
/* Angular client making GET requests */
cross_repo_seed_client_call(&fixture, "angular-2768", "getItems",
"this.http.get", "/items/list", "GET") &&
cross_repo_seed_client_call(&fixture, "angular-2768", "getItem",
"this.http.get", "/items/42", NULL) &&
cross_repo_seed_client_call(&fixture, "angular-2768", "getJob",
"this.http.get", "/items/job/10", NULL);
if (!setup) {
cross_repo_fixture_end(&fixture);
FAIL("failed to seed issue 2768 fixture");
}
const char *target = "api-2768";
cbm_cross_repo_result_t fwd = cbm_cross_repo_match("angular-2768", &target, 1);
ASSERT_FALSE(fwd.failed);
ASSERT_EQ(fwd.http_edges, 3);
/* Assert that client CROSS_HTTP_CALLS edges are linked to GET handlers, not DELETE or POST */
ASSERT_EQ(cross_repo_count_edges_like(&fixture, "angular-2768", "CROSS_HTTP_CALLS",
"%handle_delete_item%"),
0);
ASSERT_EQ(cross_repo_count_edges_like(&fixture, "angular-2768", "CROSS_HTTP_CALLS",
"%handle_post_nested_item%"),
0);
ASSERT_EQ(cross_repo_count_edges_like(&fixture, "angular-2768", "CROSS_HTTP_CALLS",
"%handle_list_items%"),
1);
ASSERT_EQ(cross_repo_count_edges_like(&fixture, "angular-2768", "CROSS_HTTP_CALLS",
"%handle_get_item%"),
1);
ASSERT_EQ(cross_repo_count_edges_like(&fixture, "angular-2768", "CROSS_HTTP_CALLS",
"%handle_get_job%"),
1);
/* Reverse pass */
const char *consumer = "angular-2768";
cbm_cross_repo_result_t rev = cbm_cross_repo_match("api-2768", &consumer, 1);
ASSERT_FALSE(rev.failed);
ASSERT_EQ(rev.http_edges, 3);
cross_repo_fixture_end(&fixture);
cbm_cross_repo_result_free(&fwd);
cbm_cross_repo_result_free(&rev);
PASS();
}
SUITE(cross_repo) {
RUN_TEST(cross_repo_caller_local_handler_blocks_cross_http_issue1459);
RUN_TEST(cross_repo_methodless_call_to_local_get_route_blocks_cross_http_issue1459);
RUN_TEST(cross_repo_caller_without_local_handler_keeps_cross_http_issue1459);
RUN_TEST(cross_repo_angular_http_client_exact_verb_matching_issue2768);
RUN_TEST(cross_repo_accepts_project_with_missed_shadow_row_issue1609);
RUN_TEST(cross_repo_null_target_fails_without_dereference);
RUN_TEST(cross_repo_wildcard_keeps_projects_containing_internal_tokens);
+171
View File
@@ -1226,6 +1226,174 @@ TEST(cslsp_ordinary_same_leaf_calls_join_by_exact_site) {
PASS();
}
/* A qualified type name resolves its qualifier as a TYPE when no namespace
* binds it (nested types, #2120). A using alias whose target is qualified by
* the alias itself (`using Loop = Loop.Inner;`) or by a mutual alias re-enters
* that walk through the alias step forever: each `Loop` resolves the target
* `Loop.Inner`, whose qualifier is `Loop` again. The walk is depth-bounded;
* the file must still finish and resolve its ordinary calls. */
TEST(cslsp_cyclic_alias_qualifier_terminates) {
const char *src = "using Loop = Loop.Inner;\n"
"using Ping = Pong.Inner;\n"
"using Pong = Ping.Inner;\n"
"namespace App {\n"
" public class Svc { public void Go() { } }\n"
" public class Runner {\n"
" public void Run(Loop a, Ping b) {\n"
" var x = new Loop();\n"
" var y = new Ping.Leaf();\n"
" var s = new Svc();\n"
" s.Go();\n"
" }\n"
" }\n"
"}\n";
CBMFileResult *r = extract_cs(src);
ASSERT(r);
ASSERT(require_resolved(r, "Runner.Run", "Svc.Go") >= 0);
cbm_free_result(r);
PASS();
}
static const CBMDefinition *find_def_named(const CBMFileResult *r, const char *name) {
for (int i = 0; i < r->defs.count; i++) {
if (r->defs.items[i].name && strcmp(r->defs.items[i].name, name) == 0)
return &r->defs.items[i];
}
return NULL;
}
/* A top-level C# type records its declared namespace; "" means the global
* namespace and is claimed only on a clean parse. The vendored grammar does
* not parse a `ref partial struct` with members, and error recovery dissolves
* the enclosing namespace, so the types after it surface with no namespace
* ancestor. Those must not read as global (they would hide every imported
* type of the same name); unknown (NULL) keeps them out of namespace binding
* exactly as before global-namespace binding existed. */
TEST(cslsp_decl_namespace_global_needs_clean_parse) {
CBMFileResult *clean = extract_cs("public class Loose { }\n"
"namespace Acme.Lib { public class Kept { } }\n");
ASSERT(clean);
const CBMDefinition *loose = find_def_named(clean, "Loose");
const CBMDefinition *kept = find_def_named(clean, "Kept");
ASSERT(loose && kept);
ASSERT(loose->decl_namespace && strcmp(loose->decl_namespace, "") == 0);
ASSERT(kept->decl_namespace && strcmp(kept->decl_namespace, "Acme.Lib") == 0);
cbm_free_result(clean);
CBMFileResult *damaged = extract_cs("namespace Acme.Lib\n"
"{\n"
" public ref partial struct Handler\n"
" {\n"
" public void Append(string value) { }\n"
" }\n"
" public static partial class Gadget { }\n"
"}\n");
ASSERT(damaged);
const CBMDefinition *gadget = find_def_named(damaged, "Gadget");
ASSERT(gadget);
if (gadget->decl_namespace && !gadget->decl_namespace[0])
printf(" Gadget after a parse error claims the global namespace\n");
ASSERT(!gadget->decl_namespace || gadget->decl_namespace[0]);
cbm_free_result(damaged);
PASS();
}
static bool has_resolved_row(const CBMResolvedCallArray *out, const char *caller_qn,
const char *callee_qn) {
for (int i = 0; i < out->count; i++) {
const CBMResolvedCall *rc = &out->items[i];
if (rc->caller_qn && rc->callee_qn && strcmp(rc->caller_qn, caller_qn) == 0 &&
strcmp(rc->callee_qn, callee_qn) == 0)
return true;
}
return false;
}
/* `partial` pieces of one namespaced type are separate registry entries, so
* a member declared on another piece still binds (#2120). Types declared in
* the GLOBAL namespace carry the empty namespace, and the same simple name
* there is a different type per assembly across a repository (each test
* project's own `Helper`). They must never pool as twins: the twin probe runs
* before the base-class walk, so pooling bound `h.Stop()` on ToolA's Helper
* to the unrelated ToolB Helper instead of ToolA's own base ToolABase. Runs
* the production Tier-2 C# registry builder over cross-file defs. */
TEST(cslsp_global_namespace_types_are_not_partial_twins) {
CBMArena arena;
cbm_arena_init(&arena);
CBMLSPDef defs[] = {
{.qualified_name = "p.ToolA.Helper.Helper",
.short_name = "Helper",
.label = "Class",
.def_module_qn = "p.ToolA.Helper",
.embedded_types = "p.ToolA.Base.ToolABase",
.lang = CBM_LANG_CSHARP},
{.qualified_name = "p.ToolA.Helper.Helper.Start",
.short_name = "Start",
.label = "Method",
.receiver_type = "p.ToolA.Helper.Helper",
.def_module_qn = "p.ToolA.Helper",
.lang = CBM_LANG_CSHARP},
{.qualified_name = "p.ToolA.Base.ToolABase",
.short_name = "ToolABase",
.label = "Class",
.def_module_qn = "p.ToolA.Base",
.lang = CBM_LANG_CSHARP},
{.qualified_name = "p.ToolA.Base.ToolABase.Stop",
.short_name = "Stop",
.label = "Method",
.receiver_type = "p.ToolA.Base.ToolABase",
.def_module_qn = "p.ToolA.Base",
.lang = CBM_LANG_CSHARP},
{.qualified_name = "p.ToolB.Helper.Helper",
.short_name = "Helper",
.label = "Class",
.def_module_qn = "p.ToolB.Helper",
.lang = CBM_LANG_CSHARP},
{.qualified_name = "p.ToolB.Helper.Helper.Stop",
.short_name = "Stop",
.label = "Method",
.receiver_type = "p.ToolB.Helper.Helper",
.def_module_qn = "p.ToolB.Helper",
.lang = CBM_LANG_CSHARP},
};
CBMTypeRegistry *reg =
cbm_cs_build_cross_registry(&arena, defs, (int)(sizeof(defs) / sizeof(defs[0])));
ASSERT_NOT_NULL(reg);
/* No namespace anywhere: both Helpers and the base live in the global
* namespace. The caller sits in ToolA's Helper file, so `new Helper()`
* binds ToolA's Helper through the same-module step, independent of
* registration order. */
const char *src = "public class Helper : ToolABase { public void Start() { } }\n"
"public class Caller {\n"
" public void Go() {\n"
" var h = new Helper();\n"
" h.Start();\n"
" h.Stop();\n"
" }\n"
"}\n";
CBMResolvedCallArray out = {0};
cbm_run_cs_lsp_cross_with_registry(&arena, src, (int)strlen(src), "p.ToolA.Helper", reg, NULL,
0, NULL, &out);
const char *caller = "p.ToolA.Helper.Caller.Go";
bool start_own = has_resolved_row(&out, caller, "p.ToolA.Helper.Helper.Start");
bool stop_base = has_resolved_row(&out, caller, "p.ToolA.Base.ToolABase.Stop");
bool stop_twin = has_resolved_row(&out, caller, "p.ToolB.Helper.Helper.Stop");
if (!start_own || !stop_base || stop_twin) {
printf(" start_own=%d stop_base=%d stop_unrelated_global_twin=%d\n", start_own, stop_base,
stop_twin);
for (int i = 0; i < out.count; i++) {
printf(" %s -> %s [%s]\n", out.items[i].caller_qn ? out.items[i].caller_qn : "?",
out.items[i].callee_qn ? out.items[i].callee_qn : "?",
out.items[i].strategy ? out.items[i].strategy : "?");
}
}
cbm_arena_destroy(&arena);
ASSERT_TRUE(start_own);
ASSERT_TRUE(stop_base);
ASSERT_FALSE(stop_twin);
PASS();
}
/* ── Suite ─────────────────────────────────────────────────── */
SUITE(cs_lsp) {
@@ -1290,4 +1458,7 @@ SUITE(cs_lsp) {
RUN_TEST(cslsp_stringbuilder_chain);
RUN_TEST(cslsp_httpclient_chain);
RUN_TEST(cslsp_ordinary_same_leaf_calls_join_by_exact_site);
RUN_TEST(cslsp_cyclic_alias_qualifier_terminates);
RUN_TEST(cslsp_global_namespace_types_are_not_partial_twins);
RUN_TEST(cslsp_decl_namespace_global_needs_clean_parse);
}
+89 -3
View File
@@ -1662,6 +1662,7 @@ typedef struct {
* itself instead, which is the edge a reader on another thread needs. */
atomic_bool args_captured[APP_FAKE_MAX_ATTEMPTS];
size_t memory_budgets[APP_FAKE_MAX_ATTEMPTS];
bool background_requests[APP_FAKE_MAX_ATTEMPTS];
} app_fake_worker_context_t;
typedef struct {
@@ -1729,6 +1730,8 @@ static int app_fake_worker_start(void *opaque, const char *args_json, size_t mem
args_json ? args_json : "");
atomic_store(&context->args_captured[worker->attempt], true);
context->memory_budgets[worker->attempt] = memory_budget_bytes;
context->background_requests[worker->attempt] =
cbm_mcp_get_bool_arg(args_json, "_cbm_background");
if (marker_file) {
(void)snprintf(context->marker_paths[worker->attempt], APP_TEST_PATH_CAP, "%s",
marker_file);
@@ -2546,6 +2549,7 @@ TEST(daemon_application_initialize_coalesces_auto_index_for_full_sessions) {
ASSERT_TRUE(first_initialized);
ASSERT_TRUE(first_owned);
ASSERT_TRUE(auto_policy_propagated);
ASSERT_TRUE(fake.background_requests[0]);
ASSERT_TRUE(second_initialized);
ASSERT_TRUE(coalesced);
ASSERT_TRUE(restricted_disconnect_kept_job);
@@ -4287,6 +4291,7 @@ TEST(daemon_application_request_cancel_preserves_persistent_watch_and_session) {
ASSERT_TRUE(thread_started);
ASSERT_TRUE(subscribed);
ASSERT_TRUE(worker_started);
ASSERT_FALSE(fixture.fake.background_requests[0]);
ASSERT_TRUE(request_returned);
ASSERT_TRUE(thread_joined);
ASSERT_EQ(request.status, CBM_DAEMON_RUNTIME_APPLICATION_CANCELLED);
@@ -4584,6 +4589,7 @@ TEST(daemon_application_final_cancel_drains_admitted_watcher_job) {
ASSERT_TRUE(fixture_ready);
ASSERT_TRUE(thread_started);
ASSERT_TRUE(worker_started);
ASSERT_TRUE(fixture.fake.background_requests[0]);
ASSERT_TRUE(job_active);
ASSERT_EQ(watches_after_cancel, 0);
ASSERT_TRUE(thread_joined);
@@ -7292,8 +7298,9 @@ typedef struct {
uint32_t context_length;
} app_async_fixture_t;
static bool app_async_fixture_init(app_async_fixture_t *fixture, const char *label,
bool permanent) {
/* stored_config is optional; a test that needs auto_index passes its own. */
static bool app_async_fixture_init_with_config(app_async_fixture_t *fixture, const char *label,
bool permanent, cbm_config_t *stored_config) {
memset(fixture, 0, sizeof(*fixture));
app_fake_worker_context_init(&fixture->fake);
fixture->ops = (cbm_daemon_application_worker_ops_t){
@@ -7304,7 +7311,8 @@ static bool app_async_fixture_init(app_async_fixture_t *fixture, const char *lab
.log_path = app_fake_worker_log_path,
.destroy = app_fake_worker_destroy,
};
cbm_daemon_application_config_t config = {.worker_ops = &fixture->ops};
cbm_daemon_application_config_t config = {.config = stored_config,
.worker_ops = &fixture->ops};
fixture->application = cbm_daemon_application_new(&config);
if (!fixture->application) {
return false;
@@ -7325,6 +7333,11 @@ static bool app_async_fixture_init(app_async_fixture_t *fixture, const char *lab
&fixture->context_length);
}
static bool app_async_fixture_init(app_async_fixture_t *fixture, const char *label,
bool permanent) {
return app_async_fixture_init_with_config(fixture, label, permanent, NULL);
}
static cbm_daemon_runtime_application_session_t *app_async_session(app_async_fixture_t *fixture,
cbm_daemon_client_id_t id) {
cbm_daemon_runtime_application_session_t *session = app_test_open(&fixture->callbacks, id);
@@ -7702,6 +7715,78 @@ TEST(daemon_application_cut_short_sync_index_advice_reaches_next_call) {
* the daemon's only live session is refused async (the daemon would stop and
* cancel the job as soon as the command exits), while status stays allowed and
* a long-lived MCP session on the same temporary daemon remains a valid host. */
/* Automatic indexing (session auto-index, watcher re-index) marks its job
* _cbm_background so the worker leaves CPU headroom. The marker picks the
* worker count, never what is indexed, so an explicit index_repository for the
* same root still JOINS the automatic job already running for it, the contract
* the repo_path fold above restored, instead of being refused as an options
* conflict. The worker is held, so the join is observed while the job runs. */
TEST(daemon_application_explicit_index_joins_running_auto_index_job) {
app_env_backup_t cache_environment;
bool cache_saved = app_env_backup_capture(&cache_environment, "CBM_CACHE_DIR");
char cache[APP_TEST_PATH_CAP];
(void)snprintf(cache, sizeof(cache), "%s/cbm-app-auto-join-cache-XXXXXX", cbm_tmpdir());
bool cache_ready =
cache_saved && cbm_mkdtemp(cache) != NULL && cbm_setenv("CBM_CACHE_DIR", cache, 1) == 0;
cbm_config_t *stored_config = cache_ready ? cbm_config_open(cache) : NULL;
bool config_ready = stored_config &&
cbm_config_set(stored_config, CBM_CONFIG_AUTO_INDEX, "true") == 0 &&
cbm_config_set(stored_config, CBM_CONFIG_AUTO_WATCH, "false") == 0;
app_async_fixture_t fixture;
bool setup = config_ready &&
app_async_fixture_init_with_config(&fixture, "auto-join", true, stored_config);
cbm_daemon_runtime_application_session_t *automatic =
setup ? app_test_open(&fixture.callbacks, 7801) : NULL;
bool auto_started = automatic &&
app_test_initialize_profile(&fixture.callbacks, automatic, fixture.root,
CBM_MCP_TOOL_PROFILE_ALL, NULL, NULL) &&
app_wait_for_atomic_int(&fixture.fake.starts, 1);
/* args_json is published by the args_captured release store; the
* background_requests slot is written after it, so read the args. */
bool auto_marked = auto_started &&
app_wait_for_atomic_bool(&fixture.fake.args_captured[0], true) &&
cbm_mcp_get_bool_arg(fixture.fake.args_json[0], "_cbm_background");
cbm_daemon_runtime_application_session_t *explicit_session =
auto_marked ? app_async_session(&fixture, 7802) : NULL;
char *joined =
explicit_session ? app_async_call_now(&fixture, explicit_session, ",\"async\":true") : NULL;
int starts_while_running = atomic_load(&fixture.fake.starts);
size_t active_while_running =
setup ? cbm_daemon_application_active_jobs(fixture.application) : 0;
if (explicit_session) {
fixture.callbacks.session_close(fixture.callbacks.context, explicit_session);
}
if (automatic) {
fixture.callbacks.session_cancel(fixture.callbacks.context, automatic);
fixture.callbacks.session_close(fixture.callbacks.context, automatic);
}
bool stopped = !setup || app_async_fixture_finish(&fixture);
cbm_config_close(stored_config);
bool cleaned = !cache_ready || th_rmtree(cache) == 0;
bool cache_restored = app_env_backup_restore(&cache_environment);
ASSERT_TRUE(cache_ready);
ASSERT_TRUE(config_ready);
ASSERT_TRUE(setup);
ASSERT_TRUE(auto_started);
ASSERT_TRUE(auto_marked);
ASSERT_NOT_NULL(explicit_session);
ASSERT_NOT_NULL(joined);
ASSERT_FALSE(app_async_contains(joined, "different options"));
ASSERT_FALSE(app_async_contains(joined, "\"isError\":true"));
ASSERT_TRUE(app_async_contains(joined, "\"joined\":true"));
ASSERT_EQ(starts_while_running, 1);
ASSERT_EQ(active_while_running, 1);
ASSERT_TRUE(stopped);
ASSERT_TRUE(cleaned);
ASSERT_TRUE(cache_restored);
free(joined);
PASS();
}
TEST(daemon_application_async_refused_for_lone_one_shot_client_of_temporary_daemon) {
app_async_fixture_t fixture;
bool setup = app_async_fixture_init(&fixture, "async-refuse", false);
@@ -8031,6 +8116,7 @@ SUITE(daemon_application) {
RUN_TEST(daemon_application_async_and_status_validation_errors);
RUN_TEST(daemon_application_cancelled_sync_index_reply_offers_async);
RUN_TEST(daemon_application_cut_short_sync_index_advice_reaches_next_call);
RUN_TEST(daemon_application_explicit_index_joins_running_auto_index_job);
RUN_TEST(daemon_application_async_refused_for_lone_one_shot_client_of_temporary_daemon);
RUN_TEST(daemon_application_unnamed_status_finds_async_job_of_root_owner_issue2134);
RUN_TEST(daemon_application_unnamed_status_keeps_path_derived_key_issue2134);
+19
View File
@@ -173,6 +173,13 @@ TEST(infrascan_service_pattern_match_rejects_ids_inside_words) {
{"kafkaesque.story.tell", CBM_SVC_NONE},
/* "Axios" glued to a lowercase continuation */
{"geo.Axiosphere.map", CBM_SVC_NONE},
/* Angular receiver rows end in a dot (#2768): a longer receiver that
* only starts with the same letters is not the HttpClient (NestJS
* route registration, AngularJS test double) */
{"this.httpAdapter.get", CBM_SVC_NONE},
{"this.httpServer.post", CBM_SVC_NONE},
{"$httpBackend.expectGET", CBM_SVC_NONE},
{"this.httpAdapter\n .get", CBM_SVC_NONE},
{NULL, CBM_SVC_NONE},
};
ASSERT_EQ(svc_case_mismatches(cases), 0);
@@ -202,6 +209,18 @@ TEST(infrascan_service_pattern_match_keeps_real_library_qns) {
{"Axios.post", CBM_SVC_HTTP},
{"AxiosInstance.get", CBM_SVC_HTTP},
{"curl_exec", CBM_SVC_HTTP},
/* Angular HttpClient receivers (#2768); `_httpClient` is the common
* C# field spelling of the same client */
{"this.http.get", CBM_SVC_HTTP},
{"this.httpClient.post", CBM_SVC_HTTP},
{"httpClient.put", CBM_SVC_HTTP},
{"_httpClient.GetAsync", CBM_SVC_HTTP},
{"$http.get", CBM_SVC_HTTP},
/* a chain wrapped before the verb keeps the line break (LF / CRLF) */
{"this.http\n .post", CBM_SVC_HTTP},
{"this.http\r\n .delete", CBM_SVC_HTTP},
{"this.httpClient\n .put", CBM_SVC_HTTP},
{"$http\n .get", CBM_SVC_HTTP},
{"surf.get", CBM_SVC_HTTP},
{"hyper.Client.request", CBM_SVC_HTTP},
{"phin.promisified", CBM_SVC_HTTP},
+367 -1
View File
@@ -7843,6 +7843,200 @@ TEST(tool_index_status_includes_git_metadata) {
* TOOL HANDLERS WITH DATA
* ══════════════════════════════════════════════════════════════════ */
/* #2816: stored recursion must remain visible even when the CALLS graph has
* no self-edges. Removing metadata emission must fail these assertions. */
TEST(tool_trace_path_recursion_metadata_issue2816) {
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
ASSERT_NOT_NULL(srv);
cbm_store_t *st = cbm_mcp_server_store(srv);
const char *proj = "recursion-proj";
cbm_mcp_server_set_project(srv, proj);
ASSERT_EQ(cbm_store_upsert_project(st, proj, "/tmp/recursion"), CBM_STORE_OK);
const struct {
const char *name;
const char *props;
const char *self;
const char *recursive;
} cases[] = {
{"countdown", "{\"self_recursive\":true,\"recursive\":true}", "true", "true"},
{"visit", "{\"self_recursive\":true,\"recursive\":true}", "true", "true"},
{"mutual", "{\"self_recursive\":false,\"recursive\":true}", "false", "true"},
{"leaf", "{\"self_recursive\":false,\"recursive\":false}", "false", "false"},
{"legacy", "{}", "null", "null"},
{"partial", "{\"self_recursive\":true}", "true", "null"},
{"invalid", "{\"self_recursive\":\"true\",\"recursive\":1}", "null", "null"},
};
for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
char qn[128];
snprintf(qn, sizeof(qn), "%s.%s", proj, cases[i].name);
cbm_node_t n = {.project = proj,
.label = i == 1 ? "Method" : "Function",
.name = cases[i].name,
.qualified_name = qn,
.file_path = "recursion.ts",
.start_line = 1,
.end_line = 5,
.properties_json = cases[i].props};
ASSERT_GT(cbm_store_upsert_node(st, &n), 0);
for (int json = 0; json < 2; json++) {
for (int inbound = 0; inbound < 2; inbound++) {
char args[512];
snprintf(args, sizeof(args),
"{\"project\":\"%s\",\"function_name\":\"%s\","
"\"direction\":\"%s\",\"format\":\"%s\"}",
proj, cases[i].name, inbound ? "inbound" : "outbound",
json ? "json" : "tree");
char *response = cbm_mcp_handle_tool(srv, "trace_path", args);
char *inner = extract_text_content(response);
ASSERT_NOT_NULL(inner);
char expected[128];
snprintf(expected, sizeof(expected),
json ? "\"self_recursive\":%s" : "self_recursive: %s\n", cases[i].self);
ASSERT_NOT_NULL(strstr(inner, expected));
snprintf(expected, sizeof(expected), json ? "\"recursive\":%s" : "recursive: %s\n",
cases[i].recursive);
ASSERT_NOT_NULL(strstr(inner, expected));
ASSERT_NOT_NULL(strstr(inner, json ? "\"self_call_policy\":\"omitted\""
: "self_call_policy: omitted\n"));
snprintf(expected, sizeof(expected), json ? "\"%s_total\":0" : "%s_total: 0\n",
inbound ? "callers" : "callees");
ASSERT_NOT_NULL(strstr(inner, expected));
free(inner);
free(response);
}
}
}
cbm_mcp_server_free(srv);
PASS();
}
TEST(tool_trace_path_recursion_union_and_edges_issue2816) {
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
ASSERT_NOT_NULL(srv);
cbm_store_t *st = cbm_mcp_server_store(srv);
const char *proj = "recursion-union";
cbm_mcp_server_set_project(srv, proj);
ASSERT_EQ(cbm_store_upsert_project(st, proj, "/tmp/recursion-union"), CBM_STORE_OK);
cbm_node_t def = {.project = proj,
.label = "Function",
.name = "countdown",
.qualified_name = "recursion-union.impl.countdown",
.file_path = "impl.ts",
.start_line = 1,
.end_line = 10,
.properties_json = "{\"self_recursive\":false,\"recursive\":false}"};
int64_t def_id = cbm_store_upsert_node(st, &def);
ASSERT_GT(def_id, 0);
cbm_node_t stub = def;
stub.qualified_name = "recursion-union.stub.countdown";
stub.file_path = "stub.d.ts";
stub.end_line = 1;
stub.properties_json = "{\"self_recursive\":true,\"recursive\":true}";
ASSERT_GT(cbm_store_upsert_node(st, &stub), 0);
cbm_node_t leaf = def;
leaf.name = "leaf";
leaf.qualified_name = "recursion-union.impl.leaf";
leaf.properties_json = "{\"self_recursive\":true,\"recursive\":true}";
int64_t leaf_id = cbm_store_upsert_node(st, &leaf);
ASSERT_GT(leaf_id, 0);
cbm_edge_t edge = {.project = proj, .source_id = def_id, .target_id = leaf_id, .type = "CALLS"};
ASSERT_GT(cbm_store_insert_edge(st, &edge), 0);
const char *expected_flags[] = {"true", "null", "false", "true"};
for (int state = 0; state < 4; state++) {
def.properties_json = state == 3 ? "{\"self_recursive\":true,\"recursive\":true}"
: "{\"self_recursive\":false,\"recursive\":false}";
stub.properties_json = state == 0 ? "{\"self_recursive\":true,\"recursive\":true}"
: state == 2 ? "{\"self_recursive\":false,\"recursive\":false}"
: "{}";
ASSERT_GT(cbm_store_upsert_node(st, &def), 0);
ASSERT_GT(cbm_store_upsert_node(st, &stub), 0);
for (int json = 0; json < 2; json++) {
char args[512];
snprintf(args, sizeof(args),
"{\"project\":\"%s\",\"function_name\":\"countdown\","
"\"direction\":\"outbound\",\"format\":\"%s\"}",
proj, json ? "json" : "tree");
char *response = cbm_mcp_handle_tool(srv, "trace_path", args);
char *inner = extract_text_content(response);
ASSERT_NOT_NULL(inner);
ASSERT_NOT_NULL(strstr(inner, json ? "\"callees_total\":1" : "callees_total: 1\n"));
ASSERT_NOT_NULL(strstr(inner, json ? "\"leaf\"" : ".leaf"));
char expected[128];
snprintf(expected, sizeof(expected),
json ? "\"self_recursive\":%s" : "self_recursive: %s\n",
expected_flags[state]);
ASSERT_NOT_NULL(strstr(inner, expected));
snprintf(expected, sizeof(expected), json ? "\"recursive\":%s" : "recursive: %s\n",
expected_flags[state]);
ASSERT_NOT_NULL(strstr(inner, expected));
free(inner);
free(response);
}
}
/* Recursion is root metadata, but the omission policy concerns CALLS only. */
char *response =
cbm_mcp_handle_tool(srv, "trace_path",
"{\"project\":\"recursion-union\",\"function_name\":\"countdown\","
"\"edge_types\":[\"IMPORTS\"],\"format\":\"json\"}");
char *inner = extract_text_content(response);
ASSERT_NOT_NULL(inner);
ASSERT_NOT_NULL(strstr(inner, "\"self_recursive\":true"));
ASSERT_NULL(strstr(inner, "self_call_policy"));
free(inner);
free(response);
cbm_mcp_server_free(srv);
PASS();
}
TEST(tool_trace_path_recursion_survives_budget_floor_issue2816) {
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
ASSERT_NOT_NULL(srv);
cbm_store_t *st = cbm_mcp_server_store(srv);
const char *proj = "recursion-budget";
cbm_mcp_server_set_project(srv, proj);
ASSERT_EQ(cbm_store_upsert_project(st, proj, "/tmp/recursion-budget"), CBM_STORE_OK);
char huge[4096];
memset(huge, 'x', sizeof(huge) - 1);
huge[sizeof(huge) - 1] = '\0';
cbm_node_t n = {.project = proj,
.label = "Function",
.name = "countdown",
.qualified_name = "recursion-budget.countdown",
.file_path = "recursion.ts",
.start_line = 1,
.end_line = 10,
.properties_json = "{\"self_recursive\":true,\"recursive\":true}"};
int64_t root_id = cbm_store_upsert_node(st, &n);
ASSERT_GT(root_id, 0);
n.name = huge;
n.qualified_name = huge;
int64_t callee_id = cbm_store_upsert_node(st, &n);
ASSERT_GT(callee_id, 0);
cbm_edge_t edge = {
.project = proj, .source_id = root_id, .target_id = callee_id, .type = "CALLS"};
ASSERT_GT(cbm_store_insert_edge(st, &edge), 0);
for (int json = 0; json < 2; json++) {
char args[512];
snprintf(args, sizeof(args),
"{\"project\":\"%s\",\"function_name\":\"countdown\","
"\"direction\":\"outbound\",\"format\":\"%s\",\"max_output_tokens\":128}",
proj, json ? "json" : "tree");
char *response = cbm_mcp_handle_tool(srv, "trace_path", args);
char *inner = extract_text_content(response);
ASSERT_NOT_NULL(inner);
ASSERT_NOT_NULL(strstr(inner, "output_budget"));
ASSERT_NULL(strstr(inner, huge));
ASSERT_NOT_NULL(strstr(inner, json ? "\"self_recursive\":true" : "self_recursive: true\n"));
ASSERT_NOT_NULL(strstr(inner, json ? "\"recursive\":true" : "recursive: true\n"));
ASSERT_NOT_NULL(strstr(inner, json ? "\"self_call_policy\":\"omitted\""
: "self_call_policy: omitted\n"));
free(inner);
free(response);
}
cbm_mcp_server_free(srv);
PASS();
}
TEST(tool_trace_call_path_not_found) {
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
@@ -8540,7 +8734,8 @@ TEST(tool_trace_cursor_survives_output_budget_increase) {
.qualified_name = "trace-budget-cursor.hub",
.file_path = "src/hub.c",
.start_line = 1,
.end_line = 2};
.end_line = 2,
.properties_json = "{\"self_recursive\":true,\"recursive\":true}"};
cbm_node_t short_node = {.project = project,
.label = "Function",
.name = "short_row",
@@ -8580,6 +8775,9 @@ TEST(tool_trace_cursor_survives_output_budget_increase) {
"\"format\":\"json\"}");
char *inner = extract_text_content(response);
ASSERT_NOT_NULL(inner);
ASSERT_NOT_NULL(strstr(inner, "\"self_recursive\":true"));
ASSERT_NOT_NULL(strstr(inner, "\"recursive\":true"));
ASSERT_NOT_NULL(strstr(inner, "\"self_call_policy\":\"omitted\""));
ASSERT_NOT_NULL(strstr(inner, "short_row"));
yyjson_doc *doc = yyjson_read(inner, strlen(inner), 0);
ASSERT_NOT_NULL(doc);
@@ -8600,6 +8798,9 @@ TEST(tool_trace_cursor_survives_output_budget_increase) {
response = cbm_mcp_handle_tool(srv, "trace_path", args);
inner = extract_text_content(response);
ASSERT_NOT_NULL(inner);
ASSERT_NOT_NULL(strstr(inner, "\"self_recursive\":true"));
ASSERT_NOT_NULL(strstr(inner, "\"recursive\":true"));
ASSERT_NOT_NULL(strstr(inner, "\"self_call_policy\":\"omitted\""));
ASSERT_NULL(strstr(inner, "cursor_params_mismatch"));
ASSERT_NULL(strstr(inner, long_qn));
doc = yyjson_read(inner, strlen(inner), 0);
@@ -8619,6 +8820,9 @@ TEST(tool_trace_cursor_survives_output_budget_increase) {
response = cbm_mcp_handle_tool(srv, "trace_path", args);
inner = extract_text_content(response);
ASSERT_NOT_NULL(inner);
ASSERT_NOT_NULL(strstr(inner, "\"self_recursive\":true"));
ASSERT_NOT_NULL(strstr(inner, "\"recursive\":true"));
ASSERT_NOT_NULL(strstr(inner, "\"self_call_policy\":\"omitted\""));
ASSERT_NULL(strstr(inner, "cursor_params_mismatch"));
ASSERT_NULL(strstr(inner, "short_row"));
doc = yyjson_read(inner, strlen(inner), 0);
@@ -24436,6 +24640,161 @@ TEST(autoindex_limit_guards_git_root_issue713) {
PASS();
}
/* A servable project row is a committed index. user_version stays 0, so a
* later run cannot take the incremental no-op and skip recording workers. */
static bool mcp_publish_committed_index(const char *root) {
char *project = root ? cbm_project_name_from_path(root) : NULL;
cbm_store_t *store = project ? cbm_store_open(project) : NULL;
bool published = store && cbm_store_upsert_project(store, project, root) == CBM_STORE_OK;
cbm_store_close(store);
free(project);
return published;
}
#ifdef CBM_ENABLE_TEST_SEAMS
typedef struct {
const char *root;
bool published;
} mcp_committed_seed_t;
/* maybe_auto_index returns before the thread when <project>.db already
* exists. Publish the committed index from the count hook, which runs after
* that check and before autoindex_thread chooses a width. */
static void mcp_autoindex_publish_committed(void *context) {
mcp_committed_seed_t *seed = context;
if (seed) {
seed->published = mcp_publish_committed_index(seed->root);
}
}
#endif
/* First index, including one started automatically, uses every core. Headroom
* applies only once a servable project database already exists.
* automatic: in-process auto-index (initialize → autoindex_thread).
* Otherwise index_repository with _cbm_background, the daemon/watcher path. */
static int mcp_assert_inprocess_worker_policy(bool committed, bool automatic) {
char cache[256];
char repo[512];
(void)snprintf(cache, sizeof(cache), "%s/cbm-worker-policy-%d%d-XXXXXX", cbm_tmpdir(),
committed ? 1 : 0, automatic ? 1 : 0);
bool cache_ready = cbm_mkdtemp(cache) != NULL;
(void)snprintf(repo, sizeof(repo), "%s/repo", cache);
char source[640];
(void)snprintf(source, sizeof(source), "%s/main.py", repo);
bool repo_ready = cache_ready && th_mkdir_p(repo) == 0 &&
th_write_file(source, "def background_index():\n return True\n") == 0;
mcp_test_env_backup_t environment[] = {
{.name = "CBM_CACHE_DIR"},
{.name = "CBM_WORKERS"},
{.name = "CBM_INDEX_SINGLE_THREAD"},
};
bool environment_saved = true;
for (size_t i = 0; i < sizeof(environment) / sizeof(environment[0]); i++) {
const char *value = getenv(environment[i].name);
environment[i].present = value != NULL;
environment[i].value = value ? strdup(value) : NULL;
environment_saved = environment_saved && (!value || environment[i].value);
}
bool environment_ready = environment_saved && cbm_setenv("CBM_CACHE_DIR", cache, 1) == 0 &&
cbm_unsetenv("CBM_WORKERS") == 0 &&
cbm_unsetenv("CBM_INDEX_SINGLE_THREAD") == 0;
bool seeded = !committed;
char old_cwd[CBM_SZ_4K] = {0};
bool cwd_ready = true;
cbm_config_t *config = NULL;
bool config_ready = true;
cbm_mcp_server_t *server = NULL;
char *response = NULL;
if (automatic) {
cwd_ready = repo_ready && environment_ready && cbm_getcwd(old_cwd, sizeof(old_cwd)) &&
cbm_chdir(repo) == 0;
config = cwd_ready ? cbm_config_open(cache) : NULL;
config_ready = config && cbm_config_set(config, CBM_CONFIG_AUTO_INDEX, "true") == 0 &&
cbm_config_set(config, CBM_CONFIG_AUTO_WATCH, "false") == 0;
cbm_pipeline_worker_count_test_reset();
server = config_ready ? cbm_mcp_server_new(NULL) : NULL;
if (server) {
cbm_mcp_server_set_config(server, config);
#ifdef CBM_ENABLE_TEST_SEAMS
mcp_committed_seed_t seed = {.root = repo, .published = !committed};
if (committed) {
cbm_mcp_server_set_auto_index_count_test_hook(
server, mcp_autoindex_publish_committed, &seed);
}
#endif
response = cbm_mcp_server_handle(
server, "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{}}");
#ifdef CBM_ENABLE_TEST_SEAMS
seeded = seed.published;
#endif
cbm_mcp_server_free(server); /* joins the automatic index thread */
}
} else {
if (committed && repo_ready && environment_ready) {
seeded = mcp_publish_committed_index(repo);
}
cbm_pipeline_worker_count_test_reset();
server = repo_ready && environment_ready && seeded ? cbm_mcp_server_new(NULL) : NULL;
if (server) {
char repo_json[512];
char args[CBM_SZ_1K];
(void)snprintf(repo_json, sizeof(repo_json), "%s", repo);
cbm_normalize_path_sep(repo_json);
(void)snprintf(args, sizeof(args), "{\"_cbm_background\":true,\"repo_path\":\"%s\"}",
repo_json);
response = cbm_mcp_handle_tool(server, "index_repository", args);
cbm_mcp_server_free(server);
}
}
int selected_workers = cbm_pipeline_worker_count_test_last();
bool server_ready = server != NULL;
bool response_ready = response != NULL;
free(response);
cbm_config_close(config);
if (automatic && cwd_ready) {
(void)cbm_chdir(old_cwd);
}
/* Capture while CBM_WORKERS is still unset. Restoring first makes the
* expected count follow the lane override instead of this policy. */
int expected = committed ? cbm_pipeline_background_worker_width(cbm_default_worker_count(true),
cbm_default_worker_count(false))
: cbm_default_worker_count(true);
mcp_test_restore_env(environment, sizeof(environment) / sizeof(environment[0]));
bool cleaned = !cache_ready || th_rmtree(cache) == 0;
ASSERT_TRUE(cache_ready);
ASSERT_TRUE(repo_ready);
ASSERT_TRUE(environment_saved);
ASSERT_TRUE(environment_ready);
ASSERT_TRUE(cwd_ready);
ASSERT_TRUE(config_ready);
ASSERT_TRUE(seeded);
ASSERT_TRUE(server_ready);
ASSERT_TRUE(response_ready);
ASSERT_EQ(selected_workers, expected);
ASSERT_TRUE(cleaned);
PASS();
}
TEST(mcp_auto_index_in_process_fresh_project_uses_full_width) {
return mcp_assert_inprocess_worker_policy(false, true);
}
TEST(mcp_auto_index_in_process_existing_index_keeps_headroom) {
return mcp_assert_inprocess_worker_policy(true, true);
}
TEST(mcp_background_flag_fresh_project_uses_full_width) {
return mcp_assert_inprocess_worker_policy(false, false);
}
TEST(mcp_background_flag_existing_index_keeps_headroom) {
return mcp_assert_inprocess_worker_policy(true, false);
}
/* ══════════════════════════════════════════════════════════════════
* #853 — auto_watch=false must ALSO gate the SUPERVISED fresh-index
* watcher registration (keystone × #849 merge interaction)
@@ -26542,6 +26901,9 @@ SUITE(mcp) {
RUN_TEST(tool_index_status_includes_git_metadata);
/* Tool handlers with validation */
RUN_TEST(tool_trace_path_recursion_metadata_issue2816);
RUN_TEST(tool_trace_path_recursion_union_and_edges_issue2816);
RUN_TEST(tool_trace_path_recursion_survives_budget_floor_issue2816);
RUN_TEST(tool_trace_call_path_not_found);
RUN_TEST(tool_call_invalid_project_name_leaves_no_corrupt_litter_issue1425);
RUN_TEST(tool_trace_missing_function_name);
@@ -26787,6 +27149,10 @@ SUITE(mcp) {
RUN_TEST(autoindex_limit_guards_non_git_root_issue713);
RUN_TEST(autoindex_limit_admits_non_git_root_under_limit_issue713);
RUN_TEST(autoindex_limit_guards_git_root_issue713);
RUN_TEST(mcp_auto_index_in_process_fresh_project_uses_full_width);
RUN_TEST(mcp_auto_index_in_process_existing_index_keeps_headroom);
RUN_TEST(mcp_background_flag_fresh_project_uses_full_width);
RUN_TEST(mcp_background_flag_existing_index_keeps_headroom);
RUN_TEST(search_code_context_window_is_bounded);
RUN_TEST(search_code_context_extreme_values_answer_normally);
RUN_TEST(tool_query_graph_max_rows_above_ceiling_is_capped);
+307
View File
@@ -3026,6 +3026,311 @@ TEST(parallel_csharp_same_name_class_binds_by_namespace) {
PASS();
}
/* #2120 residual, reported on a real repo: step 8b binds only namespace-
* qualified TOP-LEVEL types. A qualified `A.B` whose qualifier A is a TYPE
* (nested-type constructor `new Command.Result(...)`, chained-constructor
* receiver `new Grain.Handler(dep).Handle(...)`) and a top-level type with NO
* namespace declaration (global namespace, e.g. a `[Inject]`-consumed service)
* both fell through to the short-name fallback, where the prefix score against
* a path-derived QN is 0 for every variant — registration order picked the
* target. Decoy files are registered FIRST so the old registry-order behavior
* binds every check to the decoy and the test fails. */
TEST(parallel_csharp_nested_type_and_global_namespace_binding) {
static const char decoy_src[] =
"namespace Acme.Layouts.Cqrs;\n"
"public class GrainCommand {\n"
" public class Handler {\n"
" public int Handle() => 9;\n"
" }\n"
" public class Result {\n"
" public Result(bool ok) { }\n"
" public int Code() => 9;\n"
" }\n"
"}\n";
static const char partner_src[] =
"namespace Acme.Partner.Cqrs;\n"
"public class GrainCommand {\n"
" public class Handler {\n"
" private readonly int _v;\n"
" public Handler(int v) { _v = v; }\n"
" public int Handle() => _v;\n"
" }\n"
" public class Result {\n"
" public Result(bool ok) { }\n"
" public int Code() => 1;\n"
" }\n"
"}\n";
static const char backoffice_src[] =
"namespace Acme.BackOffice.Cqrs;\n"
"public class GrainCommand {\n"
" public class Result {\n"
" public Result(bool ok) { }\n"
" public int Code() => 2;\n"
" }\n"
"}\n";
static const char nested_user_src[] =
"using Acme.Partner.Cqrs;\n"
"namespace Acme.App;\n"
"public class NestedUser {\n"
" public int Run() {\n"
" var r = new GrainCommand.Result(true);\n"
" return r.Code();\n"
" }\n"
"}\n";
static const char chained_user_src[] =
"using Acme.Partner.Cqrs;\n"
"namespace Acme.App;\n"
"public class ChainedUser {\n"
" public int Run() {\n"
" return new GrainCommand.Handler(3).Handle();\n"
" }\n"
"}\n";
static const char other_update_src[] =
"namespace Acme.Other.Web.Services;\n"
"public class UpdateService {\n"
" public string Check() => \"other\";\n"
"}\n";
/* No namespace declaration: the global namespace. */
static const char global_update_src[] =
"public class UpdateService {\n"
" public string Check() => \"global\";\n"
"}\n";
static const char boot_src[] =
"namespace Acme.App;\n"
"public class Boot {\n"
" public string Boot1() {\n"
" var u = new UpdateService();\n"
" return u.Check();\n"
" }\n"
"}\n";
/* The lookup-order probes: the SAME simple name is importable from
* Acme.Other.Web.Services AND declared in the global namespace. C# looks
* a simple name up namespace by namespace, innermost outward; at each one
* it checks that namespace's members, then the usings of the namespace
* declaration enclosing the reference. The global namespace comes last,
* and its "declaration" is the compilation unit, so:
* - a compilation-unit `using` (ImportUser) is consulted only AFTER the
* global namespace's members: the global UpdateService wins;
* - a `using` inside the namespace body (NsUsingUser) is consulted
* before any outer namespace: the imported UpdateService wins. */
static const char import_user_src[] =
"using Acme.Other.Web.Services;\n"
"namespace Acme.App;\n"
"public class ImportUser {\n"
" public string Run2() {\n"
" var u = new UpdateService();\n"
" return u.Check();\n"
" }\n"
"}\n";
static const char ns_using_user_src[] =
"namespace Acme.App {\n"
" using Acme.Other.Web.Services;\n"
" public class NsUsingUser {\n"
" public string Run3() {\n"
" var u = new UpdateService();\n"
" return u.Check();\n"
" }\n"
" }\n"
"}\n";
const char *project = "cbm_cs_nested";
static const char *rels[] = {
"src/Decoy/Layouts.cs",
"src/Decoy/OtherUpdate.cs",
"src/Partner/Cqrs/GrainCommand.cs",
"src/BackOffice/Cqrs/GrainCommand.cs",
"src/Globals/UpdateService.cs",
"src/App/NestedUser.cs",
"src/App/ChainedUser.cs",
"src/App/Boot.cs",
"src/App/ImportUser.cs",
"src/App/NsUsingUser.cs",
};
const char *srcs[] = {decoy_src, other_update_src, partner_src,
backoffice_src, global_update_src, nested_user_src,
chained_user_src, boot_src, import_user_src,
ns_using_user_src};
enum { N_FILES = 10, N_CHECKS = 12 };
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_par_cs_nested_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 C# nested-type 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 *pf_code = "cbm_cs_nested.src.Partner.Cqrs.GrainCommand.GrainCommand."
"Result.Code";
const char *pf_handle = "cbm_cs_nested.src.Partner.Cqrs.GrainCommand.GrainCommand."
"Handler.Handle";
const char *bo_code = "cbm_cs_nested.src.BackOffice.Cqrs.GrainCommand.GrainCommand."
"Result.Code";
const char *decoy_code = "cbm_cs_nested.src.Decoy.Layouts.GrainCommand.GrainCommand."
"Result.Code";
const char *decoy_handle = "cbm_cs_nested.src.Decoy.Layouts.GrainCommand.GrainCommand."
"Handler.Handle";
const char *global_check = "cbm_cs_nested.src.Globals.UpdateService.UpdateService.Check";
const char *other_check = "cbm_cs_nested.src.Decoy.OtherUpdate.UpdateService.Check";
bool ok[2][N_CHECKS];
bool all = true;
for (int g = 0; g < 2; g++) {
cbm_gbuf_t *gb = graphs[g];
ok[g][0] = has_edge_from_callable_to_qn(gb, "NestedUser.Run", pf_code, "CALLS");
ok[g][1] = !has_edge_from_callable_to_qn(gb, "NestedUser.Run", bo_code, "CALLS") &&
!has_edge_from_callable_to_qn(gb, "NestedUser.Run", decoy_code, "CALLS");
/* The ctor EDGE itself joins through the pipeline's textual carrier and
* is out of scope here (see PR notes); what must hold is that the
* nested type RESOLUTION is right, proven by the Code()/Handle() edges
* above, and that no ctor edge binds to a variant the caller cannot
* see: neither the BackOffice nor the registry-first decoy. */
ok[g][2] = !has_edge_from_callable_to_qn(
gb, "NestedUser.Run",
"cbm_cs_nested.src.BackOffice.Cqrs.GrainCommand.GrainCommand.Result",
"CALLS") &&
!has_edge_from_callable_to_qn(
gb, "NestedUser.Run",
"cbm_cs_nested.src.BackOffice.Cqrs.GrainCommand.GrainCommand."
"Result.Result",
"CALLS");
ok[g][3] = !has_edge_from_callable_to_qn(gb, "NestedUser.Run",
"cbm_cs_nested.src.Decoy.Layouts.GrainCommand."
"GrainCommand.Result.Result",
"CALLS");
ok[g][4] = has_edge_from_callable_to_qn(gb, "ChainedUser.Run", pf_handle, "CALLS");
ok[g][5] = !has_edge_from_callable_to_qn(gb, "ChainedUser.Run", decoy_handle, "CALLS");
ok[g][6] = has_edge_from_callable_to_qn(gb, "Boot.Boot1", global_check, "CALLS");
ok[g][7] = !has_edge_from_callable_to_qn(gb, "Boot.Boot1", other_check, "CALLS");
ok[g][8] = has_edge_from_callable_to_qn(gb, "ImportUser.Run2", global_check, "CALLS");
ok[g][9] = !has_edge_from_callable_to_qn(gb, "ImportUser.Run2", other_check, "CALLS");
ok[g][10] = has_edge_from_callable_to_qn(gb, "NsUsingUser.Run3", other_check, "CALLS");
ok[g][11] = !has_edge_from_callable_to_qn(gb, "NsUsingUser.Run3", global_check, "CALLS");
for (int k = 0; k < N_CHECKS; k++) {
/* The ctor-edge checks 2/3 gate the parallel path only. In the
* sequential path the `new GrainCommand.Result(true)` edge is
* still drawn by the textual ctor carrier, not by the resolved
* type (the out-of-scope carrier join above); on current main it
* lands on the BackOffice Result already with the nested-type
* resolution alone. The diagnostic below still prints both. */
if (g == 0 && (k == 2 || k == 3))
continue;
all = all && ok[g][k];
}
}
if (!all) {
for (int g = 0; g < 2; g++) {
printf(" C# nested/global diagnostic %s: nested_code=%d/%d nested_ctor=%d/%d "
"chained=%d/%d global_ns=%d/%d cu_using=%d/%d ns_using=%d/%d\n",
g == 0 ? "sequential" : "parallel", ok[g][0], ok[g][1], ok[g][2], ok[g][3],
ok[g][4], ok[g][5], ok[g][6], ok[g][7], ok[g][8], ok[g][9], ok[g][10],
ok[g][11]);
}
}
cbm_gbuf_free(graphs[0]);
cbm_gbuf_free(graphs[1]);
th_rmtree(tmpdir);
ASSERT_TRUE(all);
PASS();
}
/* A type extracted with no namespace is global only if its file parsed
* cleanly. The vendored grammar does not parse a `ref partial struct` with
* members (reference assemblies are full of them), and error recovery
* dissolves the namespace around it, so the static class after it surfaces
* with no namespace. Read as global, it outranked the compilation-unit
* `using` and took every `Gadget.Spin()` call from the real, visible type;
* on dotnet/runtime that moved 5,313 CALLS from implementation files into
* reference assemblies. The damaged file is registered FIRST so neither the
* old registry order nor the global rank can explain a pass. */
TEST(parallel_csharp_parse_damaged_namespace_is_not_global) {
static const char damaged_src[] = "namespace Acme.Lib\n"
"{\n"
" public ref partial struct Handler\n"
" {\n"
" public void Append(string value) { }\n"
" }\n"
" public static partial class Gadget {\n"
" public static int Spin() => 9;\n"
" }\n"
"}\n";
static const char real_src[] = "namespace Acme.Lib;\n"
"public static class Gadget {\n"
" public static int Spin() => 1;\n"
"}\n";
static const char user_src[] = "using Acme.Lib;\n"
"namespace Acme.App;\n"
"public class GadgetUser {\n"
" public int Run() {\n"
" return Gadget.Spin();\n"
" }\n"
"}\n";
const char *project = "cbm_cs_damaged";
static const char *rels[] = {"src/Ref/Lib.cs", "src/Lib/Gadget.cs", "src/App/GadgetUser.cs"};
const char *srcs[] = {damaged_src, real_src, user_src};
enum { N_FILES = 3 };
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_par_cs_damaged_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 C# damaged-namespace 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 *real_spin = "cbm_cs_damaged.src.Lib.Gadget.Gadget.Spin";
const char *damaged_spin = "cbm_cs_damaged.src.Ref.Lib.Gadget.Spin";
bool ok[2][2];
for (int g = 0; g < 2; g++) {
ok[g][0] = has_edge_from_callable_to_qn(graphs[g], "GadgetUser.Run", real_spin, "CALLS");
ok[g][1] = !has_edge_from_callable_to_qn(graphs[g], "GadgetUser.Run", damaged_spin,
"CALLS");
}
bool all = ok[0][0] && ok[0][1] && ok[1][0] && ok[1][1];
if (!all) {
printf(" C# damaged-namespace diagnostic: sequential real=%d no_damaged=%d "
"parallel real=%d no_damaged=%d\n",
ok[0][0], ok[0][1], ok[1][0], ok[1][1]);
}
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;
@@ -5694,6 +5999,8 @@ SUITE(parallel) {
RUN_TEST(parallel_typescript_import_namespace_exact_parity);
RUN_TEST(parallel_tsx_import_namespace_exact_parity);
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_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);
+690 -3
View File
@@ -13,6 +13,7 @@
#include "lsp/c_lsp.h"
#include "lsp/py_lsp.h"
#include "pipeline/lsp_surface.h"
#include "pipeline/worker_pool.h"
#include "pipeline/pass_lsp_cross.h"
#include "pipeline/pipeline_internal.h"
#include "pipeline/helm_template.h"
@@ -122,6 +123,269 @@ TEST(pipeline_create_free) {
PASS();
}
TEST(pipeline_background_worker_policy_preserves_overrides) {
const char *old_workers = getenv("CBM_WORKERS");
char *saved_workers = old_workers ? strdup(old_workers) : NULL;
const char *old_single = getenv("CBM_INDEX_SINGLE_THREAD");
char *saved_single = old_single ? strdup(old_single) : NULL;
cbm_unsetenv("CBM_WORKERS");
cbm_unsetenv("CBM_INDEX_SINGLE_THREAD");
cbm_pipeline_t *foreground = cbm_pipeline_new("/some/path", NULL, CBM_MODE_FULL);
cbm_pipeline_t *background = cbm_pipeline_new("/some/path", NULL, CBM_MODE_FULL);
ASSERT_NOT_NULL(foreground);
ASSERT_NOT_NULL(background);
cbm_pipeline_set_background(background, true);
int foreground_default = cbm_pipeline_worker_count(foreground);
int background_default = cbm_pipeline_worker_count(background);
bool defaults_match = foreground_default == cbm_default_worker_count(true) &&
background_default == cbm_pipeline_background_worker_width(
cbm_default_worker_count(true),
cbm_default_worker_count(false)) &&
foreground_default >= 1 && background_default >= 1 &&
(foreground_default < 2 || background_default >= 2);
/* A pinned single worker leaves no parallel route to preserve. */
cbm_setenv("CBM_WORKERS", "1", 1);
bool env_override =
cbm_pipeline_worker_count(foreground) == 1 && cbm_pipeline_worker_count(background) == 1;
cbm_setenv("CBM_WORKERS", "3", 1);
env_override = env_override && cbm_pipeline_worker_count(foreground) == 3 &&
cbm_pipeline_worker_count(background) == 3;
cbm_setenv("CBM_INDEX_SINGLE_THREAD", "1", 1);
bool recovery_override = cbm_pipeline_worker_count(foreground) == 1 &&
cbm_pipeline_worker_count(background) == 1;
cbm_pipeline_free(foreground);
cbm_pipeline_free(background);
saved_workers ? cbm_setenv("CBM_WORKERS", saved_workers, 1) : cbm_unsetenv("CBM_WORKERS");
saved_single ? cbm_setenv("CBM_INDEX_SINGLE_THREAD", saved_single, 1)
: cbm_unsetenv("CBM_INDEX_SINGLE_THREAD");
free(saved_workers);
free(saved_single);
ASSERT_TRUE(defaults_match);
ASSERT_TRUE(env_override);
ASSERT_TRUE(recovery_override);
PASS();
}
/* An automatic refresh keeps CPU headroom but never falls to one worker, the
* sequential route, while the foreground width could run in parallel. Each
* host shape is given as the two widths cbm_default_worker_count() derives
* from it (foreground: every core; headroom: performance cores minus one, at
* least one), so the verdict does not depend on the machine running it. */
TEST(pipeline_background_worker_width_keeps_parallel_route) {
/* One core: foreground 1, headroom 1. Nothing to preserve. */
ASSERT_EQ(cbm_pipeline_background_worker_width(1, 1), 1);
/* Two performance cores: headroom alone would be 1. */
ASSERT_EQ(cbm_pipeline_background_worker_width(2, 1), 2);
/* Two performance cores beside efficiency cores: same floor. */
ASSERT_EQ(cbm_pipeline_background_worker_width(8, 1), 2);
/* Larger hosts keep their headroom unchanged. */
ASSERT_EQ(cbm_pipeline_background_worker_width(3, 2), 2);
ASSERT_EQ(cbm_pipeline_background_worker_width(4, 3), 3);
ASSERT_EQ(cbm_pipeline_background_worker_width(18, 5), 5);
PASS();
}
#if defined(CBM_ENABLE_TEST_SEAMS) && CBM_ENABLE_TEST_SEAMS
enum { SURFACE_BUDGET_FILES = 5 };
typedef struct {
atomic_int expected;
atomic_int arrivals;
atomic_int live;
atomic_int peak;
atomic_bool released;
atomic_int visits[SURFACE_BUDGET_FILES];
int helpers;
} surface_budget_probe_t;
static void surface_budget_spawned(int count, int helpers, void *arg) {
surface_budget_probe_t *probe = arg;
probe->helpers = helpers;
int participants = helpers + 1;
atomic_store(&probe->expected, count < participants ? count : participants);
}
static void surface_budget_row(int index, bool entering, void *arg) {
surface_budget_probe_t *probe = arg;
if (!entering) {
atomic_fetch_sub(&probe->live, 1);
return;
}
atomic_fetch_add(&probe->visits[index], 1);
int live = atomic_fetch_add(&probe->live, 1) + 1;
int peak = atomic_load(&probe->peak);
while (peak < live && !atomic_compare_exchange_weak(&probe->peak, &peak, live)) {}
atomic_fetch_add(&probe->arrivals, 1);
/* No timing window: hold the first wave until every possible participant
* has entered. The post-spawn observation accounts for partial thread
* creation and serial fallback, so neither can strand this gate. The test
* runner's watchdog bounds a broken dispatch without making time a verdict. */
while (!atomic_load(&probe->released)) {
int expected = atomic_load(&probe->expected);
if (expected > 0 && atomic_load(&probe->arrivals) >= expected) {
atomic_store(&probe->released, true);
}
}
}
TEST(pipeline_surface_respects_total_worker_budget) {
CBMFileResult results[SURFACE_BUDGET_FILES] = {0};
CBMFileResult *cache[SURFACE_BUDGET_FILES];
cbm_file_info_t files[SURFACE_BUDGET_FILES] = {
{.rel_path = "a.go", .language = CBM_LANG_GO},
{.rel_path = "b.go", .language = CBM_LANG_GO},
{.rel_path = "c.go", .language = CBM_LANG_GO},
{.rel_path = "d.go", .language = CBM_LANG_GO},
{.rel_path = "e.go", .language = CBM_LANG_GO},
};
surface_budget_probe_t probe = {0};
for (int i = 0; i < SURFACE_BUDGET_FILES; i++) {
cache[i] = &results[i];
atomic_init(&probe.visits[i], 0);
}
cache[1] = NULL; /* The skipped row must not change stable file ordering. */
atomic_init(&probe.expected, 0);
atomic_init(&probe.arrivals, 0);
atomic_init(&probe.live, 0);
atomic_init(&probe.peak, 0);
atomic_init(&probe.released, false);
const char *old_workers = getenv("CBM_WORKERS");
char *saved_workers = old_workers ? strdup(old_workers) : NULL;
const char *old_single = getenv("CBM_INDEX_SINGLE_THREAD");
char *saved_single = old_single ? strdup(old_single) : NULL;
cbm_setenv("CBM_WORKERS", "2", 1);
cbm_unsetenv("CBM_INDEX_SINGLE_THREAD");
cbm_pipeline_t *pipeline = cbm_pipeline_new("/some/path", NULL, CBM_MODE_FULL);
cbm_pipeline_set_background(pipeline, true);
cbm_pipeline_ctx_t ctx = {.pipeline = pipeline};
cbm_lsp_surface_row_t *rows = NULL;
int count = 0;
cbm_parallel_test_set_spawn_observer(surface_budget_spawned, &probe);
cbm_lsp_surface_test_set_row_observer(surface_budget_row, &probe);
int rc = cbm_lsp_surface_build_rows(&ctx, "budget", cache, files, SURFACE_BUDGET_FILES, NULL,
NULL, &rows, &count);
int parallel_peak = atomic_load(&probe.peak);
int parallel_helpers = probe.helpers;
atomic_store(&probe.expected, 0);
atomic_store(&probe.arrivals, 0);
atomic_store(&probe.peak, 0);
atomic_store(&probe.released, false);
cbm_setenv("CBM_INDEX_SINGLE_THREAD", "1", 1);
cbm_lsp_surface_row_t *serial_rows = NULL;
int serial_count = 0;
int serial_rc = cbm_lsp_surface_build_rows(&ctx, "budget", cache, files, SURFACE_BUDGET_FILES,
NULL, NULL, &serial_rows, &serial_count);
cbm_lsp_surface_test_set_row_observer(NULL, NULL);
cbm_parallel_test_set_spawn_observer(NULL, NULL);
bool same = rc == 0 && serial_rc == 0 && rows && serial_rows &&
count == SURFACE_BUDGET_FILES - 1 && count == serial_count;
for (int i = 0; same && i < count; i++) {
same = strcmp(rows[i].rel_path, files[i < 1 ? i : i + 1].rel_path) == 0 &&
strcmp(rows[i].defs_json, serial_rows[i].defs_json) == 0 &&
strcmp(rows[i].surface_sha, serial_rows[i].surface_sha) == 0;
}
cbm_store_free_lsp_surfaces(rows, count);
cbm_store_free_lsp_surfaces(serial_rows, serial_count);
cbm_pipeline_free(pipeline);
saved_workers ? cbm_setenv("CBM_WORKERS", saved_workers, 1) : cbm_unsetenv("CBM_WORKERS");
saved_single ? cbm_setenv("CBM_INDEX_SINGLE_THREAD", saved_single, 1)
: cbm_unsetenv("CBM_INDEX_SINGLE_THREAD");
free(saved_workers);
free(saved_single);
ASSERT_EQ(rc, 0);
ASSERT_EQ(serial_rc, 0);
ASSERT_TRUE(same);
ASSERT_GTE(parallel_helpers, 1); /* A serial setup is not a concurrency repro. */
ASSERT_EQ(probe.helpers, 0);
ASSERT_EQ(atomic_load(&probe.live), 0);
for (int i = 0; i < SURFACE_BUDGET_FILES; i++) {
ASSERT_EQ(atomic_load(&probe.visits[i]), 2);
}
ASSERT_EQ(atomic_load(&probe.peak), 1);
ASSERT_EQ(parallel_peak, 2);
PASS();
}
#endif
TEST(pipeline_background_policy_preserves_index_results) {
ASSERT_EQ(setup_test_repo(), 0);
/* 64 fillers plus setup_test_repo's three files exceed MIN_FILES_FOR_PARALLEL
* (50). Below that threshold both policies take the sequential path, so the
* comparison stays green even with the background policy removed. */
for (int i = 0; i < 64; i++) {
char name[32];
char body[96];
(void)snprintf(name, sizeof(name), "pad%02d.go", i);
(void)snprintf(body, sizeof(body), "package main\n\nfunc Pad%02d() int {\n\treturn %d\n}\n",
i, i);
if (th_write_file(TH_PATH(g_tmpdir, name), body) != 0) {
teardown_test_repo();
FAIL("failed to pad fixture past MIN_FILES_FOR_PARALLEL");
}
}
/* An inherited CBM_WORKERS pin makes both runs use the same count. Drop it
* so this compares an all-core index against a headroom index. */
const char *old_workers = getenv("CBM_WORKERS");
char *saved_workers = old_workers ? strdup(old_workers) : NULL;
const char *old_single = getenv("CBM_INDEX_SINGLE_THREAD");
char *saved_single = old_single ? strdup(old_single) : NULL;
if ((old_workers && !saved_workers) || (old_single && !saved_single)) {
free(saved_workers);
free(saved_single);
teardown_test_repo();
FAIL("failed to save worker env");
}
cbm_unsetenv("CBM_WORKERS");
cbm_unsetenv("CBM_INDEX_SINGLE_THREAD");
char foreground_db[512];
char background_db[512];
(void)snprintf(foreground_db, sizeof(foreground_db), "%s/foreground.db", g_tmpdir);
(void)snprintf(background_db, sizeof(background_db), "%s/background.db", g_tmpdir);
cbm_pipeline_t *foreground = cbm_pipeline_new(g_tmpdir, foreground_db, CBM_MODE_FULL);
cbm_pipeline_t *background = cbm_pipeline_new(g_tmpdir, background_db, CBM_MODE_FULL);
int foreground_rc = -1;
int background_rc = -1;
int foreground_nodes = -1;
int foreground_edges = -1;
int background_nodes = -1;
int background_edges = -1;
bool created = foreground && background;
if (created) {
cbm_pipeline_set_background(background, true);
foreground_rc = cbm_pipeline_run(foreground);
background_rc = cbm_pipeline_run(background);
cbm_pipeline_get_committed_counts(foreground, &foreground_nodes, &foreground_edges);
cbm_pipeline_get_committed_counts(background, &background_nodes, &background_edges);
}
cbm_pipeline_free(foreground);
cbm_pipeline_free(background);
teardown_test_repo();
saved_workers ? cbm_setenv("CBM_WORKERS", saved_workers, 1) : cbm_unsetenv("CBM_WORKERS");
saved_single ? cbm_setenv("CBM_INDEX_SINGLE_THREAD", saved_single, 1)
: cbm_unsetenv("CBM_INDEX_SINGLE_THREAD");
free(saved_workers);
free(saved_single);
ASSERT_TRUE(created);
ASSERT_EQ(foreground_rc, 0);
ASSERT_EQ(background_rc, 0);
ASSERT_GT(foreground_nodes, 0);
ASSERT_EQ(background_nodes, foreground_nodes);
ASSERT_EQ(background_edges, foreground_edges);
PASS();
}
TEST(pipeline_null_repo) {
cbm_pipeline_t *p = cbm_pipeline_new(NULL, NULL, CBM_MODE_FULL);
ASSERT_NULL(p);
@@ -9344,8 +9608,8 @@ TEST(pipeline_semantic_manifest_rejects_non_directory_root) {
cbm_file_hash_t *manifest = NULL;
int manifest_count = -1;
int rc = cbm_pipeline_build_semantic_manifest("manifest-fail-closed", root_path, NULL, 0, NULL,
0, NULL, NULL, &manifest, &manifest_count);
int rc = cbm_pipeline_build_semantic_manifest(NULL, "manifest-fail-closed", root_path, NULL, 0,
NULL, 0, NULL, NULL, &manifest, &manifest_count);
cbm_pipeline_free_semantic_manifest(manifest, manifest_count > 0 ? manifest_count : 0);
th_rmtree(tmp);
@@ -9373,7 +9637,7 @@ TEST(pipeline_semantic_manifest_skips_unreadable_subdirectory) {
cbm_file_hash_t *manifest = NULL;
int manifest_count = -1;
int rc = cbm_pipeline_build_semantic_manifest("manifest-skip-locked", tmp, NULL, 0, NULL, 0,
int rc = cbm_pipeline_build_semantic_manifest(NULL, "manifest-skip-locked", tmp, NULL, 0, NULL, 0,
NULL, NULL, &manifest, &manifest_count);
bool found_sibling_gitignore = false;
@@ -11666,6 +11930,221 @@ TEST(pipeline_go_bare_init_binds_nothing_parallel) {
PASS();
}
TEST(pipeline_go_parallel_field_hint_requires_owner_segment) {
/* #1927: try_field_type_hint (parallel resolver only) matched the hinted
* type name as a raw SUBSTRING of the candidate QN. A single-letter
* receiver — `f.Close()` on an *os.File — hints "F", and "F" is a
* substring of "…FileStore.Close", so a stdlib call was rebound to an
* arbitrary project method at PP_FIELD_HINT_CONF and presented as
* field_type_hint (the one weak strategy the Go selector guard trusts).
* The hint must fire only when the candidate's OWNING segment — the
* dot-segment immediately before the method — EQUALS the hinted type;
* otherwise the resolution stays a weak short-name match and the Go
* guard (#1906) drops it. >= 50 files forces pass_parallel.c, the only
* path that runs try_field_type_hint(). */
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_go_fth_XXXXXX");
if (!cbm_mkdtemp(tmp)) {
FAIL("tmpdir");
}
write_temp_file(tmp, "go.mod", "module example.com/fxhint\n\ngo 1.22\n");
/* Two project methods named Close → the registry resolves f.Close() as an
* ambiguous short-name match (candidate_count 2), the shape the hint
* upgrades. FileStore's QN carries the capital F the buggy substring
* latches onto; Conn is the second candidate. */
write_temp_file(tmp, "filestore/filestore.go",
"package filestore\n"
"\n"
"type FileStore struct{ open bool }\n"
"\n"
"func (fs *FileStore) Close() {\n"
"\tfs.open = false\n"
"}\n");
write_temp_file(tmp, "conn/conn.go",
"package conn\n"
"\n"
"type Conn struct{ live bool }\n"
"\n"
"func (cn *Conn) Close() {\n"
"\tcn.live = false\n"
"}\n");
/* Two project methods named Find → same ambiguity, but here the receiver
* variable IS named after its type (`finder`), the case the hint exists
* for: the owning segment of Finder.Find equals the hinted "Finder". */
write_temp_file(tmp, "finder/finder.go",
"package finder\n"
"\n"
"type Finder struct{ n int }\n"
"\n"
"func NewFinder() *Finder { return &Finder{n: 1} }\n"
"\n"
"func (fi *Finder) Find(id int) int {\n"
"\treturn fi.n + id\n"
"}\n");
write_temp_file(tmp, "locator/locator.go",
"package locator\n"
"\n"
"type Locator struct{ n int }\n"
"\n"
"func (lo *Locator) Find(id int) int {\n"
"\treturn lo.n - id\n"
"}\n");
write_temp_file(tmp, "app/app.go",
"package app\n"
"\n"
"import (\n"
"\t\"os\"\n"
"\n"
"\txf \"example.com/fxhint/finder\"\n"
")\n"
"\n"
"func Lookup(path string) int64 {\n"
"\tf, err := os.Open(path)\n"
"\tif err != nil {\n"
"\t\treturn 0\n"
"\t}\n"
"\tdefer f.Close()\n"
"\tst, err := f.Stat()\n"
"\tif err != nil {\n"
"\t\treturn 0\n"
"\t}\n"
"\treturn st.Size()\n"
"}\n"
"\n"
"func UseFinder(id int) int {\n"
"\tfinder := xf.NewFinder()\n"
"\treturn finder.Find(id)\n"
"}\n");
for (int i = 0; i < 52; i++) {
char name[64];
char body[128];
snprintf(name, sizeof(name), "pad/filler%d.go", i);
snprintf(body, sizeof(body), "package pad\n\nfunc filler%d() int { return %d }\n", i, i);
write_temp_file(tmp, name, body);
}
char db_path[512];
snprintf(db_path, sizeof(db_path), "%s/go_fth.db", tmp);
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
ASSERT_NOT_NULL(p);
ASSERT_EQ(cbm_pipeline_run(p), 0);
const char *project = cbm_pipeline_project_name(p);
cbm_store_t *s = cbm_store_open_path(db_path);
ASSERT_NOT_NULL(s);
/* (1) Reproduce-first: RED before the fix. The substring hint rebinds the
* stdlib f.Close() to FileStore.Close ("F" ⊂ QN) at high confidence and
* the Go guard keeps it. With segment equality the upgrade is refused,
* the match stays suffix_match, and the Go guard drops it. */
ASSERT_FALSE(cross_file_call_exists(s, project, "Lookup", "Close"));
/* (2) The intended hint case survives: `finder` names its type, so the
* owning segment of Finder.Find equals the hint. */
ASSERT_TRUE(cross_file_call_exists(s, project, "UseFinder", "Find"));
cbm_store_close(s);
cbm_pipeline_free(p);
th_rmtree(tmp);
PASS();
}
static int count_calls_with(cbm_store_t *s, const char *project, const char *caller_suffix,
const char *callee_suffix, const char *fragment);
TEST(pipeline_csharp_parallel_field_hint_requires_owner_segment) {
/* #1927, non-Go twin. Go no longer runs try_field_type_hint (e554a2ab),
* but every other language on the parallel path still does, and C# is the
* hint's intended consumer (it strips the _ / m_ field prefixes). C#
* method names are PascalCase, so a one-letter receiver hints a letter
* that every candidate QN already contains in the METHOD name: `w.Write()`
* hints "W", and a raw substring test accepts "...FileStore.Write" and
* "...Conn.Write" alike, whichever the registry lists first. The hint must
* bind only a candidate whose OWNING segment equals the hinted type.
* >= 50 files forces pass_parallel.c, the only path with the hint. */
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_cs_fth_XXXXXX");
if (!cbm_mkdtemp(tmp)) {
FAIL("tmpdir");
}
/* Two project methods named Write: an untyped receiver resolves as an
* ambiguous short-name match (candidate_count 2), the shape the hint
* upgrades. */
write_temp_file(tmp, "Storage/FileStore.cs",
"namespace Storage {\n"
" public class FileStore {\n"
" private int written;\n"
" public void Write(int v) { written += v; }\n"
" public void Put(int v) { written = v; }\n"
" }\n"
"}\n");
write_temp_file(tmp, "Net/Conn.cs",
"namespace Net {\n"
" public class Conn {\n"
" private int sent;\n"
" public void Write(int v) { sent += v; }\n"
" }\n"
"}\n");
/* Two project methods named Put, one owned by Store: the receiver `store`
* names its type, the case the hint exists for. FileStore's QN also
* contains "Store", so only segment equality picks Store.Put whatever
* the candidate order. */
write_temp_file(tmp, "Repo/Store.cs",
"namespace Repo {\n"
" public class Store {\n"
" private int held;\n"
" public void Put(int v) { held = v; }\n"
" }\n"
"}\n");
write_temp_file(tmp, "App/Worker.cs",
"namespace App {\n"
" public class Worker {\n"
" public void Log(System.IO.StreamWriter w) {\n"
" w.Write(1);\n"
" }\n"
" public void Keep() {\n"
" var store = Services.Resolve();\n"
" store.Put(2);\n"
" }\n"
" }\n"
"}\n");
for (int i = 0; i < 52; i++) {
char name[64];
char body[192];
snprintf(name, sizeof(name), "Pad/Filler%d.cs", i);
snprintf(body, sizeof(body),
"namespace Pad {\n public class Filler%d {\n"
" public int Get%d() { return %d; }\n }\n}\n",
i, i, i);
write_temp_file(tmp, name, body);
}
char db_path[512];
snprintf(db_path, sizeof(db_path), "%s/cs_fth.db", tmp);
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
ASSERT_NOT_NULL(p);
ASSERT_EQ(cbm_pipeline_run(p), 0);
const char *project = cbm_pipeline_project_name(p);
cbm_store_t *s = cbm_store_open_path(db_path);
ASSERT_NOT_NULL(s);
/* (1) Reproduce-first: RED before the fix. "W" is a substring of every
* Write candidate, so the old hint upgraded the StreamWriter call to an
* arbitrary project Write at PP_FIELD_HINT_CONF. No owner segment is "W",
* so the fixed hint must not fire at all. */
ASSERT_FALSE(cross_file_call_has_strategy(s, project, "Log", "Write", "field_type_hint"));
/* (2) The intended hint case binds the owner it names, never FileStore. */
ASSERT_EQ(count_calls_with(s, project, ".Keep", ".Store.Put", "field_type_hint"), 1);
ASSERT_EQ(count_calls_with(s, project, ".Keep", ".FileStore.Put", NULL), 0);
cbm_store_close(s);
cbm_pipeline_free(p);
th_rmtree(tmp);
PASS();
}
/* Fixture for the #1928 cross-language reference-guard probes (sequential and
* parallel twins). pad_files > 0 adds filler files to push the index over the
* parallel-pipeline threshold, since USAGE/WRITES/READS have one resolver per
@@ -15973,6 +16452,203 @@ TEST(pipeline_typed_http_call_has_no_arg_url_twin_sequential) {
return typed_http_no_url_twin_case(0);
}
/* Issue #2768: Angular HttpClient calls (this.http.get, this.httpClient.post,
* this.http.delete) emit HTTP_CALLS edges typed with the method verb, and are
* never misclassified as route registrations. */
static int angular_http_client_typed_case(int fillers) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_angular_http_XXXXXX");
if (!cbm_mkdtemp(tmp)) {
FAIL("tmpdir");
}
write_temp_file(tmp, "src/item.service.ts",
"import { Injectable } from '@angular/core';\n"
"import { HttpClient } from '@angular/common/http';\n"
"\n"
"@Injectable({ providedIn: 'root' })\n"
"export class ItemService {\n"
" constructor(private http: HttpClient) {}\n"
"\n"
" getItems() {\n"
" return this.http.get('/items/list');\n"
" }\n"
"\n"
" getItem(id: string) {\n"
" return this.http.get(`/items/${id}`);\n"
" }\n"
"\n"
" createItem(item: any) {\n"
" return this.http.post('/items', item);\n"
" }\n"
"\n"
" deleteItem(id: string) {\n"
" return this.http.delete(`/items/${id}`);\n"
" }\n"
"\n"
" updateItem(id: string, item: any) {\n"
" return this.http\n"
" .put(`/items/${id}`, item);\n"
" }\n"
"}\n");
for (int i = 0; i < fillers; i++) {
char name[64];
char body[128];
snprintf(name, sizeof(name), "src/filler%d.ts", i);
snprintf(body, sizeof(body), "export function filler%d(): number {\n return %d;\n}\n", i,
i);
write_temp_file(tmp, name, body);
}
char *old_workers = getenv("CBM_WORKERS");
char *saved = old_workers ? strdup(old_workers) : NULL;
cbm_setenv("CBM_WORKERS", "4", 1);
char db_path[512];
snprintf(db_path, sizeof(db_path), "%s/angular_http.db", tmp);
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
ASSERT_NOT_NULL(p);
ASSERT_EQ(cbm_pipeline_run(p), 0);
const char *project = cbm_pipeline_project_name(p);
cbm_store_t *s = cbm_store_open_path(db_path);
ASSERT_NOT_NULL(s);
/* Assert exact HTTP_CALLS with proper methods */
ASSERT_EQ(http_calls_to_route(s, project, "getItems", "__route__GET__/items/list"), 1);
ASSERT_EQ(http_calls_to_route(s, project, "getItem", "__route__GET__/items/{}"), 1);
ASSERT_EQ(http_calls_to_route(s, project, "createItem", "__route__POST__/items"), 1);
ASSERT_EQ(http_calls_to_route(s, project, "deleteItem", "__route__DELETE__/items/{}"), 1);
/* A chain wrapped before the verb (prettier style) is the same client call */
ASSERT_EQ(http_calls_to_route(s, project, "updateItem", "__route__PUT__/items/{}"), 1);
ASSERT_EQ(http_calls_to_route(s, project, "updateItem", "__route__ANY__"), 0);
/* Assert no ANY twins */
ASSERT_EQ(http_calls_to_route(s, project, "getItems", "__route__ANY__"), 0);
ASSERT_EQ(http_calls_to_route(s, project, "getItem", "__route__ANY__"), 0);
ASSERT_EQ(http_calls_to_route(s, project, "createItem", "__route__ANY__"), 0);
ASSERT_EQ(http_calls_to_route(s, project, "deleteItem", "__route__ANY__"), 0);
/* Angular service must NOT emit HANDLES edges (not a server route registration) */
ASSERT_EQ(cbm_store_count_edges_by_type(s, project, "HANDLES"), 0);
cbm_store_close(s);
cbm_pipeline_free(p);
if (saved) {
cbm_setenv("CBM_WORKERS", saved, 1);
free(saved);
} else {
cbm_unsetenv("CBM_WORKERS");
}
th_rmtree(tmp);
PASS();
}
TEST(pipeline_angular_http_client_typed_parallel) {
return angular_http_client_typed_case(52);
}
TEST(pipeline_angular_http_client_typed_sequential) {
return angular_http_client_typed_case(0);
}
/* #2768: the arg-URL heuristic (detect_url_in_args, via=arg_url) covers calls
* no service pattern knows, and it now types the edge with the verb the callee
* names instead of minting a method-less __route__ANY__ route. Two wrapper
* shapes reach it on the parallel resolver:
* - a project wrapper method whose verb is not a route-registration suffix
* (`this.api.head('/items/health')`, `.options(...)`), resolved to the
* project method, so the URL may be the first argument;
* - a route-verb wrapper (`.get`) whose URL is not route shaped for the
* registration reader: here a keyword it does not read as a path
* (`self.session.get(resource="/items/list")`).
* Parallel only: the sequential resolver has no arg-URL heuristic (#2848). */
TEST(pipeline_arg_url_wrapper_call_typed_with_verb_issue2768) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_arg_url_verb_XXXXXX");
if (!cbm_mkdtemp(tmp)) {
FAIL("tmpdir");
}
write_temp_file(tmp, "src/items.api.ts",
"export class ItemsApi {\n"
" head(path: string): Promise<Response> {\n"
" return fetch(path, { method: 'HEAD' });\n"
" }\n"
" options(path: string): Promise<Response> {\n"
" return fetch(path, { method: 'OPTIONS' });\n"
" }\n"
"}\n");
write_temp_file(tmp, "src/item.service.ts",
"import { ItemsApi } from './items.api';\n"
"export class ItemService {\n"
" constructor(private readonly api: ItemsApi) {}\n"
" pingItems() {\n"
" return this.api.head('/items/health');\n"
" }\n"
" probeItems() {\n"
" return this.api.options('/items/list');\n"
" }\n"
"}\n");
write_temp_file(tmp, "src/reader.py",
"class ItemsSession:\n"
" def get(self, *args, **kwargs):\n"
" return args\n"
"\n"
"class Reader:\n"
" def __init__(self):\n"
" self.session = ItemsSession()\n"
"\n"
" def read_items(self):\n"
" return self.session.get(resource=\"/items/list\")\n");
for (int i = 0; i < 52; i++) {
char name[64];
char body[128];
snprintf(name, sizeof(name), "src/filler%d.ts", i);
snprintf(body, sizeof(body), "export function filler%d(): number {\n return %d;\n}\n", i,
i);
write_temp_file(tmp, name, body);
}
char *old_workers = getenv("CBM_WORKERS");
char *saved = old_workers ? strdup(old_workers) : NULL;
cbm_setenv("CBM_WORKERS", "4", 1);
char db_path[512];
snprintf(db_path, sizeof(db_path), "%s/arg_url_verb.db", tmp);
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
ASSERT_NOT_NULL(p);
ASSERT_EQ(cbm_pipeline_run(p), 0);
const char *project = cbm_pipeline_project_name(p);
cbm_store_t *s = cbm_store_open_path(db_path);
ASSERT_NOT_NULL(s);
static const struct {
const char *caller;
const char *typed_route;
} wrapped[] = {{"pingItems", "__route__HEAD__/items/health"},
{"probeItems", "__route__OPTIONS__/items/list"},
{"read_items", "__route__GET__/items/list"}};
for (size_t i = 0; i < sizeof(wrapped) / sizeof(wrapped[0]); i++) {
/* The edge carries the callee's verb ... */
ASSERT_EQ(http_calls_to_route(s, project, wrapped[i].caller, wrapped[i].typed_route), 1);
/* ... and is the call site's only HTTP_CALLS edge: no ANY twin. */
ASSERT_EQ(http_calls_to_route(s, project, wrapped[i].caller, "__route__ANY__"), 0);
ASSERT_EQ(http_calls_to_route(s, project, wrapped[i].caller, "__route__"), 1);
}
/* Not a route registration */
ASSERT_EQ(cbm_store_count_edges_by_type(s, project, "HANDLES"), 0);
cbm_store_close(s);
cbm_pipeline_free(p);
if (saved) {
cbm_setenv("CBM_WORKERS", saved, 1);
free(saved);
} else {
cbm_unsetenv("CBM_WORKERS");
}
th_rmtree(tmp);
PASS();
}
/* #706 / #1147 end to end: a URL composed from module constants reaches the
* HTTP classifier, so the client call forms an HTTP_CALLS edge. Python
* `API_URL = API_BASE_URL + "/orders/create"` (env-derived base, literal path
@@ -30326,6 +31002,12 @@ SUITE(pipeline) {
RUN_TEST(pipeline_lock_release_allows_contender);
/* Lifecycle */
RUN_TEST(pipeline_create_free);
RUN_TEST(pipeline_background_worker_policy_preserves_overrides);
RUN_TEST(pipeline_background_worker_width_keeps_parallel_route);
#if defined(CBM_ENABLE_TEST_SEAMS) && CBM_ENABLE_TEST_SEAMS
RUN_TEST(pipeline_surface_respects_total_worker_budget);
#endif
RUN_TEST(pipeline_background_policy_preserves_index_results);
RUN_TEST(pipeline_null_repo);
RUN_TEST(pipeline_free_null);
RUN_TEST(pipeline_cancel);
@@ -30415,6 +31097,8 @@ SUITE(pipeline) {
RUN_TEST(pipeline_go_receiver_parallel_suppresses_weak_method_edge);
RUN_TEST(pipeline_go_multi_return_receiver_is_typed);
RUN_TEST(pipeline_go_multi_return_receiver_is_typed_parallel);
RUN_TEST(pipeline_go_parallel_field_hint_requires_owner_segment);
RUN_TEST(pipeline_csharp_parallel_field_hint_requires_owner_segment);
RUN_TEST(pipeline_go_multi_init_nodes_survive);
RUN_TEST(pipeline_go_init_lsp_edges_per_file);
RUN_TEST(pipeline_go_init_lsp_edges_per_file_parallel);
@@ -30471,6 +31155,9 @@ SUITE(pipeline) {
RUN_TEST(pipeline_local_fetch_shadow_not_classified_as_http);
RUN_TEST(pipeline_typed_http_call_has_no_arg_url_twin);
RUN_TEST(pipeline_typed_http_call_has_no_arg_url_twin_sequential);
RUN_TEST(pipeline_angular_http_client_typed_parallel);
RUN_TEST(pipeline_angular_http_client_typed_sequential);
RUN_TEST(pipeline_arg_url_wrapper_call_typed_with_verb_issue2768);
RUN_TEST(pipeline_url_constant_folding_http_calls_issue706_1147);
RUN_TEST(pipeline_folded_topics_preserve_identity_sequential_parallel);
RUN_TEST(pipeline_relative_folded_wrapper_url_candidates);