← back to Cli Printing Press
fix(cli): preserve body/content payload on single-object compact path (#1509)
c7e1b82b85ef47df1a011100fcb31a42a095b7ac · 2026-05-16 02:52:06 -0700 · Trevin Chow
* fix(cli): preserve body/content payload on single-object compact path
The single helpers.go map compactVerboseFields was consulted by both
compactListFields and compactObjectFields. On a list, "body"/"content"/
"html"/"markdown" are verbose noise and stripping them is right. On a
single-object `get --agent` response those fields are the primary
payload, and stripping them silently returns a useless envelope with
no error and no stderr signal.
Split the map into compactVerboseListFields (unchanged contents) and
compactVerboseObjectFields (description/comments/attachments only),
so compactObjectFields preserves payload fields while list compaction
behaves exactly as before. Agents that still want to omit body/content
on `get` can pass `--select`.
Closes #1481
* test(cli): use balanced-brace scan to bound compactVerboseObjectFields literal
Greptile flagged that strings.Index(body[objStart:], "}") returns the
first `}` after the map declaration, which works today (no intermediate
braces) but would silently truncate objBody if a future edit added a
field whose value contained `}` or an inline comment with `}`, causing
the NotContains assertions to give false-positive passes.
Replace with a small matchClosingBrace helper that counts brace depth
from the first `{` and returns the index of the matching close. Same
test semantics; resilient to future map shape changes.
---------
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Files touched
M internal/generator/generator_test.goM internal/generator/templates/helpers.go.tmplM testdata/golden/expected/generate-embedded-paged-api/embedded-paged-api/internal/cli/helpers.goM testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/cli/helpers.goM testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/helpers.go
Diff
commit c7e1b82b85ef47df1a011100fcb31a42a095b7ac
Author: Trevin Chow <trevin@trevinchow.com>
Date: Sat May 16 02:52:06 2026 -0700
fix(cli): preserve body/content payload on single-object compact path (#1509)
* fix(cli): preserve body/content payload on single-object compact path
The single helpers.go map compactVerboseFields was consulted by both
compactListFields and compactObjectFields. On a list, "body"/"content"/
"html"/"markdown" are verbose noise and stripping them is right. On a
single-object `get --agent` response those fields are the primary
payload, and stripping them silently returns a useless envelope with
no error and no stderr signal.
Split the map into compactVerboseListFields (unchanged contents) and
compactVerboseObjectFields (description/comments/attachments only),
so compactObjectFields preserves payload fields while list compaction
behaves exactly as before. Agents that still want to omit body/content
on `get` can pass `--select`.
Closes #1481
* test(cli): use balanced-brace scan to bound compactVerboseObjectFields literal
Greptile flagged that strings.Index(body[objStart:], "}") returns the
first `}` after the map declaration, which works today (no intermediate
braces) but would silently truncate objBody if a future edit added a
field whose value contained `}` or an inline comment with `}`, causing
the NotContains assertions to give false-positive passes.
Replace with a small matchClosingBrace helper that counts brace depth
from the first `{` and returns the index of the matching close. Same
test semantics; resilient to future map shape changes.
---------
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
---
internal/generator/generator_test.go | 59 +++++++++++++++++++++-
internal/generator/templates/helpers.go.tmpl | 30 ++++++++---
.../embedded-paged-api/internal/cli/helpers.go | 30 ++++++++---
.../internal/cli/helpers.go | 30 ++++++++---
.../printing-press-golden/internal/cli/helpers.go | 30 ++++++++---
5 files changed, 146 insertions(+), 33 deletions(-)
diff --git a/internal/generator/generator_test.go b/internal/generator/generator_test.go
index da908f17..5bd95d38 100644
--- a/internal/generator/generator_test.go
+++ b/internal/generator/generator_test.go
@@ -4054,12 +4054,69 @@ func TestCompactListFieldsPreservesUnknownShapes(t *testing.T) {
"compactListFields must count per-key occurrence so frequent novel-command keys survive")
assert.Contains(t, body, "isCompactScalar",
"compactListFields must filter the data-driven extension by scalar type so nested objects/arrays don't bloat --compact output")
- assert.Contains(t, body, "compactVerboseFields",
+ assert.Contains(t, body, "compactVerboseListFields",
"compactListFields must exclude description/body/content from the data-driven extension regardless of frequency")
assert.Contains(t, body, "threshold",
"compactListFields must compute a frequency threshold for the data-driven extension")
}
+// matchClosingBrace walks s from start, finds the first `{`, then returns
+// the index of the matching `}` by counting depth. Returns -1 if either no
+// opening brace exists at/after start or the input is unbalanced.
+func matchClosingBrace(s string, start int) int {
+ depth := 0
+ seenOpen := false
+ for i := start; i < len(s); i++ {
+ switch s[i] {
+ case '{':
+ depth++
+ seenOpen = true
+ case '}':
+ depth--
+ if seenOpen && depth == 0 {
+ return i
+ }
+ }
+ }
+ return -1
+}
+
+// TestCompactObjectFieldsPreservesPayloadFields pins the contract that
+// single-object `get` responses retain their primary payload fields under
+// `--agent`/`--compact`. The list-path blocklist correctly strips body/
+// content/html/markdown from list items (verbose noise), but applying the
+// same blocklist to single-object responses silently drops the field the
+// caller asked for. Pinned at the template level so the two blocklists stay
+// separate as the helper renders into every printed CLI.
+func TestCompactObjectFieldsPreservesPayloadFields(t *testing.T) {
+ t.Parallel()
+
+ path := filepath.Join("templates", "helpers.go.tmpl")
+ data, err := os.ReadFile(path)
+ require.NoError(t, err, "template must exist: %s", path)
+ body := string(data)
+
+ require.Contains(t, body, "compactVerboseObjectFields",
+ "compactObjectFields must read from a dedicated object-path blocklist, not the list-path one")
+ require.Contains(t, body, "if !compactVerboseObjectFields[k] {",
+ "compactObjectFields must consult the object-path blocklist when deciding which keys to keep")
+
+ objStart := strings.Index(body, "compactVerboseObjectFields = map[string]bool{")
+ require.GreaterOrEqual(t, objStart, 0, "compactVerboseObjectFields map literal must exist")
+ objEnd := matchClosingBrace(body, objStart)
+ require.GreaterOrEqual(t, objEnd, 0, "compactVerboseObjectFields map literal must close")
+ objBody := body[objStart : objEnd+1]
+
+ for _, payload := range []string{`"body"`, `"content"`, `"html"`, `"markdown"`} {
+ assert.NotContains(t, objBody, payload,
+ "compactVerboseObjectFields must not list %s — those fields are the primary payload on single-object get responses", payload)
+ }
+ for _, metadata := range []string{`"description"`, `"comments"`, `"attachments"`} {
+ assert.Contains(t, objBody, metadata,
+ "compactVerboseObjectFields must still strip %s — that field is metadata on single-object responses, not payload", metadata)
+ }
+}
+
// The cursor param's "0" must survive paginatedGet's zero-value strip:
// offset-paginated APIs require offset=0 on the first page request.
func TestPaginatedGetExemptsCursorParamFromZeroStripping(t *testing.T) {
diff --git a/internal/generator/templates/helpers.go.tmpl b/internal/generator/templates/helpers.go.tmpl
index abdca004..7ef3fd34 100644
--- a/internal/generator/templates/helpers.go.tmpl
+++ b/internal/generator/templates/helpers.go.tmpl
@@ -1122,14 +1122,25 @@ func extractResponseData(data json.RawMessage) json.RawMessage {
}
}
-// compactVerboseFields are the prose-shaped fields stripped by both the
-// list and single-object compact paths. Centralised so the contract stays
-// in lockstep across both helpers.
-var compactVerboseFields = map[string]bool{
+// compactVerboseListFields are prose-shaped fields stripped from list-item
+// projections. On lists, "body"/"content"/"html"/"markdown" are verbose
+// noise and the row's identity is carried by id/name/title/etc.
+var compactVerboseListFields = map[string]bool{
"description": true, "body": true, "content": true,
"comments": true, "attachments": true, "html": true, "markdown": true,
}
+// compactVerboseObjectFields are metadata fields stripped from single-object
+// responses. "body"/"content"/"html"/"markdown" are intentionally absent:
+// for a `get` command those fields are the primary payload, and stripping
+// them under `--agent`/`--compact` silently emits a useless envelope.
+// Use `--select` to drop them explicitly.
+var compactVerboseObjectFields = map[string]bool{
+ "description": true,
+ "comments": true,
+ "attachments": true,
+}
+
// compactFields keeps only the most important fields for agent consumption.
// For arrays: allowlist of high-gravity fields (no descriptions).
// For single objects: blocklist that strips known-verbose fields (descriptions, comments, etc.).
@@ -1194,7 +1205,7 @@ func compactListFields(items []map[string]any) json.RawMessage {
keyCounts := map[string]int{}
for _, item := range items {
for k, v := range item {
- if compactVerboseFields[k] || !isCompactScalar(v) {
+ if compactVerboseListFields[k] || !isCompactScalar(v) {
continue
}
keyCounts[k]++
@@ -1247,12 +1258,15 @@ func isCompactScalar(v any) bool {
}
}
-// compactObjectFields strips known-verbose fields from single-object responses.
-// Uses a blocklist so it works across all API domains (project management, payments, CRM, etc.).
+// compactObjectFields strips known-verbose metadata fields from single-object
+// responses. The blocklist deliberately excludes "body"/"content"/"html"/
+// "markdown" — those fields are payload on `get` commands and stripping them
+// under `--agent`/`--compact` is a silent loss; agents who want to omit them
+// can pass `--select` to specify only the fields they need.
func compactObjectFields(obj map[string]any) json.RawMessage {
compact := map[string]any{}
for k, v := range obj {
- if !compactVerboseFields[k] {
+ if !compactVerboseObjectFields[k] {
compact[k] = v
}
}
diff --git a/testdata/golden/expected/generate-embedded-paged-api/embedded-paged-api/internal/cli/helpers.go b/testdata/golden/expected/generate-embedded-paged-api/embedded-paged-api/internal/cli/helpers.go
index 662fafb7..23502a66 100644
--- a/testdata/golden/expected/generate-embedded-paged-api/embedded-paged-api/internal/cli/helpers.go
+++ b/testdata/golden/expected/generate-embedded-paged-api/embedded-paged-api/internal/cli/helpers.go
@@ -921,14 +921,25 @@ func extractResponseData(data json.RawMessage) json.RawMessage {
}
}
-// compactVerboseFields are the prose-shaped fields stripped by both the
-// list and single-object compact paths. Centralised so the contract stays
-// in lockstep across both helpers.
-var compactVerboseFields = map[string]bool{
+// compactVerboseListFields are prose-shaped fields stripped from list-item
+// projections. On lists, "body"/"content"/"html"/"markdown" are verbose
+// noise and the row's identity is carried by id/name/title/etc.
+var compactVerboseListFields = map[string]bool{
"description": true, "body": true, "content": true,
"comments": true, "attachments": true, "html": true, "markdown": true,
}
+// compactVerboseObjectFields are metadata fields stripped from single-object
+// responses. "body"/"content"/"html"/"markdown" are intentionally absent:
+// for a `get` command those fields are the primary payload, and stripping
+// them under `--agent`/`--compact` silently emits a useless envelope.
+// Use `--select` to drop them explicitly.
+var compactVerboseObjectFields = map[string]bool{
+ "description": true,
+ "comments": true,
+ "attachments": true,
+}
+
// compactFields keeps only the most important fields for agent consumption.
// For arrays: allowlist of high-gravity fields (no descriptions).
// For single objects: blocklist that strips known-verbose fields (descriptions, comments, etc.).
@@ -993,7 +1004,7 @@ func compactListFields(items []map[string]any) json.RawMessage {
keyCounts := map[string]int{}
for _, item := range items {
for k, v := range item {
- if compactVerboseFields[k] || !isCompactScalar(v) {
+ if compactVerboseListFields[k] || !isCompactScalar(v) {
continue
}
keyCounts[k]++
@@ -1046,12 +1057,15 @@ func isCompactScalar(v any) bool {
}
}
-// compactObjectFields strips known-verbose fields from single-object responses.
-// Uses a blocklist so it works across all API domains (project management, payments, CRM, etc.).
+// compactObjectFields strips known-verbose metadata fields from single-object
+// responses. The blocklist deliberately excludes "body"/"content"/"html"/
+// "markdown" — those fields are payload on `get` commands and stripping them
+// under `--agent`/`--compact` is a silent loss; agents who want to omit them
+// can pass `--select` to specify only the fields they need.
func compactObjectFields(obj map[string]any) json.RawMessage {
compact := map[string]any{}
for k, v := range obj {
- if !compactVerboseFields[k] {
+ if !compactVerboseObjectFields[k] {
compact[k] = v
}
}
diff --git a/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/cli/helpers.go b/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/cli/helpers.go
index 5826f23a..85b8e7df 100644
--- a/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/cli/helpers.go
+++ b/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/cli/helpers.go
@@ -784,14 +784,25 @@ func extractResponseData(data json.RawMessage) json.RawMessage {
}
}
-// compactVerboseFields are the prose-shaped fields stripped by both the
-// list and single-object compact paths. Centralised so the contract stays
-// in lockstep across both helpers.
-var compactVerboseFields = map[string]bool{
+// compactVerboseListFields are prose-shaped fields stripped from list-item
+// projections. On lists, "body"/"content"/"html"/"markdown" are verbose
+// noise and the row's identity is carried by id/name/title/etc.
+var compactVerboseListFields = map[string]bool{
"description": true, "body": true, "content": true,
"comments": true, "attachments": true, "html": true, "markdown": true,
}
+// compactVerboseObjectFields are metadata fields stripped from single-object
+// responses. "body"/"content"/"html"/"markdown" are intentionally absent:
+// for a `get` command those fields are the primary payload, and stripping
+// them under `--agent`/`--compact` silently emits a useless envelope.
+// Use `--select` to drop them explicitly.
+var compactVerboseObjectFields = map[string]bool{
+ "description": true,
+ "comments": true,
+ "attachments": true,
+}
+
// compactFields keeps only the most important fields for agent consumption.
// For arrays: allowlist of high-gravity fields (no descriptions).
// For single objects: blocklist that strips known-verbose fields (descriptions, comments, etc.).
@@ -856,7 +867,7 @@ func compactListFields(items []map[string]any) json.RawMessage {
keyCounts := map[string]int{}
for _, item := range items {
for k, v := range item {
- if compactVerboseFields[k] || !isCompactScalar(v) {
+ if compactVerboseListFields[k] || !isCompactScalar(v) {
continue
}
keyCounts[k]++
@@ -909,12 +920,15 @@ func isCompactScalar(v any) bool {
}
}
-// compactObjectFields strips known-verbose fields from single-object responses.
-// Uses a blocklist so it works across all API domains (project management, payments, CRM, etc.).
+// compactObjectFields strips known-verbose metadata fields from single-object
+// responses. The blocklist deliberately excludes "body"/"content"/"html"/
+// "markdown" — those fields are payload on `get` commands and stripping them
+// under `--agent`/`--compact` is a silent loss; agents who want to omit them
+// can pass `--select` to specify only the fields they need.
func compactObjectFields(obj map[string]any) json.RawMessage {
compact := map[string]any{}
for k, v := range obj {
- if !compactVerboseFields[k] {
+ if !compactVerboseObjectFields[k] {
compact[k] = v
}
}
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/helpers.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/helpers.go
index c2b796cd..9000da76 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/helpers.go
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/helpers.go
@@ -791,14 +791,25 @@ func extractResponseData(data json.RawMessage) json.RawMessage {
}
}
-// compactVerboseFields are the prose-shaped fields stripped by both the
-// list and single-object compact paths. Centralised so the contract stays
-// in lockstep across both helpers.
-var compactVerboseFields = map[string]bool{
+// compactVerboseListFields are prose-shaped fields stripped from list-item
+// projections. On lists, "body"/"content"/"html"/"markdown" are verbose
+// noise and the row's identity is carried by id/name/title/etc.
+var compactVerboseListFields = map[string]bool{
"description": true, "body": true, "content": true,
"comments": true, "attachments": true, "html": true, "markdown": true,
}
+// compactVerboseObjectFields are metadata fields stripped from single-object
+// responses. "body"/"content"/"html"/"markdown" are intentionally absent:
+// for a `get` command those fields are the primary payload, and stripping
+// them under `--agent`/`--compact` silently emits a useless envelope.
+// Use `--select` to drop them explicitly.
+var compactVerboseObjectFields = map[string]bool{
+ "description": true,
+ "comments": true,
+ "attachments": true,
+}
+
// compactFields keeps only the most important fields for agent consumption.
// For arrays: allowlist of high-gravity fields (no descriptions).
// For single objects: blocklist that strips known-verbose fields (descriptions, comments, etc.).
@@ -863,7 +874,7 @@ func compactListFields(items []map[string]any) json.RawMessage {
keyCounts := map[string]int{}
for _, item := range items {
for k, v := range item {
- if compactVerboseFields[k] || !isCompactScalar(v) {
+ if compactVerboseListFields[k] || !isCompactScalar(v) {
continue
}
keyCounts[k]++
@@ -916,12 +927,15 @@ func isCompactScalar(v any) bool {
}
}
-// compactObjectFields strips known-verbose fields from single-object responses.
-// Uses a blocklist so it works across all API domains (project management, payments, CRM, etc.).
+// compactObjectFields strips known-verbose metadata fields from single-object
+// responses. The blocklist deliberately excludes "body"/"content"/"html"/
+// "markdown" — those fields are payload on `get` commands and stripping them
+// under `--agent`/`--compact` is a silent loss; agents who want to omit them
+// can pass `--select` to specify only the fields they need.
func compactObjectFields(obj map[string]any) json.RawMessage {
compact := map[string]any{}
for k, v := range obj {
- if !compactVerboseFields[k] {
+ if !compactVerboseObjectFields[k] {
compact[k] = v
}
}
← fe4a5ec3 feat(cli): catalog auth_env_vars declares canonical credenti
·
back to Cli Printing Press
·
fix(cli): synthesize undeclared path placeholders in OpenAPI bf9014d4 →