@elench/testkit 0.1.85 → 0.1.87

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.
@@ -0,0 +1,320 @@
1
+ const DEFAULT_SLUG_MAX_LENGTH = 63;
2
+ const DEFAULT_STRING_SUFFIX_LENGTH = 12;
3
+ const DEFAULT_TOKEN_SUFFIX_LENGTH = 16;
4
+ const DEFAULT_EMAIL_DOMAIN = "example.test";
5
+ const STACK_LOCATION_PATTERN = /(file:\/\/[^\s)]+|\/[^\s):]+):(\d+):(\d+)/;
6
+
7
+ export function createFixtureScope({ namespace, locationSkips = [] } = {}) {
8
+ const normalizedNamespace = normalizeNamespace(namespace);
9
+ const entries = new Map();
10
+ const creationOrder = [];
11
+ const activeEntries = [];
12
+ const values = createFixtureValues(normalizedNamespace);
13
+ const skipMatchers = [
14
+ "/runtime-src/shared/fixture-engine.mjs",
15
+ "/runtime-src/k6/dal-fixtures.js",
16
+ ...locationSkips,
17
+ ].filter(Boolean);
18
+
19
+ function seed(kind, key, signature, create) {
20
+ const normalizedKind = normalizeRequiredString(kind, "fixture kind");
21
+ const normalizedKey = normalizeRequiredString(key, "fixture key");
22
+ if (typeof create !== "function") {
23
+ throw new Error(
24
+ `Fixture ${formatFixtureRef(normalizedKind, normalizedKey)} requires a synchronous create callback.`
25
+ );
26
+ }
27
+
28
+ const entryId = buildEntryId(normalizedKind, normalizedKey);
29
+ const parentEntry = activeEntries[activeEntries.length - 1] || null;
30
+ if (parentEntry) {
31
+ parentEntry.dependencies.add(entryId);
32
+ }
33
+
34
+ const normalizedSignature = normalizeJsonValue(signature);
35
+ const signatureJson = stableJsonStringify(normalizedSignature);
36
+ const existing = entries.get(entryId);
37
+ if (existing) {
38
+ if (existing.state === "creating") {
39
+ throw new Error(formatFixtureCycleError(existing, activeEntries, normalizedKind, normalizedKey));
40
+ }
41
+
42
+ if (existing.signatureJson !== signatureJson) {
43
+ throw new Error(
44
+ formatFixtureConflictError(existing, normalizedSignature, signatureJson)
45
+ );
46
+ }
47
+
48
+ existing.reuseCount += 1;
49
+ return existing.result;
50
+ }
51
+
52
+ const entry = {
53
+ entryId,
54
+ kind: normalizedKind,
55
+ key: normalizedKey,
56
+ state: "creating",
57
+ signature: normalizedSignature,
58
+ signatureJson,
59
+ location: captureCallsite(skipMatchers),
60
+ dependencies: new Set(),
61
+ reuseCount: 0,
62
+ order: creationOrder.length + 1,
63
+ result: undefined,
64
+ };
65
+
66
+ entries.set(entryId, entry);
67
+ creationOrder.push(entryId);
68
+ activeEntries.push(entry);
69
+
70
+ try {
71
+ const result = create();
72
+ if (result && typeof result.then === "function") {
73
+ throw new Error(
74
+ `Fixture ${formatFixtureRef(normalizedKind, normalizedKey)} returned a Promise. DAL fixtures must be synchronous.`
75
+ );
76
+ }
77
+ entry.state = "ready";
78
+ entry.result = result;
79
+ return result;
80
+ } catch (error) {
81
+ entries.delete(entryId);
82
+ creationOrder.pop();
83
+ throw error;
84
+ } finally {
85
+ activeEntries.pop();
86
+ }
87
+ }
88
+
89
+ function records() {
90
+ return creationOrder
91
+ .map((entryId) => entries.get(entryId))
92
+ .filter(Boolean)
93
+ .map((entry) => ({
94
+ kind: entry.kind,
95
+ key: entry.key,
96
+ signature: cloneJsonValue(entry.signature),
97
+ reuseCount: entry.reuseCount,
98
+ order: entry.order,
99
+ dependencies: [...entry.dependencies]
100
+ .map((dependencyId) => entries.get(dependencyId))
101
+ .filter(Boolean)
102
+ .map((dependency) => ({
103
+ kind: dependency.kind,
104
+ key: dependency.key,
105
+ })),
106
+ ...(entry.location ? { location: { ...entry.location } } : {}),
107
+ }));
108
+ }
109
+
110
+ return {
111
+ namespace: normalizedNamespace,
112
+ values,
113
+ id: values.uuid,
114
+ uuid: values.uuid,
115
+ slug: values.slug,
116
+ email: values.email,
117
+ string: values.string,
118
+ token: values.token,
119
+ seed,
120
+ records,
121
+ };
122
+ }
123
+
124
+ export function createFixtureValues(namespace) {
125
+ const normalizedNamespace = normalizeNamespace(namespace);
126
+
127
+ function uuid(label) {
128
+ const hex = namespacedHex(normalizedNamespace, label).slice(0, 32).split("");
129
+ hex[12] = "4";
130
+ hex[16] = ((Number.parseInt(hex[16] || "0", 16) & 0x3) | 0x8).toString(16);
131
+ return `${hex.slice(0, 8).join("")}-${hex.slice(8, 12).join("")}-${hex.slice(12, 16).join("")}-${hex.slice(16, 20).join("")}-${hex.slice(20, 32).join("")}`;
132
+ }
133
+
134
+ function slug(label, options = {}) {
135
+ const fallback = normalizeToken(options.fallback) || "resource";
136
+ const base = normalizeToken(label) || fallback;
137
+ const maxLength = normalizePositiveInteger(options.maxLength) || DEFAULT_SLUG_MAX_LENGTH;
138
+ return `${base}-${namespacedSuffix(normalizedNamespace, `slug:${label}`, DEFAULT_STRING_SUFFIX_LENGTH)}`.slice(
139
+ 0,
140
+ maxLength
141
+ );
142
+ }
143
+
144
+ function email(label, options = {}) {
145
+ const fallback = normalizeToken(options.fallback) || "user";
146
+ const base = normalizeToken(label) || fallback;
147
+ const domain = normalizeEmailDomain(options.domain) || DEFAULT_EMAIL_DOMAIN;
148
+ const suffixLength =
149
+ normalizePositiveInteger(options.suffixLength) || DEFAULT_STRING_SUFFIX_LENGTH;
150
+ return `${base}-${namespacedSuffix(normalizedNamespace, `email:${label}`, suffixLength)}@${domain}`;
151
+ }
152
+
153
+ function string(label, options = {}) {
154
+ const fallback = normalizeToken(options.fallback) || "value";
155
+ const prefix = normalizeToken(options.prefix) || normalizeToken(label) || fallback;
156
+ const suffixLength =
157
+ normalizePositiveInteger(options.suffixLength) || DEFAULT_STRING_SUFFIX_LENGTH;
158
+ const maxLength = normalizePositiveInteger(options.maxLength) || null;
159
+ const value = `${prefix}-${namespacedSuffix(normalizedNamespace, `string:${label}:${prefix}`, suffixLength)}`;
160
+ return maxLength ? value.slice(0, maxLength) : value;
161
+ }
162
+
163
+ function token(label, options = {}) {
164
+ return string(label, {
165
+ ...options,
166
+ fallback: options.fallback || "token",
167
+ suffixLength: normalizePositiveInteger(options.suffixLength) || DEFAULT_TOKEN_SUFFIX_LENGTH,
168
+ });
169
+ }
170
+
171
+ return {
172
+ uuid,
173
+ slug,
174
+ email,
175
+ string,
176
+ token,
177
+ };
178
+ }
179
+
180
+ function formatFixtureConflictError(existing, nextSignature, nextSignatureJson) {
181
+ const location = existing.location ? ` First declared at ${formatLocation(existing.location)}.` : "";
182
+ return [
183
+ `Fixture ${formatFixtureRef(existing.kind, existing.key)} was seeded twice with conflicting signatures.`,
184
+ location,
185
+ ` Existing: ${existing.signatureJson}`,
186
+ ` New: ${nextSignatureJson || stableJsonStringify(normalizeJsonValue(nextSignature))}`,
187
+ ].join("");
188
+ }
189
+
190
+ function formatFixtureCycleError(existing, activeEntries, kind, key) {
191
+ const cycleStartIndex = activeEntries.findIndex((entry) => entry.entryId === existing.entryId);
192
+ const cycleEntries = cycleStartIndex >= 0 ? activeEntries.slice(cycleStartIndex) : activeEntries;
193
+ const chain = [...cycleEntries.map((entry) => formatFixtureRef(entry.kind, entry.key)), formatFixtureRef(kind, key)];
194
+ return `Fixture dependency cycle detected: ${chain.join(" -> ")}.`;
195
+ }
196
+
197
+ function formatFixtureRef(kind, key) {
198
+ return `${kind}:${key}`;
199
+ }
200
+
201
+ function formatLocation(location) {
202
+ return `${location.path}:${location.line}:${location.column}`;
203
+ }
204
+
205
+ function buildEntryId(kind, key) {
206
+ return `${kind}::${key}`;
207
+ }
208
+
209
+ function normalizeNamespace(namespace) {
210
+ return normalizeToken(namespace) || "standalone";
211
+ }
212
+
213
+ function normalizeRequiredString(value, label) {
214
+ const normalized = String(value || "").trim();
215
+ if (normalized.length === 0) {
216
+ throw new Error(`Fixture ${label} must be a non-empty string.`);
217
+ }
218
+ return normalized;
219
+ }
220
+
221
+ function normalizePositiveInteger(value) {
222
+ if (!Number.isInteger(value) || value <= 0) return null;
223
+ return value;
224
+ }
225
+
226
+ function normalizeEmailDomain(value) {
227
+ const normalized = String(value || "").trim().toLowerCase();
228
+ return normalized.length > 0 ? normalized : null;
229
+ }
230
+
231
+ function normalizeToken(value) {
232
+ return String(value || "")
233
+ .trim()
234
+ .toLowerCase()
235
+ .replace(/[^a-z0-9]+/g, "-")
236
+ .replace(/^-+|-+$/g, "");
237
+ }
238
+
239
+ function hash32(input) {
240
+ let hash = 0x811c9dc5;
241
+ for (let index = 0; index < input.length; index += 1) {
242
+ hash ^= input.charCodeAt(index);
243
+ hash = Math.imul(hash, 0x01000193) >>> 0;
244
+ }
245
+ return hash.toString(16).padStart(8, "0");
246
+ }
247
+
248
+ function namespacedHex(namespace, label) {
249
+ return [0, 1, 2, 3].map((part) => hash32(`${namespace}:${label}:${part}`)).join("");
250
+ }
251
+
252
+ function namespacedSuffix(namespace, label, length) {
253
+ return namespacedHex(namespace, label).slice(0, length);
254
+ }
255
+
256
+ function captureCallsite(skipMatchers) {
257
+ const stack = new Error().stack;
258
+ if (!stack) return null;
259
+
260
+ for (const line of stack.split("\n").slice(1)) {
261
+ const match = line.match(STACK_LOCATION_PATTERN);
262
+ if (!match) continue;
263
+ const [rawPath, rawLine, rawColumn] = match.slice(1);
264
+ if (!rawPath || !rawLine || !rawColumn) continue;
265
+ const path = normalizeStackPath(rawPath);
266
+ if (skipMatchers.some((matcher) => path.endsWith(matcher) || path.includes(matcher))) {
267
+ continue;
268
+ }
269
+ return {
270
+ path,
271
+ line: Number(rawLine),
272
+ column: Number(rawColumn),
273
+ };
274
+ }
275
+
276
+ return null;
277
+ }
278
+
279
+ function normalizeStackPath(rawPath) {
280
+ return String(rawPath || "").replace(/^file:\/\//, "");
281
+ }
282
+
283
+ function stableJsonStringify(value) {
284
+ return JSON.stringify(normalizeJsonValue(value));
285
+ }
286
+
287
+ function cloneJsonValue(value) {
288
+ return normalizeJsonValue(value);
289
+ }
290
+
291
+ function normalizeJsonValue(value) {
292
+ if (
293
+ value === null ||
294
+ value === undefined ||
295
+ typeof value === "boolean" ||
296
+ typeof value === "number" ||
297
+ typeof value === "string"
298
+ ) {
299
+ return value;
300
+ }
301
+
302
+ if (typeof value === "bigint") {
303
+ return value.toString();
304
+ }
305
+
306
+ if (Array.isArray(value)) {
307
+ return value.map((entry) => normalizeJsonValue(entry));
308
+ }
309
+
310
+ if (typeof value === "object") {
311
+ return Object.keys(value)
312
+ .sort()
313
+ .reduce((result, key) => {
314
+ result[key] = normalizeJsonValue(value[key]);
315
+ return result;
316
+ }, {});
317
+ }
318
+
319
+ return String(value);
320
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/next-analysis",
3
- "version": "0.1.85",
3
+ "version": "0.1.87",
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.85",
3
+ "version": "0.1.87",
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.85"
25
+ "@elench/testkit-protocol": "0.1.87"
26
26
  },
27
27
  "private": false
28
28
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/testkit-protocol",
3
- "version": "0.1.85",
3
+ "version": "0.1.87",
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.85",
3
+ "version": "0.1.87",
4
4
  "description": "TypeScript compiler-backed source analysis primitives for Erench tools",
5
5
  "type": "module",
6
6
  "exports": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/testkit",
3
- "version": "0.1.85",
3
+ "version": "0.1.87",
4
4
  "description": "CLI for discovering and running local HTTP, DAL, and Playwright test suites",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -82,10 +82,10 @@
82
82
  },
83
83
  "dependencies": {
84
84
  "@babel/code-frame": "^7.29.0",
85
- "@elench/next-analysis": "0.1.85",
86
- "@elench/testkit-bridge": "0.1.85",
87
- "@elench/testkit-protocol": "0.1.85",
88
- "@elench/ts-analysis": "0.1.85",
85
+ "@elench/next-analysis": "0.1.87",
86
+ "@elench/testkit-bridge": "0.1.87",
87
+ "@elench/testkit-protocol": "0.1.87",
88
+ "@elench/ts-analysis": "0.1.87",
89
89
  "@oclif/core": "^4.10.6",
90
90
  "esbuild": "^0.25.11",
91
91
  "execa": "^9.5.0",
@@ -1,45 +0,0 @@
1
- import { Args, Command } from "@oclif/core";
2
- import { sharedFlags } from "../command-helpers.mjs";
3
- import { collectArtifactEntries, loadCurrentRunArtifact } from "../viewer.mjs";
4
-
5
- export default class ArtifactsCommand extends Command {
6
- static summary = "List persisted artifacts from the latest run";
7
-
8
- static enableJsonFlag = true;
9
-
10
- static args = {
11
- file: Args.string({
12
- description: "Optional file path to filter artifacts",
13
- required: false,
14
- }),
15
- };
16
-
17
- static flags = sharedFlags;
18
-
19
- async run() {
20
- const { args, flags } = await this.parse(ArtifactsCommand);
21
- const productDir = flags.dir || process.cwd();
22
- const runArtifact = loadCurrentRunArtifact(productDir);
23
- const entries = collectArtifactEntries(productDir, runArtifact, args.file || null, flags.service || null)
24
- .map((entry) => ({
25
- service: entry.service.name,
26
- suite: `${entry.suite.type}:${entry.suite.name}`,
27
- file: entry.file.path,
28
- name: entry.artifactRef.name,
29
- kind: entry.artifactRef.kind,
30
- summary: entry.artifactRef.summary,
31
- path: entry.artifactRef.path,
32
- }));
33
-
34
- if (!this.jsonEnabled()) {
35
- for (const entry of entries) {
36
- this.log(`${entry.file}`);
37
- this.log(` ${entry.name}${entry.kind ? ` [${entry.kind}]` : ""}`);
38
- if (entry.summary) this.log(` ${entry.summary}`);
39
- this.log(` ${entry.path}`);
40
- }
41
- }
42
-
43
- return entries;
44
- }
45
- }
@@ -1,47 +0,0 @@
1
- import { Args, Command, Flags } from "@oclif/core";
2
- import { sharedFlags } from "../command-helpers.mjs";
3
- import { readLogTail } from "../../runner/logs.mjs";
4
- import { getServiceLogRefs, loadCurrentRunArtifact, resolveFileSubject } from "../viewer.mjs";
5
- import path from "path";
6
-
7
- export default class LogsCommand extends Command {
8
- static summary = "Show backend log tails relevant to one file from the latest run";
9
-
10
- static enableJsonFlag = true;
11
-
12
- static args = {
13
- file: Args.string({
14
- description: "Optional file path; defaults to the first failed file",
15
- required: false,
16
- }),
17
- };
18
-
19
- static flags = {
20
- ...sharedFlags,
21
- tail: Flags.integer({
22
- description: "Number of lines to show from each log",
23
- default: 40,
24
- }),
25
- };
26
-
27
- async run() {
28
- const { args, flags } = await this.parse(LogsCommand);
29
- const productDir = flags.dir || process.cwd();
30
- const runArtifact = loadCurrentRunArtifact(productDir);
31
- const subject = resolveFileSubject(runArtifact, args.file || null, flags.service || null);
32
- const logs = getServiceLogRefs(runArtifact, subject.service.name).map((entry) => ({
33
- ...entry,
34
- lines: readLogTail(path.join(productDir, entry.path), flags.tail),
35
- }));
36
-
37
- if (!this.jsonEnabled()) {
38
- for (const entry of logs) {
39
- this.log(`${entry.runtimeLabel}`);
40
- this.log(` ${entry.path}`);
41
- for (const line of entry.lines) this.log(` ${line}`);
42
- }
43
- }
44
-
45
- return logs;
46
- }
47
- }
@@ -1,47 +0,0 @@
1
- import { Args, Command, Flags } from "@oclif/core";
2
- import { sharedFlags } from "../command-helpers.mjs";
3
- import { formatFileDetail, loadCurrentRunArtifact, resolveFileSubject } from "../viewer.mjs";
4
-
5
- export default class ShowCommand extends Command {
6
- static summary = "Show the most useful details for one file from the latest run";
7
-
8
- static enableJsonFlag = true;
9
-
10
- static args = {
11
- file: Args.string({
12
- description: "File path to inspect; defaults to the first failed file",
13
- required: false,
14
- }),
15
- };
16
-
17
- static flags = {
18
- ...sharedFlags,
19
- "log-tail": Flags.integer({
20
- description: "Number of backend log lines to include",
21
- default: 12,
22
- }),
23
- };
24
-
25
- async run() {
26
- const { args, flags } = await this.parse(ShowCommand);
27
- const productDir = flags.dir || process.cwd();
28
- const runArtifact = loadCurrentRunArtifact(productDir);
29
- const subject = resolveFileSubject(runArtifact, args.file || null, flags.service || null);
30
- const result = {
31
- file: subject.file,
32
- suite: {
33
- name: subject.suite.name,
34
- type: subject.suite.type,
35
- },
36
- service: {
37
- name: subject.service.name,
38
- },
39
- };
40
- if (!this.jsonEnabled()) {
41
- for (const line of formatFileDetail(productDir, runArtifact, subject, { logTail: flags["log-tail"] })) {
42
- this.log(line);
43
- }
44
- }
45
- return result;
46
- }
47
- }
@@ -1,23 +0,0 @@
1
- import React, { createElement } from "react";
2
- import { Command } from "@oclif/core";
3
- import { render } from "ink";
4
- import { sharedFlags } from "../command-helpers.mjs";
5
- import { WatchApp } from "../tui/watch-app.mjs";
6
-
7
- export default class WatchCommand extends Command {
8
- static summary = "Open an interactive viewer for the latest run artifact";
9
-
10
- static flags = sharedFlags;
11
-
12
- async run() {
13
- const { flags } = await this.parse(WatchCommand);
14
- const productDir = flags.dir || process.cwd();
15
- const app = render(
16
- createElement(WatchApp, {
17
- productDir,
18
- serviceFilter: flags.service || null,
19
- })
20
- );
21
- await app.waitUntilExit();
22
- }
23
- }
@@ -1 +0,0 @@
1
- export { RunSessionApp as RunApp } from "./run-session-app.mjs";