Files
2026-07-13 12:47:58 +08:00

227 lines
7.0 KiB
TypeScript

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<void>((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'))
})
})