Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions src/mcp/mcp.c
Original file line number Diff line number Diff line change
Expand Up @@ -9921,10 +9921,16 @@ static char *handle_cross_repo_mode(cbm_mcp_server_t *srv, const char *repo_path
free(lease_keys);
yyjson_doc_free(jdoc);

if (result.failed) {
if (result.no_targets || result.failed) {
free(project);
return cbm_mcp_text_result(
"cross-repo source or target project is missing, invalid, or not indexed", true);
result.no_targets
? "cross-repo-intelligence resolved zero target projects: no indexed project "
"other than the source matched target_projects. Index the other service first "
"(run list_projects to see what is indexed); existing cross-repo edges were "
"left unchanged."
: "cross-repo source or target project is missing, invalid, or not indexed",
true);
}

int total = result.http_edges + result.async_edges + result.channel_edges + result.grpc_edges +
Expand Down
18 changes: 17 additions & 1 deletion src/pipeline/pass_cross_repo.c
Original file line number Diff line number Diff line change
Expand Up @@ -1239,17 +1239,33 @@ cbm_cross_repo_result_t cbm_cross_repo_match_cancellable(const char *project,
result.failed = !result.cancelled;
return result;
}
int other_targets = 0;
for (int i = 0; i < resolved_count; i++) {
if (cr_cancel_requested(&run)) {
result.cancelled = true;
free_project_list(resolved, resolved_count);
return result;
}
if (strcmp(resolved[i], project) != 0 && !cr_project_exists(resolved[i])) {
if (strcmp(resolved[i], project) == 0) {
continue;
}
if (!cr_project_exists(resolved[i])) {
result.failed = true;
free_project_list(resolved, resolved_count);
return result;
}
other_targets++;
}
/* Nothing but the source itself to match against (a self-only list, or
* ["*"] in a store holding only the source). Reporting that as success
* with projects_scanned:0 is indistinguishable from "these services share
* no routes", and the source's existing CROSS_* edges would be wiped below
* with nothing to rebuild them. Fail before any write. (#1133) */
if (other_targets == 0) {
result.failed = true;
result.no_targets = true;
free_project_list(resolved, resolved_count);
return result;
}

/* Every input is known to exist before destructive source cleanup. The
Expand Down
1 change: 1 addition & 0 deletions src/pipeline/pass_cross_repo.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ typedef struct {
int projects_scanned;
double elapsed_ms;
bool failed; /* source/target validation, open, or allocation failed */
bool no_targets; /* failed: nothing but the source project resolved (#1133) */
bool cancelled; /* stopped at a bounded cancellation checkpoint */
bool partial_results; /* committed writes before cancellation were retained */
} cbm_cross_repo_result_t;
Expand Down
23 changes: 23 additions & 0 deletions tests/test_cli.c
Original file line number Diff line number Diff line change
Expand Up @@ -15228,6 +15228,28 @@ TEST(cli_build_args_json_array_flag_accepts_json_literal) {
PASS();
}

/* #1133: `index_repository --target-projects '["*"]'` (the form the help
* documents) must reach the cross-repo matcher as a one-element array, not as
* a single string holding the literal text -- that shape resolved to zero
* targets and returned a silent "success" with projects_scanned:0. */
TEST(cli_build_args_json_target_projects_literal_issue1133) {
char *err = NULL;
char *argv[] = {"--repo-path", "/r", "--mode", "cross-repo-intelligence",
"--target-projects", "[\"*\"]"};
char *json = cbm_cli_build_args_json("index_repository", 6, argv, &err);
ASSERT_NOT_NULL(json);
ASSERT_NULL(err);
ASSERT(strstr(json, "\"target_projects\":[\"*\"]") != NULL);
free(json);

char *argv2[] = {"--repo-path", "/r", "--target-projects", "[\"svc-a\",\"svc-b\"]"};
json = cbm_cli_build_args_json("index_repository", 4, argv2, &err);
ASSERT_NOT_NULL(json);
ASSERT(strstr(json, "\"target_projects\":[\"svc-a\",\"svc-b\"]") != NULL);
free(json);
PASS();
}

/* An unknown flag for a KNOWN tool must be rejected loudly, not silently
* typed as a string and dropped server-side (#997). GF1 eval: `trace_path
* --max-depth 1` was accepted, the real --depth stayed at default 3, and
Expand Down Expand Up @@ -16385,6 +16407,7 @@ SUITE(cli) {
RUN_TEST(cli_build_args_json_integer_flag_issue680);
RUN_TEST(cli_build_args_json_bare_boolean_issue680);
RUN_TEST(cli_build_args_json_array_flag_accepts_json_literal);
RUN_TEST(cli_build_args_json_target_projects_literal_issue1133);
RUN_TEST(cli_build_args_json_unknown_flag_rejected);
RUN_TEST(cli_build_args_json_repeated_array_issue680);
RUN_TEST(cli_build_args_json_kebab_to_snake_issue680);
Expand Down
54 changes: 54 additions & 0 deletions tests/test_cross_repo.c
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,58 @@ TEST(cross_repo_pre_cancel_preserves_existing_cross_edges) {
PASS();
}

/* #1133: a run whose targets resolve to nothing but the source project must
* fail explicitly. It used to report "success" with projects_scanned:0 --
* indistinguishable from "these services share no routes" -- and, worse, it
* had already wiped the source's existing CROSS_* edges before noticing there
* was nothing to match against. */
TEST(cross_repo_self_only_target_fails_and_keeps_edges_issue1133) {
cross_repo_fixture_t fixture;
bool setup =
cross_repo_fixture_begin(&fixture) &&
cross_repo_seed_http_pair(&fixture, "self-source", "self-target", "/self-only", "self");
if (!setup) {
cross_repo_fixture_end(&fixture);
FAIL("failed to seed self-only fixture");
}

const char *target = "self-target";
cbm_cross_repo_result_t initial = cbm_cross_repo_match("self-source", &target, 1);
int before = cross_repo_count_edges(&fixture, "self-source", "CROSS_HTTP_CALLS");
const char *self = "self-source";
cbm_cross_repo_result_t result = cbm_cross_repo_match("self-source", &self, 1);
int after = cross_repo_count_edges(&fixture, "self-source", "CROSS_HTTP_CALLS");
cross_repo_fixture_end(&fixture);

ASSERT_FALSE(initial.failed);
ASSERT_TRUE(before > 0);
ASSERT_TRUE(result.failed);
ASSERT_TRUE(result.no_targets);
ASSERT_EQ(result.projects_scanned, 0);
ASSERT_EQ(after, before);
PASS();
}

/* #1133: ["*"] in a store that holds only the source project resolves to zero
* targets -- same contract as an explicit self-only list. */
TEST(cross_repo_wildcard_with_no_other_project_fails_issue1133) {
cross_repo_fixture_t fixture;
if (!cross_repo_fixture_begin(&fixture) ||
!cross_repo_create_project(&fixture, "lonely-source")) {
cross_repo_fixture_end(&fixture);
FAIL("failed to create isolated source project");
}

const char *targets[] = {"*"};
cbm_cross_repo_result_t result = cbm_cross_repo_match("lonely-source", targets, 1);
cross_repo_fixture_end(&fixture);

ASSERT_TRUE(result.failed);
ASSERT_TRUE(result.no_targets);
ASSERT_EQ(result.projects_scanned, 0);
PASS();
}

/* Add the internal "<name>::missed" miss-graph row that indexing writes into
* the SAME db whenever a file parses partially. */
static bool cross_repo_add_missed_shadow(const cross_repo_fixture_t *fixture, const char *project) {
Expand Down Expand Up @@ -528,4 +580,6 @@ SUITE(cross_repo) {
RUN_TEST(cross_repo_failed_bidirectional_insert_is_not_counted);
RUN_TEST(cross_repo_cancel_mid_run_keeps_completed_target_and_stops_before_later_target);
RUN_TEST(cross_repo_pre_cancel_preserves_existing_cross_edges);
RUN_TEST(cross_repo_self_only_target_fails_and_keeps_edges_issue1133);
RUN_TEST(cross_repo_wildcard_with_no_other_project_fails_issue1133);
}
Loading