Skip to content

Commit b74f5e2

Browse files
committed
refactor: derive StderrIsTerminal as a precomputed field
Mirror the existing IsTerminal field instead of a live method, computed once in NewIOStreams. This drops the stderrIsTerminal package-level test seam (the gate now reads the field directly); tests inject the field like bind_test.go does for IsTerminal. The warnIfProxied seam stays, since the real function is guarded by an internal sync.Once and cannot be counted across subtests.
1 parent 1a941ba commit b74f5e2

3 files changed

Lines changed: 38 additions & 37 deletions

File tree

internal/cmdutil/factory_default.go

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -100,19 +100,17 @@ func safeRedirectPolicy(req *http.Request, via []*http.Request) error {
100100
return nil
101101
}
102102

103-
// Seams for unit-testing the proxy-warning gate. Production wires them to the
104-
// real implementations; tests substitute a predicate and a spy to verify the
105-
// warning fires only on an interactive stderr (see factory_proxy_warn_test.go).
106-
// StderrIsTerminal is a concrete method (real TTY only) and WarnIfProxied is a
107-
// package function, so neither is otherwise injectable.
108-
var (
109-
warnIfProxied = transport.WarnIfProxied
110-
stderrIsTerminal = func(s *IOStreams) bool { return s.StderrIsTerminal() }
111-
)
103+
// warnIfProxied is a test seam for the proxy-warning gate. Production wires it
104+
// to transport.WarnIfProxied; tests swap in a spy to count invocations. It is
105+
// needed because the real function is guarded by an internal sync.Once, so
106+
// calling it directly would only fire on the first test (see
107+
// factory_proxy_warn_test.go). The terminal check is the IOStreams
108+
// .StderrIsTerminal field, which tests set directly.
109+
var warnIfProxied = transport.WarnIfProxied
112110

113111
func cachedHttpClientFunc(f *Factory) func() (*http.Client, error) {
114112
return sync.OnceValues(func() (*http.Client, error) {
115-
if stderrIsTerminal(f.IOStreams) {
113+
if f.IOStreams.StderrIsTerminal {
116114
warnIfProxied(f.IOStreams.ErrOut)
117115
}
118116

@@ -141,7 +139,7 @@ func cachedLarkClientFunc(f *Factory) func() (*lark.Client, error) {
141139
lark.WithLogLevel(larkcore.LogLevelError),
142140
lark.WithHeaders(BaseSecurityHeaders()),
143141
}
144-
if stderrIsTerminal(f.IOStreams) {
142+
if f.IOStreams.StderrIsTerminal {
145143
warnIfProxied(f.IOStreams.ErrOut)
146144
}
147145
opts = append(opts, lark.WithHttpClient(&http.Client{

internal/cmdutil/factory_proxy_warn_test.go

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,16 @@ import (
1111
"github.com/larksuite/cli/internal/envvars"
1212
)
1313

14-
// installProxyWarnSpy swaps the package-level proxy-warning seams for one test:
15-
// stderrIsTerminal is forced to `terminal`, and warnIfProxied is replaced with a
16-
// counter. The real implementations are restored on cleanup. Returns a pointer
17-
// to the call count so the caller can assert how many times the warning fired.
18-
func installProxyWarnSpy(t *testing.T, terminal bool) *int {
14+
// installProxyWarnSpy replaces warnIfProxied with a counter for one test and
15+
// restores it on cleanup. Returns a pointer to the call count so the caller can
16+
// assert how many times the warning fired. The terminal state is controlled via
17+
// the IOStreams.StderrIsTerminal field, not a seam.
18+
func installProxyWarnSpy(t *testing.T) *int {
1919
t.Helper()
20-
prevWarn, prevTTY := warnIfProxied, stderrIsTerminal
21-
t.Cleanup(func() {
22-
warnIfProxied, stderrIsTerminal = prevWarn, prevTTY
23-
})
20+
prevWarn := warnIfProxied
21+
t.Cleanup(func() { warnIfProxied = prevWarn })
2422
calls := 0
2523
warnIfProxied = func(io.Writer) { calls++ }
26-
stderrIsTerminal = func(*IOStreams) bool { return terminal }
2724
return &calls
2825
}
2926

@@ -41,9 +38,11 @@ var proxyWarnGateCases = []struct {
4138
func TestCachedHttpClientFunc_ProxyWarnGate(t *testing.T) {
4239
for _, tc := range proxyWarnGateCases {
4340
t.Run(tc.name, func(t *testing.T) {
44-
calls := installProxyWarnSpy(t, tc.terminal)
41+
calls := installProxyWarnSpy(t)
4542

46-
fn := cachedHttpClientFunc(&Factory{IOStreams: &IOStreams{ErrOut: io.Discard}})
43+
fn := cachedHttpClientFunc(&Factory{IOStreams: &IOStreams{
44+
ErrOut: io.Discard, StderrIsTerminal: tc.terminal,
45+
}})
4746
if _, err := fn(); err != nil {
4847
t.Fatalf("http client init: %v", err)
4948
}
@@ -69,9 +68,11 @@ func TestCachedLarkClientFunc_ProxyWarnGate(t *testing.T) {
6968
t.Setenv(envvars.CliTenantAccessToken, "")
7069
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
7170

72-
calls := installProxyWarnSpy(t, tc.terminal)
71+
calls := installProxyWarnSpy(t)
7372

74-
f := NewDefault(&IOStreams{ErrOut: io.Discard}, InvocationContext{})
73+
// normalizeStreams copies the struct (out := *s), so the
74+
// StderrIsTerminal field survives into f.IOStreams.
75+
f := NewDefault(&IOStreams{ErrOut: io.Discard, StderrIsTerminal: tc.terminal}, InvocationContext{})
7576
if _, err := cachedLarkClientFunc(f)(); err != nil {
7677
t.Fatalf("lark client init: %v", err)
7778
}

internal/cmdutil/iostreams.go

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,26 +18,28 @@ type IOStreams struct {
1818
Out io.Writer
1919
ErrOut io.Writer
2020
IsTerminal bool
21+
// StderrIsTerminal reports whether ErrOut is an interactive terminal.
22+
// Advisory warnings written to stderr (e.g. the proxy notice) gate on this
23+
// so they stay out of non-interactive output (pipes, CI, agent runs).
24+
// Computed once in NewIOStreams, mirroring IsTerminal; tests assign it
25+
// directly like cmd/config/bind_test.go does for IsTerminal.
26+
StderrIsTerminal bool
2127
}
2228

2329
// NewIOStreams builds an IOStreams from arbitrary readers/writers.
24-
// IsTerminal is derived from in's underlying *os.File, if any; non-file
25-
// readers (bytes.Buffer, strings.Reader, …) yield IsTerminal=false.
30+
// IsTerminal / StderrIsTerminal are derived from in's / errOut's underlying
31+
// *os.File, if any; non-file streams (bytes.Buffer, strings.Reader, …) yield
32+
// false.
2633
func NewIOStreams(in io.Reader, out, errOut io.Writer) *IOStreams {
2734
isTerminal := false
2835
if f, ok := in.(*os.File); ok {
2936
isTerminal = term.IsTerminal(int(f.Fd()))
3037
}
31-
return &IOStreams{In: in, Out: out, ErrOut: errOut, IsTerminal: isTerminal}
32-
}
33-
34-
// StderrIsTerminal reports whether ErrOut is an interactive terminal. Advisory
35-
// warnings written to stderr (e.g. the proxy notice) gate on this so they stay
36-
// out of non-interactive output (pipes, CI, agent runs). Buffers (tests) and
37-
// redirects are not *os.File terminals, so they yield false.
38-
func (s *IOStreams) StderrIsTerminal() bool {
39-
f, ok := s.ErrOut.(*os.File)
40-
return ok && term.IsTerminal(int(f.Fd()))
38+
stderrIsTerminal := false
39+
if f, ok := errOut.(*os.File); ok {
40+
stderrIsTerminal = term.IsTerminal(int(f.Fd()))
41+
}
42+
return &IOStreams{In: in, Out: out, ErrOut: errOut, IsTerminal: isTerminal, StderrIsTerminal: stderrIsTerminal}
4143
}
4244

4345
// SystemIO creates an IOStreams wired to the process's standard file descriptors.

0 commit comments

Comments
 (0)