diff --git a/src/mcp/mcp.c b/src/mcp/mcp.c index d3aa3bf3..3b61335c 100644 --- a/src/mcp/mcp.c +++ b/src/mcp/mcp.c @@ -430,7 +430,8 @@ static const tool_def_t TOOLS[] = { "{\"type\":\"object\",\"properties\":{\"project\":{\"type\":\"string\"},\"scope\":{\"type\":" "\"string\"},\"depth\":{\"type\":\"integer\",\"default\":2},\"base_branch\":{\"type\":" "\"string\",\"default\":\"main\"},\"since\":{\"type\":\"string\",\"description\":" - "\"Git ref or date to compare from (e.g. HEAD~5, v0.5.0, 2026-01-01)\"}},\"required\":" + "\"Git ref or tag to compare from (e.g. HEAD~5, v0.5.0). Diffs ...HEAD.\"}}," + "\"required\":" "[\"project\"]}"}, {"manage_adr", "Create or update Architecture Decision Records", @@ -3865,12 +3866,25 @@ static void detect_add_impacted_symbols(cbm_store_t *store, const char *project, static char *handle_detect_changes(cbm_mcp_server_t *srv, const char *args) { char *project = cbm_mcp_get_string_arg(args, "project"); char *base_branch = cbm_mcp_get_string_arg(args, "base_branch"); + char *since = cbm_mcp_get_string_arg(args, "since"); char *scope = cbm_mcp_get_string_arg(args, "scope"); int depth = cbm_mcp_get_int_arg(args, "depth", MCP_DEFAULT_BFS_DEPTH); /* scope: "files" = just changed files, "symbols" = files + symbols (default) */ bool want_symbols = !scope || strcmp(scope, "symbols") == 0 || strcmp(scope, "impact") == 0; + /* `since` (e.g. "HEAD~10", "v0.5.0") is the documented diff base but was + * previously parsed and never used: it takes precedence over base_branch. + * Route it through base_branch so the shared shell-arg validation and the + * existing `...HEAD` (three-dot) diff apply unchanged — `since` thus + * adopts the same merge-base semantics base_branch already uses. */ + if (since && since[0]) { + free(base_branch); + base_branch = since; /* transfer ownership */ + since = NULL; + } + free(since); /* no-op after the swap (since is NULL); frees it otherwise */ + if (!base_branch) { base_branch = heap_strdup("main"); } diff --git a/tests/test_incremental.c b/tests/test_incremental.c index c210d543..5a082705 100644 --- a/tests/test_incremental.c +++ b/tests/test_incremental.c @@ -1763,6 +1763,34 @@ TEST(tool_detect_changes_custom_branch) { PASS(); } +/* Regression: `since` was advertised in the schema but ignored by the handler; + * it must be honored as the diff base. Fixture is a --depth=1 shallow clone, so + * HEAD~N won't resolve — use HEAD for a valid (empty) diff. */ +TEST(tool_detect_changes_since) { + double ms; + char *r = call_tool_timed("detect_changes", &ms, "{\"project\":\"%s\",\"since\":\"HEAD\"}", + g_project); + TOOL_OK(r, ms); + ASSERT(resp_has_key(r, "changed_files")); + free(r); + PASS(); +} + +/* Regression: `since` must take precedence over base_branch. A valid since plus a + * bogus base_branch must still succeed (proving since won) and must not reference + * the bogus branch. */ +TEST(tool_detect_changes_since_precedence) { + double ms; + char *r = call_tool_timed( + "detect_changes", &ms, + "{\"project\":\"%s\",\"since\":\"HEAD\",\"base_branch\":\"no-such-branch-xyz\"}", + g_project); + TOOL_OK(r, ms); + ASSERT(strstr(r, "no-such-branch-xyz") == NULL); + free(r); + PASS(); +} + TEST(tool_detect_changes_depth) { double ms; char *r = call_tool_timed("detect_changes", &ms, "{\"project\":\"%s\",\"depth\":5}", g_project); @@ -2956,6 +2984,8 @@ SUITE(incremental) { /* Phase 15: detect_changes */ RUN_TEST(tool_detect_changes_default); RUN_TEST(tool_detect_changes_custom_branch); + RUN_TEST(tool_detect_changes_since); + RUN_TEST(tool_detect_changes_since_precedence); RUN_TEST(tool_detect_changes_depth); /* Phase 16: manage_adr */