@elench/testkit 0.1.69 → 0.1.71

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 (53) hide show
  1. package/README.md +70 -10
  2. package/lib/app/doctor.mjs +1 -1
  3. package/lib/app/typecheck.mjs +8 -4
  4. package/lib/bundler/index.mjs +17 -17
  5. package/lib/cli/command-helpers.mjs +1 -1
  6. package/lib/cli/commands/doctor.mjs +1 -1
  7. package/lib/cli/commands/typecheck.mjs +1 -1
  8. package/lib/cli/known-failures.mjs +4 -4
  9. package/lib/cli/presentation/discovery-reporter.mjs +2 -2
  10. package/lib/config/{setup-loader.mjs → config-loader.mjs} +32 -32
  11. package/lib/config/index.mjs +16 -16
  12. package/lib/config/runtime.mjs +1 -1
  13. package/lib/config/telemetry.mjs +4 -4
  14. package/lib/config/validation.mjs +1 -1
  15. package/lib/{setup → config-api}/index.d.ts +7 -7
  16. package/lib/{setup → config-api}/index.mjs +10 -10
  17. package/lib/{setup → config-api}/index.test.mjs +16 -3
  18. package/lib/{setup → config-api}/runtime.mjs +10 -10
  19. package/lib/coverage/index.test.mjs +3 -3
  20. package/lib/discovery/file-metadata.mjs +1 -1
  21. package/lib/discovery/file-metadata.test.mjs +2 -2
  22. package/lib/discovery/index.d.ts +1 -1
  23. package/lib/discovery/index.mjs +28 -28
  24. package/lib/discovery/index.test.mjs +10 -10
  25. package/lib/drizzle/index.d.ts +14 -0
  26. package/lib/drizzle/index.mjs +15 -0
  27. package/lib/drizzle/index.test.mjs +33 -0
  28. package/lib/env/index.d.ts +17 -0
  29. package/lib/env/index.mjs +65 -0
  30. package/lib/env/index.test.mjs +82 -0
  31. package/lib/known-failures/github.mjs +4 -4
  32. package/lib/package.test.mjs +24 -4
  33. package/lib/playwright/index.d.ts +21 -0
  34. package/lib/playwright/index.mjs +53 -0
  35. package/lib/playwright/index.test.mjs +43 -0
  36. package/lib/runner/template-steps.mjs +5 -5
  37. package/lib/runner/template.mjs +2 -2
  38. package/lib/runtime-src/k6/scenario-suite.js +1 -1
  39. package/lib/runtime-src/k6/suite.js +1 -1
  40. package/lib/shared/build-config.mjs +1 -1
  41. package/lib/shared/build-config.test.mjs +1 -1
  42. package/lib/shared/configured-steps.test.mjs +2 -2
  43. package/lib/toolchains/index.mjs +2 -2
  44. package/lib/vitest/index.d.ts +12 -0
  45. package/lib/vitest/index.mjs +31 -0
  46. package/lib/vitest/index.test.mjs +20 -0
  47. package/node_modules/@elench/next-analysis/package.json +1 -1
  48. package/node_modules/@elench/testkit-bridge/package.json +2 -2
  49. package/node_modules/@elench/testkit-protocol/package.json +1 -1
  50. package/node_modules/@elench/ts-analysis/package.json +1 -1
  51. package/package.json +24 -8
  52. /package/lib/{setup → config-api}/next-runtime-tsconfig.mjs +0 -0
  53. /package/lib/{setup → config-api}/next-runtime-tsconfig.test.mjs +0 -0
@@ -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
+ }
@@ -0,0 +1,82 @@
1
+ import fs from "fs";
2
+ import os from "os";
3
+ import path from "path";
4
+ import { describe, expect, it } from "vitest";
5
+ import {
6
+ assertLocalDatabaseUrl,
7
+ isManagedRuntime,
8
+ loadDotenvFiles,
9
+ shouldLoadDotenv,
10
+ } from "./index.mjs";
11
+
12
+ describe("testkit env helpers", () => {
13
+ it("detects managed runtimes", () => {
14
+ expect(isManagedRuntime({ TESTKIT_ACTIVE: "1" })).toBe(true);
15
+ expect(isManagedRuntime({ TESTKIT_ACTIVE: "0" })).toBe(false);
16
+ });
17
+
18
+ it("only loads dotenv when not production and not managed", () => {
19
+ expect(shouldLoadDotenv({ NODE_ENV: "development" })).toBe(true);
20
+ expect(shouldLoadDotenv({ NODE_ENV: "production" })).toBe(false);
21
+ expect(shouldLoadDotenv({ NODE_ENV: "development", TESTKIT_ACTIVE: "1" })).toBe(false);
22
+ });
23
+
24
+ it("loads dotenv files in order and respects override by default", () => {
25
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "testkit-env-"));
26
+ fs.writeFileSync(path.join(tmpDir, ".env"), "FOO=base\nBAR=base\n");
27
+ fs.writeFileSync(path.join(tmpDir, ".env.local"), "BAR=local\nBAZ=local\n");
28
+ const env = {};
29
+
30
+ const result = loadDotenvFiles({ cwd: tmpDir, env });
31
+
32
+ expect(result.loaded).toHaveLength(2);
33
+ expect(env).toEqual({
34
+ FOO: "base",
35
+ BAR: "local",
36
+ BAZ: "local",
37
+ });
38
+ fs.rmSync(tmpDir, { recursive: true, force: true });
39
+ });
40
+
41
+ it("supports non-overriding dotenv loads", () => {
42
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "testkit-env-"));
43
+ fs.writeFileSync(path.join(tmpDir, ".env"), "FOO=base\n");
44
+ const env = { FOO: "preset" };
45
+
46
+ loadDotenvFiles({ cwd: tmpDir, env, override: false });
47
+
48
+ expect(env.FOO).toBe("preset");
49
+ fs.rmSync(tmpDir, { recursive: true, force: true });
50
+ });
51
+
52
+ it("skips dotenv loading when managed unless explicitly overridden", () => {
53
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "testkit-env-"));
54
+ fs.writeFileSync(path.join(tmpDir, ".env"), "FOO=base\n");
55
+ const env = { TESTKIT_ACTIVE: "1" };
56
+
57
+ const skipped = loadDotenvFiles({ cwd: tmpDir, env });
58
+ expect(skipped.loaded).toEqual([]);
59
+ expect(env.FOO).toBeUndefined();
60
+
61
+ const forced = loadDotenvFiles({ cwd: tmpDir, env, onlyWhenAllowed: false });
62
+ expect(forced.loaded).toHaveLength(1);
63
+ expect(env.FOO).toBe("base");
64
+ fs.rmSync(tmpDir, { recursive: true, force: true });
65
+ });
66
+
67
+ it("enforces local postgres urls during managed runs", () => {
68
+ expect(() =>
69
+ assertLocalDatabaseUrl(
70
+ { TESTKIT_ACTIVE: "1", DATABASE_URL: "postgres://user:pass@db.example.com:5432/app" },
71
+ "drizzle.config.ts"
72
+ )
73
+ ).toThrow("drizzle.config.ts testkit runs require a local PostgreSQL DATABASE_URL.");
74
+
75
+ expect(() =>
76
+ assertLocalDatabaseUrl(
77
+ { TESTKIT_ACTIVE: "1", DATABASE_URL: "postgres://user:pass@127.0.0.1:5432/app" },
78
+ "drizzle.config.ts"
79
+ )
80
+ ).not.toThrow();
81
+ });
82
+ });
@@ -21,18 +21,18 @@ const MODES = new Set(["off", "warn", "error"]);
21
21
  export function normalizeKnownFailureIssueValidationConfig(value) {
22
22
  if (value == null) return null;
23
23
  if (!value || typeof value !== "object") {
24
- throw new Error("testkit.setup.ts reporting.issueValidation must be an object");
24
+ throw new Error("testkit.config.ts reporting.issueValidation must be an object");
25
25
  }
26
26
 
27
27
  const provider = normalizeOptionalString(value.provider) || "github";
28
28
  if (provider !== "github") {
29
- throw new Error('testkit.setup.ts reporting.issueValidation.provider must be "github"');
29
+ throw new Error('testkit.config.ts reporting.issueValidation.provider must be "github"');
30
30
  }
31
31
 
32
32
  const mode = normalizeOptionalString(value.mode) || "warn";
33
33
  if (!MODES.has(mode)) {
34
34
  throw new Error(
35
- 'testkit.setup.ts reporting.issueValidation.mode must be one of: off, warn, error'
35
+ 'testkit.config.ts reporting.issueValidation.mode must be one of: off, warn, error'
36
36
  );
37
37
  }
38
38
 
@@ -41,7 +41,7 @@ export function normalizeKnownFailureIssueValidationConfig(value) {
41
41
  ? DEFAULT_CACHE_TTL_SECONDS
42
42
  : normalizePositiveInteger(
43
43
  value.cacheTtlSeconds,
44
- "testkit.setup.ts reporting.issueValidation.cacheTtlSeconds"
44
+ "testkit.config.ts reporting.issueValidation.cacheTtlSeconds"
45
45
  );
46
46
 
47
47
  return {