tape-six 1.13.0 → 1.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -117,7 +117,7 @@ tape-six/
117
117
 
118
118
  ## Docs
119
119
 
120
- The documentation can be found in the [wiki](https://github.com/uhop/tape-six/wiki).
120
+ Full documentation is in the **[wiki](https://github.com/uhop/tape-six/wiki)** — browse the [index](https://github.com/uhop/tape-six/wiki/Home), or [search it](https://uhop.github.io/wiki-search/app/?wiki=uhop/tape-six) by name.
121
121
  See how it can be used in [tests/](https://github.com/uhop/tape-six/tree/master/tests).
122
122
 
123
123
  For AI assistants: see [llms.txt](https://github.com/uhop/tape-six/blob/master/llms.txt) and [llms-full.txt](https://github.com/uhop/tape-six/blob/master/llms-full.txt) for LLM-optimized documentation.
@@ -427,6 +427,8 @@ Test output can be controlled by flags. See [Supported flags](https://github.com
427
427
 
428
428
  The most recent releases:
429
429
 
430
+ - 1.14.1 _Bug fixes. Updated dependencies._
431
+ - 1.14.0 _Internal housekeeping. Removed the Deno 2.9.0 workaround shim._
430
432
  - 1.13.0 _TypeScript declarations for the `EventServer`._
431
433
  - 1.12.0 _`tape6-server` is now pluggable, embeddable server for hermetic integration tests, runtime plugin registration, and an opt-in HTTP/2 mode._
432
434
  - 1.11.0 _Invariant host hooks for the new zero-dependency `tape-six-invariant` companion package. Workaround for a Deno 2.9.0 `node_modules/.bin` symlink regression._
@@ -453,4 +455,4 @@ The most recent releases:
453
455
  - 1.5.1 _Better support for stopping parallel tests, better support for "failed to load" errors._
454
456
  - 1.5.0 _Internal refactoring (moved state to reporters), added type identification of values in the DOM and TTY reporters, multiple minor fixes._
455
457
 
456
- For more info consult full [release notes](https://github.com/uhop/tape-six/wiki/Release-notes).
458
+ The full release notes are in the wiki: [Release notes](https://github.com/uhop/tape-six/wiki/Release-notes).
package/bin/tape6-deno.js CHANGED
@@ -1,9 +1,106 @@
1
1
  #!/usr/bin/env -S deno run --allow-all --ext=js
2
2
 
3
- // Deno 2.9.0 doesn't realpath a symlinked entry (denoland/deno#35551), so the
4
- // `.bin/tape6-deno` symlink would resolve the runner's ../src imports wrong.
5
- // Realpath this entry, then load the runner (static imports intact) beside it.
6
- import {fileURLToPath, pathToFileURL} from 'node:url';
3
+ import process from 'node:process';
4
+ import {fileURLToPath} from 'node:url';
7
5
 
8
- const here = pathToFileURL(Deno.realPathSync(fileURLToPath(import.meta.url)));
9
- await import(new URL('tape6-deno-main.js', here).href);
6
+ import {
7
+ getOptions,
8
+ initFiles,
9
+ initReporter,
10
+ showInfo,
11
+ printVersion,
12
+ printHelp,
13
+ printFlagOptions
14
+ } from '../src/utils/config.js';
15
+
16
+ import {getReporter, setReporter} from '../src/test.js';
17
+ import {selectTimer} from '../src/utils/timer.js';
18
+
19
+ import TestWorker from '../src/runners/deno/TestWorker.js';
20
+
21
+ const rootFolder = Deno.cwd();
22
+
23
+ const showSelf = () => {
24
+ const self = new URL(import.meta.url);
25
+ if (self.protocol === 'file:') {
26
+ console.log(fileURLToPath(self));
27
+ } else {
28
+ console.log(self);
29
+ }
30
+ Deno.exit(0);
31
+ };
32
+
33
+ const showVersion = () => {
34
+ printVersion('tape6-deno');
35
+ Deno.exit(0);
36
+ };
37
+
38
+ const showHelp = () => {
39
+ printHelp('tape6-deno', 'Tape6 test runner for Deno', 'tape6-deno [options] [files...]', [
40
+ ['--flags, -f <flags>', 'Set reporter flags (env: TAPE6_FLAGS)'],
41
+ ['--par, -p <n>', 'Set parallelism level (env: TAPE6_PAR)'],
42
+ ['--info', 'Show configuration info and exit'],
43
+ ['--self', 'Print the path to this script and exit'],
44
+ ['--help, -h', 'Show this help message and exit'],
45
+ ['--version, -v', 'Show version and exit']
46
+ ]);
47
+ printFlagOptions();
48
+ Deno.exit(0);
49
+ };
50
+
51
+ const main = async () => {
52
+ const currentOptions = getOptions({
53
+ '--self': {fn: showSelf, isValueRequired: false},
54
+ '--info': {isValueRequired: false},
55
+ '--help': {aliases: ['-h'], fn: showHelp, isValueRequired: false},
56
+ '--version': {aliases: ['-v'], fn: showVersion, isValueRequired: false}
57
+ });
58
+
59
+ const [files] = await Promise.all([
60
+ initFiles(currentOptions.files, rootFolder),
61
+ initReporter(getReporter, setReporter, currentOptions.flags),
62
+ selectTimer()
63
+ ]);
64
+
65
+ addEventListener('error', event => {
66
+ console.log('UNHANDLED ERROR:', event.message);
67
+ event.preventDefault();
68
+ });
69
+
70
+ if (currentOptions.optionFlags['--info'] === '') {
71
+ showInfo(currentOptions, files);
72
+ await new Promise(r => process.stdout.write('', r));
73
+ process.exitCode = 0;
74
+ return;
75
+ }
76
+
77
+ if (!files.length) {
78
+ console.log('No files found.');
79
+ await new Promise(r => process.stdout.write('', r));
80
+ process.exitCode = 1;
81
+ return;
82
+ }
83
+
84
+ const reporter = getReporter(),
85
+ worker = new TestWorker(reporter, currentOptions.parallel, currentOptions.flags);
86
+
87
+ reporter.report({type: 'test', test: 0});
88
+
89
+ await new Promise(resolve => {
90
+ worker.done = () => resolve();
91
+ worker.execute(files);
92
+ });
93
+
94
+ const hasFailed = reporter.state && reporter.state.failed > 0;
95
+
96
+ reporter.report({
97
+ type: 'end',
98
+ test: 0,
99
+ fail: hasFailed
100
+ });
101
+
102
+ await new Promise(r => process.stdout.write('', r));
103
+ process.exitCode = hasFailed ? 1 : 0;
104
+ };
105
+
106
+ main().catch(error => console.error('ERROR:', error));
package/index.js CHANGED
@@ -225,60 +225,67 @@ const getTestFileName = ({isBrowser, isBun, isDeno, isNode}) => {
225
225
  let settings = null;
226
226
 
227
227
  const testRunner = async () => {
228
- if (!settings) {
229
- await selectTimer();
230
- settings = await init();
231
- }
228
+ // ref'd keep-alive — a bare run holds no other ref'd handle, so a test awaiting
229
+ // only an unref'd timer (AbortSignal.timeout on Node/Bun) would exit 0 mid-suite
230
+ const keepAlive = setInterval(() => {}, 2 ** 31 - 1);
231
+ try {
232
+ if (!settings) {
233
+ await selectTimer();
234
+ settings = await init();
235
+ }
232
236
 
233
- const {isBrowser, isCli} = settings,
234
- reporter = getReporter(),
235
- testFileName = getTestFileName(settings);
237
+ const {isBrowser, isCli} = settings,
238
+ reporter = getReporter(),
239
+ testFileName = getTestFileName(settings);
236
240
 
237
- reporter.report({
238
- type: 'test',
239
- test: 0,
240
- name: testFileName ? 'FILE: /' + testFileName : ''
241
- });
241
+ reporter.report({
242
+ type: 'test',
243
+ test: 0,
244
+ name: testFileName ? 'FILE: /' + testFileName : ''
245
+ });
242
246
 
243
- reporter.state.beforeAll = getBeforeAll();
244
- clearBeforeAll();
245
- reporter.state.afterAll = getAfterAll();
246
- clearAfterAll();
247
- reporter.state.beforeEach = getBeforeEach();
248
- clearBeforeEach();
249
- reporter.state.afterEach = getAfterEach();
250
- clearAfterEach();
251
- reporter.state.isBeforeAllUsed = false;
247
+ reporter.state.beforeAll = getBeforeAll();
248
+ clearBeforeAll();
249
+ reporter.state.afterAll = getAfterAll();
250
+ clearAfterAll();
251
+ reporter.state.beforeEach = getBeforeEach();
252
+ clearBeforeEach();
253
+ reporter.state.afterEach = getAfterEach();
254
+ clearAfterEach();
255
+ reporter.state.isBeforeAllUsed = false;
252
256
 
253
- const currentState = reporter.state;
257
+ const currentState = reporter.state;
254
258
 
255
- for (;;) {
256
- const tests = getTests();
257
- if (!tests.length) break;
258
- clearTests();
259
- const canContinue = await runTests(tests);
260
- if (!canContinue) break;
261
- await new Promise(resolve => defer(resolve));
262
- }
259
+ for (;;) {
260
+ const tests = getTests();
261
+ if (!tests.length) break;
262
+ clearTests();
263
+ const canContinue = await runTests(tests);
264
+ if (!canContinue) break;
265
+ await new Promise(resolve => defer(resolve));
266
+ }
263
267
 
264
- await currentState?.runAfterAll();
268
+ await currentState?.runAfterAll();
265
269
 
266
- const runHasFailed = reporter.state && reporter.state.failed > 0;
270
+ const runHasFailed = reporter.state && reporter.state.failed > 0;
267
271
 
268
- reporter.report({
269
- type: 'end',
270
- test: 0,
271
- name: testFileName ? 'FILE: /' + testFileName : '',
272
- fail: runHasFailed
273
- });
272
+ reporter.report({
273
+ type: 'end',
274
+ test: 0,
275
+ name: testFileName ? 'FILE: /' + testFileName : '',
276
+ fail: runHasFailed
277
+ });
274
278
 
275
- if (!getConfiguredFlag() && runHasFailed) {
276
- if (isCli) {
277
- process.exitCode = 1;
279
+ if (!getConfiguredFlag() && runHasFailed) {
280
+ if (isCli) {
281
+ process.exitCode = 1;
282
+ }
278
283
  }
279
- }
280
- if (isBrowser && typeof __tape6_reportResults == 'function') {
281
- __tape6_reportResults(runHasFailed ? 'failure' : 'success');
284
+ if (isBrowser && typeof __tape6_reportResults == 'function') {
285
+ __tape6_reportResults(runHasFailed ? 'failure' : 'success');
286
+ }
287
+ } finally {
288
+ clearInterval(keepAlive);
282
289
  }
283
290
  };
284
291
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tape-six",
3
- "version": "1.13.0",
3
+ "version": "1.14.1",
4
4
  "description": "TAP-inspired unit test library for Node, Deno, Bun, and browsers. ES modules, TypeScript, zero dependencies.",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -68,8 +68,6 @@
68
68
  "url": "https://github.com/uhop/tape-six/issues"
69
69
  },
70
70
  "homepage": "https://github.com/uhop/tape-six#readme",
71
- "llms": "https://raw.githubusercontent.com/uhop/tape-six/master/llms.txt",
72
- "llmsFull": "https://raw.githubusercontent.com/uhop/tape-six/master/llms-full.txt",
73
71
  "files": [
74
72
  "index.*",
75
73
  "bin",
@@ -103,8 +101,8 @@
103
101
  }
104
102
  },
105
103
  "devDependencies": {
106
- "@types/node": "^26.1.0",
107
- "prettier": "^3.9.4",
108
- "typescript": "^6.0.3"
104
+ "@types/node": "^26.1.1",
105
+ "prettier": "^3.9.5",
106
+ "typescript": "^7.0.2"
109
107
  }
110
108
  }
package/src/State.d.ts ADDED
@@ -0,0 +1,94 @@
1
+ import type {Timer} from './utils/timer.js';
2
+
3
+ /** Marker property stamped on tape-six-owned errors and serialized specials. */
4
+ export const signature: string;
5
+
6
+ /** Thrown to unwind a test when `failOnce` or a bail-out fires. Detect with `isStopTest`. */
7
+ export class StopTest extends Error {
8
+ constructor(...args: ConstructorParameters<typeof Error>);
9
+ }
10
+
11
+ /**
12
+ * `true` for `StopTest` instances, including cross-realm copies revived from
13
+ * serialized events (matched structurally via the `signature` marker).
14
+ */
15
+ export const isStopTest: (error: unknown) => boolean;
16
+
17
+ /** Structural check for `node:assert`-style `AssertionError` objects (any realm). */
18
+ export const isAssertionError: (error: unknown) => boolean;
19
+
20
+ /** Extract the `at ...` frames of an error stack as trimmed strings. */
21
+ export const getStackList: (error: {stack: string}) => string[];
22
+
23
+ /** A test event routed through the reporter pipeline — a loose bag. See TESTING.md. */
24
+ export type TestEvent = {[key: string]: unknown};
25
+
26
+ export type Hook = () => void | Promise<void>;
27
+
28
+ export interface StateOptions {
29
+ name?: string;
30
+ test?: number;
31
+ time?: number;
32
+ skip?: boolean;
33
+ todo?: boolean;
34
+ failOnce?: boolean;
35
+ timer?: Timer;
36
+ }
37
+
38
+ /**
39
+ * Per-test bookkeeping node: assert/skip/fail counters, inherited flags
40
+ * (`skip`, `todo`, `failOnce`), hook queues, and the abort controller backing
41
+ * `t.signal`. States chain via `parent`; iterating a state walks that chain
42
+ * from the innermost out.
43
+ */
44
+ export class State {
45
+ constructor(parent: State | null | undefined, options: StateOptions);
46
+
47
+ parent: State | null | undefined;
48
+ name: string;
49
+ test: number;
50
+ skip: boolean | undefined;
51
+ todo: boolean | undefined;
52
+ failOnce: boolean | undefined;
53
+ /** The parent's assert count at creation — offsets this state's assert ids. */
54
+ offset: number;
55
+ asserts: number;
56
+ skipped: number;
57
+ failed: number;
58
+ /** Direct assertions in this state only — `t.plan()`'s comparison basis. */
59
+ localAsserts: number;
60
+ stopTest: boolean;
61
+ timer: Timer;
62
+ startTime: number;
63
+ time: number;
64
+ abortController: AbortController;
65
+
66
+ beforeAll: Hook[];
67
+ afterAll: Hook[];
68
+ beforeEach: Hook[];
69
+ afterEach: Hook[];
70
+ isBeforeAllUsed: boolean;
71
+
72
+ /** Aborted when the test ends — backs `t.signal`. */
73
+ get signal(): AbortSignal;
74
+ abort(): void;
75
+ dispose(): void;
76
+
77
+ /** Roll this state's counters up into the parent. */
78
+ updateParent(): void;
79
+
80
+ runBeforeAll(): Promise<void>;
81
+ runAfterAll(): Promise<void>;
82
+ runBeforeEach(): Promise<void>;
83
+ runAfterEach(): Promise<void>;
84
+
85
+ /** Normalize an event: counters, ids, timing, stack extraction, stop propagation. */
86
+ preprocess(event: TestEvent): TestEvent;
87
+ /** Throws `StopTest` when the (pre-processed) event demands a stop, unless suppressed. */
88
+ postprocess(event: TestEvent, suppressStopTest?: boolean): void;
89
+
90
+ /** Walk this state and its ancestors, innermost first. */
91
+ [Symbol.iterator](): IterableIterator<State>;
92
+ }
93
+
94
+ export default State;
package/src/State.js CHANGED
@@ -1,3 +1,5 @@
1
+ // @ts-self-types="./State.d.ts"
2
+
1
3
  import {getTimer} from './utils/timer.js';
2
4
 
3
5
  export const signature = 'tape6-!@#$%^&*';
package/src/test.d.ts ADDED
@@ -0,0 +1,82 @@
1
+ import type {Test, Tester, TestOptions} from '../index.js';
2
+ import type {State, TestEvent} from './State.js';
3
+ import type {Deferred} from './utils/makeDeferred.js';
4
+
5
+ /**
6
+ * The output-reporter surface `runTests` relies on: an event sink plus the
7
+ * root `State` carrying suite-level hooks and the stop flag. Every shipped
8
+ * reporter satisfies it.
9
+ */
10
+ export interface OutputReporter {
11
+ report(event: TestEvent): void;
12
+ state: State;
13
+ [key: string]: unknown;
14
+ }
15
+
16
+ /** `true` once a runner claimed this process (test files skip self-running then). */
17
+ export const getConfiguredFlag: () => boolean;
18
+ export const setConfiguredFlag: (value: unknown) => boolean;
19
+
20
+ /** Resolves with the runner module once `setTestRunner` announces it. */
21
+ export const testRunner: Promise<unknown>;
22
+ export const setTestRunner: (runner: unknown) => void;
23
+
24
+ /**
25
+ * Ask to be called (once, deferred) when the first top-level test registers —
26
+ * how runners learn a test file has content. Fires immediately if tests are
27
+ * already queued.
28
+ */
29
+ export const registerNotifyCallback: (callback: () => void) => void;
30
+ export const unregisterNotifyCallback: (callback: () => void) => boolean;
31
+
32
+ /** The active tester stack, innermost last. */
33
+ export const getTesters: () => Tester[];
34
+
35
+ /** The innermost currently-running tester, or `null` outside any test. */
36
+ export const getTester: () => Tester | null;
37
+
38
+ /** A queued top-level test: normalized options + the deferred resolved when it finishes. */
39
+ export interface QueuedTest {
40
+ options: TestOptions;
41
+ deferred?: Deferred<unknown>;
42
+ }
43
+
44
+ export const getTests: () => QueuedTest[];
45
+ export const clearTests: () => void;
46
+
47
+ export const getReporter: () => OutputReporter | null;
48
+ export const setReporter: (newReporter: OutputReporter) => OutputReporter;
49
+
50
+ export type Hook = () => void | Promise<void>;
51
+
52
+ export const getBeforeAll: () => Hook[];
53
+ export const clearBeforeAll: () => void;
54
+ export const getAfterAll: () => Hook[];
55
+ export const clearAfterAll: () => void;
56
+ export const getBeforeEach: () => Hook[];
57
+ export const clearBeforeEach: () => void;
58
+ export const getAfterEach: () => Hook[];
59
+ export const clearAfterEach: () => void;
60
+
61
+ /**
62
+ * Run queued tests against the current reporter. Returns `false` when stopped
63
+ * early (`failOnce` / bail-out), `true` otherwise. Runner-facing — test files
64
+ * use `test()`.
65
+ */
66
+ export const runTests: (tests: QueuedTest[]) => Promise<boolean>;
67
+
68
+ /**
69
+ * Register a top-level test (delegates to the innermost tester when called
70
+ * inside a running test). Carries `skip` / `todo` / `asPromise` and the hook
71
+ * registrars as properties — see the `Test` interface.
72
+ */
73
+ export const test: Test;
74
+
75
+ export const beforeAll: (fn: Hook) => void;
76
+ export const afterAll: (fn: Hook) => void;
77
+ export const beforeEach: (fn: Hook) => void;
78
+ export const afterEach: (fn: Hook) => void;
79
+
80
+ export {beforeAll as before, afterAll as after};
81
+
82
+ export default test;
package/src/test.js CHANGED
@@ -1,3 +1,5 @@
1
+ // @ts-self-types="./test.d.ts"
2
+
1
3
  import {selectTimer} from './utils/timer.js';
2
4
  import {isAssertionError, isStopTest} from './State.js';
3
5
  import makeDeferred from './utils/makeDeferred.js';
@@ -21,13 +21,15 @@ export type TestEvent = {[key: string]: unknown};
21
21
  */
22
22
  export interface EventServerReporter {
23
23
  report(event: TestEvent, suppressStopTest?: boolean): void;
24
- state?: {stopTest?: boolean; [key: string]: unknown} | null;
24
+ // no index signature: class-typed states (`State`) have none and must remain assignable
25
+ state?: {stopTest?: boolean} | null;
25
26
  }
26
27
 
27
28
  /**
28
29
  * Options consumed by the base class. Runners pass their own keys through
29
30
  * (`flags`, `importmap`, `serverUrl`, `browser`, ...) — hence the index
30
- * signature.
31
+ * signature; `any`, not `unknown`, so checked-JS siblings read pass-through
32
+ * keys without casts (2026-07-10 decision, applies to all sidecar bags).
31
33
  */
32
34
  export interface EventServerOptions {
33
35
  /**
@@ -40,7 +42,7 @@ export interface EventServerOptions {
40
42
  * `TAPE6_WORKER_TIMEOUT` via `getOptions()`.
41
43
  */
42
44
  workerTimeout?: number;
43
- [key: string]: unknown;
45
+ [key: string]: any;
44
46
  }
45
47
 
46
48
  /**
@@ -0,0 +1,139 @@
1
+ import type {OutputReporter} from '../test.js';
2
+
3
+ /** Print `<commandName> <version>` (version read from package.json). */
4
+ export const printVersion: (commandName: string) => void;
5
+
6
+ /** Print the standard `--help` header, usage line, and an aligned option table. */
7
+ export const printHelp: (
8
+ commandName: string,
9
+ description: string,
10
+ usage: string,
11
+ options?: [flag: string, description: string][]
12
+ ) => void;
13
+
14
+ /** Print the flag legend (uppercase = on, lowercase = off). */
15
+ export const printFlagOptions: () => void;
16
+
17
+ /**
18
+ * Expand wildcard patterns to files under `rootFolder`. A `!`-prefixed pattern
19
+ * excludes matches accumulated so far — order matters. Returns paths relative
20
+ * to `rootFolder`.
21
+ */
22
+ export const resolvePatterns: (rootFolder: string, patterns: string[]) => Promise<string[]>;
23
+
24
+ /** The `tape6` config bag: test-set patterns keyed by environment, plus the importmap. */
25
+ export interface Tape6Config {
26
+ tests?: string | string[];
27
+ cli?: string | string[];
28
+ browser?: string | string[];
29
+ importmap?: {imports: Record<string, string>};
30
+ [key: string]: any;
31
+ }
32
+
33
+ /** Read `tape6.json`, else `package.json#tape6`, else the default patterns. */
34
+ export const getConfig: (
35
+ rootFolder: string,
36
+ traceFn?: (msg: string) => void
37
+ ) => Promise<Tape6Config>;
38
+
39
+ /** Resolve the file list for `type` (an environment-named test set + `cli` + `tests`). */
40
+ export const resolveTests: (
41
+ rootFolder: string,
42
+ type: string,
43
+ traceFn?: (msg: string) => void
44
+ ) => Promise<string[]>;
45
+
46
+ export type RuntimeName = 'node' | 'bun' | 'deno' | 'browser' | 'unknown';
47
+
48
+ /** The detected runtime and its env-var accessor (returns `undefined` in browsers). */
49
+ export const runtime: {name: RuntimeName; getEnvVar: (name: string) => string | undefined};
50
+
51
+ export type ReporterType = 'jsonl' | 'min' | 'tap' | 'tty';
52
+
53
+ export const reporterFileNames: Record<ReporterType, string>;
54
+
55
+ /** Reporter selected by `TAPE6_JSONL` / `TAPE6_MIN` / `TAPE6_TAP`; `null` in browsers. */
56
+ export const getReporterType: () => ReporterType | null;
57
+ export const getReporterFileName: (type?: ReporterType | null) => string;
58
+
59
+ export const DEFAULT_START_TIMEOUT: number;
60
+ export const DEFAULT_GRACE_TIMEOUT: number;
61
+
62
+ /** Per-worker startup budget (`TAPE6_WORKER_START_TIMEOUT`) in ms. */
63
+ export const getTimeoutValue: () => number;
64
+ /** Cooperative-drain window before force-kill (`TAPE6_GRACE_TIMEOUT`) in ms. */
65
+ export const getGraceTimeout: () => number;
66
+ /** Optional per-worker wall-clock budget (`TAPE6_WORKER_TIMEOUT`) in ms; 0 = off. */
67
+ export const getWorkerTimeout: () => number;
68
+
69
+ /** Single-letter flag → option name (`f` → `failureOnly`, ...). */
70
+ export const flagNames: Record<string, string>;
71
+
72
+ export interface ArgOption {
73
+ aliases?: string[];
74
+ initialValue?: string | null;
75
+ isValueRequired?: boolean;
76
+ /** Custom handler; mutate `flags` directly. */
77
+ fn?: (flags: Record<string, any>, name: string, value: string) => void;
78
+ }
79
+
80
+ /** A bare function is shorthand for `{fn, isValueRequired: true}`. */
81
+ export type ArgOptions = Record<
82
+ string,
83
+ ArgOption | ((flags: Record<string, any>, name: string, value: string) => void)
84
+ >;
85
+
86
+ /** Parse CLI args into files + flag values. CLI-only: in a browser returns an empty array. */
87
+ export const processArgs: (argOptions: ArgOptions) => {
88
+ files: string[];
89
+ flags: Record<string, any>;
90
+ };
91
+
92
+ export interface RunnerFlags {
93
+ /** The raw flag string, uppercase = on, lowercase = off. */
94
+ flags: string;
95
+ failureOnly?: boolean;
96
+ showTime?: boolean;
97
+ showBanner?: boolean;
98
+ showData?: boolean;
99
+ failOnce?: boolean;
100
+ showStack?: boolean;
101
+ showLog?: boolean;
102
+ showAssertNumber?: boolean;
103
+ monochrome?: boolean;
104
+ useJsonL?: boolean;
105
+ noConsoleCapture?: boolean;
106
+ hideStreams?: boolean;
107
+ /** Consumed by the parent (`EventServer`) only; children ignore them. */
108
+ graceTimeout?: number;
109
+ workerTimeout?: number;
110
+ [key: string]: any;
111
+ }
112
+
113
+ export interface RunnerOptions {
114
+ flags: RunnerFlags;
115
+ parallel: number;
116
+ files: string[];
117
+ /** Raw parsed option values, keyed by canonical option name. */
118
+ optionFlags: Record<string, any>;
119
+ }
120
+
121
+ /**
122
+ * Parse standard runner options (`--flags` / `-f`, `--par` / `-p`, plus
123
+ * `extraOptions`), fold in `TAPE6_*` env defaults, re-export `TAPE6_FLAGS`,
124
+ * and resolve `parallel` to the available hardware when unset.
125
+ */
126
+ export const getOptions: (extraOptions?: ArgOptions) => RunnerOptions;
127
+
128
+ /** Instantiate and install the environment-appropriate reporter if none is set. */
129
+ export const initReporter: (
130
+ getReporter: () => OutputReporter | null,
131
+ setReporter: (reporter: OutputReporter) => void,
132
+ flags: RunnerFlags
133
+ ) => Promise<void>;
134
+
135
+ /** Explicit `files` win; otherwise resolve the configured test sets for `type`. */
136
+ export const initFiles: (files: string[], rootFolder: string, type?: string) => Promise<string[]>;
137
+
138
+ /** Print the runtime / reporter / parallelism / flags / files banner. */
139
+ export const showInfo: (options: RunnerOptions, files?: string[] | null) => void;
@@ -1,3 +1,5 @@
1
+ // @ts-self-types="./config.d.ts"
2
+
1
3
  import {promises as fsp, readFileSync} from 'node:fs';
2
4
  import path from 'node:path';
3
5
  import os from 'node:os';
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Control-plane client for the tape6 test server's endpoints (`/--tests`,
3
+ * `/--patterns`, `/--importmap`). Owned by tape-six core: the endpoints and
4
+ * the cert-cache location (`node_modules/.cache/tape6/cert.pem`, written by
5
+ * `src/test-server/certs.js`) are core's contract — browser providers import
6
+ * this client instead of hand-mirroring it.
7
+ */
8
+
9
+ /** `true` when a TLS request read a plaintext answer — an h1 server behind an `https:` URL. */
10
+ export const isProtocolMismatch: (error: unknown) => boolean;
11
+
12
+ /**
13
+ * The response surface the control client guarantees — the common subset of
14
+ * global `fetch`'s `Response` and the scoped-TLS path's shim.
15
+ */
16
+ export interface ControlResponse {
17
+ ok: boolean;
18
+ status: number;
19
+ json(): Promise<any>;
20
+ }
21
+
22
+ /**
23
+ * Build a control fetch rooted at `rootFolder` (defaults to `process.cwd()`).
24
+ *
25
+ * Every request carries a 3s hard deadline (a dying listener can accept and
26
+ * never answer); timeouts surface as errors with `code: 'ETIMEDOUT'` and are
27
+ * never retried on a lower trust rung. `http:` URLs go through global `fetch`;
28
+ * `https:` goes through `node:https` so TLS trust stays scoped to control
29
+ * requests — never process-wide — walking the ladder: `TAPE6_CERT` pinned as
30
+ * CA (failures stay loud, no fallback), else the server's cached self-signed
31
+ * cert, else relaxed verification. The first successful rung is cached for
32
+ * the fetch's lifetime.
33
+ */
34
+ export const createControlFetch: (rootFolder?: string) => (url: string) => Promise<ControlResponse>;
@@ -0,0 +1,88 @@
1
+ // @ts-self-types="./controlFetch.d.ts"
2
+
3
+ import {readFile} from 'node:fs/promises';
4
+ import https from 'node:https';
5
+ import path from 'node:path';
6
+ import process from 'node:process';
7
+
8
+ // Control-plane client (/--tests, /--patterns, /--importmap). Global fetch can't
9
+ // relax TLS per request, so https: goes through node:https with trust scoped to
10
+ // these requests only — never process-wide. Ladder: TAPE6_CERT pinned as CA;
11
+ // else the server's cached self-signed cert (tape6 cert-ladder location);
12
+ // else relaxed verification.
13
+
14
+ // hard deadline on every request: a dying listener can accept and never answer
15
+ // (the 2026-07-04 chained-run hang)
16
+ const TIMEOUT = 3000;
17
+
18
+ // a TLS request read a plaintext HTTP answer — an h1 server behind an https: URL
19
+ export const isProtocolMismatch = error =>
20
+ !!error && (error.code === 'EPROTO' || /wrong version number/i.test(error.message || ''));
21
+
22
+ export const createControlFetch = (rootFolder = process.cwd()) => {
23
+ let tlsOptions = null;
24
+ const get = (url, options) =>
25
+ new Promise((resolve, reject) => {
26
+ const request = https.get(url, options, response => {
27
+ const chunks = [];
28
+ response.on('data', chunk => chunks.push(chunk));
29
+ response.on('error', error => {
30
+ clearTimeout(timer);
31
+ reject(error);
32
+ });
33
+ response.on('end', () => {
34
+ clearTimeout(timer);
35
+ const body = Buffer.concat(chunks);
36
+ resolve({
37
+ ok: response.statusCode >= 200 && response.statusCode < 300,
38
+ status: response.statusCode,
39
+ json: async () => JSON.parse(body.toString())
40
+ });
41
+ });
42
+ });
43
+ const timer = setTimeout(
44
+ () =>
45
+ request.destroy(
46
+ Object.assign(new Error(`control request timed out after ${TIMEOUT}ms: ${url}`), {
47
+ code: 'ETIMEDOUT'
48
+ })
49
+ ),
50
+ TIMEOUT
51
+ );
52
+ request.on('error', error => {
53
+ clearTimeout(timer);
54
+ reject(error);
55
+ });
56
+ });
57
+ return async url => {
58
+ if (!/^https:/i.test(url)) return fetch(url, {signal: AbortSignal.timeout(TIMEOUT)});
59
+ if (tlsOptions) return get(url, tlsOptions);
60
+ const certPath = process.env.TAPE6_CERT;
61
+ if (certPath) {
62
+ // explicit pin: failures stay loud, no fallback
63
+ const options = {ca: await readFile(path.resolve(rootFolder, certPath))};
64
+ const response = await get(url, options);
65
+ tlsOptions = options;
66
+ return response;
67
+ }
68
+ const cached = await readFile(
69
+ path.join(rootFolder, 'node_modules', '.cache', 'tape6', 'cert.pem')
70
+ ).catch(() => null);
71
+ if (cached) {
72
+ try {
73
+ const options = {ca: cached};
74
+ const response = await get(url, options);
75
+ tlsOptions = options;
76
+ return response;
77
+ } catch (error) {
78
+ // a relaxed retry after a timeout would just burn a second deadline
79
+ if (error.code === 'ETIMEDOUT') throw error;
80
+ // stale cache or an external server with its own cert — try relaxed
81
+ }
82
+ }
83
+ const options = {rejectUnauthorized: false};
84
+ const response = await get(url, options);
85
+ tlsOptions = options;
86
+ return response;
87
+ };
88
+ };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * A promise with its `resolve` / `reject` exposed — `Promise.withResolvers()`
3
+ * where the platform provides it, a hand-rolled equivalent otherwise.
4
+ */
5
+ export interface Deferred<T = unknown> {
6
+ promise: Promise<T>;
7
+ resolve: (value: T | PromiseLike<T>) => void;
8
+ reject: (reason?: unknown) => void;
9
+ }
10
+
11
+ export const makeDeferred: <T = unknown>() => Deferred<T>;
12
+
13
+ export default makeDeferred;
@@ -1,3 +1,5 @@
1
+ // @ts-self-types="./makeDeferred.d.ts"
2
+
1
3
  class Deferred {
2
4
  constructor() {
3
5
  this.promise = new Promise((resolve, reject) => {
@@ -0,0 +1,16 @@
1
+ /** Minimal millisecond clock. `Date` satisfies it; so does a wrapped `performance`. */
2
+ export interface Timer {
3
+ now(): number;
4
+ }
5
+
6
+ /** The current shared timer. Defaults to `Date` until `selectTimer()` upgrades it. */
7
+ export const getTimer: () => Timer;
8
+
9
+ /** Replace the shared timer; returns the new one. */
10
+ export const setTimer: (newTimer: Timer) => Timer;
11
+
12
+ /**
13
+ * Select the best clock available: `performance.now() + performance.timeOrigin`
14
+ * (native or via `node:perf_hooks`), falling back to `Date`.
15
+ */
16
+ export const selectTimer: () => Promise<void>;
@@ -1,3 +1,5 @@
1
+ // @ts-self-types="./timer.d.ts"
2
+
1
3
  let timer = Date;
2
4
 
3
5
  export const getTimer = () => timer;
@@ -1,104 +0,0 @@
1
- import process from 'node:process';
2
- import {fileURLToPath} from 'node:url';
3
-
4
- import {
5
- getOptions,
6
- initFiles,
7
- initReporter,
8
- showInfo,
9
- printVersion,
10
- printHelp,
11
- printFlagOptions
12
- } from '../src/utils/config.js';
13
-
14
- import {getReporter, setReporter} from '../src/test.js';
15
- import {selectTimer} from '../src/utils/timer.js';
16
-
17
- import TestWorker from '../src/runners/deno/TestWorker.js';
18
-
19
- const rootFolder = Deno.cwd();
20
-
21
- const showSelf = () => {
22
- const self = new URL(import.meta.url);
23
- if (self.protocol === 'file:') {
24
- console.log(fileURLToPath(self));
25
- } else {
26
- console.log(self);
27
- }
28
- Deno.exit(0);
29
- };
30
-
31
- const showVersion = () => {
32
- printVersion('tape6-deno');
33
- Deno.exit(0);
34
- };
35
-
36
- const showHelp = () => {
37
- printHelp('tape6-deno', 'Tape6 test runner for Deno', 'tape6-deno [options] [files...]', [
38
- ['--flags, -f <flags>', 'Set reporter flags (env: TAPE6_FLAGS)'],
39
- ['--par, -p <n>', 'Set parallelism level (env: TAPE6_PAR)'],
40
- ['--info', 'Show configuration info and exit'],
41
- ['--self', 'Print the path to this script and exit'],
42
- ['--help, -h', 'Show this help message and exit'],
43
- ['--version, -v', 'Show version and exit']
44
- ]);
45
- printFlagOptions();
46
- Deno.exit(0);
47
- };
48
-
49
- const main = async () => {
50
- const currentOptions = getOptions({
51
- '--self': {fn: showSelf, isValueRequired: false},
52
- '--info': {isValueRequired: false},
53
- '--help': {aliases: ['-h'], fn: showHelp, isValueRequired: false},
54
- '--version': {aliases: ['-v'], fn: showVersion, isValueRequired: false}
55
- });
56
-
57
- const [files] = await Promise.all([
58
- initFiles(currentOptions.files, rootFolder),
59
- initReporter(getReporter, setReporter, currentOptions.flags),
60
- selectTimer()
61
- ]);
62
-
63
- addEventListener('error', event => {
64
- console.log('UNHANDLED ERROR:', event.message);
65
- event.preventDefault();
66
- });
67
-
68
- if (currentOptions.optionFlags['--info'] === '') {
69
- showInfo(currentOptions, files);
70
- await new Promise(r => process.stdout.write('', r));
71
- process.exitCode = 0;
72
- return;
73
- }
74
-
75
- if (!files.length) {
76
- console.log('No files found.');
77
- await new Promise(r => process.stdout.write('', r));
78
- process.exitCode = 1;
79
- return;
80
- }
81
-
82
- const reporter = getReporter(),
83
- worker = new TestWorker(reporter, currentOptions.parallel, currentOptions.flags);
84
-
85
- reporter.report({type: 'test', test: 0});
86
-
87
- await new Promise(resolve => {
88
- worker.done = () => resolve();
89
- worker.execute(files);
90
- });
91
-
92
- const hasFailed = reporter.state && reporter.state.failed > 0;
93
-
94
- reporter.report({
95
- type: 'end',
96
- test: 0,
97
- fail: hasFailed
98
- });
99
-
100
- await new Promise(r => process.stdout.write('', r));
101
- process.exitCode = hasFailed ? 1 : 0;
102
- };
103
-
104
- main().catch(error => console.error('ERROR:', error));