import { afterEach, describe, expect, it, vi } from 'vitest'; import { createShellExecutor } from '../src/executor.js'; afterEach(() => { vi.unstubAllEnvs(); }); /** * @param {import('../src/workflow.js').WorkflowStep[]} steps * @param {{timeoutMs?: number, env?: Record}} [options] */ async function run(steps, options = {}) { /** @type {string[]} */ const log = []; const executor = createShellExecutor( options.timeoutMs ? { timeoutMs: options.timeoutMs } : {}, ); const results = await executor({ dir: process.cwd(), steps, env: options.env ?? {}, onLog: (chunk) => log.push(chunk), }); return { results, log: log.join('') }; } describe('createShellExecutor', () => { it('runs every step and reports each exit code', async () => { const { results } = await run([ { name: 'first', run: 'true' }, { name: 'second', run: 'true' }, ]); expect(results.map((r) => [r.name, r.exitCode])).toEqual([ ['first', 0], ['second', 0], ]); expect(results.every((r) => r.durationMs >= 0)).toBe(true); }); it('stops at the first failing step', async () => { const { results } = await run([ { name: 'fails', run: 'exit 3' }, { name: 'never runs', run: 'true' }, ]); expect(results).toHaveLength(1); expect(results[0].exitCode).toBe(3); }); it('captures stdout and stderr', async () => { const { log } = await run([ { name: 'talk', run: 'echo out; echo err 1>&2' }, ]); expect(log).toContain('out'); expect(log).toContain('err'); expect(log).toContain('=== talk ==='); }); it('passes the run environment to the step', async () => { const { log } = await run([{ name: 'env', run: 'echo "$MARKER"' }], { env: { MARKER: 'from-workflow' }, }); expect(log).toContain('from-workflow'); }); it('withholds the runner credentials from the step', async () => { vi.stubEnv('PDSJS_CI_IDENTIFIER', 'runner.example.com'); vi.stubEnv('PDSJS_CI_PASSWORD', 'runner-app-password'); const { log } = await run([ { name: 'env', run: 'echo "id=${PDSJS_CI_IDENTIFIER:-absent} pw=${PDSJS_CI_PASSWORD:-absent}"', }, ]); expect(log).toContain('id=absent pw=absent'); }); it('reports a timed out step as a failure', async () => { const { results, log } = await run([{ name: 'hang', run: 'sleep 5' }], { timeoutMs: 100, }); expect(results[0].exitCode).not.toBe(0); expect(log).toContain('timed out'); }); });