tape-six 1.7.13 → 1.8.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.
Files changed (43) hide show
  1. package/README.md +3 -1
  2. package/TESTING.md +246 -47
  3. package/bin/tape6-server.js +19 -19
  4. package/index.d.ts +67 -0
  5. package/llms-full.txt +102 -0
  6. package/llms.txt +66 -0
  7. package/package.json +13 -4
  8. package/skills/write-tests/SKILL.md +63 -16
  9. package/src/State.js +26 -21
  10. package/src/Tester.js +59 -27
  11. package/src/deep6/env.d.ts +174 -0
  12. package/src/deep6/env.js +4 -4
  13. package/src/deep6/index.d.ts +86 -0
  14. package/src/deep6/index.js +10 -7
  15. package/src/deep6/traverse/assemble.d.ts +59 -0
  16. package/src/deep6/traverse/assemble.js +4 -3
  17. package/src/deep6/traverse/clone.d.ts +57 -0
  18. package/src/deep6/traverse/clone.js +4 -2
  19. package/src/deep6/traverse/deref.d.ts +59 -0
  20. package/src/deep6/traverse/deref.js +3 -2
  21. package/src/deep6/traverse/preprocess.d.ts +65 -0
  22. package/src/deep6/traverse/preprocess.js +2 -1
  23. package/src/deep6/traverse/walk.d.ts +219 -0
  24. package/src/deep6/traverse/walk.js +9 -4
  25. package/src/deep6/unifiers/matchCondition.d.ts +45 -0
  26. package/src/deep6/unifiers/matchCondition.js +1 -0
  27. package/src/deep6/unifiers/matchInstanceOf.d.ts +37 -0
  28. package/src/deep6/unifiers/matchInstanceOf.js +1 -0
  29. package/src/deep6/unifiers/matchString.d.ts +56 -0
  30. package/src/deep6/unifiers/matchString.js +1 -0
  31. package/src/deep6/unifiers/matchTypeOf.d.ts +37 -0
  32. package/src/deep6/unifiers/matchTypeOf.js +1 -0
  33. package/src/deep6/unifiers/ref.d.ts +52 -0
  34. package/src/deep6/unifiers/ref.js +1 -0
  35. package/src/deep6/unify.d.ts +95 -0
  36. package/src/deep6/unify.js +130 -66
  37. package/src/deep6/utils/replaceVars.d.ts +25 -0
  38. package/src/deep6/utils/replaceVars.js +23 -19
  39. package/src/response.d.ts +43 -0
  40. package/src/response.js +57 -0
  41. package/src/server.d.ts +81 -0
  42. package/src/server.js +69 -0
  43. package/src/test.js +26 -53
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  [npm-url]: https://npmjs.org/package/tape-six
5
5
 
6
6
  `tape-six` is a [TAP](https://en.wikipedia.org/wiki/Test_Anything_Protocol)-based library for unit tests.
7
- It is written in the modern JavaScript for the modern JavaScript and works in [Node](https://nodejs.org/), [Deno](https://deno.land/), [Bun](https://bun.sh/) and browsers. **Zero runtime dependencies.**
7
+ Written in modern JavaScript for modern JavaScript runtimes works in [Node](https://nodejs.org/), [Deno](https://deno.land/), [Bun](https://bun.sh/), and browsers. **Zero runtime dependencies.**
8
8
 
9
9
  It runs ES modules (`import`-based code) natively and supports CommonJS modules transparently using the built-in [ESM](https://nodejs.org/api/esm.html).
10
10
 
@@ -425,6 +425,8 @@ Test output can be controlled by flags. See [Supported flags](https://github.com
425
425
 
426
426
  The most recent releases:
427
427
 
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._
428
430
  - 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._
429
431
  - 1.7.12 _Added `--help`/`-h` and `--version`/`-v` options to all CLI utilities. Added flag documentation to help output._
430
432
  - 1.7.11 _Documentation consistency: added missing sequential test commands to AI docs, fixed test glob references._
package/TESTING.md CHANGED
@@ -110,29 +110,55 @@ test.todo('in progress', t => {
110
110
 
111
111
  All `msg` arguments are optional. If omitted, a generic message is used.
112
112
 
113
- | Method | Checks | Common aliases |
114
- | ------------------------------------ | ------------------------------- | -------------------------------- |
115
- | `t.pass(msg)` | Unconditional pass | |
116
- | `t.fail(msg)` | Unconditional fail | |
117
- | `t.ok(val, msg)` | `val` is truthy | `true`, `assert` |
118
- | `t.notOk(val, msg)` | `val` is falsy | `false`, `notok` |
119
- | `t.error(err, msg)` | `err` is falsy | `ifError`, `ifErr` |
120
- | `t.equal(a, b, msg)` | `a === b` | `is`, `strictEqual`, `isEqual` |
121
- | `t.notEqual(a, b, msg)` | `a !== b` | `not`, `notStrictEqual`, `isNot` |
122
- | `t.deepEqual(a, b, msg)` | Deep strict equality | `same`, `isEquivalent` |
123
- | `t.notDeepEqual(a, b, msg)` | Not deeply equal | `notSame`, `notEquivalent` |
124
- | `t.looseEqual(a, b, msg)` | `a == b` | |
125
- | `t.notLooseEqual(a, b, msg)` | `a != b` | |
126
- | `t.deepLooseEqual(a, b, msg)` | Deep loose equality | |
127
- | `t.notDeepLooseEqual(a, b, msg)` | Not deeply loosely equal | |
128
- | `t.throws(fn, msg)` | `fn()` throws | |
129
- | `t.doesNotThrow(fn, msg)` | `fn()` does not throw | |
130
- | `t.matchString(str, re, msg)` | `str` matches `re` | |
131
- | `t.doesNotMatchString(str, re, msg)` | `str` doesn't match `re` | |
132
- | `t.match(a, b, msg)` | Structural pattern match | |
133
- | `t.doesNotMatch(a, b, msg)` | No structural match | |
134
- | `t.rejects(promise, msg)` | Promise rejects (**await it**) | `doesNotResolve` |
135
- | `t.resolves(promise, msg)` | Promise resolves (**await it**) | `doesNotReject` |
113
+ | Method | Checks | Common aliases |
114
+ | ------------------------------------ | --------------------------------- | -------------------------------- |
115
+ | `t.pass(msg)` | Unconditional pass | |
116
+ | `t.fail(msg)` | Unconditional fail | |
117
+ | `t.ok(val, msg)` | `val` is truthy | `true`, `assert` |
118
+ | `t.notOk(val, msg)` | `val` is falsy | `false`, `notok` |
119
+ | `t.error(err, msg)` | `err` is falsy | `ifError`, `ifErr` |
120
+ | `t.equal(a, b, msg)` | `a === b` | `is`, `strictEqual`, `isEqual` |
121
+ | `t.notEqual(a, b, msg)` | `a !== b` | `not`, `notStrictEqual`, `isNot` |
122
+ | `t.deepEqual(a, b, msg)` | Deep strict equality | `same`, `isEquivalent` |
123
+ | `t.notDeepEqual(a, b, msg)` | Not deeply equal | `notSame`, `notEquivalent` |
124
+ | `t.looseEqual(a, b, msg)` | `a == b` | |
125
+ | `t.notLooseEqual(a, b, msg)` | `a != b` | |
126
+ | `t.deepLooseEqual(a, b, msg)` | Deep loose equality | |
127
+ | `t.notDeepLooseEqual(a, b, msg)` | Not deeply loosely equal | |
128
+ | `t.throws(fn, m?, msg)` | `fn()` throws (matches `m?`) | |
129
+ | `t.doesNotThrow(fn, msg)` | `fn()` does not throw | |
130
+ | `t.matchString(str, re, msg)` | `str` matches `re` | |
131
+ | `t.doesNotMatchString(str, re, msg)` | `str` doesn't match `re` | |
132
+ | `t.match(a, b, msg)` | Structural pattern match | |
133
+ | `t.doesNotMatch(a, b, msg)` | No structural match | |
134
+ | `t.rejects(promise, m?, msg)` | Rejects (matches `m?`) — `await` | `doesNotResolve` |
135
+ | `t.resolves(promise, m?, msg)` | Resolves (matches `m?`) — `await` | `doesNotReject` |
136
+
137
+ ### Matching errors and resolved values
138
+
139
+ `t.throws`, `t.rejects`, and `t.resolves` accept an optional **matcher** as their second argument. If the second argument is a string, it's still treated as the message (backward compatible).
140
+
141
+ ```js
142
+ t.throws(() => parse(''), TypeError); // Error subclass — instanceof
143
+ t.throws(() => parse(''), /unexpected end/); // RegExp on error.message
144
+ t.throws(
145
+ () => parse(''),
146
+ e => e.code === 'EPARSE'
147
+ ); // predicate
148
+ t.throws(() => parse(''), {code: 'EPARSE'}); // deep6 object pattern
149
+ await t.rejects(fetchData(), TypeError, 'wrong type');
150
+ await t.resolves(fetchData(), {status: 200}); // matches resolved value
151
+ ```
152
+
153
+ Matcher rules:
154
+
155
+ - **Error subclass** (a function whose prototype is Error) → `instanceof` check.
156
+ - **RegExp** → tested against `error.message` for `Error` instances, otherwise against `String(value)`.
157
+ - **Function** (non-Error-class) → predicate; truthy return = match.
158
+ - **Object** (non-null, non-RegExp) → uses deep6's `match()` for partial structural matching.
159
+ - **`null` or any primitive** → strict equality.
160
+
161
+ Falsy reasons (`null`, `0`, `false`, `''`, `NaN`) are correctly handled: `Promise.reject(null)` is still a rejection, and `throw 0` is still a throw.
136
162
 
137
163
  ### Async assertions
138
164
 
@@ -145,6 +171,69 @@ test('async asserts', async t => {
145
171
  });
146
172
  ```
147
173
 
174
+ ### Wildcards in deep equality
175
+
176
+ Use `t.any` (or its alias `t._`) inside an expected value to accept any value at that position. Useful for non-deterministic fields like timestamps or generated IDs:
177
+
178
+ ```js
179
+ test('partial match', t => {
180
+ const result = {id: 123, name: 'Alice', createdAt: new Date()};
181
+ t.deepEqual(result, {id: t.any, name: 'Alice', createdAt: t.any});
182
+ });
183
+ ```
184
+
185
+ ### Async cancellation with `t.signal`
186
+
187
+ `t.signal` is an `AbortSignal` that fires when the test ends, times out, or is stopped. Pass it to long-running async work so it cancels cleanly:
188
+
189
+ ```js
190
+ test('aborts on test end', async t => {
191
+ const res = await fetch(url, {signal: t.signal});
192
+ t.equal(res.status, 200);
193
+ });
194
+ ```
195
+
196
+ ### Test options
197
+
198
+ `test(name, options, fn)` accepts an options object. Type-recognition makes the form flexible (any of `test(opts, fn)`, `test(name, fn)`, `test(name, opts, fn)` is valid).
199
+
200
+ | Option | Effect |
201
+ | ---------------------- | ---------------------------------------------------------- |
202
+ | `name` | Test name (overrides positional) |
203
+ | `timeout` | Milliseconds; test fails and `t.signal` aborts if exceeded |
204
+ | `skip` | Skip the test (does not run) |
205
+ | `todo` | Run, but failures don't count |
206
+ | `beforeAll` / `before` | Run once before the test's first embedded test |
207
+ | `afterAll` / `after` | Run once after the test's last embedded test |
208
+ | `beforeEach` | Run before each embedded test |
209
+ | `afterEach` | Run after each embedded test |
210
+
211
+ ```js
212
+ test('with timeout', {timeout: 5000}, async t => {
213
+ await longOperation();
214
+ });
215
+ ```
216
+
217
+ By default tests have **no timeout**. Set `timeout` per test, or wrap a fixture suite to apply it broadly.
218
+
219
+ ### Other tester methods
220
+
221
+ - `t.comment(msg)` — emit a TAP comment line.
222
+ - `t.skipTest(msg)` — skip the **current** test from inside it (e.g. when a precondition isn't met at runtime).
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`.
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
+
227
+ ```js
228
+ test('OK evaluator', t => {
229
+ const a = 1,
230
+ b = 2,
231
+ c = 'three';
232
+ eval(t.OK('a + b + c === "3three"'));
233
+ // on failure, the report includes: {a: 1, b: 2, c: "three"}
234
+ });
235
+ ```
236
+
148
237
  ### Nested (embedded) tests
149
238
 
150
239
  Tests can be nested. The preferred way is `t.test()` because it makes the delegation explicit. Top-level `test()`/`it()` are equivalent inside a test body — they auto-delegate to the current tester:
@@ -246,6 +335,127 @@ test('suite B', dbOpts, async t => {
246
335
  });
247
336
  ```
248
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
+
249
459
  ## Migrating from other test frameworks
250
460
 
251
461
  `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.
@@ -400,30 +610,6 @@ Note: you can also keep using `node:assert` inside tape-six tests — `Assertion
400
610
  | `expect(a).to.be.ok` (Chai) | `t.ok(a)` or keep `expect` |
401
611
  | `expect(fn).to.throw()` (Chai) | `t.throws(fn)` or keep `expect` |
402
612
 
403
- ### Wildcard matching with `t.any`
404
-
405
- Use `t.any` (or `t._`) in deep equality checks to match any value:
406
-
407
- ```js
408
- test('partial match', t => {
409
- const result = {id: 123, name: 'Alice', createdAt: new Date()};
410
- t.deepEqual(result, {id: 123, name: 'Alice', createdAt: t.any});
411
- });
412
- ```
413
-
414
- ### Testing exceptions
415
-
416
- ```js
417
- test('errors', async t => {
418
- t.throws(() => {
419
- throw new Error('boom');
420
- }, 'should throw');
421
- t.doesNotThrow(() => 42, 'should not throw');
422
- await t.rejects(Promise.reject(new Error('fail')), 'should reject');
423
- await t.resolves(Promise.resolve(42), 'should resolve');
424
- });
425
- ```
426
-
427
613
  ### 3rd-party assertion libraries
428
614
 
429
615
  `tape-six` catches `AssertionError` automatically. You can use `chai` or `node:assert`:
@@ -514,6 +700,18 @@ Common combinations: `FO` (failures only + stop at first), `FOT` (+ show time).
514
700
 
515
701
  When multiple `--flags` are given, the last one wins. To see which files are being run (overriding `--flags FO` from a script), append `--flags fo` — lowercase disables the flags.
516
702
 
703
+ ### Other CLI options
704
+
705
+ | Option | Effect |
706
+ | ----------------- | --------------------------------------------------------------------- |
707
+ | `--par N` | Limit `tape6` to `N` parallel workers (default: number of CPU cores). |
708
+ | `--info` | Print the resolved test config (file lists, env, importmap) and exit. |
709
+ | `--self` | Print the path to the runner's own entry script (used in scripts). |
710
+ | `--help`, `-h` | Show usage. |
711
+ | `--version`, `-v` | Show version. |
712
+
713
+ Options that take a value accept both space-separated (`--par 4`) and `=`-separated (`--par=4`) forms.
714
+
517
715
  ### Environment variables
518
716
 
519
717
  - `TAPE6_FLAGS` — flags string (alternative to `--flags`).
@@ -521,6 +719,7 @@ When multiple `--flags` are given, the last one wins. To see which files are bei
521
719
  - `TAPE6_TAP` — force TAP output format.
522
720
  - `TAPE6_JSONL` — force JSONL output format.
523
721
  - `TAPE6_MIN` — force minimal output format.
722
+ - `TAPE6_WORKER_START_TIMEOUT` — milliseconds to wait for a worker to register its first test before declaring "no tests found". Default: `5000`. Bump this (e.g., `60000`) for tests with heavy `beforeAll` work like Docker container spawn or large fixture loads.
524
723
 
525
724
  ## Configuring test discovery
526
725
 
@@ -13,6 +13,7 @@ import {
13
13
  printVersion,
14
14
  printHelp
15
15
  } from '../src/utils/config.js';
16
+ import {startServer} from '../src/server.js';
16
17
 
17
18
  const fsp = fs.promises;
18
19
 
@@ -241,24 +242,8 @@ const portToString = port => (typeof port === 'string' ? 'pipe' : 'port') + ' '
241
242
  const host = process.env.HOST || 'localhost',
242
243
  port = normalizePort(process.env.PORT || '3000');
243
244
 
244
- server.listen(port, host);
245
-
246
- server.on('error', error => {
247
- if (error.syscall !== 'listen') throw error;
248
- const bind = portToString(port);
249
- switch (error.code) {
250
- case 'EACCES':
251
- console.log(red('Error: ') + yellow(bind) + red(' requires elevated privileges') + '\n');
252
- process.exit(1);
253
- case 'EADDRINUSE':
254
- console.log(red('Error: ') + yellow(bind) + red(' is already in use') + '\n');
255
- process.exit(1);
256
- }
257
- throw error;
258
- });
259
-
260
- server.on('listening', () => {
261
- //const addr = server.address();
245
+ try {
246
+ await startServer(server, {host, port});
262
247
  const bind = portToString(port);
263
248
  console.log(
264
249
  grey('Listening on ') +
@@ -274,4 +259,19 @@ server.on('listening', () => {
274
259
  );
275
260
  }
276
261
  console.log();
277
- });
262
+ } catch (error) {
263
+ if (error.syscall === 'listen') {
264
+ const bind = portToString(port);
265
+ if (error.code === 'EACCES') {
266
+ console.log(red('Error: ') + yellow(bind) + red(' requires elevated privileges') + '\n');
267
+ process.exitCode = 1;
268
+ } else if (error.code === 'EADDRINUSE') {
269
+ console.log(red('Error: ') + yellow(bind) + red(' is already in use') + '\n');
270
+ process.exitCode = 1;
271
+ } else {
272
+ throw error;
273
+ }
274
+ } else {
275
+ throw error;
276
+ }
277
+ }
package/index.d.ts CHANGED
@@ -1,3 +1,19 @@
1
+ /**
2
+ * Matcher accepted by `throws`, `rejects`, and `resolves`.
3
+ *
4
+ * - An `Error` subclass — checks `instanceof`.
5
+ * - A `RegExp` — tested against the error's `message` (or `String(value)` for non-Errors).
6
+ * - A predicate function — called with the value; truthy return = match.
7
+ * - An object pattern — uses deep6's `match` for structural matching.
8
+ * - `null` or any primitive — strict equality.
9
+ */
10
+ export type AssertionMatcher =
11
+ | (new (...args: any[]) => Error)
12
+ | RegExp
13
+ | ((value: unknown) => boolean)
14
+ | object
15
+ | null;
16
+
1
17
  /**
2
18
  * Test options
3
19
  */
@@ -501,6 +517,19 @@ export declare interface Tester {
501
517
  * @param message - Optional message to display if the assertion fails
502
518
  */
503
519
  throws(fn: () => void, message?: string): void;
520
+ /**
521
+ * Asserts that `fn` throws an error matching `matcher`.
522
+ * `matcher` may be:
523
+ * - an `Error` subclass (checks `instanceof`)
524
+ * - a `RegExp` (tested against the error's message, or `String(error)` for non-Errors)
525
+ * - a predicate function (called with the thrown value; truthy return = match)
526
+ * - an object pattern (uses deep6's `match`)
527
+ * - `null` or any primitive (strict equality)
528
+ * @param fn - The function to test
529
+ * @param matcher - The matcher to apply to the thrown value
530
+ * @param message - Optional message to display if the assertion fails
531
+ */
532
+ throws(fn: () => void, matcher: AssertionMatcher, message?: string): void;
504
533
 
505
534
  /**
506
535
  * Asserts that `fn` does not throw an error.
@@ -547,6 +576,14 @@ export declare interface Tester {
547
576
  * @param message - Optional message to display if the assertion fails
548
577
  */
549
578
  rejects(promise: Promise<unknown>, message?: string): Promise<void>;
579
+ /**
580
+ * Asserts that `promise` is rejected with a reason matching `matcher`.
581
+ * See `throws` for the `matcher` semantics.
582
+ * @param promise - The promise to test
583
+ * @param matcher - The matcher to apply to the rejection reason
584
+ * @param message - Optional message to display if the assertion fails
585
+ */
586
+ rejects(promise: Promise<unknown>, matcher: AssertionMatcher, message?: string): Promise<void>;
550
587
 
551
588
  /**
552
589
  * Asserts that `promise` is resolved.
@@ -554,6 +591,14 @@ export declare interface Tester {
554
591
  * @param message - Optional message to display if the assertion fails
555
592
  */
556
593
  resolves(promise: Promise<unknown>, message?: string): Promise<void>;
594
+ /**
595
+ * Asserts that `promise` is resolved with a value matching `matcher`.
596
+ * See `throws` for the `matcher` semantics (applied to the resolved value).
597
+ * @param promise - The promise to test
598
+ * @param matcher - The matcher to apply to the resolved value
599
+ * @param message - Optional message to display if the assertion fails
600
+ */
601
+ resolves(promise: Promise<unknown>, matcher: AssertionMatcher, message?: string): Promise<void>;
557
602
 
558
603
  /**
559
604
  * Returns a code as a string for evaluation that checks if the condition is truthy.
@@ -818,6 +863,17 @@ export declare interface Tester {
818
863
  * @param message - Optional message to display if the assertion fails
819
864
  */
820
865
  doesNotResolve(promise: Promise<unknown>, message?: string): Promise<void>;
866
+ /**
867
+ * Asserts that `promise` is rejected with a reason matching `matcher`. Alias of `rejects`.
868
+ * @param promise - The promise to test
869
+ * @param matcher - The matcher to apply to the rejection reason
870
+ * @param message - Optional message to display if the assertion fails
871
+ */
872
+ doesNotResolve(
873
+ promise: Promise<unknown>,
874
+ matcher: AssertionMatcher,
875
+ message?: string
876
+ ): Promise<void>;
821
877
 
822
878
  /**
823
879
  * Asserts that `promise` is resolved. Alias of `resolves`.
@@ -825,6 +881,17 @@ export declare interface Tester {
825
881
  * @param message - Optional message to display if the assertion fails
826
882
  */
827
883
  doesNotReject(promise: Promise<unknown>, message?: string): Promise<void>;
884
+ /**
885
+ * Asserts that `promise` is resolved with a value matching `matcher`. Alias of `resolves`.
886
+ * @param promise - The promise to test
887
+ * @param matcher - The matcher to apply to the resolved value
888
+ * @param message - Optional message to display if the assertion fails
889
+ */
890
+ doesNotReject(
891
+ promise: Promise<unknown>,
892
+ matcher: AssertionMatcher,
893
+ message?: string
894
+ ): Promise<void>;
828
895
 
829
896
  /**
830
897
  * Returns a code as a string for evaluation that checks if the condition is truthy.
package/llms-full.txt CHANGED
@@ -257,6 +257,108 @@ 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
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:
309
+
310
+ ```js
311
+ const server = setupServer(handler);
312
+
313
+ test('first', async t => {
314
+ const res = await fetch(`${server.base}/foo`);
315
+ });
316
+
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();
330
+ });
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
+ ```
353
+
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.
359
+
360
+ For status code, just use `res.status` — same on both `Response` and `IncomingMessage`.
361
+
260
362
  ## Configuring tests
261
363
 
262
364
  Configuration is read from `tape6.json` or the `"tape6"` section of `package.json`: