Guard startup terminal probing on Windows when stderr is redirected#37823
Conversation
Co-authored-by: pelikhan <[email protected]>
Co-authored-by: pelikhan <[email protected]>
Co-authored-by: pelikhan <[email protected]>
There was a problem hiding this comment.
Pull request overview
This PR prevents gh aw from hanging on startup under certain Windows pty/pipe wrapper environments by conditionally skipping Lipgloss terminal capability probing when stderr is not a character device.
Changes:
- Added a Windows-only eligibility gate for running
configureLipglossCompat()during package init based onos.Stderr.Stat()mode. - Centralized the probe policy in
shouldConfigureLipglossCompat(goos, stderrMode, statErr). - Added table-driven unit tests covering Windows/non-Windows probe eligibility scenarios.
Show a summary per file
| File | Description |
|---|---|
| pkg/styles/theme.go | Gates Lipgloss compat initialization on Windows to avoid startup hangs when stderr is redirected/pipe-backed. |
| pkg/styles/theme_test.go | Adds targeted tests validating the new probe gating policy. |
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 2/2 changed files
- Comments generated: 0
|
🧪 Test Quality Sentinel completed test quality analysis. |
|
✅ Design Decision Gate 🏗️ completed the design decision gate check. No ADR enforcement needed: PR #37823 does not have the implementation label (has_implementation_label=false) and has 75 new lines in business logic directories, below the 100-line threshold (requires_adr_by_default_volume=false). |
|
✅ PR Code Quality Reviewer completed the code quality review. |
|
🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅ |
🧪 Test Quality Sentinel Report✅ Test Quality Score: 100/100 — Excellent
📊 Metrics & Test Classification (4 scenarios analyzed)
Test Classification Details
Language SupportTests analyzed:
Verdict
📖 Understanding Test ClassificationsDesign Tests (High Value) verify what the system does:
Implementation Tests (Low Value) verify how the system does it:
Goal: Shift toward tests that describe the system's behavioral contract — the promises it makes to its users and collaborators.
|
There was a problem hiding this comment.
✅ Test Quality Sentinel: 100/100. Test quality is excellent — 0% of new tests are implementation tests (threshold: 30%). The new table-driven TestShouldConfigureLipglossCompat covers all four branches of the Windows stderr-probe guard with clear behavioral assertions.
There was a problem hiding this comment.
Skills-Based Review 🧠
Applied /diagnose and /tdd — approving with minor test-coverage suggestions.
📋 Key Themes & Highlights
Key Themes
- Test table completeness: three edge cases are worth adding — combined
ModeCharDevice | ModeDeviceflags, a zero-mode stat-success case, and an explicitos.ErrInvalidentry to trace theinit()nil-nil fallback - Untestable
init()branch: the nil →ErrInvalidconversion is logically covered by the existingos.ErrPermissiontest, but the traceability is invisible — easy to close with one extra table row or a structural refactor
Positive Highlights
- ✅ Root cause properly targeted: gating the probe at the source rather than papering over symptoms
- ✅ Pure function extraction (
shouldConfigureLipglossCompat) is exactly the right testability pattern for aninit()path - ✅ Table-driven tests cover the key decision boundaries (char device / named pipe / stat error / non-Windows)
- ✅ Non-Windows behaviour deliberately unchanged — minimal blast radius
- ✅ Defensive nil-nil guard with clear inline comments
🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 165.8 AIC · ⌖ 13.8 AIC
| { | ||
| name: "windows character device", | ||
| goos: "windows", | ||
| stderrMode: os.ModeCharDevice, |
There was a problem hiding this comment.
[/tdd] The "windows character device" case tests os.ModeCharDevice in isolation. Real Windows console handles frequently report combined modes like os.ModeCharDevice | os.ModeDevice. Adding a combined-flags case would anchor the bitmask semantics for realistic inputs.
💡 Suggested additional test case
{
name: "windows character device with device flag",
goos: "windows",
stderrMode: os.ModeCharDevice | os.ModeDevice,
want: true,
},The implementation uses stderrMode&os.ModeCharDevice != 0, which handles combined flags correctly — this test would make that intent part of the explicit spec.
| stderrMode: os.ModeNamedPipe, | ||
| statErr: os.ErrPermission, | ||
| want: true, | ||
| }, |
There was a problem hiding this comment.
[/tdd] Consider adding a "windows stat succeeds, zero mode" case: goos: "windows", stderrMode: os.FileMode(0), statErr: nil, want: false. This pins the behaviour when stat returns a FileMode with no device flags set — a plausible handle state (e.g. a virtual file handle that stat-succeeds but reports nothing) that is currently untested.
💡 Suggested test case
{
name: "windows zero mode stat success",
goos: "windows",
stderrMode: os.FileMode(0),
want: false,
},Since init() initialises stderrMode to os.FileMode(0) before reading stderrInfo.Mode(), this also documents the fallback default intentionally.
| // impossible state as an invalid stderr handle so Windows startup probing is | ||
| // safely skipped. | ||
| if statErr == nil && stderrInfo == nil { | ||
| statErr = os.ErrInvalid |
There was a problem hiding this comment.
[/diagnose] The nil-nil → os.ErrInvalid conversion here cannot be exercised from the shouldConfigureLipglossCompat test table — the pure function always receives already-converted values. The existing "windows stat error" case uses os.ErrPermission, which is equivalent at runtime, but the traceability from this init() fallback to its test coverage is invisible.
💡 Options to close the gap
Option A — add an os.ErrInvalid case to the test table with a comment linking it to this path:
{
name: "windows ErrInvalid (init nil-nil fallback)",
goos: "windows",
statErr: os.ErrInvalid,
want: false,
},Option B — hoist the nil-nil guard into shouldConfigureLipglossCompat by passing stderrInfo directly (or a (mode, modeValid bool) pair), making the entire decision testable through the pure function.
Option A is a lower-effort win; Option B is structurally cleaner.
|
@copilot apply copilot-review skill |
There was a problem hiding this comment.
The fix is logically sound and addresses the stated Windows hang scenario cleanly. Three non-blocking observations on test fidelity and check precision.
### Summary of findings
Test uses os.ModeCharDevice alone, not the real production mode (pkg/styles/theme_test.go:294): Windows FILE_TYPE_CHAR (console) handles cause Go to set os.ModeDevice | os.ModeCharDevice together. The test case uses only os.ModeCharDevice, which works today but will silently become a false negative if the check is ever hardened to require both bits.
Zero-mode case (stderrMode=0, statErr=nil) is not tested (pkg/styles/theme_test.go:315): The init() comment explicitly documents this as the "unknown/unset" conservative default on Windows, but this input is absent from the table. A future change that treats zero-mode as "assume TTY" would not be caught.
os.ModeCharDevice check is semantically imprecise (pkg/styles/theme.go:71): Go docs define ModeCharDevice as meaningful "when ModeDevice is set". The two-bit equality check stderrMode&(os.ModeDevice|os.ModeCharDevice) == (os.ModeDevice|os.ModeCharDevice) is both more precise and consistent with the suggested test fix above.
Warning
Firewall blocked 2 domains
The following domains were blocked by the firewall during workflow execution:
patchdiff.githubusercontent.compkg.go.dev
To allow these domains, add them to the
network.allowedlist in your workflow frontmatter:
network:
allowed:
- defaults
- "patchdiff.githubusercontent.com"
- "pkg.go.dev"See Network Configuration for more information.
🔎 Code quality review by PR Code Quality Reviewer · 10.1 AIC · ⌖ 13.3 AIC
| { | ||
| name: "windows character device", | ||
| goos: "windows", | ||
| stderrMode: os.ModeCharDevice, |
There was a problem hiding this comment.
Test uses single-bit os.ModeCharDevice, not the actual production value: on Windows, FILE_TYPE_CHAR (console) handles cause Go to set os.ModeDevice | os.ModeCharDevice together — never ModeCharDevice alone.
💡 Why this matters
The current check is stderrMode&os.ModeCharDevice != 0, so both values satisfy it today. But the test case supplies a value that cannot arise in practice and creates a latent trap: if the check is ever tightened to the semantically precise form:
return stderrMode&(os.ModeDevice|os.ModeCharDevice) == (os.ModeDevice|os.ModeCharDevice)the existing os.ModeCharDevice-only case would become a false negative — the test would pass a value the stricter check rejects, masking the regression.
Suggested fix: use the real Windows console mode shape:
{
name: "windows character device",
goos: "windows",
stderrMode: os.ModeDevice | os.ModeCharDevice, // FILE_TYPE_CHAR on Windows
want: true,
},| stderrMode: os.ModeNamedPipe, | ||
| statErr: os.ErrPermission, | ||
| want: true, | ||
| }, |
There was a problem hiding this comment.
Missing test case for stderrMode=0, statErr=nil on Windows: the init() comment explicitly calls this the "unknown/unset" conservative default, but shouldConfigureLipglossCompat("windows", 0, nil) is never in the table.
💡 Suggested addition
The stderrMode := os.FileMode(0) fallback in init() is justified by: "Zero mode means 'unknown/unset'; on Windows this keeps the startup probe disabled unless stderr is explicitly confirmed as a character device." A test should pin this:
{
name: "windows unknown mode (zero) no error",
goos: "windows",
stderrMode: os.FileMode(0),
statErr: nil,
want: false,
},Without this, a refactor that treats zero-mode as "assume TTY" would go undetected.
| if statErr != nil { | ||
| return false | ||
| } | ||
| return stderrMode&os.ModeCharDevice != 0 |
There was a problem hiding this comment.
os.ModeCharDevice alone is semantically imprecise: Go's fs package documents ModeCharDevice as "Unix character device, when ModeDevice is set." The check should require both bits:
💡 Suggested fix and rationale
// Before
return stderrMode&os.ModeCharDevice != 0
// After
return stderrMode&(os.ModeDevice|os.ModeCharDevice) == (os.ModeDevice|os.ModeCharDevice)On Windows, Go sets ModeDevice | ModeCharDevice together for FILE_TYPE_CHAR handles, so the current single-bit check happens to work. But accepting ModeCharDevice without ModeDevice is technically a false-positive admission of an invalid mode shape.
This also brings the check into alignment with the test fix suggested on the stderrMode: os.ModeCharDevice case — if the test is updated to use os.ModeDevice | os.ModeCharDevice (the real production value), the stricter check here would correctly accept it, while the current looser check would accept anything with just ModeCharDevice set.
Co-authored-by: pelikhan <[email protected]>
Applied. Fixed the in-scope review feedback in |
gh awcould hang before any output when launched under Windows pty/pipe wrappers, including on non-interactive commands likeversion. The hang appears on startup-path terminal capability probing rather than command-specific execution.What changed: startup probe gating (Windows-only)
pkg/styles/theme.go.configureLipglossCompat()now runs only ifstderris confirmed as a console-style character device.stderris redirected, pipe-backed, unknown/unset, orStatfails, startup probing is skipped.What changed: explicit probe policy
shouldConfigureLipglossCompat(goos, stderrMode, statErr)to centralize probe eligibility.os.ModeDevice | os.ModeCharDevice.What changed: focused coverage
pkg/styles/theme_test.gofor:ModeDevice | ModeCharDevice) → probe enabledos.ErrInvalidfallback → probe disabled