From c4bec4a6b38ed66ccd97df06490e051d3d9d81cd Mon Sep 17 00:00:00 2001 From: kavish-19 <63698788+kavish-19@users.noreply.github.com> Date: Sat, 5 Sep 2026 22:54:00 +0530 Subject: [PATCH] test(mcp): over-long grep records never invent matches (#2011) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A grep record longer than the read buffer used to be split across two reads, and the continuation was parsed as a fresh file:line:content record — a fragment of matched content became a file path and the text after the next delimiter became a line number. An agent reading that result called get_code_snippet on a path that was never in the repository. The same split could also lose a real match, when the tail held fewer than two delimiters and the record was dropped instead. The production fix landed via #1597 (3c7427e): search_code now reads each record with cbm_getline into a growable buffer, so a record can no longer split. That closed #2011 as a side effect, and nothing on main guards the shape — the buffer could be reintroduced by a future rewrite of the read loop with no test to catch it. Add the regression test the fix never got. It writes a >2 KiB colon-laden line plus a normal one, calls search_code through cbm_mcp_server_handle, and asserts the repository's two real matches are reported as two — not three — and that no row carries the line 0 the fabricated record used to produce. Signed-off-by: kavish-19 <63698788+kavish-19@users.noreply.github.com> --- tests/test_mcp.c | 68 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/tests/test_mcp.c b/tests/test_mcp.c index f97eda09e..b45d34446 100644 --- a/tests/test_mcp.c +++ b/tests/test_mcp.c @@ -10118,6 +10118,73 @@ TEST(search_code_path_filter_prefilter_keeps_matches) { PASS(); } +/* #2011: a grep record longer than the read buffer used to be split across two + * fgets calls, and the continuation was parsed as a fresh file:line:content + * record — inventing a file path out of matched content and a line number of 0. + * A single >2 KiB line containing colons reproduces it: the repository holds + * two matches, and the split used to report three. */ +TEST(search_code_long_line_does_not_invent_matches) { + char tmp[512]; + snprintf(tmp, sizeof(tmp), "/tmp/cbm_srch_longline_XXXXXX"); + ASSERT_TRUE(cbm_mkdtemp(tmp) != NULL); + + char big_path[768], normal_path[768]; + snprintf(big_path, sizeof(big_path), "%s/big.js", tmp); + snprintf(normal_path, sizeof(normal_path), "%s/normal.js", tmp); + + /* One line well over the 2 KiB read buffer, full of colons, with the + * needle at the very end so the match lands past the split point. */ + FILE *fp = fopen(big_path, "w"); + ASSERT_NOT_NULL(fp); + fprintf(fp, "var CFG=({"); + for (int i = 0; i < 260; i++) { + fprintf(fp, "k%d:\"v%d\",", i, i); + } + fprintf(fp, "NEEDLEmarker:1});\n"); + fclose(fp); + + fp = fopen(normal_path, "w"); + ASSERT_NOT_NULL(fp); + fprintf(fp, "const NEEDLEmarker = 42;\n"); + fclose(fp); + + cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL); + ASSERT_NOT_NULL(srv); + const char *proj = "longline-search"; + cbm_mcp_server_set_project(srv, proj); + cbm_store_upsert_project(cbm_mcp_server_store(srv), proj, tmp); + + char *resp = cbm_mcp_server_handle( + srv, "{\"jsonrpc\":\"2.0\",\"id\":96,\"method\":\"tools/call\"," + "\"params\":{\"name\":\"search_code\"," + "\"arguments\":{\"pattern\":\"NEEDLEmarker\",\"project\":\"longline-search\"}}}"); + ASSERT_NOT_NULL(resp); + char *inner = extract_text_content(resp); + ASSERT_NOT_NULL(inner); + + /* Exactly the two real matches — the split must not produce a third. */ + int grep_matches = -1; + const char *g = strstr(inner, "\"total_grep_matches\":"); + if (g) { + sscanf(g, "\"total_grep_matches\":%d", &grep_matches); + } else if ((g = strstr(inner, "total_grep_matches: ")) != NULL) { + sscanf(g, "total_grep_matches: %d", &grep_matches); + } + ASSERT_EQ(grep_matches, 2); + + /* Every reported line number belongs to a real line: the fabricated row + * carried line 0, which no grep -n record can produce. */ + ASSERT_TRUE(strstr(inner, "\"line\":0") == NULL); + + free(inner); + free(resp); + cbm_mcp_server_free(srv); + unlink(big_path); + unlink(normal_path); + rmdir(tmp); + PASS(); +} + /* PR #756 (distilled): path_filter matching ZERO indexed files. With the * prefilter the scoped filelist has 0 records, and handle_search_code now * skips the grep subprocess entirely (xargs on an empty filelist is @@ -19923,6 +19990,7 @@ SUITE(mcp) { RUN_TEST(search_code_scoped_path_with_cjk_root_issue903); #endif RUN_TEST(search_code_path_filter_prefilter_keeps_matches); + RUN_TEST(search_code_long_line_does_not_invent_matches); RUN_TEST(search_code_path_filter_matches_nothing); RUN_TEST(search_code_file_pattern_prefilter_boundaries); RUN_TEST(search_code_windows_scope_prefilter_removes_pipeline_filter);