From 78d83b7a611bd67bea542b0a7ac8891b5d84f27b Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Sat, 19 Sep 2026 12:38:12 +1000 Subject: [PATCH] Reject an empty pr edit title Gitea would ignore --- cmd/agentpr/main.go | 5 +++++ cmd/agentpr/main_test.go | 16 ++++++++++++++++ internal/agent/client_test.go | 10 +++++----- internal/agent/gitea.go | 6 ++++-- 4 files changed, 30 insertions(+), 7 deletions(-) diff --git a/cmd/agentpr/main.go b/cmd/agentpr/main.go index fe43423..f5faf40 100644 --- a/cmd/agentpr/main.go +++ b/cmd/agentpr/main.go @@ -163,6 +163,11 @@ func newPREditCmd() *cobra.Command { // leave the title as it is, not blank it. var opts agent.EditPROptions if cmd.Flags().Changed("title") { + // Gitea ignores an empty title, so sending one would report + // success while changing nothing. + if title == "" { + return fmt.Errorf("--title cannot be empty: a title can be set but not cleared") + } opts.Title = &title } if cmd.Flags().Changed("body") { diff --git a/cmd/agentpr/main_test.go b/cmd/agentpr/main_test.go index b37f47f..b3faecf 100644 --- a/cmd/agentpr/main_test.go +++ b/cmd/agentpr/main_test.go @@ -33,3 +33,19 @@ func TestPREditRequiresTitleOrBody(t *testing.T) { t.Errorf("error = %q, want it to name the missing flags", err) } } + +// Gitea silently ignores an empty title, so `pr edit --title ""` would report +// success while changing nothing; it must fail before any Vault/Gitea call. +func TestPREditRejectsEmptyTitle(t *testing.T) { + cmd := newRootCmd() + cmd.SetArgs([]string{"pr", "edit", "--repo", "unkin/repo", "--pr", "7", "--title", ""}) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + err := cmd.Execute() + if err == nil { + t.Fatal("Execute() = nil, want an error for an empty --title") + } + if !strings.Contains(err.Error(), "--title cannot be empty") { + t.Errorf("error = %q, want it to reject the empty title", err) + } +} diff --git a/internal/agent/client_test.go b/internal/agent/client_test.go index b13bc15..e0cb8c8 100644 --- a/internal/agent/client_test.go +++ b/internal/agent/client_test.go @@ -155,19 +155,19 @@ func TestEditPRSendsOnlySuppliedFields(t *testing.T) { func TestEditPRAPIError(t *testing.T) { mux := http.NewServeMux() mux.HandleFunc("/api/v1/repos/unkin/repo/pulls/7", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusUnprocessableEntity) - _, _ = io.WriteString(w, `{"message":"title cannot be empty"}`) + w.WriteHeader(http.StatusNotFound) + _, _ = io.WriteString(w, `{"message":"pull request does not exist"}`) }) srv := httptest.NewServer(mux) defer srv.Close() - title := "" + title := "new title" c := &GiteaClient{BaseURL: srv.URL, Token: "t", HTTP: srv.Client()} _, err := c.EditPR("unkin/repo", 7, EditPROptions{Title: &title}) if err == nil { - t.Fatal("expected error on 422") + t.Fatal("expected error on 404") } - if !strings.Contains(err.Error(), "title cannot be empty") { + if !strings.Contains(err.Error(), "pull request does not exist") { t.Errorf("error %q should carry the API message", err) } } diff --git a/internal/agent/gitea.go b/internal/agent/gitea.go index fa7c516..2f7fd74 100644 --- a/internal/agent/gitea.go +++ b/internal/agent/gitea.go @@ -215,8 +215,10 @@ func (c *GiteaClient) CreatePR(repoPath string, opts CreatePROptions) (PullReque } // EditPROptions are the fields an edit may change. Pointers so an unset field -// is omitted from the payload entirely: Gitea overwrites whatever it is sent, -// so a nil Title leaves the title alone while a pointer to "" clears it. +// is omitted from the payload entirely, leaving that field as it is. The two +// fields are not symmetric: Gitea only applies a title when it is non-empty, +// so Title can be set but never cleared and a "" title is a silent no-op, +// while a pointer to "" Body really does blank the body. type EditPROptions struct { Title *string `json:"title,omitempty"` Body *string `json:"body,omitempty"`