Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 65 additions & 58 deletions internal/cli/report.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"os"
"regexp"
"strings"
Expand Down Expand Up @@ -167,47 +168,15 @@ func cmdReport(ctx context.Context, args []string) int {
// Track the worst-case outcome so the overall command exit code
// reflects reality. Previously cmdReport always returned 0 even
// when every create failed, which hid failures from scripts.
anyFailed := false
anyCreated := false

for i, m := range matches {
fmt.Printf("\n[%d/%d] %s\n", i+1, len(matches), m.Proposal.Title)
if m.IsDuplicate {
fmt.Printf(" duplicate of #%d %q (score %.2f)\n", m.BestExisting.Number, m.BestExisting.Title, m.Score)
fmt.Printf(" → would comment with: %s\n", trim(m.Proposal.Body, 120))
if confirmReport(yes) {
url, err := report.CommentOnIssue(ctx, m.BestExisting.Number,
"From `i report`:\n\n"+m.Proposal.Body)
if err != nil {
errf("report: comment failed: %v", err)
anyFailed = true
continue
}
anyCreated = true
fmt.Printf(" ✓ commented: %s\n", url)
}
} else {
kept, dropped := report.FilterLabels(m.Proposal.Labels, known)
if len(dropped) > 0 {
fmt.Printf(" (dropping labels not on repo: %v)\n", dropped)
}
p := m.Proposal
p.Labels = kept
fmt.Printf(" new issue\n")
fmt.Printf(" labels: %v\n", p.Labels)
fmt.Printf(" body: %s\n", trim(p.Body, 200))
if confirmReport(yes) {
url, err := report.CreateIssue(ctx, p)
if err != nil {
errf("report: create failed: %v", err)
anyFailed = true
continue
}
anyCreated = true
fmt.Printf(" ✓ created: %s\n", url)
}
}
}
anyCreated, anyFailed := applyReportMatches(
ctx,
os.Stdout,
matches,
known,
yes,
report.CreateIssue,
report.CommentOnIssue,
)
switch {
case anyFailed:
// At least one op failed; propagate. Use 3 to match the
Expand All @@ -222,23 +191,61 @@ func cmdReport(ctx context.Context, args []string) int {
return 0
}

// confirmReport is the per-proposal interactive check. Defaults to
// YES because the user explicitly invoked `i report` intending to
// file issues — asking them to type "y" for every proposal after
// they already chose to report is noise. Pressing Enter (or y / yes)
// proceeds; only an explicit n / no / anything-else declines.
func confirmReport(yes bool) bool {
if yes {
return true
}
fmt.Print(" proceed? [Y/n] ")
r := bufio.NewReader(os.Stdin)
line, _ := r.ReadString('\n')
line = strings.TrimSpace(strings.ToLower(line))
if line == "" || line == "y" || line == "yes" {
return true
}
return false
type reportCreateIssueFunc func(context.Context, report.Proposal) (string, error)
type reportCommentIssueFunc func(context.Context, int, string) (string, error)

func applyReportMatches(
ctx context.Context,
out io.Writer,
matches []report.Match,
known map[string]bool,
yes bool,
createIssue reportCreateIssueFunc,
commentOnIssue reportCommentIssueFunc,
) (anyCreated, anyFailed bool) {
for i, m := range matches {
fmt.Fprintf(out, "\n[%d/%d] %s\n", i+1, len(matches), m.Proposal.Title)
if m.IsDuplicate {
fmt.Fprintf(out, " duplicate of #%d %q (score %.2f)\n", m.BestExisting.Number, m.BestExisting.Title, m.Score)
fmt.Fprintf(out, " → would comment with: %s\n", trim(m.Proposal.Body, 120))
if !yes {
fmt.Fprintln(out, " → dry run only; pass --yes to post the comment")
continue
}
url, err := commentOnIssue(ctx, m.BestExisting.Number, "From `i report`:\n\n"+m.Proposal.Body)
if err != nil {
errf("report: comment failed: %v", err)
anyFailed = true
continue
}
anyCreated = true
fmt.Fprintf(out, " ✓ commented: %s\n", url)
continue
}

kept, dropped := report.FilterLabels(m.Proposal.Labels, known)
if len(dropped) > 0 {
fmt.Fprintf(out, " (dropping labels not on repo: %v)\n", dropped)
}
p := m.Proposal
p.Labels = kept
fmt.Fprintln(out, " new issue")
fmt.Fprintf(out, " labels: %v\n", p.Labels)
fmt.Fprintf(out, " body: %s\n", trim(p.Body, 200))
if !yes {
fmt.Fprintln(out, " → would create a new issue (pass --yes to execute)")
continue
}
url, err := createIssue(ctx, p)
if err != nil {
errf("report: create failed: %v", err)
anyFailed = true
continue
}
anyCreated = true
fmt.Fprintf(out, " ✓ created: %s\n", url)
}
return anyCreated, anyFailed
}

func trim(s string, n int) string {
Expand Down
151 changes: 150 additions & 1 deletion internal/cli/report_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
package cli

import "testing"
import (
"bytes"
"context"
"strings"
"testing"

"github.com/CoreyRDean/intent/internal/report"
)

// TestExtractProposals covers the real-world failure modes small local
// models have produced when asked to emit issue-proposal JSON.
Expand Down Expand Up @@ -145,3 +152,145 @@ func TestBuildReportUserInput(t *testing.T) {
})
}
}

func TestApplyReportMatches_DryRunSkipsWrites(t *testing.T) {
t.Parallel()

matches := []report.Match{
{
Proposal: report.Proposal{
Title: "Existing bug",
Body: "comment body",
},
BestExisting: &report.SearchResult{
Number: 42,
Title: "Existing bug",
},
Score: 0.91,
IsDuplicate: true,
},
{
Proposal: report.Proposal{
Title: "New bug",
Body: "new issue body",
Labels: []string{"bug", "needs-triage"},
},
},
}

var out bytes.Buffer
createCalls := 0
commentCalls := 0
anyCreated, anyFailed := applyReportMatches(
context.Background(),
&out,
matches,
map[string]bool{"bug": true},
false,
func(context.Context, report.Proposal) (string, error) {
createCalls++
return "", nil
},
func(context.Context, int, string) (string, error) {
commentCalls++
return "", nil
},
)

if anyCreated {
t.Fatal("dry run should not report created work")
}
if anyFailed {
t.Fatal("dry run should not report failures")
}
if createCalls != 0 || commentCalls != 0 {
t.Fatalf("dry run should skip writes, got create=%d comment=%d", createCalls, commentCalls)
}

got := out.String()
for _, want := range []string{
`duplicate of #42 "Existing bug"`,
"dry run only; pass --yes to post the comment",
"(dropping labels not on repo: [needs-triage])",
"labels: [bug]",
"would create a new issue (pass --yes to execute)",
} {
if !strings.Contains(got, want) {
t.Fatalf("expected output to contain %q, got:\n%s", want, got)
}
}
}

func TestApplyReportMatches_YesExecutesWrites(t *testing.T) {
t.Parallel()

matches := []report.Match{
{
Proposal: report.Proposal{
Title: "Existing bug",
Body: "comment body",
},
BestExisting: &report.SearchResult{
Number: 42,
Title: "Existing bug",
},
Score: 0.91,
IsDuplicate: true,
},
{
Proposal: report.Proposal{
Title: "New bug",
Body: "new issue body",
Labels: []string{"bug"},
},
},
}

var out bytes.Buffer
createCalls := 0
commentCalls := 0
anyCreated, anyFailed := applyReportMatches(
context.Background(),
&out,
matches,
map[string]bool{"bug": true},
true,
func(_ context.Context, p report.Proposal) (string, error) {
createCalls++
if p.Title != "New bug" {
t.Fatalf("unexpected issue title %q", p.Title)
}
return "https://github.com/CoreyRDean/intent/issues/99", nil
},
func(_ context.Context, number int, body string) (string, error) {
commentCalls++
if number != 42 {
t.Fatalf("unexpected comment target %d", number)
}
if !strings.Contains(body, "comment body") {
t.Fatalf("unexpected comment body %q", body)
}
return "https://github.com/CoreyRDean/intent/issues/42#issuecomment-1", nil
},
)

if !anyCreated {
t.Fatal("write mode should report created work")
}
if anyFailed {
t.Fatal("write mode should not report failures")
}
if createCalls != 1 || commentCalls != 1 {
t.Fatalf("write mode should execute both actions, got create=%d comment=%d", createCalls, commentCalls)
}

got := out.String()
for _, want := range []string{
"✓ commented: https://github.com/CoreyRDean/intent/issues/42#issuecomment-1",
"✓ created: https://github.com/CoreyRDean/intent/issues/99",
} {
if !strings.Contains(got, want) {
t.Fatalf("expected output to contain %q, got:\n%s", want, got)
}
}
}
Loading