@jterrazz/test 11.0.0 → 12.0.1

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/README.md CHANGED
@@ -247,7 +247,7 @@ The handle destructures to `{ mobile, cleanup, udid }` (plus `backendUrl` with `
247
247
 
248
248
  ### Root auto-discovery
249
249
 
250
- When `root` is absent, the framework walks up from the specification file to the first directory containing `docker/compose.test.yaml`, else the first containing `package.json`. Pass `root` only when the convention does not fit. `root` is strictly the **project root** (compose detection + local-bin resolution, or the cwd of a `specification.website()` server command) — it is not a fixtures root; `.fixture()` resolves its own paths.
250
+ When `root` is absent, the framework walks up from the specification file to the **nearest** directory carrying `package.json` or `docker/compose.test.yaml`. In a workspace that is the member being tested, not the repository root above it. Pass `root` only when the convention does not fit. `root` is strictly the **project root** (compose detection + local-bin resolution, or the cwd of a `specification.website()` server command) — it is not a fixtures root; `.fixture()` resolves its own paths.
251
251
 
252
252
  ## Builder API
253
253
 
package/dist/index.d.ts CHANGED
@@ -1412,7 +1412,15 @@ interface OrchestratorOptions {
1412
1412
  */
1413
1413
  services: Record<string, ServiceHandle>;
1414
1414
  mode: 'e2e' | 'integration';
1415
- root?: string;
1415
+ /**
1416
+ * The project root, already resolved. REQUIRED: the constructors derive it
1417
+ * by walking up from the calling specification file (CONVENTIONS A9), and
1418
+ * the orchestrator is handed the answer. Defaulting to `process.cwd()` here
1419
+ * meant one silent second opinion about what the project is — the runner
1420
+ * resolving from the caller, this from wherever the runner happened to be
1421
+ * launched, which in a workspace is rarely the same directory.
1422
+ */
1423
+ root: string;
1416
1424
  /** Compose project name — used for per-worker stack isolation. */
1417
1425
  projectName?: string;
1418
1426
  }
@@ -2016,8 +2024,9 @@ interface SqliteOptions {
2016
2024
  */
2017
2025
  init?: string;
2018
2026
  /**
2019
- * Path to a Prisma schema directory or file.
2020
- * The adapter runs `prisma db push` to create the template.
2027
+ * Path to a Prisma schema directory or file, resolved against the current
2028
+ * working directory. The adapter runs `prisma db push --schema <path>` to
2029
+ * create the template, so the schema needs no Prisma config at the cwd.
2021
2030
  * Mutually exclusive with `init`.
2022
2031
  */
2023
2032
  prismaSchema?: string;
@@ -2035,6 +2044,7 @@ declare class SqliteHandle implements DatabasePort, ServiceHandle {
2035
2044
  private workerDbPath;
2036
2045
  private initSql;
2037
2046
  private prismaSchema;
2047
+ private readonly options;
2038
2048
  constructor(options?: SqliteOptions);
2039
2049
  buildConnectionString(): string;
2040
2050
  createDatabaseAdapter(): DatabasePort;
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { i as match, n as Matcher, t as CaptureScope } from "./match.js";
2
2
  import { a as isContract, c as mergeTextPreservingPlaceholders, d as structuralSubset, f as textEquals, i as defineContracts, l as renderExpected, n as contractsOf, o as isContracts, r as defineContract, s as mergePreservingPlaceholders, t as ContractQueue, u as structuralEquals } from "./queue.js";
3
- import { closeSync, copyFileSync, cpSync, existsSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, realpathSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
4
- import { basename, dirname, isAbsolute, relative, resolve, sep } from "node:path";
3
+ import { closeSync, copyFileSync, cpSync, existsSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, realpathSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
4
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
5
  import { parse } from "yaml";
6
6
  import { execFile, execSync, spawn, spawnSync } from "node:child_process";
7
7
  import { Client } from "pg";
@@ -12,6 +12,7 @@ import { createServer } from "node:http";
12
12
  import { createServer as createServer$1 } from "node:net";
13
13
  import { promisify } from "node:util";
14
14
  import Database from "better-sqlite3";
15
+ import { createHash } from "node:crypto";
15
16
  import { mockDeep } from "vitest-mock-extended";
16
17
  import MockDatePackage from "mockdate";
17
18
  //#region src/core/specification/shared/registry.ts
@@ -790,13 +791,55 @@ var FilesystemAccessor = class {
790
791
  };
791
792
  //#endregion
792
793
  //#region src/core/specification/shared/caller.ts
794
+ /** Sibling module tests are callers, not internals (CONVENTIONS I2). */
795
+ const TEST_FILE = /\.test\.[cm]?[jt]s$/;
796
+ /** Where this very module sits inside the framework's SOURCE tree. */
797
+ const SOURCE_LOCATION = join("core", "specification", "shared");
798
+ /** The real path of `path`, or `path` itself when it cannot be resolved. */
799
+ function realPath(path) {
800
+ try {
801
+ return realpathSync(path);
802
+ } catch {
803
+ return path;
804
+ }
805
+ }
793
806
  /**
794
807
  * Directory of the running framework module itself. From the built package
795
808
  * this is `<package root>/dist`, whatever the resolution path — including a
796
809
  * `file:` link, where the real path carries neither `node_modules` nor
797
810
  * `/src/…` and would otherwise be mistaken for a caller frame.
798
811
  */
799
- const FRAMEWORK_DIR = dirname(fileURLToPath(import.meta.url));
812
+ const FRAMEWORK_DIR = realPath(dirname(fileURLToPath(import.meta.url)));
813
+ /**
814
+ * The directory holding the framework's OWN modules: `<package>/dist` for a
815
+ * built install (everything is bundled there), `<repo>/src` when the framework
816
+ * runs from source — this module's known location inside that tree is what
817
+ * tells the two apart.
818
+ */
819
+ const FRAMEWORK_TREE = FRAMEWORK_DIR.endsWith(`${sep}${SOURCE_LOCATION}`) ? resolve(FRAMEWORK_DIR, "../../..") : FRAMEWORK_DIR;
820
+ /** Is `path` inside `directory` (strictly — a directory is not inside itself)? */
821
+ function isInside(directory, path) {
822
+ const rest = relative(directory, path);
823
+ return rest !== "" && !rest.startsWith("..") && !isAbsolute(rest);
824
+ }
825
+ /**
826
+ * Is this stack frame a module of the framework itself, rather than of the code
827
+ * calling it?
828
+ *
829
+ * Answered by IDENTITY: the frame is inside the framework's own directory.
830
+ * Answering it by substring — `/src/core/`, `/src/integrations/`, `/src/vitest/`
831
+ * — was answering a different question, "does this path look like the framework's
832
+ * layout", and a consumer that happens to have its own `src/core/` matched it.
833
+ * Its files were then skipped as framework internals, and fixture resolution
834
+ * anchored on the wrong directory (or fell through to the cwd) with nothing
835
+ * reported.
836
+ *
837
+ * @internal
838
+ */
839
+ function isFrameworkFrame(filePath, frameworkTree = FRAMEWORK_TREE) {
840
+ if (TEST_FILE.test(filePath)) return false;
841
+ return isInside(realPath(frameworkTree), realPath(filePath));
842
+ }
800
843
  /**
801
844
  * Detect the directory of the first stack frame that lives outside this
802
845
  * package. Used to anchor fixture resolution (`seeds/`, `requests/`,
@@ -813,7 +856,7 @@ function getCallerDir() {
813
856
  if (!match?.groups?.filePath) continue;
814
857
  const filePath = match.groups.filePath;
815
858
  if (filePath.includes("node_modules")) continue;
816
- if ((filePath.includes("/src/core/") || filePath.includes("/src/integrations/") || filePath.includes("/src/vitest/") || resolve(filePath, "..") === FRAMEWORK_DIR) && !/\.test\.[cm]?[jt]s$/.test(filePath)) continue;
859
+ if (isFrameworkFrame(filePath)) continue;
817
860
  return resolve(filePath, "..");
818
861
  }
819
862
  return process.cwd();
@@ -2583,7 +2626,7 @@ var Orchestrator = class {
2583
2626
  constructor(options) {
2584
2627
  this.services = options.services;
2585
2628
  this.mode = options.mode;
2586
- this.root = options.root ?? process.cwd();
2629
+ this.root = options.root;
2587
2630
  this.projectName = options.projectName;
2588
2631
  }
2589
2632
  /**
@@ -2804,23 +2847,39 @@ var Orchestrator = class {
2804
2847
  };
2805
2848
  //#endregion
2806
2849
  //#region src/core/specification/shared/resolve.ts
2850
+ /** What makes a directory a project root — a manifest, or a test stack. */
2851
+ const ROOT_MARKERS = ["package.json", "docker/compose.test.yaml"];
2852
+ /**
2853
+ * The NEAREST ancestor of `startDir` (itself included) carrying a root marker,
2854
+ * `undefined` when none does.
2855
+ *
2856
+ * Both markers are probed at each ancestor, in ONE walk. Two walks — the
2857
+ * compose file all the way up, then `package.json` all the way up — made the
2858
+ * further directory win: in a workspace, a `docker/compose.test.yaml` at the
2859
+ * repository root outranked the `package.json` of the very package being
2860
+ * tested, and every path the runner resolved was measured from the wrong unit.
2861
+ * The unit is the nearest thing that declares itself a project.
2862
+ *
2863
+ * The existence probe is injected so the lint layer can pass its cached one
2864
+ * (A9's `a9w-redundant-root` derives the same root, and must derive it the
2865
+ * same way).
2866
+ */
2867
+ function findRoot(startDir, exists) {
2868
+ let dir = startDir;
2869
+ for (;;) {
2870
+ if (ROOT_MARKERS.some((marker) => exists(resolve(dir, marker)))) return dir;
2871
+ const parent = dirname(dir);
2872
+ if (parent === dir) return;
2873
+ dir = parent;
2874
+ }
2875
+ }
2807
2876
  /**
2808
2877
  * Auto-discover the project root from a starting directory (CONVENTIONS A9):
2809
- * walk up to the first directory containing `docker/compose.test.yaml`;
2810
- * if none, walk up to the first directory containing `package.json`;
2811
- * if none, the starting directory itself.
2878
+ * walk up to the first directory that carries `package.json` or
2879
+ * `docker/compose.test.yaml`; if none does, the starting directory itself.
2812
2880
  */
2813
2881
  function discoverRoot(startDir) {
2814
- for (const marker of ["docker/compose.test.yaml", "package.json"]) {
2815
- let dir = startDir;
2816
- for (;;) {
2817
- if (existsSync(resolve(dir, marker))) return dir;
2818
- const parent = dirname(dir);
2819
- if (parent === dir) break;
2820
- dir = parent;
2821
- }
2822
- }
2823
- return startDir;
2882
+ return findRoot(startDir, existsSync) ?? startDir;
2824
2883
  }
2825
2884
  /**
2826
2885
  * Resolve the project root for a specification. An explicit `root` option is
@@ -3861,6 +3920,64 @@ function isValidSqliteTemplate(path) {
3861
3920
  closeSync(fd);
3862
3921
  }
3863
3922
  }
3923
+ /** The invocation used when no `prismaSchema` is declared — Prisma discovers its own schema. */
3924
+ const PRISMA_PUSH = "npx prisma db push --force-reset";
3925
+ /**
3926
+ * The `prisma db push` command line for a `prismaSchema` option.
3927
+ *
3928
+ * The declared path is resolved against `process.cwd()` and handed to the CLI
3929
+ * as `--schema`. Without the flag Prisma falls back to its own discovery
3930
+ * (`prisma.config.ts` / `package.json#prisma` at the cwd), which silently
3931
+ * ignores the option: a repo whose config does not sit at the cwd then fails
3932
+ * with "Could not find Prisma Schema that is required for this command" — but
3933
+ * only on a machine without a cached template, so the divergence hides until CI.
3934
+ *
3935
+ * `null` (no option) keeps the bare invocation, discovery and all.
3936
+ */
3937
+ function prismaPushCommand(prismaSchema) {
3938
+ if (!prismaSchema) return PRISMA_PUSH;
3939
+ return `${PRISMA_PUSH} --schema ${JSON.stringify(resolve(prismaSchema))}`;
3940
+ }
3941
+ /** Every template file this package writes carries that prefix, then its key. */
3942
+ const TEMPLATE_PREFIX = "jterrazz-test-sqlite-template";
3943
+ /**
3944
+ * The schema source as bytes: a file's content, or every file under a
3945
+ * directory in a stable order (a Prisma `schema/` folder is a directory). Each
3946
+ * entry carries its own path, so moving a schema changes the fingerprint.
3947
+ * Anything unreadable degrades to a marker rather than throwing — the path is
3948
+ * already in the key, so two projects still never collide.
3949
+ */
3950
+ function schemaBytes(path) {
3951
+ try {
3952
+ if (statSync(path).isDirectory()) return readdirSync(path).sort().map((entry) => schemaBytes(join(path, entry))).join("\0");
3953
+ return `${path}\0${readFileSync(path, "utf8")}`;
3954
+ } catch {
3955
+ return `${path}\0<unreadable>`;
3956
+ }
3957
+ }
3958
+ /**
3959
+ * The name of the template database for a set of options — `<prefix>-<sha8>`,
3960
+ * the digest taken over the schema KIND, its resolved path and its content.
3961
+ *
3962
+ * The template lives in the machine-global OS tmpdir, so the name is the only
3963
+ * thing keeping two projects apart. A single fixed name meant the first project
3964
+ * to run on a machine built the template and every other project that day
3965
+ * silently inherited its tables — the file is header-valid, so the staleness
3966
+ * check accepts it and the schema never gets built. Keying on the schema makes
3967
+ * reuse mean "the same schema", which is what the cache was ever for.
3968
+ */
3969
+ function sqliteTemplateName(options = {}) {
3970
+ let fingerprint;
3971
+ if (options.prismaSchema) {
3972
+ const path = resolve(options.prismaSchema);
3973
+ fingerprint = `prisma\0${path}\0${schemaBytes(path)}`;
3974
+ } else if (options.init) {
3975
+ const path = resolve(options.init);
3976
+ fingerprint = `init\0${path}\0${schemaBytes(path)}`;
3977
+ } else fingerprint = "empty";
3978
+ const key = createHash("sha256").update(fingerprint).digest("hex").slice(0, 8);
3979
+ return `${TEMPLATE_PREFIX}-${key}.sqlite`;
3980
+ }
3864
3981
  var SqliteHandle = class {
3865
3982
  type = "sqlite";
3866
3983
  composeName = null;
@@ -3874,7 +3991,9 @@ var SqliteHandle = class {
3874
3991
  workerDbPath = "";
3875
3992
  initSql;
3876
3993
  prismaSchema;
3994
+ options;
3877
3995
  constructor(options = {}) {
3996
+ this.options = options;
3878
3997
  this.initSql = options.init ?? null;
3879
3998
  this.prismaSchema = options.prismaSchema ?? null;
3880
3999
  }
@@ -3886,7 +4005,7 @@ var SqliteHandle = class {
3886
4005
  }
3887
4006
  async healthcheck() {}
3888
4007
  async initialize() {
3889
- this.templatePath = resolve(tmpdir(), "jterrazz-test-sqlite-template.sqlite");
4008
+ this.templatePath = resolve(tmpdir(), sqliteTemplateName(this.options));
3890
4009
  const lockPath = `${this.templatePath}.lock`;
3891
4010
  if (existsSync(lockPath)) {
3892
4011
  const start = Date.now();
@@ -3906,7 +4025,7 @@ var SqliteHandle = class {
3906
4025
  writeFileSync(lockPath, process.pid.toString());
3907
4026
  if (this.prismaSchema) {
3908
4027
  const { execSync } = await import("node:child_process");
3909
- execSync("npx prisma db push --force-reset", {
4028
+ execSync(prismaPushCommand(this.prismaSchema), {
3910
4029
  env: {
3911
4030
  ...process.env,
3912
4031
  DATABASE_URL: `file:${this.templatePath}`,