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/llms.txt CHANGED
@@ -139,6 +139,37 @@ test('suite', async t => {
139
139
  test.beforeAll(() => { /* ... */ });
140
140
  ```
141
141
 
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
+
142
173
  ## Common patterns
143
174
 
144
175
  ### Basic test file
@@ -213,6 +244,41 @@ describe('my module', () => {
213
244
  });
214
245
  ```
215
246
 
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
+
216
282
  ### Skip and TODO
217
283
 
218
284
  ```js
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tape-six",
3
- "version": "1.7.13",
3
+ "version": "1.8.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,6 +8,14 @@
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
+ },
11
19
  "./bin/*": "./bin/*",
12
20
  "./*": "./src/*"
13
21
  },
@@ -87,7 +95,8 @@
87
95
  "/tests/test-*.mjs"
88
96
  ],
89
97
  "cli": [
90
- "/tests/test-*.cjs"
98
+ "/tests/test-*.cjs",
99
+ "/tests/cli/test-*.js"
91
100
  ],
92
101
  "browser": [
93
102
  "/tests/browser/test-*.html"
@@ -102,8 +111,8 @@
102
111
  },
103
112
  "devDependencies": {
104
113
  "@types/chai": "^5.2.3",
105
- "@types/node": "^25.5.0",
114
+ "@types/node": "^25.6.0",
106
115
  "chai": "^6.2.2",
107
- "typescript": "^6.0.2"
116
+ "typescript": "^6.0.3"
108
117
  }
109
118
  }
@@ -12,29 +12,76 @@ Write or update tests using the tape-six testing library.
12
12
  - `tape-six` supports ES modules (`.js`, `.mjs`, `.ts`, `.mts`) and CommonJS (`.cjs`, `.cts`).
13
13
  - TypeScript is supported natively — no transpilation needed (Node 22+, Deno, Bun run `.ts` files directly).
14
14
  - The default `tape6` runner uses worker threads for parallel execution. `tape6-seq` runs sequentially in-process — useful for debugging or when tests share state.
15
+ - `tape-six` catches `AssertionError` automatically, so you can use Chai or `node:assert` inside tape-six tests if a project already uses them.
15
16
 
16
17
  ## Steps
17
18
 
18
- 1. Read the testing guide at `node_modules/tape-six/TESTING.md` for API reference and patterns.
19
+ 1. Read the testing guide at `node_modules/tape-six/TESTING.md` for the full API reference and patterns.
19
20
  2. Identify the module or feature to test. Read its source code to understand the public API.
20
21
  3. Create or update the test file in `tests/test-<name>.js` (or `.ts` for TypeScript, `.cjs` for CommonJS):
21
22
  - **ESM** (`.js`): `import test from 'tape-six'` and import the module under test using the project's package name.
22
23
  - **CJS** (`.cjs`): `const {test} = require('tape-six')` and `const {...} = require('my-package')`. Always use `require()` — it is the correct CJS pattern. **Do NOT use `await import()` unless you have confirmed** (e.g., grep for `^await` at the top level) that the module under test uses top-level `await`, which is rare.
23
24
  - Write one top-level `test()` per logical group.
24
- - Use embedded `await t.test()` for sub-cases.
25
- - Use `t.beforeEach`/`t.afterEach` for shared setup/teardown.
25
+ - Use embedded `await t.test()` for sub-cases. **Always `await` embedded tests** to preserve execution order.
26
+ - Use `t.beforeEach`/`t.afterEach` for shared setup/teardown; `t.beforeAll`/`t.afterAll` (aliases `t.before`/`t.after`) for one-shot fixtures.
26
27
  - Cover: normal operation, edge cases, error conditions.
27
- - Use `t.equal` for primitives, `t.deepEqual` for objects/arrays, `t.throws` for errors, `await t.rejects` for async errors.
28
28
  - All `msg` arguments are optional but recommended for clarity.
29
- 4. **Browser-specific tests** — if the project uses browser testing with `tape6-server`. See the "Browser testing" section in `TESTING.md` for full details.
30
- - Browsers run `.js` and `.mjs` only no TypeScript, no CommonJS.
31
- - Browsers can also run `.html` shim files (with inline importmap and `<script type="module">`).
32
- - Place browser-only files in a subdirectory (e.g., `tests/browser/`) and add patterns to `"browser"` in the `tape6` config.
33
- - Run: `npx tape6-server --trace`, then open `http://localhost:3000`.
34
- 5. **Environment-specific tests** — the `tape6` config in `package.json` supports per-environment patterns (`tests`, `cli`, `node`, `bun`, `deno`, `browser`). All are additive. See "Configuring test discovery" in `TESTING.md`.
35
- 6. **Verify (CLI):** run the new test file directly: `node tests/test-<name>.js`
36
- - Run the full suite to check for regressions: `npm test`
37
- - If debugging, use `npm run test:seq` (runs sequentially, easier to trace issues).
38
- - To see which files are being run, add `--flags fo` (overrides the default `--flags FO`).
39
- 7. **Verify (browser):** start `npx tape6-server --trace`, then open `http://localhost:3000/?q=/tests/test-<name>.js` to run a specific file. Use multiple `?q=` parameters to run several files. Open `http://localhost:3000/` to run all configured tests.
40
- 8. Report results and any failures.
29
+ 4. **Pick the right assertion:**
30
+ - Primitives: `t.equal(a, b)` (strict). Use `t.deepEqual(a, b)` for objects/arrays.
31
+ - Truthiness: `t.ok(value)`, `t.notOk(value)`.
32
+ - Errors: `t.error(err)` asserts `err` is falsy (callback-style "no error").
33
+ - Sync throws: `t.throws(fn)`, `t.doesNotThrow(fn)`.
34
+ - Async: `await t.rejects(promise)`, `await t.resolves(promise)`.
35
+ - Strings: `t.matchString(str, /re/)`, `t.doesNotMatchString(str, /re/)`.
36
+ - Structural: `t.match(actual, pattern)` / `t.doesNotMatch(actual, pattern)` for partial object matching (uses deep6 `match()`).
37
+ 5. **Match error/value shape** (`throws` / `rejects` / `resolves` accept an optional matcher as the second arg):
38
+ ```js
39
+ t.throws(() => parse(''), TypeError); // Error subclass
40
+ t.throws(() => parse(''), /unexpected end/); // RegExp on error.message
41
+ t.throws(
42
+ () => parse(''),
43
+ e => e.code === 'EPARSE'
44
+ ); // predicate
45
+ t.throws(() => parse(''), {code: 'EPARSE'}); // deep6 object pattern
46
+ await t.rejects(fetchData(), /404/);
47
+ await t.resolves(fetchData(), {status: 200}); // matches resolved value
48
+ ```
49
+ A string second arg is still treated as the message for backward compatibility.
50
+ 6. **Wildcards in deep equality** — use `t.any` (alias `t._`) inside expected values to skip non-deterministic fields:
51
+ ```js
52
+ t.deepEqual(result, {id: t.any, name: 'Alice', createdAt: t.any});
53
+ ```
54
+ 7. **Async cancellation** — `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:
55
+ ```js
56
+ test('aborts on test end', async t => {
57
+ const res = await fetch(url, {signal: t.signal});
58
+ t.equal(res.status, 200);
59
+ });
60
+ ```
61
+ 8. **Test options** — `test(name, options, fn)` accepts `{timeout, skip, todo, before, after, beforeAll, afterAll, beforeEach, afterEach}`. Use `timeout` (ms) for tests with bounded async work; the test fails and `t.signal` fires when exceeded.
62
+ 9. **Misc:**
63
+ - `test.skip(name, fn)` / `test.todo(name, fn)` — `skip` doesn't run; `todo` runs but failures don't fail the suite.
64
+ - `t.comment(msg)` — emit a TAP comment line.
65
+ - `t.skipTest(msg)` — skip the _current_ test from inside it.
66
+ - `t.bailOut(msg)` — stop the entire run (catastrophic).
67
+ - `t.OK(expr, msg)` (aliases `t.TRUE`, `t.ASSERT`) — returns a code string for `eval()` that asserts an expression and dumps top-level variables on failure. Useful for compact arithmetic/state checks: `eval(t.OK('a + b === 3'))`. Do not use in CSP-restricted contexts.
68
+ 10. **Testing HTTP code** — for tests that need an ephemeral HTTP server or want to read responses uniformly:
69
+ - **`tape-six/server`** (`withServer` / `startServer` / `setupServer`) — wraps the `node:http` lifecycle. `withServer(serverHandler, clientHandler, opts?)` is the per-test scoped resource: starts a server, runs the test body with the bound base URL, tears down in `finally`. `setupServer(serverHandler, opts?)` registers `beforeAll`/`afterAll` and returns a live-getter handle for suite-shared servers (don't destructure at module load — properties read live state). `startServer(server, opts?)` is the procedural primitive for multi-phase tests. Default host is `'127.0.0.1'`. Cross-runtime (Node, Bun, Deno).
70
+ - **`tape-six/response`** (`asText` / `asJson` / `asBytes` / `header` / `headers`) — reading helpers that work uniformly on both W3C `Response` (fetch results) and Node `http.IncomingMessage`.
71
+ - Per-test mock-server state reset (e.g., clearing a `recorded[]` array) stays user-side: compose your own `beforeEach`. `setupServer` owns the server lifecycle; you own state.
72
+ - **Placement:** these tests can't run in the browser worker — `node:http` doesn't exist there. Don't drop them into a folder matched by the universal `tape6.tests` glob; put them in a CLI-only path (e.g. `tests/cli/test-*.js`) and ensure the matching glob is in `tape6.cli` instead.
73
+ - See "Testing HTTP code" in `TESTING.md` for examples.
74
+ 11. **Browser-specific tests** — if the project uses browser testing with `tape6-server`. See "Browser testing" in `TESTING.md`.
75
+ - Browsers run `.js` and `.mjs` only — no TypeScript, no CommonJS.
76
+ - Browsers can also run `.html` shim files (with inline importmap and `<script type="module">`).
77
+ - Place browser-only files in `tests/browser/` and add patterns to `"browser"` in the `tape6` config.
78
+ - Run: `npx tape6-server --trace`, then open `http://localhost:3000`. Use `?q=<glob>` to filter, `?flags=FO` and `?par=N` to control output and parallelism.
79
+ 12. **Environment-specific tests** — the `tape6` config in `package.json` supports per-env patterns (`tests`, `cli`, `node`, `bun`, `deno`, `browser`). All are additive. See "Configuring test discovery" in `TESTING.md`.
80
+ 13. **Verify (CLI):** run the new test file directly: `node tests/test-<name>.js`
81
+ - Run the full suite to check for regressions: `npm test`
82
+ - For debugging, use `npm run test:seq` (sequential, in-process).
83
+ - To see which files are being run, add `--flags fo` (overrides the default `--flags FO`).
84
+ - To inspect the resolved config without running, use `npx tape6 --info`.
85
+ - For tests with heavy `beforeAll` (Docker spawn, big fixture loads), bump the worker startup timer with `TAPE6_WORKER_START_TIMEOUT=60000`. The default is 5 s.
86
+ 14. **Verify (browser):** start `npx tape6-server --trace`, then open `http://localhost:3000/?q=/tests/test-<name>.js` to run a specific file. Use multiple `?q=` parameters to run several files. Open `http://localhost:3000/` to run all configured tests.
87
+ 15. Report results and any failures.
package/src/State.js CHANGED
@@ -186,29 +186,34 @@ export class State {
186
186
  typeof event.diffTime !== 'number' && (event.diffTime = event.time - this.startTime);
187
187
  }
188
188
 
189
- if (
190
- event.type === 'assert' &&
191
- (event.operator === 'error' || event.operator === 'exception') &&
192
- typeof event.data?.actual?.stack == 'string'
193
- ) {
194
- event.stackList = getStackList(event.data.actual);
195
- event.at = event.stackList[0];
196
- }
189
+ // Stack walking is only needed when the event will be reported as a failure.
190
+ // Reporters (TapReporter, TTYReporter) only read event.at / event.stackList
191
+ // inside `if (event.fail)` blocks, so for passing assertions this is wasted work.
192
+ if (isFailed) {
193
+ if (
194
+ event.type === 'assert' &&
195
+ (event.operator === 'error' || event.operator === 'exception') &&
196
+ typeof event.data?.actual?.stack == 'string'
197
+ ) {
198
+ event.stackList = getStackList(event.data.actual);
199
+ event.at = event.stackList[0];
200
+ }
197
201
 
198
- if (event.type === 'assertion-error' && typeof event.data?.error?.stack == 'string') {
199
- event.stackList = getStackList(event.data.error);
200
- event.at = event.stackList[0];
201
- }
202
+ if (event.type === 'assertion-error' && typeof event.data?.error?.stack == 'string') {
203
+ event.stackList = getStackList(event.data.error);
204
+ event.at = event.stackList[0];
205
+ }
202
206
 
203
- if (!event.at && typeof event.marker?.stack == 'string') {
204
- event.stackList = getStackList(event.marker);
205
- event.at =
206
- event.stackList[
207
- Math.min(
208
- typeof event.markerIndex == 'number' ? event.markerIndex : 1,
209
- event.stackList.length
210
- )
211
- ];
207
+ if (!event.at && typeof event.marker?.stack == 'string') {
208
+ event.stackList = getStackList(event.marker);
209
+ event.at =
210
+ event.stackList[
211
+ Math.min(
212
+ typeof event.markerIndex == 'number' ? event.markerIndex : 1,
213
+ event.stackList.length
214
+ )
215
+ ];
216
+ }
212
217
  }
213
218
 
214
219
  if ((event.type === 'assert' || event.type === 'assertion-error') && event.data) {
package/src/Tester.js CHANGED
@@ -1,13 +1,30 @@
1
1
  import {equal, match, any} from './deep6/index.js';
2
2
  import {getTimer} from './utils/timer.js';
3
3
 
4
- const throwHelper = fn => {
4
+ const tryFn = fn => {
5
5
  try {
6
6
  fn();
7
+ return {threw: false};
7
8
  } catch (error) {
8
- return error;
9
+ return {threw: true, error};
9
10
  }
10
- return null;
11
+ };
12
+
13
+ const isErrorClass = fn =>
14
+ fn === Error || (typeof fn === 'function' && fn.prototype instanceof Error);
15
+
16
+ const applyMatcher = (actual, matcher) => {
17
+ if (matcher === undefined) return true;
18
+ if (typeof matcher === 'function') {
19
+ if (isErrorClass(matcher)) return actual instanceof matcher;
20
+ return !!matcher(actual);
21
+ }
22
+ if (matcher instanceof RegExp) {
23
+ const str = actual instanceof Error ? actual.message : String(actual);
24
+ return matcher.test(str);
25
+ }
26
+ if (matcher !== null && typeof matcher === 'object') return match(actual, matcher);
27
+ return actual === matcher;
11
28
  };
12
29
 
13
30
  export class Tester {
@@ -264,36 +281,41 @@ export class Tester {
264
281
  });
265
282
  }
266
283
 
267
- throws(fn, msg) {
284
+ throws(fn, matcher, msg) {
268
285
  if (typeof fn != 'function') throw new TypeError('the first argument should be a function');
269
- const result = throwHelper(fn);
286
+ if (typeof matcher === 'string' && msg === undefined) {
287
+ msg = matcher;
288
+ matcher = undefined;
289
+ }
290
+ const {threw, error} = tryFn(fn);
291
+ const matched = threw && applyMatcher(error, matcher);
270
292
  this.reporter.report({
271
293
  name: msg || 'should throw',
272
294
  test: this.testNumber,
273
295
  marker: new Error(),
274
296
  time: this.timer.now(),
275
297
  operator: 'throws',
276
- fail: !result,
298
+ fail: !matched,
277
299
  data: {
278
- expected: null,
279
- actual: result
300
+ expected: matcher === undefined ? null : matcher,
301
+ actual: threw ? error : null
280
302
  }
281
303
  });
282
304
  }
283
305
 
284
306
  doesNotThrow(fn, msg) {
285
307
  if (typeof fn != 'function') throw new TypeError('the first argument should be a function');
286
- const result = throwHelper(fn);
308
+ const {threw, error} = tryFn(fn);
287
309
  this.reporter.report({
288
310
  name: msg || 'should not throw',
289
311
  test: this.testNumber,
290
312
  marker: new Error(),
291
313
  time: this.timer.now(),
292
314
  operator: 'doesNotThrow',
293
- fail: !!result,
315
+ fail: threw,
294
316
  data: {
295
317
  expected: null,
296
- actual: result
318
+ actual: threw ? error : null
297
319
  }
298
320
  });
299
321
  }
@@ -325,7 +347,7 @@ export class Tester {
325
347
  test: this.testNumber,
326
348
  marker: new Error(),
327
349
  time: this.timer.now(),
328
- operator: 'doesNotMatch',
350
+ operator: 'doesNotMatchString',
329
351
  fail: regexp.test(string),
330
352
  data: {
331
353
  expected: regexp,
@@ -355,7 +377,7 @@ export class Tester {
355
377
  test: this.testNumber,
356
378
  marker: new Error(),
357
379
  time: this.timer.now(),
358
- operator: 'doesNotMatchObject',
380
+ operator: 'doesNotMatch',
359
381
  fail: match(a, b),
360
382
  data: {
361
383
  expected: b,
@@ -364,49 +386,59 @@ export class Tester {
364
386
  });
365
387
  }
366
388
 
367
- rejects(promise, msg) {
389
+ rejects(promise, matcher, msg) {
368
390
  if (!promise || typeof promise.then != 'function')
369
391
  throw new TypeError('the first argument should be a promise');
392
+ if (typeof matcher === 'string' && msg === undefined) {
393
+ msg = matcher;
394
+ matcher = undefined;
395
+ }
370
396
  return promise
371
397
  .then(
372
- () => null,
373
- error => error
398
+ () => ({resolved: true}),
399
+ error => ({resolved: false, error})
374
400
  )
375
- .then(result => {
401
+ .then(({resolved, error}) => {
402
+ const matched = !resolved && applyMatcher(error, matcher);
376
403
  this.reporter.report({
377
404
  name: msg || 'should be rejected',
378
405
  test: this.testNumber,
379
406
  marker: new Error(),
380
407
  time: this.timer.now(),
381
408
  operator: 'rejects',
382
- fail: !result,
409
+ fail: !matched,
383
410
  data: {
384
- expected: null,
385
- actual: result
411
+ expected: matcher === undefined ? null : matcher,
412
+ actual: resolved ? null : error
386
413
  }
387
414
  });
388
415
  });
389
416
  }
390
417
 
391
- resolves(promise, msg) {
418
+ resolves(promise, matcher, msg) {
392
419
  if (!promise || typeof promise.then != 'function')
393
420
  throw new TypeError('the first argument should be a promise');
421
+ if (typeof matcher === 'string' && msg === undefined) {
422
+ msg = matcher;
423
+ matcher = undefined;
424
+ }
394
425
  return promise
395
426
  .then(
396
- () => null,
397
- error => error
427
+ value => ({resolved: true, value}),
428
+ error => ({resolved: false, error})
398
429
  )
399
- .then(result => {
430
+ .then(({resolved, value, error}) => {
431
+ const matched = resolved && applyMatcher(value, matcher);
400
432
  this.reporter.report({
401
433
  name: msg || 'should not be rejected',
402
434
  test: this.testNumber,
403
435
  marker: new Error(),
404
436
  time: this.timer.now(),
405
437
  operator: 'resolves',
406
- fail: !!result,
438
+ fail: !matched,
407
439
  data: {
408
- expected: null,
409
- actual: result
440
+ expected: matcher === undefined ? null : matcher,
441
+ actual: resolved ? (matcher === undefined ? null : value) : error
410
442
  }
411
443
  });
412
444
  });
@@ -0,0 +1,174 @@
1
+ // Type definitions for deep6 environment
2
+ // Generated from src/env.js
3
+
4
+ /**
5
+ * Unification environment managing variable bindings and stack frames
6
+ *
7
+ * Maintains two parallel prototype-chain structures:
8
+ * - `variables`: maps variable names to their alias groups
9
+ * - `values`: maps variable names/aliases to their bound values
10
+ *
11
+ * Stack frames enable scoped bindings that can be reverted.
12
+ */
13
+ export declare class Env {
14
+ /** Map of variable names to their alias groups */
15
+ variables: Record<string | symbol, unknown>;
16
+ /** Map of variable names/aliases to their bound values */
17
+ values: Record<string | symbol, unknown>;
18
+ /** Current stack frame depth */
19
+ depth: number;
20
+
21
+ constructor();
22
+
23
+ /** Pushes a new stack frame for nested scoping */
24
+ push(): void;
25
+
26
+ /**
27
+ * Pops the current stack frame, reverting bindings
28
+ * @throws {Error} If stack is empty
29
+ */
30
+ pop(): void;
31
+
32
+ /**
33
+ * Reverts to a specific stack frame depth
34
+ * @param depth - Target depth to revert to
35
+ * @throws {Error} If depth is higher than current depth
36
+ */
37
+ revert(depth: number): void;
38
+
39
+ /**
40
+ * Creates an alias between two variable names
41
+ * @param name1 - First variable name
42
+ * @param name2 - Second variable name
43
+ */
44
+ bindVar(name1: string | symbol, name2: string | symbol): void;
45
+
46
+ /**
47
+ * Binds a variable name (and its aliases) to a value
48
+ * @param name - Variable name to bind
49
+ * @param val - Value to bind
50
+ */
51
+ bindVal(name: string | symbol, val: unknown): void;
52
+
53
+ /**
54
+ * Checks if a variable is bound to a value
55
+ * @param name - Variable name to check
56
+ * @returns True if variable has a bound value
57
+ */
58
+ isBound(name: string | symbol): boolean;
59
+
60
+ /**
61
+ * Checks if two variable names are aliases
62
+ * @param name1 - First variable name
63
+ * @param name2 - Second variable name
64
+ * @returns True if variables are aliases
65
+ */
66
+ isAlias(name1: string | symbol, name2: string | symbol): boolean;
67
+
68
+ /**
69
+ * Gets the value bound to a variable
70
+ * @param name - Variable name
71
+ * @returns The bound value, or undefined if unbound
72
+ */
73
+ get(name: string | symbol): unknown;
74
+
75
+ /**
76
+ * Returns all variable bindings (for debugging)
77
+ * @returns Array of name/value pairs
78
+ */
79
+ getAllValues(): Array<{name: string | symbol; value: unknown}>;
80
+ }
81
+
82
+ /**
83
+ * Base class for custom unification behavior
84
+ *
85
+ * Subclasses must implement the `unify` method.
86
+ */
87
+ export declare class Unifier {
88
+ /**
89
+ * Unifies this unifier with a value
90
+ * @param val - Value to unify with
91
+ * @param ls - Left argument stack
92
+ * @param rs - Right argument stack
93
+ * @param env - Unification environment
94
+ * @returns True if unification succeeds
95
+ */
96
+ unify(val: unknown, ls: unknown[], rs: unknown[], env: Env): boolean;
97
+ }
98
+
99
+ /**
100
+ * Type guard for Unifier instances
101
+ * @param x - Value to check
102
+ * @returns True if x is a Unifier
103
+ */
104
+ export declare const isUnifier: (x: unknown) => x is Unifier;
105
+
106
+ /**
107
+ * Wildcard symbol that matches any value during unification
108
+ */
109
+ export declare const any: unique symbol;
110
+
111
+ /** Alias for `any` */
112
+ export declare const _: typeof any;
113
+
114
+ /**
115
+ * Logical variable for capturing values during unification
116
+ *
117
+ * Variables can be bound to values, aliased to other variables,
118
+ * and dereferenced through an Env.
119
+ */
120
+ export declare class Variable extends Unifier {
121
+ /** Variable identifier */
122
+ name: string | symbol;
123
+
124
+ /**
125
+ * @param name - Optional identifier (defaults to a unique Symbol)
126
+ */
127
+ constructor(name?: string | symbol);
128
+
129
+ /**
130
+ * Checks if this variable is bound in the given environment
131
+ * @param env - Unification environment
132
+ * @returns True if bound
133
+ */
134
+ isBound(env: Env): boolean;
135
+
136
+ /**
137
+ * Checks if this variable is aliased to another
138
+ * @param name - Variable name, symbol, or Variable instance
139
+ * @param env - Unification environment
140
+ * @returns True if aliased
141
+ */
142
+ isAlias(name: Variable | string | symbol, env: Env): boolean;
143
+
144
+ /**
145
+ * Gets the bound value of this variable
146
+ * @param env - Unification environment
147
+ * @returns The bound value, or undefined if unbound
148
+ */
149
+ get(env: Env): unknown;
150
+
151
+ /**
152
+ * Unifies this variable with a value
153
+ * @param val - Value to unify with
154
+ * @param ls - Left argument stack
155
+ * @param rs - Right argument stack
156
+ * @param env - Unification environment
157
+ * @returns True if unification succeeds
158
+ */
159
+ unify(val: unknown, ls: unknown[], rs: unknown[], env: Env): boolean;
160
+ }
161
+
162
+ /**
163
+ * Type guard for Variable instances
164
+ * @param x - Value to check
165
+ * @returns True if x is a Variable
166
+ */
167
+ export declare const isVariable: (x: unknown) => x is Variable;
168
+
169
+ /**
170
+ * Creates a new Variable
171
+ * @param name - Optional identifier (defaults to a unique Symbol)
172
+ * @returns A new Variable instance
173
+ */
174
+ export declare const variable: (name?: string | symbol) => Variable;
package/src/deep6/env.js CHANGED
@@ -13,7 +13,7 @@ const collectSymbols = object => {
13
13
  };
14
14
 
15
15
  const ensure = (object, depth) => {
16
- while (object[keyDepth] > depth) object = object.getPrototypeOf(object);
16
+ while (object[keyDepth] > depth) object = Object.getPrototypeOf(object);
17
17
  if (object[keyDepth] < depth) {
18
18
  object = Object.create(object);
19
19
  object[keyDepth] = depth;
@@ -90,14 +90,14 @@ class Env {
90
90
  }
91
91
  // helpers
92
92
  isBound(name) {
93
- return name in env.values;
93
+ return name in this.values;
94
94
  }
95
95
  isAlias(name1, name2) {
96
- const u = env.variables[name2];
96
+ const u = this.variables[name2];
97
97
  return u && u[name1] === 1;
98
98
  }
99
99
  get(name) {
100
- return env.values[name];
100
+ return this.values[name];
101
101
  }
102
102
  // debugging
103
103
  getAllValues() {