import { PassThrough, Readable } from 'node:stream' import { describe, expect, test } from 'vitest' import { buildCommand, chunkBytesBySize, isPipedStdin, readStdinIfPiped, readStdinFrom, streamStdinChunks, shellQuote, } from '../../../src/commands/sandbox/exec_helpers' describe('exec helpers', () => { test('shellQuote leaves safe args untouched', () => { expect(shellQuote('python3')).toBe('python3') expect(shellQuote('-c')).toBe('-c') expect(shellQuote('path/to/file.txt')).toBe('path/to/file.txt') }) test('shellQuote wraps special chars and spaces', () => { expect(shellQuote('print(input())')).toBe("'print(input())'") expect(shellQuote('hello world')).toBe("'hello world'") expect(shellQuote("it's ok")).toBe("'it'\"'\"'s ok'") expect(shellQuote('')).toBe("''") }) test('buildCommand returns a single command as-is', () => { const cmd = 'python3 -c "print(input())"' expect(buildCommand([cmd])).toBe(cmd) }) test('buildCommand quotes args that need shell escaping', () => { expect(buildCommand(['python3', '-c', 'print(input())'])).toBe( "python3 -c 'print(input())'" ) expect(buildCommand(['echo', 'hello world'])).toBe("echo 'hello world'") expect(buildCommand(['echo', "it's ok"])).toBe("echo 'it'\"'\"'s ok'") }) test('readStdinFrom reads full input and resolves on EOF', async () => { const stream = Readable.from(['foo', 'bar']) await expect(readStdinFrom(stream)).resolves.toEqual(Buffer.from('foobar')) }) test('readStdinFrom handles EOF without trailing newline', async () => { const stream = Readable.from(['no-newline']) await expect(readStdinFrom(stream)).resolves.toEqual( Buffer.from('no-newline') ) }) test('readStdinIfPiped returns undefined when stdin is not a pipe', async () => { const fsMock = { fstatSync: () => ({ isFIFO: () => false }), } const stream = new PassThrough() stream.end('data') await expect(readStdinIfPiped({ fsModule: fsMock, stream })).resolves.toBe( undefined ) }) test('readStdinIfPiped reads from provided stream when piped', async () => { const fsMock = { fstatSync: () => ({ isFIFO: () => true }), } const stream = new PassThrough() const promise = readStdinIfPiped({ fsModule: fsMock, stream }) stream.write(Buffer.from([0xe2, 0x98])) stream.write(Buffer.from([0x83])) stream.end(Buffer.from([0x21])) await expect(promise).resolves.toEqual(Buffer.from([0xe2, 0x98, 0x83, 0x21])) }) test('isPipedStdin returns true for FIFO', () => { const fsMock = { fstatSync: () => ({ isFIFO: () => true, isCharacterDevice: () => false }), } expect(isPipedStdin(0, fsMock)).toBe(true) }) test('isPipedStdin returns true for file redirection', () => { const fsMock = { fstatSync: () => ({ isFile: () => true, isCharacterDevice: () => false }), } expect(isPipedStdin(0, fsMock)).toBe(true) }) test('isPipedStdin returns false for interactive terminal', () => { const fsMock = { fstatSync: () => ({ isCharacterDevice: () => true, isFIFO: () => false, isFile: () => false, }), } expect(isPipedStdin(0, fsMock)).toBe(false) }) test('isPipedStdin returns false for non-FIFO or errors', () => { const fsMockFalse = { fstatSync: () => ({ isFIFO: () => false, isCharacterDevice: () => false }), } expect(isPipedStdin(0, fsMockFalse)).toBe(false) const fsMockThrow = { fstatSync: () => { throw new Error('fail') }, } expect(isPipedStdin(0, fsMockThrow)).toBe(false) }) test('chunkBytesBySize splits large input into byte-sized chunks', () => { const maxBytes = 64 * 1024 const data = Buffer.from('a'.repeat(maxBytes * 2 + 1)) const chunks = chunkBytesBySize(data, maxBytes) expect(chunks).toHaveLength(3) expect(chunks[0].byteLength).toBe(maxBytes) expect(chunks[1].byteLength).toBe(maxBytes) expect(chunks[2].byteLength).toBe(1) expect(Buffer.concat(chunks.map((c) => Buffer.from(c)))).toEqual(data) }) test('chunkBytesBySize keeps byte content intact', () => { const maxBytes = 64 * 1024 const data = Buffer.from('\u{1F600}'.repeat(20000)) // 😀 (4 bytes each) const chunks = chunkBytesBySize(data, maxBytes) for (const chunk of chunks) { expect(chunk.byteLength).toBeLessThanOrEqual(maxBytes) } expect(Buffer.concat(chunks.map((c) => Buffer.from(c)))).toEqual(data) }) test('chunkBytesBySize returns empty array for empty input', () => { const chunks = chunkBytesBySize(Buffer.alloc(0), 64 * 1024) expect(chunks).toHaveLength(0) }) test('chunkBytesBySize returns single chunk for small input', () => { const data = Buffer.from('hello') const chunks = chunkBytesBySize(data, 64 * 1024) expect(chunks).toHaveLength(1) expect(Buffer.from(chunks[0])).toEqual(data) }) test('chunkBytesBySize throws on invalid maxBytes', () => { expect(() => chunkBytesBySize(Buffer.from('data'), 0)).toThrow() expect(() => chunkBytesBySize(Buffer.from('data'), -1)).toThrow() }) test('readStdinFrom resolves with empty buffer on immediate EOF', async () => { const stream = Readable.from([]) await expect(readStdinFrom(stream)).resolves.toEqual(Buffer.alloc(0)) }) test('streamStdinChunks delivers chunks incrementally before EOF', async () => { const stream = new PassThrough() const seen: Buffer[] = [] let resolveFirstChunk: (() => void) | undefined const firstChunkSeen = new Promise((resolve) => { resolveFirstChunk = resolve }) const done = streamStdinChunks( stream, async (chunk) => { seen.push(Buffer.from(chunk)) if (resolveFirstChunk) { resolveFirstChunk() resolveFirstChunk = undefined } }, 64 * 1024 ) stream.write('first') await firstChunkSeen expect(seen).toEqual([Buffer.from('first')]) stream.end('second') await done expect(seen).toEqual([Buffer.from('first'), Buffer.from('second')]) }) test('streamStdinChunks splits oversized stream chunks by max bytes', async () => { const stream = Readable.from([Buffer.from('a'.repeat(64 * 1024 + 3))]) const chunks: Uint8Array[] = [] await streamStdinChunks( stream, async (chunk) => { chunks.push(chunk) }, 64 * 1024 ) expect(chunks).toHaveLength(2) expect(chunks[0].byteLength).toBe(64 * 1024) expect(chunks[1].byteLength).toBe(3) expect(Buffer.concat(chunks.map((c) => Buffer.from(c)))).toEqual( Buffer.from('a'.repeat(64 * 1024 + 3)) ) }) test('streamStdinChunks stops early when onChunk returns false', async () => { const stream = Readable.from([Buffer.from('first'), Buffer.from('second')]) const chunks: Uint8Array[] = [] await streamStdinChunks( stream, async (chunk) => { chunks.push(chunk) return false }, 64 * 1024 ) expect(chunks).toHaveLength(1) expect(Buffer.from(chunks[0])).toEqual(Buffer.from('first')) }) })