From d90cf1ef6d6f86b9b6c92cee5f4149097ae88577 Mon Sep 17 00:00:00 2001 From: Omkar P <45419097+omkar-foss@users.noreply.github.com> Date: Tue, 26 May 2026 23:06:35 +0530 Subject: [PATCH] Add support to detect Assisted-By trailer Signed-off-by: Omkar P <45419097+omkar-foss@users.noreply.github.com> --- cmd/cmd.go | 2 + detection/assistedby/assistedby.go | 77 ++++++++++++ detection/assistedby/assistedby_test.go | 158 ++++++++++++++++++++++++ scan/scan_test.go | 66 +++++++--- 4 files changed, 288 insertions(+), 15 deletions(-) create mode 100644 detection/assistedby/assistedby.go create mode 100644 detection/assistedby/assistedby_test.go diff --git a/cmd/cmd.go b/cmd/cmd.go index 4d41d6f..512884c 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -6,6 +6,7 @@ import ( "os" "github.com/chaoss/ai-detection-action/detection" + "github.com/chaoss/ai-detection-action/detection/assistedby" "github.com/chaoss/ai-detection-action/detection/coauthor" "github.com/chaoss/ai-detection-action/detection/committer" "github.com/chaoss/ai-detection-action/detection/gitnotes" @@ -27,6 +28,7 @@ const ( func allDetectors() []detection.Detector { return []detection.Detector{ + &assistedby.Detector{}, &committer.Detector{}, &coauthor.Detector{}, &gitnotes.Detector{}, diff --git a/detection/assistedby/assistedby.go b/detection/assistedby/assistedby.go new file mode 100644 index 0000000..555ffe4 --- /dev/null +++ b/detection/assistedby/assistedby.go @@ -0,0 +1,77 @@ +package assistedby + +import ( + "fmt" + "regexp" + "strings" + + "github.com/chaoss/ai-detection-action/detection" +) + +var assistedByPattern = regexp.MustCompile(`(?im)^assisted-by\s*:\s*([^\r\n]+?)\s*$`) +var toolLineReplacePattern = regexp.MustCompile(`\s*<[^>]+>`) + +func getMatchedTools(toolLine string) []string { + toolLine = strings.TrimSpace(toolLine) + if toolLine == "" { + return nil + } + toolLine = toolLineReplacePattern.ReplaceAllString(toolLine, "") + parts := strings.Split(toolLine, "\n") + var tools []string + for _, p := range parts { + p = strings.TrimSpace(strings.Split(strings.TrimSpace(p), "(")[0]) + if p == "" { + continue + } + words := strings.Fields(p) + for i, w := range words { + if len(w) > 0 { + words[i] = strings.ToUpper(w[:1]) + w[1:] + } + } + tools = append(tools, strings.Join(words, " ")) + } + return tools +} + +type Detector struct{} + +func (d *Detector) Name() string { return "assistedby" } + +func (d *Detector) Detect(input detection.Input) []detection.Finding { + if input.CommitMessage == "" { + return nil + } + + matches := assistedByPattern.FindAllStringSubmatch( + input.CommitMessage, + -1, + ) + + if len(matches) == 0 { + return nil + } + + var findings []detection.Finding + + for _, match := range matches { + if len(match) < 2 { + continue + } + + for _, matchedTool := range getMatchedTools(match[1]) { + findings = append(findings, detection.Finding{ + Detector: d.Name(), + Tool: matchedTool, + Confidence: detection.ConfidenceHigh, + Detail: fmt.Sprintf( + "Assisted-By trailer with tool %s", + matchedTool, + ), + }) + } + } + + return findings +} diff --git a/detection/assistedby/assistedby_test.go b/detection/assistedby/assistedby_test.go new file mode 100644 index 0000000..c828502 --- /dev/null +++ b/detection/assistedby/assistedby_test.go @@ -0,0 +1,158 @@ +package assistedby + +import ( + "testing" + + "github.com/chaoss/ai-detection-action/detection" +) + +func TestDetect(t *testing.T) { + d := &Detector{} + tests := []struct { + name string + message string + wantTools []string + }{ + { + name: "Claude trailer with Opus model", + message: "fix: update handler\n\nAssisted-By: Claude Opus 4 ", + wantTools: []string{"Claude Opus 4"}, + }, + { + name: "Claude trailer with Sonnet model", + message: "fix: update handler\n\nAssisted-By: Claude Sonnet 4 ", + wantTools: []string{"Claude Sonnet 4"}, + }, + { + name: "Cursor trailer", + message: "refactor: extract method\n\nAssisted-By: Cursor ", + wantTools: []string{"Cursor"}, + }, + { + name: "Aider trailer with model name", + message: "feat: add endpoint\n\nAssisted-By: aider (gpt-4o) ", + wantTools: []string{"Aider"}, + }, + { + name: "Aider trailer with different model", + message: "feat: add endpoint\n\nAssisted-By: aider (claude-4.7-opus) ", + wantTools: []string{"Aider"}, + }, + { + name: "multiple trailers with Claude and human", + message: "fix: bug\n\nAssisted-By: Claude Opus 4 ", + wantTools: []string{"Claude Opus 4"}, + }, + { + name: "multiple AI trailers", + message: "fix: bug\n\nAssisted-By: Claude Opus 4 \nAssisted-By: aider (gpt-4o) ", + wantTools: []string{"Claude Opus 4", "Aider"}, + }, + { + name: "case variation", + message: "fix: something\n\nassisted-by: Claude ", + wantTools: []string{"Claude"}, + }, + { + name: "ASSISTED-BY uppercase", + message: "fix: something\n\nASSISTED-BY: Claude ", + wantTools: []string{"Claude"}, + }, + { + name: "Assisted-By trailer in commit message", + message: "this is a commit message with\nAssisted-By: Claude Code", + wantTools: []string{"Claude Code"}, + }, + { + name: "Another Assisted-By trailer in commit message 1", + message: "this is a commit message with\nAssisted-By: Gemini", + wantTools: []string{"Gemini"}, + }, + { + name: "Another Assisted-By trailer in commit message 2", + message: "this is a commit message with\nAssisted-By: Kimi K2.6", + wantTools: []string{"Kimi K2.6"}, + }, + { + name: "Multiple Assisted-By trailer in commit message", + message: "this is a commit message with\nAssisted-By: Claude Code\nAssisted-By: Gemini", + wantTools: []string{"Claude Code", "Gemini"}, + }, + { + name: "Multiple Assisted-By trailers (with purpose brackets) in commit message", + message: ` +this is a commit message + +Co-Authored-By: Cursor + +Assisted-by: Claude 4.7 Opus + (logic optimization and design fixes) +Assisted-by: Kimi K2.6 (unit tests, integration tests) +Assisted-by: ChatGPT (documentation review) +Assisted-by: Gemini (documentation) +`, + wantTools: []string{"Claude 4.7 Opus", "Kimi K2.6", "ChatGPT", "Gemini"}, + }, + { + name: "Assisted-By trailer in commit message in lower case", + message: "this is a commit message with\nassisted-by: Claude Code", + wantTools: []string{"Claude Code"}, + }, + { + name: "Two different attributions (assistedby and coauthor) both with email address", + message: "Fix bug\n\nAssisted-By: Claude Sonnet 4 \nCo-Authored-By: Copilot ", + wantTools: []string{"Claude Sonnet 4"}, + }, + { + name: "Two different attributions (assistedby and coauthor) one with model name, other with email address", + message: "Add validation logic\n\nCo-Authored-By: Claude Sonnet 4.6 \nAssisted-by: GitHub Copilot", + wantTools: []string{"GitHub Copilot"}, + }, + { + name: "Claude Opus model attribution trailer", + message: "Fix bug\n\nAssisted-by: Claude Opus 4 ", + wantTools: []string{"Claude Opus 4"}, + }, + { + name: "no trailers", + message: "just a normal commit message", + wantTools: nil, + }, + { + name: "empty message", + message: "", + wantTools: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + findings := d.Detect(detection.Input{CommitMessage: tt.message}) + gotTools := make([]string, len(findings)) + for i, f := range findings { + gotTools[i] = f.Tool + if f.Confidence != detection.ConfidenceHigh { + t.Errorf("confidence = %d, want %d", f.Confidence, detection.ConfidenceHigh) + } + if f.Detector != "assistedby" { + t.Errorf("detector = %q, want %q", f.Detector, "assistedby") + } + } + + if len(gotTools) == 0 { + gotTools = nil + } + + if len(gotTools) != len(tt.wantTools) { + t.Errorf("tools = %v, want %v", gotTools, tt.wantTools) + return + } + for i := range gotTools { + if gotTools[i] != tt.wantTools[i] { + t.Errorf("tools = %v, want %v", gotTools, tt.wantTools) + return + } + } + }) + } +} diff --git a/scan/scan_test.go b/scan/scan_test.go index 7bdd2f0..6bf4905 100644 --- a/scan/scan_test.go +++ b/scan/scan_test.go @@ -8,6 +8,7 @@ import ( "time" "github.com/chaoss/ai-detection-action/detection" + "github.com/chaoss/ai-detection-action/detection/assistedby" "github.com/chaoss/ai-detection-action/detection/coauthor" "github.com/chaoss/ai-detection-action/detection/committer" "github.com/chaoss/ai-detection-action/detection/gitnotes" @@ -19,6 +20,7 @@ import ( func allDetectors() []detection.Detector { return []detection.Detector{ + &assistedby.Detector{}, &committer.Detector{}, &coauthor.Detector{}, &gitnotes.Detector{}, @@ -48,6 +50,19 @@ func initTestRepo(t *testing.T) (string, []string) { {"initial commit", "human@example.com"}, {"fix: update handler\n\nCo-Authored-By: Claude Opus 4 ", "human@example.com"}, {"aider: refactor auth module", "human@example.com"}, + {` +this is a commit message + +Co-Authored-By: Cursor + +Assisted-by: Claude 4.7 Opus + (logic optimization and design fixes) +Assisted-by: Kimi K2.6 (unit tests, integration tests) +Assisted-by: ChatGPT (documentation review) +Assisted-by: Gemini (documentation) +`, + "human@example.com", + }, } var hashes []string @@ -84,17 +99,17 @@ func TestScanCommitRange(t *testing.T) { dir, hashes := initTestRepo(t) detectors := allDetectors() - report, err := ScanCommitRange(dir, hashes[0]+".."+hashes[2], detectors) + report, err := ScanCommitRange(dir, hashes[0]+".."+hashes[3], detectors) if err != nil { t.Fatalf("ScanCommitRange: %v", err) } - if report.Summary.TotalCommits != 2 { - t.Errorf("total commits = %d, want 2", report.Summary.TotalCommits) + if report.Summary.TotalCommits != 3 { + t.Errorf("total commits = %d, want 3", report.Summary.TotalCommits) } - if report.Summary.AICommits != 2 { - t.Errorf("ai commits = %d, want 2", report.Summary.AICommits) + if report.Summary.AICommits != 3 { + t.Errorf("ai commits = %d, want 3", report.Summary.AICommits) } // Check that Claude Code was detected via co-author @@ -106,6 +121,21 @@ func TestScanCommitRange(t *testing.T) { if count, ok := report.Summary.ToolCounts["Aider"]; !ok || count == 0 { t.Error("expected Aider in tool counts") } + + // Check detection via assisted-by pattern 1 + if count, ok := report.Summary.ToolCounts["ChatGPT"]; !ok || count == 0 { + t.Error("expected ChatGPT in tool counts") + } + + // Check detection via assisted-by pattern 2 + if count, ok := report.Summary.ToolCounts["Claude 4.7 Opus"]; !ok || count == 0 { + t.Error("expected Claude 4.7 Opus in tool counts") + } + + // Check detection via assisted-by pattern 3 + if count, ok := report.Summary.ToolCounts["Kimi K2.6"]; !ok || count == 0 { + t.Error("expected Kimi K2.6 Opus in tool counts") + } } func TestScanCommitRangeAll(t *testing.T) { @@ -117,8 +147,8 @@ func TestScanCommitRangeAll(t *testing.T) { t.Fatalf("ScanCommitRange: %v", err) } - if report.Summary.TotalCommits != 3 { - t.Errorf("total commits = %d, want 3", report.Summary.TotalCommits) + if report.Summary.TotalCommits != 4 { + t.Errorf("total commits = %d, want 4", report.Summary.TotalCommits) } } @@ -126,28 +156,34 @@ func TestScanCommit(t *testing.T) { dir, hashes := initTestRepo(t) detectors := allDetectors() - // Scan the commit with co-author trailer - result, err := ScanCommit(dir, hashes[1], detectors) + // Scan the commit with assisted-by and co-author trailers + result, err := ScanCommit(dir, hashes[3], detectors) if err != nil { t.Fatalf("ScanCommit: %v", err) } - if result.Hash != hashes[1] { - t.Errorf("hash = %q, want %q", result.Hash, hashes[1]) + if result.Hash != hashes[3] { + t.Errorf("hash = %q, want %q", result.Hash, hashes[3]) } if len(result.Findings) == 0 { - t.Error("expected findings for co-author commit") + t.Error("expected findings for assisted-by and co-author trailers") } foundCoauthor := false + foundAssistedBy := false for _, f := range result.Findings { - if f.Detector == "coauthor" && f.Tool == "Claude Code" { + if f.Detector == "coauthor" && f.Tool == "Cursor" { foundCoauthor = true + } else if f.Detector == "assistedby" && f.Tool == "Kimi K2.6" { + foundAssistedBy = true } } if !foundCoauthor { - t.Error("expected coauthor finding for Claude Code") + t.Error("expected coauthor finding for Cursor") + } + if !foundAssistedBy { + t.Error("expected assistedby finding for Kimi K2.6") } } @@ -281,7 +317,7 @@ func TestReportSummaryByConfidence(t *testing.T) { t.Fatalf("ScanCommitRange: %v", err) } - // Co-author trailer should give high confidence + // Co-author and Assisted-By trailers should give high confidence if count, ok := report.Summary.ByConfidence["high"]; !ok || count == 0 { t.Error("expected high confidence findings") }