feat(app): report mutations on stderr and say what --web opens

Mutations printed a list row, or nothing, so a person could not tell
what changed and scripts parsed rows meant for people. Commands now get
the helpers for the gh convention:

- done and warn print "✓ Closed issue alice/x#5 (Fix login)" and
  "! … is already closed" to stderr, only when stdout is a terminal and
  --json is not set.
- printURL prints a created resource's URL to stdout unless --json is
  set, so url=$(tea issue create …) works.
- batchDone applies a mutation to several selections like batch and
  reports each success with a done line instead of a table.
- issueRef formats "alice/x#5 (title)" references.

--web now prints "Opening localhost:3001/alice/project/issues/1 in your
browser." to stderr on a terminal before the browser starts.

Signed-off-by: bircni <bircni@icloud.com>
This commit is contained in:
bircni committed 2026-10-11 22:43:11 +02:00
1 parent 55574a9fd0
commit 4fddcc6781
4 files changed
+167 -12

No files matched your search

+61
View File
@@ -6,6 +6,7 @@ package app
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"net/http"
@@ -348,6 +349,66 @@ func TestWriteView(t *testing.T) {
}
}
func TestMutationFeedback(t *testing.T) {
for _, tc := range []struct {
name, json, out, stderr string
tty bool
}{
{name: "terminal", tty: true, out: "https://example.org/12\n", stderr: "✓ Created issue alice/x#12 (Fix[2J login)\n! Issue alice/x#12 (Fix[2J login) is already closed\n"},
{name: "piped", out: "https://example.org/12\n"},
{name: "JSON", tty: true, json: "number"},
} {
t.Run(tc.name, func(t *testing.T) {
deps, out, diagnostics := testDeps(t)
deps.Streams.OutTTY = tc.tty
deps.Getenv = envFunc(map[string]string{"NO_COLOR": "1"})
a, cmd := outputApplication(deps, tc.json)
ref := issueRef("alice/x", 12, "Fix\x1b[2J login")
a.done(cmd, "Created issue %s", ref)
a.warn(cmd, "Issue %s is already closed", ref)
require.NoError(t, a.printURL(cmd, "https://example.org/12"))
require.Equal(t, tc.out, out.String())
require.Equal(t, tc.stderr, diagnostics.String())
})
}
deps, _, diagnostics := testDeps(t)
deps.Streams.OutTTY = true
a, cmd := outputApplication(deps, "")
a.done(cmd, "Closed issue %s", "alice/x#5 (Bug)")
a.warn(cmd, "nothing to do")
require.Equal(t, "\x1b[32m✓\x1b[0m Closed issue alice/x#5 (Bug)\n\x1b[33m!\x1b[0m nothing to do\n", diagnostics.String())
}
func TestBatchDoneReportsEachSuccess(t *testing.T) {
run := func(n int64) (listed, error) {
if n == 2 {
return listed{}, errors.New("not found")
}
return listed{Number: n, Title: fmt.Sprintf("Issue %d", n)}, nil
}
label := func(n int64) string { return fmt.Sprintf("#%d", n) }
report := func(item listed) string { return "Closed issue " + issueRef("alice/x", item.Number, item.Title) }
for _, tc := range []struct {
name, json, out, stderr string
tty bool
}{
{name: "terminal", tty: true, stderr: "✓ Closed issue alice/x#1 (Issue 1)\n✓ Closed issue alice/x#3 (Issue 3)\n"},
{name: "piped"},
{name: "JSON", json: "number", out: `[{"number":1},{"number":3}]` + "\n"},
} {
t.Run(tc.name, func(t *testing.T) {
deps, out, diagnostics := testDeps(t)
deps.Streams.OutTTY = tc.tty
deps.Getenv = envFunc(map[string]string{"NO_COLOR": "1"})
a, cmd := outputApplication(deps, tc.json)
err := batchDone(a, cmd, []int64{1, 2, 3}, label, run, report)
require.EqualError(t, err, "#2: not found")
require.Equal(t, tc.out, out.String())
require.Equal(t, tc.stderr, diagnostics.String())
})
}
}
func TestShowing(t *testing.T) {
for _, tc := range []struct {
shown int