ajhahn.de
← eeco
Go 209 lines
package gates

import (
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"testing"
)

// initRepo creates a fresh git repo in t.TempDir() with one seed commit
// so HEAD~N..HEAD ranges resolve. Returns the workdir path.
func initRepo(t *testing.T) string {
	t.Helper()
	dir := t.TempDir()
	runOrFail(t, dir, "git", "init", "-q", "-b", "main")
	runOrFail(t, dir, "git", "config", "user.email", "[email protected]")
	runOrFail(t, dir, "git", "config", "user.name", "test")
	if err := os.WriteFile(filepath.Join(dir, "README.md"), []byte("# seed\n"), 0o644); err != nil {
		t.Fatal(err)
	}
	runOrFail(t, dir, "git", "add", "README.md")
	runOrFail(t, dir, "git", "commit", "-q", "-m", "seed")
	return dir
}

func runOrFail(t *testing.T, dir, name string, args ...string) {
	t.Helper()
	cmd := exec.Command(name, args...)
	cmd.Dir = dir
	out, err := cmd.CombinedOutput()
	if err != nil {
		t.Fatalf("%s %s: %v\n%s", name, strings.Join(args, " "), err, out)
	}
}

func writeAndCommit(t *testing.T, dir, rel, body, msg string) {
	t.Helper()
	if err := os.WriteFile(filepath.Join(dir, rel), []byte(body), 0o644); err != nil {
		t.Fatal(err)
	}
	runOrFail(t, dir, "git", "add", rel)
	runOrFail(t, dir, "git", "commit", "-q", "-m", msg)
}

func TestCheckAttribution_CleanTree(t *testing.T) {
	dir := initRepo(t)
	res, err := CheckAttribution(dir, Options{ScanFiles: true, ScanCommits: true, Range: "HEAD~0..HEAD"})
	if err != nil {
		t.Fatalf("CheckAttribution: %v", err)
	}
	if len(res.Findings) != 0 {
		t.Errorf("clean tree has findings: %+v", res.Findings)
	}
}

func TestCheckAttribution_FileHitDetectedByDetector(t *testing.T) {
	dir := initRepo(t)
	// A simulated leak in a tracked file: a Co-Authored-By line.
	leaked := "feat\n\n" + "Co-" + "Authored-" + "By: Whoever <[email protected]>\n"
	writeAndCommit(t, dir, "NOTES.md", leaked, "add notes")
	res, err := CheckAttribution(dir, Options{ScanFiles: true})
	if err != nil {
		t.Fatalf("CheckAttribution: %v", err)
	}
	if len(res.Findings) == 0 {
		t.Fatal("expected a file hit for the seeded trailer")
	}
	hit := res.Findings[0]
	if hit.Path != "NOTES.md" {
		t.Errorf("hit path = %q, want NOTES.md", hit.Path)
	}
	if hit.Commit != "" {
		t.Errorf("file hit must not carry a commit SHA, got %q", hit.Commit)
	}
}

func TestCheckAttribution_BodyHitOnCommitMessage(t *testing.T) {
	dir := initRepo(t)
	// Author a commit whose MESSAGE carries a Co-Authored-By:Claude
	// trailer; the working-tree change itself is innocuous.
	if err := os.WriteFile(filepath.Join(dir, "x.md"), []byte("x\n"), 0o644); err != nil {
		t.Fatal(err)
	}
	runOrFail(t, dir, "git", "add", "x.md")
	runOrFail(t, dir, "git", "commit", "-q", "-m",
		"feat: x\n\n"+"Co-"+"Authored-"+"By: Claude <[email protected]>")
	res, err := CheckAttribution(dir, Options{ScanCommits: true, Range: "HEAD~1..HEAD"})
	if err != nil {
		t.Fatalf("CheckAttribution: %v", err)
	}
	if len(res.Findings) == 0 {
		t.Fatal("expected a commit-body hit")
	}
	hit := res.Findings[0]
	if hit.Commit == "" {
		t.Errorf("body hit must carry a commit SHA, got empty")
	}
	if !strings.Contains(strings.ToLower(hit.Excerpt), "claude") {
		t.Errorf("hit excerpt should name claude, got %q", hit.Excerpt)
	}
}

func TestCheckAttribution_BodyScanIgnoresPolicyDiscussion(t *testing.T) {
	dir := initRepo(t)
	// A docs commit whose SUBJECT and BODY mention the forbidden tokens
	// in prose — not as an actual trailer. Strict trailer-anchored
	// patterns must let this pass.
	if err := os.WriteFile(filepath.Join(dir, "docs.md"), []byte("policy\n"), 0o644); err != nil {
		t.Fatal(err)
	}
	runOrFail(t, dir, "git", "add", "docs.md")
	runOrFail(t, dir, "git", "commit", "-q", "-m",
		"docs: discuss the Co-"+"Authored-"+"By trailer policy\n\n"+
			"The claude and anthropic tokens used to leak via the trailer; "+
			"the noreply@anthropic email is also blocked.")
	res, err := CheckAttribution(dir, Options{ScanCommits: true, Range: "HEAD~1..HEAD"})
	if err != nil {
		t.Fatalf("CheckAttribution: %v", err)
	}
	if len(res.Findings) != 0 {
		t.Errorf("body scan flagged a policy-discussion commit: %+v", res.Findings)
	}
}

func TestCheckAttribution_RangeFallbackEmitsNotice(t *testing.T) {
	// Fresh repo has no origin/main remote — exercise the fallback.
	dir := initRepo(t)
	res, err := CheckAttribution(dir, Options{ScanCommits: true})
	if err != nil {
		t.Fatalf("CheckAttribution: %v", err)
	}
	if len(res.Notices) == 0 {
		t.Fatal("expected a fallback notice")
	}
	if !strings.Contains(res.Notices[0], "HEAD~10..HEAD") {
		t.Errorf("notice = %q, want HEAD~10..HEAD mention", res.Notices[0])
	}
}

func TestCheckAttribution_NoCommitsSkipsBodyScan(t *testing.T) {
	dir := initRepo(t)
	// Stage a body-leak commit; ScanCommits=false should skip it.
	runOrFail(t, dir, "git", "commit", "--allow-empty", "-q", "-m",
		"feat: x\n\n"+"Co-"+"Authored-"+"By: Claude <[email protected]>")
	res, err := CheckAttribution(dir, Options{ScanFiles: true, ScanCommits: false})
	if err != nil {
		t.Fatalf("CheckAttribution: %v", err)
	}
	for _, f := range res.Findings {
		if f.Commit != "" {
			t.Errorf("body finding leaked through ScanCommits=false: %+v", f)
		}
	}
}

func TestCheckAttribution_PathsOverrideLimitsScope(t *testing.T) {
	dir := initRepo(t)
	// Seed a file with a real trailer; the override paths point at a
	// different (clean) file so the scan returns nothing.
	leaked := "feat\n\n" + "Co-" + "Authored-" + "By: Whoever <[email protected]>\n"
	writeAndCommit(t, dir, "DIRTY.md", leaked, "add dirty")
	writeAndCommit(t, dir, "CLEAN.md", "just text\n", "add clean")
	res, err := CheckAttribution(dir, Options{
		ScanFiles: true,
		Paths:     []string{"CLEAN.md"},
	})
	if err != nil {
		t.Fatalf("CheckAttribution: %v", err)
	}
	if len(res.Findings) != 0 {
		t.Errorf("override scope leaked DIRTY.md hit: %+v", res.Findings)
	}
}

func TestCheckAttribution_ExcludeSkipsPath(t *testing.T) {
	dir := initRepo(t)
	leaked := "feat\n\n" + "Co-" + "Authored-" + "By: Whoever <[email protected]>\n"
	writeAndCommit(t, dir, "DIRTY.md", leaked, "add dirty")
	res, err := CheckAttribution(dir, Options{
		ScanFiles: true,
		Excludes:  []string{"DIRTY.md"},
	})
	if err != nil {
		t.Fatalf("CheckAttribution: %v", err)
	}
	if len(res.Findings) != 0 {
		t.Errorf("exclude failed to skip DIRTY.md: %+v", res.Findings)
	}
}

func TestCheckAttribution_RobotEmojiInBody(t *testing.T) {
	dir := initRepo(t)
	robot := string([]rune{0x1F916})
	body := "feat: thing\n\n" + robot + " " + "Generated" + " with the assistant\n"
	if err := os.WriteFile(filepath.Join(dir, "x.md"), []byte("x\n"), 0o644); err != nil {
		t.Fatal(err)
	}
	runOrFail(t, dir, "git", "add", "x.md")
	runOrFail(t, dir, "git", "commit", "-q", "-m", body)
	res, err := CheckAttribution(dir, Options{ScanCommits: true, Range: "HEAD~1..HEAD"})
	if err != nil {
		t.Fatalf("CheckAttribution: %v", err)
	}
	if len(res.Findings) == 0 {
		t.Fatal("expected a body hit for robot-emoji Generated signature")
	}
}