@elench/testkit 0.1.145 → 0.1.147
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 +12 -0
- package/lib/cli/operations/db/schema/refresh/operation.mjs +11 -1
- package/lib/cli/operations/db/schema/verify/operation.mjs +11 -1
- package/lib/config-api/auth-fixtures.mjs +41 -10
- package/lib/database/admin.mjs +227 -0
- package/lib/database/cleanup.mjs +201 -0
- package/lib/database/constants.mjs +10 -0
- package/lib/database/index.mjs +46 -720
- package/lib/database/local-postgres.mjs +158 -0
- package/lib/database/locks.mjs +31 -0
- package/lib/database/resource-postgres.mjs +72 -0
- package/lib/database/state-files.mjs +53 -0
- package/lib/ownership/docker.mjs +9 -0
- package/lib/runner/default-runtime-runner.mjs +2 -0
- package/lib/runner/lifecycle.mjs +1 -1
- package/lib/runner/maintenance.mjs +3 -0
- package/lib/runner/playwright-runner.mjs +10 -2
- package/lib/runner/scheduler/index.mjs +3 -0
- package/lib/runtime/index.d.ts +8 -0
- package/lib/runtime-src/k6/http.js +9 -2
- 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/node_modules/es-toolkit/CHANGELOG.md +801 -0
- package/node_modules/es-toolkit/src/compat/_internal/Equals.d.ts +1 -0
- package/node_modules/es-toolkit/src/compat/_internal/IsWritable.d.ts +3 -0
- package/node_modules/es-toolkit/src/compat/_internal/MutableList.d.ts +4 -0
- package/node_modules/es-toolkit/src/compat/_internal/RejectReadonly.d.ts +4 -0
- package/node_modules/esprima/ChangeLog +235 -0
- package/package.json +6 -6
- package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/package.json +1 -1
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { execa } from "execa";
|
|
4
|
+
import {
|
|
5
|
+
LOCAL_ADMIN_DB,
|
|
6
|
+
LOCAL_IMAGE,
|
|
7
|
+
LOCAL_PASSWORD,
|
|
8
|
+
LOCAL_POLL_INTERVAL_MS,
|
|
9
|
+
LOCAL_READY_TIMEOUT_MS,
|
|
10
|
+
LOCAL_USER,
|
|
11
|
+
} from "./constants.mjs";
|
|
12
|
+
import { buildContainerName } from "./naming.mjs";
|
|
13
|
+
import { getLocalInfraDir, readStateValue } from "./state.mjs";
|
|
14
|
+
import { buildDockerResourceLabels, dockerLabelArgs } from "../ownership/docker.mjs";
|
|
15
|
+
import { writeLocalInfraState } from "./state-files.mjs";
|
|
16
|
+
|
|
17
|
+
export async function ensureLocalContainer(productDir, database = {}) {
|
|
18
|
+
const infraDir = getLocalInfraDir(productDir);
|
|
19
|
+
fs.mkdirSync(infraDir, { recursive: true });
|
|
20
|
+
|
|
21
|
+
const containerName =
|
|
22
|
+
readStateValue(path.join(infraDir, "container_name")) || buildContainerName(productDir);
|
|
23
|
+
const image = database.image || readStateValue(path.join(infraDir, "image")) || LOCAL_IMAGE;
|
|
24
|
+
const user = database.user || readStateValue(path.join(infraDir, "user")) || LOCAL_USER;
|
|
25
|
+
const password =
|
|
26
|
+
database.password || readStateValue(path.join(infraDir, "password")) || LOCAL_PASSWORD;
|
|
27
|
+
|
|
28
|
+
let inspect = await inspectContainer(containerName);
|
|
29
|
+
if (inspect && inspect.Config?.Image !== image) {
|
|
30
|
+
await stopAndRemoveContainer(containerName);
|
|
31
|
+
inspect = null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!inspect) {
|
|
35
|
+
const labels = buildLocalPostgresContainerLabels(productDir, containerName);
|
|
36
|
+
await execa("docker", [
|
|
37
|
+
"run",
|
|
38
|
+
"-d",
|
|
39
|
+
"--name",
|
|
40
|
+
containerName,
|
|
41
|
+
...dockerLabelArgs(labels),
|
|
42
|
+
"-e",
|
|
43
|
+
`POSTGRES_USER=${user}`,
|
|
44
|
+
"-e",
|
|
45
|
+
`POSTGRES_PASSWORD=${password}`,
|
|
46
|
+
"-e",
|
|
47
|
+
`POSTGRES_DB=${LOCAL_ADMIN_DB}`,
|
|
48
|
+
"-p",
|
|
49
|
+
"127.0.0.1::5432",
|
|
50
|
+
image,
|
|
51
|
+
]);
|
|
52
|
+
inspect = await inspectContainer(containerName);
|
|
53
|
+
} else if (!inspect.State?.Running) {
|
|
54
|
+
await execa("docker", ["start", containerName]);
|
|
55
|
+
inspect = await inspectContainer(containerName);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const hostPort = inspect?.NetworkSettings?.Ports?.["5432/tcp"]?.[0]?.HostPort;
|
|
59
|
+
if (!hostPort) {
|
|
60
|
+
throw new Error(`Could not determine published port for local database container ${containerName}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const infra = {
|
|
64
|
+
containerName,
|
|
65
|
+
containerId: inspect.Id,
|
|
66
|
+
image,
|
|
67
|
+
user,
|
|
68
|
+
password,
|
|
69
|
+
host: "127.0.0.1",
|
|
70
|
+
port: Number(hostPort),
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
await waitForLocalContainerReady(infra);
|
|
74
|
+
writeLocalInfraState(infraDir, infra);
|
|
75
|
+
return infra;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function loadExistingLocalContainer(productDir) {
|
|
79
|
+
const infraDir = getLocalInfraDir(productDir);
|
|
80
|
+
const containerName = readStateValue(path.join(infraDir, "container_name"));
|
|
81
|
+
if (!containerName) return null;
|
|
82
|
+
|
|
83
|
+
const inspect = await inspectContainer(containerName);
|
|
84
|
+
if (!inspect) return null;
|
|
85
|
+
|
|
86
|
+
if (!inspect.State?.Running) {
|
|
87
|
+
await execa("docker", ["start", containerName]);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const nextInspect = await inspectContainer(containerName);
|
|
91
|
+
const hostPort = nextInspect?.NetworkSettings?.Ports?.["5432/tcp"]?.[0]?.HostPort;
|
|
92
|
+
if (!hostPort) return null;
|
|
93
|
+
|
|
94
|
+
const infra = {
|
|
95
|
+
containerName,
|
|
96
|
+
containerId: nextInspect.Id,
|
|
97
|
+
image: nextInspect.Config?.Image || readStateValue(path.join(infraDir, "image")) || LOCAL_IMAGE,
|
|
98
|
+
user: readStateValue(path.join(infraDir, "user")) || LOCAL_USER,
|
|
99
|
+
password: readStateValue(path.join(infraDir, "password")) || LOCAL_PASSWORD,
|
|
100
|
+
host: "127.0.0.1",
|
|
101
|
+
port: Number(hostPort),
|
|
102
|
+
};
|
|
103
|
+
await waitForLocalContainerReady(infra);
|
|
104
|
+
return infra;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function inspectContainer(containerName) {
|
|
108
|
+
try {
|
|
109
|
+
const { stdout } = await execa("docker", ["inspect", containerName]);
|
|
110
|
+
const parsed = JSON.parse(stdout);
|
|
111
|
+
return parsed[0] || null;
|
|
112
|
+
} catch {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function stopAndRemoveContainer(containerName) {
|
|
118
|
+
try {
|
|
119
|
+
await execa("docker", ["rm", "-f", containerName]);
|
|
120
|
+
} catch {
|
|
121
|
+
// Already gone.
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function waitForLocalContainerReady(infra) {
|
|
126
|
+
const startedAt = Date.now();
|
|
127
|
+
while (Date.now() - startedAt < LOCAL_READY_TIMEOUT_MS) {
|
|
128
|
+
try {
|
|
129
|
+
await execa("docker", [
|
|
130
|
+
"exec",
|
|
131
|
+
infra.containerName,
|
|
132
|
+
"pg_isready",
|
|
133
|
+
"-U",
|
|
134
|
+
infra.user,
|
|
135
|
+
"-d",
|
|
136
|
+
LOCAL_ADMIN_DB,
|
|
137
|
+
]);
|
|
138
|
+
return;
|
|
139
|
+
} catch {
|
|
140
|
+
await sleep(LOCAL_POLL_INTERVAL_MS);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
throw new Error(`Timed out waiting for local database container ${infra.containerName}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function buildLocalPostgresContainerLabels(productDir, containerName) {
|
|
148
|
+
return buildDockerResourceLabels(productDir, {
|
|
149
|
+
kind: "postgres-container",
|
|
150
|
+
name: containerName,
|
|
151
|
+
scope: "local-postgres",
|
|
152
|
+
cachePolicy: "product-cache",
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function sleep(ms) {
|
|
157
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
158
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
export async function withLock(lockPath, fn) {
|
|
5
|
+
const lockDir = `${lockPath}.dir`;
|
|
6
|
+
const timeoutMs = 60_000;
|
|
7
|
+
const startedAt = Date.now();
|
|
8
|
+
|
|
9
|
+
while (true) {
|
|
10
|
+
try {
|
|
11
|
+
fs.mkdirSync(lockDir, { recursive: false });
|
|
12
|
+
break;
|
|
13
|
+
} catch (error) {
|
|
14
|
+
if (error.code !== "EEXIST") throw error;
|
|
15
|
+
if (Date.now() - startedAt > timeoutMs) {
|
|
16
|
+
throw new Error(`Timed out waiting for lock ${path.basename(lockPath)}`);
|
|
17
|
+
}
|
|
18
|
+
await sleep(200);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
return await fn();
|
|
24
|
+
} finally {
|
|
25
|
+
fs.rmSync(lockDir, { recursive: true, force: true });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function sleep(ms) {
|
|
30
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
31
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { LOCAL_ADMIN_DB } from "./constants.mjs";
|
|
3
|
+
import { readResourceConnectionState } from "./state-files.mjs";
|
|
4
|
+
|
|
5
|
+
export function resolveResourcePostgresInfra(config, processEnv = process.env) {
|
|
6
|
+
const resourceName = String(config.testkit?.database?.resource || "").trim();
|
|
7
|
+
if (!resourceName) {
|
|
8
|
+
throw new Error("Resource-backed Postgres database requires database.resource");
|
|
9
|
+
}
|
|
10
|
+
const connections = parseResourceConnections(processEnv.TESTKIT_RESOURCE_CONNECTIONS_JSON);
|
|
11
|
+
const connection = connections[resourceName];
|
|
12
|
+
if (!connection) {
|
|
13
|
+
const available = Object.keys(connections).sort().join(", ") || "none";
|
|
14
|
+
throw new Error(`Postgres resource "${resourceName}" is not available. Available resources: ${available}`);
|
|
15
|
+
}
|
|
16
|
+
return normalizeResourcePostgresConnection(resourceName, connection);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function resolveResourcePostgresInfraFromState(stateDir, readStateValue, processEnv = process.env) {
|
|
20
|
+
const resourceName = readStateValue(path.join(stateDir, "resource_name"));
|
|
21
|
+
if (!resourceName) return null;
|
|
22
|
+
const connections = parseResourceConnections(processEnv.TESTKIT_RESOURCE_CONNECTIONS_JSON);
|
|
23
|
+
const connection = connections[resourceName] || readResourceConnectionState(stateDir, readStateValue);
|
|
24
|
+
return connection ? normalizeResourcePostgresConnection(resourceName, connection) : null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseResourceConnections(raw) {
|
|
28
|
+
if (!raw) return {};
|
|
29
|
+
try {
|
|
30
|
+
const parsed = JSON.parse(raw);
|
|
31
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
32
|
+
} catch (error) {
|
|
33
|
+
throw new Error(`Invalid TESTKIT_RESOURCE_CONNECTIONS_JSON: ${error.message}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizeResourcePostgresConnection(resourceName, connection) {
|
|
38
|
+
const fromUrl = connection.url ? parsePostgresConnectionUrl(connection.url) : {};
|
|
39
|
+
const host = connection.host || fromUrl.host;
|
|
40
|
+
const port = Number(connection.port || fromUrl.port || 5432);
|
|
41
|
+
const user = connection.user || fromUrl.user;
|
|
42
|
+
const password = connection.password || fromUrl.password || "";
|
|
43
|
+
const adminDatabase = connection.adminDatabase || connection.admin_database || fromUrl.database || LOCAL_ADMIN_DB;
|
|
44
|
+
const sslMode = connection.sslMode || connection.sslmode || fromUrl.sslMode || "disable";
|
|
45
|
+
if (!host) throw new Error(`Postgres resource "${resourceName}" connection is missing host`);
|
|
46
|
+
if (!Number.isInteger(port) || port <= 0) {
|
|
47
|
+
throw new Error(`Postgres resource "${resourceName}" connection has invalid port`);
|
|
48
|
+
}
|
|
49
|
+
if (!user) throw new Error(`Postgres resource "${resourceName}" connection is missing user`);
|
|
50
|
+
return {
|
|
51
|
+
backend: "resource",
|
|
52
|
+
resourceName,
|
|
53
|
+
host,
|
|
54
|
+
port,
|
|
55
|
+
user,
|
|
56
|
+
password,
|
|
57
|
+
adminDatabase,
|
|
58
|
+
sslMode,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parsePostgresConnectionUrl(rawUrl) {
|
|
63
|
+
const parsed = new URL(rawUrl);
|
|
64
|
+
return {
|
|
65
|
+
host: parsed.hostname,
|
|
66
|
+
port: parsed.port ? Number(parsed.port) : 5432,
|
|
67
|
+
database: decodeURIComponent(parsed.pathname.replace(/^\//, "")) || LOCAL_ADMIN_DB,
|
|
68
|
+
user: decodeURIComponent(parsed.username || ""),
|
|
69
|
+
password: decodeURIComponent(parsed.password || ""),
|
|
70
|
+
sslMode: parsed.searchParams.get("sslmode") || undefined,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { LOCAL_ADMIN_DB } from "./constants.mjs";
|
|
4
|
+
import { buildProductIdentity } from "../ownership/docker.mjs";
|
|
5
|
+
|
|
6
|
+
export function writeLocalInfraState(infraDir, infra) {
|
|
7
|
+
const product = buildProductIdentity(path.resolve(infraDir, "..", "..", ".."));
|
|
8
|
+
fs.writeFileSync(path.join(infraDir, "database_backend"), "local");
|
|
9
|
+
fs.writeFileSync(path.join(infraDir, "container_name"), infra.containerName);
|
|
10
|
+
fs.writeFileSync(path.join(infraDir, "container_id"), infra.containerId);
|
|
11
|
+
fs.writeFileSync(path.join(infraDir, "ownership_product_id"), product.id);
|
|
12
|
+
fs.writeFileSync(path.join(infraDir, "image"), infra.image);
|
|
13
|
+
fs.writeFileSync(path.join(infraDir, "user"), infra.user);
|
|
14
|
+
fs.writeFileSync(path.join(infraDir, "password"), infra.password);
|
|
15
|
+
fs.writeFileSync(path.join(infraDir, "host"), infra.host);
|
|
16
|
+
fs.writeFileSync(path.join(infraDir, "port"), String(infra.port));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function writeCacheState(cacheDir, config, infra, templateDbName, fingerprint) {
|
|
20
|
+
const backend = config.testkit.database.provider;
|
|
21
|
+
fs.writeFileSync(path.join(cacheDir, "database_backend"), backend);
|
|
22
|
+
fs.writeFileSync(path.join(cacheDir, "template_database_name"), templateDbName);
|
|
23
|
+
fs.writeFileSync(path.join(cacheDir, "template_fingerprint"), fingerprint);
|
|
24
|
+
if (backend === "local") {
|
|
25
|
+
fs.writeFileSync(path.join(cacheDir, "container_name"), infra.containerName);
|
|
26
|
+
} else {
|
|
27
|
+
fs.writeFileSync(path.join(cacheDir, "resource_name"), infra.resourceName);
|
|
28
|
+
writeResourceConnectionState(cacheDir, infra);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function writeResourceConnectionState(stateDir, infra) {
|
|
33
|
+
fs.writeFileSync(path.join(stateDir, "resource_host"), infra.host);
|
|
34
|
+
fs.writeFileSync(path.join(stateDir, "resource_port"), String(infra.port));
|
|
35
|
+
fs.writeFileSync(path.join(stateDir, "resource_user"), infra.user);
|
|
36
|
+
fs.writeFileSync(path.join(stateDir, "resource_password"), infra.password);
|
|
37
|
+
fs.writeFileSync(path.join(stateDir, "resource_admin_database"), infra.adminDatabase || LOCAL_ADMIN_DB);
|
|
38
|
+
fs.writeFileSync(path.join(stateDir, "resource_sslmode"), infra.sslMode || "disable");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function readResourceConnectionState(stateDir, readStateValue) {
|
|
42
|
+
const host = readStateValue(path.join(stateDir, "resource_host"));
|
|
43
|
+
const user = readStateValue(path.join(stateDir, "resource_user"));
|
|
44
|
+
if (!host || !user) return null;
|
|
45
|
+
return {
|
|
46
|
+
host,
|
|
47
|
+
port: Number(readStateValue(path.join(stateDir, "resource_port")) || 5432),
|
|
48
|
+
user,
|
|
49
|
+
password: readStateValue(path.join(stateDir, "resource_password")) || "",
|
|
50
|
+
adminDatabase: readStateValue(path.join(stateDir, "resource_admin_database")) || LOCAL_ADMIN_DB,
|
|
51
|
+
sslMode: readStateValue(path.join(stateDir, "resource_sslmode")) || "disable",
|
|
52
|
+
};
|
|
53
|
+
}
|
package/lib/ownership/docker.mjs
CHANGED
|
@@ -74,6 +74,15 @@ export async function removeDockerContainer(containerRef) {
|
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
export async function stopDockerContainer(containerRef) {
|
|
78
|
+
try {
|
|
79
|
+
await execa("docker", ["stop", "--time", "5", containerRef]);
|
|
80
|
+
return true;
|
|
81
|
+
} catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
77
86
|
export function dockerContainerSummary(container) {
|
|
78
87
|
const status = container.running ? "running" : "stopped";
|
|
79
88
|
return `${container.name} (${status})`;
|
|
@@ -97,6 +97,8 @@ export async function runDefaultRuntimeTask(
|
|
|
97
97
|
lease,
|
|
98
98
|
{
|
|
99
99
|
...buildFileTimeoutEnv(fileTimeoutSeconds, startedAt),
|
|
100
|
+
TESTKIT_TEST_FILE: task.file,
|
|
101
|
+
TESTKIT_TEST_ID: String(task.id),
|
|
100
102
|
...(task.scenarioSeed ? { TESTKIT_SCENARIO_SEED: task.scenarioSeed } : {}),
|
|
101
103
|
},
|
|
102
104
|
process.env
|
package/lib/runner/lifecycle.mjs
CHANGED
|
@@ -189,7 +189,7 @@ export async function cleanupRuns(productDir, { includeActive = false } = {}) {
|
|
|
189
189
|
summary.cleaned.push(manifest);
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
-
await cleanupOrphanedLocalInfrastructure(productDir);
|
|
192
|
+
summary.resourceCleanup = await cleanupOrphanedLocalInfrastructure(productDir);
|
|
193
193
|
return summary;
|
|
194
194
|
}
|
|
195
195
|
|
|
@@ -126,6 +126,9 @@ export async function cleanup(productDir, options = {}) {
|
|
|
126
126
|
label: "assistant session",
|
|
127
127
|
dryRun,
|
|
128
128
|
});
|
|
129
|
+
for (const target of summary.resourceCleanup?.targets || []) {
|
|
130
|
+
lines.push(formatDatabaseResourceCleanupLine(target, dryRun));
|
|
131
|
+
}
|
|
129
132
|
for (const target of resourceCleanup.targets) {
|
|
130
133
|
lines.push(formatDatabaseResourceCleanupLine(target, dryRun));
|
|
131
134
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import fs from "fs";
|
|
3
|
+
import { createRequire } from "module";
|
|
3
4
|
import { parsePlaywrightJsonResults } from "../reporters/playwright.mjs";
|
|
4
5
|
import { resolveServiceCwd } from "../config/paths.mjs";
|
|
5
6
|
import { buildFileTimeoutEnv, formatFileTimeoutBudgetError } from "../shared/file-timeout.mjs";
|
|
@@ -10,6 +11,8 @@ import { normalizePathSeparators } from "./state.mjs";
|
|
|
10
11
|
import { buildPlaywrightEnv } from "./template.mjs";
|
|
11
12
|
import { settleManagedSubprocess, startManagedSubprocess } from "./subprocess.mjs";
|
|
12
13
|
|
|
14
|
+
const require = createRequire(import.meta.url);
|
|
15
|
+
|
|
13
16
|
export async function runPlaywrightTask(targetConfig, task, lifecycle, lease, reporter = null) {
|
|
14
17
|
const local = targetConfig.testkit.local;
|
|
15
18
|
if (!local?.baseUrl) {
|
|
@@ -33,9 +36,10 @@ export async function runPlaywrightTask(targetConfig, task, lifecycle, lease, re
|
|
|
33
36
|
{ globalTimeoutMs: fileTimeoutSeconds * 1000 }
|
|
34
37
|
);
|
|
35
38
|
const jsonReportPath = buildPlaywrightJsonReportPath(lease, task);
|
|
39
|
+
const playwrightCliPath = resolvePlaywrightCliPath();
|
|
36
40
|
const subprocess = startManagedSubprocess(
|
|
37
|
-
|
|
38
|
-
[
|
|
41
|
+
process.execPath,
|
|
42
|
+
[playwrightCliPath, "test", "--config", playwrightConfigPath, "--reporter=json"],
|
|
39
43
|
{
|
|
40
44
|
cwd,
|
|
41
45
|
env: {
|
|
@@ -113,6 +117,10 @@ export async function runPlaywrightTask(targetConfig, task, lifecycle, lease, re
|
|
|
113
117
|
};
|
|
114
118
|
}
|
|
115
119
|
|
|
120
|
+
export function resolvePlaywrightCliPath() {
|
|
121
|
+
return require.resolve("@playwright/test/cli");
|
|
122
|
+
}
|
|
123
|
+
|
|
116
124
|
export function buildPlaywrightJsonReportPath(lease, task) {
|
|
117
125
|
if (!lease?.leaseDir) {
|
|
118
126
|
throw new Error(`Playwright task ${task?.file || ""} requires a lease-scoped directory`);
|
|
@@ -105,6 +105,9 @@ export function compareScheduledTasks(a, b) {
|
|
|
105
105
|
|
|
106
106
|
function resolveTaskLocks(config, suite, file) {
|
|
107
107
|
const locks = new Set();
|
|
108
|
+
if (suite.type === "dal") {
|
|
109
|
+
locks.add(`database:${config.name}`);
|
|
110
|
+
}
|
|
108
111
|
const matchedSuiteRules = config.testkit.requirements?.suites || [];
|
|
109
112
|
for (const rule of matchedSuiteRules) {
|
|
110
113
|
if (matchesSuiteSelectors(suite.displayType, suite.name, [rule.selector])) {
|
package/lib/runtime/index.d.ts
CHANGED
|
@@ -61,6 +61,7 @@ export interface WaitForOptions {
|
|
|
61
61
|
export interface RuntimeEnv {
|
|
62
62
|
BASE: string;
|
|
63
63
|
MACHINE_ID?: string;
|
|
64
|
+
rawEnv?: Record<string, string | undefined>;
|
|
64
65
|
routeParams: RuntimeHeaders;
|
|
65
66
|
}
|
|
66
67
|
|
|
@@ -290,6 +291,13 @@ export interface ActorRequestClient {
|
|
|
290
291
|
}
|
|
291
292
|
|
|
292
293
|
export interface HttpClient<TSetup = unknown> {
|
|
294
|
+
(
|
|
295
|
+
method: RuntimeMethod,
|
|
296
|
+
path: string,
|
|
297
|
+
setupData?: TSetup | null,
|
|
298
|
+
body?: unknown,
|
|
299
|
+
extraHeaders?: RuntimeHeaders
|
|
300
|
+
): RuntimeResponse;
|
|
293
301
|
as(actorName: string): ActorRequestClient;
|
|
294
302
|
headers(extraHeaders?: RuntimeHeaders): RuntimeHeaders;
|
|
295
303
|
multipart: MultipartRequestClient;
|
|
@@ -233,7 +233,12 @@ export function createHttpClient(config) {
|
|
|
233
233
|
const defaultClient = createActorClient(defaultActor);
|
|
234
234
|
const rawClient = createRawInvoker(null);
|
|
235
235
|
|
|
236
|
-
|
|
236
|
+
function client(method, path, setupDataOrBody = null, body, extraHeaders = {}) {
|
|
237
|
+
const requestBody = arguments.length >= 4 ? body : setupDataOrBody;
|
|
238
|
+
return defaultClient.request(method, path, requestBody, extraHeaders);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
Object.assign(client, {
|
|
237
242
|
rawHttp: http,
|
|
238
243
|
headers(extraHeaders = {}) {
|
|
239
244
|
return resolvedHeadersFor(defaultActor, extraHeaders);
|
|
@@ -299,7 +304,9 @@ export function createHttpClient(config) {
|
|
|
299
304
|
return rawClient.multipart.patch(path, payload, extraHeaders);
|
|
300
305
|
},
|
|
301
306
|
},
|
|
302
|
-
};
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
return client;
|
|
303
310
|
}
|
|
304
311
|
|
|
305
312
|
export function makeReq(baseUrl, sessionBundle = null, routeHeaders = {}, getHeaders = null, defaultActor = null) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elench/testkit-bridge",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.147",
|
|
4
4
|
"description": "Browser bridge helpers for testkit",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@elench/testkit-protocol": "0.1.
|
|
25
|
+
"@elench/testkit-protocol": "0.1.146"
|
|
26
26
|
},
|
|
27
27
|
"private": false
|
|
28
28
|
}
|