fix(app): pretty-print JSON on a terminal and reconcile the foundation streams

writeJSON uses the indented, colored JSON writer when stdout is a
terminal. Help and JSON tests now expect the -L shorthand column and
compare terminal JSON without color sequences.

Signed-off-by: bircni <bircni@icloud.com>
This commit is contained in:
bircni committed 2026-10-11 22:57:38 +02:00
1 parent 04d5f2aed0
commit 48a188ceac
4 files changed
+16 -7

No files matched your search

+1 -1
View File
@@ -264,7 +264,7 @@ func TestCommandHelpShowsRelevantFlagsOnly(t *testing.T) {
} }
out.Reset() out.Reset()
require.NoError(t, Execute(context.Background(), deps, []string{"label", "list", "--help"})) require.NoError(t, Execute(context.Background(), deps, []string{"label", "list", "--help"}))
require.Contains(t, out.String(), "\nFLAGS\n --limit int Maximum results (1..100) (default 30)\n --org string ") require.Contains(t, out.String(), "\nFLAGS\n -L, --limit int Maximum number of results (1..1000) (default 30)\n --org string ")
for _, args := range [][]string{{"version", "--help"}, {"alias", "list", "--help"}, {"auth", "status", "--help"}} { for _, args := range [][]string{{"version", "--help"}, {"alias", "list", "--help"}, {"auth", "status", "--help"}} {
out.Reset() out.Reset()
require.NoError(t, Execute(context.Background(), deps, args)) require.NoError(t, Execute(context.Background(), deps, args))
+8
View File
@@ -15,6 +15,7 @@ import (
"net/http/httptest" "net/http/httptest"
"os" "os"
"path/filepath" "path/filepath"
"regexp"
"strings" "strings"
"testing" "testing"
@@ -291,3 +292,10 @@ func selectablesHandler(t *testing.T, push bool, next http.HandlerFunc) http.Han
} }
} }
} }
var ansiSequence = regexp.MustCompile("\x1b\\[[0-9;]*m")
// withoutANSI drops SGR color sequences, such as those in pretty JSON on a terminal.
func withoutANSI(s string) string {
return ansiSequence.ReplaceAllString(s, "")
}
+2 -1
View File
@@ -55,7 +55,8 @@ func (a *application) writeHuman(cmd *cobra.Command, text string) error {
// writeJSON writes v as JSON, filtered by --json fields and --jq. // writeJSON writes v as JSON, filtered by --json fields and --jq.
func (a *application) writeJSON(cmd *cobra.Command, v any) error { func (a *application) writeJSON(cmd *cobra.Command, v any) error {
return terminal.WriteJSON(cmd.OutOrStdout(), v, a.options.json, a.options.jq) pretty := a.deps.Streams.OutTTY
return terminal.JSONOutput{Fields: a.options.json, Query: a.options.jq, Pretty: pretty, Color: pretty && a.colorEnabled()}.Write(cmd.OutOrStdout(), v)
} }
// listSpec describes how a list command shows its items to people. // listSpec describes how a list command shows its items to people.
+5 -5
View File
@@ -93,7 +93,7 @@ func TestIssueViewJSONDoesNotPage(t *testing.T) {
deps, out, _, pager := pagedDeps(t, server.URL, map[string]string{"PAGER": "fake-pager"}) deps, out, _, pager := pagedDeps(t, server.URL, map[string]string{"PAGER": "fake-pager"})
require.NoError(t, Execute(context.Background(), deps, []string{"issue", "view", "7", "-R", "alice/project", "--json=number,title", "--no-input"})) require.NoError(t, Execute(context.Background(), deps, []string{"issue", "view", "7", "-R", "alice/project", "--json=number,title", "--no-input"}))
require.Zero(t, pager.calls) require.Zero(t, pager.calls)
require.JSONEq(t, `{"number":7,"title":"Title"}`, out.String()) require.JSONEq(t, `{"number":7,"title":"Title"}`, withoutANSI(out.String()))
} }
func TestConfiguredPagerOverridesPagerEnvironment(t *testing.T) { func TestConfiguredPagerOverridesPagerEnvironment(t *testing.T) {
@@ -121,10 +121,10 @@ func TestTeaPagerAndColorEnvironmentOverrides(t *testing.T) {
require.Contains(t, pager.input, "\x1b[38;5;196mbug\x1b[0m") require.Contains(t, pager.input, "\x1b[38;5;196mbug\x1b[0m")
out.Reset() out.Reset()
require.NoError(t, Execute(context.Background(), deps, []string{"config", "get", "pager", "--show-source", "--json"})) require.NoError(t, Execute(context.Background(), deps, []string{"config", "get", "pager", "--show-source", "--json"}))
require.JSONEq(t, `{"pager":{"value":"tea-pager","source":"TEA_PAGER"}}`, out.String()) require.JSONEq(t, `{"pager":{"value":"tea-pager","source":"TEA_PAGER"}}`, withoutANSI(out.String()))
out.Reset() out.Reset()
require.NoError(t, Execute(context.Background(), deps, []string{"config", "get", "color", "--show-source", "--json"})) require.NoError(t, Execute(context.Background(), deps, []string{"config", "get", "color", "--show-source", "--json"}))
require.JSONEq(t, `{"color":{"value":"always","source":"TEA_COLOR"}}`, out.String()) require.JSONEq(t, `{"color":{"value":"always","source":"TEA_COLOR"}}`, withoutANSI(out.String()))
} }
func TestColorAutoUsesStdoutTTY(t *testing.T) { func TestColorAutoUsesStdoutTTY(t *testing.T) {
@@ -308,7 +308,7 @@ func TestWriteTable(t *testing.T) {
}, },
{name: "empty message on a terminal", tty: true, page: listPage{Number: 1}, stderr: "no open issues in alice/x\n"}, {name: "empty message on a terminal", tty: true, page: listPage{Number: 1}, stderr: "no open issues in alice/x\n"},
{name: "nothing when empty and piped", page: listPage{Number: 1}}, {name: "nothing when empty and piped", page: listPage{Number: 1}},
{name: "JSON", tty: true, json: "number", items: items[:1], page: listPage{Number: 1, More: true}, out: `[{"number":12}]` + "\n"}, {name: "JSON", tty: true, json: "number", items: items[:1], page: listPage{Number: 1, More: true}, out: "[\n {\n \"number\": 12\n }\n]\n"},
} { } {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
deps, out, diagnostics := testDeps(t) deps, out, diagnostics := testDeps(t)
@@ -336,7 +336,7 @@ func TestWriteView(t *testing.T) {
}{ }{
{name: "terminal", tty: true, want: "Login fails #1\nOpen • alice opened about 2 hours ago\n\nSteps\n\nView this issue on Gitea: https://example.org/1\n"}, {name: "terminal", tty: true, want: "Login fails #1\nOpen • alice opened about 2 hours ago\n\nSteps\n\nView this issue on Gitea: https://example.org/1\n"},
{name: "piped", want: "title:\tLogin fails\nurl:\thttps://example.org/1\n--\nSteps\n"}, {name: "piped", want: "title:\tLogin fails\nurl:\thttps://example.org/1\n--\nSteps\n"},
{name: "JSON", tty: true, json: "title", want: `{"title":"Login fails"}` + "\n"}, {name: "JSON", tty: true, json: "title", want: "{\n \"title\": \"Login fails\"\n}\n"},
} { } {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
deps, out, _ := testDeps(t) deps, out, _ := testDeps(t)