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
291 行
8.5 KiB
Go
291 行
8.5 KiB
Go
package agent
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
type stubAgent struct {
|
|
result *Result
|
|
err error
|
|
|
|
gotPrompt string
|
|
gotCWD string
|
|
gotSchema json.RawMessage
|
|
}
|
|
|
|
func (s *stubAgent) Name() string { return "stub" }
|
|
func (s *stubAgent) Run(_ context.Context, opts RunOpts) (*Result, error) {
|
|
s.gotPrompt = opts.Prompt
|
|
s.gotCWD = opts.CWD
|
|
s.gotSchema = opts.JSONSchema
|
|
if s.err != nil {
|
|
return nil, s.err
|
|
}
|
|
return s.result, nil
|
|
}
|
|
func (s *stubAgent) Close() error { return nil }
|
|
|
|
func TestSuggestBranchName(t *testing.T) {
|
|
ag := &stubAgent{result: &Result{
|
|
Output: json.RawMessage(`{"name":"feat/onboarding-wizard"}`),
|
|
}}
|
|
name, err := SuggestBranchName(context.Background(), ag, "/tmp/repo")
|
|
if err != nil {
|
|
t.Fatalf("SuggestBranchName failed: %v", err)
|
|
}
|
|
if name != "feat/onboarding-wizard" {
|
|
t.Fatalf("expected feat/onboarding-wizard, got %q", name)
|
|
}
|
|
if ag.gotCWD != "/tmp/repo" {
|
|
t.Fatalf("expected CWD to be forwarded, got %q", ag.gotCWD)
|
|
}
|
|
if len(ag.gotSchema) == 0 {
|
|
t.Fatal("expected JSONSchema to be set on RunOpts")
|
|
}
|
|
if ag.gotPrompt == "" {
|
|
t.Fatal("expected prompt to be non-empty")
|
|
}
|
|
}
|
|
|
|
func TestSuggestBranchNameSanitizes(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
raw string
|
|
want string
|
|
}{
|
|
{"spaces to hyphen", "feat new wizard", "feat-new-wizard"},
|
|
{"uppercase to lower", "FEAT/NewThing", "feat/newthing"},
|
|
{"strip quotes", "\"fix/bug\"", "fix/bug"},
|
|
{"collapse dashes", "fix--double---dash", "fix-double-dash"},
|
|
{"strip trailing slash", "feat/", "feat"},
|
|
{"strip bad chars", "fix: thing! @home", "fix-thing-home"},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
ag := &stubAgent{result: &Result{
|
|
Output: json.RawMessage(`{"name":` + jsonQuote(tc.raw) + `}`),
|
|
}}
|
|
got, err := SuggestBranchName(context.Background(), ag, "/tmp")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if got != tc.want {
|
|
t.Fatalf("expected %q, got %q", tc.want, got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSuggestBranchNameRejectsInvalidGitRefs(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
raw string
|
|
}{
|
|
{name: "double dot", raw: "feat/bug..fix"},
|
|
{name: "double slash", raw: "feat//wizard"},
|
|
{name: "lock suffix", raw: "feat/wizard.lock"},
|
|
{name: "lock path component", raw: "feat/topic.lock/extra"},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
ag := &stubAgent{result: &Result{
|
|
Output: json.RawMessage(`{"name":` + jsonQuote(tc.raw) + `}`),
|
|
}}
|
|
if _, err := SuggestBranchName(context.Background(), ag, "/tmp"); err == nil {
|
|
t.Fatalf("expected invalid ref %q to be rejected", tc.raw)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSuggestBranchNameLengthCapped(t *testing.T) {
|
|
long := "feat/really-long-branch-name-that-exceeds-the-limit-we-enforce-for-clarity"
|
|
ag := &stubAgent{result: &Result{
|
|
Output: json.RawMessage(`{"name":` + jsonQuote(long) + `}`),
|
|
}}
|
|
got, err := SuggestBranchName(context.Background(), ag, "/tmp")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if len(got) > 60 {
|
|
t.Fatalf("expected <= 60 chars, got %d: %q", len(got), got)
|
|
}
|
|
}
|
|
|
|
func TestSuggestBranchNameEmpty(t *testing.T) {
|
|
ag := &stubAgent{result: &Result{
|
|
Output: json.RawMessage(`{"name":""}`),
|
|
}}
|
|
_, err := SuggestBranchName(context.Background(), ag, "/tmp")
|
|
if err == nil {
|
|
t.Fatal("expected error for empty name")
|
|
}
|
|
}
|
|
|
|
func TestSuggestBranchNameOnlyInvalidChars(t *testing.T) {
|
|
ag := &stubAgent{result: &Result{
|
|
Output: json.RawMessage(`{"name":"!@#$%"}`),
|
|
}}
|
|
_, err := SuggestBranchName(context.Background(), ag, "/tmp")
|
|
if err == nil {
|
|
t.Fatal("expected error when all chars are stripped")
|
|
}
|
|
}
|
|
|
|
func TestSuggestBranchNameAgentError(t *testing.T) {
|
|
ag := &stubAgent{err: errors.New("boom")}
|
|
_, err := SuggestBranchName(context.Background(), ag, "/tmp")
|
|
if err == nil {
|
|
t.Fatal("expected error from agent failure")
|
|
}
|
|
}
|
|
|
|
func TestSuggestCommitMessage(t *testing.T) {
|
|
ag := &stubAgent{result: &Result{
|
|
Output: json.RawMessage(`{"subject":"feat(cli): add onboarding wizard"}`),
|
|
}}
|
|
got, err := SuggestCommitMessage(context.Background(), ag, "/tmp")
|
|
if err != nil {
|
|
t.Fatalf("SuggestCommitMessage failed: %v", err)
|
|
}
|
|
if got != "feat(cli): add onboarding wizard" {
|
|
t.Fatalf("unexpected subject: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestSuggestCommitMessageTrimsNewlines(t *testing.T) {
|
|
ag := &stubAgent{result: &Result{
|
|
Output: json.RawMessage(`{"subject":"fix: thing\n\nbody"}`),
|
|
}}
|
|
got, err := SuggestCommitMessage(context.Background(), ag, "/tmp")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if got != "fix: thing" {
|
|
t.Fatalf("expected first line only, got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestSuggestCommitMessageKeepsConventionalNonReleaseType(t *testing.T) {
|
|
ag := &stubAgent{result: &Result{
|
|
Output: json.RawMessage(`{"subject":"refactor: improve CLI output"}`),
|
|
}}
|
|
got, err := SuggestCommitMessage(context.Background(), ag, "/tmp")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if got != "refactor: improve CLI output" {
|
|
t.Fatalf("subject = %q, want conventional agent subject unchanged", got)
|
|
}
|
|
}
|
|
|
|
func TestSuggestCommitMessagePromptRequiresReleaseTypesForProductImpact(t *testing.T) {
|
|
ag := &stubAgent{result: &Result{
|
|
Output: json.RawMessage(`{"subject":"fix: improve CLI output"}`),
|
|
}}
|
|
if _, err := SuggestCommitMessage(context.Background(), ag, "/tmp"); err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if !strings.Contains(ag.gotPrompt, "user-facing product impact") {
|
|
t.Fatalf("prompt should mention user-facing product impact rule, got:\n%s", ag.gotPrompt)
|
|
}
|
|
if !strings.Contains(ag.gotPrompt, "must use feat or fix") {
|
|
t.Fatalf("prompt should require feat or fix for product impact, got:\n%s", ag.gotPrompt)
|
|
}
|
|
}
|
|
|
|
func TestSuggestCommitMessageEmpty(t *testing.T) {
|
|
ag := &stubAgent{result: &Result{
|
|
Output: json.RawMessage(`{"subject":" "}`),
|
|
}}
|
|
_, err := SuggestCommitMessage(context.Background(), ag, "/tmp")
|
|
if err == nil {
|
|
t.Fatal("expected error for empty subject")
|
|
}
|
|
}
|
|
|
|
func TestSuggestBranchAndCommit(t *testing.T) {
|
|
ag := &stubAgent{result: &Result{
|
|
Output: json.RawMessage(`{"branch":"feat/onboarding-wizard","subject":"feat(cli): add onboarding wizard"}`),
|
|
}}
|
|
branch, subject, err := SuggestBranchAndCommit(context.Background(), ag, "/tmp/repo")
|
|
if err != nil {
|
|
t.Fatalf("SuggestBranchAndCommit failed: %v", err)
|
|
}
|
|
if branch != "feat/onboarding-wizard" {
|
|
t.Fatalf("expected feat/onboarding-wizard, got %q", branch)
|
|
}
|
|
if subject != "feat(cli): add onboarding wizard" {
|
|
t.Fatalf("expected commit subject, got %q", subject)
|
|
}
|
|
if ag.gotCWD != "/tmp/repo" {
|
|
t.Fatalf("expected CWD to be forwarded, got %q", ag.gotCWD)
|
|
}
|
|
if len(ag.gotSchema) == 0 {
|
|
t.Fatal("expected JSONSchema to be set on RunOpts")
|
|
}
|
|
if ag.gotPrompt == "" {
|
|
t.Fatal("expected prompt to be non-empty")
|
|
}
|
|
}
|
|
|
|
func TestSuggestBranchAndCommitSanitizes(t *testing.T) {
|
|
ag := &stubAgent{result: &Result{
|
|
Output: json.RawMessage(`{"branch":"FEAT/New Thing","subject":"feat(cli): clean up thing\n\nbody paragraph"}`),
|
|
}}
|
|
branch, subject, err := SuggestBranchAndCommit(context.Background(), ag, "/tmp")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if branch != "feat/new-thing" {
|
|
t.Fatalf("expected branch to be sanitized, got %q", branch)
|
|
}
|
|
if subject != "feat(cli): clean up thing" {
|
|
t.Fatalf("expected first-line commit subject, got %q", subject)
|
|
}
|
|
}
|
|
|
|
func TestSuggestBranchAndCommitEmptyBranchErrors(t *testing.T) {
|
|
ag := &stubAgent{result: &Result{
|
|
Output: json.RawMessage(`{"branch":"","subject":"feat: something"}`),
|
|
}}
|
|
if _, _, err := SuggestBranchAndCommit(context.Background(), ag, "/tmp"); err == nil {
|
|
t.Fatal("expected error when branch is empty")
|
|
}
|
|
}
|
|
|
|
func TestSuggestBranchAndCommitEmptySubjectReturnsBranch(t *testing.T) {
|
|
// An empty commit subject is tolerated: callers can fall back to a
|
|
// dedicated SuggestCommitMessage call rather than failing the wizard.
|
|
ag := &stubAgent{result: &Result{
|
|
Output: json.RawMessage(`{"branch":"fix/bug","subject":""}`),
|
|
}}
|
|
branch, subject, err := SuggestBranchAndCommit(context.Background(), ag, "/tmp")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if branch != "fix/bug" {
|
|
t.Fatalf("expected fix/bug, got %q", branch)
|
|
}
|
|
if subject != "" {
|
|
t.Fatalf("expected empty subject, got %q", subject)
|
|
}
|
|
}
|
|
|
|
func TestSuggestBranchAndCommitAgentError(t *testing.T) {
|
|
ag := &stubAgent{err: errors.New("boom")}
|
|
if _, _, err := SuggestBranchAndCommit(context.Background(), ag, "/tmp"); err == nil {
|
|
t.Fatal("expected error from agent failure")
|
|
}
|
|
}
|
|
|
|
func jsonQuote(s string) string {
|
|
b, _ := json.Marshal(s)
|
|
return string(b)
|
|
}
|