@@ -10237,6 +10237,73 @@ TEST(search_code_path_filter_prefilter_keeps_matches) {
1023710237 PASS();
1023810238}
1023910239
10240+ /* #2011: a grep record longer than the read buffer used to be split across two
10241+ * fgets calls, and the continuation was parsed as a fresh file:line:content
10242+ * record — inventing a file path out of matched content and a line number of 0.
10243+ * A single >2 KiB line containing colons reproduces it: the repository holds
10244+ * two matches, and the split used to report three. */
10245+ TEST(search_code_long_line_does_not_invent_matches) {
10246+ char tmp[512];
10247+ snprintf(tmp, sizeof(tmp), "/tmp/cbm_srch_longline_XXXXXX");
10248+ ASSERT_TRUE(cbm_mkdtemp(tmp) != NULL);
10249+
10250+ char big_path[768], normal_path[768];
10251+ snprintf(big_path, sizeof(big_path), "%s/big.js", tmp);
10252+ snprintf(normal_path, sizeof(normal_path), "%s/normal.js", tmp);
10253+
10254+ /* One line well over the 2 KiB read buffer, full of colons, with the
10255+ * needle at the very end so the match lands past the split point. */
10256+ FILE *fp = fopen(big_path, "w");
10257+ ASSERT_NOT_NULL(fp);
10258+ fprintf(fp, "var CFG=({");
10259+ for (int i = 0; i < 260; i++) {
10260+ fprintf(fp, "k%d:\"v%d\",", i, i);
10261+ }
10262+ fprintf(fp, "NEEDLEmarker:1});\n");
10263+ fclose(fp);
10264+
10265+ fp = fopen(normal_path, "w");
10266+ ASSERT_NOT_NULL(fp);
10267+ fprintf(fp, "const NEEDLEmarker = 42;\n");
10268+ fclose(fp);
10269+
10270+ cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
10271+ ASSERT_NOT_NULL(srv);
10272+ const char *proj = "longline-search";
10273+ cbm_mcp_server_set_project(srv, proj);
10274+ cbm_store_upsert_project(cbm_mcp_server_store(srv), proj, tmp);
10275+
10276+ char *resp = cbm_mcp_server_handle(
10277+ srv, "{\"jsonrpc\":\"2.0\",\"id\":96,\"method\":\"tools/call\","
10278+ "\"params\":{\"name\":\"search_code\","
10279+ "\"arguments\":{\"pattern\":\"NEEDLEmarker\",\"project\":\"longline-search\"}}}");
10280+ ASSERT_NOT_NULL(resp);
10281+ char *inner = extract_text_content(resp);
10282+ ASSERT_NOT_NULL(inner);
10283+
10284+ /* Exactly the two real matches — the split must not produce a third. */
10285+ int grep_matches = -1;
10286+ const char *g = strstr(inner, "\"total_grep_matches\":");
10287+ if (g) {
10288+ sscanf(g, "\"total_grep_matches\":%d", &grep_matches);
10289+ } else if ((g = strstr(inner, "total_grep_matches: ")) != NULL) {
10290+ sscanf(g, "total_grep_matches: %d", &grep_matches);
10291+ }
10292+ ASSERT_EQ(grep_matches, 2);
10293+
10294+ /* Every reported line number belongs to a real line: the fabricated row
10295+ * carried line 0, which no grep -n record can produce. */
10296+ ASSERT_TRUE(strstr(inner, "\"line\":0") == NULL);
10297+
10298+ free(inner);
10299+ free(resp);
10300+ cbm_mcp_server_free(srv);
10301+ unlink(big_path);
10302+ unlink(normal_path);
10303+ rmdir(tmp);
10304+ PASS();
10305+ }
10306+
1024010307/* PR #756 (distilled): path_filter matching ZERO indexed files. With the
1024110308 * prefilter the scoped filelist has 0 records, and handle_search_code now
1024210309 * skips the grep subprocess entirely (xargs on an empty filelist is
@@ -20044,6 +20111,7 @@ SUITE(mcp) {
2004420111 RUN_TEST(search_code_scoped_path_with_cjk_root_issue903);
2004520112#endif
2004620113 RUN_TEST(search_code_path_filter_prefilter_keeps_matches);
20114+ RUN_TEST(search_code_long_line_does_not_invent_matches);
2004720115 RUN_TEST(search_code_path_filter_matches_nothing);
2004820116 RUN_TEST(search_code_file_pattern_prefilter_boundaries);
2004920117 RUN_TEST(search_code_windows_scope_prefilter_removes_pipeline_filter);
0 commit comments