/* ©AngelaMos | 2026 source_test.go Tests for source/directory.go and source/git.go Tests: Directory source emits chunks for text files, skips binary extensions Directory exclude patterns suppress matching files Directory skips .git directory automatically Max file size limit skips oversized files Files longer than 50 lines split into multiple chunks with correct LineStart Context cancellation propagates from Chunks() as context.Canceled Directory String() returns "directory:" Git source emits chunks with CommitSHA and Author metadata from commit history Git staged-only mode scans the index rather than commit history Git depth limit restricts the number of commits scanned Git exclude patterns skip matching files from history Git String() returns "git:" splitIntoChunks produces 50-line windows with correct LineStart offsets isBinaryExt correctly classifies binary and text extension types */ package source import ( "context" "os" "path/filepath" "testing" "time" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing/object" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/CarterPerez-dev/portia/pkg/types" ) func collectChunks( t *testing.T, src Source, ) []types.Chunk { t.Helper() out := make(chan types.Chunk, 100) ctx := context.Background() errCh := make(chan error, 1) go func() { errCh <- src.Chunks(ctx, out) close(out) }() var chunks []types.Chunk for c := range out { chunks = append(chunks, c) } require.NoError(t, <-errCh) return chunks } func TestDirectoryChunks(t *testing.T) { t.Parallel() dir := t.TempDir() require.NoError(t, os.WriteFile( //nolint:gosec filepath.Join(dir, "config.py"), []byte(`password = "sk_live_4eC39HqLyjWDarjtT1zdp7dc"`+"\n"), 0o644, )) require.NoError(t, os.WriteFile( //nolint:gosec filepath.Join(dir, "logo.png"), []byte{0x89, 0x50, 0x4E, 0x47}, 0o644, )) src := NewDirectory(dir, 0, nil) chunks := collectChunks(t, src) require.Len(t, chunks, 1) assert.Equal(t, "config.py", chunks[0].FilePath) assert.Contains(t, chunks[0].Content, "sk_live_") assert.Equal(t, 1, chunks[0].LineStart) } func TestDirectoryExcludes(t *testing.T) { t.Parallel() dir := t.TempDir() require.NoError(t, os.WriteFile( //nolint:gosec filepath.Join(dir, "main.go"), []byte("package main\n"), 0o644, )) require.NoError(t, os.WriteFile( //nolint:gosec filepath.Join(dir, "secret.env"), []byte("API_KEY=test\n"), 0o644, )) src := NewDirectory(dir, 0, []string{"*.env"}) chunks := collectChunks(t, src) require.Len(t, chunks, 1) assert.Equal(t, "main.go", chunks[0].FilePath) } func TestDirectorySkipsDotGit(t *testing.T) { t.Parallel() dir := t.TempDir() gitDir := filepath.Join(dir, ".git", "objects") require.NoError(t, os.MkdirAll(gitDir, 0o755)) //nolint:gosec require.NoError(t, os.WriteFile( //nolint:gosec filepath.Join(gitDir, "data"), []byte("git internal data\n"), 0o644, )) require.NoError(t, os.WriteFile( //nolint:gosec filepath.Join(dir, "app.go"), []byte("package main\n"), 0o644, )) src := NewDirectory(dir, 0, nil) chunks := collectChunks(t, src) require.Len(t, chunks, 1) assert.Equal(t, "app.go", chunks[0].FilePath) } func TestDirectoryMaxSize(t *testing.T) { t.Parallel() dir := t.TempDir() require.NoError(t, os.WriteFile( //nolint:gosec filepath.Join(dir, "small.txt"), []byte("small\n"), 0o644, )) bigContent := make([]byte, 2*1024*1024) for i := range bigContent { bigContent[i] = 'x' } require.NoError(t, os.WriteFile( //nolint:gosec filepath.Join(dir, "big.txt"), bigContent, 0o644, )) src := NewDirectory(dir, 1024*1024, nil) chunks := collectChunks(t, src) require.Len(t, chunks, 1) assert.Equal(t, "small.txt", chunks[0].FilePath) } func TestDirectoryChunking(t *testing.T) { t.Parallel() dir := t.TempDir() var lines string for i := range 120 { lines += "line " + string(rune('A'+i%26)) + "\n" } require.NoError(t, os.WriteFile( //nolint:gosec filepath.Join(dir, "long.txt"), []byte(lines), 0o644, )) src := NewDirectory(dir, 0, nil) chunks := collectChunks(t, src) require.GreaterOrEqual(t, len(chunks), 2) assert.Equal(t, 1, chunks[0].LineStart) assert.Equal(t, 51, chunks[1].LineStart) } func TestDirectoryContextCancel(t *testing.T) { t.Parallel() dir := t.TempDir() for i := range 10 { name := filepath.Join(dir, "file"+string(rune('0'+i))+".txt") require.NoError(t, os.WriteFile( //nolint:gosec name, []byte("content\n"), 0o644, )) } ctx, cancel := context.WithCancel(context.Background()) cancel() src := NewDirectory(dir, 0, nil) out := make(chan types.Chunk, 100) err := src.Chunks(ctx, out) close(out) assert.ErrorIs(t, err, context.Canceled) } func TestDirectoryString(t *testing.T) { t.Parallel() src := NewDirectory("/tmp/test", 0, nil) assert.Equal(t, "directory:/tmp/test", src.String()) } func TestGitChunks(t *testing.T) { t.Parallel() dir := t.TempDir() repo, err := git.PlainInit(dir, false) require.NoError(t, err) wt, err := repo.Worktree() require.NoError(t, err) require.NoError(t, os.WriteFile( //nolint:gosec filepath.Join(dir, "secret.py"), []byte(`api_key = "sk_live_4eC39HqLyjWDarjtT1zdp7dc"`+"\n"), 0o644, )) _, err = wt.Add("secret.py") require.NoError(t, err) _, err = wt.Commit("add secret", &git.CommitOptions{ Author: &object.Signature{ Name: "Test", Email: "test@example.com", When: time.Now(), }, }) require.NoError(t, err) src := NewGit(dir, "", "", 0, false, 0, nil) chunks := collectChunks(t, src) require.GreaterOrEqual(t, len(chunks), 1) assert.Equal(t, "secret.py", chunks[0].FilePath) assert.Contains(t, chunks[0].Content, "sk_live_") assert.NotEmpty(t, chunks[0].CommitSHA) assert.Equal(t, "test@example.com", chunks[0].Author) } func TestGitStagedOnly(t *testing.T) { t.Parallel() dir := t.TempDir() repo, err := git.PlainInit(dir, false) require.NoError(t, err) wt, err := repo.Worktree() require.NoError(t, err) require.NoError(t, os.WriteFile( //nolint:gosec filepath.Join(dir, "committed.txt"), []byte("committed content\n"), 0o644, )) _, err = wt.Add("committed.txt") require.NoError(t, err) _, err = wt.Commit("initial", &git.CommitOptions{ Author: &object.Signature{ Name: "Test", Email: "test@example.com", When: time.Now(), }, }) require.NoError(t, err) require.NoError(t, os.WriteFile( //nolint:gosec filepath.Join(dir, "staged.txt"), []byte("new staged content\n"), 0o644, )) _, err = wt.Add("staged.txt") require.NoError(t, err) src := NewGit(dir, "", "", 0, true, 0, nil) chunks := collectChunks(t, src) found := false for _, c := range chunks { if c.FilePath == "staged.txt" { found = true assert.Contains(t, c.Content, "new staged content") } } assert.True(t, found, "expected to find staged.txt chunk") } func TestGitDepthLimit(t *testing.T) { t.Parallel() dir := t.TempDir() repo, err := git.PlainInit(dir, false) require.NoError(t, err) wt, err := repo.Worktree() require.NoError(t, err) for i := range 5 { name := "file" + string(rune('0'+i)) + ".txt" require.NoError(t, os.WriteFile( //nolint:gosec filepath.Join(dir, name), []byte("content "+name+"\n"), 0o644, )) _, err = wt.Add(name) require.NoError(t, err) _, err = wt.Commit("commit "+name, &git.CommitOptions{ Author: &object.Signature{ Name: "Test", Email: "test@example.com", When: time.Now().Add(time.Duration(i) * time.Minute), }, }) require.NoError(t, err) } src := NewGit(dir, "", "", 2, false, 0, nil) chunks := collectChunks(t, src) files := make(map[string]bool) for _, c := range chunks { files[c.FilePath] = true } assert.LessOrEqual(t, len(files), 5) } func TestGitExcludes(t *testing.T) { t.Parallel() dir := t.TempDir() repo, err := git.PlainInit(dir, false) require.NoError(t, err) wt, err := repo.Worktree() require.NoError(t, err) require.NoError(t, os.WriteFile( //nolint:gosec filepath.Join(dir, "keep.go"), []byte("package main\n"), 0o644, )) require.NoError(t, os.WriteFile( //nolint:gosec filepath.Join(dir, "skip.lock"), []byte("lock data\n"), 0o644, )) _, err = wt.Add("keep.go") require.NoError(t, err) _, err = wt.Add("skip.lock") require.NoError(t, err) _, err = wt.Commit("add files", &git.CommitOptions{ Author: &object.Signature{ Name: "Test", Email: "test@example.com", When: time.Now(), }, }) require.NoError(t, err) src := NewGit(dir, "", "", 0, false, 0, []string{".lock"}) chunks := collectChunks(t, src) for _, c := range chunks { assert.NotContains(t, c.FilePath, ".lock") } } func TestGitString(t *testing.T) { t.Parallel() src := NewGit("/tmp/repo", "", "", 0, false, 0, nil) assert.Equal(t, "git:/tmp/repo", src.String()) } func TestSplitIntoChunks(t *testing.T) { t.Parallel() var lines []string for i := range 120 { lines = append(lines, "line "+string(rune('A'+i%26))) } content := "" for i, l := range lines { if i > 0 { content += "\n" } content += l } chunks := splitIntoChunks( content, "test.txt", "abc123", "dev@test.com", time.Now(), ) require.Len(t, chunks, 3) assert.Equal(t, 1, chunks[0].LineStart) assert.Equal(t, 51, chunks[1].LineStart) assert.Equal(t, 101, chunks[2].LineStart) assert.Equal(t, "abc123", chunks[0].CommitSHA) assert.Equal(t, "dev@test.com", chunks[0].Author) } func TestSplitIntoChunksEmpty(t *testing.T) { t.Parallel() chunks := splitIntoChunks( "", "test.txt", "", "", time.Time{}, ) assert.Empty(t, chunks) } func TestIsBinaryExt(t *testing.T) { t.Parallel() assert.True(t, isBinaryExt("image.png")) assert.True(t, isBinaryExt("data.PDF")) assert.True(t, isBinaryExt("app.exe")) assert.True(t, isBinaryExt("model.onnx")) assert.True(t, isBinaryExt("font.otf")) assert.True(t, isBinaryExt("data.parquet")) assert.True(t, isBinaryExt("archive.xz")) assert.True(t, isBinaryExt("package.deb")) assert.True(t, isBinaryExt("movie.mkv")) assert.False(t, isBinaryExt("main.go")) assert.False(t, isBinaryExt("config.yaml")) assert.False(t, isBinaryExt("script.py")) }