Skip to content

feat(agent): add SSH session test driver#26135

Draft
mafredri wants to merge 1 commit into
mainfrom
mf/agenttest-ssh-session
Draft

feat(agent): add SSH session test driver#26135
mafredri wants to merge 1 commit into
mainfrom
mf/agenttest-ssh-session

Conversation

@mafredri
Copy link
Copy Markdown
Member

@mafredri mafredri commented Jun 8, 2026

Summary

PR 3 of the SSH session test redesign (issue coder/internal#1560), following
#26102 (merged). Test-only; no
production change.

#1560 is the class of flaky SSH session tests that drive a session with a
bufio.Scanner read loop and blind Fprintf writes. When the remote process
dies, the write surfaces a bare io.EOF with no exit code or stderr, so the
failure is undiagnosable, and unbounded reads hang.

This adds a deterministic, composable replacement in agent/agenttest.

API

agenttest.WrapSession(t, *gossh.Session) returns a Session; chain .PTY()
then .Command(ctx, cmd) or .Shell(ctx).

  • Process implements io.ReadWriteCloser: Write feeds stdin (logged to
    t), Read consumes stdout (io.EOF on exit), Close closes stdin. Plus
    ReadUntil(ctx, token), Signal, Wait, Status, Stderr.
  • A stdin op against an exited process returns a *ProcessError carrying the
    exit code and captured stderr, never a bare EOF.
  • Shell adds prompt policy: Shell(ctx) returns after the first prompt;
    Run/ReadPrompt/Banner.
  • agenttest.ShellEnvInfo(t, ...) forces a deterministic shell (/bin/sh,
    cmd.exe) whose prompt is PromptMarker, so interactive tests stop
    depending on the host shell or hand-rolled prompt detection.

Reads use testutil.WaitBuffer for ctx-bounded matching; gossh does the
stream copying (session.Stdout/Stderr), so the driver carries no copy
goroutines of its own.

Testing

Verified on Linux and Windows (a live windows-2022-class host). Windows skips
are deliberate and documented: no reliable non-PTY line echo (findstr rejects
piped stdin), and the agent does not deliver SSH signals to Windows processes.
The PTY Shell path is verified against ConPTY/cmd.exe.

Design decisions
  • No usershelltest package. The test EnvInfoer lives in agenttest
    because all consumers (agent_test, agentssh_test, cli/ssh_test) are
    external _test packages, so importing agenttest causes no cycle. A
    separate package was unjustified structure.
  • Process is an io.ReadWriteCloser. Earlier drafts took ctx on
    Write/Close/Signal and hand-rolled stdin/stdout copy goroutines. This
    version conforms to the stdlib interfaces (composes with io.Copy,
    io.WriteString, io.ReadAll), logs writes, and keeps ctx only where it
    is honored (Wait, ReadUntil, Run).
  • Reads fail the test on timeout (with the captured output) rather than
    returning an error, matching the test-helper contract. *ProcessError is
    reserved for the stdin path, which is the actual chore: add container image to releases page #1560 scenario.
  • Default wiring deferred. Defaulting agenttest.New's EnvInfo (and the
    migration of existing tests) is left to PR 4 so it lands with the consumers
    it serves; PR 3 is opt-in via o.EnvInfo = agenttest.ShellEnvInfo(t).
  • Platform mechanics (validated, not assumed): cmd.exe under ConPTY
    takes CR (not LF) to submit a line; the marker is matched as a substring
    on a wide PTY with ANSI stripped from returned text; the Signal test uses
    exec sleep so the signal targets the long-running process rather than a
    resident shell with an orphaned child.

🤖 This PR was created with the help of Coder Agents, and will be reviewed by a human. 🏂🏻

Replace fragile bufio.Scanner and blind-write SSH session test patterns
with a deterministic, composable driver: agenttest.WrapSession yields a
Session that starts a Command or a login Shell over a PTY. Reads are
ctx-bounded via testutil.WaitBuffer, and a write to an exited process
returns a *ProcessError carrying the exit code and captured stderr, the
diagnosable form of the bare EOF in coder/internal#1560.

agenttest.ShellEnvInfo forces a deterministic shell (/bin/sh, cmd.exe)
whose prompt is a sentinel marker, so interactive-shell tests no longer
depend on the host shell or hand-rolled prompt detection.

Test-only; no production change. Verified on Linux and Windows.
@mafredri mafredri force-pushed the mf/agenttest-ssh-session branch from e65c1c7 to d44bd12 Compare June 8, 2026 13:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant