# tape-six-proc

> A helper for [tape-six](https://github.com/uhop/tape-six) that runs test files in separate processes (subprocesses) instead of worker threads. Works with Node, Deno, and Bun. Supports TypeScript natively without transpilation. The npm package name is `tape-six-proc` and the CLI command is `tape6-proc`.

- Process isolation: each test file runs in its own subprocess
- Cross-runtime: Node, Deno, and Bun with the same test files
- TypeScript without transpilation: runs `.ts` test files natively on modern Node, Deno, and Bun
- Drop-in replacement for `tape6` (worker-thread runner)
- Same configuration format as `tape-six`
- Parallel execution with configurable concurrency
- TAP, TTY (colored), JSONL, and minimal output formats
- Worker control channel: `failOnce` stops in-flight workers, plus an optional per-worker time limit

## Install

```bash
npm i -D tape-six-proc
```

## Quick start

Write a test (`tests/test-example.js`):

```js
import test from 'tape-six';

test('example', t => {
  t.ok(true, 'truthy');
  t.equal(1 + 1, 2, 'math works');
  t.deepEqual([1, 2], [1, 2], 'arrays match');
});
```

Run it directly:

```bash
node tests/test-example.js
```

Or run all configured tests via processes:

```bash
tape6-proc --flags FO
```

## CLI: tape6-proc

Runs test files in parallel, each in its own subprocess.

```bash
tape6-proc [--flags FLAGS] [--par N] [--runFileArgs ARGS] [tests...]
```

### Options

- `--flags FLAGS` (`-f`) — output control flags (see Supported flags below).
- `--par N` (`-p`) — number of parallel processes. Default: all CPU cores (via `os.availableParallelism()` or `navigator.hardwareConcurrency`).
- `--runFileArgs ARGS` (`-r`) — extra arguments passed to the spawned interpreter. Can be specified multiple times. Mainly used for Deno permissions.
- `--info` — prints runtime, reporter, parallelism, flags, and test files, then exits.
- `--self` — prints the absolute path to `tape6-proc.js` and exits. Used in cross-runtime scripts.
- `--help` (`-h`) — show help message and exit.
- `--version` (`-v`) — show version and exit.
- Positional arguments: test file glob patterns. If none given, reads from configuration.
- Options accept `--flags FO` or `--flags=FO`. The `=` form does not support quoting.

### Examples

```bash
# Run all configured tests
tape6-proc --flags FO

# Run specific test files
tape6-proc tests/test-foo.js tests/test-bar.js

# Limit parallelism
tape6-proc --par 4 --flags FO

# Pass Deno permissions to spawned processes
tape6-proc -r --allow-read -r --allow-env --flags FO
```

## Cross-runtime usage

`tape6-proc` is a Node CLI by default. For Bun and Deno, use the `--self` flag to get the script path:

```json
{
  "scripts": {
    "test": "tape6-proc --flags FO",
    "test:node": "tape6-proc --flags FO",
    "test:bun": "bun run `tape6-proc --self` --flags FO",
    "test:deno": "deno run -A `tape6-proc --self` --flags FO -r -A"
  }
}
```

### Deno permissions

Deno requires explicit permissions. Use `-r` (alias for `--runFileArgs`) to pass them to each spawned test process:

```bash
# All permissions
deno run -A `tape6-proc --self` -r -A

# Fine-grained permissions
deno run -A `tape6-proc --self` -r --allow-read -r --allow-env
```

Note: the first `-A` is for the `tape6-proc` process itself; the `-r` flags are forwarded to spawned test processes.

## Supported flags

Flags are a string of characters. Uppercase = enabled, lowercase = disabled.

- `F` — Failures only: show only failed tests.
- `T` — show Time for each test.
- `B` — show Banner with summary.
- `D` — show Data of failed tests.
- `O` — fail Once: stop at first failure. Via the control channel this also drains the workers still in flight (cleanup hooks run), not just the file queue.
- `N` — show assert Number.
- `M` — Monochrome: no colors.
- `C` — don't Capture console output.
- `H` — Hide streams and console output.

Usage:

```bash
tape6-proc --flags FO
TAPE6_FLAGS=FO node tests/test-example.js
```

## Configuration

Configuration is read from `tape6.json` or the `"tape6"` section of `package.json` (same format as `tape-six`):

```json
{
  "tape6": {
    "tests": ["/tests/test-*.*js"],
    "cli": ["/tests/test-*.cjs"]
  }
}
```

Environment-specific subsections (`node`, `deno`, `bun`) are supported. The runner auto-detects the current runtime and uses the appropriate subsection.

## Environment variables

- `TAPE6_FLAGS` — flags string (combined with `--flags` CLI argument).
- `TAPE6_PAR` — number of parallel processes (overridden by `--par`).
- `TAPE6_TAP` — force TAP reporter (any non-empty value).
- `TAPE6_JSONL` — force JSONL reporter (any non-empty value).
- `TAPE6_MIN` — force minimal reporter (any non-empty value).
- `TAPE6_GRACE_TIMEOUT` — milliseconds a worker is given to drain (run cleanup hooks, flush) after a `terminate` before it is force-killed. Default 5000. Shared with `tape-six`.
- `TAPE6_WORKER_TIMEOUT` — per-worker wall-clock deadline in milliseconds; on expiry the worker is terminated (drain → kill). Default 0 (disabled). Shared with `tape-six`.
- `TAPE6_TEST` — set by the runner: unique ID for each spawned test process.
- `TAPE6_TEST_FILE_NAME` — set by the runner: file name of the current test.
- `TAPE6_JSONL_PREFIX` — set by the runner: UUID prefix for JSONL lines in stdout.
- `TAPE6_CONTROL` — set by the runner: marks a spawned child as controlled, so the tape-six runtime opens the stdin control channel and stays alive after `end` until told to exit.

## Architecture

### Entry point

`bin/tape6-proc.js` is the CLI entry point:

- With `--self`: prints its own absolute path and exits.
- Otherwise: delegates to `bin/tape6-proc-node.js`.

### Main CLI (`bin/tape6-proc-node.js`)

Delegates argument parsing, reporter setup, and file resolution to `tape-six/utils/config.js`:

1. Calls `getOptions()` to parse CLI arguments (`--flags`, `--par`, `--runFileArgs`, `--info`, `--help`, `--version`, positional test patterns).
2. Calls `initReporter()` to select the reporter: TTY (default for terminals), TAP, JSONL, or Min (based on environment variables).
3. Calls `initFiles()` to resolve test files from configuration or CLI patterns.
4. Creates a `TestWorker` instance and executes all test files.
5. Reports final results and exits with code 0 (success) or 1 (failures).

### TestWorker (`src/TestWorker.js`)

Extends `EventServer` from `tape-six`. Manages parallel subprocess execution:

- **`constructor(reporter, numberOfTasks, options)`** — initializes with a reporter, concurrency limit, and options. Generates a UUID prefix for JSONL parsing.
- **`makeTask(fileName)`** — spawns a child process for a test file using `dollar-shell` (`stdin: 'pipe'` so it can be controlled). Sets up stream pipelines for stdout and stderr, marks completion when the child's top-level `end` is read, and reports premature exits. Returns a task ID.
- **`destroyTask(id, reason)`** — the control-plane terminate. Writes a line-delimited `terminate` command to the child's stdin and EOFs it, then arms a `TAPE6_GRACE_TIMEOUT` deadline after which the child is force-killed (`worker.kill()`, SIGTERM). The base `EventServer` calls it with `reason` `'done'` (the worker finished — read its `end`), `'failOnce'` (a sibling tripped failOnce / bail), or `'timeout'` (the per-worker deadline). Idempotent per task.

### Stream pipeline

Each spawned process has two data-plane stream pipelines (worker → reporter) plus a stdin control plane (reporter → worker, carrying `terminate`):

**stdout pipeline:**
```
process.stdout → TextDecoderStream → lines → parse-prefixed-jsonl → report
```

- `lines` (`src/streams/lines.js`): TransformStream that splits text into individual lines.
- `parse-prefixed-jsonl` (`src/streams/parse-prefixed-jsonl.js`): TransformStream that recognizes lines starting with the UUID prefix, parses them as JSON, and passes through non-prefixed lines as `{type: 'stdout', name: line}` objects.

**stderr pipeline:**
```
process.stderr → TextDecoderStream → lines → wrap-lines → report
```

- `wrap-lines` (`src/streams/wrap-lines.js`): TransformStream that wraps each line as `{type: 'stderr', name: line}`.

### Process lifecycle

1. Process is spawned with environment variables: `TAPE6_FLAGS`, `TAPE6_TEST`, `TAPE6_TEST_FILE_NAME`, `TAPE6_JSONL=Y`, `TAPE6_JSONL_PREFIX`, `TAPE6_CONTROL=Y`, `TAPE6_GRACE_TIMEOUT`.
2. The test file runs and outputs JSONL-prefixed lines to stdout. Marked by `TAPE6_CONTROL`, the tape-six runtime opens a stdin control channel and stays alive after emitting its top-level `end` — it no longer self-exits.
3. **Normal completion** is keyed off the parent *reading* that top-level `end` (not racing the child's exit). The parent then issues `terminate` (drain + EOF stdin); the child exits and the next queued file starts. Because the child exits only after `end` has been consumed, the Bun stdout-flush bug (a dropped tail on self-exit) cannot occur.
4. **Abort** (`failOnce` / bail, or a `TAPE6_WORKER_TIMEOUT` deadline) issues `terminate` to every in-flight worker. Each drains its running test through `reporter.terminate()` — `t.signal` fires, `finally` / `afterEach` / `afterAll` run — then exits, emitting its `end` as usual.
5. **Force-kill backstop.** A worker that does not exit within `TAPE6_GRACE_TIMEOUT` of a `terminate` (a test hung in a non-signal-aware `await`) is killed with `worker.kill()` (SIGTERM).
6. A premature exit — the child closes stdout without a top-level `end` (crash, bad import, force-kill) — is reported as an error with a `{type: 'terminated'}` event, including the exit code / signal, unless a failure was already reported.

## Dependencies

- **`tape-six`** — the core test library. Imports: `utils/config.js` (`getOptions`, `initFiles`, `initReporter`, `showInfo`, `printFlagOptions`), `test.js` (`getReporter`, `setReporter`), `utils/timer.js`, `State.js`, `utils/EventServer.js`, `utils/makeDeferred.js`. The worker control channel additionally depends on tape-six's child-side listener (`src/utils/control-channel.js`, opened via `TAPE6_CONTROL`) and the `getGraceTimeout` / `getWorkerTimeout` config it injects through `getOptions`, so it requires a tape-six version that ships them.
- **`dollar-shell`** — cross-runtime process spawning. Imports: `spawn`, `currentExecPath`, `runFileArgs`.

## Writing tests

Tests are standard `tape-six` tests. See the [tape-six documentation](https://github.com/uhop/tape-six/wiki) for the full API.

```js
import test from 'tape-six';

test('arithmetic', t => {
  t.equal(2 + 2, 4, 'addition');
  t.ok(10 > 5, 'comparison');
});

test('async operation', async t => {
  const result = await fetchData();
  t.ok(result, 'got result');
  t.equal(result.status, 200, 'status is 200');
});
```

Test files should be directly executable: `node tests/test-foo.js` or `node tests/test-foo.ts`

## Links

- Docs: https://github.com/uhop/tape-six-proc/wiki
- npm: https://www.npmjs.com/package/tape-six-proc
- tape-six: https://github.com/uhop/tape-six
- tape-six LLM reference: https://github.com/uhop/tape-six/blob/master/llms.txt
