@elench/testkit 0.1.69 → 0.1.72

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.
Files changed (54) hide show
  1. package/README.md +70 -10
  2. package/lib/app/doctor.mjs +1 -1
  3. package/lib/app/typecheck.mjs +59 -21
  4. package/lib/app/typecheck.test.mjs +24 -0
  5. package/lib/bundler/index.mjs +17 -17
  6. package/lib/cli/command-helpers.mjs +1 -1
  7. package/lib/cli/commands/doctor.mjs +1 -1
  8. package/lib/cli/commands/typecheck.mjs +1 -1
  9. package/lib/cli/known-failures.mjs +4 -4
  10. package/lib/cli/presentation/discovery-reporter.mjs +2 -2
  11. package/lib/config/{setup-loader.mjs → config-loader.mjs} +32 -32
  12. package/lib/config/index.mjs +16 -16
  13. package/lib/config/runtime.mjs +1 -1
  14. package/lib/config/telemetry.mjs +4 -4
  15. package/lib/config/validation.mjs +1 -1
  16. package/lib/{setup → config-api}/index.d.ts +7 -7
  17. package/lib/{setup → config-api}/index.mjs +10 -10
  18. package/lib/{setup → config-api}/index.test.mjs +16 -3
  19. package/lib/{setup → config-api}/runtime.mjs +10 -10
  20. package/lib/coverage/index.test.mjs +3 -3
  21. package/lib/discovery/file-metadata.mjs +1 -1
  22. package/lib/discovery/file-metadata.test.mjs +2 -2
  23. package/lib/discovery/index.d.ts +1 -1
  24. package/lib/discovery/index.mjs +28 -28
  25. package/lib/discovery/index.test.mjs +10 -10
  26. package/lib/drizzle/index.d.ts +14 -0
  27. package/lib/drizzle/index.mjs +15 -0
  28. package/lib/drizzle/index.test.mjs +33 -0
  29. package/lib/env/index.d.ts +17 -0
  30. package/lib/env/index.mjs +65 -0
  31. package/lib/env/index.test.mjs +82 -0
  32. package/lib/known-failures/github.mjs +4 -4
  33. package/lib/package.test.mjs +24 -4
  34. package/lib/playwright/index.d.ts +21 -0
  35. package/lib/playwright/index.mjs +53 -0
  36. package/lib/playwright/index.test.mjs +43 -0
  37. package/lib/runner/template-steps.mjs +5 -5
  38. package/lib/runner/template.mjs +2 -2
  39. package/lib/runtime-src/k6/scenario-suite.js +1 -1
  40. package/lib/runtime-src/k6/suite.js +1 -1
  41. package/lib/shared/build-config.mjs +1 -1
  42. package/lib/shared/build-config.test.mjs +1 -1
  43. package/lib/shared/configured-steps.test.mjs +2 -2
  44. package/lib/toolchains/index.mjs +2 -2
  45. package/lib/vitest/index.d.ts +12 -0
  46. package/lib/vitest/index.mjs +31 -0
  47. package/lib/vitest/index.test.mjs +20 -0
  48. package/node_modules/@elench/next-analysis/package.json +1 -1
  49. package/node_modules/@elench/testkit-bridge/package.json +2 -2
  50. package/node_modules/@elench/testkit-protocol/package.json +1 -1
  51. package/node_modules/@elench/ts-analysis/package.json +1 -1
  52. package/package.json +24 -8
  53. /package/lib/{setup → config-api}/next-runtime-tsconfig.mjs +0 -0
  54. /package/lib/{setup → config-api}/next-runtime-tsconfig.test.mjs +0 -0
@@ -5,18 +5,18 @@ export function normalizeTelemetryConfig(telemetry) {
5
5
  try {
6
6
  parsed = new URL(telemetry.endpoint);
7
7
  } catch {
8
- throw new Error("testkit.setup telemetry.endpoint must be a valid URL");
8
+ throw new Error("testkit.config telemetry.endpoint must be a valid URL");
9
9
  }
10
10
  if (!["http:", "https:"].includes(parsed.protocol)) {
11
- throw new Error("testkit.setup telemetry.endpoint must use http or https");
11
+ throw new Error("testkit.config telemetry.endpoint must use http or https");
12
12
  }
13
13
  }
14
14
  if (telemetry.enabled === true) {
15
15
  if (!telemetry.endpoint) {
16
- throw new Error("testkit.setup telemetry.endpoint is required when telemetry.enabled is true");
16
+ throw new Error("testkit.config telemetry.endpoint is required when telemetry.enabled is true");
17
17
  }
18
18
  if (!telemetry.tokenEnv) {
19
- throw new Error("testkit.setup telemetry.tokenEnv is required when telemetry.enabled is true");
19
+ throw new Error("testkit.config telemetry.tokenEnv is required when telemetry.enabled is true");
20
20
  }
21
21
  }
22
22
  return {
@@ -32,7 +32,7 @@ export function validateServiceConfig({
32
32
 
33
33
  if (usesLocalExecution && !local) {
34
34
  throw new Error(
35
- `Service "${name}" defines non-DAL suites but no local runtime could be resolved. Add it in testkit.setup.ts.`
35
+ `Service "${name}" defines non-DAL suites but no local runtime could be resolved. Add it in testkit.config.ts.`
36
36
  );
37
37
  }
38
38
 
@@ -215,11 +215,11 @@ export interface NextAppOptions extends Omit<ServiceConfig, "local" | "runtime">
215
215
  toolchain?: string | NodeToolchainConfig;
216
216
  }
217
217
 
218
- export interface TestkitSetup {
218
+ export interface TestkitConfig {
219
219
  discovery?: DiscoveryConfig;
220
220
  execution?: TestkitExecutionConfig;
221
221
  profiles?: {
222
- http?: Record<string, HttpSuiteConfig>;
222
+ http?: Record<string, HttpSuiteConfig<any>>;
223
223
  };
224
224
  reporting?: {
225
225
  knownFailuresFile?: string;
@@ -235,9 +235,9 @@ export interface TestkitSetup {
235
235
  };
236
236
  }
237
237
 
238
- export declare function defineTestkitSetup<T extends TestkitSetup>(setup: T): T;
238
+ export declare function defineConfig<T extends TestkitConfig>(config: T): T;
239
239
  export declare function defineHttpProfile<T extends HttpSuiteConfig>(profile: T): T;
240
- export declare function defineTestkitFile<T extends TestkitFileMetadata>(metadata: T): T;
240
+ export declare function defineFile<T extends TestkitFileMetadata>(metadata: T): T;
241
241
  export declare function postgresDatabase(options?: Omit<LocalDatabaseConfig, "provider">): LocalDatabaseConfig;
242
242
  export declare function commandStep(
243
243
  cmd: string,
@@ -309,9 +309,9 @@ export declare function jsonSessionProfile(options?: {
309
309
  usernameEnv?: string;
310
310
  }): HttpSuiteConfig;
311
311
 
312
- export declare function registerRepoSetup(setup: unknown): void;
313
- export declare function getRepoSetup(): unknown;
314
- export declare function clearRepoSetup(): void;
312
+ export declare function registerRepoConfig(config: unknown): void;
313
+ export declare function getRepoConfig(): unknown;
314
+ export declare function clearRepoConfig(): void;
315
315
  export declare function registerRuntimeContext(context: unknown): void;
316
316
  export declare function getRuntimeContext(): unknown;
317
317
  export declare function clearRuntimeContext(): void;
@@ -1,24 +1,24 @@
1
1
  import {
2
- clearRepoSetup,
2
+ clearRepoConfig,
3
3
  clearRuntimeContext,
4
- getRepoSetup,
4
+ getRepoConfig,
5
5
  getRuntimeContext,
6
6
  getRuntimeEnv,
7
- registerRepoSetup,
7
+ registerRepoConfig,
8
8
  registerRuntimeContext,
9
9
  runtimeHttp,
10
10
  runtimeJson,
11
11
  } from "./runtime.mjs";
12
12
 
13
- export function defineTestkitSetup(setup) {
14
- return setup || {};
13
+ export function defineConfig(config) {
14
+ return config || {};
15
15
  }
16
16
 
17
17
  export function defineHttpProfile(profile) {
18
18
  return profile || {};
19
19
  }
20
20
 
21
- export function defineTestkitFile(metadata) {
21
+ export function defineFile(metadata) {
22
22
  return metadata || {};
23
23
  }
24
24
 
@@ -364,11 +364,11 @@ export function jsonSessionProfile(options = {}) {
364
364
  }
365
365
 
366
366
  export {
367
- clearRepoSetup,
367
+ clearRepoConfig,
368
368
  clearRuntimeContext,
369
- getRepoSetup,
369
+ getRepoConfig,
370
370
  getRuntimeContext,
371
- registerRepoSetup,
371
+ registerRepoConfig,
372
372
  registerRuntimeContext,
373
373
  runtimeHttp,
374
374
  runtimeJson,
@@ -413,7 +413,7 @@ function envValue(name) {
413
413
  const env = getRuntimeEnv();
414
414
  const value = env?.rawEnv?.[name] || env?.[name];
415
415
  if (!value) {
416
- throw new Error(`Missing required env var "${name}" for testkit setup`);
416
+ throw new Error(`Missing required env var "${name}" for testkit config`);
417
417
  }
418
418
  return value;
419
419
  }
@@ -1,7 +1,9 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
  import {
3
3
  databaseServiceEnv,
4
- defineTestkitFile,
4
+ defineConfig,
5
+ defineFile,
6
+ defineHttpProfile,
5
7
  nextApp,
6
8
  nextBuild,
7
9
  nodeToolchain,
@@ -18,9 +20,20 @@ import {
18
20
  verifyModule,
19
21
  } from "./index.mjs";
20
22
 
21
- describe("setup helpers", () => {
23
+ describe("config helpers", () => {
24
+ it("defines repo config plainly", () => {
25
+ expect(defineConfig({ execution: { workers: 4 } })).toEqual({
26
+ execution: { workers: 4 },
27
+ });
28
+ });
29
+
30
+ it("defines HTTP profiles plainly", () => {
31
+ const profile = defineHttpProfile({ headers: () => ({ Authorization: "Bearer token" }) });
32
+ expect(typeof profile.headers).toBe("function");
33
+ });
34
+
22
35
  it("defines file-local metadata plainly", () => {
23
- expect(defineTestkitFile({ skip: "Auth is stubbed", locks: ["background-workers"] })).toEqual({
36
+ expect(defineFile({ skip: "Auth is stubbed", locks: ["background-workers"] })).toEqual({
24
37
  skip: "Auth is stubbed",
25
38
  locks: ["background-workers"],
26
39
  });
@@ -1,16 +1,16 @@
1
- let activeSetup = null;
1
+ let activeConfig = null;
2
2
  let activeRuntimeContext = null;
3
3
 
4
- export function registerRepoSetup(setup) {
5
- activeSetup = setup || null;
4
+ export function registerRepoConfig(config) {
5
+ activeConfig = config || null;
6
6
  }
7
7
 
8
- export function getRepoSetup() {
9
- return activeSetup;
8
+ export function getRepoConfig() {
9
+ return activeConfig;
10
10
  }
11
11
 
12
- export function clearRepoSetup() {
13
- activeSetup = null;
12
+ export function clearRepoConfig() {
13
+ activeConfig = null;
14
14
  }
15
15
 
16
16
  export function registerRuntimeContext(context) {
@@ -48,9 +48,9 @@ export function runtimeJson(response) {
48
48
  export function resolveHttpProfile(name) {
49
49
  if (!name) return null;
50
50
 
51
- const setup = getRepoSetup();
52
- if (!setup) return null;
53
- const profile = setup?.profiles?.http?.[name];
51
+ const config = getRepoConfig();
52
+ if (!config) return null;
53
+ const profile = config?.profiles?.http?.[name];
54
54
  if (!profile) {
55
55
  throw new Error(`Unknown testkit HTTP profile "${name}"`);
56
56
  }
@@ -17,11 +17,11 @@ describe("coverage graph builder", () => {
17
17
  const productDir = createProduct();
18
18
  writeFile(
19
19
  productDir,
20
- "testkit.setup.ts",
20
+ "testkit.config.ts",
21
21
  `
22
- import { defineTestkitSetup, nextApp } from "@elench/testkit/setup";
22
+ import { defineConfig, nextApp } from "@elench/testkit/config";
23
23
 
24
- export default defineTestkitSetup({
24
+ export default defineConfig({
25
25
  services: {
26
26
  web: nextApp({
27
27
  cwd: ".",
@@ -44,7 +44,7 @@ function parseMetadataInitializer(initializer) {
44
44
  if (!initializer) return null;
45
45
  if (ts.isCallExpression(initializer)) {
46
46
  const callee = getCallIdentifier(initializer.expression);
47
- if (callee === "defineTestkitFile" && initializer.arguments[0] && ts.isObjectLiteralExpression(initializer.arguments[0])) {
47
+ if (callee === "defineFile" && initializer.arguments[0] && ts.isObjectLiteralExpression(initializer.arguments[0])) {
48
48
  return parseMetadataObject(initializer.arguments[0]);
49
49
  }
50
50
  return null;
@@ -20,8 +20,8 @@ describe("test file metadata", () => {
20
20
  fs.writeFileSync(
21
21
  path.join(productDir, "__testkit__", "billing.int.testkit.ts"),
22
22
  [
23
- 'import { defineTestkitFile } from "@elench/testkit/setup";',
24
- 'export const testkit = defineTestkitFile({',
23
+ 'import { defineFile } from "@elench/testkit/config";',
24
+ 'export const testkit = defineFile({',
25
25
  ' skip: "Billing is stubbed locally",',
26
26
  ' locks: ["background-workers", "background-workers"],',
27
27
  "});",
@@ -76,7 +76,7 @@ export interface DiscoveryResult {
76
76
  name: string;
77
77
  directory: string;
78
78
  };
79
- setupFile: string | null;
79
+ configFile: string | null;
80
80
  filters: {
81
81
  service: string | null;
82
82
  types: DiscoverySelectionType[] | ["all"];
@@ -1,7 +1,7 @@
1
1
  import path from "path";
2
2
  import { loadConfigContext, resolveProductDir } from "../config/index.mjs";
3
3
  import { discoverProject } from "../config/discovery.mjs";
4
- import { loadTestkitSetup } from "../config/setup-loader.mjs";
4
+ import { loadTestkitConfig } from "../config/config-loader.mjs";
5
5
  import { buildCoverageGraph } from "../coverage/index.mjs";
6
6
  import { historyFilePath, loadHistory, summarizeHistoryForFiles } from "../history/index.mjs";
7
7
  import {
@@ -12,12 +12,12 @@ import {
12
12
  suiteSelectionType,
13
13
  } from "../runner/suite-selection.mjs";
14
14
 
15
- const DISCOVERY_SCHEMA_VERSION = 3;
15
+ const DISCOVERY_SCHEMA_VERSION = 4;
16
16
 
17
17
  export async function discoverTests(options = {}) {
18
18
  const productDir = resolveProductDir(process.cwd(), options.dir);
19
19
  const filters = normalizeDiscoveryFilters(options);
20
- const setupContext = await loadSetupContext(productDir, filters.diagnosticsMode);
20
+ const configContext = await loadConfigDiscoveryContext(productDir, filters.diagnosticsMode);
21
21
  const baseResult = {
22
22
  schemaVersion: DISCOVERY_SCHEMA_VERSION,
23
23
  source: "testkit-discovery",
@@ -25,7 +25,7 @@ export async function discoverTests(options = {}) {
25
25
  name: path.basename(productDir),
26
26
  directory: productDir,
27
27
  },
28
- setupFile: setupContext.setupFile ? path.relative(productDir, setupContext.setupFile) || path.basename(setupContext.setupFile) : null,
28
+ configFile: configContext.configFile ? path.relative(productDir, configContext.configFile) || path.basename(configContext.configFile) : null,
29
29
  filters: {
30
30
  service: filters.serviceFilter,
31
31
  types: filters.typeValues,
@@ -38,7 +38,7 @@ export async function discoverTests(options = {}) {
38
38
  suites: [],
39
39
  files: [],
40
40
  coverageGraph: null,
41
- diagnostics: [...setupContext.diagnostics],
41
+ diagnostics: [...configContext.diagnostics],
42
42
  summary: emptySummary(),
43
43
  history: {
44
44
  available: false,
@@ -46,24 +46,24 @@ export async function discoverTests(options = {}) {
46
46
  },
47
47
  };
48
48
 
49
- if (!setupContext.setup) {
49
+ if (!configContext.config) {
50
50
  return finalizeDiscoveryResult(baseResult, productDir);
51
51
  }
52
52
 
53
- const rawDiscovery = discoverProject(productDir, setupContext.setup.services || {}, {
53
+ const rawDiscovery = discoverProject(productDir, configContext.config.services || {}, {
54
54
  strict: filters.diagnosticsMode === "error",
55
- discovery: setupContext.setup.discovery || {},
55
+ discovery: configContext.config.discovery || {},
56
56
  });
57
57
  baseResult.diagnostics.push(...rawDiscovery.diagnostics);
58
- validateRequestedService(filters.serviceFilter, setupContext.setup.services || {}, rawDiscovery);
58
+ validateRequestedService(filters.serviceFilter, configContext.config.services || {}, rawDiscovery);
59
59
 
60
- let configContext = null;
60
+ let normalizedConfigContext = null;
61
61
  try {
62
- configContext = await loadConfigContext({
62
+ normalizedConfigContext = await loadConfigContext({
63
63
  dir: productDir,
64
- setupContext: {
65
- setup: setupContext.setup,
66
- setupFile: setupContext.setupFile,
64
+ configContext: {
65
+ config: configContext.config,
66
+ configFile: configContext.configFile,
67
67
  },
68
68
  discoveryOptions: {
69
69
  strict: filters.diagnosticsMode === "error",
@@ -78,9 +78,9 @@ export async function discoverTests(options = {}) {
78
78
  });
79
79
  }
80
80
 
81
- if (configContext) {
81
+ if (normalizedConfigContext) {
82
82
  const resolved = buildResolvedDiscovery({
83
- configs: configContext.configs,
83
+ configs: normalizedConfigContext.configs,
84
84
  filters,
85
85
  });
86
86
  return finalizeDiscoveryResult(
@@ -91,8 +91,8 @@ export async function discoverTests(options = {}) {
91
91
  files: resolved.files,
92
92
  coverageGraph: buildCoverageGraph({
93
93
  productDir,
94
- repoDiscovery: setupContext.setup.discovery || {},
95
- services: setupContext.setup.services || {},
94
+ repoDiscovery: configContext.config.discovery || {},
95
+ services: configContext.config.services || {},
96
96
  discoveryFiles: rawDiscovery.files || [],
97
97
  }),
98
98
  },
@@ -102,7 +102,7 @@ export async function discoverTests(options = {}) {
102
102
 
103
103
  const rawOnly = buildRawDiscovery({
104
104
  rawDiscovery,
105
- explicitServices: setupContext.setup.services || {},
105
+ explicitServices: configContext.config.services || {},
106
106
  filters,
107
107
  });
108
108
  return finalizeDiscoveryResult(
@@ -113,8 +113,8 @@ export async function discoverTests(options = {}) {
113
113
  files: rawOnly.files,
114
114
  coverageGraph: buildCoverageGraph({
115
115
  productDir,
116
- repoDiscovery: setupContext.setup.discovery || {},
117
- services: setupContext.setup.services || {},
116
+ repoDiscovery: configContext.config.discovery || {},
117
+ services: configContext.config.services || {},
118
118
  discoveryFiles: rawDiscovery.files || [],
119
119
  }),
120
120
  },
@@ -140,22 +140,22 @@ export function formatDisplayName(value) {
140
140
  .join(" ");
141
141
  }
142
142
 
143
- async function loadSetupContext(productDir, diagnosticsMode) {
143
+ async function loadConfigDiscoveryContext(productDir, diagnosticsMode) {
144
144
  try {
145
- const { setup, setupFile } = await loadTestkitSetup(productDir);
145
+ const { config, configFile } = await loadTestkitConfig(productDir);
146
146
  return {
147
- setup,
148
- setupFile,
147
+ config,
148
+ configFile,
149
149
  diagnostics: [],
150
150
  };
151
151
  } catch (error) {
152
152
  if (diagnosticsMode === "error") throw error;
153
153
  return {
154
- setup: null,
155
- setupFile: null,
154
+ config: null,
155
+ configFile: null,
156
156
  diagnostics: [
157
157
  {
158
- code: "setup_invalid",
158
+ code: "config_invalid",
159
159
  severity: "error",
160
160
  message: formatErrorMessage(error),
161
161
  },
@@ -18,11 +18,11 @@ describe("public discovery", () => {
18
18
  const productDir = createProduct();
19
19
  writeFile(
20
20
  productDir,
21
- "testkit.setup.ts",
21
+ "testkit.config.ts",
22
22
  `
23
- import { defineTestkitSetup } from "@elench/testkit/setup";
23
+ import { defineConfig } from "@elench/testkit/config";
24
24
 
25
- export default defineTestkitSetup({
25
+ export default defineConfig({
26
26
  services: {
27
27
  api: {
28
28
  local: {
@@ -49,8 +49,8 @@ describe("public discovery", () => {
49
49
  productDir,
50
50
  "src/api/routes/__testkit__/agent-configs-auth-gate.int.testkit.ts",
51
51
  [
52
- 'import { defineTestkitFile } from "@elench/testkit/setup";',
53
- 'export const testkit = defineTestkitFile({ locks: ["route-lock"] });',
52
+ 'import { defineFile } from "@elench/testkit/config";',
53
+ 'export const testkit = defineFile({ locks: ["route-lock"] });',
54
54
  "export {};",
55
55
  ].join("\n")
56
56
  );
@@ -58,8 +58,8 @@ describe("public discovery", () => {
58
58
  productDir,
59
59
  "frontend/src/app/login/__testkit__/auth.pw.testkit.ts",
60
60
  [
61
- 'import { defineTestkitFile } from "@elench/testkit/setup";',
62
- 'export const testkit = defineTestkitFile({ skip: "Auth is stubbed locally" });',
61
+ 'import { defineFile } from "@elench/testkit/config";',
62
+ 'export const testkit = defineFile({ skip: "Auth is stubbed locally" });',
63
63
  "export {};",
64
64
  ].join("\n")
65
65
  );
@@ -135,11 +135,11 @@ describe("public discovery", () => {
135
135
  const productDir = createProduct();
136
136
  writeFile(
137
137
  productDir,
138
- "testkit.setup.ts",
138
+ "testkit.config.ts",
139
139
  `
140
- import { defineTestkitSetup } from "@elench/testkit/setup";
140
+ import { defineConfig } from "@elench/testkit/config";
141
141
 
142
- export default defineTestkitSetup({
142
+ export default defineConfig({
143
143
  services: {
144
144
  api: {
145
145
  local: {
@@ -0,0 +1,14 @@
1
+ export interface DrizzleConfigOptions {
2
+ consumer?: string;
3
+ cwd?: string;
4
+ env?: NodeJS.ProcessEnv;
5
+ files?: string[];
6
+ onlyWhenAllowed?: boolean;
7
+ override?: boolean;
8
+ requireLocalDatabase?: boolean;
9
+ }
10
+
11
+ export declare function defineConfig<T extends Record<string, unknown>>(
12
+ config: T,
13
+ options?: DrizzleConfigOptions
14
+ ): T;
@@ -0,0 +1,15 @@
1
+ import { assertLocalDatabaseUrl, loadDotenvFiles } from "../env/index.mjs";
2
+
3
+ export function defineConfig(config = {}, options = {}) {
4
+ loadDotenvFiles({
5
+ cwd: options.cwd,
6
+ env: options.env,
7
+ files: options.files,
8
+ onlyWhenAllowed: options.onlyWhenAllowed,
9
+ override: options.override,
10
+ });
11
+ if (options.requireLocalDatabase !== false) {
12
+ assertLocalDatabaseUrl(options.env || process.env, options.consumer || "drizzle.config.ts");
13
+ }
14
+ return config;
15
+ }
@@ -0,0 +1,33 @@
1
+ import fs from "fs";
2
+ import os from "os";
3
+ import path from "path";
4
+ import { describe, expect, it } from "vitest";
5
+ import { defineConfig } from "./index.mjs";
6
+
7
+ describe("testkit drizzle defineConfig", () => {
8
+ it("loads dotenv files before returning config", () => {
9
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "testkit-drizzle-"));
10
+ fs.writeFileSync(path.join(tmpDir, ".env"), "DATABASE_URL=postgres://user:pass@127.0.0.1:5432/app\n");
11
+ const env = {};
12
+
13
+ const config = defineConfig({ dialect: "postgresql" }, { cwd: tmpDir, env });
14
+
15
+ expect(config).toEqual({ dialect: "postgresql" });
16
+ expect(env.DATABASE_URL).toContain("127.0.0.1");
17
+ fs.rmSync(tmpDir, { recursive: true, force: true });
18
+ });
19
+
20
+ it("enforces local database urls during managed runs by default", () => {
21
+ expect(() =>
22
+ defineConfig(
23
+ { dialect: "postgresql" },
24
+ {
25
+ env: {
26
+ TESTKIT_ACTIVE: "1",
27
+ DATABASE_URL: "postgres://user:pass@db.example.com:5432/app",
28
+ },
29
+ }
30
+ )
31
+ ).toThrow("testkit runs require a local PostgreSQL DATABASE_URL");
32
+ });
33
+ });
@@ -0,0 +1,17 @@
1
+ export interface LoadDotenvFilesOptions {
2
+ cwd?: string;
3
+ env?: NodeJS.ProcessEnv;
4
+ files?: string[];
5
+ onlyWhenAllowed?: boolean;
6
+ override?: boolean;
7
+ }
8
+
9
+ export declare function isManagedRuntime(env?: NodeJS.ProcessEnv): boolean;
10
+ export declare function shouldLoadDotenv(env?: NodeJS.ProcessEnv): boolean;
11
+ export declare function loadDotenvFiles(options?: LoadDotenvFilesOptions): {
12
+ loaded: string[];
13
+ };
14
+ export declare function assertLocalDatabaseUrl(
15
+ env?: NodeJS.ProcessEnv,
16
+ consumer?: string
17
+ ): void;
@@ -0,0 +1,65 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { parseDotenvString } from "../config/env.mjs";
4
+
5
+ const LOCAL_DATABASE_HOSTS = new Set(["localhost", "127.0.0.1", "::1"]);
6
+ const LOCAL_DATABASE_PROTOCOLS = new Set(["postgres:", "postgresql:"]);
7
+
8
+ export function isManagedRuntime(env = process.env) {
9
+ return env?.TESTKIT_ACTIVE === "1";
10
+ }
11
+
12
+ export function shouldLoadDotenv(env = process.env) {
13
+ return env?.NODE_ENV !== "production" && !isManagedRuntime(env);
14
+ }
15
+
16
+ export function loadDotenvFiles(options = {}) {
17
+ const env = options.env || process.env;
18
+ if (options.onlyWhenAllowed !== false && !shouldLoadDotenv(env)) {
19
+ return { loaded: [] };
20
+ }
21
+
22
+ const cwd = path.resolve(options.cwd || process.cwd());
23
+ const override = options.override !== false;
24
+ const files = Array.isArray(options.files) && options.files.length > 0
25
+ ? options.files
26
+ : [".env", ".env.local"];
27
+ const loaded = [];
28
+
29
+ for (const file of files) {
30
+ const absolutePath = path.resolve(cwd, file);
31
+ if (!fs.existsSync(absolutePath)) continue;
32
+ const parsed = parseDotenvString(fs.readFileSync(absolutePath, "utf8"));
33
+ for (const [key, value] of Object.entries(parsed)) {
34
+ if (override || env[key] == null) {
35
+ env[key] = value;
36
+ }
37
+ }
38
+ loaded.push(absolutePath);
39
+ }
40
+
41
+ return { loaded };
42
+ }
43
+
44
+ export function assertLocalDatabaseUrl(env = process.env, consumer = "This command") {
45
+ if (!isManagedRuntime(env)) return;
46
+
47
+ const databaseUrl = env?.DATABASE_URL;
48
+ if (!databaseUrl) {
49
+ throw new Error(`${consumer} requires DATABASE_URL when TESTKIT_ACTIVE=1.`);
50
+ }
51
+
52
+ let parsed;
53
+ try {
54
+ parsed = new URL(databaseUrl);
55
+ } catch {
56
+ throw new Error(`${consumer} requires a valid DATABASE_URL when TESTKIT_ACTIVE=1.`);
57
+ }
58
+
59
+ if (!LOCAL_DATABASE_PROTOCOLS.has(parsed.protocol) || !LOCAL_DATABASE_HOSTS.has(parsed.hostname)) {
60
+ const location = `${parsed.protocol}//${parsed.hostname}${parsed.port ? `:${parsed.port}` : ""}`;
61
+ throw new Error(
62
+ `${consumer} testkit runs require a local PostgreSQL DATABASE_URL. Refusing ${location}.`
63
+ );
64
+ }
65
+ }