@elench/testkit 0.1.115 → 0.1.116

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 (34) hide show
  1. package/README.md +33 -8
  2. package/lib/cli/args.mjs +3 -3
  3. package/lib/cli/command-flags.mjs +4 -0
  4. package/lib/cli/commands/db/schema/refresh.mjs +21 -0
  5. package/lib/cli/commands/db/schema/verify.mjs +27 -0
  6. package/lib/cli/entrypoint.mjs +1 -0
  7. package/lib/cli/operations/db/schema/refresh/operation.mjs +56 -0
  8. package/lib/cli/operations/db/{snapshot/capture → schema/verify}/operation.mjs +6 -27
  9. package/lib/cli/operations/run/operation.mjs +1 -0
  10. package/lib/cli/renderers/db-schema/text.mjs +7 -0
  11. package/lib/config/database.mjs +64 -0
  12. package/lib/config-api/index.d.ts +16 -1
  13. package/lib/config-api/index.mjs +31 -16
  14. package/lib/database/fingerprint.mjs +2 -0
  15. package/lib/database/index.mjs +142 -104
  16. package/lib/database/schema-source.mjs +295 -0
  17. package/lib/database/template-steps.mjs +158 -38
  18. package/lib/runner/orchestrator.mjs +4 -3
  19. package/lib/runner/template-steps.mjs +12 -1
  20. package/lib/runner/template.mjs +16 -1
  21. package/node_modules/@elench/next-analysis/package.json +1 -1
  22. package/node_modules/@elench/testkit-bridge/package.json +2 -2
  23. package/node_modules/@elench/testkit-protocol/package.json +1 -1
  24. package/node_modules/@elench/ts-analysis/package.json +1 -1
  25. package/node_modules/es-toolkit/CHANGELOG.md +801 -0
  26. package/node_modules/es-toolkit/src/compat/_internal/Equals.d.ts +1 -0
  27. package/node_modules/es-toolkit/src/compat/_internal/IsWritable.d.ts +3 -0
  28. package/node_modules/es-toolkit/src/compat/_internal/MutableList.d.ts +4 -0
  29. package/node_modules/es-toolkit/src/compat/_internal/RejectReadonly.d.ts +4 -0
  30. package/node_modules/esprima/ChangeLog +235 -0
  31. package/package.json +8 -5
  32. package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/package.json +1 -1
  33. package/lib/cli/commands/db/snapshot/capture.mjs +0 -26
  34. package/lib/cli/renderers/db-snapshot-capture/text.mjs +0 -3
@@ -1,4 +1,5 @@
1
1
  import fs from "fs";
2
+ import os from "os";
2
3
  import path from "path";
3
4
  import { execa } from "execa";
4
5
  import { buildTemplateExecutionEnv } from "../runner/template.mjs";
@@ -25,6 +26,25 @@ export async function runTemplateStage(config, stageName, databaseUrl, options =
25
26
  reporter: options.reporter || null,
26
27
  setupRegistry: options.setupRegistry || null,
27
28
  parentOperation: options.parentOperation || null,
29
+ afterStep: options.afterStep || null,
30
+ });
31
+ }
32
+
33
+ export async function runTemplateStep(config, stageName, step, stepIndex, databaseUrl, options = {}) {
34
+ const env = {
35
+ ...buildTemplateExecutionEnv(config, {}, process.env),
36
+ DATABASE_URL: databaseUrl,
37
+ };
38
+
39
+ await runConfiguredSteps({
40
+ config,
41
+ steps: [step],
42
+ env,
43
+ labelPrefix: `template:${stageName}`,
44
+ reporter: options.reporter || null,
45
+ setupRegistry: options.setupRegistry || null,
46
+ parentOperation: options.parentOperation || null,
47
+ startIndex: stepIndex,
28
48
  });
29
49
  }
30
50
 
@@ -36,45 +56,95 @@ export function collectTemplateInputs(productDir, template = {}) {
36
56
  });
37
57
  }
38
58
 
39
- export async function captureTemplateSnapshot(config, outputPath, databaseUrl, options = {}) {
40
- const templateDbUrl = databaseUrl;
41
- const absoluteOutputPath = path.resolve(config.productDir, outputPath);
42
- fs.mkdirSync(path.dirname(absoluteOutputPath), { recursive: true });
59
+ export async function captureTemplateSnapshotText(config, databaseUrl, options = {}) {
60
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "testkit-schema-snapshot-"));
61
+ const tempPath = path.join(tempDir, "schema.sql");
62
+ try {
63
+ await dumpPostgresSchemaToFile(config, tempPath, databaseUrl, options);
64
+ return fs.readFileSync(tempPath, "utf8");
65
+ } finally {
66
+ fs.rmSync(tempDir, { recursive: true, force: true });
67
+ }
68
+ }
43
69
 
44
- const child = execa(
45
- "pg_dump",
70
+ export async function dumpPostgresSchemaToFile(config, outputPath, databaseUrl, options = {}) {
71
+ const env = {
72
+ ...buildTemplateExecutionEnv(config, {}, process.env),
73
+ ...buildPostgresConnectionEnv(databaseUrl),
74
+ };
75
+ const result = await runPgDumpCommand(config, "pg_dump", pgDumpArgs(), env, options);
76
+ if (result.exitCode !== 0 && isPgDumpServerVersionMismatch(result)) {
77
+ const serverMajor = parsePgDumpServerMajor(result);
78
+ if (serverMajor) {
79
+ const fallback = await runDockerizedPgDump(config, serverMajor, env, options);
80
+ if (fallback.exitCode === 0) {
81
+ fs.writeFileSync(outputPath, fallback.stdout);
82
+ sanitizeSnapshotFile(outputPath);
83
+ return;
84
+ }
85
+ throw new Error(fallback.shortMessage || fallback.stderr || fallback.stdout || "dockerized pg_dump failed");
86
+ }
87
+ }
88
+ if (result.exitCode !== 0) {
89
+ throw new Error(result.shortMessage || result.stderr || result.stdout || "pg_dump failed");
90
+ }
91
+ fs.writeFileSync(outputPath, result.stdout);
92
+ sanitizeSnapshotFile(outputPath);
93
+ }
94
+
95
+ function pgDumpArgs() {
96
+ return [
97
+ "--schema-only",
98
+ "--no-owner",
99
+ "--no-privileges",
100
+ ];
101
+ }
102
+
103
+ async function runDockerizedPgDump(config, serverMajor, env, options) {
104
+ const image = `${process.env.TESTKIT_PG_DUMP_IMAGE_PREFIX || "postgres"}:${serverMajor}`;
105
+ return runPgDumpCommand(
106
+ config,
107
+ "docker",
46
108
  [
47
- "--schema-only",
48
- "--no-owner",
49
- "--no-privileges",
50
- "--file",
51
- absoluteOutputPath,
52
- templateDbUrl,
109
+ "run",
110
+ "--rm",
111
+ "--network",
112
+ "host",
113
+ "-e",
114
+ "PGHOST",
115
+ "-e",
116
+ "PGPORT",
117
+ "-e",
118
+ "PGDATABASE",
119
+ "-e",
120
+ "PGUSER",
121
+ "-e",
122
+ "PGPASSWORD",
123
+ "-e",
124
+ "PGSSLMODE",
125
+ image,
126
+ "pg_dump",
127
+ ...pgDumpArgs(),
53
128
  ],
54
- {
55
- cwd: config.productDir,
56
- env: {
57
- ...buildTemplateExecutionEnv(config, {}, process.env),
58
- DATABASE_URL: templateDbUrl,
59
- },
60
- stdout: "pipe",
61
- stderr: "pipe",
62
- reject: false,
63
- }
129
+ env,
130
+ options
64
131
  );
132
+ }
133
+
134
+ async function runPgDumpCommand(config, command, args, env, options = {}) {
135
+ const child = execa(command, args, {
136
+ cwd: config.productDir,
137
+ env,
138
+ stdout: "pipe",
139
+ stderr: "pipe",
140
+ reject: false,
141
+ });
65
142
  const liveWriter =
66
143
  options.reporter?.outputMode === "debug"
67
144
  ? (line) => options.reporter.writeDebugLine?.(line)
68
145
  : null;
69
146
  const logRecord = options.logRecord || null;
70
147
  const drains = [
71
- captureOutput(child.stdout, {
72
- livePrefix: `[${config.runtimeLabel || config.name}:${config.name}]`,
73
- liveWriter,
74
- onLine(line) {
75
- if (logRecord) logRecord.stream.write(`${new Date().toISOString()} [stdout] ${line}\n`);
76
- },
77
- }),
78
148
  captureOutput(child.stderr, {
79
149
  livePrefix: `[${config.runtimeLabel || config.name}:${config.name}]`,
80
150
  liveWriter,
@@ -85,29 +155,79 @@ export async function captureTemplateSnapshot(config, outputPath, databaseUrl, o
85
155
  ];
86
156
  const result = await child;
87
157
  await Promise.all(drains);
88
- if (result.exitCode !== 0) {
89
- throw new Error(result.shortMessage || result.stderr || result.stdout || "pg_dump failed");
90
- }
158
+ return result;
159
+ }
160
+
161
+ function isPgDumpServerVersionMismatch(result) {
162
+ const text = `${result.stderr || ""}\n${result.stdout || ""}`;
163
+ return text.includes("server version mismatch");
164
+ }
91
165
 
92
- sanitizeSnapshotFile(absoluteOutputPath);
93
- return absoluteOutputPath;
166
+ function parsePgDumpServerMajor(result) {
167
+ const text = `${result.stderr || ""}\n${result.stdout || ""}`;
168
+ const match = text.match(/server version:\s*([0-9]+)/i);
169
+ return match ? Number(match[1]) : null;
170
+ }
171
+
172
+ function buildPostgresConnectionEnv(databaseUrl) {
173
+ const parsed = new URL(databaseUrl);
174
+ return compactObject({
175
+ PGHOST: parsed.hostname,
176
+ PGPORT: parsed.port || "5432",
177
+ PGDATABASE: decodeURIComponent(parsed.pathname.replace(/^\//, "")),
178
+ PGUSER: decodeURIComponent(parsed.username || ""),
179
+ PGPASSWORD: decodeURIComponent(parsed.password || ""),
180
+ PGSSLMODE: parsed.searchParams.get("sslmode") || undefined,
181
+ });
182
+ }
183
+
184
+ function compactObject(value) {
185
+ return Object.fromEntries(
186
+ Object.entries(value).filter(([_key, entry]) => entry !== undefined && entry !== null && entry !== "")
187
+ );
94
188
  }
95
189
 
96
190
  export function sanitizeSnapshotFile(filePath) {
97
191
  const dump = fs.readFileSync(filePath, "utf8");
98
- const sanitized = dump
99
- .split("\n")
192
+ const sanitized = sanitizeSnapshotText(dump);
193
+
194
+ if (sanitized !== dump) {
195
+ fs.writeFileSync(filePath, sanitized);
196
+ }
197
+ }
198
+
199
+ export function sanitizeSnapshotText(dump) {
200
+ return removePublicSchemaInitdbBlock(String(dump).split("\n"))
100
201
  .filter((line) => {
101
202
  const trimmed = line.trim();
102
203
  return (
204
+ !trimmed.startsWith("-- Dumped from database version ") &&
205
+ !trimmed.startsWith("-- Dumped by pg_dump version ") &&
103
206
  trimmed !== "SET transaction_timeout = 0;" &&
104
207
  !trimmed.startsWith("\\restrict ") &&
105
208
  !trimmed.startsWith("\\unrestrict ")
106
209
  );
107
210
  })
108
211
  .join("\n");
212
+ }
109
213
 
110
- if (sanitized !== dump) {
111
- fs.writeFileSync(filePath, sanitized);
214
+ function removePublicSchemaInitdbBlock(lines) {
215
+ const normalized = [];
216
+ for (let index = 0; index < lines.length; index += 1) {
217
+ if (lines[index]?.trim() === "--" && lines[index + 1]?.startsWith("-- Name: public; Type: SCHEMA;")) {
218
+ let cursor = index + 1;
219
+ while (cursor < lines.length && !lines[cursor]?.includes("*not* creating schema")) {
220
+ cursor += 1;
221
+ }
222
+ if (cursor < lines.length) {
223
+ while (cursor + 1 < lines.length && lines[cursor + 1]?.trim() === "") {
224
+ cursor += 1;
225
+ }
226
+ index = cursor;
227
+ continue;
228
+ }
229
+ }
230
+ normalized.push(lines[index]);
112
231
  }
232
+ return normalized;
113
233
  }
@@ -146,9 +146,10 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
146
146
  runtimeOptions: {
147
147
  reporter,
148
148
  logRegistry,
149
- setupRegistry,
150
- },
151
- });
149
+ setupRegistry,
150
+ skipSchemaSourceVerify: opts.skipSchemaSourceVerify,
151
+ },
152
+ });
152
153
  const timingUpdates = [];
153
154
 
154
155
  try {
@@ -51,13 +51,16 @@ export async function runConfiguredSteps({
51
51
  reporter = null,
52
52
  setupRegistry = null,
53
53
  parentOperation = null,
54
+ startIndex = 0,
55
+ afterStep = null,
54
56
  }) {
55
57
  if (steps.length === 0) return;
56
58
  const resolvedToolchain = await resolveConfiguredToolchain(config);
57
59
  await announceResolvedToolchain(config, resolvedToolchain, reporter);
58
60
 
59
61
  for (const [index, step] of steps.entries()) {
60
- const label = `${labelPrefix}:${config.name}:${index + 1}`;
62
+ const stepNumber = startIndex + index + 1;
63
+ const label = `${labelPrefix}:${config.name}:${stepNumber}`;
61
64
  const stepOperation = setupRegistry?.start({
62
65
  config,
63
66
  stage: label,
@@ -89,6 +92,14 @@ export async function runConfiguredSteps({
89
92
  if (finished) reporter?.setupOperationFinished?.(finished);
90
93
  throw error;
91
94
  }
95
+ if (afterStep) {
96
+ await afterStep({
97
+ step,
98
+ index: startIndex + index,
99
+ stepNumber,
100
+ label,
101
+ });
102
+ }
92
103
  }
93
104
  }
94
105
 
@@ -1,5 +1,8 @@
1
1
  import path from "path";
2
- import { finalizeConfiguredInputs, finalizeConfiguredSteps } from "../shared/configured-steps.mjs";
2
+ import {
3
+ finalizeConfiguredInputs,
4
+ finalizeConfiguredSteps,
5
+ } from "../shared/configured-steps.mjs";
3
6
  import { readDatabaseInfo } from "./state-io.mjs";
4
7
 
5
8
  const PORT_STRIDE = 100;
@@ -135,6 +138,7 @@ export function resolveRuntimeConfig(
135
138
  const database = config.testkit.database
136
139
  ? {
137
140
  ...config.testkit.database,
141
+ sourceSchema: finalizeSourceSchema(config.testkit.database.sourceSchema, context),
138
142
  template: finalizeDatabaseTemplate(config.testkit.database.template, context),
139
143
  }
140
144
  : undefined;
@@ -193,6 +197,17 @@ function finalizeDatabaseTemplate(template, context) {
193
197
  };
194
198
  }
195
199
 
200
+ function finalizeSourceSchema(sourceSchema, context) {
201
+ if (!sourceSchema) return null;
202
+ return {
203
+ ...sourceSchema,
204
+ ...(typeof sourceSchema.env === "string" ? { env: finalizeString(sourceSchema.env, context) } : {}),
205
+ ...(typeof sourceSchema.cachePath === "string"
206
+ ? { cachePath: finalizeString(sourceSchema.cachePath, context) }
207
+ : {}),
208
+ };
209
+ }
210
+
196
211
  export function resolveServiceStateDir(runtimeDir, config) {
197
212
  return path.join(runtimeDir, "services", config.name);
198
213
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/next-analysis",
3
- "version": "0.1.115",
3
+ "version": "0.1.116",
4
4
  "description": "SWC-backed Next.js source analysis primitives for Erench tools",
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/testkit-bridge",
3
- "version": "0.1.115",
3
+ "version": "0.1.116",
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.115"
25
+ "@elench/testkit-protocol": "0.1.116"
26
26
  },
27
27
  "private": false
28
28
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/testkit-protocol",
3
- "version": "0.1.115",
3
+ "version": "0.1.116",
4
4
  "description": "Shared browser protocol for testkit bridge and extension consumers",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/ts-analysis",
3
- "version": "0.1.115",
3
+ "version": "0.1.116",
4
4
  "description": "TypeScript compiler-backed source analysis primitives for Erench tools",
5
5
  "type": "module",
6
6
  "exports": {