@elench/testkit 0.1.116 → 0.1.117
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 +9 -0
- package/lib/cli/operations/db/schema/refresh/operation.mjs +2 -0
- package/lib/cli/renderers/db-schema/text.mjs +8 -1
- package/lib/database/schema-source.mjs +69 -15
- package/lib/database/source-refresh-lock.mjs +69 -0
- package/lib/database/source-url.mjs +110 -0
- package/lib/database/template-steps.mjs +16 -8
- 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 +5 -5
- package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/package.json +1 -1
- package/node_modules/es-toolkit/CHANGELOG.md +0 -801
- package/node_modules/es-toolkit/src/compat/_internal/Equals.d.ts +0 -1
- package/node_modules/es-toolkit/src/compat/_internal/IsWritable.d.ts +0 -3
- package/node_modules/es-toolkit/src/compat/_internal/MutableList.d.ts +0 -4
- package/node_modules/es-toolkit/src/compat/_internal/RejectReadonly.d.ts +0 -4
- package/node_modules/esprima/ChangeLog +0 -235
package/README.md
CHANGED
|
@@ -346,6 +346,15 @@ is never written into the baseline. Keep schema-changing setup in its own step
|
|
|
346
346
|
where possible; a single command that changes schema and then fails before
|
|
347
347
|
exiting cannot be refreshed at the midpoint.
|
|
348
348
|
|
|
349
|
+
Source schema refreshes are intentionally single-connection and pooler-safe.
|
|
350
|
+
If a Neon pooled source URL is configured, Testkit rewrites it to the matching
|
|
351
|
+
direct Neon endpoint before running `pg_dump` and records the original/resolved
|
|
352
|
+
host classifications in `.testkit/db/<service>/source-schema.meta.json`. Unknown
|
|
353
|
+
PgBouncer/pooler URLs fail closed; configure a direct source URL for those
|
|
354
|
+
providers. Concurrent refreshes for the same service are serialized with a
|
|
355
|
+
cache-local lock so multiple Testkit processes do not stampede the source
|
|
356
|
+
database.
|
|
357
|
+
|
|
349
358
|
For most repos, prefer declarative step objects directly inside
|
|
350
359
|
`database.postgres({ template: ... })` and `runtime.prepare.steps`.
|
|
351
360
|
The supported shapes are:
|
|
@@ -48,6 +48,8 @@ export async function executeDatabaseSchemaRefreshOperation(options = {}) {
|
|
|
48
48
|
outputPath,
|
|
49
49
|
outputLabel: path.relative(productDir, outputPath) || path.basename(outputPath),
|
|
50
50
|
envName: state.envName || null,
|
|
51
|
+
sourceUrl: state.refreshInfo?.metadata?.sourceUrl || null,
|
|
52
|
+
reusedExistingRefresh: Boolean(state.refreshInfo?.reusedExistingRefresh),
|
|
51
53
|
};
|
|
52
54
|
} finally {
|
|
53
55
|
logRegistry.closeAll();
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
export function renderDatabaseSchemaRefreshResult(result) {
|
|
2
|
-
|
|
2
|
+
const lines = [`Refreshed ${result.outputLabel}`];
|
|
3
|
+
if (result.sourceUrl?.rewritten && result.sourceUrl.originalClassification === "neon-pooler") {
|
|
4
|
+
lines.push("Source schema URL uses Neon pooler; Testkit used the direct endpoint for pg_dump.");
|
|
5
|
+
}
|
|
6
|
+
if (result.reusedExistingRefresh) {
|
|
7
|
+
lines.push("Another Testkit process refreshed this source schema cache first; reused that result.");
|
|
8
|
+
}
|
|
9
|
+
return lines;
|
|
3
10
|
}
|
|
4
11
|
|
|
5
12
|
export function renderDatabaseSchemaVerifyResult(result) {
|
|
@@ -5,6 +5,8 @@ import { execa } from "execa";
|
|
|
5
5
|
import { resolveServiceCwd } from "../config/paths.mjs";
|
|
6
6
|
import { buildExecutionEnv } from "../runner/template.mjs";
|
|
7
7
|
import { dumpPostgresSchemaToFile, captureTemplateSnapshotText, runTemplateStep, sanitizeSnapshotText } from "./template-steps.mjs";
|
|
8
|
+
import { getSourceSchemaRefreshLockPath, withSourceSchemaRefreshLock } from "./source-refresh-lock.mjs";
|
|
9
|
+
import { resolveSourceSchemaDumpUrl } from "./source-url.mjs";
|
|
8
10
|
|
|
9
11
|
const SOURCE_SCHEMA_DIR = path.join(".testkit", "db");
|
|
10
12
|
const SOURCE_SCHEMA_FILE = "source-schema.sql";
|
|
@@ -57,8 +59,8 @@ export async function prepareSourceSchemaCache(config, options = {}) {
|
|
|
57
59
|
|
|
58
60
|
if (state.sourceUrl) {
|
|
59
61
|
if (shouldRefreshSourceSchema(state)) {
|
|
60
|
-
await refreshSourceSchemaCache(config, state, options);
|
|
61
|
-
return { ...state, refreshed: true, cacheExists: true };
|
|
62
|
+
const refreshInfo = await refreshSourceSchemaCache(config, state, options);
|
|
63
|
+
return { ...state, refreshed: true, cacheExists: true, refreshInfo };
|
|
62
64
|
}
|
|
63
65
|
options.setupRegistry?.recordCached({
|
|
64
66
|
config,
|
|
@@ -93,8 +95,8 @@ export async function forceRefreshSourceSchemaCache(config, previousState, optio
|
|
|
93
95
|
const envLabel = previousState?.envName || previousState?.sourceSchema?.env || "source schema env";
|
|
94
96
|
throw new Error(`Cannot refresh source schema for service "${config.name}" because ${envLabel} is unavailable.`);
|
|
95
97
|
}
|
|
96
|
-
await refreshSourceSchemaCache(config, state, options);
|
|
97
|
-
return { ...state, refreshed: true, cacheExists: true };
|
|
98
|
+
const refreshInfo = await refreshSourceSchemaCache(config, state, options);
|
|
99
|
+
return { ...state, refreshed: true, cacheExists: true, refreshInfo };
|
|
98
100
|
}
|
|
99
101
|
|
|
100
102
|
export async function applySourceSchemaCache(config, databaseUrl, state, options = {}) {
|
|
@@ -171,6 +173,7 @@ export function appendSourceSchemaCacheToHash(hash, config) {
|
|
|
171
173
|
}
|
|
172
174
|
|
|
173
175
|
async function refreshSourceSchemaCache(config, state, options = {}) {
|
|
176
|
+
let refreshInfo = null;
|
|
174
177
|
const operation = options.setupRegistry?.start({
|
|
175
178
|
config,
|
|
176
179
|
stage: "source-schema:refresh",
|
|
@@ -178,16 +181,31 @@ async function refreshSourceSchemaCache(config, state, options = {}) {
|
|
|
178
181
|
summary: `source schema refresh: ${path.relative(config.productDir, state.cachePath)}`,
|
|
179
182
|
});
|
|
180
183
|
try {
|
|
181
|
-
await
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
184
|
+
refreshInfo = await withSourceSchemaRefreshLock(
|
|
185
|
+
getSourceSchemaRefreshLockPath(state.cachePath),
|
|
186
|
+
async ({ waited, requestedAt }) => {
|
|
187
|
+
const cachedAfterWaiting = readRecentSourceSchemaMetadata(state, requestedAt);
|
|
188
|
+
if (cachedAfterWaiting) {
|
|
189
|
+
return {
|
|
190
|
+
reusedExistingRefresh: true,
|
|
191
|
+
waitedForLock: waited,
|
|
192
|
+
metadata: cachedAfterWaiting,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
const result = await writeSourceSchemaCacheFromUrl(config, state.sourceUrl, state.cachePath, {
|
|
196
|
+
reporter: options.reporter || null,
|
|
197
|
+
logRecord: operation?._logRecord || null,
|
|
198
|
+
env: options.env || process.env,
|
|
199
|
+
});
|
|
200
|
+
const metadata = buildSourceSchemaMetadata(config, state, result);
|
|
201
|
+
writeSourceSchemaMetadata(state.metadataPath, metadata);
|
|
202
|
+
return {
|
|
203
|
+
reusedExistingRefresh: false,
|
|
204
|
+
waitedForLock: waited,
|
|
205
|
+
metadata,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
);
|
|
191
209
|
const finished = operation
|
|
192
210
|
? options.setupRegistry.finish(operation, {
|
|
193
211
|
status: "passed",
|
|
@@ -195,6 +213,7 @@ async function refreshSourceSchemaCache(config, state, options = {}) {
|
|
|
195
213
|
})
|
|
196
214
|
: null;
|
|
197
215
|
if (finished) options.reporter?.setupOperationFinished?.(finished);
|
|
216
|
+
return refreshInfo;
|
|
198
217
|
} catch (error) {
|
|
199
218
|
const finished = operation
|
|
200
219
|
? options.setupRegistry.finish(operation, {
|
|
@@ -209,17 +228,52 @@ async function refreshSourceSchemaCache(config, state, options = {}) {
|
|
|
209
228
|
}
|
|
210
229
|
|
|
211
230
|
async function writeSourceSchemaCacheFromUrl(config, sourceUrl, cachePath, options = {}) {
|
|
231
|
+
const resolution = resolveSourceSchemaDumpUrl(sourceUrl);
|
|
212
232
|
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
213
233
|
const tempDir = fs.mkdtempSync(path.join(path.dirname(cachePath), ".source-schema-"));
|
|
214
234
|
const tempPath = path.join(tempDir, path.basename(cachePath));
|
|
215
235
|
try {
|
|
216
|
-
await dumpPostgresSchemaToFile(config, tempPath,
|
|
236
|
+
await dumpPostgresSchemaToFile(config, tempPath, resolution.dumpUrl, {
|
|
237
|
+
...options,
|
|
238
|
+
pgApplicationName: sourceSchemaPgApplicationName(config.name),
|
|
239
|
+
});
|
|
217
240
|
fs.renameSync(tempPath, cachePath);
|
|
241
|
+
return {
|
|
242
|
+
resolution,
|
|
243
|
+
pgDump: {
|
|
244
|
+
args: ["--schema-only", "--no-owner", "--no-privileges"],
|
|
245
|
+
applicationName: sourceSchemaPgApplicationName(config.name),
|
|
246
|
+
},
|
|
247
|
+
};
|
|
218
248
|
} finally {
|
|
219
249
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
220
250
|
}
|
|
221
251
|
}
|
|
222
252
|
|
|
253
|
+
function buildSourceSchemaMetadata(config, state, result) {
|
|
254
|
+
return {
|
|
255
|
+
refreshedAt: new Date().toISOString(),
|
|
256
|
+
serviceName: config.name,
|
|
257
|
+
envName: state.envName,
|
|
258
|
+
cachePath: path.relative(config.productDir, state.cachePath),
|
|
259
|
+
sourceUrl: result.resolution.metadata,
|
|
260
|
+
pgDump: result.pgDump,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function readRecentSourceSchemaMetadata(state, requestedAt) {
|
|
265
|
+
if (!fs.existsSync(state.cachePath)) return null;
|
|
266
|
+
const metadata = readJson(state.metadataPath);
|
|
267
|
+
const refreshedAt = metadata?.refreshedAt ? Date.parse(metadata.refreshedAt) : 0;
|
|
268
|
+
if (!Number.isFinite(refreshedAt) || refreshedAt < requestedAt) return null;
|
|
269
|
+
return metadata;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function sourceSchemaPgApplicationName(serviceName) {
|
|
273
|
+
const suffix = String(serviceName).replace(/[^a-zA-Z0-9._:-]+/g, "-").slice(0, 24) || "service";
|
|
274
|
+
return `testkit:schema-source:${suffix}`;
|
|
275
|
+
}
|
|
276
|
+
|
|
223
277
|
function readSourceSchemaCacheText(cachePath) {
|
|
224
278
|
return sanitizeSnapshotText(fs.readFileSync(cachePath, "utf8"));
|
|
225
279
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_STALE_MS = 10 * 60 * 1000;
|
|
5
|
+
const DEFAULT_TIMEOUT_MS = 120 * 1000;
|
|
6
|
+
const DEFAULT_POLL_MS = 100;
|
|
7
|
+
|
|
8
|
+
export async function withSourceSchemaRefreshLock(lockPath, task, options = {}) {
|
|
9
|
+
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
10
|
+
const staleMs = options.staleMs || DEFAULT_STALE_MS;
|
|
11
|
+
const timeoutMs = options.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
12
|
+
const pollMs = options.pollMs || DEFAULT_POLL_MS;
|
|
13
|
+
const startedAt = Date.now();
|
|
14
|
+
|
|
15
|
+
let waited = false;
|
|
16
|
+
while (true) {
|
|
17
|
+
try {
|
|
18
|
+
const fd = fs.openSync(lockPath, "wx");
|
|
19
|
+
try {
|
|
20
|
+
fs.writeFileSync(
|
|
21
|
+
fd,
|
|
22
|
+
`${JSON.stringify(
|
|
23
|
+
{
|
|
24
|
+
pid: process.pid,
|
|
25
|
+
createdAt: new Date().toISOString(),
|
|
26
|
+
},
|
|
27
|
+
null,
|
|
28
|
+
2
|
|
29
|
+
)}\n`
|
|
30
|
+
);
|
|
31
|
+
} finally {
|
|
32
|
+
fs.closeSync(fd);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
return await task({ waited, requestedAt: startedAt });
|
|
37
|
+
} finally {
|
|
38
|
+
fs.rmSync(lockPath, { force: true });
|
|
39
|
+
}
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (error?.code !== "EEXIST") throw error;
|
|
42
|
+
waited = true;
|
|
43
|
+
removeStaleLock(lockPath, staleMs);
|
|
44
|
+
if (Date.now() - startedAt > timeoutMs) {
|
|
45
|
+
throw new Error(`Timed out waiting for source schema refresh lock: ${lockPath}`);
|
|
46
|
+
}
|
|
47
|
+
await sleep(pollMs);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function getSourceSchemaRefreshLockPath(cachePath) {
|
|
53
|
+
return path.join(path.dirname(cachePath), "source-schema.refresh.lock");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function removeStaleLock(lockPath, staleMs) {
|
|
57
|
+
try {
|
|
58
|
+
const stat = fs.statSync(lockPath);
|
|
59
|
+
if (Date.now() - stat.mtimeMs > staleMs) {
|
|
60
|
+
fs.rmSync(lockPath, { force: true });
|
|
61
|
+
}
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (error?.code !== "ENOENT") throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function sleep(ms) {
|
|
68
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
69
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
const NEON_HOST_SUFFIX = ".neon.tech";
|
|
2
|
+
const NEON_POOLER_MARKER = "-pooler.";
|
|
3
|
+
const UNSUPPORTED_POOLER_PATTERN = /(^|[-.])(pgbouncer|pooler)([-.]|$)/i;
|
|
4
|
+
|
|
5
|
+
export function resolveSourceSchemaDumpUrl(databaseUrl) {
|
|
6
|
+
const original = parseDatabaseUrl(databaseUrl);
|
|
7
|
+
const originalClassification = classifyDatabaseHost(original.hostname);
|
|
8
|
+
|
|
9
|
+
if (originalClassification.kind === "neon-pooler") {
|
|
10
|
+
const resolved = new URL(original.url.href);
|
|
11
|
+
resolved.hostname = original.hostname.replace(NEON_POOLER_MARKER, ".");
|
|
12
|
+
return buildResolution({
|
|
13
|
+
original,
|
|
14
|
+
resolved,
|
|
15
|
+
originalClassification,
|
|
16
|
+
resolvedClassification: classifyDatabaseHost(resolved.hostname),
|
|
17
|
+
rewritten: true,
|
|
18
|
+
notice: "Source schema URL uses Neon pooler; Testkit is using the direct endpoint for pg_dump.",
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (originalClassification.kind === "unsupported-pooler") {
|
|
23
|
+
throw new Error(
|
|
24
|
+
[
|
|
25
|
+
"Refusing to run pg_dump through a pooled source database URL.",
|
|
26
|
+
`Host: ${original.hostname}`,
|
|
27
|
+
"Configure a direct source database URL.",
|
|
28
|
+
].join("\n")
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return buildResolution({
|
|
33
|
+
original,
|
|
34
|
+
resolved: original.url,
|
|
35
|
+
originalClassification,
|
|
36
|
+
resolvedClassification: originalClassification,
|
|
37
|
+
rewritten: false,
|
|
38
|
+
notice: null,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function classifyDatabaseHost(hostname) {
|
|
43
|
+
const host = String(hostname || "").toLowerCase();
|
|
44
|
+
if (host.endsWith(NEON_HOST_SUFFIX) && host.includes(NEON_POOLER_MARKER)) {
|
|
45
|
+
return { kind: "neon-pooler", host };
|
|
46
|
+
}
|
|
47
|
+
if (host.endsWith(NEON_HOST_SUFFIX)) {
|
|
48
|
+
return { kind: "neon-direct", host };
|
|
49
|
+
}
|
|
50
|
+
if (UNSUPPORTED_POOLER_PATTERN.test(host)) {
|
|
51
|
+
return { kind: "unsupported-pooler", host };
|
|
52
|
+
}
|
|
53
|
+
return { kind: "unknown-direct", host };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function redactDatabaseUrl(databaseUrl) {
|
|
57
|
+
try {
|
|
58
|
+
const parsed = new URL(databaseUrl);
|
|
59
|
+
if (parsed.username) parsed.username = "REDACTED";
|
|
60
|
+
if (parsed.password) parsed.password = "REDACTED";
|
|
61
|
+
return parsed.toString();
|
|
62
|
+
} catch {
|
|
63
|
+
return "[invalid database URL]";
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function assertPgDumpArgsAreSourceSafe(args) {
|
|
68
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
69
|
+
const arg = String(args[index]);
|
|
70
|
+
if (arg === "-j" || arg === "--jobs" || arg.startsWith("--jobs=")) {
|
|
71
|
+
throw new Error("Source schema pg_dump must not use parallel jobs.");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function parseDatabaseUrl(databaseUrl) {
|
|
77
|
+
try {
|
|
78
|
+
const url = new URL(databaseUrl);
|
|
79
|
+
if (!url.hostname) throw new Error("missing host");
|
|
80
|
+
return {
|
|
81
|
+
url,
|
|
82
|
+
hostname: url.hostname.toLowerCase(),
|
|
83
|
+
};
|
|
84
|
+
} catch (error) {
|
|
85
|
+
throw new Error(`Invalid source database URL: ${error?.message || error}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function buildResolution({
|
|
90
|
+
original,
|
|
91
|
+
resolved,
|
|
92
|
+
originalClassification,
|
|
93
|
+
resolvedClassification,
|
|
94
|
+
rewritten,
|
|
95
|
+
notice,
|
|
96
|
+
}) {
|
|
97
|
+
return {
|
|
98
|
+
originalUrl: original.url.href,
|
|
99
|
+
dumpUrl: resolved.href,
|
|
100
|
+
rewritten,
|
|
101
|
+
notice,
|
|
102
|
+
metadata: {
|
|
103
|
+
originalHost: original.hostname,
|
|
104
|
+
originalClassification: originalClassification.kind,
|
|
105
|
+
resolvedHost: resolved.hostname.toLowerCase(),
|
|
106
|
+
resolvedClassification: resolvedClassification.kind,
|
|
107
|
+
rewritten,
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
runConfiguredSteps,
|
|
9
9
|
} from "../runner/template-steps.mjs";
|
|
10
10
|
import { captureOutput } from "../runner/processes.mjs";
|
|
11
|
+
import { assertPgDumpArgsAreSourceSafe } from "./source-url.mjs";
|
|
11
12
|
|
|
12
13
|
export async function runTemplateStage(config, stageName, databaseUrl, options = {}) {
|
|
13
14
|
const steps = config.testkit.database?.template?.[stageName] || [];
|
|
@@ -69,14 +70,18 @@ export async function captureTemplateSnapshotText(config, databaseUrl, options =
|
|
|
69
70
|
|
|
70
71
|
export async function dumpPostgresSchemaToFile(config, outputPath, databaseUrl, options = {}) {
|
|
71
72
|
const env = {
|
|
72
|
-
...buildTemplateExecutionEnv(config, {}, process.env),
|
|
73
|
-
...buildPostgresConnectionEnv(databaseUrl
|
|
73
|
+
...buildTemplateExecutionEnv(config, {}, options.env || process.env),
|
|
74
|
+
...buildPostgresConnectionEnv(databaseUrl, {
|
|
75
|
+
applicationName: options.pgApplicationName,
|
|
76
|
+
}),
|
|
74
77
|
};
|
|
75
|
-
const
|
|
78
|
+
const args = pgDumpArgs();
|
|
79
|
+
assertPgDumpArgsAreSourceSafe(args);
|
|
80
|
+
const result = await runPgDumpCommand(config, "pg_dump", args, env, options);
|
|
76
81
|
if (result.exitCode !== 0 && isPgDumpServerVersionMismatch(result)) {
|
|
77
82
|
const serverMajor = parsePgDumpServerMajor(result);
|
|
78
83
|
if (serverMajor) {
|
|
79
|
-
const fallback = await runDockerizedPgDump(config, serverMajor, env, options);
|
|
84
|
+
const fallback = await runDockerizedPgDump(config, serverMajor, env, args, options);
|
|
80
85
|
if (fallback.exitCode === 0) {
|
|
81
86
|
fs.writeFileSync(outputPath, fallback.stdout);
|
|
82
87
|
sanitizeSnapshotFile(outputPath);
|
|
@@ -92,7 +97,7 @@ export async function dumpPostgresSchemaToFile(config, outputPath, databaseUrl,
|
|
|
92
97
|
sanitizeSnapshotFile(outputPath);
|
|
93
98
|
}
|
|
94
99
|
|
|
95
|
-
function pgDumpArgs() {
|
|
100
|
+
export function pgDumpArgs() {
|
|
96
101
|
return [
|
|
97
102
|
"--schema-only",
|
|
98
103
|
"--no-owner",
|
|
@@ -100,7 +105,7 @@ function pgDumpArgs() {
|
|
|
100
105
|
];
|
|
101
106
|
}
|
|
102
107
|
|
|
103
|
-
async function runDockerizedPgDump(config, serverMajor, env, options) {
|
|
108
|
+
async function runDockerizedPgDump(config, serverMajor, env, pgDumpCommandArgs, options) {
|
|
104
109
|
const image = `${process.env.TESTKIT_PG_DUMP_IMAGE_PREFIX || "postgres"}:${serverMajor}`;
|
|
105
110
|
return runPgDumpCommand(
|
|
106
111
|
config,
|
|
@@ -122,9 +127,11 @@ async function runDockerizedPgDump(config, serverMajor, env, options) {
|
|
|
122
127
|
"PGPASSWORD",
|
|
123
128
|
"-e",
|
|
124
129
|
"PGSSLMODE",
|
|
130
|
+
"-e",
|
|
131
|
+
"PGAPPNAME",
|
|
125
132
|
image,
|
|
126
133
|
"pg_dump",
|
|
127
|
-
...
|
|
134
|
+
...pgDumpCommandArgs,
|
|
128
135
|
],
|
|
129
136
|
env,
|
|
130
137
|
options
|
|
@@ -169,7 +176,7 @@ function parsePgDumpServerMajor(result) {
|
|
|
169
176
|
return match ? Number(match[1]) : null;
|
|
170
177
|
}
|
|
171
178
|
|
|
172
|
-
function buildPostgresConnectionEnv(databaseUrl) {
|
|
179
|
+
function buildPostgresConnectionEnv(databaseUrl, options = {}) {
|
|
173
180
|
const parsed = new URL(databaseUrl);
|
|
174
181
|
return compactObject({
|
|
175
182
|
PGHOST: parsed.hostname,
|
|
@@ -178,6 +185,7 @@ function buildPostgresConnectionEnv(databaseUrl) {
|
|
|
178
185
|
PGUSER: decodeURIComponent(parsed.username || ""),
|
|
179
186
|
PGPASSWORD: decodeURIComponent(parsed.password || ""),
|
|
180
187
|
PGSSLMODE: parsed.searchParams.get("sslmode") || undefined,
|
|
188
|
+
PGAPPNAME: options.applicationName || undefined,
|
|
181
189
|
});
|
|
182
190
|
}
|
|
183
191
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elench/testkit-bridge",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.117",
|
|
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.117"
|
|
26
26
|
},
|
|
27
27
|
"private": false
|
|
28
28
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elench/testkit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.117",
|
|
4
4
|
"description": "Assistant-first CLI for running, inspecting, and debugging local testkit suites",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"workspaces": [
|
|
@@ -94,10 +94,10 @@
|
|
|
94
94
|
},
|
|
95
95
|
"dependencies": {
|
|
96
96
|
"@babel/code-frame": "^7.29.0",
|
|
97
|
-
"@elench/next-analysis": "0.1.
|
|
98
|
-
"@elench/testkit-bridge": "0.1.
|
|
99
|
-
"@elench/testkit-protocol": "0.1.
|
|
100
|
-
"@elench/ts-analysis": "0.1.
|
|
97
|
+
"@elench/next-analysis": "0.1.117",
|
|
98
|
+
"@elench/testkit-bridge": "0.1.117",
|
|
99
|
+
"@elench/testkit-protocol": "0.1.117",
|
|
100
|
+
"@elench/ts-analysis": "0.1.117",
|
|
101
101
|
"@oclif/core": "^4.10.6",
|
|
102
102
|
"@playwright/test": "^1.52.0",
|
|
103
103
|
"esbuild": "^0.25.11",
|