Skip to content

Commit dfa1615

Browse files
fix(gmail): make body truncation hints discoverable
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
1 parent 4a532bb commit dfa1615

9 files changed

Lines changed: 154 additions & 17 deletions

docs/commands/gog-gmail-thread-get.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ gog gmail (mail,email) thread (threads,read) get (info,show) <threadId> [flags]
2828
| `--enable-commands` | `string` | | Comma-separated list of enabled command prefixes; dot paths allowed (restricts CLI) |
2929
| `--enable-commands-exact` | `string` | | Comma-separated list of exact enabled commands; dot paths allowed and parent commands do not enable children |
3030
| `-y`<br>`--force`<br>`--assume-yes`<br>`--yes` | `bool` | | Skip confirmations for destructive commands |
31-
| `--full` | `bool` | | Show full message bodies |
31+
| `--full` | `bool` | | Show full message bodies without truncation |
3232
| `--gmail-no-send` | `bool` | false | Block Gmail send operations (agent safety) |
3333
| `-h`<br>`--help` | `kong.helpFlag` | | Show context-sensitive help. |
3434
| `--home` | `string` | | Override gogcli config/data/state/cache root (equivalent to GOG_HOME) |

internal/cmd/execute_gmail_messages_search_text_test.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,86 @@ func TestExecute_GmailMessagesSearch_JSON_IncludeBody(t *testing.T) {
199199
}
200200
}
201201

202+
func TestExecute_GmailMessagesSearch_Text_IncludeBodyTruncationDiscoverable(t *testing.T) {
203+
longBody := strings.Repeat("b", 240)
204+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
205+
path := r.URL.Path
206+
switch {
207+
case strings.Contains(path, "/users/me/messages") && !strings.Contains(path, "/users/me/messages/"):
208+
w.Header().Set("Content-Type", "application/json")
209+
_ = json.NewEncoder(w).Encode(map[string]any{
210+
"messages": []map[string]any{
211+
{"id": "m1", "threadId": "t1"},
212+
},
213+
})
214+
return
215+
case strings.Contains(path, "/users/me/messages/m1"):
216+
w.Header().Set("Content-Type", "application/json")
217+
_ = json.NewEncoder(w).Encode(map[string]any{
218+
"id": "m1",
219+
"threadId": "t1",
220+
"labelIds": []string{"INBOX"},
221+
"payload": map[string]any{
222+
"mimeType": "text/plain",
223+
"headers": []map[string]any{
224+
{"name": "From", "value": "Example <no-reply@example.com>"},
225+
{"name": "Subject", "value": "Receipt"},
226+
{"name": "Date", "value": "Mon, 02 Jan 2006 15:04:05 -0700"},
227+
},
228+
"body": map[string]any{
229+
"data": encodeBase64URL(longBody),
230+
},
231+
},
232+
})
233+
return
234+
case strings.Contains(path, "/users/me/labels"):
235+
w.Header().Set("Content-Type", "application/json")
236+
_ = json.NewEncoder(w).Encode(map[string]any{
237+
"labels": []map[string]any{
238+
{"id": "INBOX", "name": "INBOX", "type": "system"},
239+
},
240+
})
241+
return
242+
default:
243+
http.NotFound(w, r)
244+
return
245+
}
246+
}))
247+
defer srv.Close()
248+
249+
svc := newGmailServiceFromServer(t, srv)
250+
251+
defaultResult := executeWithGmailTestService(
252+
t,
253+
[]string{"--plain", "--account", "a@b.com", "gmail", "messages", "search", "from:example.com", "--include-body"},
254+
svc,
255+
)
256+
if defaultResult.err != nil {
257+
t.Fatalf("Execute: %v\nstderr=%q", defaultResult.err, defaultResult.stderr)
258+
}
259+
if !strings.Contains(defaultResult.stdout, strings.Repeat("b", 200)+gmailTextTruncationMarker) {
260+
t.Fatalf("expected actionable truncation marker, got: %q", defaultResult.stdout)
261+
}
262+
if strings.Contains(defaultResult.stdout, longBody) {
263+
t.Fatalf("expected body to be truncated, got: %q", defaultResult.stdout)
264+
}
265+
266+
fullResult := executeWithGmailTestService(
267+
t,
268+
[]string{"--plain", "--account", "a@b.com", "gmail", "messages", "search", "from:example.com", "--full"},
269+
svc,
270+
)
271+
if fullResult.err != nil {
272+
t.Fatalf("Execute full: %v\nstderr=%q", fullResult.err, fullResult.stderr)
273+
}
274+
if strings.Contains(fullResult.stdout, "[truncated") {
275+
t.Fatalf("expected full output without truncation marker, got: %q", fullResult.stdout)
276+
}
277+
if !strings.Contains(fullResult.stdout, longBody) {
278+
t.Fatalf("expected full body, got: %q", fullResult.stdout)
279+
}
280+
}
281+
202282
func TestExecute_GmailMessagesSearch_AppliesSystemLabelFilters(t *testing.T) {
203283
var gotQuery string
204284
var gotLabels []string

internal/cmd/execute_gmail_text_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ func TestExecute_GmailThread_Text_FullFlag(t *testing.T) {
134134
t.Fatalf("Execute: %v", result.err)
135135
}
136136

137-
if !strings.Contains(result.stdout, "[truncated") {
137+
if !strings.Contains(result.stdout, gmailTextTruncationMarker) {
138138
t.Fatalf("expected truncated output, got=%q", result.stdout)
139139
}
140140
if strings.Contains(result.stdout, longBody) {

internal/cmd/gmail_messages.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
const (
1818
gmailMessageBodyFormatText = "text"
1919
gmailMessageBodyFormatHTML = "html"
20+
gmailTextTruncationMarker = "... [truncated; use --full or --json]"
2021
)
2122

2223
type GmailMessagesCmd struct {
@@ -336,8 +337,5 @@ func truncateRunes(s string, maxLen int) string {
336337
if len(runes) <= maxLen {
337338
return s
338339
}
339-
if maxLen <= 3 {
340-
return string(runes[:maxLen])
341-
}
342-
return string(runes[:maxLen-3]) + "..."
340+
return string(runes[:maxLen]) + gmailTextTruncationMarker
343341
}

internal/cmd/gmail_messages_test.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,19 @@ import (
1616
func TestSanitizeMessageBody_TruncateUTF8(t *testing.T) {
1717
long := strings.Repeat("€", 210)
1818
got := sanitizeMessageBody(long, false)
19-
if !strings.HasSuffix(got, "...") {
19+
if !strings.HasSuffix(got, gmailTextTruncationMarker) {
2020
t.Fatalf("expected truncation suffix, got %q", got)
2121
}
22-
if len([]rune(got)) != 200 {
23-
t.Fatalf("expected 200 runes, got %d", len([]rune(got)))
22+
preview := strings.TrimSuffix(got, gmailTextTruncationMarker)
23+
if len([]rune(preview)) != 200 {
24+
t.Fatalf("expected 200 preview runes, got %d", len([]rune(preview)))
2425
}
2526
}
2627

2728
func TestSanitizeMessageBody_FullSkipsTruncation(t *testing.T) {
2829
long := strings.Repeat("€", 210)
2930
got := sanitizeMessageBody(long, true)
30-
if strings.HasSuffix(got, "...") {
31+
if strings.Contains(got, "[truncated") {
3132
t.Fatalf("expected no truncation with full=true, got %q", got)
3233
}
3334
if len([]rune(got)) != 210 {

internal/cmd/gmail_presentation_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ func TestGmailPresentationSchemas(t *testing.T) {
5959
t,
6060
got,
6161
"ID\tTHREAD\tDATE\tFROM\tSUBJECT\tLABELS\tBODY\n"+
62-
"m1\t\t\t\t\t\t"+strings.Repeat("x", 197)+"...\n",
62+
"m1\t\t\t\t\t\t"+strings.Repeat("x", 200)+gmailTextTruncationMarker+"\n",
6363
)
6464
})
6565

internal/cmd/gmail_thread.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ type GmailThreadCmd struct {
2525
type GmailThreadGetCmd struct {
2626
ThreadID string `arg:"" name:"threadId" help:"Thread ID"`
2727
Download bool `name:"download" help:"Download attachments"`
28-
Full bool `name:"full" help:"Show full message bodies"`
28+
Full bool `name:"full" help:"Show full message bodies without truncation"`
2929
SanitizeContent bool `name:"sanitize-content" aliases:"sanitize,safe" help:"Emit agent-oriented sanitized content: strip HTML, remove HTTP(S) URLs, and omit raw Gmail payloads from JSON"`
3030
OutputDir OutputDirFlag `embed:""`
3131
}
@@ -130,7 +130,7 @@ func (c *GmailThreadGetCmd) Run(ctx context.Context, flags *RootFlags) error {
130130
// Use runes to avoid breaking multi-byte UTF-8 characters
131131
runes := []rune(cleanBody)
132132
if len(runes) > 500 && !c.Full {
133-
cleanBody = string(runes[:500]) + "... [truncated]"
133+
cleanBody = string(runes[:500]) + gmailTextTruncationMarker
134134
}
135135
u.Out().Println(cleanBody)
136136
u.Out().Println("")

internal/cmd/help_printer_test.go

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -128,15 +128,28 @@ func TestHelpProfileNoColorEnv(t *testing.T) {
128128
}
129129

130130
func TestHelpProfileAlways(t *testing.T) {
131-
orig := os.Getenv("NO_COLOR")
132-
t.Cleanup(func() { _ = os.Setenv("NO_COLOR", orig) })
133-
134-
_ = os.Setenv("NO_COLOR", "")
131+
unsetColorEnv(t, "NO_COLOR", "CLICOLOR", "CLICOLOR_FORCE")
135132
if got := helpProfile(io.Discard, "always"); got != termenv.TrueColor {
136133
t.Fatalf("expected truecolor profile")
137134
}
138135
}
139136

137+
func unsetColorEnv(t *testing.T, keys ...string) {
138+
t.Helper()
139+
for _, key := range keys {
140+
key := key
141+
orig, ok := os.LookupEnv(key)
142+
t.Cleanup(func() {
143+
if ok {
144+
_ = os.Setenv(key, orig)
145+
return
146+
}
147+
_ = os.Unsetenv(key)
148+
})
149+
_ = os.Unsetenv(key)
150+
}
151+
}
152+
140153
func TestHelpOptionsEnv(t *testing.T) {
141154
orig := os.Getenv("GOG_HELP")
142155
t.Cleanup(func() { _ = os.Setenv("GOG_HELP", orig) })

internal/cmd/schema_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,51 @@ func TestExecute_SchemaIncludesAutomationContract(t *testing.T) {
115115
}
116116
}
117117

118+
func TestExecute_Schema_GmailTruncationHelp(t *testing.T) {
119+
threadDoc := schemaForCommand(t, "gmail thread get")
120+
threadFull := schemaFlagByName(t, threadDoc.Command, "full")
121+
if threadFull.Help != "Show full message bodies without truncation" {
122+
t.Fatalf("thread get --full help = %q", threadFull.Help)
123+
}
124+
125+
messagesDoc := schemaForCommand(t, "gmail messages search")
126+
messagesFull := schemaFlagByName(t, messagesDoc.Command, "full")
127+
if messagesFull.Help != "Show full message bodies without truncation (implies --include-body)" {
128+
t.Fatalf("messages search --full help = %q", messagesFull.Help)
129+
}
130+
includeBody := schemaFlagByName(t, messagesDoc.Command, "include-body")
131+
if includeBody.Help != "Include decoded message body (JSON is full; text output is truncated)" {
132+
t.Fatalf("messages search --include-body help = %q", includeBody.Help)
133+
}
134+
}
135+
136+
func schemaForCommand(t *testing.T, command string) schemaDoc {
137+
t.Helper()
138+
result := executeWithTestRuntime(t, []string{"schema", command}, nil)
139+
if result.err != nil {
140+
t.Fatalf("Execute schema %q: %v", command, result.err)
141+
}
142+
var doc schemaDoc
143+
if err := json.Unmarshal([]byte(result.stdout), &doc); err != nil {
144+
t.Fatalf("decode schema %q: %v", command, err)
145+
}
146+
if doc.Command == nil {
147+
t.Fatalf("schema %q missing command", command)
148+
}
149+
return doc
150+
}
151+
152+
func schemaFlagByName(t *testing.T, node *schemaNode, name string) schemaFlag {
153+
t.Helper()
154+
for _, flag := range node.Flags {
155+
if flag.Name == name {
156+
return flag
157+
}
158+
}
159+
t.Fatalf("%s missing --%s", node.Path, name)
160+
return schemaFlag{}
161+
}
162+
118163
func TestExecute_SchemaRejectsPlainMode(t *testing.T) {
119164
var runErr error
120165
errText := captureStderr(t, func() {

0 commit comments

Comments
 (0)