From a6fb942fca7c1a40bd739e0f9805038da9f21c6d Mon Sep 17 00:00:00 2001 From: izzy Date: Tue, 18 Nov 2025 16:51:28 +0000 Subject: [PATCH] test: write tests for ProcessRepository#createSpawnDuplexStream --- .../repositories/process.repository.spec.ts | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 server/src/repositories/process.repository.spec.ts diff --git a/server/src/repositories/process.repository.spec.ts b/server/src/repositories/process.repository.spec.ts new file mode 100644 index 0000000000..9f63dc05b3 --- /dev/null +++ b/server/src/repositories/process.repository.spec.ts @@ -0,0 +1,85 @@ +import { ChildProcessWithoutNullStreams } from 'node:child_process'; +import { Readable, Writable } from 'node:stream'; +import { pipeline } from 'node:stream/promises'; +import { ProcessRepository } from 'src/repositories/process.repository'; + +describe(ProcessRepository.name, () => { + let sut: ProcessRepository; + let sink: Writable; + + beforeAll(() => { + sut = new ProcessRepository(); + }); + + beforeEach(() => { + sink = new Writable({ + write(_chunk, _encoding, callback) { + callback(); + }, + + final(callback) { + callback(); + }, + }); + }); + + describe('createSpawnDuplexStream', () => { + it('should work (drain to stdout)', async () => { + const process = sut.createSpawnDuplexStream('bash', ['-c', 'exit 0']); + await pipeline(process, sink); + }); + + it('should throw on non-zero exit code', async () => { + const process = sut.createSpawnDuplexStream('bash', ['-c', 'echo "error message" >&2; exit 1']); + await expect(pipeline(process, sink)).rejects.toThrowErrorMatchingInlineSnapshot(` + [Error: bash non-zero exit code (1) + error message + ] + `); + }); + + it('should accept stdin / output stdout', async () => { + async function* data() { + yield 'Hello, world!'; + } + + let output = ''; + const sink = new Writable({ + write(chunk, _encoding, callback) { + output += chunk; + callback(); + }, + + final(callback) { + callback(); + }, + }); + + const echoProcess = sut.createSpawnDuplexStream('cat'); + await pipeline(Readable.from(data()), echoProcess, sink); + expect(output).toBe('Hello, world!'); + }); + + it('should drain stdin on process exit', async () => { + let resolve1: () => void; + let resolve2: () => void; + const promise1 = new Promise((r) => (resolve1 = r)); + const promise2 = new Promise((r) => (resolve2 = r)); + + async function* data() { + yield 'Hello, world!'; + await promise1; + await promise2; + yield 'Write after stdin close / process exit!'; + } + + const process = sut.createSpawnDuplexStream('bash', ['-c', 'exit 0']); + + const realProcess = (process as never as { _process: ChildProcessWithoutNullStreams })._process; + realProcess.on('close', () => setImmediate(() => resolve1())); + realProcess.stdin.on('close', () => setImmediate(() => resolve2())); + + await pipeline(Readable.from(data()), process); + }); + }); +});