mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
Merge origin/main into feat/graph-gate-contract
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
commit
9aa44426f7
36 files changed
+4003
-129
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
@@ -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 */
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
@@ -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, ®ion) != 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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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)));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 */
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user