kunchenguid--no-mistakes
26f897c1ec
release / release-please (push) Failing after 1m49s
docs / build (push) Failing after 6m34s
release / build-and-upload (arm64, linux) (push) Has been cancelled
release / build-and-upload (arm64, windows) (push) Has been cancelled
release / build-darwin (amd64, darwin) (push) Has been cancelled
release / checksums (push) Has been cancelled
release / finalize (push) Has been cancelled
release / build-darwin (arm64, darwin) (push) Has been cancelled
release / build-and-upload (amd64, linux) (push) Has been cancelled
release / build-and-upload (amd64, windows) (push) Has been cancelled
docs / deploy (push) Has been cancelled
314 行
9.2 KiB
Go
314 行
9.2 KiB
Go
package cli
|
|
|
|
import (
|
|
"bytes"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"github.com/kunchenguid/no-mistakes/internal/cimonitor"
|
|
"github.com/kunchenguid/no-mistakes/internal/ipc"
|
|
"github.com/kunchenguid/no-mistakes/internal/types"
|
|
)
|
|
|
|
func ciRunView(ciStatus types.StepStatus) runView {
|
|
return runView{
|
|
ID: "run-1",
|
|
Branch: "feature/x",
|
|
Status: string(types.RunRunning),
|
|
Steps: []stepView{
|
|
{Name: string(types.StepPR), Status: string(types.StepStatusCompleted)},
|
|
{Name: string(types.StepCI), Status: string(ciStatus)},
|
|
},
|
|
}
|
|
}
|
|
|
|
func TestCIReadyToMerge(t *testing.T) {
|
|
passedLogs := []string{
|
|
"monitoring CI for PR #42 (timeout: 4h)...",
|
|
cimonitor.ChecksPassedMsg,
|
|
}
|
|
runningLogs := []string{"monitoring CI for PR #42 (timeout: 4h)..."}
|
|
|
|
tests := []struct {
|
|
name string
|
|
rv runView
|
|
ciLogs []string
|
|
wantStop bool
|
|
}{
|
|
{
|
|
name: "ci running and checks passed",
|
|
rv: ciRunView(types.StepStatusRunning),
|
|
ciLogs: passedLogs,
|
|
wantStop: true,
|
|
},
|
|
{
|
|
name: "ci running but checks not passed yet",
|
|
rv: ciRunView(types.StepStatusRunning),
|
|
ciLogs: runningLogs,
|
|
wantStop: false,
|
|
},
|
|
{
|
|
name: "checks passed but ci step already completed",
|
|
rv: ciRunView(types.StepStatusCompleted),
|
|
ciLogs: passedLogs,
|
|
wantStop: false,
|
|
},
|
|
{
|
|
name: "no ci step in run",
|
|
rv: runView{Status: string(types.RunRunning), Steps: []stepView{{Name: string(types.StepPR), Status: string(types.StepStatusCompleted)}}},
|
|
ciLogs: passedLogs,
|
|
wantStop: false,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := ciReadyToMerge(tt.rv, tt.ciLogs); got != tt.wantStop {
|
|
t.Errorf("ciReadyToMerge() = %v, want %v", got, tt.wantStop)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGateResolution(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
gate stepView
|
|
alreadyFixed bool
|
|
wantAction types.ApprovalAction
|
|
wantIDs []string
|
|
}{
|
|
{
|
|
name: "actionable findings are fixed with every finding selected",
|
|
gate: stepView{
|
|
Name: "review",
|
|
Status: string(types.StepStatusAwaitingApproval),
|
|
FindingsJSON: `{"findings":[{"id":"review-1","severity":"warning","description":"design choice","action":"ask-user"},{"id":"review-2","severity":"info","description":"fyi","action":"no-op"}],"summary":"2"}`,
|
|
},
|
|
wantAction: types.ActionFix,
|
|
wantIDs: []string{"review-1", "review-2"},
|
|
},
|
|
{
|
|
name: "only non-actionable findings are approved",
|
|
gate: stepView{
|
|
Name: "test",
|
|
Status: string(types.StepStatusAwaitingApproval),
|
|
FindingsJSON: `{"findings":[{"id":"test-1","severity":"info","description":"fyi","action":"no-op"}],"summary":"1"}`,
|
|
},
|
|
wantAction: types.ActionApprove,
|
|
},
|
|
{
|
|
name: "no findings are approved",
|
|
gate: stepView{
|
|
Name: "push",
|
|
Status: string(types.StepStatusAwaitingApproval),
|
|
FindingsJSON: ``,
|
|
},
|
|
wantAction: types.ActionApprove,
|
|
},
|
|
{
|
|
name: "already fixed step is approved (no fix loop)",
|
|
gate: stepView{
|
|
Name: "review",
|
|
Status: string(types.StepStatusFixReview),
|
|
FindingsJSON: `{"findings":[{"id":"review-1","severity":"warning","description":"still here","action":"ask-user"}],"summary":"1"}`,
|
|
},
|
|
alreadyFixed: true,
|
|
wantAction: types.ActionApprove,
|
|
},
|
|
{
|
|
name: "reattached fix review is approved without in-memory fix state",
|
|
gate: stepView{
|
|
Name: "review",
|
|
Status: string(types.StepStatusFixReview),
|
|
FindingsJSON: `{"findings":[{"id":"review-1","severity":"warning","description":"still here","action":"ask-user"}],"summary":"1"}`,
|
|
},
|
|
wantAction: types.ActionApprove,
|
|
},
|
|
{
|
|
name: "actionable findings without ids are approved rather than fixing nothing",
|
|
gate: stepView{
|
|
Name: "review",
|
|
Status: string(types.StepStatusAwaitingApproval),
|
|
FindingsJSON: `{"findings":[{"severity":"warning","description":"no id","action":"ask-user"}],"summary":"1"}`,
|
|
},
|
|
wantAction: types.ActionApprove,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
action, ids := gateResolution(tt.gate, tt.alreadyFixed)
|
|
t.Logf("auto-resolution action=%s finding_ids=%v", action, ids)
|
|
if action != tt.wantAction {
|
|
t.Fatalf("action = %s, want %s", action, tt.wantAction)
|
|
}
|
|
if len(ids) != len(tt.wantIDs) {
|
|
t.Fatalf("ids = %v, want %v", ids, tt.wantIDs)
|
|
}
|
|
for i := range ids {
|
|
if ids[i] != tt.wantIDs[i] {
|
|
t.Fatalf("ids = %v, want %v", ids, tt.wantIDs)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRenderDriveResult_ChecksPassed(t *testing.T) {
|
|
run := &ipc.RunInfo{
|
|
ID: "run-1",
|
|
Branch: "feature/x",
|
|
Status: types.RunRunning, // not terminal: daemon keeps monitoring until merge
|
|
HeadSHA: "abcdef1234567890",
|
|
PRURL: strptr("https://github.com/user/repo/pull/42"),
|
|
Steps: []ipc.StepResultInfo{
|
|
{StepName: types.StepCI, Status: types.StepStatusRunning},
|
|
},
|
|
}
|
|
|
|
var out bytes.Buffer
|
|
cmd := &cobra.Command{}
|
|
cmd.SetOut(&out)
|
|
|
|
if err := renderDriveResult(cmd, run, true); err != nil {
|
|
t.Fatalf("checks-passed must exit 0, got error: %v", err)
|
|
}
|
|
|
|
got := out.String()
|
|
for _, want := range []string{
|
|
"outcome: checks-passed",
|
|
"https://github.com/user/repo/pull/42",
|
|
"merge",
|
|
"Summarize this pipeline run for the user",
|
|
} {
|
|
if !strings.Contains(got, want) {
|
|
t.Errorf("checks-passed output missing %q in:\n%s", want, got)
|
|
}
|
|
}
|
|
if strings.Contains(got, "outcome: passed\n") {
|
|
t.Errorf("checks-passed must not report a terminal passed outcome:\n%s", got)
|
|
}
|
|
// No fixes were applied, so neither the fixes table nor the
|
|
// acknowledge-your-misses instruction should appear.
|
|
for _, reject := range []string{"fixes[", "acknowledge"} {
|
|
if strings.Contains(got, reject) {
|
|
t.Errorf("checks-passed output without fixes must not contain %q:\n%s", reject, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRenderDriveResult_ChecksPassedWithFixes(t *testing.T) {
|
|
run := &ipc.RunInfo{
|
|
ID: "run-1",
|
|
Branch: "feature/x",
|
|
Status: types.RunRunning,
|
|
HeadSHA: "abcdef1234567890",
|
|
PRURL: strptr("https://github.com/user/repo/pull/42"),
|
|
Steps: []ipc.StepResultInfo{
|
|
{StepName: types.StepReview, Status: types.StepStatusCompleted, FixSummaries: []string{"handle nil pointer in executor"}},
|
|
{StepName: types.StepTest, Status: types.StepStatusCompleted, FixSummaries: []string{""}},
|
|
{StepName: types.StepCI, Status: types.StepStatusRunning},
|
|
},
|
|
}
|
|
|
|
var out bytes.Buffer
|
|
cmd := &cobra.Command{}
|
|
cmd.SetOut(&out)
|
|
|
|
if err := renderDriveResult(cmd, run, true); err != nil {
|
|
t.Fatalf("checks-passed must exit 0, got error: %v", err)
|
|
}
|
|
|
|
got := out.String()
|
|
for _, want := range []string{
|
|
"outcome: checks-passed",
|
|
"fixes[2]{step,summary}:",
|
|
"review,handle nil pointer in executor",
|
|
"test,fix applied (no summary recorded)",
|
|
"Summarize this pipeline run for the user",
|
|
"acknowledge the misses and list each fix so the user can review them",
|
|
} {
|
|
if !strings.Contains(got, want) {
|
|
t.Errorf("checks-passed output missing %q in:\n%s", want, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRenderDriveResult_TerminalPassedUnaffected(t *testing.T) {
|
|
run := &ipc.RunInfo{
|
|
ID: "run-1",
|
|
Branch: "feature/x",
|
|
Status: types.RunCompleted,
|
|
Steps: []ipc.StepResultInfo{{StepName: types.StepCI, Status: types.StepStatusCompleted}},
|
|
}
|
|
var out bytes.Buffer
|
|
cmd := &cobra.Command{}
|
|
cmd.SetOut(&out)
|
|
|
|
if err := renderDriveResult(cmd, run, false); err != nil {
|
|
t.Fatalf("terminal passed must exit 0, got error: %v", err)
|
|
}
|
|
got := out.String()
|
|
if !strings.Contains(got, "outcome: passed") {
|
|
t.Errorf("expected terminal passed outcome, got:\n%s", got)
|
|
}
|
|
if !strings.Contains(got, "Summarize this pipeline run for the user") {
|
|
t.Errorf("terminal passed output missing the summarize instruction:\n%s", got)
|
|
}
|
|
}
|
|
|
|
func TestRenderDriveResult_TerminalPassedWithFixes(t *testing.T) {
|
|
run := &ipc.RunInfo{
|
|
ID: "run-1",
|
|
Branch: "feature/x",
|
|
Status: types.RunCompleted,
|
|
Steps: []ipc.StepResultInfo{
|
|
{StepName: types.StepLint, Status: types.StepStatusCompleted, FixSummaries: []string{"remove unused import"}},
|
|
{StepName: types.StepCI, Status: types.StepStatusCompleted},
|
|
},
|
|
}
|
|
var out bytes.Buffer
|
|
cmd := &cobra.Command{}
|
|
cmd.SetOut(&out)
|
|
|
|
if err := renderDriveResult(cmd, run, false); err != nil {
|
|
t.Fatalf("terminal passed must exit 0, got error: %v", err)
|
|
}
|
|
got := out.String()
|
|
for _, want := range []string{
|
|
"outcome: passed",
|
|
"fixes[1]{step,summary}:",
|
|
"lint,remove unused import",
|
|
"acknowledge the misses and list each fix so the user can review them",
|
|
} {
|
|
if !strings.Contains(got, want) {
|
|
t.Errorf("terminal passed output missing %q in:\n%s", want, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRenderDriveResult_FailedHasNoSummarizeInstruction(t *testing.T) {
|
|
run := &ipc.RunInfo{
|
|
ID: "run-1",
|
|
Branch: "feature/x",
|
|
Status: types.RunFailed,
|
|
Steps: []ipc.StepResultInfo{
|
|
{StepName: types.StepTest, Status: types.StepStatusFailed, FixSummaries: []string{"partial fix"}},
|
|
},
|
|
}
|
|
var out bytes.Buffer
|
|
cmd := &cobra.Command{}
|
|
cmd.SetOut(&out)
|
|
|
|
err := renderDriveResult(cmd, run, false)
|
|
if err == nil {
|
|
t.Fatal("failed outcome must exit non-zero")
|
|
}
|
|
got := out.String()
|
|
if strings.Contains(got, "Summarize this pipeline run for the user") {
|
|
t.Errorf("failed outcome must not carry the success summary instruction:\n%s", got)
|
|
}
|
|
}
|