Schema properties, DATA_FLOWS, YAML nested extraction, trace_path tool

- Serialize return_type, parent_class, base_classes on definition nodes
- DATA_FLOWS edges link callers through Route nodes to handlers
- YAML nested field extraction with dotted key paths (key_path property)
- Rename trace_call_path → trace_path with mode parameter:
  calls (default), data_flow, cross_service
- Backward compatible: trace_call_path alias still accepted
- Updated CLI help text and all tests
This commit is contained in:
Martin Vogel
2026-03-28 13:53:22 +01:00
parent b5e288a640
commit 17db27adfa
13 changed files with 918 additions and 490 deletions
+1
View File
@@ -173,6 +173,7 @@ typedef enum {
typedef struct {
const char *value; // the string literal content
const char *enclosing_func_qn; // QN of enclosing function
const char *key_path; // dotted key path from YAML/JSON nesting (NULL if flat)
CBMStringRefKind kind; // URL, CONFIG
} CBMStringRef;
+111
View File
@@ -248,6 +248,116 @@ static void handle_string_refs(CBMExtractCtx *ctx, TSNode node, const WalkState
cbm_stringref_push(&ctx->result->string_refs, ctx->arena, ref);
}
// --- YAML nested field extraction (D2) ---
/* Recursively walk YAML block_mapping_pair nodes, building dotted key paths.
* Emits string_refs with key_path for leaf values that are URLs or config values.
* Example: body.operational_info.post_url → "https://..." */
static void walk_yaml_mapping(CBMExtractCtx *ctx, TSNode node, const char *prefix) {
uint32_t nc = ts_node_named_child_count(node);
for (uint32_t i = 0; i < nc; i++) {
TSNode child = ts_node_named_child(node, i);
const char *ck = ts_node_type(child);
if (strcmp(ck, "block_mapping_pair") != 0) {
continue;
}
/* Get key */
TSNode key = ts_node_child_by_field_name(child, "key", 3);
if (ts_node_is_null(key)) {
continue;
}
char *key_text = cbm_node_text(ctx->arena, key, ctx->source);
if (!key_text || !key_text[0]) {
continue;
}
/* Build dotted path */
const char *path =
prefix ? cbm_arena_sprintf(ctx->arena, "%s.%s", prefix, key_text) : key_text;
/* Get value node */
TSNode val = ts_node_child_by_field_name(child, "value", 5);
if (ts_node_is_null(val)) {
continue;
}
const char *vk = ts_node_type(val);
/* If value is a nested mapping (block_node → block_mapping), recurse */
if (strcmp(vk, "block_node") == 0 || strcmp(vk, "block_mapping") == 0) {
/* Walk children for nested block_mapping */
uint32_t vnc = ts_node_named_child_count(val);
for (uint32_t vi = 0; vi < vnc; vi++) {
TSNode vc = ts_node_named_child(val, vi);
const char *vctype = ts_node_type(vc);
if (strcmp(vctype, "block_mapping") == 0 ||
strcmp(vctype, "block_mapping_pair") == 0) {
walk_yaml_mapping(ctx, vc, path);
}
}
continue;
}
/* Leaf value: extract and classify */
char *val_text = cbm_node_text(ctx->arena, val, ctx->source);
if (!val_text || !val_text[0]) {
continue;
}
/* Strip quotes */
int vlen = (int)strlen(val_text);
const char *content = val_text;
if (vlen >= 2 && (val_text[0] == '"' || val_text[0] == '\'')) {
content = val_text + 1;
vlen -= 2;
if (vlen <= 0) {
continue;
}
}
int kind_val = cbm_classify_string(content, vlen);
if (kind_val < 0) {
continue;
}
char *stored = cbm_arena_strndup(ctx->arena, content, (size_t)vlen);
if (!stored) {
continue;
}
CBMStringRef ref = {
.value = stored,
.enclosing_func_qn = ctx->module_qn,
.key_path = path,
.kind = (CBMStringRefKind)kind_val,
};
cbm_stringref_push(&ctx->result->string_refs, ctx->arena, ref);
}
}
/* Handle YAML files: walk top-level block_mapping recursively */
static void handle_yaml_nested(CBMExtractCtx *ctx, TSNode node) {
if (ctx->language != CBM_LANG_YAML) {
return;
}
const char *kind = ts_node_type(node);
if (strcmp(kind, "block_mapping") != 0) {
return;
}
/* Only process root-level block_mapping (depth 0 or 1) */
TSNode parent = ts_node_parent(node);
if (ts_node_is_null(parent)) {
walk_yaml_mapping(ctx, node, NULL);
} else {
const char *pk = ts_node_type(parent);
if (strcmp(pk, "stream") == 0 || strcmp(pk, "document") == 0 ||
strcmp(pk, "block_node") == 0) {
walk_yaml_mapping(ctx, node, NULL);
}
}
}
// --- Main unified cursor walk ---
void cbm_extract_unified(CBMExtractCtx *ctx) {
@@ -281,6 +391,7 @@ void cbm_extract_unified(CBMExtractCtx *ctx) {
handle_env_accesses(ctx, node, spec, &state);
handle_type_assigns(ctx, node, spec, &state);
handle_string_refs(ctx, node, &state);
handle_yaml_nested(ctx, node);
// 4. Push scope markers for boundary nodes
if (spec->function_node_types && cbm_kind_in_set(node, spec->function_node_types)) {
+9 -9
View File
@@ -346,7 +346,7 @@ static const char skill_tracing_content[] =
"description: Call chain and dependency expert. ALWAYS invoke this skill when the user "
"asks who calls a function, what a function calls, needs impact analysis, or traces "
"dependencies. Do not grep for function names directly — use codebase-memory-mcp "
"trace_call_path first.\n"
"trace_path first.\n"
"---\n"
"\n"
"# Call Tracing & Impact Analysis\n"
@@ -355,7 +355,7 @@ static const char skill_tracing_content[] =
"\n"
"## Workflow\n"
"1. `search_graph(name_pattern=\".*FuncName.*\")` — find exact function name\n"
"2. `trace_call_path(function_name=\"FuncName\", direction=\"both\")` — trace callers + "
"2. `trace_path(function_name=\"FuncName\", direction=\"both\")` — trace callers + "
"callees\n"
"3. `detect_changes` — find what changed and assess risk_labels\n"
"\n"
@@ -402,7 +402,7 @@ static const char skill_reference_content[] =
"- `search_graph` — find nodes by pattern\n"
"- `search_code` — text search in source\n"
"- `query_graph` — Cypher query language\n"
"- `trace_call_path` — call chain traversal\n"
"- `trace_path` — call chain traversal\n"
"- `get_code_snippet` — read function source\n"
"- `get_graph_schema` — node/edge type catalog\n"
"- `get_architecture` — high-level summary\n"
@@ -429,7 +429,7 @@ static const char codex_instructions_content[] =
"Use the MCP tools to explore and understand the code:\n"
"\n"
"- `search_graph` — find functions, classes, routes by pattern\n"
"- `trace_call_path` — trace who calls a function or what it calls\n"
"- `trace_path` — trace who calls a function or what it calls\n"
"- `get_code_snippet` — read function source code\n"
"- `query_graph` — run Cypher queries for complex patterns\n"
"- `get_architecture` — high-level project summary\n"
@@ -963,7 +963,7 @@ static const char agent_instructions_content[] =
"\n"
"## Priority Order\n"
"1. `search_graph` — find functions, classes, routes, variables by pattern\n"
"2. `trace_call_path` — trace who calls a function or what it calls\n"
"2. `trace_path` — trace who calls a function or what it calls\n"
"3. `get_code_snippet` — read specific function/class source code\n"
"4. `query_graph` — run Cypher queries for complex patterns\n"
"5. `get_architecture` — high-level project summary\n"
@@ -975,7 +975,7 @@ static const char agent_instructions_content[] =
"\n"
"## Examples\n"
"- Find a handler: `search_graph(name_pattern=\".*OrderHandler.*\")`\n"
"- Who calls it: `trace_call_path(function_name=\"OrderHandler\", direction=\"inbound\")`\n"
"- Who calls it: `trace_path(function_name=\"OrderHandler\", direction=\"inbound\")`\n"
"- Read source: `get_code_snippet(qualified_name=\"pkg/orders.OrderHandler\")`\n";
const char *cbm_get_agent_instructions(void) {
@@ -1558,7 +1558,7 @@ static void cbm_install_hook_gate_script(const char *home) {
"fi\n"
"touch \"$GATE\"\n"
"echo 'BLOCKED: For code discovery, use codebase-memory-mcp tools first: "
"search_graph(name_pattern) to find functions/classes, trace_call_path() for "
"search_graph(name_pattern) to find functions/classes, trace_path() for "
"call chains, get_code_snippet(qualified_name) to read source. If the graph "
"is not indexed yet, call index_repository first. Fall back to Grep/Glob/Read "
"only for text content search. If you need Grep, retry.' >&2\n"
@@ -1574,8 +1574,8 @@ static void cbm_install_hook_gate_script(const char *home) {
}
#define GEMINI_HOOK_MATCHER "google_search|read_file|grep_search"
#define GEMINI_HOOK_COMMAND \
"echo 'Reminder: prefer codebase-memory-mcp search_graph/trace_call_path/" \
#define GEMINI_HOOK_COMMAND \
"echo 'Reminder: prefer codebase-memory-mcp search_graph/trace_path/" \
"get_code_snippet over grep/file search for code discovery.' >&2"
int cbm_upsert_gemini_hooks(const char *settings_path) {
+1 -1
View File
@@ -148,7 +148,7 @@ static void print_help(void) {
printf(" --port=N Set UI port (default 9749, persisted)\n");
printf("\nSupported agents (auto-detected):\n");
printf(" Claude Code, Codex CLI, Gemini CLI, Zed, OpenCode, Antigravity, Aider, KiloCode\n");
printf("\nTools: index_repository, search_graph, query_graph, trace_call_path,\n");
printf("\nTools: index_repository, search_graph, query_graph, trace_path,\n");
printf(" get_code_snippet, get_graph_schema, get_architecture, search_code,\n");
printf(" list_projects, delete_project, index_status, detect_changes,\n");
printf(" manage_adr, ingest_traces\n");
+75 -9
View File
@@ -253,14 +253,19 @@ static const tool_def_t TOOLS[] = {
"\"Optional row limit. Default: unlimited (100k "
"ceiling)\"}},\"required\":[\"query\",\"project\"]}"},
{"trace_call_path",
"Trace function call paths — who calls a function and what it calls. Use INSTEAD OF grep when "
"finding callers, dependencies, or impact analysis.",
{"trace_path",
"Trace paths through the code graph. Modes: calls (callers/callees), data_flow (value "
"propagation with args at each hop), cross_service (through HTTP/async Route nodes). "
"Use INSTEAD OF grep for callers, dependencies, impact analysis, or data flow tracing.",
"{\"type\":\"object\",\"properties\":{\"function_name\":{\"type\":\"string\"},\"project\":{"
"\"type\":\"string\"},\"direction\":{\"type\":\"string\",\"enum\":[\"inbound\",\"outbound\","
"\"both\"],\"default\":\"both\"},\"depth\":{\"type\":\"integer\",\"default\":3},\"edge_"
"types\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}}},\"required\":[\"function_"
"name\",\"project\"]}"},
"\"both\"],\"default\":\"both\"},\"depth\":{\"type\":\"integer\",\"default\":3},\"mode\":{"
"\"type\":\"string\",\"enum\":[\"calls\",\"data_flow\",\"cross_service\"],\"default\":"
"\"calls\",\"description\":\"calls: follow CALLS edges. data_flow: follow CALLS+DATA_FLOWS "
"with arg expressions. cross_service: follow HTTP_CALLS+ASYNC_CALLS+DATA_FLOWS through "
"Routes.\"},\"parameter_name\":{\"type\":\"string\",\"description\":\"For data_flow mode: "
"scope trace to a specific parameter name\"},\"edge_types\":{\"type\":\"array\",\"items\":{"
"\"type\":\"string\"}}},\"required\":[\"function_name\",\"project\"]}"},
{"get_code_snippet",
"Read source code for a function/class/symbol. IMPORTANT: First call search_graph to find the "
@@ -1223,11 +1228,15 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
char *project = cbm_mcp_get_string_arg(args, "project");
cbm_store_t *store = resolve_store(srv, project);
char *direction = cbm_mcp_get_string_arg(args, "direction");
char *mode = cbm_mcp_get_string_arg(args, "mode");
char *param_name = cbm_mcp_get_string_arg(args, "parameter_name");
int depth = cbm_mcp_get_int_arg(args, "depth", 3);
if (!func_name) {
free(project);
free(direction);
free(mode);
free(param_name);
return cbm_mcp_text_result("function_name is required", true);
}
if (!store) {
@@ -1237,6 +1246,8 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
free(func_name);
free(project);
free(direction);
free(mode);
free(param_name);
return _res;
}
@@ -1245,6 +1256,8 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
free(func_name);
free(project);
free(direction);
free(mode);
free(param_name);
return not_indexed;
}
@@ -1261,6 +1274,8 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
free(func_name);
free(project);
free(direction);
free(mode);
free(param_name);
cbm_store_free_nodes(nodes, 0);
return cbm_mcp_text_result("{\"error\":\"function not found\"}", true);
}
@@ -1271,9 +1286,55 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
yyjson_mut_obj_add_str(doc, root, "function", func_name);
yyjson_mut_obj_add_str(doc, root, "direction", direction);
if (mode) {
yyjson_mut_obj_add_str(doc, root, "mode", mode);
}
const char *edge_types[] = {"CALLS"};
int edge_type_count = 1;
/* Edge types: explicit > mode-based > default */
static const char *mode_calls[] = {"CALLS"};
static const char *mode_data_flow[] = {"CALLS", "DATA_FLOWS"};
static const char *mode_cross_svc[] = {"HTTP_CALLS", "ASYNC_CALLS", "DATA_FLOWS", "CALLS"};
const char *edge_types[16];
int edge_type_count = 0;
/* Try parsing explicit edge_types array from args */
yyjson_doc *et_doc = yyjson_read(args, strlen(args), 0);
if (et_doc) {
yyjson_val *et_arr = yyjson_obj_get(yyjson_doc_get_root(et_doc), "edge_types");
if (et_arr && yyjson_is_arr(et_arr)) {
size_t idx2;
size_t max2;
yyjson_val *val2;
yyjson_arr_foreach(et_arr, idx2, max2, val2) {
if (yyjson_is_str(val2) && edge_type_count < 16) {
edge_types[edge_type_count++] = yyjson_get_str(val2);
}
}
}
}
yyjson_doc *et_doc_keep = et_doc;
if (edge_type_count == 0) {
/* Select defaults by mode */
const char **defaults = mode_calls;
int n_defaults = 1;
if (mode && strcmp(mode, "data_flow") == 0) {
defaults = mode_data_flow;
n_defaults = 2;
} else if (mode && strcmp(mode, "cross_service") == 0) {
defaults = mode_cross_svc;
n_defaults = 4;
}
for (int i = 0; i < n_defaults; i++) {
edge_types[i] = defaults[i];
}
edge_type_count = n_defaults;
if (et_doc_keep) {
yyjson_doc_free(et_doc_keep);
et_doc_keep = NULL;
}
}
/* Run BFS for each requested direction.
* IMPORTANT: yyjson_mut_obj_add_str borrows pointers we must keep
@@ -1338,6 +1399,11 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
free(func_name);
free(project);
free(direction);
free(mode);
free(param_name);
if (et_doc_keep) {
yyjson_doc_free(et_doc_keep);
}
char *result = cbm_mcp_text_result(json, false);
free(json);
@@ -2698,7 +2764,7 @@ char *cbm_mcp_handle_tool(cbm_mcp_server_t *srv, const char *tool_name, const ch
if (strcmp(tool_name, "delete_project") == 0) {
return handle_delete_project(srv, args_json);
}
if (strcmp(tool_name, "trace_call_path") == 0) {
if (strcmp(tool_name, "trace_path") == 0 || strcmp(tool_name, "trace_call_path") == 0) {
return handle_trace_call_path(srv, args_json);
}
if (strcmp(tool_name, "get_architecture") == 0) {
+3
View File
@@ -182,7 +182,10 @@ static void build_def_props(char *buf, size_t bufsize, const CBMDefinition *def)
size_t pos = (size_t)n;
append_json_string(buf, bufsize, &pos, "docstring", def->docstring);
append_json_string(buf, bufsize, &pos, "signature", def->signature);
append_json_string(buf, bufsize, &pos, "return_type", def->return_type);
append_json_string(buf, bufsize, &pos, "parent_class", def->parent_class);
append_json_str_array(buf, bufsize, &pos, "decorators", def->decorators);
append_json_str_array(buf, bufsize, &pos, "base_classes", def->base_classes);
append_json_str_array(buf, bufsize, &pos, "param_names", def->param_names);
append_json_str_array(buf, bufsize, &pos, "param_types", def->param_types);
+3
View File
@@ -193,7 +193,10 @@ static void build_def_props(char *buf, size_t bufsize, const CBMDefinition *def)
size_t pos = (size_t)n;
append_json_string(buf, bufsize, &pos, "docstring", def->docstring);
append_json_string(buf, bufsize, &pos, "signature", def->signature);
append_json_string(buf, bufsize, &pos, "return_type", def->return_type);
append_json_string(buf, bufsize, &pos, "parent_class", def->parent_class);
append_json_str_array(buf, bufsize, &pos, "decorators", def->decorators);
append_json_str_array(buf, bufsize, &pos, "base_classes", def->base_classes);
append_json_str_array(buf, bufsize, &pos, "param_names", def->param_names);
append_json_str_array(buf, bufsize, &pos, "param_types", def->param_types);
if (pos < bufsize - 1) {
+77
View File
@@ -242,6 +242,80 @@ static void match_infra_routes(cbm_gbuf_t *gb) {
}
}
/* Phase 3: Create DATA_FLOWS edges by linking callers through Route to handlers.
* For each HTTP_CALLS/ASYNC_CALLS edge (caller Route), find the HANDLES edge
* (handler Route) and create DATA_FLOWS (caller handler) with route context. */
static void create_data_flows(cbm_gbuf_t *gb) {
const cbm_gbuf_node_t **routes = NULL;
int route_count = 0;
if (cbm_gbuf_find_by_label(gb, "Route", &routes, &route_count) != 0 || route_count == 0) {
return;
}
int flows = 0;
/* For each Route node, find callers (HTTP_CALLS/ASYNC_CALLS → Route)
* and handlers (HANDLES Route), then create DATA_FLOWS links. */
for (int ri = 0; ri < route_count; ri++) {
const cbm_gbuf_node_t *route = routes[ri];
/* Find HTTP_CALLS → Route */
const cbm_gbuf_edge_t **http_edges = NULL;
int http_count = 0;
cbm_gbuf_find_edges_by_target_type(gb, route->id, "HTTP_CALLS", &http_edges, &http_count);
/* Find ASYNC_CALLS → Route */
const cbm_gbuf_edge_t **async_edges = NULL;
int async_count = 0;
cbm_gbuf_find_edges_by_target_type(gb, route->id, "ASYNC_CALLS", &async_edges,
&async_count);
/* Find HANDLES → Route */
const cbm_gbuf_edge_t **handles_edges = NULL;
int handles_count = 0;
cbm_gbuf_find_edges_by_target_type(gb, route->id, "HANDLES", &handles_edges,
&handles_count);
/* Collect caller IDs */
int64_t callers[64];
int n_callers = 0;
for (int ei = 0; ei < http_count && n_callers < 64; ei++) {
callers[n_callers++] = http_edges[ei]->source_id;
}
for (int ei = 0; ei < async_count && n_callers < 64; ei++) {
callers[n_callers++] = async_edges[ei]->source_id;
}
/* Collect handler IDs */
int64_t handlers[16];
int n_handlers = 0;
for (int ei = 0; ei < handles_count && n_handlers < 16; ei++) {
handlers[n_handlers++] = handles_edges[ei]->source_id;
}
/* Create DATA_FLOWS: each caller → each handler through this Route */
for (int ci = 0; ci < n_callers; ci++) {
for (int hi = 0; hi < n_handlers; hi++) {
if (callers[ci] == handlers[hi]) {
continue; /* skip self-links */
}
char props[512];
snprintf(props, sizeof(props), "{\"via_route\":\"%s\",\"route_qn\":\"%s\"}",
route->name ? route->name : "",
route->qualified_name ? route->qualified_name : "");
cbm_gbuf_insert_edge(gb, callers[ci], handlers[hi], "DATA_FLOWS", props);
flows++;
}
}
}
if (flows > 0) {
char buf[16];
snprintf(buf, sizeof(buf), "%d", flows);
cbm_log_info("pass.data_flows", "created", buf);
}
}
void cbm_pipeline_create_route_nodes(cbm_gbuf_t *gb) {
if (!gb) {
return;
@@ -258,4 +332,7 @@ void cbm_pipeline_create_route_nodes(cbm_gbuf_t *gb) {
/* Phase 2: match infra Routes to handler Routes by URL path */
match_infra_routes(gb);
/* Phase 3: create DATA_FLOWS edges through Routes */
create_data_flows(gb);
}
+8 -2
View File
@@ -344,8 +344,14 @@ static void cbm_pipeline_extract_infra_routes(cbm_gbuf_t *gbuf, const cbm_file_i
strstr(sr->value, "://") != NULL) {
char route_qn[CBM_ROUTE_QN_SIZE];
snprintf(route_qn, sizeof(route_qn), "__route__infra__%s", sr->value);
cbm_gbuf_upsert_node(gbuf, "Route", sr->value, route_qn, fp, 0, 0,
"{\"source\":\"infra\"}");
char route_props[512];
if (sr->key_path != NULL) {
snprintf(route_props, sizeof(route_props),
"{\"source\":\"infra\",\"key_path\":\"%s\"}", sr->key_path);
} else {
snprintf(route_props, sizeof(route_props), "{\"source\":\"infra\"}");
}
cbm_gbuf_upsert_node(gbuf, "Route", sr->value, route_qn, fp, 0, 0, route_props);
}
}
}
+297 -180
View File
@@ -174,7 +174,8 @@ TEST(cli_version_get_set) {
* */
TEST(cli_detect_shell_rc_zsh) {
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-rc-XXXXXX");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-rc-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
@@ -197,7 +198,8 @@ TEST(cli_detect_shell_rc_zsh) {
}
TEST(cli_detect_shell_rc_bash) {
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-rc-XXXXXX");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-rc-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
@@ -221,7 +223,8 @@ TEST(cli_detect_shell_rc_bash) {
TEST(cli_detect_shell_rc_bash_with_bashrc) {
/* Port of TestDetectShellRC_BashWithBashrc */
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-rc-XXXXXX");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-rc-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
@@ -248,7 +251,8 @@ TEST(cli_detect_shell_rc_bash_with_bashrc) {
}
TEST(cli_detect_shell_rc_fish) {
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-rc-XXXXXX");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-rc-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
@@ -269,7 +273,8 @@ TEST(cli_detect_shell_rc_fish) {
}
TEST(cli_detect_shell_rc_default) {
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-rc-XXXXXX");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-rc-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
@@ -295,7 +300,8 @@ TEST(cli_detect_shell_rc_default) {
TEST(cli_find_cli_not_found) {
/* Port of TestFindCLI_NotFound */
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-find-XXXXXX");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-find-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
@@ -319,7 +325,8 @@ TEST(cli_find_cli_on_path) {
SKIP("PATH search differs on Windows");
#endif
/* Port of TestFindCLI_FoundOnPATH */
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-find-XXXXXX");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-find-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
@@ -350,7 +357,8 @@ TEST(cli_find_cli_fallback_paths) {
SKIP("shell scripts + chmod not available on Windows");
#endif
/* Port of TestFindCLI_FallbackPaths */
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-find-XXXXXX");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-find-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
@@ -403,7 +411,8 @@ TEST(cli_dry_run_flags) {
TEST(cli_skill_creation) {
/* Port of TestInstallSkillCreation */
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-skill-XXXXXX");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-skill-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
@@ -433,7 +442,8 @@ TEST(cli_skill_creation) {
TEST(cli_skill_idempotent) {
/* Port of TestInstallIdempotent */
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-skill-XXXXXX");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-skill-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
@@ -462,7 +472,8 @@ TEST(cli_skill_idempotent) {
TEST(cli_skill_force_overwrite) {
/* Port of TestCLI_InstallForceOverwrites */
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-skill-XXXXXX");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-skill-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
@@ -481,7 +492,8 @@ TEST(cli_skill_force_overwrite) {
TEST(cli_uninstall_removes_skills) {
/* Port of TestUninstallRemovesSkills */
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-skill-XXXXXX");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-skill-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
@@ -507,7 +519,8 @@ TEST(cli_uninstall_removes_skills) {
TEST(cli_remove_old_monolithic_skill) {
/* Port of TestRemoveOldMonolithicSkill */
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-skill-XXXXXX");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-skill-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
@@ -548,7 +561,7 @@ TEST(cli_skill_files_content) {
}
if (strcmp(sk[i].name, "codebase-memory-tracing") == 0) {
found_tracing = true;
ASSERT(strstr(sk[i].content, "trace_call_path") != NULL);
ASSERT(strstr(sk[i].content, "trace_path") != NULL);
ASSERT(strstr(sk[i].content, "direction") != NULL);
ASSERT(strstr(sk[i].content, "detect_changes") != NULL);
}
@@ -576,7 +589,7 @@ TEST(cli_codex_instructions) {
const char *instr = cbm_get_codex_instructions();
ASSERT_NOT_NULL(instr);
ASSERT(strstr(instr, "Codebase Knowledge Graph") != NULL);
ASSERT(strstr(instr, "trace_call_path") != NULL);
ASSERT(strstr(instr, "trace_path") != NULL);
PASS();
}
@@ -586,7 +599,8 @@ TEST(cli_codex_instructions) {
TEST(cli_editor_mcp_install) {
/* Port of TestEditorMCPInstall */
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-mcp-XXXXXX");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-mcp-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
@@ -608,7 +622,8 @@ TEST(cli_editor_mcp_install) {
TEST(cli_editor_mcp_idempotent) {
/* Port of TestEditorMCPInstallIdempotent */
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-mcp-XXXXXX");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-mcp-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
@@ -638,7 +653,8 @@ TEST(cli_editor_mcp_idempotent) {
TEST(cli_editor_mcp_preserves_others) {
/* Port of TestEditorMCPPreservesOtherServers */
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-mcp-XXXXXX");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-mcp-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
@@ -666,7 +682,8 @@ TEST(cli_editor_mcp_preserves_others) {
TEST(cli_editor_mcp_uninstall) {
/* Port of TestEditorMCPUninstall */
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-mcp-XXXXXX");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-mcp-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
@@ -688,7 +705,8 @@ TEST(cli_editor_mcp_uninstall) {
TEST(cli_gemini_mcp_install) {
/* Port of TestGeminiMCPInstall */
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-mcp-XXXXXX");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-mcp-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
@@ -714,7 +732,8 @@ TEST(cli_gemini_mcp_install) {
TEST(cli_vscode_mcp_install) {
/* Port of TestVSCodeMCPInstall */
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-mcp-XXXXXX");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-mcp-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
@@ -738,7 +757,8 @@ TEST(cli_vscode_mcp_install) {
TEST(cli_vscode_mcp_uninstall) {
/* Port of TestVSCodeMCPUninstall */
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-mcp-XXXXXX");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-mcp-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
@@ -763,7 +783,8 @@ TEST(cli_vscode_mcp_uninstall) {
TEST(cli_zed_mcp_install) {
/* Port of TestZedMCPInstall */
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-mcp-XXXXXX");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-mcp-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
@@ -787,7 +808,8 @@ TEST(cli_zed_mcp_install) {
TEST(cli_zed_mcp_preserves_settings) {
/* Port of TestZedMCPPreservesSettings */
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-mcp-XXXXXX");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-mcp-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
@@ -817,7 +839,8 @@ TEST(cli_zed_mcp_preserves_settings) {
TEST(cli_zed_mcp_uninstall) {
/* Port of TestZedMCPUninstall */
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-mcp-XXXXXX");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-mcp-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
@@ -838,7 +861,8 @@ TEST(cli_zed_mcp_uninstall) {
TEST(cli_zed_mcp_jsonc_comments) {
/* Issue #24: Zed settings.json uses JSONC (comments + trailing commas) */
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-mcp-XXXXXX");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-mcp-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
@@ -849,14 +873,13 @@ TEST(cli_zed_mcp_jsonc_comments) {
test_mkdirp(dir);
/* JSONC with comments and trailing commas — must not fail */
write_test_file(configpath,
"// Zed settings\n"
"{\n"
" \"theme\": \"One Dark\",\n"
" /* multi-line\n"
" comment */\n"
" \"vim_mode\": true,\n" /* trailing comma */
"}\n");
write_test_file(configpath, "// Zed settings\n"
"{\n"
" \"theme\": \"One Dark\",\n"
" /* multi-line\n"
" comment */\n"
" \"vim_mode\": true,\n" /* trailing comma */
"}\n");
int rc = cbm_install_zed_mcp("/usr/local/bin/codebase-memory-mcp", configpath);
ASSERT_EQ(rc, 0);
@@ -880,7 +903,8 @@ TEST(cli_zed_mcp_jsonc_comments) {
TEST(cli_ensure_path_append) {
/* Port of TestCLI_InstallPATHAppend */
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-path-XXXXXX");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-path-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
@@ -899,7 +923,8 @@ TEST(cli_ensure_path_append) {
}
TEST(cli_ensure_path_already_present) {
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-path-XXXXXX");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-path-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
@@ -915,7 +940,8 @@ TEST(cli_ensure_path_already_present) {
}
TEST(cli_ensure_path_dry_run) {
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-path-XXXXXX");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-path-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
@@ -940,7 +966,8 @@ TEST(cli_ensure_path_dry_run) {
TEST(cli_copy_file) {
/* Port of TestCopyFile */
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-copy-XXXXXX");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-copy-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
@@ -962,7 +989,8 @@ TEST(cli_copy_file) {
TEST(cli_copy_file_source_not_found) {
/* Port of TestCopyFile_SourceNotFound */
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-copy-XXXXXX");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-copy-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
@@ -1040,21 +1068,29 @@ static unsigned char *create_test_zip_stored(const char *filename, const unsigne
int eocd_sz = 22;
int total = local_hdr_sz + content_len + cd_hdr_sz + eocd_sz;
unsigned char *zip = calloc(1, (size_t)total);
if (!zip) return NULL;
if (!zip)
return NULL;
int pos = 0;
/* Local file header */
zip[pos] = 0x50; zip[pos+1] = 0x4B; zip[pos+2] = 0x03; zip[pos+3] = 0x04; /* signature */
zip[pos+4] = 20; zip[pos+5] = 0; /* version needed = 2.0 */
zip[pos+8] = 0; zip[pos+9] = 0; /* compression = stored */
zip[pos+18] = (unsigned char)(content_len & 0xFF);
zip[pos+19] = (unsigned char)((content_len >> 8) & 0xFF);
zip[pos+20] = (unsigned char)((content_len >> 16) & 0xFF);
zip[pos+21] = (unsigned char)((content_len >> 24) & 0xFF);
zip[pos+22] = zip[pos+18]; zip[pos+23] = zip[pos+19];
zip[pos+24] = zip[pos+20]; zip[pos+25] = zip[pos+21];
zip[pos+26] = (unsigned char)(name_len & 0xFF);
zip[pos+27] = (unsigned char)((name_len >> 8) & 0xFF);
zip[pos] = 0x50;
zip[pos + 1] = 0x4B;
zip[pos + 2] = 0x03;
zip[pos + 3] = 0x04; /* signature */
zip[pos + 4] = 20;
zip[pos + 5] = 0; /* version needed = 2.0 */
zip[pos + 8] = 0;
zip[pos + 9] = 0; /* compression = stored */
zip[pos + 18] = (unsigned char)(content_len & 0xFF);
zip[pos + 19] = (unsigned char)((content_len >> 8) & 0xFF);
zip[pos + 20] = (unsigned char)((content_len >> 16) & 0xFF);
zip[pos + 21] = (unsigned char)((content_len >> 24) & 0xFF);
zip[pos + 22] = zip[pos + 18];
zip[pos + 23] = zip[pos + 19];
zip[pos + 24] = zip[pos + 20];
zip[pos + 25] = zip[pos + 21];
zip[pos + 26] = (unsigned char)(name_len & 0xFF);
zip[pos + 27] = (unsigned char)((name_len >> 8) & 0xFF);
memcpy(zip + pos + 30, filename, (size_t)name_len);
pos += 30 + name_len;
memcpy(zip + pos, content, (size_t)content_len);
@@ -1062,27 +1098,36 @@ static unsigned char *create_test_zip_stored(const char *filename, const unsigne
int cd_start = pos;
/* Central directory header */
zip[pos] = 0x50; zip[pos+1] = 0x4B; zip[pos+2] = 0x01; zip[pos+3] = 0x02;
zip[pos+10] = 0; zip[pos+11] = 0; /* compression = stored */
zip[pos+20] = (unsigned char)(content_len & 0xFF);
zip[pos+21] = (unsigned char)((content_len >> 8) & 0xFF);
zip[pos+22] = (unsigned char)((content_len >> 16) & 0xFF);
zip[pos+23] = (unsigned char)((content_len >> 24) & 0xFF);
zip[pos+24] = zip[pos+20]; zip[pos+25] = zip[pos+21];
zip[pos+26] = zip[pos+22]; zip[pos+27] = zip[pos+23];
zip[pos+28] = (unsigned char)(name_len & 0xFF);
zip[pos+29] = (unsigned char)((name_len >> 8) & 0xFF);
zip[pos] = 0x50;
zip[pos + 1] = 0x4B;
zip[pos + 2] = 0x01;
zip[pos + 3] = 0x02;
zip[pos + 10] = 0;
zip[pos + 11] = 0; /* compression = stored */
zip[pos + 20] = (unsigned char)(content_len & 0xFF);
zip[pos + 21] = (unsigned char)((content_len >> 8) & 0xFF);
zip[pos + 22] = (unsigned char)((content_len >> 16) & 0xFF);
zip[pos + 23] = (unsigned char)((content_len >> 24) & 0xFF);
zip[pos + 24] = zip[pos + 20];
zip[pos + 25] = zip[pos + 21];
zip[pos + 26] = zip[pos + 22];
zip[pos + 27] = zip[pos + 23];
zip[pos + 28] = (unsigned char)(name_len & 0xFF);
zip[pos + 29] = (unsigned char)((name_len >> 8) & 0xFF);
pos += 46 + name_len;
/* EOCD */
zip[pos] = 0x50; zip[pos+1] = 0x4B; zip[pos+2] = 0x05; zip[pos+3] = 0x06;
zip[pos+8] = 1; /* num entries this disk */
zip[pos+10] = 1; /* total entries */
zip[pos] = 0x50;
zip[pos + 1] = 0x4B;
zip[pos + 2] = 0x05;
zip[pos + 3] = 0x06;
zip[pos + 8] = 1; /* num entries this disk */
zip[pos + 10] = 1; /* total entries */
int cd_size = pos - cd_start;
zip[pos+12] = (unsigned char)(cd_size & 0xFF);
zip[pos+13] = (unsigned char)((cd_size >> 8) & 0xFF);
zip[pos+16] = (unsigned char)(cd_start & 0xFF);
zip[pos+17] = (unsigned char)((cd_start >> 8) & 0xFF);
zip[pos + 12] = (unsigned char)(cd_size & 0xFF);
zip[pos + 13] = (unsigned char)((cd_size >> 8) & 0xFF);
zip[pos + 16] = (unsigned char)(cd_start & 0xFF);
zip[pos + 17] = (unsigned char)((cd_start >> 8) & 0xFF);
*out_len = total;
return zip;
@@ -1091,9 +1136,8 @@ static unsigned char *create_test_zip_stored(const char *filename, const unsigne
TEST(cli_extract_binary_from_zip) {
const char *content = "#!/bin/sh\necho test\n";
int zip_len = 0;
unsigned char *zip =
create_test_zip_stored("codebase-memory-mcp", (const unsigned char *)content,
(int)strlen(content), &zip_len);
unsigned char *zip = create_test_zip_stored(
"codebase-memory-mcp", (const unsigned char *)content, (int)strlen(content), &zip_len);
ASSERT_NOT_NULL(zip);
int out_len = 0;
@@ -1109,9 +1153,8 @@ TEST(cli_extract_binary_from_zip) {
TEST(cli_extract_binary_from_zip_not_found) {
const char *content = "data";
int zip_len = 0;
unsigned char *zip =
create_test_zip_stored("other-file.txt", (const unsigned char *)content,
(int)strlen(content), &zip_len);
unsigned char *zip = create_test_zip_stored("other-file.txt", (const unsigned char *)content,
(int)strlen(content), &zip_len);
ASSERT_NOT_NULL(zip);
int out_len = 0;
@@ -1150,7 +1193,8 @@ TEST(cli_extract_binary_from_zip_invalid) {
TEST(cli_install_dry_run) {
/* Port of TestCLI_InstallDryRun */
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-dry-XXXXXX");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-dry-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
@@ -1175,7 +1219,8 @@ TEST(cli_install_dry_run) {
TEST(cli_uninstall_dry_run) {
/* Port of TestCLI_UninstallDryRun */
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-dry-XXXXXX");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-dry-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
@@ -1205,7 +1250,8 @@ TEST(cli_uninstall_dry_run) {
TEST(cli_install_and_uninstall) {
/* Port of TestCLI_InstallAndUninstall */
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-full-XXXXXX");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-full-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
@@ -1341,8 +1387,10 @@ TEST(cli_yaml_has) {
* */
TEST(cli_detect_agents_finds_claude) {
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-detect-XXXXXX");
if (!cbm_mkdtemp(tmpdir)) SKIP("cbm_mkdtemp failed");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-detect-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
char dir[512];
snprintf(dir, sizeof(dir), "%s/.claude", tmpdir);
@@ -1356,8 +1404,10 @@ TEST(cli_detect_agents_finds_claude) {
}
TEST(cli_detect_agents_finds_codex) {
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-detect-XXXXXX");
if (!cbm_mkdtemp(tmpdir)) SKIP("cbm_mkdtemp failed");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-detect-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
char dir[512];
snprintf(dir, sizeof(dir), "%s/.codex", tmpdir);
@@ -1371,8 +1421,10 @@ TEST(cli_detect_agents_finds_codex) {
}
TEST(cli_detect_agents_finds_gemini) {
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-detect-XXXXXX");
if (!cbm_mkdtemp(tmpdir)) SKIP("cbm_mkdtemp failed");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-detect-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
char dir[512];
snprintf(dir, sizeof(dir), "%s/.gemini", tmpdir);
@@ -1386,8 +1438,10 @@ TEST(cli_detect_agents_finds_gemini) {
}
TEST(cli_detect_agents_finds_zed) {
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-detect-XXXXXX");
if (!cbm_mkdtemp(tmpdir)) SKIP("cbm_mkdtemp failed");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-detect-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
char dir[512];
#ifdef __APPLE__
@@ -1405,8 +1459,10 @@ TEST(cli_detect_agents_finds_zed) {
}
TEST(cli_detect_agents_finds_antigravity) {
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-detect-XXXXXX");
if (!cbm_mkdtemp(tmpdir)) SKIP("cbm_mkdtemp failed");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-detect-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
char dir[512];
snprintf(dir, sizeof(dir), "%s/.gemini/antigravity", tmpdir);
@@ -1421,12 +1477,13 @@ TEST(cli_detect_agents_finds_antigravity) {
}
TEST(cli_detect_agents_finds_kilocode) {
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-detect-XXXXXX");
if (!cbm_mkdtemp(tmpdir)) SKIP("cbm_mkdtemp failed");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-detect-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
char dir[512];
snprintf(dir, sizeof(dir),
"%s/.config/Code/User/globalStorage/kilocode.kilo-code", tmpdir);
snprintf(dir, sizeof(dir), "%s/.config/Code/User/globalStorage/kilocode.kilo-code", tmpdir);
test_mkdirp(dir);
cbm_detected_agents_t agents = cbm_detect_agents(tmpdir);
@@ -1437,8 +1494,10 @@ TEST(cli_detect_agents_finds_kilocode) {
}
TEST(cli_detect_agents_none_found) {
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-detect-XXXXXX");
if (!cbm_mkdtemp(tmpdir)) SKIP("cbm_mkdtemp failed");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-detect-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
/* Empty home dir → no config dirs → no directory-based agents detected.
* Note: opencode/aider may still be detected via system fallback paths
@@ -1460,8 +1519,10 @@ TEST(cli_detect_agents_none_found) {
* */
TEST(cli_upsert_codex_mcp_fresh) {
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-codex-XXXXXX");
if (!cbm_mkdtemp(tmpdir)) SKIP("cbm_mkdtemp failed");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-codex-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
char configpath[512];
snprintf(configpath, sizeof(configpath), "%s/config.toml", tmpdir);
@@ -1479,8 +1540,10 @@ TEST(cli_upsert_codex_mcp_fresh) {
}
TEST(cli_upsert_codex_mcp_existing) {
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-codex-XXXXXX");
if (!cbm_mkdtemp(tmpdir)) SKIP("cbm_mkdtemp failed");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-codex-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
char configpath[512];
snprintf(configpath, sizeof(configpath), "%s/config.toml", tmpdir);
@@ -1502,16 +1565,17 @@ TEST(cli_upsert_codex_mcp_existing) {
}
TEST(cli_upsert_codex_mcp_replace) {
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-codex-XXXXXX");
if (!cbm_mkdtemp(tmpdir)) SKIP("cbm_mkdtemp failed");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-codex-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
char configpath[512];
snprintf(configpath, sizeof(configpath), "%s/config.toml", tmpdir);
write_test_file(configpath,
"[mcp_servers.codebase-memory-mcp]\n"
"command = \"/old/path/codebase-memory-mcp\"\n"
"\n"
"[other_setting]\nfoo = \"bar\"\n");
write_test_file(configpath, "[mcp_servers.codebase-memory-mcp]\n"
"command = \"/old/path/codebase-memory-mcp\"\n"
"\n"
"[other_setting]\nfoo = \"bar\"\n");
int rc = cbm_upsert_codex_mcp("/new/path/codebase-memory-mcp", configpath);
ASSERT_EQ(rc, 0);
@@ -1534,8 +1598,10 @@ TEST(cli_upsert_codex_mcp_replace) {
TEST(cli_zed_mcp_uses_args_format) {
/* Verify Zed uses args:[""] NOT source:"custom" */
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-zed-XXXXXX");
if (!cbm_mkdtemp(tmpdir)) SKIP("cbm_mkdtemp failed");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-zed-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
char configpath[512];
snprintf(configpath, sizeof(configpath), "%s/settings.json", tmpdir);
@@ -1557,8 +1623,10 @@ TEST(cli_zed_mcp_uses_args_format) {
* */
TEST(cli_upsert_opencode_mcp_fresh) {
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-ocode-XXXXXX");
if (!cbm_mkdtemp(tmpdir)) SKIP("cbm_mkdtemp failed");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-ocode-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
char configpath[512];
snprintf(configpath, sizeof(configpath), "%s/opencode.json", tmpdir);
@@ -1574,15 +1642,18 @@ TEST(cli_upsert_opencode_mcp_fresh) {
/* command must be emitted as an array, not a string */
ASSERT(strstr(data, "\"command\":[") != NULL || strstr(data, "\"command\": [") != NULL);
/* type must be explicitly set to \"local\" */
ASSERT(strstr(data, "\"type\":\"local\"") != NULL || strstr(data, "\"type\": \"local\"") != NULL);
ASSERT(strstr(data, "\"type\":\"local\"") != NULL ||
strstr(data, "\"type\": \"local\"") != NULL);
test_rmdir_r(tmpdir);
PASS();
}
TEST(cli_upsert_opencode_mcp_existing) {
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-ocode-XXXXXX");
if (!cbm_mkdtemp(tmpdir)) SKIP("cbm_mkdtemp failed");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-ocode-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
char configpath[512];
snprintf(configpath, sizeof(configpath), "%s/opencode.json", tmpdir);
@@ -1605,8 +1676,10 @@ TEST(cli_upsert_opencode_mcp_existing) {
* */
TEST(cli_upsert_antigravity_mcp_fresh) {
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-anti-XXXXXX");
if (!cbm_mkdtemp(tmpdir)) SKIP("cbm_mkdtemp failed");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-anti-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
char configpath[512];
snprintf(configpath, sizeof(configpath), "%s/mcp_config.json", tmpdir);
@@ -1623,13 +1696,15 @@ TEST(cli_upsert_antigravity_mcp_fresh) {
}
TEST(cli_upsert_antigravity_mcp_replace) {
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-anti-XXXXXX");
if (!cbm_mkdtemp(tmpdir)) SKIP("cbm_mkdtemp failed");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-anti-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
char configpath[512];
snprintf(configpath, sizeof(configpath), "%s/mcp_config.json", tmpdir);
write_test_file(configpath,
"{\"mcpServers\":{\"codebase-memory-mcp\":{\"command\":\"/old/path\"}}}");
"{\"mcpServers\":{\"codebase-memory-mcp\":{\"command\":\"/old/path\"}}}");
int rc = cbm_upsert_antigravity_mcp("/new/path/codebase-memory-mcp", configpath);
ASSERT_EQ(rc, 0);
@@ -1648,8 +1723,10 @@ TEST(cli_upsert_antigravity_mcp_replace) {
* */
TEST(cli_upsert_instructions_fresh) {
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-instr-XXXXXX");
if (!cbm_mkdtemp(tmpdir)) SKIP("cbm_mkdtemp failed");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-instr-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
char filepath[512];
snprintf(filepath, sizeof(filepath), "%s/AGENTS.md", tmpdir);
@@ -1668,8 +1745,10 @@ TEST(cli_upsert_instructions_fresh) {
}
TEST(cli_upsert_instructions_existing) {
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-instr-XXXXXX");
if (!cbm_mkdtemp(tmpdir)) SKIP("cbm_mkdtemp failed");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-instr-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
char filepath[512];
snprintf(filepath, sizeof(filepath), "%s/AGENTS.md", tmpdir);
@@ -1692,17 +1771,18 @@ TEST(cli_upsert_instructions_existing) {
}
TEST(cli_upsert_instructions_replace) {
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-instr-XXXXXX");
if (!cbm_mkdtemp(tmpdir)) SKIP("cbm_mkdtemp failed");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-instr-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
char filepath[512];
snprintf(filepath, sizeof(filepath), "%s/AGENTS.md", tmpdir);
write_test_file(filepath,
"# Rules\n"
"<!-- codebase-memory-mcp:start -->\n"
"OLD CONTENT\n"
"<!-- codebase-memory-mcp:end -->\n"
"# Other stuff\n");
write_test_file(filepath, "# Rules\n"
"<!-- codebase-memory-mcp:start -->\n"
"OLD CONTENT\n"
"<!-- codebase-memory-mcp:end -->\n"
"# Other stuff\n");
int rc = cbm_upsert_instructions(filepath, "NEW CONTENT\n");
ASSERT_EQ(rc, 0);
@@ -1721,8 +1801,10 @@ TEST(cli_upsert_instructions_replace) {
}
TEST(cli_upsert_instructions_no_duplicate) {
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-instr-XXXXXX");
if (!cbm_mkdtemp(tmpdir)) SKIP("cbm_mkdtemp failed");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-instr-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
char filepath[512];
snprintf(filepath, sizeof(filepath), "%s/AGENTS.md", tmpdir);
@@ -1736,7 +1818,10 @@ TEST(cli_upsert_instructions_no_duplicate) {
/* Only one start marker */
int count = 0;
const char *p = data;
while ((p = strstr(p, "codebase-memory-mcp:start")) != NULL) { count++; p += 25; }
while ((p = strstr(p, "codebase-memory-mcp:start")) != NULL) {
count++;
p += 25;
}
ASSERT_EQ(count, 1);
/* Latest content */
ASSERT(strstr(data, "Content v2") != NULL);
@@ -1747,17 +1832,18 @@ TEST(cli_upsert_instructions_no_duplicate) {
}
TEST(cli_remove_instructions) {
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-instr-XXXXXX");
if (!cbm_mkdtemp(tmpdir)) SKIP("cbm_mkdtemp failed");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-instr-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
char filepath[512];
snprintf(filepath, sizeof(filepath), "%s/AGENTS.md", tmpdir);
write_test_file(filepath,
"# Rules\n"
"<!-- codebase-memory-mcp:start -->\n"
"CMM Content\n"
"<!-- codebase-memory-mcp:end -->\n"
"# Other\n");
write_test_file(filepath, "# Rules\n"
"<!-- codebase-memory-mcp:start -->\n"
"CMM Content\n"
"<!-- codebase-memory-mcp:end -->\n"
"# Other\n");
int rc = cbm_remove_instructions(filepath);
ASSERT_EQ(rc, 0);
@@ -1777,7 +1863,7 @@ TEST(cli_agent_instructions_content) {
const char *instr = cbm_get_agent_instructions();
ASSERT_NOT_NULL(instr);
ASSERT(strstr(instr, "search_graph") != NULL);
ASSERT(strstr(instr, "trace_call_path") != NULL);
ASSERT(strstr(instr, "trace_path") != NULL);
ASSERT(strstr(instr, "get_code_snippet") != NULL);
PASS();
}
@@ -1787,8 +1873,10 @@ TEST(cli_agent_instructions_content) {
* */
TEST(cli_upsert_claude_hook_fresh) {
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-hook-XXXXXX");
if (!cbm_mkdtemp(tmpdir)) SKIP("cbm_mkdtemp failed");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-hook-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
char settingspath[512];
snprintf(settingspath, sizeof(settingspath), "%s/settings.json", tmpdir);
@@ -1807,15 +1895,17 @@ TEST(cli_upsert_claude_hook_fresh) {
}
TEST(cli_upsert_claude_hook_existing) {
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-hook-XXXXXX");
if (!cbm_mkdtemp(tmpdir)) SKIP("cbm_mkdtemp failed");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-hook-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
char settingspath[512];
snprintf(settingspath, sizeof(settingspath), "%s/settings.json", tmpdir);
/* Pre-existing settings with other hooks */
write_test_file(settingspath,
"{\"hooks\":{\"PreToolUse\":[{\"matcher\":\"Bash\","
"\"hooks\":[{\"type\":\"command\",\"command\":\"echo firewall\"}]}]}}");
"{\"hooks\":{\"PreToolUse\":[{\"matcher\":\"Bash\","
"\"hooks\":[{\"type\":\"command\",\"command\":\"echo firewall\"}]}]}}");
int rc = cbm_upsert_claude_hooks(settingspath);
ASSERT_EQ(rc, 0);
@@ -1833,15 +1923,17 @@ TEST(cli_upsert_claude_hook_existing) {
}
TEST(cli_upsert_claude_hook_replace) {
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-hook-XXXXXX");
if (!cbm_mkdtemp(tmpdir)) SKIP("cbm_mkdtemp failed");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-hook-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
char settingspath[512];
snprintf(settingspath, sizeof(settingspath), "%s/settings.json", tmpdir);
/* Pre-existing CMM hook with old message */
write_test_file(settingspath,
"{\"hooks\":{\"PreToolUse\":[{\"matcher\":\"Grep|Glob|Read\","
"\"hooks\":[{\"type\":\"command\",\"command\":\"echo old-cmm-message\"}]}]}}");
"{\"hooks\":{\"PreToolUse\":[{\"matcher\":\"Grep|Glob|Read\","
"\"hooks\":[{\"type\":\"command\",\"command\":\"echo old-cmm-message\"}]}]}}");
int rc = cbm_upsert_claude_hooks(settingspath);
ASSERT_EQ(rc, 0);
@@ -1857,15 +1949,17 @@ TEST(cli_upsert_claude_hook_replace) {
}
TEST(cli_upsert_claude_hook_preserves_others) {
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-hook-XXXXXX");
if (!cbm_mkdtemp(tmpdir)) SKIP("cbm_mkdtemp failed");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-hook-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
char settingspath[512];
snprintf(settingspath, sizeof(settingspath), "%s/settings.json", tmpdir);
write_test_file(settingspath,
"{\"apiKey\":\"sk-123\","
"\"hooks\":{\"PreToolUse\":[{\"matcher\":\"Bash\","
"\"hooks\":[{\"type\":\"command\",\"command\":\"echo guard\"}]}]}}");
"{\"apiKey\":\"sk-123\","
"\"hooks\":{\"PreToolUse\":[{\"matcher\":\"Bash\","
"\"hooks\":[{\"type\":\"command\",\"command\":\"echo guard\"}]}]}}");
cbm_upsert_claude_hooks(settingspath);
@@ -1883,8 +1977,10 @@ TEST(cli_upsert_claude_hook_preserves_others) {
}
TEST(cli_remove_claude_hooks) {
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-hook-XXXXXX");
if (!cbm_mkdtemp(tmpdir)) SKIP("cbm_mkdtemp failed");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-hook-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
char settingspath[512];
snprintf(settingspath, sizeof(settingspath), "%s/settings.json", tmpdir);
@@ -1907,8 +2003,10 @@ TEST(cli_remove_claude_hooks) {
* */
TEST(cli_upsert_gemini_hook_fresh) {
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-ghook-XXXXXX");
if (!cbm_mkdtemp(tmpdir)) SKIP("cbm_mkdtemp failed");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-ghook-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
char settingspath[512];
snprintf(settingspath, sizeof(settingspath), "%s/settings.json", tmpdir);
@@ -1926,14 +2024,16 @@ TEST(cli_upsert_gemini_hook_fresh) {
}
TEST(cli_upsert_gemini_hook_existing) {
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-ghook-XXXXXX");
if (!cbm_mkdtemp(tmpdir)) SKIP("cbm_mkdtemp failed");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-ghook-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
char settingspath[512];
snprintf(settingspath, sizeof(settingspath), "%s/settings.json", tmpdir);
write_test_file(settingspath,
"{\"hooks\":{\"BeforeTool\":[{\"matcher\":\"shell\","
"\"hooks\":[{\"type\":\"command\",\"command\":\"echo guard\"}]}]}}");
"{\"hooks\":{\"BeforeTool\":[{\"matcher\":\"shell\","
"\"hooks\":[{\"type\":\"command\",\"command\":\"echo guard\"}]}]}}");
int rc = cbm_upsert_gemini_hooks(settingspath);
ASSERT_EQ(rc, 0);
@@ -1950,12 +2050,15 @@ TEST(cli_upsert_gemini_hook_existing) {
}
TEST(cli_upsert_gemini_hook_replace) {
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-ghook-XXXXXX");
if (!cbm_mkdtemp(tmpdir)) SKIP("cbm_mkdtemp failed");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-ghook-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
char settingspath[512];
snprintf(settingspath, sizeof(settingspath), "%s/settings.json", tmpdir);
write_test_file(settingspath,
write_test_file(
settingspath,
"{\"hooks\":{\"BeforeTool\":[{\"matcher\":\"google_search|read_file|grep_search\","
"\"hooks\":[{\"type\":\"command\",\"command\":\"echo old-cmm\"}]}]}}");
@@ -1972,8 +2075,10 @@ TEST(cli_upsert_gemini_hook_replace) {
}
TEST(cli_remove_gemini_hooks) {
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-ghook-XXXXXX");
if (!cbm_mkdtemp(tmpdir)) SKIP("cbm_mkdtemp failed");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-ghook-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
char settingspath[512];
snprintf(settingspath, sizeof(settingspath), "%s/settings.json", tmpdir);
@@ -2009,8 +2114,10 @@ TEST(cli_skill_descriptions_directive) {
* */
TEST(cli_config_open_close) {
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-cfg-XXXXXX");
if (!cbm_mkdtemp(tmpdir)) SKIP("cbm_mkdtemp failed");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-cfg-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
cbm_config_t *cfg = cbm_config_open(tmpdir);
ASSERT_NOT_NULL(cfg);
@@ -2027,8 +2134,10 @@ TEST(cli_config_open_close) {
}
TEST(cli_config_get_set) {
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-cfg-XXXXXX");
if (!cbm_mkdtemp(tmpdir)) SKIP("cbm_mkdtemp failed");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-cfg-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
cbm_config_t *cfg = cbm_config_open(tmpdir);
ASSERT_NOT_NULL(cfg);
@@ -2050,8 +2159,10 @@ TEST(cli_config_get_set) {
}
TEST(cli_config_get_bool) {
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-cfg-XXXXXX");
if (!cbm_mkdtemp(tmpdir)) SKIP("cbm_mkdtemp failed");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-cfg-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
cbm_config_t *cfg = cbm_config_open(tmpdir);
ASSERT_NOT_NULL(cfg);
@@ -2080,8 +2191,10 @@ TEST(cli_config_get_bool) {
}
TEST(cli_config_get_int) {
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-cfg-XXXXXX");
if (!cbm_mkdtemp(tmpdir)) SKIP("cbm_mkdtemp failed");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-cfg-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
cbm_config_t *cfg = cbm_config_open(tmpdir);
ASSERT_NOT_NULL(cfg);
@@ -2101,8 +2214,10 @@ TEST(cli_config_get_int) {
}
TEST(cli_config_delete) {
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-cfg-XXXXXX");
if (!cbm_mkdtemp(tmpdir)) SKIP("cbm_mkdtemp failed");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-cfg-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
cbm_config_t *cfg = cbm_config_open(tmpdir);
ASSERT_NOT_NULL(cfg);
@@ -2120,8 +2235,10 @@ TEST(cli_config_delete) {
TEST(cli_config_persists) {
/* Values survive close + reopen */
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-cfg-XXXXXX");
if (!cbm_mkdtemp(tmpdir)) SKIP("cbm_mkdtemp failed");
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-cfg-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
SKIP("cbm_mkdtemp failed");
cbm_config_t *cfg = cbm_config_open(tmpdir);
ASSERT_NOT_NULL(cfg);
+320 -276
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -367,7 +367,7 @@ TEST(integ_mcp_get_architecture) {
PASS();
}
TEST(integ_mcp_trace_call_path) {
TEST(integ_mcp_trace_path) {
/* Trace outbound calls from Compute → should reach Add and Multiply */
char args[256];
snprintf(args, sizeof(args),
@@ -375,7 +375,7 @@ TEST(integ_mcp_trace_call_path) {
"\"direction\":\"outbound\",\"max_depth\":3}",
g_project);
char *resp = call_tool("trace_call_path", args);
char *resp = call_tool("trace_path", args);
ASSERT_NOT_NULL(resp);
/* Should find the function and show some path */
/* Either finds the function, or returns not found if name doesn't match exactly */
@@ -554,7 +554,7 @@ SUITE(integration) {
RUN_TEST(integ_mcp_query_graph_calls);
RUN_TEST(integ_mcp_get_graph_schema);
RUN_TEST(integ_mcp_get_architecture);
RUN_TEST(integ_mcp_trace_call_path);
RUN_TEST(integ_mcp_trace_path);
RUN_TEST(integ_mcp_index_status);
/* Store query validation */
+10 -10
View File
@@ -133,7 +133,7 @@ TEST(mcp_tools_list) {
ASSERT_NOT_NULL(strstr(json, "index_repository"));
ASSERT_NOT_NULL(strstr(json, "search_graph"));
ASSERT_NOT_NULL(strstr(json, "query_graph"));
ASSERT_NOT_NULL(strstr(json, "trace_call_path"));
ASSERT_NOT_NULL(strstr(json, "trace_path"));
ASSERT_NOT_NULL(strstr(json, "get_code_snippet"));
ASSERT_NOT_NULL(strstr(json, "get_graph_schema"));
ASSERT_NOT_NULL(strstr(json, "get_architecture"));
@@ -581,7 +581,8 @@ TEST(tool_search_code_no_project) {
"\"project\":\"nonexistent\"}}}");
ASSERT_NOT_NULL(resp);
/* No project indexed → error */
ASSERT_TRUE(strstr(resp, "not found") || strstr(resp, "not indexed") || strstr(resp, "required"));
ASSERT_TRUE(strstr(resp, "not found") || strstr(resp, "not indexed") ||
strstr(resp, "required"));
free(resp);
cbm_mcp_server_free(srv);
@@ -1536,9 +1537,9 @@ TEST(server_handle_tools_call_missing_name) {
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
/* tools/call with no tool name in params */
char *resp = cbm_mcp_server_handle(
srv, "{\"jsonrpc\":\"2.0\",\"id\":50,\"method\":\"tools/call\","
"\"params\":{\"arguments\":{}}}");
char *resp =
cbm_mcp_server_handle(srv, "{\"jsonrpc\":\"2.0\",\"id\":50,\"method\":\"tools/call\","
"\"params\":{\"arguments\":{}}}");
ASSERT_NOT_NULL(resp);
/* Should return error about unknown/missing tool */
ASSERT_NOT_NULL(strstr(resp, "\"id\":50"));
@@ -1577,11 +1578,10 @@ TEST(mcp_server_run_rapid_messages) {
ASSERT_EQ(pipe(fds), 0);
/* Write all 3 messages to the write end in one shot */
const char *msgs =
"{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\","
"\"params\":{\"protocolVersion\":\"2025-11-25\",\"capabilities\":{}}}\n"
"{\"jsonrpc\":\"2.0\",\"method\":\"notifications/initialized\"}\n"
"{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/list\",\"params\":{}}\n";
const char *msgs = "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\","
"\"params\":{\"protocolVersion\":\"2025-11-25\",\"capabilities\":{}}}\n"
"{\"jsonrpc\":\"2.0\",\"method\":\"notifications/initialized\"}\n"
"{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/list\",\"params\":{}}\n";
ssize_t written = write(fds[1], msgs, strlen(msgs));
ASSERT_TRUE(written > 0);
close(fds[1]); /* EOF signals end of input to the server */