Skip to content

Commit 0effbe2

Browse files
committed
test(session): model stderr PTY-folding faithfully in the shell fake
1 parent 4655bf7 commit 0effbe2

1 file changed

Lines changed: 14 additions & 12 deletions

File tree

tests/test-session-tools.js

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ class FakeShellStream extends EventEmitter {
7373
if (echo) this.emit('data', Buffer.from(echo));
7474
// Emit any scripted stdout.
7575
if (script.stdout) this.emit('data', Buffer.from(script.stdout));
76-
if (script.stderr) this.stderr.emit('data', Buffer.from(script.stderr));
76+
// PTY folds fd2 into fd1 -> scripted stderr arrives on `data`, not `.stderr`.
77+
if (script.stderr) this.emit('data', Buffer.from(script.stderr));
7778
// Emit the sentinel line last -- unless the script wants to misbehave.
7879
if (!script.skipMarker) {
7980
this.emit('data', Buffer.from(`${marker} ${script.exit ?? 0}\n`));
@@ -312,25 +313,26 @@ await test('runCommand: timeout cancels and rejects', async () => {
312313
await sess.close();
313314
});
314315

315-
await test('stream: stderr is folded into the pty stream, not decoded separately', async () => {
316-
// client.shell() merges stderr into `data`. The session must NOT wire a
317-
// separate stderr decoder -- a multibyte char straddling a stdout/stderr
318-
// boundary would otherwise splice across two decoders and corrupt.
316+
await test('stream: stderr folds into data; one decoder stitches split chars', async () => {
317+
// client.shell() gives a PTY -- fd2 multiplexes onto the same master as fd1,
318+
// so stderr arrives interleaved on `data`. SSHSessionV2 wires ONE decoder on
319+
// `data` and no separate `.stderr` listener; a multibyte char split across
320+
// chunks (stdout/stderr interleaved) must still decode intact, none lost.
319321
const stream = new FakeShellStream({ scriptFor: () => ({ stdout: '', exit: 0, skipMarker: true }) });
320322
const sess = new SSHSessionV2({ id: 'sess_dec', server: 's', shell: 'bash', stream });
323+
// fix removed the dead `.stderr` listener -> nothing wires it
324+
assert.strictEqual(stream.stderr.listenerCount('data'), 0, 'no separate stderr listener');
321325
const p = sess._waitForMarker({ timeoutMs: 1000 });
322326

323-
// A 3-byte UTF-8 char ('e' with acute, U+00E9 is 2 bytes; use U+20AC euro = 3 bytes).
324-
const euro = Buffer.from('€', 'utf8'); // [0xE2,0x82,0xAC]
325-
// Deliver the char split across two `data` events -- single decoder must
326-
// stitch it. Anything emitted on stream.stderr must be ignored entirely.
327-
stream.stderr.emit('data', Buffer.from('this stderr must be ignored'));
327+
const euro = Buffer.from('€', 'utf8'); // [0xE2,0x82,0xAC] -- 3 bytes
328+
// PTY interleaving: stderr text, then a stdout char split mid-byte across events.
329+
stream.emit('data', Buffer.from('ERR-folded '));
328330
stream.emit('data', euro.slice(0, 1));
329331
stream.emit('data', Buffer.concat([euro.slice(1), Buffer.from(`done\n${sess.marker} 0\n`)]));
330332

331333
const r = await p;
332-
assert(r.raw.includes('€'), 'split multibyte char decoded intact on the data stream');
333-
assert(!r.raw.includes('stderr must be ignored'), 'separate stderr stream is not buffered');
334+
assert(r.raw.includes('€'), 'split multibyte char decoded intact across data chunks');
335+
assert(r.raw.includes('ERR-folded'), 'stderr folded into data is captured, not lost');
334336
await sess.close();
335337
});
336338

0 commit comments

Comments
 (0)