From 99de8a93b96f1d70ba5c43a5f236fbb62ad0fb1b Mon Sep 17 00:00:00 2001 From: Lukasz Raczylo Date: Sun, 21 Jun 2026 00:07:08 +0100 Subject: [PATCH] test(search): cover cross-file context attribution in parseOutput parseOutput's pendingFile guard attaches before-context only when it belongs to the same file as the following match. Add tests for both the cross-file leak guard (fileA context must not attach to a fileB match) and the same-file positive case. Test-only; no behavior change. --- internal/search/search_test.go | 45 ++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/internal/search/search_test.go b/internal/search/search_test.go index cbb25af..faacf2c 100644 --- a/internal/search/search_test.go +++ b/internal/search/search_test.go @@ -13,6 +13,51 @@ import ( "github.com/lukaszraczylo/mcp-filepuff/internal/config" ) +// parseOutput must not attach one file's pending before-context to a match in a +// different file (cross-file leak guard in the JSON stream parser). +func TestParseOutputCrossFileContextNotLeaked(t *testing.T) { + s := newTestSearcher(t) + var buf bytes.Buffer + // fileA emits a before-context with no following match in fileA; then fileB + // has a match. fileA's pending context must NOT attach to fileB's match. + buf.WriteString(`{"type":"context","data":{"path":{"text":"fileA.go"},"lines":{"text":"orphan a ctx\n"},"line_number":1}}` + "\n") + buf.WriteString(`{"type":"match","data":{"path":{"text":"fileB.go"},"lines":{"text":"hit B\n"},"line_number":5,"submatches":[{"match":{"text":"hit"},"start":0,"end":3}]}}` + "\n") + + res, err := s.parseOutput(&buf, 0) + if err != nil { + t.Fatalf("parseOutput: %v", err) + } + if len(res.Results) != 1 { + t.Fatalf("want 1 result, got %d", len(res.Results)) + } + if res.Results[0].File != "fileB.go" { + t.Errorf("result file = %q, want fileB.go", res.Results[0].File) + } + if len(res.Results[0].Context.Before) != 0 { + t.Errorf("fileA context leaked into fileB match: %v", res.Results[0].Context.Before) + } +} + +// parseOutput must attach a same-file before-context to the following match. +func TestParseOutputSameFileBeforeContext(t *testing.T) { + s := newTestSearcher(t) + var buf bytes.Buffer + buf.WriteString(`{"type":"context","data":{"path":{"text":"f.go"},"lines":{"text":"before line\n"},"line_number":4}}` + "\n") + buf.WriteString(`{"type":"match","data":{"path":{"text":"f.go"},"lines":{"text":"the match\n"},"line_number":5,"submatches":[{"match":{"text":"match"},"start":4,"end":9}]}}` + "\n") + + res, err := s.parseOutput(&buf, 0) + if err != nil { + t.Fatalf("parseOutput: %v", err) + } + if len(res.Results) != 1 { + t.Fatalf("want 1 result, got %d", len(res.Results)) + } + got := res.Results[0].Context.Before + if len(got) != 1 || got[0] != "before line" { + t.Errorf("same-file before-context not attached: %v", got) + } +} + // truncateLine must back off to a rune boundary so multibyte characters are // never split into invalid UTF-8. func TestTruncateLineRuneSafe(t *testing.T) {