@jterrazz/test 14.1.0 → 14.3.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.
@@ -179,12 +179,12 @@ function projectScreen(xml) {
179
179
  }
180
180
  //#endregion
181
181
  //#region src/integrations/appium/appium.adapter.ts
182
- const ACTION_TIMEOUT_MS = 3e4;
182
+ const DEFAULT_ACTION_TIMEOUT_MS = 3e4;
183
183
  const POLL_INTERVAL_MS = 500;
184
184
  /** How many candidates the ambiguity error enumerates before truncating. */
185
185
  const MAX_REPORTED_MATCHES = 10;
186
186
  /** WebDriverAgent builds once per simulator — the first session is the slow one. */
187
- const WDA_LAUNCH_TIMEOUT_MS = 24e4;
187
+ const DEFAULT_WDA_LAUNCH_TIMEOUT_MS = 24e4;
188
188
  const delay = (ms) => new Promise((resolveDelay) => setTimeout(resolveDelay, ms));
189
189
  /** Escape a name for an NSPredicate single-quoted string literal. */
190
190
  function escapePredicate(value) {
@@ -213,6 +213,19 @@ function scopeChain(element) {
213
213
  for (let current = element; current; current = current.scope) chain.unshift(current);
214
214
  return chain;
215
215
  }
216
+ /**
217
+ * Load webdriverio's session factory, naming the install when the optional
218
+ * peer dependency is absent. Lazy: the dependency is only required by a spec
219
+ * that actually opens the app.
220
+ */
221
+ async function loadRemote() {
222
+ try {
223
+ const { remote: remoteFn } = await import("webdriverio");
224
+ return remoteFn;
225
+ } catch {
226
+ throw new Error(".open() requires webdriverio (optional peer dependency): npm install -D appium webdriverio && npx appium driver install xcuitest");
227
+ }
228
+ }
216
229
  /** Capture one candidate's evidence for the ambiguity refusal. */
217
230
  async function captureMatch(element) {
218
231
  const [type, label, value, identifier] = await Promise.all([
@@ -241,10 +254,27 @@ async function captureMatch(element) {
241
254
  * this module is only loaded when a spec calls `.open()`.
242
255
  */
243
256
  var AppiumAdapter = class {
257
+ /** The verb poll budget — the runner's `timeouts.action`, or the default. */
258
+ actionTimeoutMs;
244
259
  driver = null;
260
+ /** The WebDriverAgent launch budget — the runner's `timeouts.launch`, or the default. */
261
+ launchTimeoutMs;
245
262
  options;
263
+ /**
264
+ * The session factory. Absent in production — webdriverio is then loaded
265
+ * lazily on the first `open()`. Injected by this module's own unit test,
266
+ * which drives the waits over a stub driver with no simulator in reach
267
+ * (mocks are CODE here, CONVENTIONS I4).
268
+ */
269
+ remote;
246
270
  constructor(options) {
247
- this.options = options;
271
+ this.options = {
272
+ serverUrl: options.serverUrl,
273
+ udid: options.udid
274
+ };
275
+ this.remote = options.remote;
276
+ this.actionTimeoutMs = options.timeouts?.action ?? DEFAULT_ACTION_TIMEOUT_MS;
277
+ this.launchTimeoutMs = options.timeouts?.launch ?? DEFAULT_WDA_LAUNCH_TIMEOUT_MS;
248
278
  }
249
279
  async close() {
250
280
  if (this.driver) {
@@ -305,7 +335,7 @@ var AppiumAdapter = class {
305
335
  */
306
336
  async resolveOne(driver, element, verb, cardinality) {
307
337
  const chain = scopeChain(element);
308
- const deadline = Date.now() + ACTION_TIMEOUT_MS;
338
+ const deadline = Date.now() + this.actionTimeoutMs;
309
339
  let scrollAttempted = false;
310
340
  for (;;) {
311
341
  const resolved = await this.resolveChain(driver, chain, cardinality, { tryScroll: !scrollAttempted });
@@ -367,17 +397,12 @@ var AppiumAdapter = class {
367
397
  const labels = [...new Set([...source.matchAll(/label="(?<label>[^"]{2,60})"/g)].map((match) => match.groups?.["label"] ?? ""))].slice(0, 15);
368
398
  if (labels.length > 0) excerpt = `\nCurrently on screen: ${labels.join(" | ")}`;
369
399
  } catch {}
370
- return /* @__PURE__ */ new Error(`${verb}(${formatElement(element)}) timed out after ${ACTION_TIMEOUT_MS}ms — no visible match.${excerpt}`);
400
+ return /* @__PURE__ */ new Error(`${verb}(${formatElement(element)}) timed out after ${this.actionTimeoutMs}ms — no visible match.${excerpt}`);
371
401
  }
372
402
  /** Create the shared driver session (once), with an actionable error when webdriverio is absent. */
373
403
  async session(bundleId) {
374
404
  if (this.driver) return this.driver;
375
- let remoteFn;
376
- try {
377
- ({remote: remoteFn} = await import("webdriverio"));
378
- } catch {
379
- throw new Error(".open() requires webdriverio (optional peer dependency): npm install -D appium webdriverio && npx appium driver install xcuitest");
380
- }
405
+ const remoteFn = this.remote ?? await loadRemote();
381
406
  const url = new URL(this.options.serverUrl);
382
407
  this.driver = await remoteFn({
383
408
  capabilities: {
@@ -385,7 +410,7 @@ var AppiumAdapter = class {
385
410
  "appium:bundleId": bundleId,
386
411
  "appium:noReset": true,
387
412
  "appium:udid": this.options.udid,
388
- "appium:wdaLaunchTimeout": WDA_LAUNCH_TIMEOUT_MS,
413
+ "appium:wdaLaunchTimeout": this.launchTimeoutMs,
389
414
  platformName: "iOS"
390
415
  },
391
416
  hostname: url.hostname,
package/dist/artifacts.js CHANGED
@@ -932,6 +932,95 @@ function removeContainers(ids) {
932
932
  } catch {}
933
933
  }
934
934
  //#endregion
935
+ //#region src/core/specification/shared/resolve.ts
936
+ /** What makes a directory a project root — a manifest, or a test stack. */
937
+ const ROOT_MARKERS = ["package.json", "docker/compose.test.yaml"];
938
+ /**
939
+ * The NEAREST ancestor of `startDir` (itself included) carrying a root marker,
940
+ * `undefined` when none does.
941
+ *
942
+ * Both markers are probed at each ancestor, in ONE walk. Two walks — the
943
+ * compose file all the way up, then `package.json` all the way up — made the
944
+ * further directory win: in a workspace, a `docker/compose.test.yaml` at the
945
+ * repository root outranked the `package.json` of the very package being
946
+ * tested, and every path the runner resolved was measured from the wrong unit.
947
+ * The unit is the nearest thing that declares itself a project.
948
+ *
949
+ * The existence probe is injected so the lint layer can pass its cached one
950
+ * (A9's `a9w-redundant-root` derives the same root, and must derive it the
951
+ * same way).
952
+ */
953
+ function findRoot(startDir, exists) {
954
+ let dir = startDir;
955
+ for (;;) {
956
+ if (ROOT_MARKERS.some((marker) => exists(resolve(dir, marker)))) return dir;
957
+ const parent = dirname(dir);
958
+ if (parent === dir) return;
959
+ dir = parent;
960
+ }
961
+ }
962
+ /**
963
+ * Auto-discover the project root from a starting directory (CONVENTIONS A9):
964
+ * walk up to the first directory that carries `package.json` or
965
+ * `docker/compose.test.yaml`; if none does, the starting directory itself.
966
+ */
967
+ function discoverRoot(startDir) {
968
+ return findRoot(startDir, existsSync) ?? startDir;
969
+ }
970
+ /**
971
+ * Resolve the project root for a specification. An explicit `root` option is
972
+ * an override (resolved from the caller's directory when relative); when
973
+ * absent, the root is auto-discovered by walking up from the calling
974
+ * specification file.
975
+ */
976
+ function resolveRoot(root, callerDir) {
977
+ if (!root) return discoverRoot(callerDir);
978
+ if (isAbsolute(root)) return root;
979
+ return resolve(callerDir, root);
980
+ }
981
+ /**
982
+ * Resolve a command — checks node_modules/.bin, then treats as absolute/PATH.
983
+ */
984
+ function resolveCommand(command, root) {
985
+ if (isAbsolute(command)) return command;
986
+ const binPath = resolve(root, "node_modules/.bin", command);
987
+ if (existsSync(binPath)) return binPath;
988
+ const cwdBinPath = resolve(process.cwd(), "node_modules/.bin", command);
989
+ if (existsSync(cwdBinPath)) return cwdBinPath;
990
+ return command;
991
+ }
992
+ /**
993
+ * The realpath form of a working directory — what a child process prints as
994
+ * `$PWD` (macOS resolves the tmpdir symlink), and therefore what `{{workdir}}`
995
+ * compares against. Falls back to the path itself when it cannot be resolved.
996
+ *
997
+ * @internal Shared by the result scope, the literate runner and `$WORKDIR`.
998
+ */
999
+ function safeRealpath(path) {
1000
+ try {
1001
+ return realpathSync(path);
1002
+ } catch {
1003
+ return path;
1004
+ }
1005
+ }
1006
+ /**
1007
+ * Expand the `$WORKDIR` token in every string value of a child environment —
1008
+ * the one place the token is spelled out, for both doors (`.env()` on the
1009
+ * chain, `env:` in a `<case>.spec.yaml`).
1010
+ *
1011
+ * It expands to the RESOLVED directory, never the raw temp path: a value the
1012
+ * child receives comes back in its output, and it must come back spelled the
1013
+ * way `{{workdir}}` holds it (realpath form, CONVENTIONS D4). Expanding the raw
1014
+ * path made `HOME=$WORKDIR` on macOS echo `/var/folders/…` against a
1015
+ * `{{workdir}}` holding `/private/var/folders/…` — a golden no run could match.
1016
+ */
1017
+ function expandWorkdir(env, workDir) {
1018
+ const workdir = safeRealpath(workDir);
1019
+ const expanded = {};
1020
+ for (const [key, value] of Object.entries(env)) expanded[key] = typeof value === "string" ? value.replace(/\$WORKDIR/g, workdir) : value;
1021
+ return expanded;
1022
+ }
1023
+ //#endregion
935
1024
  //#region src/core/specification/shared/result/result.ts
936
1025
  /**
937
1026
  * Base result - common accessors available after any action type.
@@ -986,20 +1075,6 @@ var BaseResult = class {
986
1075
  }
987
1076
  };
988
1077
  /**
989
- * The realpath form of a working directory — what a child process prints as
990
- * `$PWD` (macOS resolves the tmpdir symlink), and therefore what `{{workdir}}`
991
- * compares against. Falls back to the path itself when it cannot be resolved.
992
- *
993
- * @internal Shared with the literate runner, which builds its own file-wide scope.
994
- */
995
- function safeRealpath(path) {
996
- try {
997
- return realpathSync(path);
998
- } catch {
999
- return path;
1000
- }
1001
- }
1002
- /**
1003
1078
  * Enforce CONVENTIONS A7: with two or more databases in the services record
1004
1079
  * the `database` option is mandatory on `.seed()` / `.table()`; with exactly
1005
1080
  * one it is forbidden (redundant).
@@ -1596,11 +1671,19 @@ function updates(outcomes, scope) {
1596
1671
  }));
1597
1672
  }
1598
1673
  /**
1599
- * Start every server the document names, in declaration order, before the first
1600
- * run. They all stay live for the whole file and are stopped together — one
1601
- * document is one scenario, and its servers are part of its ground.
1674
+ * Start every server the document names, in declaration order, after `fixture:`
1675
+ * has been copied and before the first run. They all stay live for the whole
1676
+ * file and are stopped together — one document is one scenario, and its servers
1677
+ * are part of its ground.
1678
+ *
1679
+ * Every child is seeded with `TEST_WORKDIR`, the resolved working directory the
1680
+ * `{{workdir}}` token holds and `$WORKDIR` expands to. The fixtures are already
1681
+ * on disk when the child starts, so a served stub reads the world the document
1682
+ * laid down exactly as a stub binary reads its own fixture tree: a scenario is
1683
+ * a layer of files, never a flag threaded through the server. A document's
1684
+ * mapping-form env wins over it, key by key.
1602
1685
  */
1603
- async function startServers(document, config, displayPath) {
1686
+ async function startServers(document, config, displayPath, workdir) {
1604
1687
  const registry = config.serveRegistry ?? {};
1605
1688
  const started = [];
1606
1689
  const env = {};
@@ -1617,7 +1700,10 @@ async function startServers(document, config, displayPath) {
1617
1700
  const adapter = new ServeAdapter({
1618
1701
  command: registration.command,
1619
1702
  ready: registration.ready
1620
- }, config.root ?? process.cwd(), "cli", entry.env);
1703
+ }, config.root ?? process.cwd(), "cli", {
1704
+ TEST_WORKDIR: workdir,
1705
+ ...entry.env
1706
+ });
1621
1707
  started.push(adapter);
1622
1708
  try {
1623
1709
  await adapter.start();
@@ -1650,11 +1736,6 @@ function documentEnv(document, config, displayPath) {
1650
1736
  }
1651
1737
  return env;
1652
1738
  }
1653
- function expandWorkdir(env, workDir) {
1654
- const resolved = {};
1655
- for (const [key, value] of Object.entries(env)) resolved[key] = typeof value === "string" ? value.replace(/\$WORKDIR/g, workDir) : value;
1656
- return resolved;
1657
- }
1658
1739
  /**
1659
1740
  * Run one spec document end to end and resolve with the LAST run's result —
1660
1741
  * the handle a `.test.ts` adds its own assertion to (a directory golden, a
@@ -1679,8 +1760,9 @@ async function runSpecDocument(options) {
1679
1760
  const { dest, src } = copyPlan(fixture.path, fileDir, workDir);
1680
1761
  cpSync(src, dest, { recursive: true });
1681
1762
  }
1682
- const servers = await startServers(document, config, displayPath);
1683
- const scope = new CaptureScope(safeRealpath(workDir));
1763
+ const workdir = safeRealpath(workDir);
1764
+ const servers = await startServers(document, config, displayPath, workdir);
1765
+ const scope = new CaptureScope(workdir);
1684
1766
  const env = expandWorkdir({
1685
1767
  ...options.baseEnv,
1686
1768
  ...servers.env,
@@ -1734,4 +1816,4 @@ const COVERAGE_DIR = `${VITEST_ARTIFACTS_DIR}/coverage`;
1734
1816
  /** Where `sqlite()` caches the schema template it copies per worker. */
1735
1817
  const SQLITE_TEMPLATE_DIR = `${VITEST_ARTIFACTS_DIR}/sqlite`;
1736
1818
  //#endregion
1737
- export { hasPlaceholders as A, diffDirectories as C, formatStartupReport as D, formatResponseDiff as E, structuralEquals as F, structuralSubset as I, textEquals as L, mergeTextPreservingPlaceholders as M, placeholderPatternSource as N, formatStdoutDiff as O, renderExpected as P, DirectoryAccessor as S, formatDirectoryDiff as T, JsonAccessor as _, specDescription as a, getCallerDir as b, shouldUpdateSnapshots as c, BaseResult as d, validateDatabaseOption as f, TableAccessor as g, removeContainers as h, runSpecDocument as i, mergePreservingPlaceholders as j, formatTableDiff as k, ContainerAccessor as l, inspectContainer as m, SQLITE_TEMPLATE_DIR as n, ServeAdapter as o, findContainersByLabel as p, VITEST_ARTIFACTS_DIR as r, UPDATE_HINT as s, COVERAGE_DIR as t, CliResult as u, TextAccessor as v, walkDirectory as w, FilesystemAccessor as x, text as y };
1819
+ export { formatResponseDiff as A, structuralSubset as B, text as C, diffDirectories as D, DirectoryAccessor as E, mergePreservingPlaceholders as F, mergeTextPreservingPlaceholders as I, placeholderPatternSource as L, formatStdoutDiff as M, formatTableDiff as N, walkDirectory as O, hasPlaceholders as P, renderExpected as R, TextAccessor as S, FilesystemAccessor as T, textEquals as V, findContainersByLabel as _, specDescription as a, TableAccessor as b, shouldUpdateSnapshots as c, BaseResult as d, validateDatabaseOption as f, resolveRoot as g, resolveCommand as h, runSpecDocument as i, formatStartupReport as j, formatDirectoryDiff as k, ContainerAccessor as l, expandWorkdir as m, SQLITE_TEMPLATE_DIR as n, ServeAdapter as o, discoverRoot as p, VITEST_ARTIFACTS_DIR as r, UPDATE_HINT as s, COVERAGE_DIR as t, CliResult as u, inspectContainer as v, getCallerDir as w, JsonAccessor as x, removeContainers as y, structuralEquals as z };
package/dist/index.d.ts CHANGED
@@ -798,6 +798,24 @@ interface DeviceOpenOptions {
798
798
  /** The interaction scenario to run after launch; the capture reflects the final state. */
799
799
  scenario?: MobileScenario;
800
800
  }
801
+ /**
802
+ * How long a device session waits, in milliseconds. Every field is optional
803
+ * and falls back to the framework's own default, so a runner states only the
804
+ * wait it needs to move — the defaults suit a release build, and a project
805
+ * that drives a DEV build pays for its bundler's cold boot.
806
+ */
807
+ interface DeviceTimeouts {
808
+ /**
809
+ * How long every verb polls for a visible match before refusing.
810
+ * Default: `30_000` — enough for a cold app boot on a release bundle.
811
+ */
812
+ action?: number;
813
+ /**
814
+ * How long the FIRST session waits for WebDriverAgent to build and
815
+ * launch on the simulator. Default: `240_000`.
816
+ */
817
+ launch?: number;
818
+ }
801
819
  /**
802
820
  * Abstract device interface for the mobile specification runner.
803
821
  * One implementation lives in `integrations/appium/` — a single driver
@@ -1776,6 +1794,13 @@ interface MobileSpecificationOptions {
1776
1794
  * file when absent.
1777
1795
  */
1778
1796
  root?: string;
1797
+ /**
1798
+ * How long this runner's session waits, in milliseconds — every field
1799
+ * optional, each falling back to the framework's default. A project
1800
+ * driving a DEV build states the wait its bundler's cold boot needs
1801
+ * (`{ action: 45_000 }`) instead of sleeping inside its scenarios.
1802
+ */
1803
+ timeouts?: DeviceTimeouts;
1779
1804
  }
1780
1805
  /**
1781
1806
  * The record returned by {@link startMobile | specification.mobile}.
@@ -2512,4 +2537,4 @@ declare module 'vitest' {
2512
2537
  }
2513
2538
  }
2514
2539
  //#endregion
2515
- export { type ApiHandle, type ApiSpecification, type ApiSpecificationOptions, BaseResult, type BrowserConsoleMessage, type BrowserLinkElement, type BrowserMetaElement, type BrowserOpenOptions, type BrowserPage, type BrowserPort, type CaptureScope, type CliEnv, type CliHandle, type CliInput, type CliOutput, type CliPort, CliResult, type CliSpecification, type CliSpecificationOptions, ContainerAccessor, type ContainerPort, type Contract, type ContractInput, type ContractRequest, type ContractResponder, type ContractResponse, type ContractResponseValue, type Contracts, type DatabaseKeys, type DatabasePort, type DeviceOpenOptions, type DevicePort, type DeviceScreen, DirectoryAccessor, type DockerSpecConfig, type ElementKind, type ElementMatch, type ElementOptions, type ElementRef, type ExecOptions, FetchResult, type FileAccessor, FilesystemAccessor, type HonoApp, type HttpContractFilter, type HttpResponseInit, HttpResult, type IsolationStrategy, type JobHandle, type JobsHandle, type JobsSpecification, type JobsSpecificationOptions, JsonAccessor, type LandmarkKind, type LiterateRunFlags, type LiterateServeRegistration, type MatchFixtureOptions, type MatchableRequest, Matcher, type MatcherKind, type MobileBackendOptions, type MobileElementKind, type MobileElementMatch, type MobileElementRef, type MobileHandle, type MobileScenario, type MobileSpecification, type MobileSpecificationOptions, type MobileVisitor, type MockDatePort, type MockPort, Orchestrator, PageResult, type PostgresOptions, type RedisOptions, ResponseAccessor, type ScreenNode, ScreenResult, type ServeOptions, type ServerPort, type ServerResponse, type ServiceHandle, type ServiceRecord, type SpecDocument, type SpecEnvToken, type SpecFileAssertion, type SpecFixture, type SpecKind, type SpecRun, type SpecServeEntry, type SpecStream, type SpecificationConfig, type SpecificationMode, type SqliteOptions, TableAccessor, TextAccessor, type TextFilter, type VisitScenario, type Visitor, type WebsiteBackendOptions, type WebsiteHandle, type WebsiteSpecification, type WebsiteSpecificationOptions, anthropic, banner, button, complementary, content, contentinfo, defineContract, defineContracts, field, findContainersByLabel, form, heading, http, inspectContainer, link, main, match, mockOf, mockOfDate, navigation, openai, postgres, redis, region, removeContainers, search, specification, sqlite, testId, text, within };
2540
+ export { type ApiHandle, type ApiSpecification, type ApiSpecificationOptions, BaseResult, type BrowserConsoleMessage, type BrowserLinkElement, type BrowserMetaElement, type BrowserOpenOptions, type BrowserPage, type BrowserPort, type CaptureScope, type CliEnv, type CliHandle, type CliInput, type CliOutput, type CliPort, CliResult, type CliSpecification, type CliSpecificationOptions, ContainerAccessor, type ContainerPort, type Contract, type ContractInput, type ContractRequest, type ContractResponder, type ContractResponse, type ContractResponseValue, type Contracts, type DatabaseKeys, type DatabasePort, type DeviceOpenOptions, type DevicePort, type DeviceScreen, type DeviceTimeouts, DirectoryAccessor, type DockerSpecConfig, type ElementKind, type ElementMatch, type ElementOptions, type ElementRef, type ExecOptions, FetchResult, type FileAccessor, FilesystemAccessor, type HonoApp, type HttpContractFilter, type HttpResponseInit, HttpResult, type IsolationStrategy, type JobHandle, type JobsHandle, type JobsSpecification, type JobsSpecificationOptions, JsonAccessor, type LandmarkKind, type LiterateRunFlags, type LiterateServeRegistration, type MatchFixtureOptions, type MatchableRequest, Matcher, type MatcherKind, type MobileBackendOptions, type MobileElementKind, type MobileElementMatch, type MobileElementRef, type MobileHandle, type MobileScenario, type MobileSpecification, type MobileSpecificationOptions, type MobileVisitor, type MockDatePort, type MockPort, Orchestrator, PageResult, type PostgresOptions, type RedisOptions, ResponseAccessor, type ScreenNode, ScreenResult, type ServeOptions, type ServerPort, type ServerResponse, type ServiceHandle, type ServiceRecord, type SpecDocument, type SpecEnvToken, type SpecFileAssertion, type SpecFixture, type SpecKind, type SpecRun, type SpecServeEntry, type SpecStream, type SpecificationConfig, type SpecificationMode, type SqliteOptions, TableAccessor, TextAccessor, type TextFilter, type VisitScenario, type Visitor, type WebsiteBackendOptions, type WebsiteHandle, type WebsiteSpecification, type WebsiteSpecificationOptions, anthropic, banner, button, complementary, content, contentinfo, defineContract, defineContracts, field, findContainersByLabel, form, heading, http, inspectContainer, link, main, match, mockOf, mockOfDate, navigation, openai, postgres, redis, region, removeContainers, search, specification, sqlite, testId, text, within };
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { a as copyPlan, c as GROUND_EXPECTED, d as GROUND_SEEDS, i as match, n as Matcher, t as CaptureScope, u as GROUND_REQUESTS } from "./match.js";
2
- import { C as diffDirectories, D as formatStartupReport, E as formatResponseDiff, F as structuralEquals, I as structuralSubset, L as textEquals, M as mergeTextPreservingPlaceholders, O as formatStdoutDiff, P as renderExpected, S as DirectoryAccessor, T as formatDirectoryDiff, _ as JsonAccessor, b as getCallerDir, c as shouldUpdateSnapshots, d as BaseResult, f as validateDatabaseOption, g as TableAccessor, h as removeContainers, i as runSpecDocument, j as mergePreservingPlaceholders, k as formatTableDiff, l as ContainerAccessor, m as inspectContainer, n as SQLITE_TEMPLATE_DIR, o as ServeAdapter, p as findContainersByLabel, s as UPDATE_HINT, u as CliResult, v as TextAccessor, w as walkDirectory, x as FilesystemAccessor, y as text } from "./artifacts.js";
2
+ import { A as formatResponseDiff, B as structuralSubset, C as text, D as diffDirectories, E as DirectoryAccessor, F as mergePreservingPlaceholders, I as mergeTextPreservingPlaceholders, M as formatStdoutDiff, N as formatTableDiff, O as walkDirectory, R as renderExpected, S as TextAccessor, T as FilesystemAccessor, V as textEquals, _ as findContainersByLabel, b as TableAccessor, c as shouldUpdateSnapshots, d as BaseResult, f as validateDatabaseOption, g as resolveRoot, h as resolveCommand, i as runSpecDocument, j as formatStartupReport, k as formatDirectoryDiff, l as ContainerAccessor, m as expandWorkdir, n as SQLITE_TEMPLATE_DIR, o as ServeAdapter, p as discoverRoot, s as UPDATE_HINT, u as CliResult, v as inspectContainer, w as getCallerDir, x as JsonAccessor, y as removeContainers, z as structuralEquals } from "./artifacts.js";
3
3
  import { a as isContract, i as defineContracts, n as contractsOf, o as isContracts, r as defineContract, t as ContractQueue } from "./queue.js";
4
4
  import { closeSync, copyFileSync, cpSync, existsSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
5
- import { dirname, isAbsolute, join, relative, resolve } from "node:path";
5
+ import { dirname, join, relative, resolve } from "node:path";
6
6
  import { parse } from "yaml";
7
7
  import { execFile, execSync, spawn, spawnSync } from "node:child_process";
8
8
  import { Client } from "pg";
@@ -1092,8 +1092,10 @@ var SpecificationBuilder = class {
1092
1092
  * Set environment variables for the command process. Merged on top of process.env.
1093
1093
  * Use `null` to unset a variable. Multiple calls merge.
1094
1094
  *
1095
- * The token `$WORKDIR` (in any value) is replaced with the actual working
1096
- * directory at run-time — useful for tests that need a fully isolated `HOME`.
1095
+ * The token `$WORKDIR` (in any value) is replaced at run-time with the
1096
+ * spec's working directory in its resolved form — the same string
1097
+ * `{{workdir}}` asserts against — so a path handed to the child comes back
1098
+ * matchable. Useful for tests that need a fully isolated `HOME`.
1097
1099
  *
1098
1100
  * @example
1099
1101
  * cli.env({ HOME: "$WORKDIR", TZ: "UTC" }).exec("status");
@@ -1346,14 +1348,8 @@ var SpecificationBuilder = class {
1346
1348
  }
1347
1349
  }
1348
1350
  resolveEnv(workDir) {
1349
- const keys = Object.keys(this.commandEnv);
1350
- if (keys.length === 0) return;
1351
- const resolved = {};
1352
- for (const key of keys) {
1353
- const value = this.commandEnv[key];
1354
- resolved[key] = typeof value === "string" ? value.replace(/\$WORKDIR/g, workDir) : value;
1355
- }
1356
- return resolved;
1351
+ if (Object.keys(this.commandEnv).length === 0) return;
1352
+ return expandWorkdir(this.commandEnv, workDir);
1357
1353
  }
1358
1354
  prepareWorkDir() {
1359
1355
  return mkdtempSync(resolve(tmpdir(), "spec-command-"));
@@ -1890,64 +1886,6 @@ var Orchestrator = class {
1890
1886
  }
1891
1887
  };
1892
1888
  //#endregion
1893
- //#region src/core/specification/shared/resolve.ts
1894
- /** What makes a directory a project root — a manifest, or a test stack. */
1895
- const ROOT_MARKERS = ["package.json", "docker/compose.test.yaml"];
1896
- /**
1897
- * The NEAREST ancestor of `startDir` (itself included) carrying a root marker,
1898
- * `undefined` when none does.
1899
- *
1900
- * Both markers are probed at each ancestor, in ONE walk. Two walks — the
1901
- * compose file all the way up, then `package.json` all the way up — made the
1902
- * further directory win: in a workspace, a `docker/compose.test.yaml` at the
1903
- * repository root outranked the `package.json` of the very package being
1904
- * tested, and every path the runner resolved was measured from the wrong unit.
1905
- * The unit is the nearest thing that declares itself a project.
1906
- *
1907
- * The existence probe is injected so the lint layer can pass its cached one
1908
- * (A9's `a9w-redundant-root` derives the same root, and must derive it the
1909
- * same way).
1910
- */
1911
- function findRoot(startDir, exists) {
1912
- let dir = startDir;
1913
- for (;;) {
1914
- if (ROOT_MARKERS.some((marker) => exists(resolve(dir, marker)))) return dir;
1915
- const parent = dirname(dir);
1916
- if (parent === dir) return;
1917
- dir = parent;
1918
- }
1919
- }
1920
- /**
1921
- * Auto-discover the project root from a starting directory (CONVENTIONS A9):
1922
- * walk up to the first directory that carries `package.json` or
1923
- * `docker/compose.test.yaml`; if none does, the starting directory itself.
1924
- */
1925
- function discoverRoot(startDir) {
1926
- return findRoot(startDir, existsSync) ?? startDir;
1927
- }
1928
- /**
1929
- * Resolve the project root for a specification. An explicit `root` option is
1930
- * an override (resolved from the caller's directory when relative); when
1931
- * absent, the root is auto-discovered by walking up from the calling
1932
- * specification file.
1933
- */
1934
- function resolveRoot(root, callerDir) {
1935
- if (!root) return discoverRoot(callerDir);
1936
- if (isAbsolute(root)) return root;
1937
- return resolve(callerDir, root);
1938
- }
1939
- /**
1940
- * Resolve a command — checks node_modules/.bin, then treats as absolute/PATH.
1941
- */
1942
- function resolveCommand(command, root) {
1943
- if (isAbsolute(command)) return command;
1944
- const binPath = resolve(root, "node_modules/.bin", command);
1945
- if (existsSync(binPath)) return binPath;
1946
- const cwdBinPath = resolve(process.cwd(), "node_modules/.bin", command);
1947
- if (existsSync(cwdBinPath)) return cwdBinPath;
1948
- return command;
1949
- }
1950
- //#endregion
1951
1889
  //#region src/core/specification/shared/services.ts
1952
1890
  function getWorkerId() {
1953
1891
  return process.env.VITEST_POOL_ID ?? "0";
@@ -2615,6 +2553,7 @@ async function startMobile(options) {
2615
2553
  const { AppiumAdapter } = await import("./appium.adapter.js");
2616
2554
  device = new AppiumAdapter({
2617
2555
  serverUrl: appium.url,
2556
+ timeouts: options.timeouts,
2618
2557
  udid
2619
2558
  });
2620
2559
  }
package/dist/queue.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { t as CaptureScope } from "./match.js";
2
- import { A as hasPlaceholders, L as textEquals, N as placeholderPatternSource } from "./artifacts.js";
2
+ import { L as placeholderPatternSource, P as hasPlaceholders, V as textEquals } from "./artifacts.js";
3
3
  //#region src/core/contracts/contract.ts
4
4
  /**
5
5
  * Declare one contract. Identity function — its value is the enforced shape
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jterrazz/test",
3
- "version": "14.1.0",
3
+ "version": "14.3.0",
4
4
  "description": "Declarative testing framework for HTTP APIs, CLIs, and background jobs — one entry point, real infrastructure, golden-file assertions, plus an oxlint convention plugin.",
5
5
  "keywords": [
6
6
  "api-testing",