tape-six 1.8.0 → 1.10.0

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
@@ -122,6 +122,8 @@ See how it can be used in [tests/](https://github.com/uhop/tape-six/tree/master/
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.
124
124
 
125
+ When you use `tape-six` as a dependency, point your AI coding agent at the guides shipped inside `node_modules/tape-six/`: [`TESTING.md`](https://github.com/uhop/tape-six/blob/master/TESTING.md) and the `skills/` directory (`write-tests`, `run-tests`). They are written so an agent can write and run `tape-six` tests correctly without fetching anything.
126
+
125
127
  The whole API is based on two objects: `test` and `Tester`.
126
128
 
127
129
  ### `test`
@@ -319,7 +321,7 @@ The following methods are available (all `msg` arguments are optional):
319
321
  - `any` — a symbol that can be used in deep equivalency asserts to match any value.
320
322
  See [deep6's any](https://github.com/uhop/deep6/wiki/any) for details.
321
323
  - `_` — an alias of `any`.
322
- - `plan(n)` — sets the number of tests in the test suite. Rarely used.
324
+ - `plan(n)` — records the expected number of direct assertions; emits a TAP-comment diagnostic at test end if the count diverges (does not fail the test).
323
325
  - `comment(msg)` — sends a comment to the test harness. Rarely used.
324
326
  - `skipTest(...args, msg)` — skips the current test yet sends a message to the test harness.
325
327
  - `bailOut(msg)` — stops the test suite and sends a message to the test harness.
@@ -425,8 +427,10 @@ Test output can be controlled by flags. See [Supported flags](https://github.com
425
427
 
426
428
  The most recent releases:
427
429
 
428
- - 1.8.0 _New subpath modules `tape-six/server` (withServer/startServer/setupServer — ephemeral HTTP server harness with port-busy-rejects-not-hangs lifecycle, plus a hook helper for suite-shared servers) and `tape-six/response` (asText/asJson/asBytes/header/headers — reading helpers that work uniformly on W3C Response and Node http.IncomingMessage). Cross-runtime via `node:http` (Node, Bun, Deno). `bin/tape6-server.js` migrated to use `startServer` and `process.exitCode`. `node:` protocol prefix consistently applied to all Node built-in imports; added `types: ["node"]` to tsconfig._
429
- - 1.7.14 _Updated vendored `deep6` to 1.2.0: added `URL` support (unifier/walker/cloner + processors), named exports alongside default exports, fast path for naked objects, performance optimizations, improved TS typings, removed CommonJS build._
430
+ - 1.10.0 _Worker control channel stops in-flight workers, draining cleanup before a force-kill._
431
+ - 1.9.0 _New features: `t.plan(n)` emits a TAP-comment diagnostic on mismatch, `registerTesterMethod(name, fn)` registers tester plugins idempotently._
432
+ - 1.8.0 _New subpath modules: `tape-six/server` (HTTP server harness) and `tape-six/response` (response reading helpers for `Response` and `IncomingMessage`)._
433
+ - 1.7.14 _Updated vendored `deep6` to 1.2.0._
430
434
  - 1.7.13 _Replaced `process.exit()` / `Deno.exit()` with `process.exitCode` in test runners and `index.js` for graceful stdout flushing. Updated dependencies, GitHub Actions._
431
435
  - 1.7.12 _Added `--help`/`-h` and `--version`/`-v` options to all CLI utilities. Added flag documentation to help output._
432
436
  - 1.7.11 _Documentation consistency: added missing sequential test commands to AI docs, fixed test glob references._
package/TESTING.md CHANGED
@@ -221,7 +221,7 @@ By default tests have **no timeout**. Set `timeout` per test, or wrap a fixture
221
221
  - `t.comment(msg)` — emit a TAP comment line.
222
222
  - `t.skipTest(msg)` — skip the **current** test from inside it (e.g. when a precondition isn't met at runtime).
223
223
  - `t.bailOut(msg)` — stop the entire run. Catastrophic; use sparingly.
224
- - `t.plan(n)` — currently a documented no-op kept for migration compatibility from `tape` and `node:test`.
224
+ - `t.plan(n)` — record the expected number of direct assertions. If the count diverges at test end, a `# plan != count: expected N, ran M` TAP comment is emitted (diagnostic only, doesn't fail the test). Subtest assertions don't count toward the parent's plan.
225
225
  - `t.OK(expr, msg)` (aliases `t.TRUE`, `t.ASSERT`) — returns a code string for `eval()` that asserts an expression and dumps the values of the top-level identifiers in the expression on failure. Useful for compact arithmetic/state checks. Not usable in CSP-restricted contexts (uses `eval`).
226
226
 
227
227
  ```js
@@ -335,127 +335,6 @@ test('suite B', dbOpts, async t => {
335
335
  });
336
336
  ```
337
337
 
338
- ## Testing HTTP code
339
-
340
- Two subpath modules ship for tests that need HTTP-shaped fixtures. Both work on Node, Bun, and Deno via `node:http`. Browsers don't get them — running an HTTP server inside a webpage isn't a use case.
341
-
342
- > **Placement matters.** Test files that import `tape-six/server`, `tape-six/response`, or any other node-only API (e.g. `node:http`, `node:fs`, `node:child_process`) **must not** match a `tape6.tests` pattern that's universal to all environments — the browser worker would try to load them and fail with `Worker error` on the missing built-in. Put them in a CLI-only location (e.g. `tests/cli/test-*.js`) and add the matching glob to `tape6.cli` so it covers Node/Bun/Deno but not browsers. See [Configuring test discovery](#configuring-test-discovery).
343
-
344
- ### `tape-six/server` — server harness
345
-
346
- ```js
347
- import {withServer, startServer, setupServer} from 'tape-six/server';
348
- ```
349
-
350
- Three exports for three lifetimes. `withServer` is the per-test scoped resource (95% case). `startServer` is the procedural primitive. `setupServer` registers `beforeAll`/`afterAll` for suite-shared servers.
351
-
352
- **Per-test server** — start before the test body, tear down in `finally`:
353
-
354
- ```js
355
- import test from 'tape-six';
356
- import {withServer} from 'tape-six/server';
357
-
358
- test('GET / returns 200', t =>
359
- withServer(myHandler, async base => {
360
- const res = await fetch(`${base}/`);
361
- t.equal(res.status, 200);
362
- }));
363
- ```
364
-
365
- `myHandler` is a `(req, res) => void` callback (`http.RequestListener`). `base` is the bound URL, e.g. `"http://127.0.0.1:54321"`. Default host is `'127.0.0.1'` (explicit IPv4); pass `{host, port}` to override.
366
-
367
- `withServer` handles the symmetric case where the test makes requests directly. It's also the right tool when the SUT is something else (a CLI spawned in a subprocess) and the handler is a mock — the test body just looks different:
368
-
369
- ```js
370
- test('CLI hits the API correctly', t =>
371
- withServer(mockApi, async base => {
372
- const result = await spawnCli({env: {API_URL: base}});
373
- t.equal(result.exitCode, 0);
374
- t.equal(recordedRequests.length, 1);
375
- }));
376
- ```
377
-
378
- Either side may be the SUT. The names `serverHandler` / `clientHandler` reflect role on the wire, not which side is being tested.
379
-
380
- **Suite-shared server** — register hooks once, share across many tests:
381
-
382
- ```js
383
- import test, {beforeEach} from 'tape-six';
384
- import {setupServer} from 'tape-six/server';
385
-
386
- let recorded;
387
- const server = setupServer((req, res) => {
388
- recorded.push({method: req.method, url: req.url});
389
- res.writeHead(204).end();
390
- });
391
- beforeEach(() => {
392
- recorded = [];
393
- });
394
-
395
- test('records exactly one request', async t => {
396
- await fetch(`${server.base}/foo`);
397
- t.equal(recorded.length, 1);
398
- });
399
-
400
- test('records the path correctly', async t => {
401
- await fetch(`${server.base}/bar`);
402
- t.equal(recorded[0].url, '/bar');
403
- });
404
- ```
405
-
406
- The returned handle has live getters — `server.base` reads the current state on each access. **Don't destructure** at module load (`const {base} = setupServer(...)`); `base` is `undefined` until `beforeAll` runs. Per-test state reset (the `recorded = []` above) is user-side via your own `beforeEach`; `setupServer` owns the server lifecycle, you own state.
407
-
408
- **Procedural primitive** — for multi-phase tests or non-test code:
409
-
410
- ```js
411
- import http from 'node:http';
412
- import {startServer} from 'tape-six/server';
413
-
414
- const lc = await startServer(http.createServer(handler), {host, port});
415
- try {
416
- // ... lc.base, lc.port, lc.host ...
417
- } finally {
418
- await lc.close();
419
- }
420
- ```
421
-
422
- `startServer` accepts a fully-constructed server (so the caller can attach `'clientError'` listeners or configure TLS first). It races `'listening'` against `'error'` so port-busy / `EACCES` rejects with the original error rather than hanging — the failure mode existing helpers across the ecosystem suffer from on busy CI machines.
423
-
424
- `close()` is idempotent and calls `server.closeAllConnections()` (when available, Node 18.2+) so keep-alive sockets from `fetch` don't delay teardown.
425
-
426
- ### `tape-six/response` — response reading helpers
427
-
428
- ```js
429
- import {asText, asJson, asBytes, header, headers} from 'tape-six/response';
430
- ```
431
-
432
- Five reading helpers that work uniformly on both W3C `Response` (from `fetch`) and Node `http.IncomingMessage` (from `http.request`).
433
-
434
- ```js
435
- import test from 'tape-six';
436
- import {withServer} from 'tape-six/server';
437
- import {asJson, header} from 'tape-six/response';
438
-
439
- test('GET / returns JSON', t =>
440
- withServer(handler, async base => {
441
- const res = await fetch(`${base}/`);
442
- t.equal(res.status, 200);
443
- t.match(header(res, 'content-type'), /^application\/json/);
444
- const body = await asJson(res);
445
- t.deepEqual(body, {ok: true});
446
- }));
447
- ```
448
-
449
- | Helper | Returns | Notes |
450
- | ------------------- | ------------------------ | ----------------------------------- |
451
- | `asText(res)` | `Promise<string>` | UTF-8 |
452
- | `asJson(res)` | `Promise<unknown>` | parses JSON |
453
- | `asBytes(res)` | `Promise<Uint8Array>` | raw bytes |
454
- | `header(res, name)` | `string \| null` | case-insensitive single-header read |
455
- | `headers(res)` | `Record<string, string>` | all headers, lowercase keys |
456
-
457
- For status code, just use `res.status` (W3C `Response`) or `res.statusCode` (Node `IncomingMessage`) directly — no helper needed.
458
-
459
338
  ## Migrating from other test frameworks
460
339
 
461
340
  `tape-six` supports `describe`/`it` and `before`/`after` aliases. When called inside a test body, all top-level functions automatically delegate to the current tester. This means migration from Mocha, Jest, or `node:test` is nearly mechanical — change the import and swap assertions.
package/index.d.ts CHANGED
@@ -95,8 +95,11 @@ export declare interface Tester {
95
95
  signal: AbortSignal;
96
96
 
97
97
  /**
98
- * Plans the number of assertions that will be run. Unused.
99
- * @param n - The number of assertions
98
+ * Records the expected number of direct assertions. When the test ends, a
99
+ * `# plan != count: expected N, ran M` TAP comment is emitted if the count
100
+ * diverges. Diagnostic only — does not fail the test. Subtest assertions
101
+ * don't count toward the parent's plan.
102
+ * @param n - The expected number of assertions (non-negative integer)
100
103
  */
101
104
  plan(n: number): void;
102
105
 
@@ -1251,4 +1254,29 @@ export declare const before: typeof test.before;
1251
1254
  */
1252
1255
  export declare const after: typeof test.after;
1253
1256
 
1257
+ /**
1258
+ * Idempotently install a method on `Tester.prototype` so it appears as a
1259
+ * method on every tester instance (`t.<name>(...)`). Same name + same function
1260
+ * is a no-op (allows duplicate side-effect imports). Same name + a different
1261
+ * function throws so collisions surface loudly.
1262
+ *
1263
+ * Usage from a plugin module (e.g., `tape-six-spawn`):
1264
+ *
1265
+ * ```ts
1266
+ * import {registerTesterMethod} from 'tape-six/Tester.js';
1267
+ *
1268
+ * declare module 'tape-six' {
1269
+ * interface Tester {
1270
+ * spawnBin(bin: string, args: string[]): Promise<{code: number; stdout: string; stderr: string}>;
1271
+ * }
1272
+ * }
1273
+ *
1274
+ * registerTesterMethod('spawnBin', async function (bin, args) { ... });
1275
+ * ```
1276
+ *
1277
+ * @param name - non-empty method name
1278
+ * @param fn - function to install on `Tester.prototype`
1279
+ */
1280
+ export declare const registerTesterMethod: (name: string, fn: (...args: any[]) => any) => void;
1281
+
1254
1282
  export default test;
package/index.js CHANGED
@@ -162,6 +162,33 @@ const init = async () => {
162
162
  setCurrentReporter?.(reporter);
163
163
  }
164
164
 
165
+ if (isBrowser && typeof window.addEventListener == 'function') {
166
+ // Control plane: the parent page posts `tape6-terminate` to drain a running
167
+ // test (failOnce / bail / worker deadline). The reporter arms stopTest +
168
+ // fires the abort signal so the test unwinds. There is no in-page
169
+ // force-kill — see dev-docs/worker-control-channel.md.
170
+ window.addEventListener('message', event => {
171
+ if (event.data?.type === 'tape6-terminate') getReporter()?.terminate();
172
+ });
173
+ }
174
+
175
+ if (isCli) {
176
+ // Control plane (proc transport): a child spawned by tape-six-proc is
177
+ // marked with TAPE6_CONTROL. Open the stdin control channel so the parent
178
+ // can drain / terminate it, and so it stays alive after `end` until told to
179
+ // exit — see dev-docs/worker-control-channel.md.
180
+ const controlled =
181
+ (isDeno && Deno.env.get('TAPE6_CONTROL')) ||
182
+ (isBun && Bun.env.TAPE6_CONTROL) ||
183
+ (isNode && process.env.TAPE6_CONTROL);
184
+ if (controlled) {
185
+ const {listenControlChannel} = await import(
186
+ new URL('./src/utils/control-channel.js', import.meta.url)
187
+ );
188
+ listenControlChannel(getReporter);
189
+ }
190
+ }
191
+
165
192
  return {options, isBrowser, isBun, isDeno, isNode, isCli};
166
193
  };
167
194
 
@@ -254,6 +281,8 @@ if (!getConfiguredFlag()) {
254
281
  registerNotifyCallback(testRunner);
255
282
  }
256
283
 
284
+ export {registerTesterMethod} from './src/Tester.js';
285
+
257
286
  export {
258
287
  test,
259
288
  before,
package/llms-full.txt CHANGED
@@ -10,7 +10,7 @@
10
10
  - Before/after hooks: `beforeAll`, `afterAll`, `beforeEach`, `afterEach`
11
11
  - `test()` is aliased as `suite()`, `describe()`, and `it()` for easy migration
12
12
  - When called inside a test body, top-level functions auto-delegate to the current tester
13
- - Compatible with `AssertionError`-based libraries like `node:assert` and `chai`
13
+ - BYO assertion / mocking / property-based libs: `node:assert`, `chai`, `expect`, `node:test` mock, `sinon`, `fast-check`
14
14
 
15
15
  ## Quick start
16
16
 
@@ -192,7 +192,7 @@ The `Tester` object is passed to test functions. All `msg` arguments are optiona
192
192
 
193
193
  ### Miscellaneous
194
194
 
195
- - `plan(n)` — set expected number of assertions.
195
+ - `plan(n)` — record expected number of direct assertions. On test end, emits a `# plan != count: expected N, ran M` TAP comment if the count diverges (diagnostic only, doesn't fail the test). Subtest assertions don't count toward the parent's plan.
196
196
  - `comment(msg)` — send a comment to the reporter.
197
197
  - `skipTest(...args, msg)` — skip current test with a message.
198
198
  - `bailOut(msg)` — abort the test suite.
@@ -257,107 +257,27 @@ test('suite', opts, async t => {
257
257
 
258
258
  Multiple hooks of the same type run in registration order (before) or reverse order (after).
259
259
 
260
- ## Subpath modules
260
+ ## Plugins
261
261
 
262
- Two helper modules ship as separate subpath imports for tests that need HTTP-shaped fixtures. Both are cross-runtime (Node, Bun, Deno via `node:http`); browsers don't need them because running an HTTP server inside a webpage isn't a use case.
263
-
264
- ### tape-six/server — HTTP server harness
265
-
266
- ```js
267
- import {withServer, startServer, setupServer} from 'tape-six/server';
268
- ```
269
-
270
- #### `withServer(serverHandler, clientHandler, opts?)` — scoped resource (95% case)
271
-
272
- Spins up an `http.Server` with `serverHandler`, runs `clientHandler` with the bound base URL, tears down in `finally`. Cleanup runs whether `clientHandler` resolves, rejects, or throws synchronously. Returns whatever `clientHandler` returns.
273
-
274
- ```js
275
- test('GET / returns 200', t =>
276
- withServer(handler, async base => {
277
- const res = await fetch(`${base}/`);
278
- t.equal(res.status, 200);
279
- }));
280
- ```
281
-
282
- `serverHandler` is the per-request callback Node invokes on each incoming request. `clientHandler` is the per-scope test body, called once with `(base, lifecycle)` — naming reflects role on the wire (server side / client side), not which side is the SUT. Either side may be the code under test.
283
-
284
- `clientHandler` may make HTTP requests itself (`fetch(base)`-style), or set up a separate SUT that does (e.g., a spawned CLI given `${base}` as its endpoint env var), or do neither (multi-phase setup + assertions).
285
-
286
- #### `startServer(server, opts?)` — procedural primitive
287
-
288
- For multi-phase tests that span the lifecycle, or non-test code (e.g. `bin/tape6-server`) that wants long-term control. Accepts a fully-constructed `http.Server` (so callers can attach `'clientError'` handlers, configure TLS, etc., before listen).
289
-
290
- ```js
291
- const lc = await startServer(http.createServer(handler), {host, port});
292
- // ... use lc.base ...
293
- await lc.close();
294
- ```
295
-
296
- Returns a lifecycle handle:
297
-
298
- - `server` — the underlying `http.Server`.
299
- - `base` — bound base URL, e.g. `"http://127.0.0.1:54321"`.
300
- - `port` — actual bound port (OS-assigned when `port: 0`).
301
- - `host` — bound host.
302
- - `close()` — idempotent. Calls `server.closeAllConnections()` (when available) so keep-alive sockets don't delay teardown.
303
-
304
- Races `'listening'` against `'error'`: port-busy / `EACCES` rejects with the original error rather than hanging. Existing helpers across the toolkit ecosystem don't do this and have hung CI machines as a result.
305
-
306
- #### `setupServer(serverHandler, opts?)` — hook helper
307
-
308
- Registers `beforeAll` (start) and `afterAll` (close) and returns a live-getter handle for suite-shared servers:
262
+ Tester methods can be added via `registerTesterMethod(name, fn)`. Same name + same fn no-op (idempotent re-import); same name + different fn throws (loud collision detection).
309
263
 
310
264
  ```js
311
- const server = setupServer(handler);
265
+ import {registerTesterMethod} from 'tape-six';
312
266
 
313
- test('first', async t => {
314
- const res = await fetch(`${server.base}/foo`);
267
+ registerTesterMethod('spawnBin', async function (bin, args) {
268
+ // ... implementation, can call this.equal(...), this.reporter.report({...}), etc.
315
269
  });
316
270
 
317
- test('second', async t => {
318
- const res = await fetch(`${server.base}/bar`);
319
- });
320
- ```
321
-
322
- The returned object is `Object.freeze`d and uses property getters that read the live lifecycle. Don't destructure at module load (`const {base} = setupServer(...)`) — `base` is `undefined` until `beforeAll` runs.
323
-
324
- State reset stays user-side. For mock-server scenarios that need per-test state clearing, compose your own `beforeEach`:
325
-
326
- ```js
327
- const server = setupServer((req, res) => {
328
- recorded.push({method: req.method, url: req.url});
329
- res.writeHead(204).end();
271
+ // then in tests:
272
+ test('cli', async t => {
273
+ const {code} = await t.spawnBin('node', ['-v']);
274
+ t.equal(code, 0);
330
275
  });
331
- let recorded;
332
- beforeEach(() => { recorded = []; });
333
- ```
334
-
335
- `setupServer` owns the suite lifecycle; the caller owns suite state.
336
-
337
- #### Options
338
-
339
- ```ts
340
- interface ServerOptions {
341
- host?: string; // default '127.0.0.1' — explicit IPv4 avoids dual-stack ambiguity
342
- port?: number; // default 0 — OS-assigned
343
- }
344
- ```
345
-
346
- ### tape-six/response — HTTP response helpers
347
-
348
- Reading helpers that work uniformly with both `Response` (fetch results) and Node `http.IncomingMessage`:
349
-
350
- ```js
351
- import {asText, asJson, asBytes, header, headers} from 'tape-six/response';
352
276
  ```
353
277
 
354
- - `asText(res)` `Promise<string>` body as UTF-8.
355
- - `asJson(res)` → `Promise<unknown>` — body parsed as JSON.
356
- - `asBytes(res)` → `Promise<Uint8Array>` — body as raw bytes.
357
- - `header(res, name)` → `string | null` — case-insensitive single-header read. Array-valued `IncomingMessage` headers (e.g. `set-cookie`) are joined with `, `.
358
- - `headers(res)` → `Record<string, string>` — all headers as a plain object with lowercase keys.
278
+ `Tester` is declared as an `interface` in `index.d.ts` to make TS module augmentation natural — plugins extend the interface, no full class shape needed. The built-in `t.OK()` evaluator (`src/OK.js`) uses this exact pattern.
359
279
 
360
- For status code, just use `res.status` same on both `Response` and `IncomingMessage`.
280
+ Plugin installation is **per-file**. `tape6-seq` runs all files in one process; `tape6` uses workers; `tape6-proc` (from the sister package `tape-six-proc`) uses subprocesses; browser tests run in iframes. Each isolation context has its own module graph, so a plugin import in file A is invisible to file B when they're in different contexts. Every test file that uses a plugin must import it directly. `registerTesterMethod`'s idempotency makes repeated imports safe. See [Writing plugins](https://github.com/uhop/tape-six/wiki/Writing-plugins) for the full guide.
361
281
 
362
282
  ## Configuring tests
363
283
 
@@ -458,6 +378,8 @@ Browser: `http://localhost:3000/?flags=FO`
458
378
  - `TAPE6_JSONL` — force JSONL reporter (any non-empty value).
459
379
  - `TAPE6_MIN` — force minimal reporter (any non-empty value).
460
380
  - `TAPE6_TEST_FILE_NAME` — set by runners to identify the current test file.
381
+ - `TAPE6_GRACE_TIMEOUT` — ms a worker gets to drain (run cleanup, flush) after the runner asks it to stop, before being force-killed (default: 5000).
382
+ - `TAPE6_WORKER_TIMEOUT` — per-file wall-clock budget in ms; on expiry the runner stops that worker (default: 0, disabled). The complement to the per-test `timeout` option: this one can stop a worker that ignores `t.signal`.
461
383
 
462
384
  ## Browser testing
463
385
 
@@ -473,19 +395,18 @@ Browser: `http://localhost:3000/?flags=FO`
473
395
  - tape-six-puppeteer (https://www.npmjs.com/package/tape-six-puppeteer) — automates browser testing with Puppeteer.
474
396
  - tape-six-playwright (https://www.npmjs.com/package/tape-six-playwright) — automates browser testing with Playwright.
475
397
 
476
- ## 3rd-party assertion libraries
398
+ ## 3rd-party libraries (bring-your-own)
477
399
 
478
- `tape-six` supports `AssertionError`-based assertions. You can use `node:assert` or `chai` inside test functions:
400
+ `tape-six` doesn't bundle assertion / mock / property-based libraries. It catches whatever the test throws — anything throwing a node-compatible `AssertionError` (with `name`, `operator`, `actual`, `expected`) is reported as a regular assertion; other errors are reported as `UNEXPECTED EXCEPTION` (test still fails).
479
401
 
480
- ```js
481
- import test from 'tape-six';
482
- import {assert, expect} from 'chai';
402
+ **Verified candidates** (full per-lib examples and browser importmap snippets in the wiki):
483
403
 
484
- test('with chai', t => {
485
- expect(1).to.be.lessThan(2);
486
- assert.deepEqual([1], [1]);
487
- });
488
- ```
404
+ - `node:assert` — built-in to Node/Bun/Deno, throws `AssertionError`. Cleanest path.
405
+ - `chai` — `expect`/`should`/`assert` styles, throws `AssertionError`. Works in browsers via importmap entry.
406
+ - `expect` (Jest's standalone) — throws plain `Error`, not `AssertionError`. Works but failures render as `UNEXPECTED EXCEPTION` with ANSI-colored messages. Wrap negative cases in `t.throws()`.
407
+ - `node:test` mock — built-in spies/stubs/timers via `import {mock} from 'node:test'`. Doesn't throw; assert on `spy.mock.calls` with `t.*`.
408
+ - `sinon` — same pattern as `node:test` mock. Assert on `spy.callCount` / `spy.firstCall.args`.
409
+ - `fast-check` — property-based testing. Throws plain `Error` on counterexample (with seed + path for repro). Wrap negative cases in `t.throws()`.
489
410
 
490
411
  ```js
491
412
  import test from 'tape-six';
@@ -497,4 +418,18 @@ test('with node:assert', t => {
497
418
  });
498
419
  ```
499
420
 
421
+ ```js
422
+ import test from 'tape-six';
423
+ import {mock} from 'node:test';
424
+
425
+ test('with node:test mock', t => {
426
+ const spy = mock.fn();
427
+ spy(1, 2);
428
+ t.equal(spy.mock.calls.length, 1);
429
+ t.deepEqual(spy.mock.calls[0].arguments, [1, 2]);
430
+ });
431
+ ```
432
+
433
+ Wiki: [3rd-party assertion libraries](https://github.com/uhop/tape-six/wiki/3rd%E2%80%90party-assertion-libraries), [mock libraries](https://github.com/uhop/tape-six/wiki/3rd%E2%80%90party-mock-libraries), [property-based testing](https://github.com/uhop/tape-six/wiki/3rd%E2%80%90party-property-based-testing).
434
+
500
435
  Assertions that throw `AssertionError` are automatically caught and reported.
package/llms.txt CHANGED
@@ -105,11 +105,15 @@ The object passed to test functions. Provides assertions and test control.
105
105
 
106
106
  #### Utilities
107
107
 
108
- - `t.plan(n)` — declare expected assertion count (rarely needed).
108
+ - `t.plan(n)` — record expected direct-assertion count; emits a `# plan != count: expected N, ran M` TAP comment at test end on mismatch (diagnostic, doesn't fail).
109
109
  - `t.comment(msg)` — emit a comment.
110
110
  - `t.skipTest(msg)` — skip current test with a message.
111
111
  - `t.bailOut(msg)` — abort entire test suite.
112
112
 
113
+ ### Plugins
114
+
115
+ - `registerTesterMethod(name, fn)` — install a method on `Tester.prototype` from a plugin module. Idempotent for same `fn`; throws on collision with a different `fn`. Plugin imports are per-file (workers / subprocesses / iframes don't share module graphs).
116
+
113
117
  ### Hooks
114
118
 
115
119
  Hooks can be registered as standalone functions or via test options or Tester methods.
@@ -139,37 +143,6 @@ test('suite', async t => {
139
143
  test.beforeAll(() => { /* ... */ });
140
144
  ```
141
145
 
142
- ## Subpath modules
143
-
144
- ### tape-six/server — HTTP server harness
145
-
146
- Helpers for tests that need an ephemeral `node:http` server. Cross-runtime (Node, Bun, Deno; not for browser-side tests).
147
-
148
- ```js
149
- import {withServer, startServer, setupServer} from 'tape-six/server';
150
- ```
151
-
152
- - `withServer(serverHandler, clientHandler, opts?)` — scoped resource: spin up a server, run the test body with the base URL, tear down in `finally`. The 95% case.
153
- - `startServer(server, opts?)` — procedural primitive: returns `{server, base, port, host, close}`. For multi-phase tests or non-test code (e.g., `bin/tape6-server`) that wants long-term control. Races `'listening'` against `'error'` so port-busy rejects instead of hanging.
154
- - `setupServer(serverHandler, opts?)` — registers `beforeAll`/`afterAll` and returns a live-getter context for suite-shared servers. Don't destructure the returned object at module load — properties read live state on each access.
155
- - Options: `{host = '127.0.0.1', port = 0}`. Default host is explicit IPv4.
156
-
157
- `serverHandler` is the per-request callback (Node calls it once per incoming request). `clientHandler` is the per-scope test body (called once with the base URL). Either side may be the SUT.
158
-
159
- ### tape-six/response — HTTP response helpers
160
-
161
- Reading helpers that work uniformly with both `Response` (fetch results) and `http.IncomingMessage` (Node low-level requests).
162
-
163
- ```js
164
- import {asText, asJson, asBytes, header, headers} from 'tape-six/response';
165
- ```
166
-
167
- - `asText(res)` — body as UTF-8 string.
168
- - `asJson(res)` — body parsed as JSON.
169
- - `asBytes(res)` — body as `Uint8Array`.
170
- - `header(res, name)` — single header value, case-insensitive. Returns `null` if absent.
171
- - `headers(res)` — all headers as a plain object with lowercase keys.
172
-
173
146
  ## Common patterns
174
147
 
175
148
  ### Basic test file
@@ -244,41 +217,6 @@ describe('my module', () => {
244
217
  });
245
218
  ```
246
219
 
247
- ### Testing HTTP code with `withServer`
248
-
249
- ```js
250
- import test from 'tape-six';
251
- import {withServer} from 'tape-six/server';
252
- import {asJson} from 'tape-six/response';
253
-
254
- test('GET /users returns the list', t =>
255
- withServer(myHandler, async base => {
256
- const res = await fetch(`${base}/users`);
257
- t.equal(res.status, 200);
258
- const body = await asJson(res);
259
- t.equal(body.length, 3);
260
- }));
261
- ```
262
-
263
- Suite-shared server (multiple tests against one instance):
264
-
265
- ```js
266
- import test, {beforeEach} from 'tape-six';
267
- import {setupServer} from 'tape-six/server';
268
-
269
- const server = setupServer((req, res) => {
270
- recorded.push({method: req.method, url: req.url});
271
- res.writeHead(204).end();
272
- });
273
- let recorded;
274
- beforeEach(() => { recorded = []; });
275
-
276
- test('records one request', async t => {
277
- await fetch(`${server.base}/foo`);
278
- t.equal(recorded.length, 1);
279
- });
280
- ```
281
-
282
220
  ### Skip and TODO
283
221
 
284
222
  ```js
@@ -303,6 +241,8 @@ npx tape6-server --trace # start browser test server
303
241
  - `TAPE6_JSONL` — force JSONL reporter (any non-empty value).
304
242
  - `TAPE6_MIN` — force minimal reporter (any non-empty value).
305
243
  - `TAPE6_TEST_FILE_NAME` — set by runners to identify the current test file.
244
+ - `TAPE6_GRACE_TIMEOUT` — ms a worker gets to drain (run cleanup, flush) after the runner asks it to stop, before being force-killed (default: 5000).
245
+ - `TAPE6_WORKER_TIMEOUT` — per-file wall-clock budget in ms; on expiry the runner stops that worker (default: 0, disabled). Complements the per-test `timeout` option: this one can stop a worker that ignores `t.signal`.
306
246
 
307
247
  ## Configuration (package.json)
308
248
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tape-six",
3
- "version": "1.8.0",
3
+ "version": "1.10.0",
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",
@@ -8,15 +8,6 @@
8
8
  "types": "index.d.ts",
9
9
  "exports": {
10
10
  ".": "./index.js",
11
- "./server": {
12
- "types": "./src/server.d.ts",
13
- "default": "./src/server.js"
14
- },
15
- "./response": {
16
- "types": "./src/response.d.ts",
17
- "default": "./src/response.js"
18
- },
19
- "./bin/*": "./bin/*",
20
11
  "./*": "./src/*"
21
12
  },
22
13
  "bin": {
@@ -98,21 +89,21 @@
98
89
  "/tests/test-*.cjs",
99
90
  "/tests/cli/test-*.js"
100
91
  ],
92
+ "node": [
93
+ "/tests/node/test-*.js"
94
+ ],
101
95
  "browser": [
102
96
  "/tests/browser/test-*.html"
103
97
  ],
104
98
  "importmap": {
105
99
  "imports": {
106
100
  "tape-six": "../index.js",
107
- "tape-six/": "../src/",
108
- "chai": "../node_modules/chai/index.js"
101
+ "tape-six/": "../src/"
109
102
  }
110
103
  }
111
104
  },
112
105
  "devDependencies": {
113
- "@types/chai": "^5.2.3",
114
- "@types/node": "^25.6.0",
115
- "chai": "^6.2.2",
106
+ "@types/node": "^25.9.1",
116
107
  "typescript": "^6.0.3"
117
108
  }
118
109
  }