@specific.dev/spectest 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/daemon.ts +35 -8
- package/src/index.ts +34 -86
- package/src/inspect.ts +61 -4
- package/src/recorder.ts +6 -0
package/package.json
CHANGED
package/src/daemon.ts
CHANGED
|
@@ -3016,11 +3016,27 @@ function installFetchWrapper(): () => void {
|
|
|
3016
3016
|
};
|
|
3017
3017
|
}
|
|
3018
3018
|
|
|
3019
|
-
|
|
3019
|
+
/** Build the `docker exec` argv for a service command. An optional `cwd`
|
|
3020
|
+
* becomes `-w <cwd>` (the working-directory option of `ctx.exec`), so the
|
|
3021
|
+
* command runs from that directory without it being baked into the command
|
|
3022
|
+
* string. Used by both the buffered and streaming variants so they stay in
|
|
3023
|
+
* lockstep. */
|
|
3024
|
+
function dockerExecArgs(service: string, command: string, cwd?: string): string[] {
|
|
3025
|
+
const args = ["exec"];
|
|
3026
|
+
if (cwd) args.push("-w", cwd);
|
|
3027
|
+
args.push(service, "sh", "-lc", command);
|
|
3028
|
+
return args;
|
|
3029
|
+
}
|
|
3030
|
+
|
|
3031
|
+
function execInService(
|
|
3032
|
+
service: string,
|
|
3033
|
+
command: string,
|
|
3034
|
+
cwd?: string,
|
|
3035
|
+
): Promise<ExecResult> {
|
|
3020
3036
|
return new Promise((resolve) => {
|
|
3021
3037
|
execFile(
|
|
3022
3038
|
"docker",
|
|
3023
|
-
|
|
3039
|
+
dockerExecArgs(service, command, cwd),
|
|
3024
3040
|
{ maxBuffer: 16 * 1024 * 1024 },
|
|
3025
3041
|
(err, stdout, stderr) => {
|
|
3026
3042
|
const exitCode =
|
|
@@ -3046,8 +3062,9 @@ function execInService(service: string, command: string): Promise<ExecResult> {
|
|
|
3046
3062
|
async function execInServiceWrapped(
|
|
3047
3063
|
service: string,
|
|
3048
3064
|
command: string,
|
|
3065
|
+
opts?: { cwd?: string },
|
|
3049
3066
|
): Promise<Wrapped<ExecResult>> {
|
|
3050
|
-
const res = await execInService(service, command);
|
|
3067
|
+
const res = await execInService(service, command, opts?.cwd);
|
|
3051
3068
|
return wrap(res, undefined) as unknown as Wrapped<ExecResult>;
|
|
3052
3069
|
}
|
|
3053
3070
|
|
|
@@ -3074,9 +3091,10 @@ function execInServiceStreaming(
|
|
|
3074
3091
|
service: string,
|
|
3075
3092
|
command: string,
|
|
3076
3093
|
onChunk: (stream: "stdout" | "stderr", data: string) => void,
|
|
3094
|
+
cwd?: string,
|
|
3077
3095
|
): Promise<ExecResult> {
|
|
3078
3096
|
return new Promise((resolve) => {
|
|
3079
|
-
const child = spawn("docker",
|
|
3097
|
+
const child = spawn("docker", dockerExecArgs(service, command, cwd), {
|
|
3080
3098
|
stdio: ["ignore", "pipe", "pipe"],
|
|
3081
3099
|
});
|
|
3082
3100
|
const acc = { stdout: "", stderr: "" };
|
|
@@ -3267,8 +3285,13 @@ async function runOne(testCase: TestCase<unknown>): Promise<RunResult> {
|
|
|
3267
3285
|
// or animated output replays with real timing in the web UI. The
|
|
3268
3286
|
// frames are presentation-only; the ExecResult (and any assertions on
|
|
3269
3287
|
// it) still sees the plain separated stdout/stderr.
|
|
3270
|
-
const recordedExec = async (
|
|
3288
|
+
const recordedExec = async (
|
|
3289
|
+
service: string,
|
|
3290
|
+
command: string,
|
|
3291
|
+
opts?: { cwd?: string },
|
|
3292
|
+
): Promise<ExecResult> => {
|
|
3271
3293
|
const t = Date.now();
|
|
3294
|
+
const cwd = opts?.cwd;
|
|
3272
3295
|
const resv = reserveEvent();
|
|
3273
3296
|
const session = newTerminalSession(
|
|
3274
3297
|
start,
|
|
@@ -3280,8 +3303,11 @@ async function runOne(testCase: TestCase<unknown>): Promise<RunResult> {
|
|
|
3280
3303
|
);
|
|
3281
3304
|
terminalSessions.push(session.record);
|
|
3282
3305
|
// Synthetic prompt frame so the replay is self-describing — the
|
|
3283
|
-
// program's own output starts on the next line, like a real shell.
|
|
3284
|
-
|
|
3306
|
+
// program's own output starts on the next line, like a real shell. A
|
|
3307
|
+
// working directory rides in the prompt sigil (`svc:/dir $`) the way a
|
|
3308
|
+
// real shell prompt shows it, so the recording stays self-describing.
|
|
3309
|
+
const sigil = cwd ? `${service}:${cwd}` : service;
|
|
3310
|
+
session.pushFrame(0, `\x1b[32m${sigil} $\x1b[0m \x1b[1m${command}\x1b[0m\r\n`);
|
|
3285
3311
|
let frameBytes = 0;
|
|
3286
3312
|
let frameCapped = false;
|
|
3287
3313
|
const res = await execInServiceStreaming(service, command, (_stream, data) => {
|
|
@@ -3302,13 +3328,14 @@ async function runOne(testCase: TestCase<unknown>): Promise<RunResult> {
|
|
|
3302
3328
|
// `\r\n` too keeps a CR|LF split across chunk boundaries
|
|
3303
3329
|
// harmless (`\r\r\n` renders identically).
|
|
3304
3330
|
session.pushFrame((Date.now() - t) / 1000, data.replace(/\r?\n/g, "\r\n"));
|
|
3305
|
-
});
|
|
3331
|
+
}, cwd);
|
|
3306
3332
|
session.markClosed();
|
|
3307
3333
|
const stdout = truncateUtf8(res.stdout);
|
|
3308
3334
|
const stderr = truncateUtf8(res.stderr);
|
|
3309
3335
|
const seq = recordExec({
|
|
3310
3336
|
service,
|
|
3311
3337
|
command,
|
|
3338
|
+
cwd,
|
|
3312
3339
|
exitCode: res.exitCode,
|
|
3313
3340
|
stdout: stdout.value,
|
|
3314
3341
|
stdoutTruncated: stdout.truncated,
|
package/src/index.ts
CHANGED
|
@@ -26,8 +26,9 @@ import type { OpTag, Provenanced, SpectestFetch, Wrapped } from "./inspect.js";
|
|
|
26
26
|
// `field` — a provenance-preserving, null-safe selector: a `null`/`undefined`
|
|
27
27
|
// leaf read off a wrapped op result is raw and untagged, so an `expect(...)` on
|
|
28
28
|
// it renders detached from its source op; `field` tags from the container so the
|
|
29
|
-
// assertion still nests. (
|
|
30
|
-
//
|
|
29
|
+
// assertion still nests. (A decoded value loses provenance the same way — that
|
|
30
|
+
// case is the `.transform(label, fn)` method every wrapped value carries, which
|
|
31
|
+
// runs the still-tagged value through `fn` and re-wraps the result.)
|
|
31
32
|
// To recover a raw value, call `.unwrap()` on it — spectest op results are
|
|
32
33
|
// always wrapped (in every context), so the method is always there; there is no
|
|
33
34
|
// `unwrap(x)` free function. See the `.unwrap()` discipline section of `spectest docs`.
|
|
@@ -572,8 +573,17 @@ export interface TestContext<
|
|
|
572
573
|
* program sees plain pipes (`isatty` false), so stdout/stderr stay
|
|
573
574
|
* byte-identical to what `exec` always returned and TTY-gated
|
|
574
575
|
* spinners/colour won't be emitted — reach for `terminal` when the
|
|
575
|
-
* CLI needs to believe it's on a TTY.
|
|
576
|
-
|
|
576
|
+
* CLI needs to believe it's on a TTY.
|
|
577
|
+
*
|
|
578
|
+
* Pass `{ cwd }` to run the command from a working directory instead of
|
|
579
|
+
* prefixing it with `cd <dir> && ` — the cwd is kept off the command
|
|
580
|
+
* string, so the timeline sidebar shows just the command and the
|
|
581
|
+
* directory surfaces in the detail view. */
|
|
582
|
+
exec(
|
|
583
|
+
service: string,
|
|
584
|
+
command: string,
|
|
585
|
+
opts?: ExecOpts,
|
|
586
|
+
): Promise<Wrapped<ExecResult>>;
|
|
577
587
|
/**
|
|
578
588
|
* Run a command inside a service container under a PTY and record the
|
|
579
589
|
* full terminal session as an asciicast for replay in the web UI.
|
|
@@ -756,6 +766,20 @@ export interface ExecResult {
|
|
|
756
766
|
exitCode: number;
|
|
757
767
|
}
|
|
758
768
|
|
|
769
|
+
/** Options for {@link TestContext.exec}. */
|
|
770
|
+
export interface ExecOpts {
|
|
771
|
+
/**
|
|
772
|
+
* Working directory inside the container to run the command from.
|
|
773
|
+
* Equivalent to prefixing the command with `cd <cwd> && `, but the
|
|
774
|
+
* directory is kept off the command string: the recorded step's
|
|
775
|
+
* sidebar summary shows just the command, while the working directory
|
|
776
|
+
* is surfaced in the detail view (the prompt line of the captured
|
|
777
|
+
* terminal and the step's panel header). Implemented as `docker exec
|
|
778
|
+
* -w <cwd>`, so a relative path resolves against the image's WORKDIR.
|
|
779
|
+
*/
|
|
780
|
+
cwd?: string;
|
|
781
|
+
}
|
|
782
|
+
|
|
759
783
|
export interface TestSuite<
|
|
760
784
|
S extends ServicesMap = ServicesMap,
|
|
761
785
|
F extends FakesMap = FakesMap,
|
|
@@ -997,7 +1021,11 @@ export interface ProjectSetupContext<
|
|
|
997
1021
|
F extends FakesMap = FakesMap,
|
|
998
1022
|
> {
|
|
999
1023
|
fetch: SpectestFetch;
|
|
1000
|
-
exec(
|
|
1024
|
+
exec(
|
|
1025
|
+
service: string,
|
|
1026
|
+
command: string,
|
|
1027
|
+
opts?: ExecOpts,
|
|
1028
|
+
): Promise<Wrapped<ExecResult>>;
|
|
1001
1029
|
readonly svc: ServiceHandlesFor<S>;
|
|
1002
1030
|
/** Same surface tests see — fakes are already up by setup time. Typed
|
|
1003
1031
|
* against the declared fakes map (see {@link TestContext.fakes}). */
|
|
@@ -1317,43 +1345,7 @@ interface Matchers {
|
|
|
1317
1345
|
toHaveLength(n: number): void;
|
|
1318
1346
|
}
|
|
1319
1347
|
|
|
1320
|
-
|
|
1321
|
-
* Provenance-preserving transforms. Each returns a fresh {@link Expectation}
|
|
1322
|
-
* bound to the *decoded* value but still carrying the originating op's tag (its
|
|
1323
|
-
* path extended by a marker like `<base64>`), so the eventual assertion nests
|
|
1324
|
-
* under the source read in the timeline. This is what lets a value that must be
|
|
1325
|
-
* decoded through a plain function — `Buffer.from(x,"base64").toString()`,
|
|
1326
|
-
* `decodeURIComponent(x)` — be asserted on with the *full* matcher vocabulary
|
|
1327
|
-
* (`toHaveLength`, `toContain`, `toMatch`, …) without dropping to `expectRaw`,
|
|
1328
|
-
* which severs the link. Transforms chain (`.base64Decoded().urlDecoded()`),
|
|
1329
|
-
* and the matcher you finish with may be negated (`.base64Decoded().not.…`).
|
|
1330
|
-
*
|
|
1331
|
-
* They supersede the old `toBeBase64Of` matcher, which was equality-only:
|
|
1332
|
-
* `expect(secret.data?.X).base64Decoded().toBe("plain")` is the direct
|
|
1333
|
-
* replacement, and `.toHaveLength(32)` / `.toContain("redis://")` / `.toMatch(re)`
|
|
1334
|
-
* are the cases it couldn't express.
|
|
1335
|
-
*/
|
|
1336
|
-
interface Transforms {
|
|
1337
|
-
/** Interpret the value as a base64 string and decode it (UTF-8), mirroring
|
|
1338
|
-
* `Buffer.from(x, "base64").toString()`. Throws-as-failed-assertion if the
|
|
1339
|
-
* value isn't a string. */
|
|
1340
|
-
base64Decoded(): Expectation;
|
|
1341
|
-
/** URL-decode the value, mirroring `decodeURIComponent(x)`. Chains after
|
|
1342
|
-
* `base64Decoded()` for base64-then-URL-encoded values. */
|
|
1343
|
-
urlDecoded(): Expectation;
|
|
1344
|
-
/**
|
|
1345
|
-
* Generic escape hatch: apply an arbitrary `fn` to the raw value and assert on
|
|
1346
|
-
* the result, keeping provenance. `label` is a plain word (`"json"`,
|
|
1347
|
-
* `"decompressed"`) appended to the op path so the timeline shows what was
|
|
1348
|
-
* derived; the UI brackets it (`<json>`) to mark it as a derived step, so do
|
|
1349
|
-
* **not** include the brackets yourself. Sugar like {@link base64Decoded} is
|
|
1350
|
-
* built on this. If `fn` throws, the next assertion records as a failure
|
|
1351
|
-
* describing the transform error.
|
|
1352
|
-
*/
|
|
1353
|
-
transform(label: string, fn: (value: unknown) => unknown): Expectation;
|
|
1354
|
-
}
|
|
1355
|
-
|
|
1356
|
-
export interface Expectation extends Matchers, Transforms {
|
|
1348
|
+
export interface Expectation extends Matchers {
|
|
1357
1349
|
not: Matchers;
|
|
1358
1350
|
}
|
|
1359
1351
|
|
|
@@ -1459,31 +1451,6 @@ function buildCore(
|
|
|
1459
1451
|
});
|
|
1460
1452
|
if (!passed) throw new ExpectationError(msg);
|
|
1461
1453
|
};
|
|
1462
|
-
// Apply `fn` to the raw value and rebuild against the result, extending the
|
|
1463
|
-
// op path by a derived-step marker so the decoded value still nests under the
|
|
1464
|
-
// source read. `label` is a plain word ("base64", "json"); the `<…>` marker
|
|
1465
|
-
// syntax that distinguishes a transform from a real property read in the
|
|
1466
|
-
// timeline is added here, so it lives in one place and never leaks into the
|
|
1467
|
-
// API surface — the same convention `inspect.ts` uses for array methods
|
|
1468
|
-
// (`<find>`, `<map>`). A throw becomes a `pendingError` on the returned
|
|
1469
|
-
// Expectation rather than escaping — a malformed decode renders as a failed
|
|
1470
|
-
// assertion in the timeline, not an unhandled exception. Propagates an
|
|
1471
|
-
// existing `pendingError` untouched.
|
|
1472
|
-
const applyTransform = (label: string, fn: (value: unknown) => unknown): Expectation => {
|
|
1473
|
-
const marker = `<${label}>`;
|
|
1474
|
-
const nextTag: OpTag | undefined = tag
|
|
1475
|
-
? { sourceSeq: tag.sourceSeq, path: [...tag.path, marker] }
|
|
1476
|
-
: undefined;
|
|
1477
|
-
if (pendingError !== undefined) {
|
|
1478
|
-
return buildCore(undefined, nextTag, false, message, pendingError);
|
|
1479
|
-
}
|
|
1480
|
-
try {
|
|
1481
|
-
return buildCore(fn(actual), nextTag, false, message);
|
|
1482
|
-
} catch (err) {
|
|
1483
|
-
const detail = err instanceof Error ? err.message : String(err);
|
|
1484
|
-
return buildCore(undefined, nextTag, false, message, `${marker}: ${detail}`);
|
|
1485
|
-
}
|
|
1486
|
-
};
|
|
1487
1454
|
return {
|
|
1488
1455
|
toBe(expected) {
|
|
1489
1456
|
const exp = readRaw(expected);
|
|
@@ -1592,25 +1559,6 @@ function buildCore(
|
|
|
1592
1559
|
{ actual: len, pathSuffix: "length" },
|
|
1593
1560
|
);
|
|
1594
1561
|
},
|
|
1595
|
-
transform(label, fn) {
|
|
1596
|
-
return applyTransform(label, fn);
|
|
1597
|
-
},
|
|
1598
|
-
base64Decoded() {
|
|
1599
|
-
return applyTransform("base64", (v) => {
|
|
1600
|
-
if (typeof v !== "string") {
|
|
1601
|
-
throw new Error(`base64Decoded expects a string, got ${typeof v}`);
|
|
1602
|
-
}
|
|
1603
|
-
return Buffer.from(v, "base64").toString();
|
|
1604
|
-
});
|
|
1605
|
-
},
|
|
1606
|
-
urlDecoded() {
|
|
1607
|
-
return applyTransform("urldecode", (v) => {
|
|
1608
|
-
if (typeof v !== "string") {
|
|
1609
|
-
throw new Error(`urlDecoded expects a string, got ${typeof v}`);
|
|
1610
|
-
}
|
|
1611
|
-
return decodeURIComponent(v);
|
|
1612
|
-
});
|
|
1613
|
-
},
|
|
1614
1562
|
get not(): Matchers {
|
|
1615
1563
|
// Re-pass the already-unwrapped value, tag and message so the tag and raw
|
|
1616
1564
|
// label are preserved for the negated branch's AssertionEvent.
|
package/src/inspect.ts
CHANGED
|
@@ -248,6 +248,12 @@ function wrapObject<T extends object>(
|
|
|
248
248
|
return () => readRaw(target);
|
|
249
249
|
}
|
|
250
250
|
}
|
|
251
|
+
// `.transform(label, fn)` — same shape/guard as `unwrap`: only intercept
|
|
252
|
+
// when the underlying object has no own `transform`, so a real data field
|
|
253
|
+
// named `transform` is never shadowed.
|
|
254
|
+
if (prop === "transform" && !(prop in (target as object))) {
|
|
255
|
+
return makeTransform(readRaw(target), sourceSeq, path);
|
|
256
|
+
}
|
|
251
257
|
// Hide thenable-ness from `await`. We must never accidentally
|
|
252
258
|
// implement `then`, or `await fetch(...)` would resolve to the
|
|
253
259
|
// wrong thing if we ever wrapped a Promise (we don't, but be safe).
|
|
@@ -298,6 +304,34 @@ function wrapObject<T extends object>(
|
|
|
298
304
|
return new Proxy(raw, handler);
|
|
299
305
|
}
|
|
300
306
|
|
|
307
|
+
// The `.transform(label, fn)` method every wrapper (carrier + object/array
|
|
308
|
+
// proxy) carries — the value-level analogue of `.unwrap()`. It runs the raw
|
|
309
|
+
// value through `fn` and re-`wrap`s the result with `path` extended by a
|
|
310
|
+
// `<label>` derived-step marker, so the decoded value keeps the source op's
|
|
311
|
+
// provenance (`sourceSeq`) and stays navigable: a later `expect(...)` on it, or
|
|
312
|
+
// on a subfield, still nests under the originating op (`secret.data.config.<json>.tier`).
|
|
313
|
+
// `label` is a plain word; the `<…>` brackets are added here so they never leak
|
|
314
|
+
// into the call site (the same convention array methods use, `<find>`/`<map>`).
|
|
315
|
+
// A throw in `fn` (a non-string value, malformed base64/JSON) is rethrown
|
|
316
|
+
// prefixed with `<label>:`, failing the test at the bad value.
|
|
317
|
+
function makeTransform(
|
|
318
|
+
raw: unknown,
|
|
319
|
+
sourceSeq: number | undefined,
|
|
320
|
+
path: readonly string[],
|
|
321
|
+
): (label: string, fn: (raw: never) => unknown) => unknown {
|
|
322
|
+
return (label, fn) => {
|
|
323
|
+
const nextPath = [...path, `<${label}>`];
|
|
324
|
+
let result: unknown;
|
|
325
|
+
try {
|
|
326
|
+
result = (fn as (r: unknown) => unknown)(raw);
|
|
327
|
+
} catch (err) {
|
|
328
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
329
|
+
throw new Error(`<${label}>: ${detail}`);
|
|
330
|
+
}
|
|
331
|
+
return wrap(result, sourceSeq, nextPath);
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
301
335
|
// Runtime shape of a primitive carrier: the public `Carrier<T>` surface
|
|
302
336
|
// (`unwrap()`) plus the internal provenance symbols. Kept private so the
|
|
303
337
|
// exported `Carrier<T>` stays clean.
|
|
@@ -316,6 +350,7 @@ function makeCarrier<T>(
|
|
|
316
350
|
[OP_TAG]: tag,
|
|
317
351
|
[UNWRAP]: raw,
|
|
318
352
|
unwrap: () => raw,
|
|
353
|
+
transform: makeTransform(raw, sourceSeq, path) as unknown as Carrier<T>["transform"],
|
|
319
354
|
// Coercion sinks recover the raw primitive instead of inheriting
|
|
320
355
|
// Object.prototype's defaults ("[object Object]" / NaN / {}). A
|
|
321
356
|
// carrier interpolated into a string, fed to arithmetic, `==`, or
|
|
@@ -338,10 +373,10 @@ function makeCarrier<T>(
|
|
|
338
373
|
// passes them through raw. So `expect(row.deleted_at).toBe(null)` reaches
|
|
339
374
|
// `expect()` as a bare `null` with no tag and renders as a disconnected
|
|
340
375
|
// top-level row. `field` recovers the link by tagging from the *container*.
|
|
341
|
-
// (
|
|
342
|
-
//
|
|
343
|
-
//
|
|
344
|
-
//
|
|
376
|
+
// (A decoded value loses its tag the same way, through the decode; the
|
|
377
|
+
// `.transform(label, fn)` method every wrapper carries (see `makeTransform`)
|
|
378
|
+
// recovers it — it runs the still-tagged value through `fn` and re-`wrap`s the
|
|
379
|
+
// result with the source path extended by a `<label>` marker.)
|
|
345
380
|
// ───────────────────────────────────────────────────────────────────────────
|
|
346
381
|
|
|
347
382
|
/**
|
|
@@ -410,6 +445,22 @@ export function field(value: unknown, ...path: Array<string | number>): unknown
|
|
|
410
445
|
export interface Carrier<T> {
|
|
411
446
|
/** Recover the raw underlying value. */
|
|
412
447
|
unwrap(): T;
|
|
448
|
+
/**
|
|
449
|
+
* Run the raw value through `fn` and get back a provenance-carrying handle to
|
|
450
|
+
* the result — for asserting on a *decoded* value (base64, JSON, JWT, …) while
|
|
451
|
+
* keeping its link to the op that produced it. The op path is extended by a
|
|
452
|
+
* `<label>` marker, so a later `expect(...)` on the result still nests under
|
|
453
|
+
* the source op. `label` is a plain word (`"base64"`, `"json"`); the UI adds
|
|
454
|
+
* the `<…>` brackets, so don't include them yourself. `fn` receives the raw
|
|
455
|
+
* value (typed `T`), so no cast is needed. `R` types the result. A throw in
|
|
456
|
+
* `fn` is rethrown prefixed with `<label>:`, failing the test at the value.
|
|
457
|
+
*
|
|
458
|
+
* ```ts
|
|
459
|
+
* expect(secret.data.url.transform("base64", (s) => Buffer.from(s, "base64").toString()))
|
|
460
|
+
* .toContain("redis://");
|
|
461
|
+
* ```
|
|
462
|
+
*/
|
|
463
|
+
transform<R = unknown>(label: string, fn: (raw: T) => R): Wrapped<R>;
|
|
413
464
|
/** Coerces to the raw value (arithmetic, `==`). */
|
|
414
465
|
valueOf(): T;
|
|
415
466
|
/** Renders the raw value (template interpolation). */
|
|
@@ -475,6 +526,9 @@ export type WrappedObject<T> = {
|
|
|
475
526
|
} & {
|
|
476
527
|
/** Recover the fully raw value (nested leaves unwrapped too). */
|
|
477
528
|
unwrap(): T;
|
|
529
|
+
/** Run the raw object through `fn`, keeping provenance (op path + `<label>`),
|
|
530
|
+
* and get back a navigable handle to the result. See {@link Carrier.transform}. */
|
|
531
|
+
transform<R = unknown>(label: string, fn: (raw: T) => R): Wrapped<R>;
|
|
478
532
|
};
|
|
479
533
|
|
|
480
534
|
/**
|
|
@@ -491,6 +545,9 @@ export interface WrappedArray<U> {
|
|
|
491
545
|
readonly [index: number]: Wrapped<U>;
|
|
492
546
|
/** Recover the raw array (elements unwrapped). */
|
|
493
547
|
unwrap(): U[];
|
|
548
|
+
/** Run the raw array through `fn`, keeping provenance (op path + `<label>`),
|
|
549
|
+
* and get back a navigable handle to the result. See {@link Carrier.transform}. */
|
|
550
|
+
transform<R = unknown>(label: string, fn: (raw: U[]) => R): Wrapped<R>;
|
|
494
551
|
at(index: number): Wrapped<U> | undefined;
|
|
495
552
|
find(
|
|
496
553
|
predicate: (value: U, index: number, obj: U[]) => unknown,
|
package/src/recorder.ts
CHANGED
|
@@ -47,6 +47,12 @@ export interface ExecEvent extends BaseEvent {
|
|
|
47
47
|
kind: "exec";
|
|
48
48
|
service: string;
|
|
49
49
|
command: string;
|
|
50
|
+
/**
|
|
51
|
+
* Working directory the command ran from (`ctx.exec(svc, cmd, { cwd })`),
|
|
52
|
+
* if any. Kept off `command` so the sidebar shows just the command; the
|
|
53
|
+
* detail view surfaces it. Absent when the call used no `cwd`.
|
|
54
|
+
*/
|
|
55
|
+
cwd?: string;
|
|
50
56
|
exitCode: number;
|
|
51
57
|
/** stdout truncated to OUTPUT_SNIPPET_BYTES. */
|
|
52
58
|
stdout: string;
|