package pierre import ( "bytes" "context" "io" "strings" "testing" "git.schreifuchs.ch/schreifuchs/pierre-bot/internal/chatter" "github.com/google/go-cmp/cmp" ) // mockChat implements the ChatAdapter interface for testing. type mockChat struct{ callCount int } func (m *mockChat) GenerateStructured(ctx context.Context, msgs []chatter.Message, target interface{}) error { m.callCount++ if cSlice, ok := target.(*[]Comment); ok { *cSlice = []Comment{{File: "file.go", Line: 1, Message: "test comment"}} return nil } return nil } func (m *mockChat) GetProviderName() string { return "mock" } // mockGit implements the GitAdapter interface for testing. type mockGit struct{} func (g *mockGit) GetDiff(ctx context.Context, owner, repo string, prID int) (io.ReadCloser, error) { diff := "diff --git a/file1.go b/file1.go\n+line1\n" + "diff --git a/file2.go b/file2.go\n+line2\n" return io.NopCloser(bytes.NewReader([]byte(diff))), nil } func (g *mockGit) AddComment(ctx context.Context, owner, repo string, prID int, comment Comment) error { return nil } func TestSplitDiffIntoChunks(t *testing.T) { cases := []struct { name string diff string maxSize int wantChunks int // 0 means we don't assert exact count wantPrefixes []string checkRecombine bool }{ { name: "small diff", diff: "diff --git a/file1.txt b/file1.txt\n+added line\n", maxSize: 1000, wantChunks: 1, wantPrefixes: []string{"diff --git a/file1.txt"}, checkRecombine: true, }, { name: "multiple files", diff: "diff --git a/file1.txt b/file1.txt\n+added line 1\n" + "diff --git a/file2.txt b/file2.txt\n+added line 2\n", maxSize: 50, wantChunks: 2, wantPrefixes: []string{"diff --git a/file1.txt", "diff --git a/file2.txt"}, checkRecombine: false, }, { name: "large single file", diff: func() string { line := "+very long added line that will be repeated many times to exceed the chunk size\n" return "diff --git a/large.txt b/large.txt\n" + strings.Repeat(line, 200) }(), maxSize: 500, wantChunks: 0, wantPrefixes: nil, checkRecombine: true, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { chunks := splitDiffIntoChunks([]byte(tc.diff), tc.maxSize) if tc.wantChunks > 0 && len(chunks) != tc.wantChunks { t.Fatalf("expected %d chunks, got %d", tc.wantChunks, len(chunks)) } for i, prefix := range tc.wantPrefixes { if i >= len(chunks) { t.Fatalf("missing chunk %d for prefix check", i) } trimmed := strings.TrimPrefix(chunks[i], "\n") if !strings.HasPrefix(trimmed, prefix) { t.Fatalf("chunk %d does not start with expected prefix %q: %s", i, prefix, chunks[i]) } } for i, c := range chunks { if tc.maxSize > 0 && len(c) > tc.maxSize { t.Fatalf("chunk %d exceeds max size %d: %d", i, tc.maxSize, len(c)) } } if tc.checkRecombine { recombined := strings.Join(chunks, "") if diff := cmp.Diff(tc.diff, recombined); diff != "" { t.Fatalf("recombined diff differs:\n%s", diff) } } }) } } func TestJudgePR_ChunkAggregationAndDeduplication(t *testing.T) { chatMock := &mockChat{} svc := &Service{ maxChunkSize: 50, guidelines: nil, git: &mockGit{}, chat: chatMock, } diffReader, err := svc.git.GetDiff(context.Background(), "", "", 0) if err != nil { t.Fatalf("failed to get diff: %v", err) } defer diffReader.Close() comments, err := svc.judgePR(context.Background(), diffReader) if err != nil { t.Fatalf("judgePR error: %v", err) } if got, want := len(comments), 1; got != want { t.Fatalf("expected %d comment after deduplication, got %d", want, got) } if chatMock.callCount != 2 { t.Fatalf("expected mockChat to be called for each chunk (2), got %d", chatMock.callCount) } }