From ec83b08c2ec70bc9e4ed62e16119c242e975aef6 Mon Sep 17 00:00:00 2001 From: Andrew Hundt Date: Wed, 1 Jul 2026 16:15:51 -0400 Subject: [PATCH] feat(mcp): report incremental containment reasons Add pipeline-owned publish_reason metadata and surface it in index_repository responses when incremental indexing falls back to containment. The reason is set on existing exact-route skip/fallback paths and cleared on exact publish/full run reset, so MCP and CLI JSON callers can distinguish a safe containment rebuild from an opaque incremental result. Update the affected-frontier benchmark harness to prefer structured publish_reason while keeping stderr parsing for older binaries. Add focused pipeline assertions and an MCP tool test for incremental_containment with missing_existing_ownership. Validated with git diff --check, bash scripts/check-source-safety.sh, bash scripts/test-source-safety.sh, python3 -m py_compile scripts/benchmark-incremental-speed.py, escalated make -j8 -f Makefile.cbm build/c/test-runner, escalated CBM_ONLY_SUITE=pipeline build/c/test-runner, escalated CBM_ONLY_SUITE=mcp build/c/test-runner, escalated make -j8 -f Makefile.cbm cbm, and escalated persistent MCP affected-frontier matrix 8/8 pass. Signed-off-by: Andrew Hundt --- scripts/benchmark-incremental-speed.py | 8 ++- src/mcp/mcp.c | 4 ++ src/pipeline/pipeline.c | 17 ++++++ src/pipeline/pipeline.h | 1 + src/pipeline/pipeline_incremental.c | 40 ++++++++++--- src/pipeline/pipeline_internal.h | 1 + tests/test_mcp.c | 77 ++++++++++++++++++++++++++ tests/test_pipeline.c | 2 + 8 files changed, 141 insertions(+), 9 deletions(-) diff --git a/scripts/benchmark-incremental-speed.py b/scripts/benchmark-incremental-speed.py index 4d5630195..cde28d030 100755 --- a/scripts/benchmark-incremental-speed.py +++ b/scripts/benchmark-incremental-speed.py @@ -274,6 +274,11 @@ def response_publish_kind(data: dict[str, Any]) -> str: return publish_kind if isinstance(publish_kind, str) else "" +def response_publish_reason(data: dict[str, Any]) -> str: + publish_reason = data.get("publish_reason") + return publish_reason if isinstance(publish_reason, str) else "" + + def is_incremental_publish_kind(publish_kind: str) -> bool: return publish_kind in { PUBLISH_INCREMENTAL_NOOP, @@ -337,6 +342,7 @@ def build_index_result( "incremental_done": parse_logged_elapsed_ms(stderr, "incremental.done"), } indexed_ms = indexed_work_elapsed_ms(logged_elapsed_ms) + publish_reason = response_publish_reason(data) result: dict[str, Any] = { "elapsed_ms": elapsed_ms_int, "indexed_work_elapsed_ms": indexed_ms, @@ -357,7 +363,7 @@ def build_index_result( or is_incremental_publish_kind(publish_kind), }, "logged_elapsed_ms": logged_elapsed_ms, - "exact_reason": parse_exact_reason(stderr), + "exact_reason": publish_reason or parse_exact_reason(stderr), "stderr_tail": log_tail(stderr), } if include_logs: diff --git a/src/mcp/mcp.c b/src/mcp/mcp.c index 25bcb5d72..7798600b4 100644 --- a/src/mcp/mcp.c +++ b/src/mcp/mcp.c @@ -4969,6 +4969,7 @@ static char *handle_index_repository(cbm_mcp_server_t *srv, const char *args) { int rc = cbm_pipeline_run(p); bool graph_changed = cbm_pipeline_graph_changed(p); cbm_pipeline_publish_kind_t publish_kind = cbm_pipeline_publish_kind(p); + const char *publish_reason = cbm_pipeline_publish_reason(p); srv->active_pipeline = NULL; cbm_pipeline_unlock(); @@ -4997,6 +4998,9 @@ static char *handle_index_repository(cbm_mcp_server_t *srv, const char *args) { yyjson_mut_obj_add_str(doc, root, "status", rc == 0 ? "indexed" : "error"); yyjson_mut_obj_add_str(doc, root, "publish_kind", cbm_pipeline_publish_kind_name(publish_kind)); + if (publish_reason && publish_reason[0]) { + yyjson_mut_obj_add_str(doc, root, "publish_reason", publish_reason); + } yyjson_mut_obj_add_bool(doc, root, "graph_changed", graph_changed); if (rc == 0) { diff --git a/src/pipeline/pipeline.c b/src/pipeline/pipeline.c index ccf980196..247f0bb3d 100644 --- a/src/pipeline/pipeline.c +++ b/src/pipeline/pipeline.c @@ -118,6 +118,7 @@ struct cbm_pipeline { int committed_edges; bool graph_changed; cbm_pipeline_publish_kind_t publish_kind; + char *publish_reason; }; /* ── Global pkgmap (one active pipeline at a time) ─────────────── */ @@ -189,6 +190,7 @@ cbm_pipeline_t *cbm_pipeline_new(const char *repo_path, const char *db_path, p->committed_edges = -1; p->graph_changed = false; p->publish_kind = CBM_PIPELINE_PUBLISH_NONE; + p->publish_reason = NULL; atomic_init(&p->cancelled, 0); return p; @@ -325,6 +327,7 @@ void cbm_pipeline_free(cbm_pipeline_t *p) { free(p->repo_path); free(p->db_path); free(p->project_name); + free(p->publish_reason); cbm_discover_free_excluded(p->excluded_dirs, p->excluded_count); p->excluded_dirs = NULL; p->excluded_count = 0; @@ -403,6 +406,10 @@ cbm_pipeline_publish_kind_t cbm_pipeline_publish_kind(const cbm_pipeline_t *p) { return p ? p->publish_kind : CBM_PIPELINE_PUBLISH_NONE; } +const char *cbm_pipeline_publish_reason(const cbm_pipeline_t *p) { + return p ? p->publish_reason : NULL; +} + const char *cbm_pipeline_publish_kind_name(cbm_pipeline_publish_kind_t kind) { switch (kind) { case CBM_PIPELINE_PUBLISH_NONE: @@ -432,6 +439,15 @@ void cbm_pipeline_set_publish_kind(cbm_pipeline_t *p, cbm_pipeline_publish_kind_ } } +void cbm_pipeline_set_publish_reason(cbm_pipeline_t *p, const char *reason) { + if (!p) { + return; + } + char *next = (reason && reason[0]) ? cbm_strdup(reason) : NULL; + free(p->publish_reason); + p->publish_reason = next; +} + static bool resolve_db_path_buf(const cbm_pipeline_t *p, char *path, size_t path_sz) { if (!p || !path || path_sz == 0) { return false; @@ -1272,6 +1288,7 @@ int cbm_pipeline_run(cbm_pipeline_t *p) { } p->graph_changed = false; p->publish_kind = CBM_PIPELINE_PUBLISH_NONE; + cbm_pipeline_set_publish_reason(p, NULL); p->committed_nodes = -1; p->committed_edges = -1; diff --git a/src/pipeline/pipeline.h b/src/pipeline/pipeline.h index 23646355b..c41c6f137 100644 --- a/src/pipeline/pipeline.h +++ b/src/pipeline/pipeline.h @@ -142,6 +142,7 @@ bool cbm_pipeline_graph_changed(const cbm_pipeline_t *p); * that need derived-view policy decisions; it does not change graph contents. */ cbm_pipeline_publish_kind_t cbm_pipeline_publish_kind(const cbm_pipeline_t *p); const char *cbm_pipeline_publish_kind_name(cbm_pipeline_publish_kind_t kind); +const char *cbm_pipeline_publish_reason(const cbm_pipeline_t *p); /* ── Index lock (prevents concurrent pipeline runs on same DB) ──── */ diff --git a/src/pipeline/pipeline_incremental.c b/src/pipeline/pipeline_incremental.c index c6aa4f65e..3ac0eb939 100644 --- a/src/pipeline/pipeline_incremental.c +++ b/src/pipeline/pipeline_incremental.c @@ -881,6 +881,7 @@ static int incr_try_exact_delete_route(cbm_pipeline_t *p, cbm_store_t *store, co const char *rel_path = deleted[0]; if (!rel_path || !rel_path[0]) { + cbm_pipeline_set_publish_reason(p, "missing_rel_path"); cbm_log_info("incremental.exact.delete.fallback", "reason", "missing_rel_path"); return CBM_STORE_OK; } @@ -901,6 +902,7 @@ static int incr_try_exact_delete_route(cbm_pipeline_t *p, cbm_store_t *store, co CBM_PIPELINE_EXACT_DELTA_MAX_AFFECTED_PATHS, &plan); if (rc != CBM_STORE_OK || plan.route != CBM_PIPELINE_DELTA_ROUTE_EXACT_CANDIDATE) { + cbm_pipeline_set_publish_reason(p, plan.reason ? plan.reason : "plan_error"); cbm_log_info("incremental.exact.delete.fallback", "reason", plan.reason ? plan.reason : "plan_error"); cbm_pipeline_file_delta_plan_free(&plan); @@ -911,6 +913,7 @@ static int incr_try_exact_delete_route(cbm_pipeline_t *p, cbm_store_t *store, co int64_t generation = 0; rc = cbm_store_reserve_index_generation(store, project, NULL, NULL, &generation); if (rc != CBM_STORE_OK || generation <= 0) { + cbm_pipeline_set_publish_reason(p, "reserve_generation"); cbm_log_info("incremental.exact.delete.fallback", "reason", "reserve_generation", "rc", itoa_buf_incr(rc)); return CBM_STORE_OK; @@ -921,6 +924,7 @@ static int incr_try_exact_delete_route(cbm_pipeline_t *p, cbm_store_t *store, co CBM_PIPELINE_EXACT_DELTA_MAX_AFFECTED_PATHS, &plan); if (rc != CBM_STORE_OK || plan.route != CBM_PIPELINE_DELTA_ROUTE_EXACT_CANDIDATE) { incr_mark_generation_failed(store, project, generation); + cbm_pipeline_set_publish_reason(p, plan.reason ? plan.reason : "apply_error"); cbm_log_info("incremental.exact.delete.fallback", "reason", plan.reason ? plan.reason : "apply_error"); cbm_pipeline_file_delta_plan_free(&plan); @@ -932,6 +936,7 @@ static int incr_try_exact_delete_route(cbm_pipeline_t *p, cbm_store_t *store, co cbm_store_count_edges(store, project)); cbm_pipeline_set_graph_changed(p, true); cbm_pipeline_set_publish_kind(p, CBM_PIPELINE_PUBLISH_INCREMENTAL_EXACT); + cbm_pipeline_set_publish_reason(p, NULL); if (cbm_pipeline_repo_path(p) && cbm_artifact_exists(cbm_pipeline_repo_path(p))) { (void)cbm_artifact_export(db_path, cbm_pipeline_repo_path(p), project, CBM_ARTIFACT_FAST); } @@ -954,14 +959,15 @@ static int incr_try_exact_upsert_route(cbm_pipeline_t *p, cbm_store_t *store, co if (deleted_count != 0 || changed_count > CBM_PIPELINE_EXACT_DELTA_MAX_CHANGED_PATHS || cbm_pipeline_get_mode(p) < CBM_MODE_FAST || changed_count > CBM_PIPELINE_EXACT_DELTA_MAX_AFFECTED_PATHS) { - cbm_log_info("incremental.exact.skip", "reason", - deleted_count != 0 - ? "has_deletes" - : (changed_count > CBM_PIPELINE_EXACT_DELTA_MAX_CHANGED_PATHS - ? "changed_batch_too_large" - : (cbm_pipeline_get_mode(p) < CBM_MODE_FAST - ? "global_derived_edges" - : "frontier_too_large"))); + const char *reason = + deleted_count != 0 + ? "has_deletes" + : (changed_count > CBM_PIPELINE_EXACT_DELTA_MAX_CHANGED_PATHS + ? "changed_batch_too_large" + : (cbm_pipeline_get_mode(p) < CBM_MODE_FAST ? "global_derived_edges" + : "frontier_too_large")); + cbm_pipeline_set_publish_reason(p, reason); + cbm_log_info("incremental.exact.skip", "reason", reason); return CBM_STORE_OK; } @@ -983,11 +989,13 @@ static int incr_try_exact_upsert_route(cbm_pipeline_t *p, cbm_store_t *store, co scratch = cbm_gbuf_new(project, cbm_pipeline_repo_path(p)); registry = cbm_registry_new(); if (!changed_paths || !deltas || !delta_ptrs || !result_cache || !scratch || !registry) { + cbm_pipeline_set_publish_reason(p, "alloc"); cbm_log_info("incremental.exact.fallback", "reason", "alloc"); goto cleanup; } for (int i = 0; i < changed_count; i++) { if (!changed_files[i].rel_path) { + cbm_pipeline_set_publish_reason(p, "missing_rel_path"); cbm_log_info("incremental.exact.fallback", "reason", "missing_rel_path"); goto cleanup; } @@ -999,6 +1007,7 @@ static int incr_try_exact_upsert_route(cbm_pipeline_t *p, cbm_store_t *store, co store, scratch, registry, project, changed_paths, changed_count); CBM_PROF_END_N("incremental_exact", "1_seed_scratch", t_exact_seed, changed_count); if (rc != CBM_STORE_OK) { + cbm_pipeline_set_publish_reason(p, "scratch_seed"); cbm_log_info("incremental.exact.fallback", "reason", "scratch_seed", "rc", itoa_buf_incr(rc)); goto cleanup; @@ -1030,6 +1039,7 @@ static int incr_try_exact_upsert_route(cbm_pipeline_t *p, cbm_store_t *store, co rc = cbm_pipeline_ensure_file_structure(scratch, project, structure_root_qn, changed_files[i].rel_path, NULL); if (rc != 0) { + cbm_pipeline_set_publish_reason(p, "ensure_structure"); cbm_log_info("incremental.exact.fallback", "reason", "ensure_structure", "rc", itoa_buf_incr(rc)); goto cleanup; @@ -1041,6 +1051,7 @@ static int incr_try_exact_upsert_route(cbm_pipeline_t *p, cbm_store_t *store, co CBM_PROF_END_N("incremental_exact", "3_extract_resolve", t_exact_extract_resolve, changed_count); if (rc != 0) { + cbm_pipeline_set_publish_reason(p, "extract_resolve"); cbm_log_info("incremental.exact.fallback", "reason", "extract_resolve", "rc", itoa_buf_incr(rc)); goto cleanup; @@ -1049,6 +1060,7 @@ static int incr_try_exact_upsert_route(cbm_pipeline_t *p, cbm_store_t *store, co rc = cbm_pipeline_pass_k8s(&ctx, changed_files, changed_count); CBM_PROF_END_N("incremental_exact", "4_k8s", t_exact_k8s, changed_count); if (rc != 0) { + cbm_pipeline_set_publish_reason(p, "k8s"); cbm_log_info("incremental.exact.fallback", "reason", "k8s", "rc", itoa_buf_incr(rc)); goto cleanup; } @@ -1056,6 +1068,7 @@ static int incr_try_exact_upsert_route(cbm_pipeline_t *p, cbm_store_t *store, co rc = run_postpasses(&ctx, changed_files, changed_count, project); CBM_PROF_END_N("incremental_exact", "5_postpasses", t_exact_postpasses, changed_count); if (rc != 0) { + cbm_pipeline_set_publish_reason(p, "postpasses"); cbm_log_info("incremental.exact.fallback", "reason", "postpasses", "rc", itoa_buf_incr(rc)); goto cleanup; @@ -1067,6 +1080,7 @@ static int incr_try_exact_upsert_route(cbm_pipeline_t *p, cbm_store_t *store, co rc = cbm_pipeline_pass_httplinks(&ctx); CBM_PROF_END_N("incremental_exact", "7_httplinks", t_exact_httplinks, changed_count); if (rc != 0) { + cbm_pipeline_set_publish_reason(p, "httplinks"); cbm_log_info("incremental.exact.fallback", "reason", "httplinks", "rc", itoa_buf_incr(rc)); goto cleanup; @@ -1080,6 +1094,7 @@ static int incr_try_exact_upsert_route(cbm_pipeline_t *p, cbm_store_t *store, co rc = cbm_pipeline_build_file_delta_from_gbuf(scratch, project, changed_files[i].rel_path, CBM_PIPELINE_COMPAT_GENERATION, &deltas[i]); if (rc != CBM_STORE_OK) { + cbm_pipeline_set_publish_reason(p, "build_delta"); cbm_log_info("incremental.exact.fallback", "reason", "build_delta", "rc", itoa_buf_incr(rc)); goto cleanup; @@ -1088,6 +1103,7 @@ static int incr_try_exact_upsert_route(cbm_pipeline_t *p, cbm_store_t *store, co &changed_files[i], pass_fingerprint); if (rc != CBM_STORE_OK) { + cbm_pipeline_set_publish_reason(p, "metadata"); cbm_log_info("incremental.exact.fallback", "reason", "metadata", "rc", itoa_buf_incr(rc)); goto cleanup; @@ -1101,6 +1117,7 @@ static int incr_try_exact_upsert_route(cbm_pipeline_t *p, cbm_store_t *store, co CBM_PIPELINE_EXACT_DELTA_MAX_AFFECTED_PATHS, &plan); CBM_PROF_END_N("incremental_exact", "10_plan_delta", t_exact_plan, changed_count); if (rc != CBM_STORE_OK || plan.route != CBM_PIPELINE_DELTA_ROUTE_EXACT_CANDIDATE) { + cbm_pipeline_set_publish_reason(p, plan.reason ? plan.reason : "plan_error"); cbm_log_info("incremental.exact.fallback", "reason", plan.reason ? plan.reason : "plan_error"); goto cleanup; @@ -1111,6 +1128,7 @@ static int incr_try_exact_upsert_route(cbm_pipeline_t *p, cbm_store_t *store, co rc = cbm_store_reserve_index_generation(store, project, NULL, NULL, &generation); CBM_PROF_END("incremental_exact", "11_reserve_generation", t_exact_reserve); if (rc != CBM_STORE_OK || generation <= 0) { + cbm_pipeline_set_publish_reason(p, "reserve_generation"); cbm_log_info("incremental.exact.fallback", "reason", "reserve_generation", "rc", itoa_buf_incr(rc)); goto cleanup; @@ -1120,6 +1138,7 @@ static int incr_try_exact_upsert_route(cbm_pipeline_t *p, cbm_store_t *store, co rc = cbm_pipeline_file_delta_stamp_generation(&deltas[i], generation); if (rc != CBM_STORE_OK) { incr_mark_generation_failed(store, project, generation); + cbm_pipeline_set_publish_reason(p, "stamp_generation"); cbm_log_info("incremental.exact.fallback", "reason", "stamp_generation", "rc", itoa_buf_incr(rc)); goto cleanup; @@ -1133,6 +1152,7 @@ static int incr_try_exact_upsert_route(cbm_pipeline_t *p, cbm_store_t *store, co CBM_PROF_END_N("incremental_exact", "13_apply_delta", t_exact_apply, changed_count); if (rc != CBM_STORE_OK || plan.route != CBM_PIPELINE_DELTA_ROUTE_EXACT_CANDIDATE) { incr_mark_generation_failed(store, project, generation); + cbm_pipeline_set_publish_reason(p, plan.reason ? plan.reason : "apply_error"); cbm_log_info("incremental.exact.fallback", "reason", plan.reason ? plan.reason : "apply_error"); goto cleanup; @@ -1142,6 +1162,7 @@ static int incr_try_exact_upsert_route(cbm_pipeline_t *p, cbm_store_t *store, co cbm_store_count_edges(store, project)); cbm_pipeline_set_graph_changed(p, true); cbm_pipeline_set_publish_kind(p, CBM_PIPELINE_PUBLISH_INCREMENTAL_EXACT); + cbm_pipeline_set_publish_reason(p, NULL); if (cbm_pipeline_repo_path(p) && cbm_artifact_exists(cbm_pipeline_repo_path(p))) { (void)cbm_artifact_export(db_path, cbm_pipeline_repo_path(p), project, CBM_ARTIFACT_FAST); } @@ -1526,6 +1547,9 @@ int cbm_pipeline_run_incremental(cbm_pipeline_t *p, const char *db_path, cbm_fil if (persist_rc == 0) { cbm_pipeline_set_graph_changed(p, true); cbm_pipeline_set_publish_kind(p, CBM_PIPELINE_PUBLISH_INCREMENTAL_CONTAINMENT); + if (!cbm_pipeline_publish_reason(p)) { + cbm_pipeline_set_publish_reason(p, "containment_rebuild"); + } } incr_classification_free(&cls); cbm_gbuf_free(existing); diff --git a/src/pipeline/pipeline_internal.h b/src/pipeline/pipeline_internal.h index 51a3871d3..d2ec3bb4e 100644 --- a/src/pipeline/pipeline_internal.h +++ b/src/pipeline/pipeline_internal.h @@ -758,6 +758,7 @@ atomic_int *cbm_pipeline_cancelled_ptr(cbm_pipeline_t *p); void cbm_pipeline_set_committed_counts(cbm_pipeline_t *p, int nodes, int edges); void cbm_pipeline_set_graph_changed(cbm_pipeline_t *p, bool changed); void cbm_pipeline_set_publish_kind(cbm_pipeline_t *p, cbm_pipeline_publish_kind_t kind); +void cbm_pipeline_set_publish_reason(cbm_pipeline_t *p, const char *reason); /* Parse a gRPC stub call "." into the canonical proto * service name + method. Returns true ONLY when a recognized gRPC stub/client diff --git a/tests/test_mcp.c b/tests/test_mcp.c index efddd3474..46223ce2b 100644 --- a/tests/test_mcp.c +++ b/tests/test_mcp.c @@ -10,6 +10,7 @@ #include "test_framework.h" #include #include +#include #include #include #include @@ -1647,6 +1648,81 @@ TEST(tool_index_repository_missing_path) { PASS(); } +TEST(tool_index_repository_reports_incremental_containment_reason) { + char *repo_tmp = th_mktempdir("cbm_mcp_publish_reason_repo"); + if (!repo_tmp) { + PASS(); + } + char repo[CBM_PATH_MAX]; + int n = snprintf(repo, sizeof(repo), "%s", repo_tmp); + ASSERT(n >= 0 && (size_t)n < sizeof(repo)); + + char *cache_tmp = th_mktempdir("cbm_mcp_publish_reason_cache"); + ASSERT_NOT_NULL(cache_tmp); + char cache[CBM_PATH_MAX]; + n = snprintf(cache, sizeof(cache), "%s", cache_tmp); + ASSERT(n >= 0 && (size_t)n < sizeof(cache)); + + const char *saved = getenv("CBM_CACHE_DIR"); + char *saved_copy = saved ? cbm_strdup(saved) : NULL; + cbm_setenv("CBM_CACHE_DIR", cache, 1); + + cbm_config_t *cfg = cbm_config_open(cache); + ASSERT_NOT_NULL(cfg); + ASSERT_EQ(cbm_config_set(cfg, CBM_CONFIG_INCREMENTAL_REINDEX, "always"), 0); + + ASSERT_EQ(th_write_file(TH_PATH(repo, "go.mod"), "module example.com/pubreason\n\ngo 1.22\n"), + 0); + ASSERT_EQ(th_write_file(TH_PATH(repo, "main.go"), "package main\n\nfunc main() {}\n"), 0); + + cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL); + ASSERT_NOT_NULL(srv); + cbm_mcp_server_set_config(srv, cfg); + + char req[CBM_SZ_4K]; + n = snprintf(req, sizeof(req), + "{\"jsonrpc\":\"2.0\",\"id\":41,\"method\":\"tools/call\"," + "\"params\":{\"name\":\"index_repository\"," + "\"arguments\":{\"repo_path\":\"%s\",\"mode\":\"fast\"}}}", + repo); + ASSERT(n >= 0 && (size_t)n < sizeof(req)); + char *resp = cbm_mcp_server_handle(srv, req); + ASSERT_NOT_NULL(resp); + ASSERT_NOT_NULL(strstr(resp, "indexed")); + free(resp); + + ASSERT_EQ(th_write_file(TH_PATH(repo, "pkg/file_created.go"), + "package pkg\n\nfunc Created() int {\n\treturn 7\n}\n"), + 0); + + n = snprintf(req, sizeof(req), + "{\"jsonrpc\":\"2.0\",\"id\":42,\"method\":\"tools/call\"," + "\"params\":{\"name\":\"index_repository\"," + "\"arguments\":{\"repo_path\":\"%s\",\"mode\":\"fast\"}}}", + repo); + ASSERT(n >= 0 && (size_t)n < sizeof(req)); + resp = cbm_mcp_server_handle(srv, req); + ASSERT_NOT_NULL(resp); + char *inner = extract_text_content(resp); + ASSERT_NOT_NULL(inner); + ASSERT_NOT_NULL(strstr(inner, "\"publish_kind\":\"incremental_containment\"")); + ASSERT_NOT_NULL(strstr(inner, "\"publish_reason\":\"missing_existing_ownership\"")); + + free(inner); + free(resp); + cbm_mcp_server_free(srv); + cbm_config_close(cfg); + if (saved_copy) { + cbm_setenv("CBM_CACHE_DIR", saved_copy, 1); + free(saved_copy); + } else { + cbm_unsetenv("CBM_CACHE_DIR"); + } + th_cleanup(repo); + th_cleanup(cache); + PASS(); +} + TEST(tool_get_code_snippet_missing_qn) { cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL); @@ -3457,6 +3533,7 @@ SUITE(mcp) { /* Pipeline-dependent tool handlers */ RUN_TEST(tool_index_repository_missing_path); + RUN_TEST(tool_index_repository_reports_incremental_containment_reason); RUN_TEST(tool_get_code_snippet_missing_qn); RUN_TEST(tool_get_code_snippet_not_found); RUN_TEST(tool_search_code_missing_pattern); diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index daa9eedb5..f5b517533 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -8694,6 +8694,7 @@ TEST(incremental_fast_falls_back_for_inbound_transitive_complexity_and_matches_f ASSERT(strstr(logs, "msg=incremental.exact.fallback reason=inbound_edges_require_full") != NULL); ASSERT_EQ(cbm_pipeline_publish_kind(p), CBM_PIPELINE_PUBLISH_INCREMENTAL_CONTAINMENT); + ASSERT_STR_EQ(cbm_pipeline_publish_reason(p), "inbound_edges_require_full"); cbm_pipeline_free(p); char diff_err[CBM_SZ_8K] = {0}; @@ -8760,6 +8761,7 @@ TEST(incremental_fast_three_file_batch_falls_back_to_full_rebuild_parity) { cbm_pipeline_apply_config(p, cfg); ASSERT_EQ(cbm_pipeline_run(p), 0); ASSERT_EQ(cbm_pipeline_publish_kind(p), CBM_PIPELINE_PUBLISH_INCREMENTAL_CONTAINMENT); + ASSERT_STR_EQ(cbm_pipeline_publish_reason(p), "changed_batch_too_large"); cbm_pipeline_free(p); ASSERT(pipeline_store_has_function_name(g_incr_dbpath, project, "NewMain"));