@jterrazz/test 12.2.0 → 13.0.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/dist/literate.js CHANGED
@@ -1,6 +1,7 @@
1
- import { a as LiterateSyntaxError, n as Matcher, o as parseLiterateFile, r as TOKEN_KINDS, s as serializeLiterateFile, t as CaptureScope } from "./match.js";
1
+ import { i as SpecSyntaxError, l as updateSpecFile, s as readSpecFile } from "./spec-document.js";
2
+ import { a as copyPlan, n as Matcher, r as TOKEN_KINDS, t as CaptureScope } from "./match.js";
2
3
  import { cpSync, existsSync, readFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
3
- import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
4
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
4
5
  import { execSync, spawn } from "node:child_process";
5
6
  import { readdir } from "node:fs/promises";
6
7
  import { fileURLToPath } from "node:url";
@@ -238,6 +239,20 @@ function textEquals(expected, actual, scope) {
238
239
  return placeholderStringMatches(expected, actual, scope);
239
240
  }
240
241
  /**
242
+ * Substring comparison with `{{token}}` support — the `contains` half of the
243
+ * `files:` assertions of a spec document. Without placeholders this is
244
+ * `String.includes`; with them the fixture text becomes an UNANCHORED pattern,
245
+ * so `wrote {{path}}` is found inside a larger file, and its `#ref` captures are
246
+ * recorded exactly as a whole-text comparison records them.
247
+ */
248
+ function textContains(expected, actual, scope) {
249
+ if (!hasPlaceholders(expected)) return actual.includes(expected);
250
+ const parsed = parsePlaceholderString(expected, scope);
251
+ const found = new RegExp(parsed.source).exec(actual);
252
+ if (!found) return false;
253
+ return parsed.refs.every((entry) => !entry.ref || recordRef(entry.ref, found[entry.index + 1], scope));
254
+ }
255
+ /**
241
256
  * Render an expected value for failure diffs and serialization: Matcher
242
257
  * instances become their placeholder text, everything else is untouched.
243
258
  */
@@ -1288,77 +1303,6 @@ function shouldUpdateSnapshots() {
1288
1303
  /** Standard hint appended to missing-fixture errors. */
1289
1304
  const UPDATE_HINT = "Run with TEST_UPDATE=1 (or vitest -u) to create it.";
1290
1305
  //#endregion
1291
- //#region src/core/specification/shared/fixtures.ts
1292
- /**
1293
- * Fixture path resolution + copy semantics for the `cli` facet's `.fixture()`.
1294
- *
1295
- * A fixture path is one of two shapes:
1296
- *
1297
- * - `$FIXTURES/<rest>` — the shared pool at `<specs-root>/fixtures/<rest>`,
1298
- * where `<specs-root>` is the nearest ancestor directory named `specs`.
1299
- * - `<path>` (no marker) — feature-local, at `<test-dir>/fixtures/<path>`.
1300
- *
1301
- * Any other `$`-prefixed marker is a usage error (listed against the known
1302
- * markers). Copy semantics mirror rsync's trailing-slash rule (see
1303
- * {@link copyPlan}).
1304
- */
1305
- /**
1306
- * Markers understood in a `.fixture()` path. Extend here + in
1307
- * {@link resolveFixtureSource}. Exported so the lint layer (rule
1308
- * `jterrazz/b2-known-fixture-marker`) validates literals against the same list.
1309
- */
1310
- const KNOWN_FIXTURE_MARKERS = ["$FIXTURES"];
1311
- /**
1312
- * Walk up from `startDir` to the nearest ancestor directory named `specs`.
1313
- * Throws with guidance if none is found up to the filesystem root — the
1314
- * `$FIXTURES` marker is meaningless without a specs root.
1315
- */
1316
- function discoverSpecsRoot(startDir) {
1317
- let dir = startDir;
1318
- for (;;) {
1319
- if (basename(dir) === "specs") return dir;
1320
- const parent = dirname(dir);
1321
- if (parent === dir) break;
1322
- dir = parent;
1323
- }
1324
- throw new Error(`.fixture(): the $FIXTURES marker resolves to <specs-root>/fixtures, but no directory named "specs" was found walking up from ${startDir}. Move the specification under a specs/ directory, or use a feature-local fixtures/ path (no $FIXTURES marker).`);
1325
- }
1326
- /**
1327
- * Resolve the absolute source path of a fixture, applying marker resolution.
1328
- * The trailing slash (spread semantics) is preserved by callers via
1329
- * {@link copyPlan} — it is irrelevant to source resolution.
1330
- */
1331
- function resolveFixtureSource(path, testDir) {
1332
- const clean = path.replace(/\/+$/, "");
1333
- if (clean === "$FIXTURES" || path.startsWith("$FIXTURES/")) {
1334
- const rest = clean.slice(9).replace(/^\/+/, "");
1335
- return resolve(discoverSpecsRoot(testDir), "fixtures", rest);
1336
- }
1337
- if (path.startsWith("$")) {
1338
- const marker = clean.split("/")[0];
1339
- throw new Error(`.fixture("${path}"): unknown marker "${marker}". Known markers: ${KNOWN_FIXTURE_MARKERS.join(", ")}. A path without a marker is feature-local (resolved under <test-dir>/fixtures/).`);
1340
- }
1341
- return resolve(testDir, "fixtures", clean);
1342
- }
1343
- /**
1344
- * Compute the source + destination for copying a fixture into the working
1345
- * directory. Copy semantics follow rsync's trailing-slash rule:
1346
- *
1347
- * - `dir/` (trailing slash) — the CONTENTS are spread into the cwd.
1348
- * - `dir` (no slash, a directory) — the directory is copied as `<cwd>/<basename>`.
1349
- * - `file` — copied as `<cwd>/<basename>`.
1350
- *
1351
- * Chained `.fixture()` calls layer in order: a later fixture overwrites files
1352
- * written by an earlier one.
1353
- */
1354
- function copyPlan(path, testDir, workDir) {
1355
- const src = resolveFixtureSource(path, testDir);
1356
- return {
1357
- dest: /\/+$/.test(path) ? workDir : resolve(workDir, basename(src)),
1358
- src
1359
- };
1360
- }
1361
- //#endregion
1362
1306
  //#region src/core/specification/website/serve.adapter.ts
1363
1307
  /** Grace period between SIGTERM and the SIGKILL escalation. */
1364
1308
  const KILL_GRACE_MS = 2e3;
@@ -1515,21 +1459,14 @@ var ServeAdapter = class {
1515
1459
  //#endregion
1516
1460
  //#region src/core/specification/cli/literate.ts
1517
1461
  /**
1518
- * The form a stream is compared in: ANSI stripped (rule D6), the runner's
1519
- * `transform` applied, and ONE trailing newline dropped — a `.cli` section is
1520
- * written as lines, and a command that ends its output with a newline must not
1521
- * be forced to spell an empty last line.
1462
+ * The form a stream is compared in: ANSI stripped (rule D6) and the runner's
1463
+ * `transform` applied. Nothing else — a block scalar states its own final
1464
+ * newline (`|` keeps it, `|-` drops it), so the comparison is byte-exact and
1465
+ * has no normalisation to hide behind.
1522
1466
  */
1523
1467
  function comparable(raw, transform) {
1524
1468
  const stripped = stripAnsiCodes(raw);
1525
- return (transform ? transform(stripped) : stripped).replace(/\n$/, "");
1526
- }
1527
- function narrative(spec) {
1528
- return [
1529
- `test: ${spec.header.test}`,
1530
- `given: ${spec.header.given}`,
1531
- `then: ${spec.header.then}`
1532
- ].join("\n");
1469
+ return transform ? transform(stripped) : stripped;
1533
1470
  }
1534
1471
  /**
1535
1472
  * Line equality AS THE COMPARISON JUDGED IT — a `{{url}}` line that matched is
@@ -1540,85 +1477,133 @@ function narrative(spec) {
1540
1477
  function tokenAwareEquals(scope) {
1541
1478
  return (expected, actual) => textEquals(expected, actual, new CaptureScope(scope.workdir));
1542
1479
  }
1480
+ /** A path under the working directory, refusing anything that would escape it. */
1481
+ function resolveUnderWorkDir(path, workDir, context) {
1482
+ const target = resolve(workDir, path);
1483
+ const inside = relative(workDir, target);
1484
+ if (isAbsolute(path) || inside.startsWith("..") || isAbsolute(inside)) throw new Error(`${context.displayPath}: files: "${path}" leaves the working directory — a files: key is a relative path under the cwd`);
1485
+ return target;
1486
+ }
1487
+ /** A one-line preview of a file's content for the failure rendering. */
1488
+ function preview(text) {
1489
+ return JSON.stringify(text.length > 160 ? `${text.slice(0, 160)}…` : text);
1490
+ }
1491
+ function readIfPresent(path) {
1492
+ try {
1493
+ return statSync(path).isFile() ? readFileSync(path, "utf8") : null;
1494
+ } catch {
1495
+ return null;
1496
+ }
1497
+ }
1498
+ /** Check one `files:` entry and render its failure, or `null` when it holds. */
1499
+ function checkFile(assertion, workDir, context, scope) {
1500
+ const target = resolveUnderWorkDir(assertion.path, workDir, context);
1501
+ let exists;
1502
+ try {
1503
+ exists = statSync(target) !== void 0;
1504
+ } catch {
1505
+ exists = false;
1506
+ }
1507
+ if (assertion.kind === "absent") return exists ? `${assertion.path}: expected absent, got a file` : null;
1508
+ if (!exists) return `${assertion.path}: expected to exist, got nothing at that path`;
1509
+ if (assertion.kind === "exists") return null;
1510
+ const content = readIfPresent(target);
1511
+ if (content === null) return `${assertion.path}: expected a readable file, got a directory`;
1512
+ for (const needle of assertion.contains) if (!textContains(needle.text, content, scope)) return `${assertion.path}: expected to contain ${preview(needle.text)}, got ${preview(content)}`;
1513
+ if (assertion.equals !== null && !textEquals(assertion.equals.text, content, scope)) return `${assertion.path}: expected ${preview(assertion.equals.text)}, got ${preview(content)}`;
1514
+ return null;
1515
+ }
1543
1516
  /**
1544
- * The failure a mismatching block throws: the narrative, the command that ran,
1545
- * the diff, and where to open the file. The header is named as never-rewritten
1546
- * so the update hint cannot be read as "this will re-generate my `given:`".
1517
+ * The failure a mismatching run throws: the description, the command that ran,
1518
+ * every detail the comparison rejected, and where to open the document. The
1519
+ * update hint names exactly what a rewrite touches, so it cannot be read as
1520
+ * "this will re-generate my `files:`".
1547
1521
  *
1548
- * The stack is REPLACED by the block's own line. Left alone it would carry the
1522
+ * The stack is REPLACED by the run's own line. Left alone it would carry the
1549
1523
  * engine's frames and end inside the module the plugin generates — pointing the
1550
- * reader at `<case>.cli:<the $ line of the module>` while the message already
1551
- * names the block. One frame, on the block, is the whole truth here.
1524
+ * reader at a line of a file they never wrote. One frame, on the `command:` key,
1525
+ * is the whole truth here.
1552
1526
  */
1553
- function failure(spec, run, context, detail) {
1554
- return atBlock(new Error([
1555
- `Literate spec mismatch (${context.displayPath}:${run.block.line})`,
1527
+ function failure(document, run, context, details) {
1528
+ return atLine(new Error([
1529
+ "Spec mismatch",
1556
1530
  "",
1557
- narrative(spec),
1531
+ document.description,
1558
1532
  "",
1559
- `$ ${run.block.argv}`,
1533
+ `$ ${run.command}`,
1560
1534
  "",
1561
- detail,
1535
+ details.join("\n\n"),
1562
1536
  "",
1563
- "Run with TEST_UPDATE=1 to rewrite the blocks (exit code and streams). The header is never rewritten."
1564
- ].join("\n")), context.filePath, run.block.line);
1537
+ `${context.displayPath}:${run.commandLine}`,
1538
+ "Run with TEST_UPDATE=1 to rewrite the runs — the exit code and the streams, and nothing else."
1539
+ ].join("\n")), context.filePath, run.commandLine);
1565
1540
  }
1566
1541
  /**
1567
- * Point an error's stack at one frame: the line of the `.cli` file that owns
1568
- * the failure. The framework's own frames carry nothing a reader of a literate
1569
- * spec can act on, and the generated module's frame actively misleads.
1542
+ * Point an error's stack at one frame: the line of the `.spec.yaml` that owns
1543
+ * the failure. The framework's own frames carry nothing a reader of a spec
1544
+ * document can act on, and the generated module's frame actively misleads.
1570
1545
  */
1571
- function atBlock(error, filePath, line) {
1546
+ function atLine(error, filePath, line) {
1572
1547
  error.stack = `${error.name}: ${error.message}\n at ${filePath}:${line}:1`;
1573
1548
  return error;
1574
1549
  }
1575
- /** Compare one block against what the command actually did. */
1576
- function assertBlock(spec, run, context, scope) {
1577
- const { actual, block } = run;
1578
- const equals = tokenAwareEquals(scope);
1579
- if (actual.exitCode !== block.exitCode) throw failure(spec, run, context, [
1550
+ /** Compare one run against what the command actually did. */
1551
+ function assertRun(document, outcome, context, workDir, scope) {
1552
+ const { actual, run } = outcome;
1553
+ if (actual.exitCode !== run.exitCode) throw failure(document, run, context, [[
1580
1554
  "exit code mismatch",
1581
- ` expected: ${block.exitCode}`,
1555
+ ` expected: ${run.exitCode}`,
1582
1556
  ` received: ${actual.exitCode}`,
1583
- ...run.stderr.length > 0 ? [
1557
+ ...outcome.stderr.length > 0 ? [
1584
1558
  "",
1585
1559
  "stderr was:",
1586
- run.stderr
1560
+ outcome.stderr.replace(/\n$/, "")
1587
1561
  ] : []
1562
+ ].join("\n")]);
1563
+ const equals = tokenAwareEquals(scope);
1564
+ const details = [];
1565
+ for (const [name, expected, received] of [[
1566
+ "stdout",
1567
+ run.stdout?.text ?? "",
1568
+ outcome.stdout
1569
+ ], [
1570
+ "stderr",
1571
+ run.stderr?.text ?? "",
1572
+ outcome.stderr
1573
+ ]]) if (!textEquals(expected, received, scope)) details.push(formatStdoutDiff(name, expected, received, { equals }));
1574
+ const fileFailures = run.files.map((assertion) => checkFile(assertion, workDir, context, scope)).filter((line) => line !== null);
1575
+ if (fileFailures.length > 0) details.push([
1576
+ "files mismatch",
1577
+ "",
1578
+ ...fileFailures.map((line) => ` ${line}`)
1588
1579
  ].join("\n"));
1589
- if (!textEquals(block.stdout, run.stdout, scope)) throw failure(spec, run, context, formatStdoutDiff("stdout", block.stdout, run.stdout, { equals }));
1590
- const expectedStderr = block.stderr ?? "";
1591
- if (!textEquals(expectedStderr, run.stderr, scope)) throw failure(spec, run, context, formatStdoutDiff("stderr", expectedStderr, run.stderr, { equals }));
1580
+ if (details.length > 0) throw failure(document, run, context, details);
1592
1581
  }
1593
1582
  /**
1594
- * The rewritten blocks: exit codes and streams from the run, placeholders of
1595
- * the previous golden preserved by pattern (rule D5). A `--- stderr` section
1596
- * that ends up empty is dropped, so a rewritten file passes the next run.
1583
+ * The rewritten runs: exit codes and streams from the run, placeholders of the
1584
+ * previous golden preserved by pattern (rule D5). A stream that ends up empty
1585
+ * loses its key, so a rewritten document passes the next run.
1597
1586
  */
1598
- function updatedBlocks(runs, scope) {
1599
- return runs.map(({ actual, block, stderr, stdout }) => {
1600
- const mergedStderr = mergeTextPreservingPlaceholders(block.stderr, stderr, scope);
1601
- return {
1602
- ...block,
1603
- exitCode: actual.exitCode,
1604
- stderr: mergedStderr.length > 0 ? mergedStderr : null,
1605
- stdout: mergeTextPreservingPlaceholders(block.stdout, stdout, scope)
1606
- };
1607
- });
1587
+ function updates(outcomes, scope) {
1588
+ return outcomes.map(({ actual, run, stderr, stdout }) => ({
1589
+ exitCode: actual.exitCode,
1590
+ stderr: mergeTextPreservingPlaceholders(run.stderr?.text ?? null, stderr, scope),
1591
+ stdout: mergeTextPreservingPlaceholders(run.stdout?.text ?? null, stdout, scope)
1592
+ }));
1608
1593
  }
1609
1594
  /**
1610
- * Start every server the header names, in declaration order, before the first
1611
- * block. They all stay live for the whole file and are stopped together — one
1612
- * `.cli` file is one scenario, and its servers are part of its ground.
1595
+ * Start every server the document names, in declaration order, before the first
1596
+ * run. They all stay live for the whole file and are stopped together — one
1597
+ * document is one scenario, and its servers are part of its ground.
1613
1598
  */
1614
- async function startServers(spec, config, displayPath) {
1599
+ async function startServers(document, config, displayPath) {
1615
1600
  const registry = config.serveRegistry ?? {};
1616
1601
  const started = [];
1617
1602
  const env = {};
1618
1603
  const stop = async () => {
1619
1604
  for (const adapter of started.toReversed()) await adapter.stop();
1620
1605
  };
1621
- for (const entry of spec.header.serve) {
1606
+ for (const entry of document.serve) {
1622
1607
  const registration = registry[entry.name];
1623
1608
  if (!registration) {
1624
1609
  await stop();
@@ -1643,16 +1628,16 @@ async function startServers(spec, config, displayPath) {
1643
1628
  stop
1644
1629
  };
1645
1630
  }
1646
- /** Resolve the header's `env:` tokens against the registered sets. `$WORKDIR` expands. */
1647
- function headerEnv(spec, config, displayPath) {
1631
+ /** Resolve the document's `env:` entries against the registered sets. `$WORKDIR` expands. */
1632
+ function documentEnv(document, config, displayPath) {
1648
1633
  const sets = config.envSets ?? {};
1649
1634
  const env = {};
1650
- for (const token of spec.header.env) {
1635
+ for (const token of document.env) {
1651
1636
  if (token.kind === "set") {
1652
1637
  const declared = sets[token.name];
1653
1638
  if (!declared) {
1654
1639
  const known = Object.keys(sets);
1655
- throw new Error(`${displayPath}: env: "${token.name}" is not a registered env set — ${known.length === 0 ? "specification.cli() declares no `env` option." : `registered sets: ${known.join(", ")}.`}`);
1640
+ throw new Error(`${displayPath}:${token.line}: env: "${token.name}" is not a registered env set — ${known.length === 0 ? "specification.cli() declares no `env` option." : `registered sets: ${known.join(", ")}.`}`);
1656
1641
  }
1657
1642
  Object.assign(env, declared);
1658
1643
  continue;
@@ -1667,43 +1652,50 @@ function expandWorkdir(env, workDir) {
1667
1652
  return resolved;
1668
1653
  }
1669
1654
  /**
1670
- * Run one literate spec end to end and resolve with the LAST block's result —
1655
+ * Run one spec document end to end and resolve with the LAST run's result —
1671
1656
  * the handle a `.test.ts` adds its own assertion to (a directory golden, a
1672
- * grep) after `cli.run('<case>.cli')`.
1657
+ * grep) after `cli.run('<case>.spec.yaml')`.
1673
1658
  *
1674
- * Under `TEST_UPDATE=1` nothing is asserted: every block runs, and what follows
1675
- * each `$` is rewritten from the actual output with the previous golden's
1676
- * placeholders preserved.
1659
+ * Under `TEST_UPDATE=1` nothing is asserted: every run executes, and each one's
1660
+ * exit code and streams are rewritten from the actual output with the previous
1661
+ * golden's placeholders preserved.
1677
1662
  */
1678
- async function runLiterateSpec(options) {
1663
+ async function runSpecDocument(options) {
1679
1664
  const { config, displayPath, filePath, testDir, workDir } = options;
1680
- if (!config.command) throw new Error(".run(): literate specs require a command adapter (use specification.cli())");
1681
- let spec;
1665
+ if (!config.command) throw new Error(".run(): spec documents require a command adapter (use specification.cli())");
1666
+ let file;
1682
1667
  try {
1683
- spec = parseLiterateFile(readFileSync(filePath, "utf8"), displayPath);
1668
+ file = readSpecFile(readFileSync(filePath, "utf8"), displayPath);
1684
1669
  } catch (error) {
1685
- throw error instanceof LiterateSyntaxError ? atBlock(error, filePath, error.line) : error;
1670
+ throw error instanceof SpecSyntaxError ? atLine(error, filePath, error.line) : error;
1686
1671
  }
1672
+ const document = file.document;
1687
1673
  const fileDir = filePath.replace(/[/\\][^/\\]*$/, "");
1688
- for (const path of spec.header.fixtures) {
1689
- const { dest, src } = copyPlan(path, fileDir, workDir);
1674
+ for (const fixture of document.fixtures) {
1675
+ const { dest, src } = copyPlan(fixture.path, fileDir, workDir);
1690
1676
  cpSync(src, dest, { recursive: true });
1691
1677
  }
1692
- const servers = await startServers(spec, config, displayPath);
1678
+ const servers = await startServers(document, config, displayPath);
1693
1679
  const scope = new CaptureScope(safeRealpath(workDir));
1694
1680
  const env = expandWorkdir({
1695
1681
  ...options.baseEnv,
1696
1682
  ...servers.env,
1697
- ...headerEnv(spec, config, displayPath)
1683
+ ...documentEnv(document, config, displayPath)
1698
1684
  }, workDir);
1699
- let runs;
1685
+ let outcomes;
1700
1686
  try {
1701
- runs = [];
1702
- for (const block of spec.blocks) {
1703
- const actual = await config.command.exec(block.argv, workDir, env);
1704
- runs.push({
1687
+ outcomes = [];
1688
+ for (const run of document.runs) {
1689
+ const actual = run.waitFor === null ? await config.command.exec(run.command, workDir, env, {
1690
+ stdin: run.stdin ?? void 0,
1691
+ timeout: run.timeout ?? void 0
1692
+ }) : await config.command.watch(run.command, workDir, {
1693
+ timeout: run.timeout ?? void 0,
1694
+ waitFor: run.waitFor
1695
+ }, env);
1696
+ outcomes.push({
1705
1697
  actual,
1706
- block,
1698
+ run,
1707
1699
  stderr: comparable(actual.stderr, config.transform),
1708
1700
  stdout: comparable(actual.stdout, config.transform)
1709
1701
  });
@@ -1711,13 +1703,13 @@ async function runLiterateSpec(options) {
1711
1703
  } finally {
1712
1704
  await servers.stop();
1713
1705
  }
1714
- if (shouldUpdateSnapshots() && options.frozen !== true) writeFileSync(filePath, serializeLiterateFile(spec.headerText, updatedBlocks(runs, scope)));
1715
- else for (const run of runs) assertBlock(spec, run, {
1706
+ if (shouldUpdateSnapshots() && options.frozen !== true) writeFileSync(filePath, updateSpecFile(file, updates(outcomes, scope)));
1707
+ else for (const outcome of outcomes) assertRun(document, outcome, {
1716
1708
  displayPath,
1717
1709
  filePath
1718
- }, scope);
1710
+ }, workDir, scope);
1719
1711
  return new CliResult({
1720
- commandOutput: runs.at(-1).actual,
1712
+ commandOutput: outcomes.at(-1).actual,
1721
1713
  config,
1722
1714
  dockerConfig: config.dockerConfig,
1723
1715
  testDir,
@@ -1726,9 +1718,9 @@ async function runLiterateSpec(options) {
1726
1718
  workDir
1727
1719
  });
1728
1720
  }
1729
- /** The title a transformed `.cli` module runs under — the header's `test:` line. */
1730
- function literateTitle(content, displayPath) {
1731
- return parseLiterateFile(content, displayPath).header.test;
1721
+ /** The title a transformed `.spec.yaml` module runs under — the `description:` line. */
1722
+ function specDescription(content, displayPath) {
1723
+ return readSpecFile(content, displayPath).document.description;
1732
1724
  }
1733
1725
  //#endregion
1734
- export { mergeTextPreservingPlaceholders as A, formatDirectoryDiff as C, formatTableDiff as D, formatStdoutDiff as E, textEquals as F, renderExpected as M, structuralEquals as N, hasPlaceholders as O, structuralSubset as P, walkDirectory as S, formatStartupReport as T, text as _, UPDATE_HINT as a, DirectoryAccessor as b, CliResult as c, findContainersByLabel as d, inspectContainer as f, TextAccessor as g, JsonAccessor as h, copyPlan as i, placeholderPatternSource as j, mergePreservingPlaceholders as k, BaseResult as l, TableAccessor as m, runLiterateSpec as n, shouldUpdateSnapshots as o, removeContainers as p, ServeAdapter as r, ContainerAccessor as s, literateTitle as t, validateDatabaseOption as u, getCallerDir as v, formatResponseDiff as w, diffDirectories as x, FilesystemAccessor as y };
1726
+ export { placeholderPatternSource as A, formatResponseDiff as C, hasPlaceholders as D, formatTableDiff as E, structuralEquals as M, structuralSubset as N, mergePreservingPlaceholders as O, textEquals as P, formatDirectoryDiff as S, formatStdoutDiff as T, getCallerDir as _, shouldUpdateSnapshots as a, diffDirectories as b, BaseResult as c, inspectContainer as d, removeContainers as f, text as g, TextAccessor as h, UPDATE_HINT as i, renderExpected as j, mergeTextPreservingPlaceholders as k, validateDatabaseOption as l, JsonAccessor as m, specDescription as n, ContainerAccessor as o, TableAccessor as p, ServeAdapter as r, CliResult as s, runSpecDocument as t, findContainersByLabel as u, FilesystemAccessor as v, formatStartupReport as w, walkDirectory as x, DirectoryAccessor as y };