@specific.dev/spectest 0.38.0 → 0.39.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.
@@ -6,7 +6,7 @@ import { readFile, unlink } from "node:fs/promises";
6
6
  import { AppsV1Api, BatchV1Api, CoreV1Api, KubeConfig, KubernetesObjectApi, PatchStrategy, ResponseContext, ServerConfiguration, createConfiguration, loadAllYaml, } from "@kubernetes/client-node";
7
7
  import { Observable } from "@kubernetes/client-node/dist/gen/rxjsStub.js";
8
8
  import { dnsName, provides, SELF_SERVICE_TOKEN } from "../index.js";
9
- import { readRaw, readTag, wrap } from "../inspect.js";
9
+ import { deepUnwrap, readTag, wrap } from "../inspect.js";
10
10
  import { recorderAnnotate, recorderRemove } from "../recorder.js";
11
11
  /** Port the in-cluster registry listens on (plain HTTP). */
12
12
  const K3S_REGISTRY_PORT = 5000;
@@ -345,29 +345,6 @@ async function doFetch(request) {
345
345
  binary: async () => buf,
346
346
  });
347
347
  }
348
- /**
349
- * Recursively strip the inspector's carrier/proxy wrappers from a
350
- * value. Needed for arguments flowing into the kubernetes/client-node
351
- * API methods — if a wrapped pod's `metadata.name` (a primitive-carrier
352
- * object) reaches a URL template, the lib stringifies it to
353
- * `"[object Object]"` and the request 404s.
354
- */
355
- function deepUnwrap(value) {
356
- if (value === null || value === undefined)
357
- return value;
358
- const raw = readRaw(value);
359
- if (raw !== value)
360
- return deepUnwrap(raw);
361
- if (typeof value !== "object")
362
- return value;
363
- if (Array.isArray(value))
364
- return value.map(deepUnwrap);
365
- const out = {};
366
- for (const [k, v] of Object.entries(value)) {
367
- out[k] = deepUnwrap(v);
368
- }
369
- return out;
370
- }
371
348
  /**
372
349
  * Wrap a `@kubernetes/client-node` Api instance so each method call
373
350
  * runs in its own AsyncLocalStorage slot — `doFetch` writes the HTTP
package/dist/daemon.js CHANGED
@@ -27,7 +27,7 @@ import { acquirePersistentBrowser } from "./browser.js";
27
27
  import { isMobileApp, openPersistentMobile } from "./mobile.js";
28
28
  import { openTerminal } from "./terminal.js";
29
29
  import { recordEnv, recordExec, recordFake, recordHttp, recordTerminal, recordWait, reserveEvent, recorderEventCount, recorderMarkChildren, recorderTruncate, startRecording, stopRecording, truncateUtf8, } from "./recorder.js";
30
- import { wrap, wrapResponse } from "./inspect.js";
30
+ import { deepUnwrap, wrap, wrapResponse } from "./inspect.js";
31
31
  import { clearRecordSecrets, setRecordSecrets } from "./record-secrets.js";
32
32
  import { encodeReplayBundle, replayChunk } from "./replay-bundle.js";
33
33
  function namedServices(cfg) {
@@ -2528,8 +2528,9 @@ async function ensureFakeHelpers(name) {
2528
2528
  fake.trackedHelpers = trackFakeHelpers(name, fake.helpers);
2529
2529
  return fake.trackedHelpers;
2530
2530
  }
2531
- /** Wrap a fake's helpers so each call becomes a recorded `fake` event
2532
- * and its return value is `wrap()`ped for assertion provenance. Helpers
2531
+ /** Wrap a fake's helpers so each call becomes a recorded `fake` event,
2532
+ * its arguments are `deepUnwrap`ped, and its return value is `wrap()`ped
2533
+ * for assertion provenance. Helpers
2533
2534
  * are functions that read/mutate the fake's private state via closure;
2534
2535
  * tests only ever see what those functions return. The proxy is built
2535
2536
  * once and shared across tests; it consults the recorder at call time,
@@ -2556,10 +2557,21 @@ function trackFakeHelpers(fakeName, helpers) {
2556
2557
  }
2557
2558
  /** Invoke a fake helper function, recording a `fake` event and wrapping
2558
2559
  * the return value. Handles both sync and async helpers, and records an
2559
- * error event (then rethrows) if the helper throws. */
2560
- function invokeFakeHelper(fakeName, member, fn, thisArg, args) {
2560
+ * error event (then rethrows) if the helper throws.
2561
+ *
2562
+ * Arguments are `deepUnwrap`ped on the way in, so a helper body is plain
2563
+ * user code operating on plain values: a test that feeds one helper's
2564
+ * return (or a `ctx.fetch` field) to another passes a provenance wrapper,
2565
+ * and `msg.html.matchAll(...)` inside the fake would otherwise throw a
2566
+ * `TypeError` from a stack the author can't see. Same treatment the k8s
2567
+ * client's arguments get (`components/k3s.ts::withTagging`). The types
2568
+ * still say the value is a handle — passing a wrapped value where the
2569
+ * parameter type is known is a compile error asking for `.unwrap()`; this
2570
+ * catches the case where it isn't known (an `any` off `req.json()`). */
2571
+ function invokeFakeHelper(fakeName, member, fn, thisArg, callArgs) {
2561
2572
  const t = Date.now();
2562
2573
  const resv = reserveEvent();
2574
+ const args = callArgs.map((a) => deepUnwrap(a));
2563
2575
  const safeArgs = args.map((a) => safeSerialize(a));
2564
2576
  const recordResult = (value) => {
2565
2577
  const seq = recordFake({
package/dist/inspect.d.ts CHANGED
@@ -22,6 +22,29 @@ export declare function clearPendingNullish(): void;
22
22
  export declare function adoptNullishTag<T>(value: T): T;
23
23
  /** If x is wrapped, return the raw value; otherwise return x. */
24
24
  export declare function readRaw<T>(x: T): T;
25
+ /**
26
+ * Strip provenance wrappers from a value AND from anything nested inside a
27
+ * plain object/array it holds. The inbound counterpart of {@link wrap}: for
28
+ * values crossing *back* into code that expects raw data — a fake's `helpers`,
29
+ * a k8s client method's arguments. Without it a helper that takes a value some
30
+ * earlier op produced gets a `Carrier`/proxy, and the first `.matchAll(...)` /
31
+ * typed-client call inside it throws a `TypeError` from deep in the callee,
32
+ * far from the call site that passed the wrapper. Coercion sinks
33
+ * ({@link makeCarrier}) rescue only the interpolation cases; a callee that
34
+ * calls a method on the value, or checks its `typeof`, still sees an object.
35
+ *
36
+ * One {@link readRaw} finishes a wrapped value: a wrapper's target is raw all
37
+ * the way down (the proxy wraps children lazily, on read), so there is nothing
38
+ * left to walk underneath it. The recursion is for the other shape — a
39
+ * container the *caller* built around wrapped leaves: `{ to: msg.to }`,
40
+ * `[row.id, row.email]`.
41
+ *
42
+ * Rebuilds only what changed, and only plain objects/arrays: a `Date`,
43
+ * `Buffer`, `Map`, `Response` or class instance is returned untouched rather
44
+ * than flattened into a plain object. Cyclic containers are left in place at
45
+ * the point the cycle closes.
46
+ */
47
+ export declare function deepUnwrap<T>(value: T): T;
25
48
  /** The raw type behind a wrapper: a `Carrier`/`WrappedObject`/`WrappedArray`/
26
49
  * `WrappedResponse` resolves to its `unwrap()` return type; anything else is
27
50
  * already raw and passes through unchanged. */
package/dist/inspect.js CHANGED
@@ -136,6 +136,71 @@ export function readRaw(x) {
136
136
  }
137
137
  return cur;
138
138
  }
139
+ /**
140
+ * Strip provenance wrappers from a value AND from anything nested inside a
141
+ * plain object/array it holds. The inbound counterpart of {@link wrap}: for
142
+ * values crossing *back* into code that expects raw data — a fake's `helpers`,
143
+ * a k8s client method's arguments. Without it a helper that takes a value some
144
+ * earlier op produced gets a `Carrier`/proxy, and the first `.matchAll(...)` /
145
+ * typed-client call inside it throws a `TypeError` from deep in the callee,
146
+ * far from the call site that passed the wrapper. Coercion sinks
147
+ * ({@link makeCarrier}) rescue only the interpolation cases; a callee that
148
+ * calls a method on the value, or checks its `typeof`, still sees an object.
149
+ *
150
+ * One {@link readRaw} finishes a wrapped value: a wrapper's target is raw all
151
+ * the way down (the proxy wraps children lazily, on read), so there is nothing
152
+ * left to walk underneath it. The recursion is for the other shape — a
153
+ * container the *caller* built around wrapped leaves: `{ to: msg.to }`,
154
+ * `[row.id, row.email]`.
155
+ *
156
+ * Rebuilds only what changed, and only plain objects/arrays: a `Date`,
157
+ * `Buffer`, `Map`, `Response` or class instance is returned untouched rather
158
+ * than flattened into a plain object. Cyclic containers are left in place at
159
+ * the point the cycle closes.
160
+ */
161
+ export function deepUnwrap(value) {
162
+ return deepUnwrapInner(value, new Set());
163
+ }
164
+ function deepUnwrapInner(value, seen) {
165
+ if (value === null || value === undefined)
166
+ return value;
167
+ const raw = readRaw(value);
168
+ // It was wrapped, so it's now fully raw — and recursing into it would be
169
+ // wrong anyway (the raw form may be an exotic object we must not rebuild).
170
+ if (raw !== value)
171
+ return raw;
172
+ if (typeof raw !== "object")
173
+ return raw;
174
+ const obj = raw;
175
+ if (seen.has(obj))
176
+ return obj;
177
+ if (Array.isArray(obj)) {
178
+ seen.add(obj);
179
+ let changed = false;
180
+ const out = obj.map((el) => {
181
+ const next = deepUnwrapInner(el, seen);
182
+ if (next !== el)
183
+ changed = true;
184
+ return next;
185
+ });
186
+ seen.delete(obj);
187
+ return changed ? out : obj;
188
+ }
189
+ const proto = Object.getPrototypeOf(obj);
190
+ if (proto !== Object.prototype && proto !== null)
191
+ return obj;
192
+ seen.add(obj);
193
+ let changed = false;
194
+ const out = {};
195
+ for (const [k, v] of Object.entries(obj)) {
196
+ const next = deepUnwrapInner(v, seen);
197
+ if (next !== v)
198
+ changed = true;
199
+ out[k] = next;
200
+ }
201
+ seen.delete(obj);
202
+ return changed ? out : obj;
203
+ }
139
204
  /**
140
205
  * Wrap a value so reads through it carry an `OpTag`. Recursion is lazy:
141
206
  * a property read on an object Proxy wraps its child on demand.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@specific.dev/spectest",
3
- "version": "0.38.0",
3
+ "version": "0.39.0",
4
4
  "description": "Spectest SDK for defining test environments in TypeScript.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",