@elench/testkit 0.1.66 → 0.1.67

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.
@@ -18,11 +18,11 @@ export function defineHttpProfile(profile) {
18
18
  return profile || {};
19
19
  }
20
20
 
21
- export function service(config) {
22
- return config || {};
21
+ export function defineTestkitFile(metadata) {
22
+ return metadata || {};
23
23
  }
24
24
 
25
- export function localDatabase(options = {}) {
25
+ export function postgresDatabase(options = {}) {
26
26
  return {
27
27
  provider: "local",
28
28
  ...options,
@@ -76,7 +76,7 @@ export function verifyModule(specifier, options = {}) {
76
76
  return moduleStep(specifier, options);
77
77
  }
78
78
 
79
- export function seededDatabaseTemplate(options = {}) {
79
+ function buildDatabaseTemplateConfig(options = {}) {
80
80
  const migrate = normalizeTemplateStepList(options.migrate);
81
81
  const seed = normalizeTemplateStepList(options.seed);
82
82
  const verify = normalizeTemplateStepList(options.verify);
@@ -90,6 +90,52 @@ export function seededDatabaseTemplate(options = {}) {
90
90
  };
91
91
  }
92
92
 
93
+ export function templateDatabase(options = {}) {
94
+ const {
95
+ inputs,
96
+ schema,
97
+ migrate,
98
+ seed,
99
+ verify,
100
+ ...databaseOptions
101
+ } = options;
102
+ return postgresDatabase({
103
+ ...databaseOptions,
104
+ template: buildDatabaseTemplateConfig(options.template || { inputs, schema, migrate, seed, verify }),
105
+ });
106
+ }
107
+
108
+ export function postgresFixture(options = {}) {
109
+ const { discovery, envFiles, ...databaseOptions } = options;
110
+ return {
111
+ discovery: discovery || {
112
+ roots: [".testkit-fixture"],
113
+ },
114
+ envFiles,
115
+ local: false,
116
+ database: postgresDatabase(databaseOptions),
117
+ };
118
+ }
119
+
120
+ export function databaseServiceEnv(prefix, serviceName) {
121
+ const normalizedPrefix = String(prefix || "").trim().replace(/[^A-Za-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
122
+ if (!normalizedPrefix) {
123
+ throw new Error("databaseServiceEnv prefix must be a non-empty string");
124
+ }
125
+ if (!serviceName || !String(serviceName).trim()) {
126
+ throw new Error("databaseServiceEnv serviceName must be a non-empty string");
127
+ }
128
+
129
+ return {
130
+ [`${normalizedPrefix}_DATABASE_HOST`]: `{dbHost:${serviceName}}`,
131
+ [`${normalizedPrefix}_DATABASE_PORT`]: `{dbPort:${serviceName}}`,
132
+ [`${normalizedPrefix}_DATABASE_NAME`]: `{dbName:${serviceName}}`,
133
+ [`${normalizedPrefix}_DATABASE_USER`]: `{dbUser:${serviceName}}`,
134
+ [`${normalizedPrefix}_DATABASE_PASSWORD`]: `{dbPassword:${serviceName}}`,
135
+ [`${normalizedPrefix}_DATABASE_SSL`]: "0",
136
+ };
137
+ }
138
+
93
139
  export function nodeToolchain(options = {}) {
94
140
  return {
95
141
  kind: "node",
@@ -97,61 +143,137 @@ export function nodeToolchain(options = {}) {
97
143
  };
98
144
  }
99
145
 
100
- export function goService(options = {}) {
101
- const cwd = options.cwd || ".";
102
- const port = requiredNumber(options.port, "goService port");
103
- const readyPath = options.readyPath || "/health";
104
- const baseUrl = options.baseUrl || "http://127.0.0.1:{port}";
146
+ export function tscBuild(options = {}) {
147
+ return {
148
+ kind: "tsc",
149
+ cwd: options.cwd,
150
+ entry: options.entry || "src/index.ts",
151
+ tsconfig: options.tsconfig || "tsconfig.json",
152
+ outDir: options.outDir || "dist",
153
+ inputs: Array.isArray(options.inputs) ? [...options.inputs] : undefined,
154
+ };
155
+ }
105
156
 
157
+ export function scriptBuild(script, options = {}) {
106
158
  return {
107
- ...service(options),
108
- local: {
109
- cwd,
110
- start: options.start || defaultGoStartCommand(options),
111
- port,
112
- baseUrl,
113
- readyUrl: options.readyUrl || `${baseUrl}${readyPath}`,
114
- readyTimeoutMs: options.readyTimeoutMs,
115
- env: options.env || {},
116
- },
159
+ kind: "script",
160
+ script,
161
+ cwd: options.cwd,
162
+ inputs: Array.isArray(options.inputs) ? [...options.inputs] : undefined,
163
+ };
164
+ }
165
+
166
+ export function stepsBuild(options = {}) {
167
+ return {
168
+ kind: "steps",
169
+ inputs: Array.isArray(options.inputs) ? [...options.inputs] : undefined,
170
+ steps: Array.isArray(options.steps) ? [...options.steps] : [],
117
171
  };
118
172
  }
119
173
 
120
- export function nextService(options = {}) {
121
- const cwd = options.cwd || ".";
122
- const port = requiredNumber(options.port, "nextService port");
123
- const baseUrl = options.baseUrl || "http://127.0.0.1:{port}";
174
+ export function nextBuild(options = {}) {
175
+ return scriptBuild("./node_modules/.bin/next build", options);
176
+ }
177
+
178
+ export function nodeApp(options = {}) {
179
+ const {
180
+ baseUrl: explicitBaseUrl,
181
+ build: explicitBuild,
182
+ buildInputs,
183
+ cwd = ".",
184
+ entry = "src/index.ts",
185
+ env = {},
186
+ envFiles,
187
+ outDir = "dist",
188
+ port,
189
+ readyPath = "/health",
190
+ readyTimeoutMs,
191
+ readyUrl: explicitReadyUrl,
192
+ runtime,
193
+ start: explicitStart,
194
+ toolchain,
195
+ tsconfig = "tsconfig.json",
196
+ ...serviceConfig
197
+ } = options;
198
+
199
+ const normalizedPort = requiredNumber(port, "nodeApp port");
200
+ const baseUrl = explicitBaseUrl || "http://127.0.0.1:{port}";
201
+ const build = explicitBuild === undefined ? tscBuild({
202
+ cwd,
203
+ entry,
204
+ tsconfig,
205
+ outDir,
206
+ inputs: buildInputs,
207
+ }) : explicitBuild;
208
+ const start = explicitStart || resolveNodeAppStart(build, entry);
124
209
 
125
210
  return {
126
- ...service(options),
211
+ ...serviceConfig,
212
+ envFiles,
213
+ runtime: {
214
+ ...(runtime || {}),
215
+ build,
216
+ toolchain: toolchain ?? runtime?.toolchain,
217
+ },
127
218
  local: {
128
219
  cwd,
129
- start: options.start || "./node_modules/.bin/next dev -p {port}",
130
- port,
220
+ start,
221
+ port: normalizedPort,
131
222
  baseUrl,
132
- readyUrl: options.readyUrl || baseUrl,
133
- readyTimeoutMs: options.readyTimeoutMs,
134
- env: options.env || {},
223
+ readyUrl: explicitReadyUrl || `${baseUrl}${readyPath}`,
224
+ readyTimeoutMs,
225
+ env,
135
226
  },
136
227
  };
137
228
  }
138
229
 
139
- export function tsxService(options = {}) {
140
- const cwd = options.cwd || ".";
141
- const port = requiredNumber(options.port, "tsxService port");
142
- const baseUrl = options.baseUrl || "http://127.0.0.1:{port}";
143
- const entry = options.entry || "src/index.ts";
230
+ export function nextApp(options = {}) {
231
+ const {
232
+ baseUrl: explicitBaseUrl,
233
+ browser,
234
+ build: explicitBuild,
235
+ buildInputs,
236
+ cwd = ".",
237
+ dependsOn,
238
+ env = {},
239
+ envFiles,
240
+ mode = "dev",
241
+ port,
242
+ readyTimeoutMs,
243
+ readyUrl: explicitReadyUrl,
244
+ runtime,
245
+ start: explicitStart,
246
+ toolchain,
247
+ ...serviceConfig
248
+ } = options;
249
+
250
+ const normalizedPort = requiredNumber(port, "nextApp port");
251
+ const baseUrl = explicitBaseUrl || "http://127.0.0.1:{port}";
252
+ const build = mode === "start" ? explicitBuild || nextBuild({ cwd, inputs: buildInputs }) : explicitBuild || null;
253
+ const start =
254
+ explicitStart ||
255
+ (mode === "start"
256
+ ? "./node_modules/.bin/next start --port {port}"
257
+ : "./node_modules/.bin/next dev -p {port}");
144
258
 
145
259
  return {
146
- ...service(options),
260
+ ...serviceConfig,
261
+ envFiles,
262
+ dependsOn: Array.isArray(dependsOn) ? [...dependsOn] : dependsOn,
263
+ browser,
264
+ runtime: {
265
+ ...(runtime || {}),
266
+ build,
267
+ toolchain: toolchain ?? runtime?.toolchain,
268
+ },
147
269
  local: {
148
270
  cwd,
149
- start: options.start || `./node_modules/.bin/tsx watch ${entry}`,
150
- port,
271
+ start,
272
+ port: normalizedPort,
151
273
  baseUrl,
152
- readyUrl: options.readyUrl || `${baseUrl}${options.readyPath || "/health"}`,
153
- readyTimeoutMs: options.readyTimeoutMs,
154
- env: options.env || {},
274
+ readyUrl: explicitReadyUrl || baseUrl,
275
+ readyTimeoutMs,
276
+ env,
155
277
  },
156
278
  };
157
279
  }
@@ -235,16 +357,6 @@ export {
235
357
  runtimeJson,
236
358
  };
237
359
 
238
- function defaultGoStartCommand(options) {
239
- if (options.command) {
240
- return `go run ${options.command}`;
241
- }
242
- if (options.entrypoint) {
243
- return `go run ${options.entrypoint}`;
244
- }
245
- return "go run ./cmd/server";
246
- }
247
-
248
360
  function requiredNumber(value, label) {
249
361
  if (!Number.isInteger(value) || value <= 0) {
250
362
  throw new Error(`${label} must be a positive integer`);
@@ -252,6 +364,14 @@ function requiredNumber(value, label) {
252
364
  return value;
253
365
  }
254
366
 
367
+ function resolveNodeAppStart(build, entry) {
368
+ if (build?.kind === "tsc") {
369
+ const compiled = compiledEntryFromSource(entry || build.entry || "src/index.ts", build.outDir || "dist");
370
+ return `node {prepareDir}/${compiled}`;
371
+ }
372
+ return `./node_modules/.bin/tsx ${entry || "src/index.ts"}`;
373
+ }
374
+
255
375
  function normalizeTemplateStepList(value) {
256
376
  if (value == null) return [];
257
377
  return Array.isArray(value) ? [...value] : [value];
@@ -265,6 +385,13 @@ function normalizeSchemaStep(value) {
265
385
  return value;
266
386
  }
267
387
 
388
+ function compiledEntryFromSource(entry, outDir) {
389
+ const normalized = String(entry || "src/index.ts").replaceAll("\\", "/");
390
+ const compiled = normalized.replace(/\.[cm]?[jt]sx?$/i, ".js");
391
+ const relative = compiled.startsWith("src/") ? compiled.slice(4) : compiled;
392
+ return `${outDir}/${relative}`.replace(/\/+/g, "/");
393
+ }
394
+
268
395
  function envValue(name) {
269
396
  const env = getRuntimeEnv();
270
397
  const value = env?.rawEnv?.[name] || env?.[name];
@@ -1,38 +1,49 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
  import {
3
- goService,
4
- nextService,
3
+ databaseServiceEnv,
4
+ defineTestkitFile,
5
+ nextApp,
6
+ nextBuild,
5
7
  nodeToolchain,
8
+ nodeApp,
9
+ postgresDatabase,
10
+ postgresFixture,
6
11
  schemaSql,
7
12
  seedCommand,
8
13
  seedModule,
9
- seededDatabaseTemplate,
10
- tsxService,
14
+ stepsBuild,
15
+ templateDatabase,
16
+ tscBuild,
11
17
  verifyCommand,
12
18
  verifyModule,
13
19
  } from "./index.mjs";
14
20
 
15
21
  describe("setup helpers", () => {
16
- it("emits plain next start commands without an exec prefix", () => {
17
- const config = nextService({ port: 3000 });
18
-
19
- expect(config.local.start).toBe("./node_modules/.bin/next dev -p {port}");
22
+ it("defines file-local metadata plainly", () => {
23
+ expect(defineTestkitFile({ skip: "Auth is stubbed", locks: ["background-workers"] })).toEqual({
24
+ skip: "Auth is stubbed",
25
+ locks: ["background-workers"],
26
+ });
20
27
  });
21
28
 
22
- it("emits plain tsx start commands without an exec prefix", () => {
23
- const config = tsxService({ port: 3000, entry: "src/server.ts" });
29
+ it("builds a Next app preset for dev mode", () => {
30
+ const config = nextApp({ port: 3000 });
24
31
 
25
- expect(config.local.start).toBe("./node_modules/.bin/tsx watch src/server.ts");
32
+ expect(config.local.start).toBe("./node_modules/.bin/next dev -p {port}");
26
33
  });
27
34
 
28
- it("emits plain go start commands without an exec prefix", () => {
29
- expect(goService({ port: 3000 }).local.start).toBe("go run ./cmd/server");
30
- expect(goService({ port: 3000, entrypoint: "./cmd/api" }).local.start).toBe(
31
- "go run ./cmd/api"
32
- );
33
- expect(goService({ port: 3000, command: "./cmd/worker" }).local.start).toBe(
34
- "go run ./cmd/worker"
35
- );
35
+ it("builds a Node app preset with tsc build defaults", () => {
36
+ const config = nodeApp({ port: 3000, entry: "src/server.ts" });
37
+
38
+ expect(config.local.start).toBe("node {prepareDir}/dist/server.js");
39
+ expect(config.runtime.build).toEqual({
40
+ kind: "tsc",
41
+ cwd: ".",
42
+ entry: "src/server.ts",
43
+ tsconfig: "tsconfig.json",
44
+ outDir: "dist",
45
+ inputs: undefined,
46
+ });
36
47
  });
37
48
 
38
49
  it("builds node toolchain profiles with a node kind", () => {
@@ -43,6 +54,40 @@ describe("setup helpers", () => {
43
54
  });
44
55
  });
45
56
 
57
+ it("builds explicit build presets", () => {
58
+ expect(tscBuild({ entry: "src/server.ts", outDir: "build" })).toEqual({
59
+ kind: "tsc",
60
+ cwd: undefined,
61
+ entry: "src/server.ts",
62
+ tsconfig: "tsconfig.json",
63
+ outDir: "build",
64
+ inputs: undefined,
65
+ });
66
+ expect(nextBuild({ cwd: "frontend" })).toEqual({
67
+ kind: "script",
68
+ script: "./node_modules/.bin/next build",
69
+ cwd: "frontend",
70
+ inputs: undefined,
71
+ });
72
+ expect(
73
+ stepsBuild({
74
+ inputs: ["scripts/prepare.mjs"],
75
+ steps: [seedCommand("node scripts/prepare.mjs")],
76
+ })
77
+ ).toEqual({
78
+ kind: "steps",
79
+ inputs: ["scripts/prepare.mjs"],
80
+ steps: [
81
+ {
82
+ kind: "command",
83
+ cmd: "node scripts/prepare.mjs",
84
+ cwd: undefined,
85
+ inputs: undefined,
86
+ },
87
+ ],
88
+ });
89
+ });
90
+
46
91
  it("emits semantic database template steps using the underlying step shapes", () => {
47
92
  expect(schemaSql("db/schema.sql")).toEqual({
48
93
  kind: "sql-file",
@@ -76,67 +121,152 @@ describe("setup helpers", () => {
76
121
  });
77
122
  });
78
123
 
79
- it("builds a seeded database template from schema, seed, and verify intents", () => {
124
+ it("builds declarative template databases from schema, seed, and verify intents", () => {
80
125
  expect(
81
- seededDatabaseTemplate({
126
+ templateDatabase({
82
127
  inputs: ["db/schema.sql", "scripts/seed.ts"],
83
128
  schema: "db/schema.sql",
84
129
  seed: seedCommand("npm run db:seed"),
85
130
  verify: verifyModule("scripts/verify.ts#verifySeed"),
86
131
  })
87
132
  ).toEqual({
88
- inputs: ["db/schema.sql", "scripts/seed.ts"],
89
- migrate: [
90
- {
91
- kind: "sql-file",
92
- path: "db/schema.sql",
93
- cwd: undefined,
94
- inputs: undefined,
95
- },
96
- ],
97
- seed: [
98
- {
99
- kind: "command",
100
- cmd: "npm run db:seed",
101
- cwd: undefined,
102
- inputs: undefined,
103
- },
104
- ],
105
- verify: [
106
- {
107
- kind: "module",
108
- specifier: "scripts/verify.ts#verifySeed",
109
- cwd: undefined,
110
- inputs: undefined,
111
- },
112
- ],
133
+ provider: "local",
134
+ template: {
135
+ inputs: ["db/schema.sql", "scripts/seed.ts"],
136
+ migrate: [
137
+ {
138
+ kind: "sql-file",
139
+ path: "db/schema.sql",
140
+ cwd: undefined,
141
+ inputs: undefined,
142
+ },
143
+ ],
144
+ seed: [
145
+ {
146
+ kind: "command",
147
+ cmd: "npm run db:seed",
148
+ cwd: undefined,
149
+ inputs: undefined,
150
+ },
151
+ ],
152
+ verify: [
153
+ {
154
+ kind: "module",
155
+ specifier: "scripts/verify.ts#verifySeed",
156
+ cwd: undefined,
157
+ inputs: undefined,
158
+ },
159
+ ],
160
+ },
113
161
  });
114
162
  });
115
163
 
116
164
  it("prepends schema before explicit migrate steps and normalizes singletons to arrays", () => {
117
165
  expect(
118
- seededDatabaseTemplate({
166
+ templateDatabase({
119
167
  schema: schemaSql("db/schema.sql", { cwd: "db" }),
120
168
  migrate: seedCommand("echo migrate"),
121
169
  })
122
170
  ).toEqual({
123
- inputs: undefined,
124
- migrate: [
125
- {
126
- kind: "sql-file",
127
- path: "db/schema.sql",
128
- cwd: "db",
129
- inputs: undefined,
130
- },
131
- {
132
- kind: "command",
133
- cmd: "echo migrate",
134
- cwd: undefined,
135
- inputs: undefined,
136
- },
137
- ],
138
- seed: [],
139
- verify: [],
171
+ provider: "local",
172
+ template: {
173
+ inputs: undefined,
174
+ migrate: [
175
+ {
176
+ kind: "sql-file",
177
+ path: "db/schema.sql",
178
+ cwd: "db",
179
+ inputs: undefined,
180
+ },
181
+ {
182
+ kind: "command",
183
+ cmd: "echo migrate",
184
+ cwd: undefined,
185
+ inputs: undefined,
186
+ },
187
+ ],
188
+ seed: [],
189
+ verify: [],
190
+ },
191
+ });
192
+ });
193
+
194
+ it("builds declarative postgres database helpers", () => {
195
+ expect(postgresDatabase({ reset: false })).toEqual({
196
+ provider: "local",
197
+ reset: false,
140
198
  });
199
+ expect(
200
+ templateDatabase({
201
+ reset: true,
202
+ schema: "db/schema.sql",
203
+ seed: seedCommand("npm run db:seed"),
204
+ })
205
+ ).toEqual({
206
+ provider: "local",
207
+ reset: true,
208
+ template: {
209
+ inputs: undefined,
210
+ migrate: [
211
+ {
212
+ kind: "sql-file",
213
+ path: "db/schema.sql",
214
+ cwd: undefined,
215
+ inputs: undefined,
216
+ },
217
+ ],
218
+ seed: [
219
+ {
220
+ kind: "command",
221
+ cmd: "npm run db:seed",
222
+ cwd: undefined,
223
+ inputs: undefined,
224
+ },
225
+ ],
226
+ verify: [],
227
+ },
228
+ });
229
+ });
230
+
231
+ it("builds support database presets and env bindings", () => {
232
+ expect(
233
+ postgresFixture({
234
+ reset: true,
235
+ })
236
+ ).toEqual({
237
+ discovery: {
238
+ roots: [".testkit-fixture"],
239
+ },
240
+ envFiles: undefined,
241
+ local: false,
242
+ database: {
243
+ provider: "local",
244
+ reset: true,
245
+ },
246
+ });
247
+ expect(databaseServiceEnv("ONIX", "catalog")).toEqual({
248
+ ONIX_DATABASE_HOST: "{dbHost:catalog}",
249
+ ONIX_DATABASE_PORT: "{dbPort:catalog}",
250
+ ONIX_DATABASE_NAME: "{dbName:catalog}",
251
+ ONIX_DATABASE_USER: "{dbUser:catalog}",
252
+ ONIX_DATABASE_PASSWORD: "{dbPassword:catalog}",
253
+ ONIX_DATABASE_SSL: "0",
254
+ });
255
+ });
256
+
257
+ it("does not leak preset-only helper fields into node app configs", () => {
258
+ const config = nodeApp({
259
+ port: 3000,
260
+ entry: "src/server.ts",
261
+ buildInputs: ["src", "package.json"],
262
+ env: { API_KEY: "test" },
263
+ readyPath: "/live",
264
+ });
265
+
266
+ expect(config).not.toHaveProperty("entry");
267
+ expect(config).not.toHaveProperty("buildInputs");
268
+ expect(config).not.toHaveProperty("readyPath");
269
+ expect(config.local.env).toEqual({ API_KEY: "test" });
270
+ expect(config.local.readyUrl).toBe("http://127.0.0.1:{port}/live");
141
271
  });
142
272
  });