@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.
- package/README.md +70 -10
- package/lib/app/doctor.mjs +1 -1
- package/lib/app/typecheck.mjs +8 -4
- package/lib/bundler/index.mjs +17 -17
- package/lib/cli/command-helpers.mjs +1 -1
- package/lib/cli/commands/doctor.mjs +1 -1
- package/lib/cli/commands/typecheck.mjs +1 -1
- package/lib/cli/known-failures.mjs +4 -4
- package/lib/cli/presentation/discovery-reporter.mjs +2 -2
- package/lib/config/{setup-loader.mjs → config-loader.mjs} +32 -32
- package/lib/config/index.mjs +16 -16
- package/lib/config/runtime.mjs +1 -1
- package/lib/config/telemetry.mjs +4 -4
- package/lib/config/validation.mjs +1 -1
- package/lib/{setup → config-api}/index.d.ts +7 -7
- package/lib/{setup → config-api}/index.mjs +10 -10
- package/lib/{setup → config-api}/index.test.mjs +16 -3
- package/lib/{setup → config-api}/runtime.mjs +10 -10
- package/lib/coverage/index.test.mjs +3 -3
- package/lib/discovery/file-metadata.mjs +1 -1
- package/lib/discovery/file-metadata.test.mjs +2 -2
- package/lib/discovery/index.d.ts +1 -1
- package/lib/discovery/index.mjs +28 -28
- package/lib/discovery/index.test.mjs +10 -10
- package/lib/drizzle/index.d.ts +14 -0
- package/lib/drizzle/index.mjs +15 -0
- package/lib/drizzle/index.test.mjs +33 -0
- package/lib/env/index.d.ts +17 -0
- package/lib/env/index.mjs +65 -0
- package/lib/env/index.test.mjs +82 -0
- package/lib/known-failures/github.mjs +4 -4
- package/lib/package.test.mjs +24 -4
- package/lib/playwright/index.d.ts +21 -0
- package/lib/playwright/index.mjs +53 -0
- package/lib/playwright/index.test.mjs +43 -0
- package/lib/runner/template-steps.mjs +5 -5
- package/lib/runner/template.mjs +2 -2
- package/lib/runtime-src/k6/scenario-suite.js +1 -1
- package/lib/runtime-src/k6/suite.js +1 -1
- package/lib/shared/build-config.mjs +1 -1
- package/lib/shared/build-config.test.mjs +1 -1
- package/lib/shared/configured-steps.test.mjs +2 -2
- package/lib/toolchains/index.mjs +2 -2
- package/lib/vitest/index.d.ts +12 -0
- package/lib/vitest/index.mjs +31 -0
- package/lib/vitest/index.test.mjs +20 -0
- package/node_modules/@elench/next-analysis/package.json +1 -1
- package/node_modules/@elench/testkit-bridge/package.json +2 -2
- package/node_modules/@elench/testkit-protocol/package.json +1 -1
- package/node_modules/@elench/ts-analysis/package.json +1 -1
- package/package.json +24 -8
- /package/lib/{setup → config-api}/next-runtime-tsconfig.mjs +0 -0
- /package/lib/{setup → config-api}/next-runtime-tsconfig.test.mjs +0 -0
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
clearRepoConfig,
|
|
3
3
|
clearRuntimeContext,
|
|
4
|
-
|
|
4
|
+
getRepoConfig,
|
|
5
5
|
getRuntimeContext,
|
|
6
6
|
getRuntimeEnv,
|
|
7
|
-
|
|
7
|
+
registerRepoConfig,
|
|
8
8
|
registerRuntimeContext,
|
|
9
9
|
runtimeHttp,
|
|
10
10
|
runtimeJson,
|
|
11
11
|
} from "./runtime.mjs";
|
|
12
12
|
|
|
13
|
-
export function
|
|
14
|
-
return
|
|
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
|
|
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
|
-
|
|
367
|
+
clearRepoConfig,
|
|
368
368
|
clearRuntimeContext,
|
|
369
|
-
|
|
369
|
+
getRepoConfig,
|
|
370
370
|
getRuntimeContext,
|
|
371
|
-
|
|
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
|
|
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
|
-
|
|
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("
|
|
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(
|
|
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
|
|
1
|
+
let activeConfig = null;
|
|
2
2
|
let activeRuntimeContext = null;
|
|
3
3
|
|
|
4
|
-
export function
|
|
5
|
-
|
|
4
|
+
export function registerRepoConfig(config) {
|
|
5
|
+
activeConfig = config || null;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
export function
|
|
9
|
-
return
|
|
8
|
+
export function getRepoConfig() {
|
|
9
|
+
return activeConfig;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
export function
|
|
13
|
-
|
|
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
|
|
52
|
-
if (!
|
|
53
|
-
const profile =
|
|
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.
|
|
20
|
+
"testkit.config.ts",
|
|
21
21
|
`
|
|
22
|
-
import {
|
|
22
|
+
import { defineConfig, nextApp } from "@elench/testkit/config";
|
|
23
23
|
|
|
24
|
-
export default
|
|
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 === "
|
|
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 {
|
|
24
|
-
'export const testkit =
|
|
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
|
"});",
|
package/lib/discovery/index.d.ts
CHANGED
package/lib/discovery/index.mjs
CHANGED
|
@@ -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 {
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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: [...
|
|
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 (!
|
|
49
|
+
if (!configContext.config) {
|
|
50
50
|
return finalizeDiscoveryResult(baseResult, productDir);
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
const rawDiscovery = discoverProject(productDir,
|
|
53
|
+
const rawDiscovery = discoverProject(productDir, configContext.config.services || {}, {
|
|
54
54
|
strict: filters.diagnosticsMode === "error",
|
|
55
|
-
discovery:
|
|
55
|
+
discovery: configContext.config.discovery || {},
|
|
56
56
|
});
|
|
57
57
|
baseResult.diagnostics.push(...rawDiscovery.diagnostics);
|
|
58
|
-
validateRequestedService(filters.serviceFilter,
|
|
58
|
+
validateRequestedService(filters.serviceFilter, configContext.config.services || {}, rawDiscovery);
|
|
59
59
|
|
|
60
|
-
let
|
|
60
|
+
let normalizedConfigContext = null;
|
|
61
61
|
try {
|
|
62
|
-
|
|
62
|
+
normalizedConfigContext = await loadConfigContext({
|
|
63
63
|
dir: productDir,
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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 (
|
|
81
|
+
if (normalizedConfigContext) {
|
|
82
82
|
const resolved = buildResolvedDiscovery({
|
|
83
|
-
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:
|
|
95
|
-
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:
|
|
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:
|
|
117
|
-
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
|
|
143
|
+
async function loadConfigDiscoveryContext(productDir, diagnosticsMode) {
|
|
144
144
|
try {
|
|
145
|
-
const {
|
|
145
|
+
const { config, configFile } = await loadTestkitConfig(productDir);
|
|
146
146
|
return {
|
|
147
|
-
|
|
148
|
-
|
|
147
|
+
config,
|
|
148
|
+
configFile,
|
|
149
149
|
diagnostics: [],
|
|
150
150
|
};
|
|
151
151
|
} catch (error) {
|
|
152
152
|
if (diagnosticsMode === "error") throw error;
|
|
153
153
|
return {
|
|
154
|
-
|
|
155
|
-
|
|
154
|
+
config: null,
|
|
155
|
+
configFile: null,
|
|
156
156
|
diagnostics: [
|
|
157
157
|
{
|
|
158
|
-
code: "
|
|
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.
|
|
21
|
+
"testkit.config.ts",
|
|
22
22
|
`
|
|
23
|
-
import {
|
|
23
|
+
import { defineConfig } from "@elench/testkit/config";
|
|
24
24
|
|
|
25
|
-
export default
|
|
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 {
|
|
53
|
-
'export const testkit =
|
|
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 {
|
|
62
|
-
'export const testkit =
|
|
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.
|
|
138
|
+
"testkit.config.ts",
|
|
139
139
|
`
|
|
140
|
-
import {
|
|
140
|
+
import { defineConfig } from "@elench/testkit/config";
|
|
141
141
|
|
|
142
|
-
export default
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
44
|
+
"testkit.config.ts reporting.issueValidation.cacheTtlSeconds"
|
|
45
45
|
);
|
|
46
46
|
|
|
47
47
|
return {
|