@jterrazz/test 13.0.1 → 14.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/index.d.ts CHANGED
@@ -6,9 +6,9 @@ import { DeepMockProxy } from "vitest-mock-extended";
6
6
  * by every assertion of a single spec execution.
7
7
  *
8
8
  * Matchers are used inside `toMatchRows` rows and any structural comparison;
9
- * their file-side twins are the `{{placeholder}}` forms in `expected/*.http`
10
- * fixtures (body AND headers), `expected/*.json` files, and text snapshots
11
- * (`expected/*.txt`). One grammar, one vocabulary (CONVENTIONS D4).
9
+ * their file-side twins are the `{{placeholder}}` forms in `_expected/*.http`
10
+ * fixtures (body AND headers), `_expected/*.json` files, and text snapshots
11
+ * (`_expected/*.txt`). One grammar, one vocabulary (CONVENTIONS D4).
12
12
  */
13
13
  /** The frozen token vocabulary (CONVENTIONS D4) plus the code-only kinds. */
14
14
  type MatcherKind = 'any' | 'base64' | 'date' | 'duration' | 'email' | 'float' | 'hex' | 'includes' | 'int' | 'ip' | 'iso8601' | 'number' | 'path' | 'port' | 'ref' | 'regex' | 'semver' | 'sha' | 'string' | 'time' | 'ulid' | 'url' | 'uuid' | 'workdir';
@@ -136,7 +136,7 @@ declare class CaptureScope {
136
136
  *
137
137
  * Assertions go through `expect()` (async — they walk the disk):
138
138
  * `await expect(result.directory('out')).toMatch('scaffold/out')`
139
- * compares the tree against the fixture directory `expected/<name>/`.
139
+ * compares the tree against the fixture directory `_expected/<name>/`.
140
140
  */
141
141
  declare class DirectoryAccessor {
142
142
  /** @internal Ref-capture scope shared by the current spec execution. */
@@ -159,7 +159,7 @@ declare class DirectoryAccessor {
159
159
  *
160
160
  * Assertions go through `expect()` (async — they walk the disk):
161
161
  * `await expect(result.filesystem).toMatch('scaffolded')` compares the
162
- * tree against the fixture directory `expected/<name>/`.
162
+ * tree against the fixture directory `_expected/<name>/`.
163
163
  */
164
164
  declare class FilesystemAccessor {
165
165
  /** @internal Ref-capture scope shared by the current spec execution. */
@@ -189,7 +189,7 @@ declare class FilesystemAccessor {
189
189
  * covered by the `{{token}}` grammar.
190
190
  *
191
191
  * Assertions go through `expect()`: `expect(result.json).toMatch('name.json')`
192
- * (deep-equal against `expected/<name>`, with `{{placeholder}}` support).
192
+ * (deep-equal against `_expected/<name>`, with `{{placeholder}}` support).
193
193
  */
194
194
  declare class JsonAccessor {
195
195
  /** @internal Ref-capture scope shared by the current spec execution. */
@@ -232,7 +232,7 @@ interface ServerPort {
232
232
  *
233
233
  * Assertions go through `expect()`:
234
234
  * `expect(result.response).toMatch('created.http')` compares status,
235
- * a subset of headers, and the JSON body against `expected/<name>` —
235
+ * a subset of headers, and the JSON body against `_expected/<name>` —
236
236
  * with `{{placeholder}}` support in all three.
237
237
  */
238
238
  declare class ResponseAccessor {
@@ -302,14 +302,14 @@ declare class TableAccessor {
302
302
  * grammar.
303
303
  *
304
304
  * Text operations are **closed** over the type: `.grep(pattern)` returns a
305
- * `TextAccessor` (not a bare string), preserving the `expected/`-resolution
305
+ * `TextAccessor` (not a bare string), preserving the `_expected/`-resolution
306
306
  * context and the `transform`, so results are chainable
307
307
  * (`result.stdout.grep(a).grep(b)`) and snapshot-able
308
308
  * (`expect(result.stdout.grep('users.ts')).toMatch('block.txt')`).
309
309
  *
310
310
  * Assertions go through `expect()` matchers: `expect(result.stdout).toContain(...)`
311
311
  * and `expect(result.stdout).toMatch('name.txt')` (resolved against
312
- * `expected/<name>`, flat — with `{{token}}` support, CONVENTIONS D4).
312
+ * `_expected/<name>`, flat — with `{{token}}` support, CONVENTIONS D4).
313
313
  */
314
314
  declare class TextAccessor {
315
315
  /** @internal Ref-capture scope shared by the current spec execution. */
@@ -349,7 +349,7 @@ declare class TextAccessor {
349
349
  *
350
350
  * ```typescript
351
351
  * const message = await catchMessage(() => expect(result.response).toMatch('wrong-body.http'));
352
- * expect(text(message)).toMatch('wrong-body-error.txt'); // resolves to expected/
352
+ * expect(text(message)).toMatch('wrong-body-error.txt'); // resolves to _expected/
353
353
  * ```
354
354
  *
355
355
  * ANSI is stripped before every comparison (the raw form stays on `.text`),
@@ -1240,7 +1240,7 @@ interface ApiSpecification<DatabaseKey extends string = string> {
1240
1240
  headers: (headers: Record<string, string>) => ApiSpecification<DatabaseKey>;
1241
1241
  /** Declare outgoing calls — a contract, a list, a composite, or an inline request + response pair. */
1242
1242
  intercept: InterceptMethod<ApiSpecification<DatabaseKey>>;
1243
- /** Queue a SQL seed file from `seeds/` to run before the action. */
1243
+ /** Queue a SQL seed file from `_seeds/` to run before the action. */
1244
1244
  seed: (file: string, options?: {
1245
1245
  database?: DatabaseKey;
1246
1246
  }) => ApiSpecification<DatabaseKey>;
@@ -1252,7 +1252,7 @@ interface ApiSpecification<DatabaseKey extends string = string> {
1252
1252
  post: (path: string, body?: unknown) => Promise<HttpResult>;
1253
1253
  /** Send a PUT request (optional inline JSON body) and resolve with the result. */
1254
1254
  put: (path: string, body?: unknown) => Promise<HttpResult>;
1255
- /** Send the complete request described by `requests/<file>` (.http format). */
1255
+ /** Send the complete request described by `_requests/<file>` (.http format). */
1256
1256
  request: (file: string) => Promise<HttpResult>;
1257
1257
  }
1258
1258
  /**
@@ -1262,7 +1262,7 @@ interface ApiSpecification<DatabaseKey extends string = string> {
1262
1262
  interface JobsSpecification<DatabaseKey extends string = string> {
1263
1263
  /** Declare outgoing calls — a contract, a list, a composite, or an inline request + response pair. */
1264
1264
  intercept: InterceptMethod<JobsSpecification<DatabaseKey>>;
1265
- /** Queue a SQL seed file from `seeds/` to run before the action. */
1265
+ /** Queue a SQL seed file from `_seeds/` to run before the action. */
1266
1266
  seed: (file: string, options?: {
1267
1267
  database?: DatabaseKey;
1268
1268
  }) => JobsSpecification<DatabaseKey>;
@@ -1285,7 +1285,7 @@ interface CliSpecification<DatabaseKey extends string = string> {
1285
1285
  * directory (or file) is copied under its own name. Chained calls layer.
1286
1286
  */
1287
1287
  fixture: (path: string) => CliSpecification<DatabaseKey>;
1288
- /** Queue a SQL seed file from `seeds/` to run against a database before the action. */
1288
+ /** Queue a SQL seed file from `_seeds/` to run against a database before the action. */
1289
1289
  seed: (file: string, options?: {
1290
1290
  database?: DatabaseKey;
1291
1291
  }) => CliSpecification<DatabaseKey>;
@@ -2454,7 +2454,7 @@ declare module 'vitest' {
2454
2454
  toBeRunning: T extends ContainerAccessor ? () => Promise<void> : never;
2455
2455
  /**
2456
2456
  * On `@jterrazz/test` accessors: assert the subject matches a fixture
2457
- * file under `expected/<name>` (flat — a slash creates a subfolder;
2457
+ * file under `_expected/<name>` (flat — a slash creates a subfolder;
2458
2458
  * `.http` format for `result.response`). Async for filesystem/directory
2459
2459
  * subjects (tree compare on disk). Other subjects keep vitest-native
2460
2460
  * `toMatch` semantics (string substring / regexp).
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as copyPlan, i as match, n as Matcher, t as CaptureScope } from "./match.js";
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
2
  import { C as formatResponseDiff, E as formatTableDiff, M as structuralEquals, N as structuralSubset, O as mergePreservingPlaceholders, P as textEquals, S as formatDirectoryDiff, T as formatStdoutDiff, _ as getCallerDir, a as shouldUpdateSnapshots, b as diffDirectories, c as BaseResult, d as inspectContainer, f as removeContainers, g as text, h as TextAccessor, i as UPDATE_HINT, j as renderExpected, k as mergeTextPreservingPlaceholders, l as validateDatabaseOption, m as JsonAccessor, o as ContainerAccessor, p as TableAccessor, r as ServeAdapter, s as CliResult, t as runSpecDocument, u as findContainersByLabel, v as FilesystemAccessor, w as formatStartupReport, x as walkDirectory, y as DirectoryAccessor } from "./literate.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, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
@@ -450,7 +450,7 @@ function parseHeaders(headerLines, fileName) {
450
450
  }
451
451
  return headers;
452
452
  }
453
- /** Parse a `requests/*.http` file. First line must be `METHOD /path`. */
453
+ /** Parse a `_requests/*.http` file. First line must be `METHOD /path`. */
454
454
  function parseRequestFile(content, fileName) {
455
455
  const { body, headerLines, startLine } = splitSections(content);
456
456
  const start = /^(?<method>[A-Z]+)\s+(?<path>\S+)$/.exec(startLine.trim());
@@ -526,7 +526,7 @@ function isJsonLike(text) {
526
526
  *
527
527
  * Assertions go through `expect()`:
528
528
  * `expect(result.response).toMatch('created.http')` compares status,
529
- * a subset of headers, and the JSON body against `expected/<name>` —
529
+ * a subset of headers, and the JSON body against `_expected/<name>` —
530
530
  * with `{{placeholder}}` support in all three.
531
531
  */
532
532
  var ResponseAccessor = class {
@@ -577,36 +577,36 @@ function formatJson(value) {
577
577
  }
578
578
  function matchStreamFile(accessor, name, frozen) {
579
579
  requireExtension(name, "stream");
580
- const filePath = resolve(accessor.testDir, "expected", name);
580
+ const filePath = resolve(accessor.testDir, GROUND_EXPECTED, name);
581
581
  const actual = accessor.comparableText;
582
582
  if (shouldUpdateSnapshots() && !frozen) {
583
583
  const merged = mergeTextPreservingPlaceholders(existsSync(filePath) ? readFileSync(filePath, "utf8") : null, actual, accessor.captures);
584
584
  mkdirSync(dirname(filePath), { recursive: true });
585
585
  writeFileSync(filePath, merged);
586
- return PASS(`updated expected/${name}`);
586
+ return PASS(`updated ${GROUND_EXPECTED}/${name}`);
587
587
  }
588
588
  if (!existsSync(filePath)) return FAIL(`${accessor.streamName} fixture "${name}" does not exist at ${filePath}.\n${UPDATE_HINT}`);
589
589
  const expected = readFileSync(filePath, "utf8");
590
- if (textEquals(expected, actual, accessor.captures)) return PASS(`expected ${accessor.streamName} not to match expected/${name}`);
590
+ if (textEquals(expected, actual, accessor.captures)) return PASS(`expected ${accessor.streamName} not to match ${GROUND_EXPECTED}/${name}`);
591
591
  return FAIL(formatStdoutDiff(name, expected, actual, { equals: (expectedLine, actualLine) => textEquals(expectedLine, actualLine, new CaptureScope(accessor.captures.workdir)) }));
592
592
  }
593
593
  function matchJsonFile(accessor, name, frozen) {
594
594
  requireExtension(name, "json");
595
- const filePath = resolve(accessor.testDir, "expected", name);
595
+ const filePath = resolve(accessor.testDir, GROUND_EXPECTED, name);
596
596
  const actual = accessor.value;
597
597
  if (shouldUpdateSnapshots() && !frozen) {
598
598
  const merged = existsSync(filePath) ? mergePreservingPlaceholders(JSON.parse(readFileSync(filePath, "utf8")), actual, accessor.captures.workdir) : mergePreservingPlaceholders(null, actual, accessor.captures.workdir);
599
599
  mkdirSync(dirname(filePath), { recursive: true });
600
600
  writeFileSync(filePath, formatJson(merged));
601
- return PASS(`updated expected/${name}`);
601
+ return PASS(`updated ${GROUND_EXPECTED}/${name}`);
602
602
  }
603
603
  if (!existsSync(filePath)) return FAIL(`JSON fixture "${name}" does not exist at ${filePath}.\n${UPDATE_HINT}`);
604
604
  const expected = JSON.parse(readFileSync(filePath, "utf8"));
605
- if (structuralEquals(expected, actual, accessor.captures)) return PASS(`expected JSON not to match expected/${name}`);
605
+ if (structuralEquals(expected, actual, accessor.captures)) return PASS(`expected JSON not to match ${GROUND_EXPECTED}/${name}`);
606
606
  return FAIL(formatResponseDiff(name, renderExpected(expected), actual));
607
607
  }
608
608
  /**
609
- * Build the updated `expected/*.http` fixture content from the previous
609
+ * Build the updated `_expected/*.http` fixture content from the previous
610
610
  * fixture and the actual response (CONVENTIONS D5). Headers are the
611
611
  * INTERSECTION with the actual response: placeholders still matching are
612
612
  * preserved, stale values are replaced, headers absent from the actual
@@ -633,7 +633,7 @@ function buildUpdatedResponse(previous, actual, workdir) {
633
633
  };
634
634
  }
635
635
  /**
636
- * Compare a parsed `expected/*.http` fixture against an actual response.
636
+ * Compare a parsed `_expected/*.http` fixture against an actual response.
637
637
  * Returns the failure message, or null when everything matches.
638
638
  *
639
639
  * @internal Exported for unit tests.
@@ -649,19 +649,19 @@ function compareResponse(name, expected, actual, scope) {
649
649
  }
650
650
  function matchResponseFile(accessor, name, frozen) {
651
651
  requireExtension(name, "response");
652
- const filePath = resolve(accessor.testDir, "expected", name);
652
+ const filePath = resolve(accessor.testDir, GROUND_EXPECTED, name);
653
653
  if (shouldUpdateSnapshots() && !frozen) {
654
- const updated = buildUpdatedResponse(existsSync(filePath) ? parseResponseFile(readFileSync(filePath, "utf8"), `expected/${name}`) : null, accessor, accessor.captures.workdir);
654
+ const updated = buildUpdatedResponse(existsSync(filePath) ? parseResponseFile(readFileSync(filePath, "utf8"), `${GROUND_EXPECTED}/${name}`) : null, accessor, accessor.captures.workdir);
655
655
  mkdirSync(dirname(filePath), { recursive: true });
656
656
  writeFileSync(filePath, serializeResponseFile(updated));
657
- return PASS(`updated expected/${name}`);
657
+ return PASS(`updated ${GROUND_EXPECTED}/${name}`);
658
658
  }
659
659
  if (!existsSync(filePath)) return FAIL(`Response fixture "${name}" does not exist at ${filePath}.\n${UPDATE_HINT}`);
660
- const failure = compareResponse(name, parseResponseFile(readFileSync(filePath, "utf8"), `expected/${name}`), accessor, accessor.captures);
661
- return failure === null ? PASS(`expected response not to match expected/${name}`) : FAIL(failure);
660
+ const failure = compareResponse(name, parseResponseFile(readFileSync(filePath, "utf8"), `${GROUND_EXPECTED}/${name}`), accessor, accessor.captures);
661
+ return failure === null ? PASS(`expected response not to match ${GROUND_EXPECTED}/${name}`) : FAIL(failure);
662
662
  }
663
663
  async function matchTreeFile(actualRoot, testDir, name, scope, frozen) {
664
- const fixtureDir = resolve(testDir, "expected", name);
664
+ const fixtureDir = resolve(testDir, GROUND_EXPECTED, name);
665
665
  if (shouldUpdateSnapshots() && !frozen) {
666
666
  const previousContents = /* @__PURE__ */ new Map();
667
667
  if (existsSync(fixtureDir)) for (const file of await walkDirectory(fixtureDir)) previousContents.set(file, readFileSync(resolve(fixtureDir, file), "utf8"));
@@ -677,11 +677,11 @@ async function matchTreeFile(actualRoot, testDir, name, scope, frozen) {
677
677
  const merged = mergeTextPreservingPlaceholders(previous, actual, scope);
678
678
  if (merged !== actual) writeFileSync(resolve(fixtureDir, file), merged);
679
679
  }
680
- return PASS(`updated expected/${name}/`);
680
+ return PASS(`updated ${GROUND_EXPECTED}/${name}/`);
681
681
  }
682
682
  if (!existsSync(fixtureDir)) return FAIL(`Directory fixture "${name}" does not exist at ${fixtureDir}.\n${UPDATE_HINT}`);
683
683
  const diff = await diffDirectories(fixtureDir, actualRoot, { scope });
684
- if (diff.added.length === 0 && diff.removed.length === 0 && diff.changed.length === 0) return PASS(`expected directory not to match expected/${name}/`);
684
+ if (diff.added.length === 0 && diff.removed.length === 0 && diff.changed.length === 0) return PASS(`expected directory not to match ${GROUND_EXPECTED}/${name}/`);
685
685
  return FAIL(formatDirectoryDiff(name, diff, "Run with TEST_UPDATE=1 to update the fixture."));
686
686
  }
687
687
  /**
@@ -1107,7 +1107,7 @@ var SpecificationBuilder = class {
1107
1107
  }
1108
1108
  /**
1109
1109
  * Set HTTP headers for the request. Multiple calls merge; chain headers
1110
- * win over headers from a `requests/*.http` file.
1110
+ * win over headers from a `_requests/*.http` file.
1111
1111
  *
1112
1112
  * @example
1113
1113
  * api.headers({ 'Accept-Language': 'fr' }).get("/articles");
@@ -1137,7 +1137,7 @@ var SpecificationBuilder = class {
1137
1137
  return this;
1138
1138
  }
1139
1139
  /**
1140
- * Send the complete request described by `requests/<file>` — first line
1140
+ * Send the complete request described by `_requests/<file>` — first line
1141
1141
  * `METHOD /path`, then headers until a blank line, then the body.
1142
1142
  * Headers set via `.headers()` merge on top of the file's headers.
1143
1143
  *
@@ -1321,7 +1321,7 @@ var SpecificationBuilder = class {
1321
1321
  if (!db) throw new Error(`seed() targets database "${entry.database}" but it was not found. Available: ${[...this.config.databases.keys()].join(", ")}`);
1322
1322
  } else db = this.config.database;
1323
1323
  if (!db) throw new Error("seed() requires a database adapter");
1324
- const sql = readFileSync(resolve(this.testDir, "seeds", entry.file), "utf8");
1324
+ const sql = readFileSync(resolve(this.testDir, GROUND_SEEDS, entry.file), "utf8");
1325
1325
  await db.seed(sql);
1326
1326
  }
1327
1327
  if (this.fixtures.length > 0 && workDir) for (const entry of this.fixtures) {
@@ -1363,7 +1363,7 @@ var SpecificationBuilder = class {
1363
1363
  let { body, method, path } = request;
1364
1364
  let fileHeaders;
1365
1365
  if (request.requestFile) {
1366
- const parsed = parseRequestFile(readFileSync(resolve(this.testDir, "requests", request.requestFile), "utf8"), `requests/${request.requestFile}`);
1366
+ const parsed = parseRequestFile(readFileSync(resolve(this.testDir, GROUND_REQUESTS, request.requestFile), "utf8"), `${GROUND_REQUESTS}/${request.requestFile}`);
1367
1367
  body = parsed.body;
1368
1368
  fileHeaders = parsed.headers;
1369
1369
  method = parsed.method;
package/dist/literate.js CHANGED
@@ -16,8 +16,8 @@ import { createServer } from "node:net";
16
16
  * - strings containing `{{placeholder}}` forms (file-side fixtures)
17
17
  *
18
18
  * One unified `{{token}}` grammar (CONVENTIONS D4) — the same vocabulary
19
- * works in `expected/*.http` (body and headers), `expected/*.json`, and
20
- * text snapshots (`expected/*.txt`).
19
+ * works in `_expected/*.http` (body and headers), `_expected/*.json`, and
20
+ * text snapshots (`_expected/*.txt`).
21
21
  *
22
22
  * Ref captures (`match.ref(name)` / `{{type#name}}`) are recorded in the
23
23
  * {@link CaptureScope} supplied by the caller.
@@ -232,7 +232,7 @@ function structuralSubset(expected, actual, scope) {
232
232
  }
233
233
  /**
234
234
  * Multi-line text comparison with `{{token}}` support — used by text
235
- * snapshots (`expected/*.txt`). Without placeholders this is strict equality.
235
+ * snapshots (`_expected/*.txt`). Without placeholders this is strict equality.
236
236
  */
237
237
  function textEquals(expected, actual, scope) {
238
238
  if (!hasPlaceholders(expected)) return expected === actual;
@@ -592,7 +592,7 @@ async function diffDirectories(expectedRoot, actualRoot, options = {}) {
592
592
  *
593
593
  * Assertions go through `expect()` (async — they walk the disk):
594
594
  * `await expect(result.directory('out')).toMatch('scaffold/out')`
595
- * compares the tree against the fixture directory `expected/<name>/`.
595
+ * compares the tree against the fixture directory `_expected/<name>/`.
596
596
  */
597
597
  var DirectoryAccessor = class {
598
598
  /** @internal Ref-capture scope shared by the current spec execution. */
@@ -619,7 +619,7 @@ var DirectoryAccessor = class {
619
619
  *
620
620
  * Assertions go through `expect()` (async — they walk the disk):
621
621
  * `await expect(result.filesystem).toMatch('scaffolded')` compares the
622
- * tree against the fixture directory `expected/<name>/`.
622
+ * tree against the fixture directory `_expected/<name>/`.
623
623
  */
624
624
  var FilesystemAccessor = class {
625
625
  /** @internal Ref-capture scope shared by the current spec execution. */
@@ -691,8 +691,8 @@ function isFrameworkFrame(filePath, frameworkTree = FRAMEWORK_TREE) {
691
691
  }
692
692
  /**
693
693
  * Detect the directory of the first stack frame that lives outside this
694
- * package. Used to anchor fixture resolution (`seeds/`, `requests/`,
695
- * `expected/`, …) on the calling test / specification file.
694
+ * package. Used to anchor fixture resolution (`_seeds/`, `_requests/`,
695
+ * `_expected/`, …) on the calling test / specification file.
696
696
  *
697
697
  * @internal
698
698
  */
@@ -745,14 +745,14 @@ function stripAnsiCodes(value) {
745
745
  * grammar.
746
746
  *
747
747
  * Text operations are **closed** over the type: `.grep(pattern)` returns a
748
- * `TextAccessor` (not a bare string), preserving the `expected/`-resolution
748
+ * `TextAccessor` (not a bare string), preserving the `_expected/`-resolution
749
749
  * context and the `transform`, so results are chainable
750
750
  * (`result.stdout.grep(a).grep(b)`) and snapshot-able
751
751
  * (`expect(result.stdout.grep('users.ts')).toMatch('block.txt')`).
752
752
  *
753
753
  * Assertions go through `expect()` matchers: `expect(result.stdout).toContain(...)`
754
754
  * and `expect(result.stdout).toMatch('name.txt')` (resolved against
755
- * `expected/<name>`, flat — with `{{token}}` support, CONVENTIONS D4).
755
+ * `_expected/<name>`, flat — with `{{token}}` support, CONVENTIONS D4).
756
756
  */
757
757
  var TextAccessor = class TextAccessor {
758
758
  /** @internal Ref-capture scope shared by the current spec execution. */
@@ -807,7 +807,7 @@ var TextAccessor = class TextAccessor {
807
807
  *
808
808
  * ```typescript
809
809
  * const message = await catchMessage(() => expect(result.response).toMatch('wrong-body.http'));
810
- * expect(text(message)).toMatch('wrong-body-error.txt'); // resolves to expected/
810
+ * expect(text(message)).toMatch('wrong-body-error.txt'); // resolves to _expected/
811
811
  * ```
812
812
  *
813
813
  * ANSI is stripped before every comparison (the raw form stays on `.text`),
@@ -832,7 +832,7 @@ function text(value) {
832
832
  * covered by the `{{token}}` grammar.
833
833
  *
834
834
  * Assertions go through `expect()`: `expect(result.json).toMatch('name.json')`
835
- * (deep-equal against `expected/<name>`, with `{{placeholder}}` support).
835
+ * (deep-equal against `_expected/<name>`, with `{{placeholder}}` support).
836
836
  */
837
837
  var JsonAccessor = class {
838
838
  /** @internal Ref-capture scope shared by the current spec execution. */
package/dist/match.js CHANGED
@@ -1,14 +1,51 @@
1
1
  import { basename, dirname, resolve } from "node:path";
2
+ //#region src/core/specification/shared/ground.ts
3
+ /**
4
+ * The GROUND a row of specs stands on — the naming law of a spec tree.
5
+ *
6
+ * In a row of specs, a MEMBER is a spec: `<aspect>.test.ts`, `<case>.spec.yaml`,
7
+ * `<name>.specification.ts`, and the facet/domain folders that hold them.
8
+ * Everything else in that row is what the members STAND ON — inert material the
9
+ * framework resolves BY PATH, never imported, never executed — and it carries a
10
+ * LEADING UNDERSCORE. A spec's own folder never does.
11
+ *
12
+ * Four names, and only four: `_fixtures/` (file state), `_expected/` (goldens),
13
+ * `_requests/` (inputs), `_seeds/` (database state). `contracts/` is NOT ground —
14
+ * a contract is TypeScript a spec imports, behaviour composed with the spec's own,
15
+ * so it stays a member. `docker/` is not ground either: it sits at the PROJECT
16
+ * root beside `package.json`, outside any row of specs.
17
+ *
18
+ * This module is the single home of those names. Every resolver, matcher, lint
19
+ * rule and checker pass reads them from here, so the runtime and the static
20
+ * channels cannot disagree about where a spec's ground lives.
21
+ */
22
+ /** Goldens: every expected fixture, flat, extension included. */
23
+ const GROUND_EXPECTED = "_expected";
24
+ /** File state copied into a working directory by `.fixture()`. */
25
+ const GROUND_FIXTURES = "_fixtures";
26
+ /** Inputs: complete `.http` request documents. */
27
+ const GROUND_REQUESTS = "_requests";
28
+ /** Database state: `.sql` fragments loaded by `.seed()`. */
29
+ const GROUND_SEEDS = "_seeds";
30
+ /** The four ground directory names, in the order they read. */
31
+ const GROUND_DIRS = [
32
+ GROUND_EXPECTED,
33
+ GROUND_FIXTURES,
34
+ GROUND_REQUESTS,
35
+ GROUND_SEEDS
36
+ ];
37
+ //#endregion
2
38
  //#region src/core/specification/shared/fixtures.ts
3
39
  /**
4
40
  * Fixture path resolution + copy semantics for the `cli` facet's `.fixture()`.
5
41
  *
6
42
  * A fixture path is one of two shapes:
7
43
  *
8
- * - `$FIXTURES/<rest>` — the shared pool at `<specs-root>/fixtures/<rest>`,
44
+ * - `$FIXTURES/<rest>` — the shared pool at `<specs-root>/_fixtures/<rest>`,
9
45
  * where `<specs-root>` is the nearest ancestor directory named `specs`.
10
- * - `<path>` (no marker) — feature-local, at `<test-dir>/fixtures/<path>`.
46
+ * - `<path>` (no marker) — feature-local, at `<test-dir>/_fixtures/<path>`.
11
47
  *
48
+ * The leading underscore is the ground rule of a spec tree ({@link GROUND_FIXTURES}).
12
49
  * Any other `$`-prefixed marker is a usage error (listed against the known
13
50
  * markers). Copy semantics mirror rsync's trailing-slash rule (see
14
51
  * {@link copyPlan}).
@@ -32,7 +69,7 @@ function discoverSpecsRoot(startDir) {
32
69
  if (parent === dir) break;
33
70
  dir = parent;
34
71
  }
35
- 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).`);
72
+ throw new Error(`.fixture(): the $FIXTURES marker resolves to <specs-root>/${GROUND_FIXTURES}, but no directory named "specs" was found walking up from ${startDir}. Move the specification under a specs/ directory, or use a feature-local ${GROUND_FIXTURES}/ path (no $FIXTURES marker).`);
36
73
  }
37
74
  /**
38
75
  * Resolve the absolute source path of a fixture, applying marker resolution.
@@ -43,13 +80,13 @@ function resolveFixtureSource(path, testDir) {
43
80
  const clean = path.replace(/\/+$/, "");
44
81
  if (clean === "$FIXTURES" || path.startsWith("$FIXTURES/")) {
45
82
  const rest = clean.slice(9).replace(/^\/+/, "");
46
- return resolve(discoverSpecsRoot(testDir), "fixtures", rest);
83
+ return resolve(discoverSpecsRoot(testDir), GROUND_FIXTURES, rest);
47
84
  }
48
85
  if (path.startsWith("$")) {
49
86
  const marker = clean.split("/")[0];
50
- 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/).`);
87
+ 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>/${GROUND_FIXTURES}/).`);
51
88
  }
52
- return resolve(testDir, "fixtures", clean);
89
+ return resolve(testDir, GROUND_FIXTURES, clean);
53
90
  }
54
91
  /**
55
92
  * Compute the source + destination for copying a fixture into the working
@@ -233,4 +270,4 @@ var CaptureScope = class {
233
270
  }
234
271
  };
235
272
  //#endregion
236
- export { copyPlan as a, match as i, Matcher as n, resolveFixtureSource as o, TOKEN_KINDS as r, CaptureScope as t };
273
+ export { copyPlan as a, GROUND_EXPECTED as c, GROUND_SEEDS as d, match as i, GROUND_FIXTURES as l, Matcher as n, resolveFixtureSource as o, TOKEN_KINDS as r, GROUND_DIRS as s, CaptureScope as t, GROUND_REQUESTS as u };