diff --git a/CHANGELOG.md b/CHANGELOG.md index cbaed28..d1fa69a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ - Gmail: auto-fill draft reply subjects from the original message when `gmail drafts create --reply-to-message-id` omits `--subject`. (#488) — thanks @jbowerbir. - Gmail: fall back to the People profile name for primary-account `From` headers when Gmail send-as settings omit a display name. (#431) — thanks @moeedahmed. - Gmail: reuse the shared paginated list runner for thread and message search so `--all`, `--page`, text, and JSON output stay consistent. +- Gmail: clarify that `gmail batch delete` is permanent and point default-scope workflows at `gmail trash`. (#151) - Drive: print large upload progress to stderr while keeping JSON output parseable. (#529) - Drive: include `hasThumbnail` and `thumbnailLink` in `drive ls`, `drive search`, and `drive get` JSON responses. (#486) — thanks @gtapps. - Secrets: time out macOS Keychain read/write/list operations with a clear recovery hint instead of hanging indefinitely when a permission prompt cannot surface. (#515, #513) — thanks @sardoru. diff --git a/internal/cmd/gmail.go b/internal/cmd/gmail.go index 895fd6e..4be0772 100644 --- a/internal/cmd/gmail.go +++ b/internal/cmd/gmail.go @@ -14,7 +14,7 @@ type GmailCmd struct { History GmailHistoryCmd `cmd:"" name:"history" group:"Read" help:"Gmail history"` Labels GmailLabelsCmd `cmd:"" name:"labels" aliases:"label" group:"Organize" help:"Label operations"` - Batch GmailBatchCmd `cmd:"" name:"batch" group:"Organize" help:"Batch operations"` + Batch GmailBatchCmd `cmd:"" name:"batch" group:"Organize" help:"Batch operations (permanent delete requires broader Gmail scope; use gmail trash for normal trashing)"` Archive GmailArchiveCmd `cmd:"" name:"archive" group:"Organize" help:"Archive messages (remove from inbox)"` Read GmailReadCmd `cmd:"" name:"mark-read" aliases:"read-messages" group:"Organize" help:"Mark messages as read"` Unread GmailUnreadCmd `cmd:"" name:"unread" aliases:"mark-unread" group:"Organize" help:"Mark messages as unread"` diff --git a/internal/cmd/gmail_batch.go b/internal/cmd/gmail_batch.go index 7cf9200..16a3797 100644 --- a/internal/cmd/gmail_batch.go +++ b/internal/cmd/gmail_batch.go @@ -12,7 +12,7 @@ import ( ) type GmailBatchCmd struct { - Delete GmailBatchDeleteCmd `cmd:"" name:"delete" aliases:"rm,del,remove" help:"Permanently delete multiple messages"` + Delete GmailBatchDeleteCmd `cmd:"" name:"delete" aliases:"rm,del,remove" help:"Permanently delete multiple messages; use 'gmail trash' to move messages to trash with the default gmail.modify scope"` Modify GmailBatchModifyCmd `cmd:"" name:"modify" aliases:"update,edit,set" help:"Modify labels on multiple messages"` } diff --git a/internal/cmd/gmail_labels_create_test.go b/internal/cmd/gmail_labels_create_test.go new file mode 100644 index 0000000..9c5451c --- /dev/null +++ b/internal/cmd/gmail_labels_create_test.go @@ -0,0 +1,72 @@ +package cmd + +import ( + "encoding/json" + "net/http" + "testing" +) + +func TestGmailLabelsCreateCmd_NestedNameDistinctFromFlatName(t *testing.T) { + createCalled := false + + newLabelsDeleteService(t, func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodGet && isLabelsListPath(r.URL.Path): + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{"labels": []map[string]any{ + {"id": "Label_flat", "name": "gog-pr-review", "type": "user"}, + }}) + return + case r.Method == http.MethodPost && isLabelsListPath(r.URL.Path): + createCalled = true + + var body struct { + Name string `json:"name"` + } + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if body.Name != "gog/pr-review" { + http.Error(w, "wrong label name", http.StatusBadRequest) + return + } + + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{ + "id": "Label_nested", + "name": body.Name, + "type": "user", + }) + return + default: + http.NotFound(w, r) + } + }) + + flags := &RootFlags{Account: "a@b.com"} + ctx := newLabelsDeleteContext(t, true) + + out := captureStdout(t, func() { + if err := runKong(t, &GmailLabelsCreateCmd{}, []string{"gog/pr-review"}, ctx, flags); err != nil { + t.Fatalf("execute: %v", err) + } + }) + + if !createCalled { + t.Fatal("expected label create call") + } + + var parsed struct { + Label struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"label"` + } + if err := json.Unmarshal([]byte(out), &parsed); err != nil { + t.Fatalf("json parse: %v\nout=%q", err, out) + } + if parsed.Label.ID != "Label_nested" || parsed.Label.Name != "gog/pr-review" { + t.Fatalf("unexpected label: %#v", parsed.Label) + } +} diff --git a/internal/cmd/tasks_due_test.go b/internal/cmd/tasks_due_test.go new file mode 100644 index 0000000..82771df --- /dev/null +++ b/internal/cmd/tasks_due_test.go @@ -0,0 +1,33 @@ +package cmd + +import ( + "bytes" + "strings" + "testing" + + "github.com/steipete/gogcli/internal/ui" +) + +func TestWarnTasksDueTime(t *testing.T) { + var stderr bytes.Buffer + u, err := ui.New(ui.Options{Stdout: &bytes.Buffer{}, Stderr: &stderr, Color: "never"}) + if err != nil { + t.Fatalf("ui.New: %v", err) + } + + warnTasksDueTime(u, "2025-01-01") + if stderr.Len() != 0 { + t.Fatalf("date-only due should not warn, got %q", stderr.String()) + } + + warnTasksDueTime(u, "2025-01-01T10:00:00Z") + if !strings.Contains(stderr.String(), "Google Tasks treats due dates as date-only") { + t.Fatalf("expected datetime warning, got %q", stderr.String()) + } + + stderr.Reset() + warnTasksDueTime(u, "2025-01-01 10:00") + if !strings.Contains(stderr.String(), "Google Tasks treats due dates as date-only") { + t.Fatalf("expected time warning, got %q", stderr.String()) + } +}