@elench/testkit 0.1.85 → 0.1.86

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 CHANGED
@@ -423,15 +423,49 @@ const suite = defineHttpSuite({ profile: "defaultAuth" }, ({ actor, actors, req
423
423
  DAL suites:
424
424
 
425
425
  ```ts
426
- import { defineDalSuite } from "@elench/testkit";
426
+ import { defineDalFixtures, defineDalSuite } from "@elench/testkit";
427
+
428
+ const fixtures = defineDalFixtures(({ db, fixtureScope }) => ({
429
+ widget() {
430
+ return fixtureScope.seed("widget", "primary", { name: "Primary Widget" }, () => {
431
+ const widgetId = fixtureScope.id("widget");
432
+ db.exec(`
433
+ INSERT INTO widgets (id, name)
434
+ VALUES ('${widgetId}', 'Primary Widget')
435
+ `);
436
+ return { widgetId };
437
+ });
438
+ },
439
+ }));
427
440
 
428
- const suite = defineDalSuite(({ db }) => {
429
- db.query("select 1");
441
+ const suite = defineDalSuite({ fixtures }, ({ db, fixtureScope, fixtures }) => {
442
+ const widget = fixtures.widget();
443
+ db.query(`select id from widgets where id = '${widget.widgetId}'`);
444
+ fixtureScope.records();
430
445
  });
431
446
 
432
447
  export default suite;
433
448
  ```
434
449
 
450
+ `defineDalFixtures(...)` is the package-owned DAL seeding model. It gives every
451
+ suite a deterministic `fixtureScope` with:
452
+
453
+ - `fixtureScope.id(label)` / `uuid(label)`
454
+ - `fixtureScope.slug(label)`
455
+ - `fixtureScope.email(label)`
456
+ - `fixtureScope.string(label, options)`
457
+ - `fixtureScope.token(label)`
458
+ - `fixtureScope.seed(kind, key, signature, create)`
459
+ - `fixtureScope.records()`
460
+
461
+ `testkit` enforces strict fixture behavior:
462
+
463
+ - one logical fixture per `kind + key`
464
+ - identical reseeds reuse the same seeded value
465
+ - conflicting reseeds fail immediately
466
+ - dependency cycles fail immediately
467
+ - seeded fixture records are persisted as a `testkit.dal-fixtures` artifact
468
+
435
469
  Scenario suites:
436
470
 
437
471
  ```ts
package/lib/index.d.ts CHANGED
@@ -1,11 +1,13 @@
1
1
  import type {
2
2
  ActorRequestClient,
3
+ DalFixtureDefinition,
3
4
  HttpClient,
4
5
  HttpClientConfig,
5
6
  RawRequestClient,
6
7
  RuntimeDb,
7
8
  RuntimeDalContext,
8
9
  RuntimeEnv,
10
+ RuntimeFixtureScope,
9
11
  RuntimeHeaders,
10
12
  RuntimeOptions,
11
13
  RuntimeResponse,
@@ -120,16 +122,24 @@ export interface HttpSuiteConfig<TSession = unknown> {
120
122
  options?: RuntimeOptions;
121
123
  }
122
124
 
123
- export interface DalSuiteContext<TSetup = unknown> {
125
+ export interface DalSuiteContext<TSetup = unknown, TFixtures = Record<string, unknown>> {
124
126
  db: RuntimeDb;
125
127
  dal: RuntimeDalContext;
128
+ fixtureScope: RuntimeFixtureScope;
129
+ fixtures: TFixtures;
126
130
  setupData: TSetup | null;
127
131
  }
128
132
 
129
- export interface DalSuiteConfig<TSetup = unknown> {
133
+ export interface DalSuiteConfig<TSetup = unknown, TFixtures = Record<string, unknown>> {
130
134
  db?: RuntimeDb;
135
+ fixtures?: DalFixtureDefinition<TFixtures> | null;
131
136
  options?: RuntimeOptions;
132
- setup?: (context: { db: RuntimeDb; dal: RuntimeDalContext }) => TSetup;
137
+ setup?: (context: {
138
+ db: RuntimeDb;
139
+ dal: RuntimeDalContext;
140
+ fixtureScope: RuntimeFixtureScope;
141
+ fixtures: TFixtures;
142
+ }) => TSetup;
133
143
  }
134
144
 
135
145
  export declare function defineHttpSuite<TSetup = unknown>(
@@ -150,13 +160,23 @@ export declare function defineScenarioSuite<TSetup = unknown>(
150
160
  run: (context: ScenarioSuiteContext<TSetup>) => unknown
151
161
  ): TestkitSuite<TSetup>;
152
162
 
153
- export declare function defineDalSuite<TSetup = unknown>(
154
- run: (context: DalSuiteContext<TSetup>) => unknown
163
+ export declare function defineDalFixtures<TFixtures = Record<string, unknown>>(
164
+ factory: (context: {
165
+ db: RuntimeDb;
166
+ dal: RuntimeDalContext;
167
+ fixtureScope: RuntimeFixtureScope;
168
+ seed: RuntimeFixtureScope["seed"];
169
+ values: RuntimeFixtureScope["values"];
170
+ }) => TFixtures
171
+ ): DalFixtureDefinition<TFixtures>;
172
+
173
+ export declare function defineDalSuite<TSetup = unknown, TFixtures = Record<string, unknown>>(
174
+ run: (context: DalSuiteContext<TSetup, TFixtures>) => unknown
155
175
  ): TestkitSuite<TSetup>;
156
176
 
157
- export declare function defineDalSuite<TSetup = unknown>(
158
- config: DalSuiteConfig<TSetup>,
159
- run: (context: DalSuiteContext<TSetup>) => unknown
177
+ export declare function defineDalSuite<TSetup = unknown, TFixtures = Record<string, unknown>>(
178
+ config: DalSuiteConfig<TSetup, TFixtures>,
179
+ run: (context: DalSuiteContext<TSetup, TFixtures>) => unknown
160
180
  ): TestkitSuite<TSetup>;
161
181
 
162
182
  export declare function createAuthAdapter<TSetup = unknown>(
@@ -164,11 +184,13 @@ export declare function createAuthAdapter<TSetup = unknown>(
164
184
  ): AuthAdapter<TSetup>;
165
185
 
166
186
  export type {
187
+ DalFixtureDefinition,
167
188
  HttpClient,
168
189
  HttpClientConfig,
169
190
  RuntimeDb,
170
191
  RuntimeDalContext,
171
192
  RuntimeEnv,
193
+ RuntimeFixtureScope,
172
194
  RuntimeHeaders,
173
195
  RuntimeOptions,
174
196
  RuntimeResponse,
package/lib/index.mjs CHANGED
@@ -1,6 +1,9 @@
1
1
  export {
2
2
  defineDalSuite,
3
3
  } from "./runtime-src/k6/dal-suite.js";
4
+ export {
5
+ defineDalFixtures,
6
+ } from "./runtime-src/k6/dal-fixtures.js";
4
7
  export {
5
8
  defineHttpSuite,
6
9
  } from "./runtime-src/k6/suite.js";
@@ -82,6 +82,83 @@ export interface RuntimeDalContext {
82
82
  truncate(...tables: string[]): void;
83
83
  }
84
84
 
85
+ export interface RuntimeFixtureLocation {
86
+ column: number;
87
+ line: number;
88
+ path: string;
89
+ }
90
+
91
+ export interface RuntimeFixtureReference {
92
+ key: string;
93
+ kind: string;
94
+ }
95
+
96
+ export interface RuntimeFixtureRecord {
97
+ dependencies: RuntimeFixtureReference[];
98
+ key: string;
99
+ kind: string;
100
+ location?: RuntimeFixtureLocation;
101
+ order: number;
102
+ reuseCount: number;
103
+ signature: unknown;
104
+ }
105
+
106
+ export interface RuntimeFixtureStringOptions {
107
+ fallback?: string;
108
+ maxLength?: number;
109
+ prefix?: string;
110
+ suffixLength?: number;
111
+ }
112
+
113
+ export interface RuntimeFixtureEmailOptions {
114
+ domain?: string;
115
+ fallback?: string;
116
+ suffixLength?: number;
117
+ }
118
+
119
+ export interface RuntimeFixtureSlugOptions {
120
+ fallback?: string;
121
+ maxLength?: number;
122
+ }
123
+
124
+ export interface RuntimeFixtureValueHelpers {
125
+ email(label: string, options?: RuntimeFixtureEmailOptions): string;
126
+ slug(label: string, options?: RuntimeFixtureSlugOptions): string;
127
+ string(label: string, options?: RuntimeFixtureStringOptions): string;
128
+ token(label: string, options?: RuntimeFixtureStringOptions): string;
129
+ uuid(label: string): string;
130
+ }
131
+
132
+ export interface RuntimeFixtureScope {
133
+ email(label: string, options?: RuntimeFixtureEmailOptions): string;
134
+ id(label: string): string;
135
+ namespace: string;
136
+ records(): RuntimeFixtureRecord[];
137
+ seed<TResult>(
138
+ kind: string,
139
+ key: string,
140
+ signature: unknown,
141
+ create: () => TResult
142
+ ): TResult;
143
+ slug(label: string, options?: RuntimeFixtureSlugOptions): string;
144
+ string(label: string, options?: RuntimeFixtureStringOptions): string;
145
+ token(label: string, options?: RuntimeFixtureStringOptions): string;
146
+ uuid(label: string): string;
147
+ values: RuntimeFixtureValueHelpers;
148
+ }
149
+
150
+ export interface DalFixtureDefinitionContext {
151
+ dal: RuntimeDalContext;
152
+ db: RuntimeDb;
153
+ fixtureScope: RuntimeFixtureScope;
154
+ seed: RuntimeFixtureScope["seed"];
155
+ values: RuntimeFixtureValueHelpers;
156
+ }
157
+
158
+ export interface DalFixtureDefinition<TFixtures = Record<string, unknown>> {
159
+ create(context: DalFixtureDefinitionContext): TFixtures;
160
+ }
161
+
85
162
  export interface HttpRequestParams {
86
163
  headers?: RuntimeHeaders;
87
164
  redirects?: number;
@@ -0,0 +1,66 @@
1
+ import { emitArtifact } from "./artifacts.js";
2
+ import { createFixtureScope } from "../shared/fixture-engine.mjs";
3
+ import { readTestkitContext } from "../../shared/test-context.mjs";
4
+
5
+ export function defineDalFixtures(factory) {
6
+ if (typeof factory !== "function") {
7
+ throw new Error("defineDalFixtures(factory) requires a fixture factory function.");
8
+ }
9
+
10
+ return {
11
+ create: factory,
12
+ };
13
+ }
14
+
15
+ export function createDalFixtureRuntime({ db, dal, definition = null, env = __ENV } = {}) {
16
+ const runtimeContext = readTestkitContext(env);
17
+ const fixtureScope = createFixtureScope({
18
+ namespace: `dal-${runtimeContext.namespace}`,
19
+ });
20
+
21
+ const fixtureDefinition = normalizeFixtureDefinition(definition);
22
+ const fixtures = fixtureDefinition
23
+ ? fixtureDefinition.create({
24
+ db,
25
+ dal,
26
+ fixtureScope,
27
+ seed: fixtureScope.seed,
28
+ values: fixtureScope.values,
29
+ }) || {}
30
+ : {};
31
+
32
+ let artifactEmitted = false;
33
+
34
+ function emitArtifactOnce() {
35
+ if (artifactEmitted) return;
36
+ const records = fixtureScope.records();
37
+ if (records.length === 0) return;
38
+
39
+ emitArtifact(
40
+ "dal-fixtures",
41
+ {
42
+ namespace: fixtureScope.namespace,
43
+ fixtures: records,
44
+ },
45
+ {
46
+ kind: "testkit.dal-fixtures",
47
+ summary: `${records.length} DAL fixture(s)`,
48
+ }
49
+ );
50
+ artifactEmitted = true;
51
+ }
52
+
53
+ return {
54
+ fixtures,
55
+ fixtureScope,
56
+ emitArtifactOnce,
57
+ };
58
+ }
59
+
60
+ function normalizeFixtureDefinition(definition) {
61
+ if (!definition) return null;
62
+ if (typeof definition.create !== "function") {
63
+ throw new Error("DAL fixture definitions must be created with defineDalFixtures(...).");
64
+ }
65
+ return definition;
66
+ }
@@ -6,23 +6,40 @@ import {
6
6
  startFailureCollection,
7
7
  } from "./checks.js";
8
8
  import { createDalContext, openDb } from "./dal.js";
9
+ import { createDalFixtureRuntime } from "./dal-fixtures.js";
9
10
 
10
11
  export function defineDalSuite(configOrRun, maybeRun) {
11
12
  const { config, run } = normalizeSuiteArgs(configOrRun, maybeRun);
12
13
  const db = config.db || openDb();
13
14
  const dal = createDalContext(db);
15
+ const fixtureRuntime = createDalFixtureRuntime({
16
+ db,
17
+ dal,
18
+ definition: config.fixtures || null,
19
+ });
14
20
 
15
21
  return {
16
22
  options: config.options || defaultOptions,
17
23
  setup() {
18
24
  if (typeof config.setup !== "function") return null;
19
25
  startFailureCollection("setup");
26
+ let setupCompleted = false;
20
27
  try {
21
- return config.setup({ db, dal });
28
+ const result = config.setup({
29
+ db,
30
+ dal,
31
+ fixtures: fixtureRuntime.fixtures,
32
+ fixtureScope: fixtureRuntime.fixtureScope,
33
+ });
34
+ setupCompleted = true;
35
+ return result;
22
36
  } catch (error) {
23
37
  recordRuntimeFailure();
24
38
  fail(formatFatalSuiteError("setup", error));
25
39
  } finally {
40
+ if (!setupCompleted) {
41
+ fixtureRuntime.emitArtifactOnce();
42
+ }
26
43
  emitFailureCollectionArtifact();
27
44
  }
28
45
  },
@@ -32,12 +49,15 @@ export function defineDalSuite(configOrRun, maybeRun) {
32
49
  return run({
33
50
  db,
34
51
  dal,
52
+ fixtures: fixtureRuntime.fixtures,
53
+ fixtureScope: fixtureRuntime.fixtureScope,
35
54
  setupData,
36
55
  });
37
56
  } catch (error) {
38
57
  recordRuntimeFailure();
39
58
  fail(formatFatalSuiteError("exec", error));
40
59
  } finally {
60
+ fixtureRuntime.emitArtifactOnce();
41
61
  emitFailureCollectionArtifact();
42
62
  }
43
63
  },
@@ -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.86",
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.86",
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.86"
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.86",
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.86",
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.86",
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.86",
86
+ "@elench/testkit-bridge": "0.1.86",
87
+ "@elench/testkit-protocol": "0.1.86",
88
+ "@elench/ts-analysis": "0.1.86",
89
89
  "@oclif/core": "^4.10.6",
90
90
  "esbuild": "^0.25.11",
91
91
  "execa": "^9.5.0",