@glubean/runner 0.1.28 → 0.1.29

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/dist/harness.js CHANGED
@@ -6,11 +6,10 @@
6
6
  * tsx harness.ts --testUrl=<url> --testId=<id>
7
7
  */
8
8
  import { parseArgs } from "node:util";
9
+ import { AsyncLocalStorage } from "node:async_hooks";
9
10
  import { inferJsonSchema, truncateDeep } from "./schema_inference.js";
10
11
  /* eslint-enable no-var */
11
12
  import ky from "ky";
12
- import { classifyHostnameBlockReason, classifyIpBlockReason, isAllowedPort, isAllowedProtocol, isIpLiteral, resolveUrlPort, } from "./network_policy.js";
13
- import { applyResponseByteBudget } from "./network_budget.js";
14
13
  import { Expectation } from "@glubean/sdk/expect";
15
14
  // Global error handlers for async errors that escape try/catch
16
15
  process.on("uncaughtException", (error) => {
@@ -40,6 +39,7 @@ const { values: args } = parseArgs({
40
39
  testIds: { type: "string" },
41
40
  exportName: { type: "string" },
42
41
  exportNames: { type: "string" },
42
+ concurrency: { type: "string" },
43
43
  emitFullTrace: { type: "boolean", default: false },
44
44
  inferSchema: { type: "boolean", default: false },
45
45
  truncateArrays: { type: "boolean", default: false },
@@ -60,6 +60,12 @@ const testId = args.testId;
60
60
  * module-level state (e.g. shared `let` variables between tests).
61
61
  */
62
62
  const testIds = args.testIds ? args.testIds.split(",") : undefined;
63
+ /**
64
+ * Max concurrency for parallel batch mode.
65
+ * When > 1 and testIds is set, parallel-marked tests run concurrently via p-queue.
66
+ * Default 1 (sequential).
67
+ */
68
+ const batchConcurrency = Math.max(1, parseInt(args.concurrency, 10) || 1);
63
69
  /** Optional export name for fallback lookup (used by test.pick/test.each). */
64
70
  const exportName = args.exportName;
65
71
  /** Optional testId→exportName mapping for batch mode fallback (test.pick). */
@@ -118,39 +124,76 @@ function parseRuntimeTestMetadata(input) {
118
124
  return { id, tags };
119
125
  }
120
126
  const runtimeTest = parseRuntimeTestMetadata(contextData.test);
121
- function parseNetworkPolicy(input) {
122
- if (!input || typeof input !== "object")
123
- return undefined;
124
- const candidate = input;
125
- if (candidate.mode !== "shared_serverless")
126
- return undefined;
127
- const allowedPorts = Array.isArray(candidate.allowedPorts)
128
- ? candidate.allowedPorts.filter((p) => typeof p === "number" && Number.isFinite(p) && p > 0 && p <= 65535).map((p) => Math.floor(p))
129
- : [];
130
- return {
131
- mode: "shared_serverless",
132
- maxRequests: Number(candidate.maxRequests) > 0 ? Math.floor(Number(candidate.maxRequests)) : 300,
133
- maxConcurrentRequests: Number(candidate.maxConcurrentRequests) > 0
134
- ? Math.floor(Number(candidate.maxConcurrentRequests))
135
- : 20,
136
- requestTimeoutMs: Number(candidate.requestTimeoutMs) > 0 ? Math.floor(Number(candidate.requestTimeoutMs)) : 30_000,
137
- maxResponseBytes: Number(candidate.maxResponseBytes) > 0
138
- ? Math.floor(Number(candidate.maxResponseBytes))
139
- : 20 * 1024 * 1024,
140
- allowedPorts: allowedPorts.length > 0 ? Array.from(new Set(allowedPorts)) : [80, 443, 8080, 8443],
141
- };
142
- }
143
- const networkPolicy = parseNetworkPolicy(contextData.networkPolicy);
144
- // Memory monitoring state
127
+ // Memory monitoring state (per-process — not isolated per-test)
145
128
  let peakMemoryBytes = 0;
146
129
  let memoryCheckInterval;
147
- // Step-level assertion tracking.
148
- // Reset before each step, incremented by ctx.assert on failure.
149
- let stepFailedAssertions = 0;
150
- let stepAssertionTotal = 0;
151
- // Current step index (null when not inside a step).
152
- // Used to tag log/assertion/trace/metric events with their containing step.
153
- let currentStepIndex = null;
130
+ // ---------------------------------------------------------------------------
131
+ // Per-test execution context via AsyncLocalStorage
132
+ // ---------------------------------------------------------------------------
133
+ /**
134
+ * Holds all mutable state scoped to a single test execution.
135
+ * Accessed via `testContext.getStore()!` from anywhere in the async call chain
136
+ * (ky hooks, globalFetch override, ctx.assert, etc.) without explicit passing.
137
+ *
138
+ * Analogous to Java's ThreadLocal but tracks Node.js async continuations.
139
+ */
140
+ class TestRunContext {
141
+ testId;
142
+ // Step-level assertion tracking
143
+ stepFailedAssertions = 0;
144
+ stepAssertionTotal = 0;
145
+ currentStepIndex = null;
146
+ // HTTP request counters
147
+ httpRequestTotal = 0;
148
+ httpErrorTotal = 0;
149
+ // Runtime identity
150
+ testMeta;
151
+ constructor(testId, meta) {
152
+ this.testId = testId;
153
+ this.testMeta = meta;
154
+ }
155
+ }
156
+ const testContext = new AsyncLocalStorage();
157
+ /**
158
+ * Get the current test's execution context.
159
+ * Returns undefined when not inside a test (e.g., module load phase).
160
+ */
161
+ function currentTestCtx() {
162
+ return testContext.getStore();
163
+ }
164
+ // Accessor helpers — read from ALS when inside a test, safe defaults otherwise.
165
+ // These replace direct reads of the old global variables.
166
+ function getStepIndex() {
167
+ return currentTestCtx()?.currentStepIndex ?? null;
168
+ }
169
+ function getStepAssertionTotal() {
170
+ return currentTestCtx()?.stepAssertionTotal ?? 0;
171
+ }
172
+ function getStepFailedAssertions() {
173
+ return currentTestCtx()?.stepFailedAssertions ?? 0;
174
+ }
175
+ function incrAssertions(passed) {
176
+ const trc = currentTestCtx();
177
+ if (trc) {
178
+ trc.stepAssertionTotal++;
179
+ if (!passed)
180
+ trc.stepFailedAssertions++;
181
+ }
182
+ }
183
+ /**
184
+ * Emit a JSON event to stdout, auto-injecting `testId` when inside a test context.
185
+ * Use this for all test-scoped event output to ensure concurrent events can be
186
+ * attributed to the correct test.
187
+ */
188
+ function emitEvent(event) {
189
+ const trc = currentTestCtx();
190
+ if (trc) {
191
+ console.log(JSON.stringify({ ...event, testId: trc.testId }));
192
+ }
193
+ else {
194
+ console.log(JSON.stringify(event));
195
+ }
196
+ }
154
197
  /**
155
198
  * Start monitoring memory usage.
156
199
  * Samples memory every 100ms and tracks peak usage.
@@ -296,14 +339,14 @@ function runSchemaValidation(data, schema, label, severity) {
296
339
  issues = [{ message: "Schema has neither safeParse nor parse method" }];
297
340
  }
298
341
  // Emit schema_validation event (always, regardless of success/severity)
299
- console.log(JSON.stringify({
342
+ emitEvent({
300
343
  type: "schema_validation",
301
344
  label,
302
345
  success,
303
346
  severity,
304
347
  ...(issues.length > 0 && { issues }),
305
- ...(currentStepIndex !== null && { stepIndex: currentStepIndex }),
306
- }));
348
+ ...(getStepIndex() !== null && { stepIndex: getStepIndex() }),
349
+ });
307
350
  if (!success) {
308
351
  const issuesSummary = issues
309
352
  .map((i) => {
@@ -379,18 +422,18 @@ const ctx = {
379
422
  },
380
423
  set: (key, value) => {
381
424
  sessionData[key] = value;
382
- console.log(JSON.stringify({ type: "session:set", key, value, ts: Date.now() }));
425
+ emitEvent({ type: "session:set", key, value, ts: Date.now() });
383
426
  },
384
427
  entries: () => ({ ...sessionData }),
385
428
  },
386
429
  // Logging function
387
430
  log: (message, data) => {
388
- console.log(JSON.stringify({
431
+ emitEvent({
389
432
  type: "log",
390
433
  message,
391
434
  data,
392
- ...(currentStepIndex !== null && { stepIndex: currentStepIndex }),
393
- }));
435
+ ...(getStepIndex() !== null && { stepIndex: getStepIndex() }),
436
+ });
394
437
  },
395
438
  // Assertion function with overloads
396
439
  // Overload 1: assert(condition: boolean, message?: string, details?: AssertionDetails)
@@ -420,19 +463,16 @@ const ctx = {
420
463
  (passed ? "Assertion passed" : "Assertion failed");
421
464
  }
422
465
  // Track per-step assertion stats (used for step retry logic + step_end event)
423
- stepAssertionTotal++;
424
- if (!passed) {
425
- stepFailedAssertions++;
426
- }
427
- console.log(JSON.stringify({
466
+ incrAssertions(passed);
467
+ emitEvent({
428
468
  type: "assertion",
429
469
  passed,
430
470
  message,
431
471
  // Truncate actual/expected on pass to save tokens; keep full on fail for debugging
432
472
  actual: passed && truncateArrays ? truncateDeep(actual) : actual,
433
473
  expected: passed && truncateArrays ? truncateDeep(expected) : expected,
434
- ...(currentStepIndex !== null && { stepIndex: currentStepIndex }),
435
- }));
474
+ ...(getStepIndex() !== null && { stepIndex: getStepIndex() }),
475
+ });
436
476
  },
437
477
  // Fluent assertion API (Jest-style, soft-by-default)
438
478
  expect: (actual) => {
@@ -448,12 +488,12 @@ const ctx = {
448
488
  // Warning function — soft check, never affects test pass/fail.
449
489
  // condition=true means OK; condition=false triggers warning.
450
490
  warn: (condition, message) => {
451
- console.log(JSON.stringify({
491
+ emitEvent({
452
492
  type: "warning",
453
493
  condition,
454
494
  message,
455
- ...(currentStepIndex !== null && { stepIndex: currentStepIndex }),
456
- }));
495
+ ...(getStepIndex() !== null && { stepIndex: getStepIndex() }),
496
+ });
457
497
  },
458
498
  // Schema validation function
459
499
  validate: (data, schema, label, options) => {
@@ -462,11 +502,11 @@ const ctx = {
462
502
  },
463
503
  // API tracing function
464
504
  trace: (request) => {
465
- console.log(JSON.stringify({
505
+ emitEvent({
466
506
  type: "trace",
467
507
  data: request,
468
- ...(currentStepIndex !== null && { stepIndex: currentStepIndex }),
469
- }));
508
+ ...(getStepIndex() !== null && { stepIndex: getStepIndex() }),
509
+ });
470
510
  // Backward compat: also emit as a typed action for timeline/filtering
471
511
  let pathname;
472
512
  try {
@@ -485,30 +525,30 @@ const ctx = {
485
525
  },
486
526
  // Action recording function
487
527
  action: (a) => {
488
- console.log(JSON.stringify({
528
+ emitEvent({
489
529
  type: "action",
490
530
  data: a,
491
- ...(currentStepIndex !== null && { stepIndex: currentStepIndex }),
492
- }));
531
+ ...(getStepIndex() !== null && { stepIndex: getStepIndex() }),
532
+ });
493
533
  },
494
534
  // Structured event emission
495
535
  event: (ev) => {
496
- console.log(JSON.stringify({
536
+ emitEvent({
497
537
  type: "event",
498
538
  data: ev,
499
- ...(currentStepIndex !== null && { stepIndex: currentStepIndex }),
500
- }));
539
+ ...(getStepIndex() !== null && { stepIndex: getStepIndex() }),
540
+ });
501
541
  },
502
542
  // Metric reporting function
503
543
  metric: (name, value, options) => {
504
- console.log(JSON.stringify({
544
+ emitEvent({
505
545
  type: "metric",
506
546
  name,
507
547
  value,
508
548
  unit: options?.unit,
509
549
  tags: options?.tags,
510
- ...(currentStepIndex !== null && { stepIndex: currentStepIndex }),
511
- }));
550
+ ...(getStepIndex() !== null && { stepIndex: getStepIndex() }),
551
+ });
512
552
  },
513
553
  /**
514
554
  * Skip the current test with an optional reason.
@@ -525,11 +565,11 @@ const ctx = {
525
565
  */
526
566
  fail: (message) => {
527
567
  // Emit a failed assertion so the failure reason appears in events
528
- console.log(JSON.stringify({
568
+ emitEvent({
529
569
  type: "assertion",
530
570
  passed: false,
531
571
  message,
532
- }));
572
+ });
533
573
  throw new FailError(message);
534
574
  },
535
575
  /**
@@ -570,10 +610,7 @@ const ctx = {
570
610
  * @param ms Timeout in milliseconds
571
611
  */
572
612
  setTimeout: (ms) => {
573
- console.log(JSON.stringify({
574
- type: "timeout_update",
575
- timeout: ms,
576
- }));
613
+ emitEvent({ type: "timeout_update", timeout: ms });
577
614
  },
578
615
  /**
579
616
  * Current execution retry count (0 for first attempt).
@@ -594,17 +631,7 @@ const ctx = {
594
631
  return process.memoryUsage();
595
632
  },
596
633
  };
597
- // ---------------------------------------------------------------------------
598
- // Auto-tracing HTTP client (ctx.http) — powered by ky
599
- // ---------------------------------------------------------------------------
600
- // Track request start time. We use a simple variable instead of a WeakMap
601
- // because ky may clone/recreate the Request object between beforeRequest and
602
- // afterResponse hooks, breaking reference equality in a WeakMap.
603
- let lastRequestStartTime = 0;
604
- let httpRequestTotal = 0;
605
- let httpErrorTotal = 0;
606
- // Captured in beforeRequest when emitFullTrace is on
607
- let lastRequestBody = undefined;
634
+ const requestTraceMap = new WeakMap();
608
635
  /** Max serialized body size (chars) to include in trace events. */
609
636
  const TRACE_BODY_MAX_SIZE = 1_048_576; // 1MB
610
637
  /**
@@ -650,187 +677,39 @@ function truncateBody(body) {
650
677
  * Reset per-test counters for file-level batch mode.
651
678
  * Called before each test when running multiple tests in a single process.
652
679
  */
680
+ /**
681
+ * Reset per-process state between tests in batch mode.
682
+ * Per-test state is now handled by TestRunContext via AsyncLocalStorage —
683
+ * each test gets a fresh instance, so no reset is needed for those.
684
+ */
653
685
  function resetTestCounters() {
654
- stepFailedAssertions = 0;
655
- stepAssertionTotal = 0;
656
- currentStepIndex = null;
657
- httpRequestTotal = 0;
658
- httpErrorTotal = 0;
659
686
  peakMemoryBytes = 0;
660
687
  }
661
- const MAX_NETWORK_WARNINGS_PER_CODE = 3;
662
- const networkWarningCounts = new Map();
663
- let networkRequestCount = 0;
664
- let networkInFlightCount = 0;
665
- let networkResponseBytes = 0;
666
- function emitNetworkWarning(code, message) {
667
- const nextCount = (networkWarningCounts.get(code) ?? 0) + 1;
668
- networkWarningCounts.set(code, nextCount);
669
- if (nextCount <= MAX_NETWORK_WARNINGS_PER_CODE) {
670
- ctx.warn(false, `[network_guard:${code}] ${message}`);
671
- }
672
- else if (nextCount === MAX_NETWORK_WARNINGS_PER_CODE + 1) {
673
- ctx.warn(false, `[network_guard:${code}] further warnings suppressed`);
674
- }
675
- }
676
- async function resolveHostIps(hostname) {
677
- const dns = await import("node:dns/promises");
678
- const ips = new Set();
679
- try {
680
- const aRecords = await dns.resolve4(hostname);
681
- for (const ip of aRecords)
682
- ips.add(ip);
683
- }
684
- catch {
685
- // Ignore; try AAAA next.
686
- }
687
- try {
688
- const aaaaRecords = await dns.resolve6(hostname);
689
- for (const ip of aaaaRecords)
690
- ips.add(ip);
691
- }
692
- catch {
693
- // Ignore; caller decides behavior when no records are resolved.
694
- }
695
- return Array.from(ips);
696
- }
697
- function toRequestUrl(input) {
698
- if (input instanceof Request)
699
- return new URL(input.url);
700
- if (input instanceof URL)
701
- return input;
702
- return new URL(input);
703
- }
704
- async function enforceNetworkPolicy(url) {
705
- if (!networkPolicy)
706
- return;
707
- if (!isAllowedProtocol(url.protocol)) {
708
- emitNetworkWarning("protocol_blocked", `Blocked protocol ${url.protocol} for ${url.href}`);
709
- throw new Error(`Network policy blocked protocol ${url.protocol}. Only http/https are allowed.`);
710
- }
711
- const port = resolveUrlPort(url);
712
- if (!isAllowedPort(port, networkPolicy.allowedPorts)) {
713
- emitNetworkWarning("port_blocked", `Blocked port ${port} for ${url.href}`);
714
- throw new Error(`Network policy blocked destination port ${port}.`);
715
- }
716
- const hostname = url.hostname.toLowerCase();
717
- const hostnameReason = classifyHostnameBlockReason(hostname);
718
- if (hostnameReason) {
719
- emitNetworkWarning(hostnameReason, `Blocked hostname ${hostname} for ${url.href}`);
720
- throw new Error(`Network policy blocked sensitive hostname ${hostname}.`);
721
- }
722
- if (isIpLiteral(hostname)) {
723
- const ipReason = classifyIpBlockReason(hostname);
724
- if (ipReason) {
725
- emitNetworkWarning(ipReason, `Blocked destination IP ${hostname} for ${url.href}`);
726
- throw new Error(`Network policy blocked destination IP ${hostname}.`);
727
- }
728
- return;
729
- }
730
- const resolvedIps = await resolveHostIps(hostname);
731
- if (resolvedIps.length === 0) {
732
- emitNetworkWarning("dns_resolution_failed", `Could not resolve ${hostname} for ${url.href}`);
733
- throw new Error(`Network policy could not resolve host ${hostname}. Request denied.`);
734
- }
735
- for (const ip of resolvedIps) {
736
- const ipReason = classifyIpBlockReason(ip);
737
- if (ipReason) {
738
- emitNetworkWarning(ipReason, `Blocked resolved IP ${ip} (${hostname}) for ${url.href}`);
739
- throw new Error(`Network policy blocked resolved destination ${ip} for host ${hostname}.`);
740
- }
741
- }
742
- }
743
- const originalFetch = globalThis.fetch.bind(globalThis);
744
- globalThis.fetch = async (input, init) => {
745
- if (!networkPolicy) {
746
- return originalFetch(input, init);
747
- }
748
- const requestUrl = toRequestUrl(input);
749
- if (networkRequestCount >= networkPolicy.maxRequests) {
750
- emitNetworkWarning("request_limit_exceeded", `Request limit exceeded (${networkPolicy.maxRequests})`);
751
- throw new Error(`Network policy exceeded max outbound requests (${networkPolicy.maxRequests}).`);
752
- }
753
- if (networkInFlightCount >= networkPolicy.maxConcurrentRequests) {
754
- emitNetworkWarning("concurrency_limit_exceeded", `In-flight request limit exceeded (${networkPolicy.maxConcurrentRequests})`);
755
- throw new Error(`Network policy exceeded max concurrent outbound requests (${networkPolicy.maxConcurrentRequests}).`);
756
- }
757
- // Reserve counters before await to avoid TOCTOU races when user code issues
758
- // concurrent requests in a single Promise.all frame.
759
- networkRequestCount++;
760
- networkInFlightCount++;
761
- const timeoutController = new AbortController();
762
- let timedOutByPolicy = false;
763
- const parentSignal = (() => {
764
- if (!init || typeof init !== "object" || !("signal" in init)) {
765
- return undefined;
766
- }
767
- const candidate = init.signal;
768
- return candidate instanceof AbortSignal ? candidate : undefined;
769
- })();
770
- const onParentAbort = () => timeoutController.abort(parentSignal?.reason);
771
- if (parentSignal) {
772
- if (parentSignal.aborted) {
773
- timeoutController.abort(parentSignal.reason);
774
- }
775
- else {
776
- parentSignal.addEventListener("abort", onParentAbort, { once: true });
777
- }
778
- }
779
- const timeoutId = setTimeout(() => {
780
- timedOutByPolicy = true;
781
- timeoutController.abort(new Error(`Network request timed out after ${networkPolicy.requestTimeoutMs}ms`));
782
- }, networkPolicy.requestTimeoutMs);
783
- try {
784
- await enforceNetworkPolicy(requestUrl);
785
- const response = await originalFetch(input, {
786
- ...init,
787
- signal: timeoutController.signal,
788
- });
789
- return applyResponseByteBudget(response, {
790
- requestUrl,
791
- maxResponseBytes: networkPolicy.maxResponseBytes,
792
- getUsedResponseBytes: () => networkResponseBytes,
793
- addUsedResponseBytes: (delta) => {
794
- networkResponseBytes += delta;
795
- },
796
- emitWarning: emitNetworkWarning,
797
- });
798
- }
799
- catch (error) {
800
- if (error instanceof Error && error.name === "AbortError") {
801
- if (timedOutByPolicy) {
802
- throw new Error(`Network request timed out after ${networkPolicy.requestTimeoutMs}ms`);
803
- }
804
- }
805
- throw error;
806
- }
807
- finally {
808
- clearTimeout(timeoutId);
809
- if (parentSignal) {
810
- parentSignal.removeEventListener("abort", onParentAbort);
811
- }
812
- networkInFlightCount = Math.max(0, networkInFlightCount - 1);
813
- }
814
- };
815
688
  const kyInstance = ky.create({
816
689
  throwHttpErrors: false,
817
690
  hooks: {
818
691
  beforeRequest: [
819
692
  (_request, options) => {
820
- lastRequestStartTime = performance.now();
821
- if (emitFullTrace) {
822
- // Capture request body from ky options before the request is sent
823
- lastRequestBody = options.json ?? options.body ?? undefined;
824
- }
693
+ requestTraceMap.set(options, {
694
+ startTime: performance.now(),
695
+ body: emitFullTrace
696
+ ? (options.json ?? options.body ?? undefined)
697
+ : undefined,
698
+ });
825
699
  },
826
700
  ],
827
701
  afterResponse: [
828
702
  async (request, _options, response) => {
829
- const duration = Math.round(performance.now() - lastRequestStartTime);
703
+ const trace = requestTraceMap.get(_options);
704
+ const duration = Math.round(performance.now() - (trace?.startTime ?? performance.now()));
830
705
  // Increment HTTP counters for summary
831
- httpRequestTotal++;
832
- if (response.status >= 400) {
833
- httpErrorTotal++;
706
+ {
707
+ const t = currentTestCtx();
708
+ if (t) {
709
+ t.httpRequestTotal++;
710
+ if (response.status >= 400)
711
+ t.httpErrorTotal++;
712
+ }
834
713
  }
835
714
  // Build trace data — enriched when emitFullTrace is on
836
715
  const traceData = {
@@ -846,8 +725,8 @@ const kyInstance = ky.create({
846
725
  }
847
726
  if (emitFullTrace) {
848
727
  traceData.requestHeaders = Object.fromEntries(request.headers.entries());
849
- if (lastRequestBody !== undefined) {
850
- traceData.requestBody = truncateBody(lastRequestBody);
728
+ if (trace?.body !== undefined) {
729
+ traceData.requestBody = truncateBody(trace.body);
851
730
  }
852
731
  traceData.responseHeaders = Object.fromEntries(response.headers.entries());
853
732
  // Clone the response to read the body without consuming the original stream
@@ -880,7 +759,7 @@ const kyInstance = ky.create({
880
759
  catch {
881
760
  // Ignore clone/parse errors — trace still emits without body
882
761
  }
883
- lastRequestBody = undefined;
762
+ // Per-request state is on the options object; no global cleanup needed.
884
763
  }
885
764
  ctx.trace(traceData);
886
765
  // Auto-metric for response time
@@ -1028,7 +907,16 @@ globalThis.__glubeanRuntime = {
1028
907
  vars: withEnvFallback(rawVars),
1029
908
  secrets: withEnvFallback(rawSecrets),
1030
909
  http: wrappedHttp,
1031
- test: runtimeTest,
910
+ // Getter: returns per-test metadata from ALS when inside a test,
911
+ // falls back to the initial runtimeTest (module load phase).
912
+ get test() {
913
+ return currentTestCtx()?.testMeta ?? runtimeTest;
914
+ },
915
+ set test(value) {
916
+ const trc = currentTestCtx();
917
+ if (trc)
918
+ trc.testMeta = value;
919
+ },
1032
920
  action: ctx.action,
1033
921
  event: ctx.event,
1034
922
  log: ctx.log,
@@ -1058,25 +946,30 @@ try {
1058
946
  session: ctx.session,
1059
947
  log: ctx.log,
1060
948
  };
1061
- try {
1062
- if (sessionMode === "setup") {
1063
- await def.setup(sessionCtx);
949
+ // Session setup/teardown needs an ALS context so network policy
950
+ // counters (in globalFetch override) work correctly.
951
+ const sessionTrc = new TestRunContext("__session__", { id: "__session__", tags: [] });
952
+ await testContext.run(sessionTrc, async () => {
953
+ try {
954
+ if (sessionMode === "setup") {
955
+ await def.setup(sessionCtx);
956
+ }
957
+ else if (sessionMode === "teardown" && typeof def.teardown === "function") {
958
+ await def.teardown(sessionCtx);
959
+ }
960
+ emitEvent({ type: "status", status: "completed" });
1064
961
  }
1065
- else if (sessionMode === "teardown" && typeof def.teardown === "function") {
1066
- await def.teardown(sessionCtx);
962
+ catch (err) {
963
+ const message = err instanceof Error ? err.message : String(err);
964
+ const stack = err instanceof Error ? err.stack : undefined;
965
+ emitEvent({
966
+ type: "status",
967
+ status: "failed",
968
+ error: message,
969
+ ...(stack && { stack }),
970
+ });
1067
971
  }
1068
- console.log(JSON.stringify({ type: "status", status: "completed" }));
1069
- }
1070
- catch (err) {
1071
- const message = err instanceof Error ? err.message : String(err);
1072
- const stack = err instanceof Error ? err.stack : undefined;
1073
- console.log(JSON.stringify({
1074
- type: "status",
1075
- status: "failed",
1076
- error: message,
1077
- ...(stack && { stack }),
1078
- }));
1079
- }
972
+ });
1080
973
  process.exit(0);
1081
974
  }
1082
975
  // ── Normal test mode: testId or testIds required ──
@@ -1089,56 +982,51 @@ try {
1089
982
  }
1090
983
  if (testIds) {
1091
984
  // ── File-level batch mode ──
1092
- // Run multiple tests sequentially in a single process.
1093
- // Module-level state (let variables) is preserved between tests.
985
+ // Runs multiple tests in a single process, preserving module-level state.
986
+ // When batchConcurrency > 1, tests run concurrently via p-queue
987
+ // (opt-in via test.each({ parallel: true }) + --concurrency flag).
1094
988
  let hasFailure = false;
1095
- for (const id of testIds) {
989
+ const runOneTest = async (id) => {
1096
990
  resetTestCounters();
1097
991
  let testObj = findTestById(userModule, id);
1098
- // Fallback for non-deterministic tests (test.pick): the testId from
1099
- // discovery may differ from this run's random selection. Use the stable
1100
- // exportName to locate the test.
1101
992
  if (!testObj && exportNamesMap[id]) {
1102
993
  testObj = findTestByExport(userModule, exportNamesMap[id]);
1103
994
  }
1104
995
  if (!testObj) {
1105
- console.log(JSON.stringify({
1106
- type: "start",
1107
- id,
1108
- name: id,
1109
- }));
1110
- console.log(JSON.stringify({
1111
- type: "status",
1112
- status: "failed",
1113
- id,
1114
- error: `Test "${id}" not found in module`,
1115
- }));
996
+ console.log(JSON.stringify({ type: "start", id, name: id, testId: id }));
997
+ console.log(JSON.stringify({ type: "status", status: "failed", id, testId: id, error: `Test "${id}" not found in module` }));
1116
998
  hasFailure = true;
1117
- continue;
999
+ return;
1118
1000
  }
1119
1001
  try {
1120
1002
  await executeNewTest(testObj);
1121
1003
  }
1122
1004
  catch (error) {
1123
1005
  if (error instanceof SkipError) {
1124
- console.log(JSON.stringify({
1125
- type: "status",
1126
- status: "skipped",
1127
- id,
1128
- reason: error.reason,
1129
- }));
1006
+ console.log(JSON.stringify({ type: "status", status: "skipped", id, testId: id, reason: error.reason }));
1130
1007
  }
1131
1008
  else {
1132
1009
  hasFailure = true;
1133
1010
  console.log(JSON.stringify({
1134
- type: "status",
1135
- status: "failed",
1136
- id,
1011
+ type: "status", status: "failed", id, testId: id,
1137
1012
  error: error instanceof Error ? error.message : String(error),
1138
1013
  stack: error instanceof Error ? error.stack : undefined,
1139
1014
  }));
1140
1015
  }
1141
1016
  }
1017
+ };
1018
+ if (batchConcurrency > 1) {
1019
+ const { default: PQueue } = await import("p-queue");
1020
+ const queue = new PQueue({ concurrency: batchConcurrency });
1021
+ for (const id of testIds) {
1022
+ void queue.add(() => runOneTest(id));
1023
+ }
1024
+ await queue.onIdle();
1025
+ }
1026
+ else {
1027
+ for (const id of testIds) {
1028
+ await runOneTest(id);
1029
+ }
1142
1030
  }
1143
1031
  process.exit(hasFailure ? 1 : 0);
1144
1032
  }
@@ -1159,11 +1047,11 @@ try {
1159
1047
  }
1160
1048
  catch (error) {
1161
1049
  if (error instanceof SkipError) {
1162
- console.log(JSON.stringify({ type: "status", status: "skipped", id: resolved.id, reason: error.reason }));
1050
+ console.log(JSON.stringify({ type: "status", status: "skipped", id: resolved.id, testId: resolved.id, reason: error.reason }));
1163
1051
  }
1164
1052
  else {
1165
1053
  hasFailure = true;
1166
- console.log(JSON.stringify({ type: "status", status: "failed", id: resolved.id, error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined }));
1054
+ console.log(JSON.stringify({ type: "status", status: "failed", id: resolved.id, testId: resolved.id, error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined }));
1167
1055
  }
1168
1056
  }
1169
1057
  }
@@ -1185,11 +1073,14 @@ try {
1185
1073
  }
1186
1074
  }
1187
1075
  catch (error) {
1076
+ // Include testId when known (single test mode)
1077
+ const tid = testId && testId !== "*" ? testId : undefined;
1188
1078
  // Check if this is a skip error
1189
1079
  if (error instanceof SkipError) {
1190
1080
  console.log(JSON.stringify({
1191
1081
  type: "status",
1192
1082
  status: "skipped",
1083
+ ...(tid && { testId: tid }),
1193
1084
  reason: error.reason,
1194
1085
  }));
1195
1086
  process.exit(0); // Exit cleanly for skipped tests
@@ -1198,6 +1089,7 @@ catch (error) {
1198
1089
  console.log(JSON.stringify({
1199
1090
  type: "status",
1200
1091
  status: "failed",
1092
+ ...(tid && { testId: tid }),
1201
1093
  error: error instanceof Error ? error.message : String(error),
1202
1094
  stack: error instanceof Error ? error.stack : undefined,
1203
1095
  }));
@@ -1274,222 +1166,231 @@ async function withFixtures(fixtures, baseCtx, runTest) {
1274
1166
  */
1275
1167
  async function executeNewTest(test) {
1276
1168
  const testTags = normalizeTestTags(test.meta.tags);
1277
- // Keep runtime metadata aligned with the actual resolved test before user code runs.
1278
- globalThis.__glubeanRuntime.test = {
1279
- id: test.meta.id,
1280
- tags: testTags,
1281
- };
1282
- console.log(JSON.stringify({
1283
- type: "start",
1284
- id: test.meta.id,
1285
- name: test.meta.name || test.meta.id,
1286
- tags: testTags,
1287
- ...(retryCount > 0 && { retryCount }),
1288
- }));
1289
- // Start memory monitoring
1290
- startMemoryMonitoring();
1291
- try {
1292
- // Core test body — receives the effective ctx (base or fixture-augmented)
1293
- const runTestBody = async (effectiveCtx) => {
1294
- if (test.type === "simple") {
1295
- if (!test.fn) {
1296
- throw new Error(`Invalid test "${test.meta.id}": missing fn`);
1297
- }
1298
- await test.fn(effectiveCtx);
1299
- }
1300
- else {
1301
- let state = undefined;
1302
- let stepFailed = false;
1303
- try {
1304
- if (test.setup) {
1305
- console.log(JSON.stringify({
1306
- type: "log",
1307
- message: "Running setup...",
1308
- }));
1309
- state = await test.setup(effectiveCtx);
1169
+ const testMeta = { id: test.meta.id, tags: testTags };
1170
+ const trc = new TestRunContext(test.meta.id, testMeta);
1171
+ // Run the test body inside an AsyncLocalStorage context so all
1172
+ // downstream code (ctx.assert, ky hooks, globalFetch) can access
1173
+ // the per-test state via currentTestCtx().
1174
+ await testContext.run(trc, async () => {
1175
+ // Runtime metadata is now served via ALS getter on __glubeanRuntime.test
1176
+ emitEvent({
1177
+ type: "start",
1178
+ id: test.meta.id,
1179
+ name: test.meta.name || test.meta.id,
1180
+ tags: testTags,
1181
+ ...(retryCount > 0 && { retryCount }),
1182
+ });
1183
+ // Start memory monitoring
1184
+ startMemoryMonitoring();
1185
+ try {
1186
+ // Core test body — receives the effective ctx (base or fixture-augmented)
1187
+ const runTestBody = async (effectiveCtx) => {
1188
+ if (test.type === "simple") {
1189
+ if (!test.fn) {
1190
+ throw new Error(`Invalid test "${test.meta.id}": missing fn`);
1310
1191
  }
1311
- if (test.steps) {
1312
- for (let i = 0; i < test.steps.length; i++) {
1313
- const step = test.steps[i];
1314
- // If a previous step failed, skip remaining steps
1315
- if (stepFailed) {
1316
- console.log(JSON.stringify({
1317
- type: "step_end",
1192
+ await test.fn(effectiveCtx);
1193
+ }
1194
+ else {
1195
+ let state = undefined;
1196
+ let stepFailed = false;
1197
+ try {
1198
+ if (test.setup) {
1199
+ emitEvent({
1200
+ type: "log",
1201
+ message: "Running setup...",
1202
+ });
1203
+ state = await test.setup(effectiveCtx);
1204
+ }
1205
+ if (test.steps) {
1206
+ for (let i = 0; i < test.steps.length; i++) {
1207
+ const step = test.steps[i];
1208
+ // If a previous step failed, skip remaining steps
1209
+ if (stepFailed) {
1210
+ emitEvent({
1211
+ type: "step_end",
1212
+ index: i,
1213
+ name: step.meta.name,
1214
+ status: "skipped",
1215
+ durationMs: 0,
1216
+ assertions: 0,
1217
+ failedAssertions: 0,
1218
+ });
1219
+ continue;
1220
+ }
1221
+ // Reset per-step assertion counters and set step scope
1222
+ {
1223
+ const trc = currentTestCtx();
1224
+ trc.stepFailedAssertions = 0;
1225
+ trc.stepAssertionTotal = 0;
1226
+ trc.currentStepIndex = i;
1227
+ }
1228
+ const stepStart = performance.now();
1229
+ emitEvent({
1230
+ type: "step_start",
1318
1231
  index: i,
1319
1232
  name: step.meta.name,
1320
- status: "skipped",
1321
- durationMs: 0,
1322
- assertions: 0,
1323
- failedAssertions: 0,
1324
- }));
1325
- continue;
1326
- }
1327
- // Reset per-step assertion counters and set step scope
1328
- stepFailedAssertions = 0;
1329
- stepAssertionTotal = 0;
1330
- currentStepIndex = i;
1331
- const stepStart = performance.now();
1332
- console.log(JSON.stringify({
1333
- type: "step_start",
1334
- index: i,
1335
- name: step.meta.name,
1336
- total: test.steps.length,
1337
- }));
1338
- let stepError;
1339
- let stepReturnState = undefined;
1340
- const retries = step.meta.retries;
1341
- const configuredRetries = typeof retries === "number" && Number.isFinite(retries)
1342
- ? Math.max(0, Math.floor(retries))
1343
- : 0;
1344
- const stepTimeout = step.meta.timeout;
1345
- const configuredStepTimeout = typeof stepTimeout === "number" && Number.isFinite(stepTimeout)
1346
- ? Math.floor(stepTimeout)
1347
- : 0;
1348
- const stepTimeoutMs = configuredStepTimeout > 0 ? configuredStepTimeout : undefined;
1349
- const maxAttempts = configuredRetries + 1;
1350
- let attemptsUsed = 0;
1351
- let lastFailedAssertions = 0;
1352
- let lastAssertions = 0;
1353
- let timeoutFailure = false;
1354
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1355
- attemptsUsed = attempt;
1356
- stepError = undefined;
1357
- stepReturnState = undefined;
1358
- stepFailedAssertions = 0;
1359
- stepAssertionTotal = 0;
1360
- timeoutFailure = false;
1361
- let stepTimeoutId;
1362
- try {
1363
- const stepResult = step.fn(effectiveCtx, state);
1364
- // Note: timed-out step bodies cannot be force-cancelled in JS.
1365
- // We treat timeout as terminal (no further retries) to avoid
1366
- // overlapping attempts mutating shared step context.
1367
- const result = stepTimeoutMs === undefined ? await stepResult : await Promise.race([
1368
- stepResult,
1369
- new Promise((_, reject) => {
1370
- stepTimeoutId = setTimeout(() => {
1371
- reject(new StepTimeoutError(step.meta.name, stepTimeoutMs));
1372
- }, stepTimeoutMs);
1373
- }),
1374
- ]);
1375
- if (result !== undefined) {
1376
- state = result;
1377
- stepReturnState = result;
1233
+ total: test.steps.length,
1234
+ });
1235
+ let stepError;
1236
+ let stepReturnState = undefined;
1237
+ const retries = step.meta.retries;
1238
+ const configuredRetries = typeof retries === "number" && Number.isFinite(retries)
1239
+ ? Math.max(0, Math.floor(retries))
1240
+ : 0;
1241
+ const stepTimeout = step.meta.timeout;
1242
+ const configuredStepTimeout = typeof stepTimeout === "number" && Number.isFinite(stepTimeout)
1243
+ ? Math.floor(stepTimeout)
1244
+ : 0;
1245
+ const stepTimeoutMs = configuredStepTimeout > 0 ? configuredStepTimeout : undefined;
1246
+ const maxAttempts = configuredRetries + 1;
1247
+ let attemptsUsed = 0;
1248
+ let lastFailedAssertions = 0;
1249
+ let lastAssertions = 0;
1250
+ let timeoutFailure = false;
1251
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1252
+ attemptsUsed = attempt;
1253
+ stepError = undefined;
1254
+ stepReturnState = undefined;
1255
+ {
1256
+ const trc = currentTestCtx();
1257
+ trc.stepFailedAssertions = 0;
1258
+ trc.stepAssertionTotal = 0;
1378
1259
  }
1379
- }
1380
- catch (err) {
1381
- stepError = err instanceof Error ? err.message : String(err);
1382
- timeoutFailure = err instanceof StepTimeoutError;
1383
- }
1384
- finally {
1385
- if (stepTimeoutId !== undefined) {
1386
- clearTimeout(stepTimeoutId);
1260
+ timeoutFailure = false;
1261
+ let stepTimeoutId;
1262
+ try {
1263
+ const stepResult = step.fn(effectiveCtx, state);
1264
+ // Note: timed-out step bodies cannot be force-cancelled in JS.
1265
+ // We treat timeout as terminal (no further retries) to avoid
1266
+ // overlapping attempts mutating shared step context.
1267
+ const result = stepTimeoutMs === undefined ? await stepResult : await Promise.race([
1268
+ stepResult,
1269
+ new Promise((_, reject) => {
1270
+ stepTimeoutId = setTimeout(() => {
1271
+ reject(new StepTimeoutError(step.meta.name, stepTimeoutMs));
1272
+ }, stepTimeoutMs);
1273
+ }),
1274
+ ]);
1275
+ if (result !== undefined) {
1276
+ state = result;
1277
+ stepReturnState = result;
1278
+ }
1279
+ }
1280
+ catch (err) {
1281
+ stepError = err instanceof Error ? err.message : String(err);
1282
+ timeoutFailure = err instanceof StepTimeoutError;
1283
+ }
1284
+ finally {
1285
+ if (stepTimeoutId !== undefined) {
1286
+ clearTimeout(stepTimeoutId);
1287
+ }
1288
+ }
1289
+ lastFailedAssertions = getStepFailedAssertions();
1290
+ lastAssertions = getStepAssertionTotal();
1291
+ const attemptFailed = !!stepError || getStepFailedAssertions() > 0;
1292
+ if (!attemptFailed) {
1293
+ break;
1294
+ }
1295
+ // Timeouts are terminal to avoid overlapping attempts from
1296
+ // dangling async operations in the timed-out step body.
1297
+ if (timeoutFailure) {
1298
+ break;
1299
+ }
1300
+ if (attempt < maxAttempts) {
1301
+ const reason = stepError ? stepError : `${getStepFailedAssertions()} failed assertion(s)`;
1302
+ emitEvent({
1303
+ type: "log",
1304
+ stepIndex: i,
1305
+ message: `Retrying step "${step.meta.name}" (${attempt + 1}/${maxAttempts}) after failure: ${reason}`,
1306
+ });
1387
1307
  }
1388
1308
  }
1389
- lastFailedAssertions = stepFailedAssertions;
1390
- lastAssertions = stepAssertionTotal;
1391
- const attemptFailed = !!stepError || stepFailedAssertions > 0;
1392
- if (!attemptFailed) {
1393
- break;
1394
- }
1395
- // Timeouts are terminal to avoid overlapping attempts from
1396
- // dangling async operations in the timed-out step body.
1397
- if (timeoutFailure) {
1398
- break;
1399
- }
1400
- if (attempt < maxAttempts) {
1401
- const reason = stepError ? stepError : `${stepFailedAssertions} failed assertion(s)`;
1402
- console.log(JSON.stringify({
1403
- type: "log",
1404
- stepIndex: i,
1405
- message: `Retrying step "${step.meta.name}" (${attempt + 1}/${maxAttempts}) after failure: ${reason}`,
1406
- }));
1407
- }
1408
- }
1409
- const durationMs = Math.round(performance.now() - stepStart);
1410
- const failed = !!stepError || lastFailedAssertions > 0;
1411
- // Serialize return state with size guard (max 4 KB)
1412
- let returnStatePayload = undefined;
1413
- if (stepReturnState !== undefined) {
1414
- try {
1415
- const serialized = JSON.stringify(stepReturnState);
1416
- if (serialized.length <= 4096) {
1417
- returnStatePayload = stepReturnState;
1309
+ const durationMs = Math.round(performance.now() - stepStart);
1310
+ const failed = !!stepError || lastFailedAssertions > 0;
1311
+ // Serialize return state with size guard (max 4 KB)
1312
+ let returnStatePayload = undefined;
1313
+ if (stepReturnState !== undefined) {
1314
+ try {
1315
+ const serialized = JSON.stringify(stepReturnState);
1316
+ if (serialized.length <= 4096) {
1317
+ returnStatePayload = stepReturnState;
1318
+ }
1319
+ else {
1320
+ returnStatePayload = `[truncated: ${serialized.length} bytes]`;
1321
+ }
1418
1322
  }
1419
- else {
1420
- returnStatePayload = `[truncated: ${serialized.length} bytes]`;
1323
+ catch {
1324
+ returnStatePayload = "[non-serializable]";
1421
1325
  }
1422
1326
  }
1423
- catch {
1424
- returnStatePayload = "[non-serializable]";
1327
+ emitEvent({
1328
+ type: "step_end",
1329
+ index: i,
1330
+ name: step.meta.name,
1331
+ status: failed ? "failed" : "passed",
1332
+ durationMs,
1333
+ assertions: lastAssertions,
1334
+ failedAssertions: lastFailedAssertions,
1335
+ attempts: attemptsUsed,
1336
+ retriesUsed: Math.max(0, attemptsUsed - 1),
1337
+ ...(stepError && { error: stepError }),
1338
+ ...(returnStatePayload !== undefined && {
1339
+ returnState: returnStatePayload,
1340
+ }),
1341
+ });
1342
+ currentTestCtx().currentStepIndex = null;
1343
+ if (failed) {
1344
+ stepFailed = true;
1345
+ // Don't throw here — let the loop continue to emit skip events
1425
1346
  }
1426
1347
  }
1427
- console.log(JSON.stringify({
1428
- type: "step_end",
1429
- index: i,
1430
- name: step.meta.name,
1431
- status: failed ? "failed" : "passed",
1432
- durationMs,
1433
- assertions: lastAssertions,
1434
- failedAssertions: lastFailedAssertions,
1435
- attempts: attemptsUsed,
1436
- retriesUsed: Math.max(0, attemptsUsed - 1),
1437
- ...(stepError && { error: stepError }),
1438
- ...(returnStatePayload !== undefined && {
1439
- returnState: returnStatePayload,
1440
- }),
1441
- }));
1442
- currentStepIndex = null;
1443
- if (failed) {
1444
- stepFailed = true;
1445
- // Don't throw here — let the loop continue to emit skip events
1446
- }
1447
1348
  }
1448
1349
  }
1449
- }
1450
- finally {
1451
- if (test.teardown) {
1452
- try {
1453
- console.log(JSON.stringify({
1454
- type: "log",
1455
- message: "Running teardown...",
1456
- }));
1457
- await test.teardown(effectiveCtx, state);
1458
- }
1459
- catch (teardownError) {
1460
- console.log(JSON.stringify({
1461
- type: "log",
1462
- message: `Teardown error: ${teardownError instanceof Error ? teardownError.message : String(teardownError)}`,
1463
- }));
1350
+ finally {
1351
+ if (test.teardown) {
1352
+ try {
1353
+ emitEvent({
1354
+ type: "log",
1355
+ message: "Running teardown...",
1356
+ });
1357
+ await test.teardown(effectiveCtx, state);
1358
+ }
1359
+ catch (teardownError) {
1360
+ emitEvent({
1361
+ type: "log",
1362
+ message: `Teardown error: ${teardownError instanceof Error ? teardownError.message : String(teardownError)}`,
1363
+ });
1364
+ }
1464
1365
  }
1465
1366
  }
1367
+ // If any step failed (assertion or throw), mark overall test as failed
1368
+ if (stepFailed) {
1369
+ throw new Error("One or more steps failed");
1370
+ }
1466
1371
  }
1467
- // If any step failed (assertion or throw), mark overall test as failed
1468
- if (stepFailed) {
1469
- throw new Error("One or more steps failed");
1470
- }
1372
+ };
1373
+ // Resolve test.extend() fixtures (if any) and run the test body
1374
+ if (test.fixtures && Object.keys(test.fixtures).length > 0) {
1375
+ await withFixtures(test.fixtures, ctx, runTestBody);
1471
1376
  }
1472
- };
1473
- // Resolve test.extend() fixtures (if any) and run the test body
1474
- if (test.fixtures && Object.keys(test.fixtures).length > 0) {
1475
- await withFixtures(test.fixtures, ctx, runTestBody);
1377
+ else {
1378
+ await runTestBody(ctx);
1379
+ }
1380
+ // Stop monitoring and get peak memory
1381
+ const peakBytes = stopMemoryMonitoring();
1382
+ emitEvent({
1383
+ type: "status",
1384
+ status: "completed",
1385
+ id: test.meta.id,
1386
+ peakMemoryBytes: peakBytes,
1387
+ peakMemoryMB: (peakBytes / 1024 / 1024).toFixed(2),
1388
+ });
1476
1389
  }
1477
- else {
1478
- await runTestBody(ctx);
1390
+ catch (error) {
1391
+ stopMemoryMonitoring();
1392
+ throw error;
1479
1393
  }
1480
- // Stop monitoring and get peak memory
1481
- const peakBytes = stopMemoryMonitoring();
1482
- console.log(JSON.stringify({
1483
- type: "status",
1484
- status: "completed",
1485
- id: test.meta.id,
1486
- peakMemoryBytes: peakBytes,
1487
- peakMemoryMB: (peakBytes / 1024 / 1024).toFixed(2),
1488
- }));
1489
- }
1490
- catch (error) {
1491
- stopMemoryMonitoring();
1492
- throw error;
1493
- }
1394
+ }); // end testContext.run()
1494
1395
  }
1495
1396
  //# sourceMappingURL=harness.js.map