@elench/testkit 0.1.137 → 0.1.139
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/lib/config/index.mjs +44 -2
- package/lib/config-api/index.d.ts +46 -0
- package/lib/config-api/index.mjs +78 -1
- package/lib/kiln/client.mjs +20 -0
- package/lib/local/kiln-driver.mjs +97 -1
- package/lib/local/orchestrator.mjs +2 -0
- package/lib/runner/template.mjs +37 -5
- 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/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/lib/config/index.mjs
CHANGED
|
@@ -236,11 +236,33 @@ function normalizeEnvironmentConfig(name, environment) {
|
|
|
236
236
|
data,
|
|
237
237
|
portOffset,
|
|
238
238
|
env: normalizeEnvironmentEnv(environment.env),
|
|
239
|
+
resources: normalizeEnvironmentResources(name, environment.resources),
|
|
240
|
+
productionLike: Boolean(environment.productionLike),
|
|
239
241
|
...(environment.publicHost ? { publicHost: String(environment.publicHost) } : {}),
|
|
240
242
|
...(kiln ? { kiln } : {}),
|
|
241
243
|
};
|
|
242
244
|
}
|
|
243
245
|
|
|
246
|
+
function normalizeEnvironmentResources(environmentName, resources = {}) {
|
|
247
|
+
if (resources == null) return {};
|
|
248
|
+
if (typeof resources !== "object" || Array.isArray(resources)) {
|
|
249
|
+
throw new Error(`Environment "${environmentName}" resources must be an object`);
|
|
250
|
+
}
|
|
251
|
+
return Object.fromEntries(
|
|
252
|
+
Object.entries(resources).map(([name, resource]) => {
|
|
253
|
+
const normalizedName = normalizeDatabaseEnvToken(name, `Environment "${environmentName}" resource name`, false);
|
|
254
|
+
if (!resource || typeof resource !== "object" || Array.isArray(resource)) {
|
|
255
|
+
throw new Error(`Environment "${environmentName}" resource "${name}" must be an object`);
|
|
256
|
+
}
|
|
257
|
+
const kind = String(resource.kind || "").trim();
|
|
258
|
+
if (!["postgres", "server"].includes(kind)) {
|
|
259
|
+
throw new Error(`Environment "${environmentName}" resource "${name}" kind must be "postgres" or "server"`);
|
|
260
|
+
}
|
|
261
|
+
return [normalizedName, { ...resource, kind }];
|
|
262
|
+
})
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
244
266
|
function normalizeKilnEnvironmentConfig(name, kiln) {
|
|
245
267
|
if (!kiln) return null;
|
|
246
268
|
if (typeof kiln !== "object" || Array.isArray(kiln)) {
|
|
@@ -277,13 +299,14 @@ function normalizeEnvironmentEnv(env) {
|
|
|
277
299
|
}
|
|
278
300
|
const hasPresetShape =
|
|
279
301
|
Object.prototype.hasOwnProperty.call(env, "values") ||
|
|
280
|
-
Object.prototype.hasOwnProperty.call(env, "databases")
|
|
302
|
+
Object.prototype.hasOwnProperty.call(env, "databases") ||
|
|
303
|
+
Object.prototype.hasOwnProperty.call(env, "resources");
|
|
281
304
|
if (!hasPresetShape) {
|
|
282
305
|
return Object.fromEntries(
|
|
283
306
|
Object.entries(env).map(([key, value]) => [key, String(value)])
|
|
284
307
|
);
|
|
285
308
|
}
|
|
286
|
-
const allowedKeys = new Set(["values", "databases"]);
|
|
309
|
+
const allowedKeys = new Set(["values", "databases", "resources"]);
|
|
287
310
|
const unexpectedKeys = Object.keys(env).filter((key) => !allowedKeys.has(key));
|
|
288
311
|
if (unexpectedKeys.length > 0) {
|
|
289
312
|
throw new Error(
|
|
@@ -293,8 +316,11 @@ function normalizeEnvironmentEnv(env) {
|
|
|
293
316
|
const values = env.values && typeof env.values === "object" && !Array.isArray(env.values) ? env.values : {};
|
|
294
317
|
const databases =
|
|
295
318
|
env.databases && typeof env.databases === "object" && !Array.isArray(env.databases) ? env.databases : {};
|
|
319
|
+
const resources =
|
|
320
|
+
env.resources && typeof env.resources === "object" && !Array.isArray(env.resources) ? env.resources : {};
|
|
296
321
|
return {
|
|
297
322
|
...expandDatabaseBindings(databases),
|
|
323
|
+
...expandResourceBindings(resources),
|
|
298
324
|
...Object.fromEntries(Object.entries(values).map(([key, value]) => [key, String(value)])),
|
|
299
325
|
};
|
|
300
326
|
}
|
|
@@ -317,6 +343,22 @@ function expandDatabaseBindings(bindings) {
|
|
|
317
343
|
return env;
|
|
318
344
|
}
|
|
319
345
|
|
|
346
|
+
function expandResourceBindings(bindings) {
|
|
347
|
+
const env = {};
|
|
348
|
+
for (const [envName, binding] of Object.entries(bindings || {})) {
|
|
349
|
+
if (typeof binding === "string") {
|
|
350
|
+
const [resourceName, field = "url"] = binding.split(".");
|
|
351
|
+
env[envName] = `{resource:${normalizeDatabaseEnvToken(resourceName, `env.resources.${envName}`, false)}:${normalizeDatabaseEnvToken(field, `env.resources.${envName} field`, false)}}`;
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
if (!binding || typeof binding !== "object" || Array.isArray(binding)) {
|
|
355
|
+
throw new Error(`env.resources.${envName} must be a string or object`);
|
|
356
|
+
}
|
|
357
|
+
env[envName] = `{resource:${normalizeDatabaseEnvToken(binding.resource, `env.resources.${envName}.resource`, false)}:${normalizeDatabaseEnvToken(binding.field || "url", `env.resources.${envName}.field`, false)}}`;
|
|
358
|
+
}
|
|
359
|
+
return env;
|
|
360
|
+
}
|
|
361
|
+
|
|
320
362
|
function normalizeDatabaseEnvToken(value, label, sanitize = true) {
|
|
321
363
|
const raw = String(value || "").trim();
|
|
322
364
|
const normalized = sanitize
|
|
@@ -280,8 +280,40 @@ export interface DatabaseBindingEnvConfig {
|
|
|
280
280
|
export interface PresetEnvConfig {
|
|
281
281
|
values?: Record<string, string>;
|
|
282
282
|
databases?: Record<string, DatabaseBindingEnvConfig>;
|
|
283
|
+
resources?: Record<string, string | { resource: string; field?: string }>;
|
|
283
284
|
}
|
|
284
285
|
|
|
286
|
+
export interface KilnVMResourceConfig {
|
|
287
|
+
autostart?: boolean;
|
|
288
|
+
disk?: string;
|
|
289
|
+
diskSize?: string | number;
|
|
290
|
+
diskSizeMB?: number;
|
|
291
|
+
memoryMB?: number;
|
|
292
|
+
name?: string;
|
|
293
|
+
networkId?: string;
|
|
294
|
+
profile?: string;
|
|
295
|
+
vcpus?: number;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export interface PostgresResourceConfig {
|
|
299
|
+
kind: "postgres";
|
|
300
|
+
data?: "reuse" | "reset" | "rebuild";
|
|
301
|
+
database?: string;
|
|
302
|
+
extensions?: string[];
|
|
303
|
+
password?: string;
|
|
304
|
+
port?: number;
|
|
305
|
+
user?: string;
|
|
306
|
+
version?: string;
|
|
307
|
+
vm?: KilnVMResourceConfig;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export interface ServerResourceConfig {
|
|
311
|
+
kind: "server";
|
|
312
|
+
vm?: KilnVMResourceConfig;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export type ResourceConfig = PostgresResourceConfig | ServerResourceConfig;
|
|
316
|
+
|
|
285
317
|
export interface LocalEnvironmentConfig {
|
|
286
318
|
kind: "local";
|
|
287
319
|
data?: "reuse" | "reset" | "rebuild";
|
|
@@ -289,7 +321,9 @@ export interface LocalEnvironmentConfig {
|
|
|
289
321
|
env?: Record<string, string>;
|
|
290
322
|
kiln?: KilnLocalEnvironmentConfig;
|
|
291
323
|
portOffset?: number;
|
|
324
|
+
productionLike?: boolean;
|
|
292
325
|
publicHost?: string;
|
|
326
|
+
resources?: Record<string, ResourceConfig>;
|
|
293
327
|
target: string;
|
|
294
328
|
}
|
|
295
329
|
|
|
@@ -599,6 +633,17 @@ export declare const database: {
|
|
|
599
633
|
}
|
|
600
634
|
): ServiceConfig;
|
|
601
635
|
};
|
|
636
|
+
export declare const resource: {
|
|
637
|
+
postgres(options?: Omit<PostgresResourceConfig, "kind">): PostgresResourceConfig;
|
|
638
|
+
server(options?: Omit<ServerResourceConfig, "kind">): ServerResourceConfig;
|
|
639
|
+
field(resourceName: string, field?: string): string;
|
|
640
|
+
url(resourceName: string): string;
|
|
641
|
+
host(resourceName: string): string;
|
|
642
|
+
port(resourceName: string): string;
|
|
643
|
+
database(resourceName: string): string;
|
|
644
|
+
user(resourceName: string): string;
|
|
645
|
+
password(resourceName: string): string;
|
|
646
|
+
};
|
|
602
647
|
export declare const step: {
|
|
603
648
|
command(run: string, options?: TemplateLifecycleStepOptions): TemplateCommandStepConfig;
|
|
604
649
|
module(target: string, options?: TemplateLifecycleStepOptions): TemplateModuleStepConfig;
|
|
@@ -615,6 +660,7 @@ export declare const ui: {
|
|
|
615
660
|
};
|
|
616
661
|
export declare const environment: {
|
|
617
662
|
local(options: LocalEnvironmentOptions): LocalEnvironmentConfig;
|
|
663
|
+
productionLike(options: LocalEnvironmentOptions): LocalEnvironmentConfig;
|
|
618
664
|
};
|
|
619
665
|
export declare const auth: {
|
|
620
666
|
fixture(options: { contract: JsonSessionContract; topology: AuthTopology }): AuthFixture;
|
package/lib/config-api/index.mjs
CHANGED
|
@@ -33,6 +33,33 @@ function postgresDatabase(options = {}) {
|
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
function postgresResource(options = {}) {
|
|
37
|
+
return {
|
|
38
|
+
kind: "postgres",
|
|
39
|
+
version: options.version || "16",
|
|
40
|
+
database: options.database || options.name || "app",
|
|
41
|
+
user: options.user || "app",
|
|
42
|
+
...(options.password ? { password: options.password } : {}),
|
|
43
|
+
...(options.port ? { port: options.port } : {}),
|
|
44
|
+
...(options.vm ? { vm: { ...options.vm } } : {}),
|
|
45
|
+
...(options.data ? { data: options.data } : {}),
|
|
46
|
+
...(options.extensions ? { extensions: [...options.extensions] } : {}),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function serverResource(options = {}) {
|
|
51
|
+
return {
|
|
52
|
+
kind: "server",
|
|
53
|
+
...(options.vm ? { vm: { ...options.vm } } : {}),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function resourceField(resourceName, field = "url") {
|
|
58
|
+
const normalizedResourceName = normalizeDatabaseEnvToken(resourceName, "resource name", false);
|
|
59
|
+
const normalizedField = normalizeDatabaseEnvToken(field, "resource field", false);
|
|
60
|
+
return `{resource:${normalizedResourceName}:${normalizedField}}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
36
63
|
function buildDatabaseTemplateConfig(options = {}) {
|
|
37
64
|
for (const legacyKey of ["schema"]) {
|
|
38
65
|
if (Object.prototype.hasOwnProperty.call(options, legacyKey)) {
|
|
@@ -412,6 +439,30 @@ export const database = {
|
|
|
412
439
|
fixture: postgresFixture,
|
|
413
440
|
};
|
|
414
441
|
|
|
442
|
+
export const resource = {
|
|
443
|
+
postgres: postgresResource,
|
|
444
|
+
server: serverResource,
|
|
445
|
+
field: resourceField,
|
|
446
|
+
url(resourceName) {
|
|
447
|
+
return resourceField(resourceName, "url");
|
|
448
|
+
},
|
|
449
|
+
host(resourceName) {
|
|
450
|
+
return resourceField(resourceName, "host");
|
|
451
|
+
},
|
|
452
|
+
port(resourceName) {
|
|
453
|
+
return resourceField(resourceName, "port");
|
|
454
|
+
},
|
|
455
|
+
database(resourceName) {
|
|
456
|
+
return resourceField(resourceName, "database");
|
|
457
|
+
},
|
|
458
|
+
user(resourceName) {
|
|
459
|
+
return resourceField(resourceName, "user");
|
|
460
|
+
},
|
|
461
|
+
password(resourceName) {
|
|
462
|
+
return resourceField(resourceName, "password");
|
|
463
|
+
},
|
|
464
|
+
};
|
|
465
|
+
|
|
415
466
|
export const step = {
|
|
416
467
|
command: commandStep,
|
|
417
468
|
module: moduleStep,
|
|
@@ -456,6 +507,13 @@ function localEnvironment(options = {}) {
|
|
|
456
507
|
|
|
457
508
|
export const environment = {
|
|
458
509
|
local: localEnvironment,
|
|
510
|
+
productionLike(options = {}) {
|
|
511
|
+
return localEnvironment({
|
|
512
|
+
driver: "kiln",
|
|
513
|
+
...options,
|
|
514
|
+
productionLike: true,
|
|
515
|
+
});
|
|
516
|
+
},
|
|
459
517
|
};
|
|
460
518
|
|
|
461
519
|
export const auth = {
|
|
@@ -497,7 +555,7 @@ function normalizePresetEnv(env) {
|
|
|
497
555
|
if (typeof env !== "object" || Array.isArray(env)) {
|
|
498
556
|
throw new Error("Preset env must be an object");
|
|
499
557
|
}
|
|
500
|
-
const allowedKeys = new Set(["values", "databases"]);
|
|
558
|
+
const allowedKeys = new Set(["values", "databases", "resources"]);
|
|
501
559
|
const unexpectedKeys = Object.keys(env).filter((key) => !allowedKeys.has(key));
|
|
502
560
|
if (unexpectedKeys.length > 0) {
|
|
503
561
|
throw new Error(
|
|
@@ -508,9 +566,12 @@ function normalizePresetEnv(env) {
|
|
|
508
566
|
const values = env.values && typeof env.values === "object" && !Array.isArray(env.values) ? env.values : {};
|
|
509
567
|
const databases =
|
|
510
568
|
env.databases && typeof env.databases === "object" && !Array.isArray(env.databases) ? env.databases : {};
|
|
569
|
+
const resources =
|
|
570
|
+
env.resources && typeof env.resources === "object" && !Array.isArray(env.resources) ? env.resources : {};
|
|
511
571
|
|
|
512
572
|
return {
|
|
513
573
|
...expandDatabaseBindings(databases),
|
|
574
|
+
...expandResourceBindings(resources),
|
|
514
575
|
...values,
|
|
515
576
|
};
|
|
516
577
|
}
|
|
@@ -533,6 +594,22 @@ function expandDatabaseBindings(bindings) {
|
|
|
533
594
|
return env;
|
|
534
595
|
}
|
|
535
596
|
|
|
597
|
+
function expandResourceBindings(bindings) {
|
|
598
|
+
const env = {};
|
|
599
|
+
for (const [envName, binding] of Object.entries(bindings || {})) {
|
|
600
|
+
if (typeof binding === "string") {
|
|
601
|
+
const [resourceName, field = "url"] = binding.split(".");
|
|
602
|
+
env[envName] = resourceField(resourceName, field);
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
if (!binding || typeof binding !== "object" || Array.isArray(binding)) {
|
|
606
|
+
throw new Error(`env.resources.${envName} must be a string or object`);
|
|
607
|
+
}
|
|
608
|
+
env[envName] = resourceField(binding.resource, binding.field || "url");
|
|
609
|
+
}
|
|
610
|
+
return env;
|
|
611
|
+
}
|
|
612
|
+
|
|
536
613
|
function normalizeDatabaseEnvToken(value, label, sanitize = true) {
|
|
537
614
|
const raw = String(value || "").trim();
|
|
538
615
|
const normalized = sanitize
|
package/lib/kiln/client.mjs
CHANGED
|
@@ -72,6 +72,26 @@ export class KilnClient {
|
|
|
72
72
|
return this.request("POST", `/vms/${encodeURIComponent(ref)}/exec`, { command });
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
ensureAppliance(req) {
|
|
76
|
+
return this.request("POST", "/appliances/ensure", req);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
getAppliance(ref) {
|
|
80
|
+
return this.request("GET", `/appliances/${encodeURIComponent(ref)}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
listAppliances() {
|
|
84
|
+
return this.request("GET", "/appliances");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
stopAppliance(ref) {
|
|
88
|
+
return this.request("POST", `/appliances/${encodeURIComponent(ref)}/stop`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
deleteAppliance(ref) {
|
|
92
|
+
return this.request("DELETE", `/appliances/${encodeURIComponent(ref)}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
75
95
|
sshKey(machineId) {
|
|
76
96
|
return this.requestRaw("GET", `/machines/${encodeURIComponent(machineId)}/ssh-key`);
|
|
77
97
|
}
|
|
@@ -45,6 +45,7 @@ export async function kilnLocalUp(context, environment, options = {}) {
|
|
|
45
45
|
|
|
46
46
|
try {
|
|
47
47
|
const network = await ensureNetwork(client, environment);
|
|
48
|
+
const resources = await ensureResources(client, environment, network);
|
|
48
49
|
let vm = await ensureVM(client, vmConfig, network?.id || "");
|
|
49
50
|
vm = await ensureStarted(client, vm.name);
|
|
50
51
|
vm = await waitForVMReady(client, vm.name);
|
|
@@ -74,7 +75,12 @@ export async function kilnLocalUp(context, environment, options = {}) {
|
|
|
74
75
|
...(environment.data === "reset" ? ["--reset"] : []),
|
|
75
76
|
...(environment.data === "rebuild" ? ["--rebuild"] : []),
|
|
76
77
|
...(environment.portOffset ? ["--port-offset", String(environment.portOffset)] : []),
|
|
77
|
-
], {
|
|
78
|
+
], {
|
|
79
|
+
TESTKIT_PUBLIC_HOST: targetHost,
|
|
80
|
+
...(Object.keys(resources.connections).length > 0
|
|
81
|
+
? { TESTKIT_RESOURCE_CONNECTIONS_JSON: JSON.stringify(resources.connections) }
|
|
82
|
+
: {}),
|
|
83
|
+
});
|
|
78
84
|
if (remote.exitCode !== 0) {
|
|
79
85
|
throw new Error(`remote testkit local up failed\n${remote.stdout}${remote.stderr}`);
|
|
80
86
|
}
|
|
@@ -90,6 +96,7 @@ export async function kilnLocalUp(context, environment, options = {}) {
|
|
|
90
96
|
network: network ? { id: network.id, name: network.name, cidr: network.cidr } : null,
|
|
91
97
|
},
|
|
92
98
|
endpoints: rewriteEndpoints(status?.manifest?.endpoints || {}, targetHost),
|
|
99
|
+
resources: resources.manifest,
|
|
93
100
|
remoteStatus: status?.manifest || null,
|
|
94
101
|
});
|
|
95
102
|
return buildKilnLocalStatus(context.productDir, environment.name);
|
|
@@ -126,6 +133,9 @@ export async function kilnLocalDown(context, name, options = {}) {
|
|
|
126
133
|
stoppedAt: new Date().toISOString(),
|
|
127
134
|
services: [],
|
|
128
135
|
});
|
|
136
|
+
if (options.destroyState) {
|
|
137
|
+
await deleteManifestResources(manifest);
|
|
138
|
+
}
|
|
129
139
|
return manifest;
|
|
130
140
|
} finally {
|
|
131
141
|
ssh.cleanup?.();
|
|
@@ -193,6 +203,14 @@ export function buildKilnLocalStatus(productDir, name) {
|
|
|
193
203
|
lines.push(" endpoints:");
|
|
194
204
|
for (const [service, url] of Object.entries(endpoints)) lines.push(` ${service}: ${url}`);
|
|
195
205
|
}
|
|
206
|
+
const resources = manifest.resources || {};
|
|
207
|
+
if (Object.keys(resources).length > 0) {
|
|
208
|
+
lines.push(" resources:");
|
|
209
|
+
for (const [name, resource] of Object.entries(resources)) {
|
|
210
|
+
const address = resource.private_ip || resource.ip || "";
|
|
211
|
+
lines.push(` ${name}: ${resource.kind} ${resource.name}${address ? ` ${address}` : ""}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
196
214
|
return { name, exists: true, active: manifest.status === "running", manifest, services: [], lines };
|
|
197
215
|
}
|
|
198
216
|
|
|
@@ -226,6 +244,70 @@ async function attachExistingVMs(client, network, vmRefs) {
|
|
|
226
244
|
}
|
|
227
245
|
}
|
|
228
246
|
|
|
247
|
+
async function ensureResources(client, environment, network) {
|
|
248
|
+
const configured = environment.resources || {};
|
|
249
|
+
const manifest = {};
|
|
250
|
+
const connections = {};
|
|
251
|
+
for (const [name, resource] of Object.entries(configured)) {
|
|
252
|
+
if (resource.kind === "postgres") {
|
|
253
|
+
const appliance = await client.ensureAppliance({
|
|
254
|
+
name: resource.vm?.name || `${environment.name}-${name}`,
|
|
255
|
+
kind: "postgres",
|
|
256
|
+
network_id: resource.vm?.networkId || network?.id || "",
|
|
257
|
+
profile: resource.vm?.profile || "ubuntu-docker",
|
|
258
|
+
disk_size_mb: parseDiskMB(resource.vm?.disk || resource.vm?.diskSize || resource.vm?.diskSizeMB || resource.disk || "24G"),
|
|
259
|
+
memory_mb: Number(resource.vm?.memoryMB || resource.memoryMB || 1536),
|
|
260
|
+
vcpus: Number(resource.vm?.vcpus || resource.vcpus || 1),
|
|
261
|
+
autostart: Boolean(resource.vm?.autostart || resource.autostart),
|
|
262
|
+
postgres: {
|
|
263
|
+
version: resource.version || "16",
|
|
264
|
+
database: resource.database || name,
|
|
265
|
+
user: resource.user || "app",
|
|
266
|
+
...(resource.password ? { password: resource.password } : {}),
|
|
267
|
+
...(resource.port ? { port: Number(resource.port) } : {}),
|
|
268
|
+
},
|
|
269
|
+
metadata: {
|
|
270
|
+
"testkit.environment": environment.name,
|
|
271
|
+
"testkit.resource": name,
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
manifest[name] = pickAppliance(appliance);
|
|
275
|
+
connections[name] = appliance.connection || {};
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (resource.kind === "server") {
|
|
279
|
+
const appliance = await client.ensureAppliance({
|
|
280
|
+
name: resource.vm?.name || `${environment.name}-${name}`,
|
|
281
|
+
kind: "server",
|
|
282
|
+
network_id: resource.vm?.networkId || network?.id || "",
|
|
283
|
+
profile: resource.vm?.profile || "ubuntu-docker",
|
|
284
|
+
disk_size_mb: parseDiskMB(resource.vm?.disk || resource.vm?.diskSize || resource.vm?.diskSizeMB || resource.disk || "16G"),
|
|
285
|
+
memory_mb: Number(resource.vm?.memoryMB || resource.memoryMB || 1024),
|
|
286
|
+
vcpus: Number(resource.vm?.vcpus || resource.vcpus || 1),
|
|
287
|
+
autostart: Boolean(resource.vm?.autostart || resource.autostart),
|
|
288
|
+
metadata: {
|
|
289
|
+
"testkit.environment": environment.name,
|
|
290
|
+
"testkit.resource": name,
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
manifest[name] = pickAppliance(appliance);
|
|
294
|
+
connections[name] = appliance.connection || {};
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return { manifest, connections };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async function deleteManifestResources(manifest) {
|
|
301
|
+
const resources = manifest.resources || {};
|
|
302
|
+
if (Object.keys(resources).length === 0) return;
|
|
303
|
+
const client = new KilnClient(manifest.kiln?.api || {});
|
|
304
|
+
for (const resource of Object.values(resources)) {
|
|
305
|
+
if (resource?.name) {
|
|
306
|
+
await client.deleteAppliance(resource.name).catch(() => {});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
229
311
|
async function ensureVM(client, vmConfig, networkId) {
|
|
230
312
|
try {
|
|
231
313
|
const existing = await client.getVM(vmConfig.name);
|
|
@@ -525,6 +607,20 @@ function pickVM(vm) {
|
|
|
525
607
|
};
|
|
526
608
|
}
|
|
527
609
|
|
|
610
|
+
function pickAppliance(appliance) {
|
|
611
|
+
return {
|
|
612
|
+
id: appliance.id,
|
|
613
|
+
name: appliance.name,
|
|
614
|
+
kind: appliance.kind,
|
|
615
|
+
vm_id: appliance.vm_id,
|
|
616
|
+
vm_name: appliance.vm_name,
|
|
617
|
+
state: appliance.state,
|
|
618
|
+
ip: appliance.ip,
|
|
619
|
+
private_ip: appliance.private_ip,
|
|
620
|
+
connection: appliance.connection || null,
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
|
|
528
624
|
function pickAPIConfig(api) {
|
|
529
625
|
return {
|
|
530
626
|
...(api.apiUrl ? { apiUrl: api.apiUrl } : {}),
|
|
@@ -242,6 +242,8 @@ export function resolveEnvironment(context, options = {}) {
|
|
|
242
242
|
env: { ...(configured?.env || {}) },
|
|
243
243
|
kiln: configured?.kiln || null,
|
|
244
244
|
publicHost: process.env.TESTKIT_PUBLIC_HOST || configured?.publicHost || null,
|
|
245
|
+
resources: configured?.resources || {},
|
|
246
|
+
productionLike: Boolean(configured?.productionLike),
|
|
245
247
|
};
|
|
246
248
|
}
|
|
247
249
|
|
package/lib/runner/template.mjs
CHANGED
|
@@ -240,7 +240,9 @@ export function buildTaskExecutionEnv(config, lease, extraEnv = {}, processEnv =
|
|
|
240
240
|
|
|
241
241
|
function buildExecutionEnvWithContext(config, lease, extraEnv, processEnv, options = {}) {
|
|
242
242
|
const inheritedEnv = { ...processEnv };
|
|
243
|
-
|
|
243
|
+
delete inheritedEnv.DATABASE_URL;
|
|
244
|
+
delete inheritedEnv.TESTKIT_RESOURCE_CONNECTIONS_JSON;
|
|
245
|
+
const templateContext = buildTemplateContext(config, lease, processEnv);
|
|
244
246
|
const serviceEnv = options.omitRuntimeDatabaseBindings
|
|
245
247
|
? omitRuntimeDatabaseBindings(config.testkit.serviceEnv || {})
|
|
246
248
|
: config.testkit.serviceEnv || {};
|
|
@@ -261,7 +263,6 @@ function buildExecutionEnvWithContext(config, lease, extraEnv, processEnv, optio
|
|
|
261
263
|
...(lease?.leaseId ? { TESTKIT_LEASE_ID: String(lease.leaseId) } : {}),
|
|
262
264
|
...(lease?.leaseDir ? { TESTKIT_LEASE_DIR: lease.leaseDir } : {}),
|
|
263
265
|
};
|
|
264
|
-
delete env.DATABASE_URL;
|
|
265
266
|
return env;
|
|
266
267
|
}
|
|
267
268
|
|
|
@@ -284,7 +285,7 @@ export function buildPlaywrightEnv(config, baseUrl, lease, processEnv = process.
|
|
|
284
285
|
);
|
|
285
286
|
}
|
|
286
287
|
|
|
287
|
-
function buildTemplateContext(config, lease) {
|
|
288
|
+
function buildTemplateContext(config, lease, processEnv = process.env) {
|
|
288
289
|
const baseContext = config.testkit?.templateContext || {};
|
|
289
290
|
return {
|
|
290
291
|
...baseContext,
|
|
@@ -296,6 +297,7 @@ function buildTemplateContext(config, lease) {
|
|
|
296
297
|
prepareDir: config.testkit?.prepareDir || baseContext.prepareDir || null,
|
|
297
298
|
leaseId: lease?.leaseId || null,
|
|
298
299
|
leaseDir: lease?.leaseDir || null,
|
|
300
|
+
resourceConnectionByName: parseResourceConnections(processEnv.TESTKIT_RESOURCE_CONNECTIONS_JSON),
|
|
299
301
|
};
|
|
300
302
|
}
|
|
301
303
|
|
|
@@ -331,7 +333,7 @@ function finalizeSerializable(value, context) {
|
|
|
331
333
|
export function resolveTemplateString(value, context) {
|
|
332
334
|
if (typeof value !== "string") return value;
|
|
333
335
|
|
|
334
|
-
return value.replace(/\{([a-zA-Z]+)(?::([
|
|
336
|
+
return value.replace(/\{([a-zA-Z]+)(?::([^}]+))?\}/g, (_match, token, arg) => {
|
|
335
337
|
switch (token) {
|
|
336
338
|
case "runtime":
|
|
337
339
|
case "runtimeId":
|
|
@@ -391,6 +393,10 @@ export function resolveTemplateString(value, context) {
|
|
|
391
393
|
const serviceName = arg || context.serviceName;
|
|
392
394
|
return resolveDatabaseTemplateValue(token, serviceName, context);
|
|
393
395
|
}
|
|
396
|
+
case "resource": {
|
|
397
|
+
const [resourceName, field = "url"] = String(arg || "").split(":");
|
|
398
|
+
return resolveResourceTemplateValue(resourceName, field, context);
|
|
399
|
+
}
|
|
394
400
|
default:
|
|
395
401
|
throw new Error(`Unsupported template token "{${token}${arg ? `:${arg}` : ""}}"`);
|
|
396
402
|
}
|
|
@@ -423,7 +429,7 @@ function omitRuntimeDatabaseBindings(values = {}) {
|
|
|
423
429
|
return Object.fromEntries(
|
|
424
430
|
Object.entries(values).filter(([_key, value]) => {
|
|
425
431
|
if (typeof value !== "string") return true;
|
|
426
|
-
return !/\{db(?:Url|Host|Port|Name|User|Password)(?::[
|
|
432
|
+
return !/\{(?:db(?:Url|Host|Port|Name|User|Password)(?::[^}]+)?|resource:[^}]+)\}/.test(value);
|
|
427
433
|
})
|
|
428
434
|
);
|
|
429
435
|
}
|
|
@@ -473,6 +479,32 @@ function resolveDatabaseTemplateValue(token, serviceName, context) {
|
|
|
473
479
|
}
|
|
474
480
|
}
|
|
475
481
|
|
|
482
|
+
function resolveResourceTemplateValue(resourceName, field, context) {
|
|
483
|
+
if (!resourceName) {
|
|
484
|
+
throw new Error("Resource placeholder requires a resource name");
|
|
485
|
+
}
|
|
486
|
+
const connections = context.resourceConnectionByName || {};
|
|
487
|
+
const resource = connections[resourceName];
|
|
488
|
+
if (!resource) {
|
|
489
|
+
throw new Error(`Unknown resource placeholder for resource "${resourceName}"`);
|
|
490
|
+
}
|
|
491
|
+
const value = resource[field];
|
|
492
|
+
if (value == null) {
|
|
493
|
+
throw new Error(`Unknown resource field "${field}" for resource "${resourceName}"`);
|
|
494
|
+
}
|
|
495
|
+
return String(value);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function parseResourceConnections(raw) {
|
|
499
|
+
if (!raw) return {};
|
|
500
|
+
try {
|
|
501
|
+
const parsed = JSON.parse(raw);
|
|
502
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
503
|
+
} catch {
|
|
504
|
+
return {};
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
476
508
|
export function rewriteUrlPort(rawUrl, port) {
|
|
477
509
|
try {
|
|
478
510
|
const original = new URL(rawUrl);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elench/testkit-bridge",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.139",
|
|
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.139"
|
|
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.139",
|
|
4
4
|
"description": "Assistant-first CLI for running, inspecting, and debugging local testkit suites",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"workspaces": [
|
|
@@ -98,10 +98,10 @@
|
|
|
98
98
|
},
|
|
99
99
|
"dependencies": {
|
|
100
100
|
"@babel/code-frame": "^7.29.0",
|
|
101
|
-
"@elench/next-analysis": "0.1.
|
|
102
|
-
"@elench/testkit-bridge": "0.1.
|
|
103
|
-
"@elench/testkit-protocol": "0.1.
|
|
104
|
-
"@elench/ts-analysis": "0.1.
|
|
101
|
+
"@elench/next-analysis": "0.1.139",
|
|
102
|
+
"@elench/testkit-bridge": "0.1.139",
|
|
103
|
+
"@elench/testkit-protocol": "0.1.139",
|
|
104
|
+
"@elench/ts-analysis": "0.1.139",
|
|
105
105
|
"@oclif/core": "^4.10.6",
|
|
106
106
|
"@playwright/test": "^1.52.0",
|
|
107
107
|
"esbuild": "^0.25.11",
|