@zudojs/testing 1.1.0 → 1.1.2

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
@@ -2,6 +2,12 @@
2
2
 
3
3
  Test helpers, fixtures, mocks, and assertions for Zudojs applications.
4
4
 
5
+ <!-- zudo-docs:start -->
6
+
7
+ **Documentation:** [zudojs.oyinlola.site/docs/packages-testing](https://zudojs.oyinlola.site/docs/packages-testing) · **For AI agents:** [Markdown version](https://zudojs.oyinlola.site/docs/packages-testing.md), [llms.txt](https://zudojs.oyinlola.site/llms.txt)
8
+
9
+ <!-- zudo-docs:end -->
10
+
5
11
  ## Installation
6
12
 
7
13
  ```bash
@@ -61,8 +67,20 @@ assertResponseBody(response, { id: "u_1", roles: new Set(["admin"]) });
61
67
  - Assertions compare structurally, not by `JSON.stringify`. `Map`, `Set`,
62
68
  `Date`, `BigInt`, `undefined` values and key order are all handled, and a
63
69
  circular value reports a mismatch instead of throwing a `TypeError`.
70
+ - Types are compared too: two objects must share a prototype (a class
71
+ instance never equals a plain object or an instance of another class;
72
+ `{}` and `Object.create(null)` count as the same), Errors must match on
73
+ `name`, `message` and `cause`, boxed primitives on their value, and typed
74
+ arrays on their constructor. Distinct Promises, WeakMaps and WeakSets are
75
+ never equal. A serializer that turns an Error into `{}` fails
76
+ `assertSerializesCorrectly`.
64
77
  - `createStub()` answers `then` with `undefined`, so awaiting a stub — or
65
78
  returning one from an async factory — resolves rather than hanging.
79
+ - `createStub()` hands back the same no-op for a given property every time,
80
+ so `stub.handler === stub.handler` and a register/unregister pair written
81
+ against a stub actually unregisters.
82
+ - `InMemoryTestStorage.set(key, value, 0)` means "already expired", not "no
83
+ expiry"; only an omitted TTL never expires.
66
84
  - `cleanup.dispose()` rejects with an `AggregateError` when any cleanup fails,
67
85
  after running them all.
68
86
  - `mockResolvedValue` and `mockRejectedValue` return promises; `results` stays
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @zudojs/testing — Map and Set comparison for the structural walk.
3
+ *
4
+ * `Map.has`/`Set.has` are identity-based, so collections holding equal but
5
+ * distinct objects would compare unequal. Each expected entry is matched
6
+ * against a still-unmatched actual entry: by identity when that entry is
7
+ * still unmatched, structurally otherwise. The identity shortcut only applies
8
+ * to entries not yet consumed; taking it for a consumed entry made
9
+ * `indexOf` return -1 and `splice(-1, 1)` drop an unrelated entry.
10
+ *
11
+ * @module assertions/deepEqual.collections
12
+ */
13
+ import type { Difference } from "./deepEqual.describe.js";
14
+ /** The recursive walker, passed in to avoid a module cycle. */
15
+ export type DiffWalker = (actual: unknown, expected: unknown, path: string, seen: Map<object, object>) => Difference | undefined;
16
+ /**
17
+ * Compares two Maps. A key matched structurally prefers the actual key whose
18
+ * value also matches, and falls back to a key-only match so the value
19
+ * difference is what gets reported.
20
+ */
21
+ export declare function diffMap(actual: Map<unknown, unknown>, expected: Map<unknown, unknown>, path: string, seen: Map<object, object>, walk: DiffWalker): Difference | undefined;
22
+ /** Compares two Sets, matching each expected item to one unmatched actual item. */
23
+ export declare function diffSet(actual: Set<unknown>, expected: Set<unknown>, path: string, seen: Map<object, object>, walk: DiffWalker): Difference | undefined;
24
+ //# sourceMappingURL=deepEqual.collections.d.ts.map
@@ -0,0 +1,81 @@
1
+ /**
2
+ * @zudojs/testing — Map and Set comparison for the structural walk.
3
+ *
4
+ * `Map.has`/`Set.has` are identity-based, so collections holding equal but
5
+ * distinct objects would compare unequal. Each expected entry is matched
6
+ * against a still-unmatched actual entry: by identity when that entry is
7
+ * still unmatched, structurally otherwise. The identity shortcut only applies
8
+ * to entries not yet consumed; taking it for a consumed entry made
9
+ * `indexOf` return -1 and `splice(-1, 1)` drop an unrelated entry.
10
+ *
11
+ * @module assertions/deepEqual.collections
12
+ */
13
+ import { describeValue } from "./deepEqual.describe.js";
14
+ const NO_MATCH = Symbol("deepEqual.noMatch");
15
+ /**
16
+ * Finds a candidate structurally equal to `expected`. Each probe walks a
17
+ * copy of `seen`: the cycle guard records a pair as "assumed equal" before
18
+ * comparing it, and a probe that fails must not leave that assumption
19
+ * behind for the next candidate.
20
+ */
21
+ function findStructuralMatch(walk, candidates, expected, seen, accept = () => true) {
22
+ for (const candidate of candidates) {
23
+ if (walk(candidate, expected, "", new Map(seen)) === undefined &&
24
+ accept(candidate)) {
25
+ return candidate;
26
+ }
27
+ }
28
+ return NO_MATCH;
29
+ }
30
+ /**
31
+ * Compares two Maps. A key matched structurally prefers the actual key whose
32
+ * value also matches, and falls back to a key-only match so the value
33
+ * difference is what gets reported.
34
+ */
35
+ export function diffMap(actual, expected, path, seen, walk) {
36
+ if (actual.size !== expected.size) {
37
+ return {
38
+ path,
39
+ reason: `expected ${expected.size} entries, received ${actual.size}`,
40
+ };
41
+ }
42
+ const unmatched = [...actual.keys()];
43
+ for (const [key, value] of expected) {
44
+ let actualKey = key;
45
+ if (!unmatched.includes(key)) {
46
+ actualKey = findStructuralMatch(walk, unmatched, key, seen, (candidate) => walk(actual.get(candidate), value, "", new Map(seen)) === undefined);
47
+ if (actualKey === NO_MATCH) {
48
+ actualKey = findStructuralMatch(walk, unmatched, key, seen);
49
+ }
50
+ }
51
+ if (actualKey === NO_MATCH) {
52
+ return { path, reason: `missing key ${describeValue(key)}` };
53
+ }
54
+ unmatched.splice(unmatched.indexOf(actualKey), 1);
55
+ const found = walk(actual.get(actualKey), value, `${path}[${describeValue(key)}]`, seen);
56
+ if (found)
57
+ return found;
58
+ }
59
+ return undefined;
60
+ }
61
+ /** Compares two Sets, matching each expected item to one unmatched actual item. */
62
+ export function diffSet(actual, expected, path, seen, walk) {
63
+ if (actual.size !== expected.size) {
64
+ return {
65
+ path,
66
+ reason: `expected ${expected.size} items, received ${actual.size}`,
67
+ };
68
+ }
69
+ const unmatched = [...actual];
70
+ for (const entry of expected) {
71
+ const match = unmatched.includes(entry)
72
+ ? entry
73
+ : findStructuralMatch(walk, unmatched, entry, seen);
74
+ if (match === NO_MATCH) {
75
+ return { path, reason: `missing item ${describeValue(entry)}` };
76
+ }
77
+ unmatched.splice(unmatched.indexOf(match), 1);
78
+ }
79
+ return undefined;
80
+ }
81
+ //# sourceMappingURL=deepEqual.collections.js.map
@@ -11,6 +11,8 @@
11
11
  * @module assertions/deepEqual
12
12
  */
13
13
  import { describeValue } from "./deepEqual.describe.js";
14
+ import { diffMap, diffSet } from "./deepEqual.collections.js";
15
+ import { compareIdentity, errorCauses } from "./deepEqual.identity.js";
14
16
  /** The constructor-level kind of a value, used to reject cross-type matches. */
15
17
  function kindOf(value) {
16
18
  if (value === null)
@@ -60,12 +62,21 @@ function diff(actual, expected, path, seen) {
60
62
  reason: `expected ${describeValue(expected)}, received ${describeValue(actual)}`,
61
63
  };
62
64
  }
65
+ const mismatch = compareIdentity(actual, expected);
66
+ if (mismatch)
67
+ return { path, reason: mismatch };
63
68
  // Guard against cycles: a pair already being compared is assumed equal
64
69
  // until proven otherwise elsewhere in the walk.
65
70
  const previous = seen.get(actual);
66
71
  if (previous === expected)
67
72
  return undefined;
68
73
  seen.set(actual, expected);
74
+ const causes = errorCauses(actual, expected);
75
+ if (causes) {
76
+ const found = diff(causes.actual, causes.expected, `${path}.cause`, seen);
77
+ if (found)
78
+ return found;
79
+ }
69
80
  if (Array.isArray(actual) && Array.isArray(expected)) {
70
81
  if (actual.length !== expected.length) {
71
82
  return {
@@ -81,50 +92,10 @@ function diff(actual, expected, path, seen) {
81
92
  return undefined;
82
93
  }
83
94
  if (actual instanceof Map && expected instanceof Map) {
84
- if (actual.size !== expected.size) {
85
- return {
86
- path,
87
- reason: `expected ${expected.size} entries, received ${actual.size}`,
88
- };
89
- }
90
- // Keys are matched by identity first and structurally second, so a
91
- // Map keyed by objects compares by value like everything else here.
92
- const unmatched = [...actual.keys()];
93
- for (const [key, value] of expected) {
94
- const actualKey = actual.has(key)
95
- ? key
96
- : findStructuralMatch(unmatched, key, seen);
97
- if (actualKey === NO_MATCH) {
98
- return { path, reason: `missing key ${describeValue(key)}` };
99
- }
100
- unmatched.splice(unmatched.indexOf(actualKey), 1);
101
- const found = diff(actual.get(actualKey), value, `${path}[${describeValue(key)}]`, seen);
102
- if (found)
103
- return found;
104
- }
105
- return undefined;
95
+ return diffMap(actual, expected, path, seen, diff);
106
96
  }
107
97
  if (actual instanceof Set && expected instanceof Set) {
108
- if (actual.size !== expected.size) {
109
- return {
110
- path,
111
- reason: `expected ${expected.size} items, received ${actual.size}`,
112
- };
113
- }
114
- // `Set.has` is identity-based, so two Sets holding equal but distinct
115
- // objects compared unequal. Each expected entry is matched against a
116
- // still-unmatched actual entry structurally instead.
117
- const unmatched = [...actual];
118
- for (const entry of expected) {
119
- const match = actual.has(entry)
120
- ? entry
121
- : findStructuralMatch(unmatched, entry, seen);
122
- if (match === NO_MATCH) {
123
- return { path, reason: `missing item ${describeValue(entry)}` };
124
- }
125
- unmatched.splice(unmatched.indexOf(match), 1);
126
- }
127
- return undefined;
98
+ return diffSet(actual, expected, path, seen, diff);
128
99
  }
129
100
  if (ArrayBuffer.isView(actual) && ArrayBuffer.isView(expected)) {
130
101
  const a = new Uint8Array(actual.buffer, actual.byteOffset, actual.byteLength);
@@ -161,21 +132,6 @@ function diff(actual, expected, path, seen) {
161
132
  }
162
133
  return undefined;
163
134
  }
164
- const NO_MATCH = Symbol("deepEqual.noMatch");
165
- /**
166
- * Finds a candidate structurally equal to `expected`. Each probe walks a
167
- * copy of `seen`: the cycle guard records a pair as "assumed equal" before
168
- * comparing it, and a probe that fails must not leave that assumption
169
- * behind for the next candidate.
170
- */
171
- function findStructuralMatch(candidates, expected, seen) {
172
- for (const candidate of candidates) {
173
- if (diff(candidate, expected, "", new Map(seen)) === undefined) {
174
- return candidate;
175
- }
176
- }
177
- return NO_MATCH;
178
- }
179
135
  /**
180
136
  * Find the first structural difference between two values.
181
137
  *
@@ -0,0 +1,33 @@
1
+ /**
2
+ * @zudojs/testing — Identity checks that run before the structural walk.
3
+ *
4
+ * The structural walk compares own enumerable keys. That alone treats every
5
+ * Error as `{}` (`message`, `name` and `stack` are not enumerable), a class
6
+ * instance as a plain object, `new Number(1)` as `new Number(2)`, and a
7
+ * `Uint8Array` as any typed array with the same bytes. These checks reject
8
+ * those pairs first, so an assertion built on `deepEqual` can fail when the
9
+ * value lost its type or its payload.
10
+ *
11
+ * @module assertions/deepEqual.identity
12
+ */
13
+ /** A mismatch reason, or undefined when the pair passes this check. */
14
+ export type IdentityMismatch = string | undefined;
15
+ /**
16
+ * Compares the parts of two objects that are invisible to a key walk:
17
+ * prototype, boxed value, Error name/message/cause, typed-array constructor,
18
+ * and the unobservable contents of Promises and weak collections.
19
+ *
20
+ * @param actual - The observed object.
21
+ * @param expected - The object it should equal.
22
+ * @returns A mismatch reason, or undefined when the pair may be walked.
23
+ */
24
+ export declare function compareIdentity(actual: object, expected: object): IdentityMismatch;
25
+ /**
26
+ * The Error `cause`, when either side has one. `cause` is an own
27
+ * non-enumerable property, so the key walk never reaches it.
28
+ */
29
+ export declare function errorCauses(actual: object, expected: object): {
30
+ readonly actual: unknown;
31
+ readonly expected: unknown;
32
+ } | undefined;
33
+ //# sourceMappingURL=deepEqual.identity.d.ts.map
@@ -0,0 +1,101 @@
1
+ /**
2
+ * @zudojs/testing — Identity checks that run before the structural walk.
3
+ *
4
+ * The structural walk compares own enumerable keys. That alone treats every
5
+ * Error as `{}` (`message`, `name` and `stack` are not enumerable), a class
6
+ * instance as a plain object, `new Number(1)` as `new Number(2)`, and a
7
+ * `Uint8Array` as any typed array with the same bytes. These checks reject
8
+ * those pairs first, so an assertion built on `deepEqual` can fail when the
9
+ * value lost its type or its payload.
10
+ *
11
+ * @module assertions/deepEqual.identity
12
+ */
13
+ import { describeValue } from "./deepEqual.describe.js";
14
+ /** Whether a prototype marks a plain record (`{}` or `Object.create(null)`). */
15
+ function isPlainPrototype(proto) {
16
+ return proto === null || proto === Object.prototype;
17
+ }
18
+ /** A readable name for the prototype of a value. */
19
+ function prototypeName(value) {
20
+ const proto = Object.getPrototypeOf(value);
21
+ if (proto === null)
22
+ return "null-prototype object";
23
+ const ctor = proto.constructor;
24
+ return ctor?.name ? ctor.name : "anonymous prototype";
25
+ }
26
+ /**
27
+ * Boxed primitives (`new Number(1)`, `Object(1n)`) carry their value in an
28
+ * internal slot, not in an own key. Returns the unboxed value, or a sentinel
29
+ * when `value` is not boxed.
30
+ */
31
+ function unbox(value) {
32
+ const tag = Object.prototype.toString.call(value);
33
+ switch (tag) {
34
+ case "[object Number]":
35
+ case "[object String]":
36
+ case "[object Boolean]":
37
+ case "[object BigInt]":
38
+ case "[object Symbol]":
39
+ return {
40
+ boxed: true,
41
+ value: value.valueOf(),
42
+ };
43
+ default:
44
+ return { boxed: false };
45
+ }
46
+ }
47
+ /** Values whose contents cannot be observed, so only identity can make them equal. */
48
+ function isOpaque(value) {
49
+ return (value instanceof Promise ||
50
+ value instanceof WeakMap ||
51
+ value instanceof WeakSet ||
52
+ (typeof WeakRef !== "undefined" && value instanceof WeakRef));
53
+ }
54
+ /**
55
+ * Compares the parts of two objects that are invisible to a key walk:
56
+ * prototype, boxed value, Error name/message/cause, typed-array constructor,
57
+ * and the unobservable contents of Promises and weak collections.
58
+ *
59
+ * @param actual - The observed object.
60
+ * @param expected - The object it should equal.
61
+ * @returns A mismatch reason, or undefined when the pair may be walked.
62
+ */
63
+ export function compareIdentity(actual, expected) {
64
+ const actualProto = Object.getPrototypeOf(actual);
65
+ const expectedProto = Object.getPrototypeOf(expected);
66
+ const bothPlain = isPlainPrototype(actualProto) && isPlainPrototype(expectedProto);
67
+ if (!bothPlain && actualProto !== expectedProto) {
68
+ return `expected instance of ${prototypeName(expected)}, received ${prototypeName(actual)}`;
69
+ }
70
+ if (isOpaque(expected)) {
71
+ return `expected the same ${prototypeName(expected)} instance, received a different one`;
72
+ }
73
+ const actualBox = unbox(actual);
74
+ const expectedBox = unbox(expected);
75
+ if (actualBox.boxed || expectedBox.boxed) {
76
+ if (!Object.is(actualBox.value, expectedBox.value)) {
77
+ return `expected boxed ${describeValue(expectedBox.value)}, received boxed ${describeValue(actualBox.value)}`;
78
+ }
79
+ }
80
+ if (actual instanceof Error && expected instanceof Error) {
81
+ if (actual.name !== expected.name) {
82
+ return `expected error name ${describeValue(expected.name)}, received ${describeValue(actual.name)}`;
83
+ }
84
+ if (actual.message !== expected.message) {
85
+ return `expected error message ${describeValue(expected.message)}, received ${describeValue(actual.message)}`;
86
+ }
87
+ }
88
+ return undefined;
89
+ }
90
+ /**
91
+ * The Error `cause`, when either side has one. `cause` is an own
92
+ * non-enumerable property, so the key walk never reaches it.
93
+ */
94
+ export function errorCauses(actual, expected) {
95
+ if (!(actual instanceof Error) || !(expected instanceof Error))
96
+ return undefined;
97
+ if (!("cause" in actual) && !("cause" in expected))
98
+ return undefined;
99
+ return { actual: actual.cause, expected: expected.cause };
100
+ }
101
+ //# sourceMappingURL=deepEqual.identity.js.map
@@ -0,0 +1,13 @@
1
+ /**
2
+ * @zudojs/testing — Structural equality barrel.
3
+ *
4
+ * `deepEqual`/`findDifference` compare values structurally, including Maps,
5
+ * Sets, typed arrays, Errors, boxed primitives and class instances (by
6
+ * prototype). `describeValue` renders values for assertion messages.
7
+ *
8
+ * @module assertions/deepEqual
9
+ */
10
+ export { deepEqual, findDifference } from "./deepEqual.core.js";
11
+ export { describeValue } from "./deepEqual.describe.js";
12
+ export type { Difference } from "./deepEqual.describe.js";
13
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @zudojs/testing — Structural equality barrel.
3
+ *
4
+ * `deepEqual`/`findDifference` compare values structurally, including Maps,
5
+ * Sets, typed arrays, Errors, boxed primitives and class instances (by
6
+ * prototype). `describeValue` renders values for assertion messages.
7
+ *
8
+ * @module assertions/deepEqual
9
+ */
10
+ export { deepEqual, findDifference } from "./deepEqual.core.js";
11
+ export { describeValue } from "./deepEqual.describe.js";
12
+ //# sourceMappingURL=index.js.map
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * Assert errors, error types, and error messages.
5
5
  */
6
- import { findDifference } from "./deepEqual.core.js";
7
- import { describeValue } from "./deepEqual.describe.js";
6
+ import { findDifference } from "./deepEqual/deepEqual.core.js";
7
+ import { describeValue } from "./deepEqual/deepEqual.describe.js";
8
8
  /**
9
9
  * Asserts that a function throws an error.
10
10
  *
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Assert events, event types, and event payloads.
5
5
  */
6
- import { findDifference } from "./deepEqual.core.js";
6
+ import { findDifference } from "./deepEqual/deepEqual.core.js";
7
7
  /**
8
8
  * Asserts that an event has a specific type.
9
9
  *
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * Assert HTTP responses, status codes, headers, and bodies.
5
5
  */
6
- import { findDifference } from "./deepEqual.core.js";
7
- import { describeValue } from "./deepEqual.describe.js";
6
+ import { findDifference } from "./deepEqual/deepEqual.core.js";
7
+ import { describeValue } from "./deepEqual/deepEqual.describe.js";
8
8
  /**
9
9
  * Asserts that a response has a specific status code.
10
10
  *
@@ -4,7 +4,6 @@
4
4
  export { assertBadRequest, assertCreated, assertNoContent, assertNotFound, assertOK, assertResponseBody, assertResponseBodyContains, assertResponseHeader, assertResponseStatus, assertServerError, } from "./httpAssertions.core.js";
5
5
  export { assertEventNotPublished, assertEventPayload, assertEventPublished, assertEventType, assertMessageDispatched, assertMessageNotDispatched, assertRecordedEventType, } from "./eventAssertions.core.js";
6
6
  export { assertErrorCode, assertErrorMetadata, assertErrorType, assertRejects, assertThrows, } from "./errorAssertions.core.js";
7
- export { deepEqual, findDifference } from "./deepEqual.core.js";
8
- export { describeValue } from "./deepEqual.describe.js";
9
- export type { Difference } from "./deepEqual.describe.js";
7
+ export { deepEqual, describeValue, findDifference } from "./deepEqual/index.js";
8
+ export type { Difference } from "./deepEqual/index.js";
10
9
  //# sourceMappingURL=index.d.ts.map
@@ -4,6 +4,5 @@
4
4
  export { assertBadRequest, assertCreated, assertNoContent, assertNotFound, assertOK, assertResponseBody, assertResponseBodyContains, assertResponseHeader, assertResponseStatus, assertServerError, } from "./httpAssertions.core.js";
5
5
  export { assertEventNotPublished, assertEventPayload, assertEventPublished, assertEventType, assertMessageDispatched, assertMessageNotDispatched, assertRecordedEventType, } from "./eventAssertions.core.js";
6
6
  export { assertErrorCode, assertErrorMetadata, assertErrorType, assertRejects, assertThrows, } from "./errorAssertions.core.js";
7
- export { deepEqual, findDifference } from "./deepEqual.core.js";
8
- export { describeValue } from "./deepEqual.describe.js";
7
+ export { deepEqual, describeValue, findDifference } from "./deepEqual/index.js";
9
8
  //# sourceMappingURL=index.js.map
@@ -44,6 +44,10 @@ const NON_CALLABLE_KEYS = new Set([
44
44
  * ```
45
45
  */
46
46
  export function createStub(overrides = {}) {
47
+ // One no-op per property key, kept for the life of the stub. Handing out a
48
+ // fresh closure per access would make `stub.handler !== stub.handler`, so a
49
+ // register/unregister pair written against a stub could never match.
50
+ const noops = new Map();
47
51
  return new Proxy({}, {
48
52
  get(_target, prop, _receiver) {
49
53
  // `hasOwn`, not `in`: `in` walks the prototype chain, so `toString`,
@@ -55,7 +59,12 @@ export function createStub(overrides = {}) {
55
59
  return undefined;
56
60
  if (typeof prop === "symbol")
57
61
  return undefined;
58
- return (..._args) => undefined;
62
+ const existing = noops.get(prop);
63
+ if (existing)
64
+ return existing;
65
+ const noop = (..._args) => undefined;
66
+ noops.set(prop, noop);
67
+ return noop;
59
68
  },
60
69
  has(_target, prop) {
61
70
  return !NON_CALLABLE_KEYS.has(prop);
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * @module spyLogger/spyLogger.context
5
5
  */
6
- import { deepEqual } from "../assertions/deepEqual.core.js";
6
+ import { deepEqual } from "../assertions/deepEqual/deepEqual.core.js";
7
7
  /** Merges two logger contexts, preserving the nested shape. */
8
8
  export function mergeLoggerContext(base, next) {
9
9
  if (!base)
@@ -4,8 +4,8 @@
4
4
  * Helpers for testing serialization round-trips and type preservation.
5
5
  */
6
6
  import { JSONSerializer } from "@zudojs/serialization";
7
- import { findDifference } from "../assertions/deepEqual.core.js";
8
- import { describeValue } from "../assertions/deepEqual.describe.js";
7
+ import { findDifference } from "../assertions/deepEqual/deepEqual.core.js";
8
+ import { describeValue } from "../assertions/deepEqual/deepEqual.describe.js";
9
9
  const defaultSerializer = new JSONSerializer();
10
10
  /**
11
11
  * Assert that a value round-trips through serialization correctly.
@@ -22,7 +22,13 @@ export declare class InMemoryTestStorage {
22
22
  * test needs to distinguish.
23
23
  */
24
24
  get<T = unknown>(key: string): T | null;
25
- /** Set a value with optional TTL in milliseconds. */
25
+ /**
26
+ * Set a value with optional TTL in milliseconds.
27
+ *
28
+ * Only an omitted (or `undefined`) TTL means "never expires". A TTL of `0`
29
+ * is a real deadline of now, so the entry is already expired on the next
30
+ * read — a cache test writing `0` to mean "already stale" gets that.
31
+ */
26
32
  set(key: string, value: unknown, ttlMs?: number): void;
27
33
  /** Delete a value by key. Returns true if deleted. */
28
34
  delete(key: string): boolean;
@@ -36,9 +36,15 @@ export class InMemoryTestStorage {
36
36
  return null;
37
37
  return (entry.value ?? null);
38
38
  }
39
- /** Set a value with optional TTL in milliseconds. */
39
+ /**
40
+ * Set a value with optional TTL in milliseconds.
41
+ *
42
+ * Only an omitted (or `undefined`) TTL means "never expires". A TTL of `0`
43
+ * is a real deadline of now, so the entry is already expired on the next
44
+ * read — a cache test writing `0` to mean "already stale" gets that.
45
+ */
40
46
  set(key, value, ttlMs) {
41
- const expiresAt = ttlMs ? new Date(Date.now() + ttlMs) : null;
47
+ const expiresAt = ttlMs === undefined ? null : new Date(Date.now() + ttlMs);
42
48
  this.store.set(key, { value, expiresAt });
43
49
  }
44
50
  /** Delete a value by key. Returns true if deleted. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zudojs/testing",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "Test helpers, fixtures, mocks, and utilities for testing Zudojs applications.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -19,21 +19,20 @@
19
19
  "!dist/.tsbuildinfo"
20
20
  ],
21
21
  "dependencies": {
22
- "@zudojs/config": "1.0.1",
23
- "@zudojs/constants": "1.0.1",
24
- "@zudojs/container": "1.1.0",
25
- "@zudojs/errors": "1.0.1",
26
- "@zudojs/events": "1.0.1",
27
- "@zudojs/http": "1.1.0",
28
- "@zudojs/logger": "1.1.0",
29
- "@zudojs/messaging": "1.0.1",
30
- "@zudojs/middleware": "1.0.1",
31
- "@zudojs/queue": "1.1.0",
32
- "@zudojs/security": "1.0.1",
33
- "@zudojs/serialization": "1.0.1",
34
- "@zudojs/storage": "1.1.0",
35
- "@zudojs/types": "1.0.0",
36
- "@zudojs/validation": "1.0.1"
22
+ "@zudojs/config": "1.2.0",
23
+ "@zudojs/constants": "1.1.1",
24
+ "@zudojs/container": "1.1.2",
25
+ "@zudojs/errors": "1.2.0",
26
+ "@zudojs/events": "1.2.0",
27
+ "@zudojs/http": "1.3.0",
28
+ "@zudojs/logger": "1.3.0",
29
+ "@zudojs/messaging": "1.1.0",
30
+ "@zudojs/middleware": "1.0.3",
31
+ "@zudojs/queue": "1.3.0",
32
+ "@zudojs/security": "1.2.0",
33
+ "@zudojs/serialization": "1.1.1",
34
+ "@zudojs/storage": "1.1.2",
35
+ "@zudojs/types": "1.1.1"
37
36
  },
38
37
  "devDependencies": {
39
38
  "typescript": "7.0.2",