Files
coder/pty/ptytest/ptytest_windows.go
Danny Kopping 24b20df7d5 fix: use os.Pipe implementation for Windows CLI tests to reduce flakiness (#21874)
On Windows, `pty.New()` was creating a `ConPTY` (`PseudoConsole`) even
when no process would be attached. `ConPTY` requires a real process to
function correctly - without one, the pipe handles become invalid
intermittently, causing flaky test failures like `read |0: The handle is
invalid.`
This affected tests using the `ptytest.New()` + `Attach()` pattern for
in-process CLI testing.
The fix splits Windows PTY creation into two paths:
- `newPty()` now returns a simple pipe-based PTY for the `Attach()` use
case
- `newConPty()` creates a real `ConPTY`, called by `Start()` when a
process will be attached
AFAICT this will result in no change in behaviour outside of tests.

Fixes coder/internal#1277   

_Disclaimer: investigated and implemented by Claude Opus 4.5, reviewed
by me._

---------

Signed-off-by: Danny Kopping <danny@coder.com>
2026-02-03 11:50:28 +02:00

91 lines
1.8 KiB
Go

//go:build windows
package ptytest
import (
"os"
"sync"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/pty"
)
// testPTY is a pipe-based PTY implementation for in-process CLI testing on
// Windows. ConPTY requires an attached process to function correctly - without
// one, the pipe handles become invalid intermittently. This implementation
// avoids ConPTY entirely for the ptytest.New() + Attach() use case.
type testPTY struct {
inputReader *os.File
inputWriter *os.File
outputReader *os.File
outputWriter *os.File
closeMutex sync.Mutex
closed bool
}
func newTestPTY(_ ...pty.Option) (pty.PTY, error) {
p := &testPTY{}
var err error
p.inputReader, p.inputWriter, err = os.Pipe()
if err != nil {
return nil, xerrors.Errorf("create input pipe: %w", err)
}
p.outputReader, p.outputWriter, err = os.Pipe()
if err != nil {
_ = p.inputReader.Close()
_ = p.inputWriter.Close()
return nil, xerrors.Errorf("create output pipe: %w", err)
}
return p, nil
}
func (*testPTY) Name() string {
return ""
}
func (p *testPTY) Input() pty.ReadWriter {
return pty.ReadWriter{
Reader: p.inputReader,
Writer: p.inputWriter,
}
}
func (p *testPTY) Output() pty.ReadWriter {
return pty.ReadWriter{
Reader: p.outputReader,
Writer: p.outputWriter,
}
}
func (*testPTY) Resize(uint16, uint16) error {
return nil
}
func (p *testPTY) Close() error {
p.closeMutex.Lock()
defer p.closeMutex.Unlock()
if p.closed {
return nil
}
p.closed = true
var firstErr error
if err := p.outputWriter.Close(); err != nil && firstErr == nil {
firstErr = err
}
if err := p.outputReader.Close(); err != nil && firstErr == nil {
firstErr = err
}
if err := p.inputWriter.Close(); err != nil && firstErr == nil {
firstErr = err
}
if err := p.inputReader.Close(); err != nil && firstErr == nil {
firstErr = err
}
return firstErr
}