elasticdash-test 0.1.18 → 0.1.19-alpha-2

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.
Files changed (53) hide show
  1. package/README.md +167 -0
  2. package/dist/ci/api-client.d.ts +23 -0
  3. package/dist/ci/api-client.d.ts.map +1 -0
  4. package/dist/ci/api-client.js +64 -0
  5. package/dist/ci/api-client.js.map +1 -0
  6. package/dist/ci/executor.d.ts +13 -0
  7. package/dist/ci/executor.d.ts.map +1 -0
  8. package/dist/ci/executor.js +539 -0
  9. package/dist/ci/executor.js.map +1 -0
  10. package/dist/ci/git-info.d.ts +13 -0
  11. package/dist/ci/git-info.d.ts.map +1 -0
  12. package/dist/ci/git-info.js +52 -0
  13. package/dist/ci/git-info.js.map +1 -0
  14. package/dist/ci/index.d.ts +6 -0
  15. package/dist/ci/index.d.ts.map +1 -0
  16. package/dist/ci/index.js +4 -0
  17. package/dist/ci/index.js.map +1 -0
  18. package/dist/ci/runner.d.ts +3 -0
  19. package/dist/ci/runner.d.ts.map +1 -0
  20. package/dist/ci/runner.js +178 -0
  21. package/dist/ci/runner.js.map +1 -0
  22. package/dist/ci/types.d.ts +108 -0
  23. package/dist/ci/types.d.ts.map +1 -0
  24. package/dist/ci/types.js +3 -0
  25. package/dist/ci/types.js.map +1 -0
  26. package/dist/cli.js +40 -0
  27. package/dist/cli.js.map +1 -1
  28. package/dist/dashboard-server.d.ts.map +1 -1
  29. package/dist/dashboard-server.js +37 -3
  30. package/dist/dashboard-server.js.map +1 -1
  31. package/dist/index.cjs +951 -123
  32. package/dist/index.d.ts +5 -0
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +4 -0
  35. package/dist/index.js.map +1 -1
  36. package/dist/portal-server.d.ts.map +1 -1
  37. package/dist/portal-server.js +15 -1
  38. package/dist/portal-server.js.map +1 -1
  39. package/dist/telemetry-batcher.d.ts.map +1 -1
  40. package/dist/telemetry-batcher.js +5 -1
  41. package/dist/telemetry-batcher.js.map +1 -1
  42. package/package.json +1 -1
  43. package/src/ci/api-client.ts +87 -0
  44. package/src/ci/executor.ts +668 -0
  45. package/src/ci/git-info.ts +66 -0
  46. package/src/ci/index.ts +5 -0
  47. package/src/ci/runner.ts +198 -0
  48. package/src/ci/types.ts +115 -0
  49. package/src/cli.ts +55 -0
  50. package/src/dashboard-server.ts +37 -3
  51. package/src/index.ts +7 -0
  52. package/src/portal-server.ts +15 -1
  53. package/src/telemetry-batcher.ts +5 -1
package/dist/index.cjs CHANGED
@@ -853,6 +853,57 @@ var init_matchers = __esm({
853
853
  }
854
854
  });
855
855
 
856
+ // src/capture/replay.ts
857
+ var ReplayController;
858
+ var init_replay = __esm({
859
+ "src/capture/replay.ts"() {
860
+ "use strict";
861
+ ReplayController = class {
862
+ constructor(replayMode, checkpoint, history) {
863
+ this.replayMode = replayMode;
864
+ this.checkpoint = checkpoint;
865
+ this.history = history;
866
+ this.historyMap = new Map(history.map((e) => [e.id, e]));
867
+ this.sideEffectMap = new Map(
868
+ history.filter((e) => e.type === "side_effect").map((e) => [e.id, e])
869
+ );
870
+ }
871
+ historyMap;
872
+ /** Side effects keyed by their assigned sideEffectId, independent of main event IDs */
873
+ sideEffectMap;
874
+ shouldReplay(eventId) {
875
+ return this.replayMode && eventId <= this.checkpoint;
876
+ }
877
+ getRecordedEvent(eventId) {
878
+ return this.historyMap.get(eventId);
879
+ }
880
+ getRecordedResult(eventId) {
881
+ return this.historyMap.get(eventId)?.output;
882
+ }
883
+ /** Returns true if the side effect with this sideEffectId has a recorded value to replay */
884
+ shouldReplaySideEffect(n) {
885
+ return this.replayMode && this.sideEffectMap.has(n);
886
+ }
887
+ getSideEffectResult(n) {
888
+ return this.sideEffectMap.get(n)?.output;
889
+ }
890
+ getRecordedSideEffectEvent(n) {
891
+ return this.sideEffectMap.get(n);
892
+ }
893
+ shouldReplaySideEffectOfType(n, expectedName) {
894
+ if (!this.replayMode) return false;
895
+ const event = this.sideEffectMap.get(n);
896
+ return !!event && event.type === "side_effect" && event.name === expectedName;
897
+ }
898
+ getSideEffectResultOfType(n, expectedName) {
899
+ const event = this.sideEffectMap.get(n);
900
+ if (!event || event.type !== "side_effect" || event.name !== expectedName) return void 0;
901
+ return event.output;
902
+ }
903
+ };
904
+ }
905
+ });
906
+
856
907
  // src/internals/mock-resolver.ts
857
908
  function normaliseMockResult(value) {
858
909
  if (typeof value === "string") {
@@ -1196,30 +1247,30 @@ function parseBody(body) {
1196
1247
  }
1197
1248
  return "[binary]";
1198
1249
  }
1199
- function normalizeHeaders(headers) {
1200
- if (!headers) return void 0;
1201
- if (headers instanceof Headers) {
1250
+ function normalizeHeaders(headers2) {
1251
+ if (!headers2) return void 0;
1252
+ if (headers2 instanceof Headers) {
1202
1253
  const obj = {};
1203
- headers.forEach((v, k) => {
1254
+ headers2.forEach((v, k) => {
1204
1255
  obj[k] = v;
1205
1256
  });
1206
1257
  return obj;
1207
1258
  }
1208
- if (Array.isArray(headers)) return Object.fromEntries(headers);
1209
- return headers;
1259
+ if (Array.isArray(headers2)) return Object.fromEntries(headers2);
1260
+ return headers2;
1210
1261
  }
1211
- function pickReplayResponseHeaders(headers) {
1212
- if (!headers) return { "Content-Type": "application/json" };
1262
+ function pickReplayResponseHeaders(headers2) {
1263
+ if (!headers2) return { "Content-Type": "application/json" };
1213
1264
  const out = {};
1214
- for (const [key, value] of Object.entries(headers)) {
1265
+ for (const [key, value] of Object.entries(headers2)) {
1215
1266
  if (typeof value === "string") out[key] = value;
1216
1267
  }
1217
1268
  if (!out["Content-Type"]) out["Content-Type"] = "application/json";
1218
1269
  return out;
1219
1270
  }
1220
- function isStreamingContentType(headers) {
1221
- const ct = headers.get("content-type") ?? "";
1222
- return ct.includes("text/event-stream") || ct.includes("application/x-ndjson") || ct.includes("application/stream+json") || ct.includes("application/jsonl") || headers.get("x-vercel-ai-data-stream") === "v1";
1271
+ function isStreamingContentType(headers2) {
1272
+ const ct = headers2.get("content-type") ?? "";
1273
+ return ct.includes("text/event-stream") || ct.includes("application/x-ndjson") || ct.includes("application/stream+json") || ct.includes("application/jsonl") || headers2.get("x-vercel-ai-data-stream") === "v1";
1223
1274
  }
1224
1275
  function parseVercelAIDataStream(raw) {
1225
1276
  let accumulatedText = "";
@@ -1308,20 +1359,20 @@ function synthesizeFrozenResponse(frozen) {
1308
1359
  const responseMeta = frozenInput?.__elasticdashResponse ?? {};
1309
1360
  const status = typeof responseMeta.status === "number" ? responseMeta.status : 200;
1310
1361
  const statusText = typeof responseMeta.statusText === "string" ? responseMeta.statusText : "";
1311
- const headers = pickReplayResponseHeaders(
1362
+ const headers2 = pickReplayResponseHeaders(
1312
1363
  responseMeta.headers && typeof responseMeta.headers === "object" ? responseMeta.headers : void 0
1313
1364
  );
1314
1365
  if (frozen.streamed === true) {
1315
1366
  const raw = typeof frozen.streamRaw === "string" ? frozen.streamRaw : "";
1316
- return new Response(reconstructStream(raw), { status, statusText, headers });
1367
+ return new Response(reconstructStream(raw), { status, statusText, headers: headers2 });
1317
1368
  }
1318
1369
  const body = frozen.output != null ? JSON.stringify(frozen.output) : null;
1319
- return new Response(body, { status, statusText, headers });
1370
+ return new Response(body, { status, statusText, headers: headers2 });
1320
1371
  }
1321
1372
  async function executeLiveAndRecord(originalFetchFn, input, init, id, url, method, rawHeaders, rawBody) {
1322
1373
  const query = parseQuery(url);
1323
1374
  const body = parseBody(rawBody);
1324
- const headers = normalizeHeaders(rawHeaders);
1375
+ const headers2 = normalizeHeaders(rawHeaders);
1325
1376
  const start = rawDateNow();
1326
1377
  const res = await originalFetchFn(input, init);
1327
1378
  const responseHeadersObj = {};
@@ -1334,7 +1385,7 @@ async function executeLiveAndRecord(originalFetchFn, input, init, id, url, metho
1334
1385
  method,
1335
1386
  ...query ? { query } : {},
1336
1387
  ...body !== void 0 ? { body } : {},
1337
- ...headers && Object.keys(headers).length > 0 ? { headers } : {},
1388
+ ...headers2 && Object.keys(headers2).length > 0 ? { headers: headers2 } : {},
1338
1389
  __elasticdashResponse: elasticdashResponse
1339
1390
  };
1340
1391
  if (isStreamingContentType(res.headers) && res.body) {
@@ -2365,11 +2416,12 @@ function* jsonStreamEvents(sessionId, events) {
2365
2416
  }
2366
2417
  yield "]}";
2367
2418
  }
2368
- var import_node_stream2, TelemetryBatcher;
2419
+ var import_node_stream2, import_node_crypto3, TelemetryBatcher;
2369
2420
  var init_telemetry_batcher = __esm({
2370
2421
  "src/telemetry-batcher.ts"() {
2371
2422
  "use strict";
2372
2423
  import_node_stream2 = require("node:stream");
2424
+ import_node_crypto3 = require("node:crypto");
2373
2425
  init_http();
2374
2426
  init_debug();
2375
2427
  init_redact();
@@ -2420,13 +2472,16 @@ var init_telemetry_batcher = __esm({
2420
2472
  }
2421
2473
  const maxRetries = 3;
2422
2474
  const url = `${this.serverUrl}/api/observability/events`;
2423
- const headers = { "Content-Type": "application/json" };
2424
- if (this.apiKey) headers["Authorization"] = `Bearer ${this.apiKey}`;
2475
+ const headers2 = {
2476
+ "Content-Type": "application/json",
2477
+ "X-Correlation-ID": (0, import_node_crypto3.randomUUID)()
2478
+ };
2479
+ if (this.apiKey) headers2["Authorization"] = `Bearer ${this.apiKey}`;
2425
2480
  try {
2426
2481
  const body = import_node_stream2.Readable.from(jsonStreamEvents(this.sessionId, batch));
2427
2482
  const res = await getOriginalFetch()(url, {
2428
2483
  method: "POST",
2429
- headers,
2484
+ headers: headers2,
2430
2485
  body,
2431
2486
  duplex: "half"
2432
2487
  });
@@ -2477,6 +2532,15 @@ var init_telemetry_batcher = __esm({
2477
2532
  });
2478
2533
 
2479
2534
  // src/execution/tool-runner.ts
2535
+ var tool_runner_exports = {};
2536
+ __export(tool_runner_exports, {
2537
+ buildToolArgs: () => buildToolArgs,
2538
+ isDenoProject: () => isDenoProject,
2539
+ resolveRuntimeModule: () => resolveRuntimeModule,
2540
+ runToolInSubprocess: () => runToolInSubprocess,
2541
+ scanTools: () => scanTools,
2542
+ scanWorkflows: () => scanWorkflows
2543
+ });
2480
2544
  function resolveWorkerScript(relativePath) {
2481
2545
  try {
2482
2546
  return new URL(relativePath, import_meta.url).pathname;
@@ -3064,8 +3128,8 @@ async function executeTrigger(serverUrl, apiKey, trigger) {
3064
3128
  return;
3065
3129
  }
3066
3130
  const baseUrl = serverUrl.replace(/\/$/, "");
3067
- const headers = { "Content-Type": "application/json" };
3068
- if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
3131
+ const headers2 = { "Content-Type": "application/json" };
3132
+ if (apiKey) headers2["Authorization"] = `Bearer ${apiKey}`;
3069
3133
  for (let stepIndex = 0; stepIndex < totalSteps; stepIndex++) {
3070
3134
  const step = trigger.steps[stepIndex];
3071
3135
  const availability = step.eventType === "ai" ? checkAIAvailability(step.provider, step.model ?? step.eventName) : checkToolAvailability(step.eventName, cwd, tools);
@@ -3124,7 +3188,7 @@ ${result.error}`);
3124
3188
  try {
3125
3189
  const res = await getOriginalFetch()(url, {
3126
3190
  method: "POST",
3127
- headers,
3191
+ headers: headers2,
3128
3192
  body: JSON.stringify({
3129
3193
  triggerId: trigger.triggerId,
3130
3194
  eventType: stepResult.eventType,
@@ -3262,7 +3326,7 @@ function initObservability(options) {
3262
3326
  throw new Error("[elasticdash] initObservability: serverUrl is required (set ELASTICDASH_API_URL or pass serverUrl option)");
3263
3327
  }
3264
3328
  const apiKey = options?.apiKey ?? process.env.ELASTICDASH_API_KEY;
3265
- const sessionId = options?.sessionId ?? process.env.ELASTICDASH_SESSION_ID ?? (0, import_node_crypto3.randomUUID)();
3329
+ const sessionId = options?.sessionId ?? process.env.ELASTICDASH_SESSION_ID ?? (0, import_node_crypto4.randomUUID)();
3266
3330
  const sampleRate = options?.sampleRate ?? 1;
3267
3331
  const redactKeys = options?.redactKeys ?? [];
3268
3332
  const heartbeatIntervalMs = options?.heartbeatIntervalMs ?? 3e4;
@@ -3281,7 +3345,7 @@ function initObservability(options) {
3281
3345
  }
3282
3346
  });
3283
3347
  let counter = 0;
3284
- const traceId = `${defaultWorkflowName}::${Date.now()}::${(0, import_node_crypto3.randomUUID)().slice(0, 8)}`;
3348
+ const traceId = `${defaultWorkflowName}::${Date.now()}::${(0, import_node_crypto4.randomUUID)().slice(0, 8)}`;
3285
3349
  const ctx = {
3286
3350
  sessionId,
3287
3351
  serverUrl,
@@ -3339,12 +3403,12 @@ async function pushCatalog(serverUrl, apiKey, workflows = []) {
3339
3403
  const body = {};
3340
3404
  if (workflows.length > 0) body.workflows = workflows.map((w) => ({ name: w.name }));
3341
3405
  if (tools.length > 0) body.tools = tools.map((t) => ({ name: t.name }));
3342
- const headers = { "Content-Type": "application/json" };
3343
- if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
3406
+ const headers2 = { "Content-Type": "application/json" };
3407
+ if (apiKey) headers2["Authorization"] = `Bearer ${apiKey}`;
3344
3408
  try {
3345
3409
  const res = await getOriginalFetch()(`${serverUrl.replace(/\/$/, "")}/api/observability/catalog`, {
3346
3410
  method: "POST",
3347
- headers,
3411
+ headers: headers2,
3348
3412
  body: JSON.stringify(body)
3349
3413
  });
3350
3414
  debugLog(`[elasticdash] Catalog pushed: ${workflows.length} workflows, ${tools.length} tools (status ${res.status})`);
@@ -3379,7 +3443,7 @@ function startTrace(workflowName) {
3379
3443
  if (!ctx) throw new Error("[elasticdash] startTrace: observability not initialised");
3380
3444
  const resolvedName = workflowName || ctx.defaultWorkflowName;
3381
3445
  const timestamp = Date.now();
3382
- const shortId = (0, import_node_crypto3.randomUUID)().slice(0, 8);
3446
+ const shortId = (0, import_node_crypto4.randomUUID)().slice(0, 8);
3383
3447
  ctx.traceId = `${resolvedName}::${timestamp}::${shortId}`;
3384
3448
  pushTelemetryEvent({
3385
3449
  id: ctx.nextId(),
@@ -3397,7 +3461,7 @@ function endTrace() {
3397
3461
  const ctx = getObservabilityContext();
3398
3462
  if (!ctx) return;
3399
3463
  const timestamp = Date.now();
3400
- const shortId = (0, import_node_crypto3.randomUUID)().slice(0, 8);
3464
+ const shortId = (0, import_node_crypto4.randomUUID)().slice(0, 8);
3401
3465
  ctx.traceId = `${ctx.defaultWorkflowName}::${timestamp}::${shortId}`;
3402
3466
  }
3403
3467
  function wrapWorkflow(name, fn) {
@@ -3431,11 +3495,11 @@ function wrapWorkflow(name, fn) {
3431
3495
  return fn(...args);
3432
3496
  };
3433
3497
  }
3434
- var import_node_crypto3, heartbeatTimer, shutdownRegistered;
3498
+ var import_node_crypto4, heartbeatTimer, shutdownRegistered;
3435
3499
  var init_observability = __esm({
3436
3500
  "src/observability.ts"() {
3437
3501
  "use strict";
3438
- import_node_crypto3 = require("node:crypto");
3502
+ import_node_crypto4 = require("node:crypto");
3439
3503
  init_telemetry_batcher();
3440
3504
  init_telemetry_push();
3441
3505
  init_ai_interceptor();
@@ -3769,7 +3833,7 @@ async function tryAutoInitHttpContext() {
3769
3833
  if (runId) {
3770
3834
  await initHttpRunContext(runId, serverUrl);
3771
3835
  } else {
3772
- setHttpRunContext((0, import_node_crypto4.randomUUID)(), serverUrl);
3836
+ setHttpRunContext((0, import_node_crypto5.randomUUID)(), serverUrl);
3773
3837
  }
3774
3838
  } catch {
3775
3839
  }
@@ -3777,12 +3841,12 @@ async function tryAutoInitHttpContext() {
3777
3841
  }
3778
3842
  await g3[AUTO_INIT_KEY];
3779
3843
  }
3780
- var import_node_async_hooks3, import_node_crypto4, g3, HTTP_RUN_ALS_KEY, GLOBAL_CTX_KEY, OBS_ALS_KEY, GLOBAL_OBS_KEY, httpRunAls, obsAls, INTERCEPTORS_KEY, AUTO_INIT_KEY;
3844
+ var import_node_async_hooks3, import_node_crypto5, g3, HTTP_RUN_ALS_KEY, GLOBAL_CTX_KEY, OBS_ALS_KEY, GLOBAL_OBS_KEY, httpRunAls, obsAls, INTERCEPTORS_KEY, AUTO_INIT_KEY;
3781
3845
  var init_telemetry_push = __esm({
3782
3846
  "src/interceptors/telemetry-push.ts"() {
3783
3847
  "use strict";
3784
3848
  import_node_async_hooks3 = require("node:async_hooks");
3785
- import_node_crypto4 = require("node:crypto");
3849
+ import_node_crypto5 = require("node:crypto");
3786
3850
  init_mock_resolver();
3787
3851
  init_debug();
3788
3852
  g3 = globalThis;
@@ -3852,6 +3916,50 @@ var init_tracing = __esm({
3852
3916
  }
3853
3917
  });
3854
3918
 
3919
+ // src/workflow-runner.ts
3920
+ var workflow_runner_exports = {};
3921
+ __export(workflow_runner_exports, {
3922
+ runWorkflow: () => runWorkflow
3923
+ });
3924
+ async function runWorkflow(workflowFn, options = {}) {
3925
+ const {
3926
+ replayMode = false,
3927
+ checkpoint = 0,
3928
+ history = [],
3929
+ interceptHttp = true,
3930
+ interceptSideEffects = true
3931
+ } = options;
3932
+ const recorder = new TraceRecorder();
3933
+ const replay = new ReplayController(replayMode, checkpoint, history);
3934
+ setCaptureContext({ recorder, replay });
3935
+ if (interceptHttp) interceptFetch();
3936
+ if (interceptSideEffects) {
3937
+ interceptRandom();
3938
+ interceptDateNow();
3939
+ }
3940
+ try {
3941
+ const result = await workflowFn();
3942
+ await recorder.flush();
3943
+ return { result, trace: recorder.toTrace() };
3944
+ } finally {
3945
+ if (interceptHttp) restoreFetch();
3946
+ if (interceptSideEffects) {
3947
+ restoreRandom();
3948
+ restoreDateNow();
3949
+ }
3950
+ setCaptureContext(void 0);
3951
+ }
3952
+ }
3953
+ var init_workflow_runner = __esm({
3954
+ "src/workflow-runner.ts"() {
3955
+ "use strict";
3956
+ init_recorder();
3957
+ init_replay();
3958
+ init_http();
3959
+ init_side_effects();
3960
+ }
3961
+ });
3962
+
3855
3963
  // src/index.ts
3856
3964
  var index_exports = {};
3857
3965
  __export(index_exports, {
@@ -3870,14 +3978,17 @@ __export(index_exports, {
3870
3978
  clearGlobalHttpContext: () => clearGlobalHttpContext,
3871
3979
  clearRegistry: () => clearRegistry,
3872
3980
  connectToBackend: () => connectToBackend,
3981
+ createBatch: () => createBatch,
3873
3982
  createTraceHandle: () => createTraceHandle,
3874
3983
  deserializeAgentState: () => deserializeAgentState,
3984
+ detectGitInfo: () => detectGitInfo,
3875
3985
  disconnectFromBackend: () => disconnectFromBackend,
3876
3986
  endTrace: () => endTrace,
3877
3987
  executePortalTask: () => executePortalTask,
3878
3988
  expect: () => import_expect.expect,
3879
3989
  extractTaskOutputs: () => extractTaskOutputs,
3880
3990
  fetchCapturedTrace: () => fetchCapturedTrace,
3991
+ fetchTestGroups: () => fetchTestGroups,
3881
3992
  getCaptureContext: () => getCaptureContext,
3882
3993
  getCurrentTrace: () => getCurrentTrace,
3883
3994
  getHttpFrozenEvent: () => getHttpFrozenEvent,
@@ -3906,6 +4017,7 @@ __export(index_exports, {
3906
4017
  restoreDateNow: () => restoreDateNow,
3907
4018
  restoreFetch: () => restoreFetch,
3908
4019
  restoreRandom: () => restoreRandom,
4020
+ runCI: () => runCI,
3909
4021
  runFiles: () => runFiles,
3910
4022
  runInHttpContext: () => runInHttpContext,
3911
4023
  runWithInitializedHttpContext: () => runWithInitializedHttpContext,
@@ -3921,6 +4033,7 @@ __export(index_exports, {
3921
4033
  startPortalServer: () => startPortalServer,
3922
4034
  startTrace: () => startTrace,
3923
4035
  startTraceSession: () => startTraceSession,
4036
+ submitTestRun: () => submitTestRun,
3924
4037
  tryAutoInitHttpContext: () => tryAutoInitHttpContext,
3925
4038
  uninstallAIInterceptor: () => uninstallAIInterceptor,
3926
4039
  uninstallDBAutoInterceptor: () => uninstallDBAutoInterceptor,
@@ -4106,9 +4219,9 @@ function extractCompletion(provider, responseBody) {
4106
4219
  }
4107
4220
  return "";
4108
4221
  }
4109
- function cloneHeaders(headers) {
4222
+ function cloneHeaders(headers2) {
4110
4223
  const result = {};
4111
- for (const [key, value] of Object.entries(headers)) {
4224
+ for (const [key, value] of Object.entries(headers2)) {
4112
4225
  if (Array.isArray(value)) {
4113
4226
  result[key] = value.join(", ");
4114
4227
  } else if (typeof value === "string") {
@@ -4184,12 +4297,12 @@ async function startLLMProxy(options = {}) {
4184
4297
  const provider = detectProvider(parsed.pathname);
4185
4298
  const traceId = req.headers[HEADER_TRACE_ID]?.toString();
4186
4299
  const isStreaming = requestBody && typeof requestBody === "object" ? requestBody.stream === true : false;
4187
- const headers = cloneHeaders(req.headers);
4300
+ const headers2 = cloneHeaders(req.headers);
4188
4301
  const upstreamBase = provider ? normalizeUpstream(provider, upstreamOverride[provider]) : void 0;
4189
4302
  if (!provider || !upstreamBase) {
4190
4303
  const passthrough = await fetch(parsed.toString(), {
4191
4304
  method: req.method,
4192
- headers,
4305
+ headers: headers2,
4193
4306
  body: bodyBuf.length > 0 ? bodyBuf : void 0
4194
4307
  });
4195
4308
  sendUpstreamResponse(passthrough, res);
@@ -4198,7 +4311,7 @@ async function startLLMProxy(options = {}) {
4198
4311
  const targetUrl = `${upstreamBase}${parsed.pathname}${parsed.search}`;
4199
4312
  const upstreamRes = await fetch(targetUrl, {
4200
4313
  method: req.method,
4201
- headers,
4314
+ headers: headers2,
4202
4315
  body: bodyBuf.length > 0 ? bodyBuf : void 0
4203
4316
  });
4204
4317
  if (traceId) {
@@ -4430,51 +4543,7 @@ init_matchers();
4430
4543
  init_matchers();
4431
4544
  init_context();
4432
4545
  init_recorder();
4433
-
4434
- // src/capture/replay.ts
4435
- var ReplayController = class {
4436
- constructor(replayMode, checkpoint, history) {
4437
- this.replayMode = replayMode;
4438
- this.checkpoint = checkpoint;
4439
- this.history = history;
4440
- this.historyMap = new Map(history.map((e) => [e.id, e]));
4441
- this.sideEffectMap = new Map(
4442
- history.filter((e) => e.type === "side_effect").map((e) => [e.id, e])
4443
- );
4444
- }
4445
- historyMap;
4446
- /** Side effects keyed by their assigned sideEffectId, independent of main event IDs */
4447
- sideEffectMap;
4448
- shouldReplay(eventId) {
4449
- return this.replayMode && eventId <= this.checkpoint;
4450
- }
4451
- getRecordedEvent(eventId) {
4452
- return this.historyMap.get(eventId);
4453
- }
4454
- getRecordedResult(eventId) {
4455
- return this.historyMap.get(eventId)?.output;
4456
- }
4457
- /** Returns true if the side effect with this sideEffectId has a recorded value to replay */
4458
- shouldReplaySideEffect(n) {
4459
- return this.replayMode && this.sideEffectMap.has(n);
4460
- }
4461
- getSideEffectResult(n) {
4462
- return this.sideEffectMap.get(n)?.output;
4463
- }
4464
- getRecordedSideEffectEvent(n) {
4465
- return this.sideEffectMap.get(n);
4466
- }
4467
- shouldReplaySideEffectOfType(n, expectedName) {
4468
- if (!this.replayMode) return false;
4469
- const event = this.sideEffectMap.get(n);
4470
- return !!event && event.type === "side_effect" && event.name === expectedName;
4471
- }
4472
- getSideEffectResultOfType(n, expectedName) {
4473
- const event = this.sideEffectMap.get(n);
4474
- if (!event || event.type !== "side_effect" || event.name !== expectedName) return void 0;
4475
- return event.output;
4476
- }
4477
- };
4546
+ init_replay();
4478
4547
 
4479
4548
  // src/interceptors/tool.ts
4480
4549
  init_recorder();
@@ -5243,39 +5312,8 @@ var isWorker = () => {
5243
5312
  return isWorkerContext();
5244
5313
  };
5245
5314
 
5246
- // src/workflow-runner.ts
5247
- init_recorder();
5248
- init_http();
5249
- init_side_effects();
5250
- async function runWorkflow(workflowFn, options = {}) {
5251
- const {
5252
- replayMode = false,
5253
- checkpoint = 0,
5254
- history = [],
5255
- interceptHttp = true,
5256
- interceptSideEffects = true
5257
- } = options;
5258
- const recorder = new TraceRecorder();
5259
- const replay = new ReplayController(replayMode, checkpoint, history);
5260
- setCaptureContext({ recorder, replay });
5261
- if (interceptHttp) interceptFetch();
5262
- if (interceptSideEffects) {
5263
- interceptRandom();
5264
- interceptDateNow();
5265
- }
5266
- try {
5267
- const result = await workflowFn();
5268
- await recorder.flush();
5269
- return { result, trace: recorder.toTrace() };
5270
- } finally {
5271
- if (interceptHttp) restoreFetch();
5272
- if (interceptSideEffects) {
5273
- restoreRandom();
5274
- restoreDateNow();
5275
- }
5276
- setCaptureContext(void 0);
5277
- }
5278
- }
5315
+ // src/index.ts
5316
+ init_workflow_runner();
5279
5317
 
5280
5318
  // src/core/agent-state.ts
5281
5319
  function serializeAgentState(plan, trace) {
@@ -5451,13 +5489,13 @@ async function startPortalServer(options) {
5451
5489
  }
5452
5490
  async function deliverResult(result) {
5453
5491
  const url2 = `${backendUrl}/api/portal/results/${result.taskId}`;
5454
- const headers = { "Content-Type": "application/json" };
5455
- if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
5492
+ const headers2 = { "Content-Type": "application/json" };
5493
+ if (apiKey) headers2["Authorization"] = `Bearer ${apiKey}`;
5456
5494
  for (let attempt = 0; attempt < 3; attempt++) {
5457
5495
  try {
5458
5496
  const res = await fetch(url2, {
5459
5497
  method: "POST",
5460
- headers,
5498
+ headers: headers2,
5461
5499
  body: JSON.stringify(result)
5462
5500
  });
5463
5501
  if (res.ok || res.status < 500) {
@@ -5481,13 +5519,27 @@ async function startPortalServer(options) {
5481
5519
  }
5482
5520
  if (isAllowedOrigin(req, allowedHosts)) {
5483
5521
  if (apiKey) {
5522
+ console.warn(JSON.stringify({
5523
+ event: "auth.failure",
5524
+ reason: "invalid_api_key",
5525
+ ip: req.ip || req.headers["x-forwarded-for"] || "unknown",
5526
+ userAgent: req.headers["user-agent"] || "unknown",
5527
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
5528
+ }));
5484
5529
  res.status(401).json({ ok: false, error: "unauthorized \u2014 valid API key required" });
5485
5530
  return;
5486
5531
  }
5487
5532
  return next();
5488
5533
  }
5489
5534
  const origin = req.headers.origin ?? req.headers.referer ?? req.ip ?? "unknown";
5490
- console.warn(`[elasticdash portal] Blocked request from disallowed origin: ${origin}`);
5535
+ console.warn(JSON.stringify({
5536
+ event: "auth.failure",
5537
+ reason: "origin_not_allowed",
5538
+ origin,
5539
+ ip: req.ip || req.headers["x-forwarded-for"] || "unknown",
5540
+ userAgent: req.headers["user-agent"] || "unknown",
5541
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
5542
+ }));
5491
5543
  res.status(403).json({ ok: false, error: "forbidden \u2014 origin not in allowlist" });
5492
5544
  }
5493
5545
  const app = (0, import_express.default)();
@@ -5573,6 +5625,777 @@ async function startPortalServer(options) {
5573
5625
 
5574
5626
  // src/index.ts
5575
5627
  init_portal_executor();
5628
+
5629
+ // src/ci/runner.ts
5630
+ var import_chalk2 = __toESM(require("chalk"), 1);
5631
+
5632
+ // src/ci/api-client.ts
5633
+ var import_node_crypto6 = require("node:crypto");
5634
+ init_http();
5635
+ function headers(apiKey) {
5636
+ return {
5637
+ "Content-Type": "application/json",
5638
+ "Authorization": `Bearer ${apiKey}`,
5639
+ "X-Correlation-ID": (0, import_node_crypto6.randomUUID)()
5640
+ };
5641
+ }
5642
+ async function apiRequest(url, apiKey, options = {}) {
5643
+ const res = await getOriginalFetch()(url, {
5644
+ ...options,
5645
+ headers: { ...headers(apiKey), ...options.headers ?? {} }
5646
+ });
5647
+ if (!res.ok) {
5648
+ const text = await res.text().catch(() => "");
5649
+ throw new Error(`API ${res.status}: ${text || res.statusText}`);
5650
+ }
5651
+ const json = await res.json();
5652
+ return json.result ?? json.data ?? json;
5653
+ }
5654
+ async function fetchTestGroups(serverUrl, apiKey, filters) {
5655
+ const base = serverUrl.replace(/\/$/, "");
5656
+ const params = new URLSearchParams();
5657
+ if (filters?.workflowName) params.set("workflowName", filters.workflowName);
5658
+ if (filters?.tags?.length) params.set("tags", filters.tags.join(","));
5659
+ if (filters?.status) params.set("status", filters.status);
5660
+ const qs = params.toString();
5661
+ const url = `${base}/api/testgroups/by-project${qs ? `?${qs}` : ""}`;
5662
+ return apiRequest(url, apiKey);
5663
+ }
5664
+ async function submitTestRun(serverUrl, apiKey, testGroupId, payload) {
5665
+ const base = serverUrl.replace(/\/$/, "");
5666
+ const url = `${base}/api/testgroups/${testGroupId}/runs`;
5667
+ return apiRequest(url, apiKey, {
5668
+ method: "POST",
5669
+ body: JSON.stringify(payload)
5670
+ });
5671
+ }
5672
+ async function createBatch(serverUrl, apiKey, payload) {
5673
+ const base = serverUrl.replace(/\/$/, "");
5674
+ const url = `${base}/api/testgroups/batches`;
5675
+ return apiRequest(url, apiKey, {
5676
+ method: "POST",
5677
+ body: JSON.stringify(payload)
5678
+ });
5679
+ }
5680
+
5681
+ // src/ci/executor.ts
5682
+ init_portal_executor();
5683
+ init_tool_runner();
5684
+ init_matchers();
5685
+ async function executeTest(test, cwd) {
5686
+ const tools = scanTools(cwd);
5687
+ const runCount = test.run_count || 1;
5688
+ const timeoutMs = test.timeout_ms || 3e4;
5689
+ const startTime = Date.now();
5690
+ const singleRuns = [];
5691
+ for (let i = 0; i < runCount; i++) {
5692
+ const runStart = Date.now();
5693
+ let result;
5694
+ try {
5695
+ const runPromise = executeSingleRun(test, cwd, tools, i);
5696
+ const timeoutPromise = new Promise(
5697
+ (_, reject) => setTimeout(() => reject(new Error(`Test timed out after ${timeoutMs}ms`)), timeoutMs)
5698
+ );
5699
+ result = await Promise.race([runPromise, timeoutPromise]);
5700
+ } catch (err) {
5701
+ result = {
5702
+ runIndex: i + 1,
5703
+ passed: false,
5704
+ durationMs: Date.now() - runStart,
5705
+ inputTokens: 0,
5706
+ outputTokens: 0,
5707
+ totalTokens: 0,
5708
+ output: null,
5709
+ trace: null,
5710
+ error: err instanceof Error ? err.message : String(err)
5711
+ };
5712
+ }
5713
+ singleRuns.push(result);
5714
+ }
5715
+ const expectationResults = await evaluateExpectations(test.expectations, singleRuns);
5716
+ const passed = determinePassFail(test.pass_threshold, singleRuns, expectationResults);
5717
+ return {
5718
+ passed,
5719
+ singleRuns,
5720
+ expectationResults,
5721
+ durationMs: Date.now() - startTime
5722
+ };
5723
+ }
5724
+ async function executeSingleRun(test, cwd, tools, runIndex) {
5725
+ const start = Date.now();
5726
+ if (test.test_type === "single-step") {
5727
+ return executeSingleStep(test, cwd, tools, runIndex, start);
5728
+ }
5729
+ return executeFullFlow(test, cwd, tools, runIndex, start);
5730
+ }
5731
+ async function executeSingleStep(test, cwd, tools, runIndex, start) {
5732
+ const stepType = test.target_step_type;
5733
+ const stepName = test.target_step_name;
5734
+ if (!stepType || !stepName) {
5735
+ return {
5736
+ runIndex: runIndex + 1,
5737
+ passed: false,
5738
+ durationMs: Date.now() - start,
5739
+ inputTokens: 0,
5740
+ outputTokens: 0,
5741
+ totalTokens: 0,
5742
+ output: null,
5743
+ trace: null,
5744
+ error: "Single-step test requires target_step_type and target_step_name."
5745
+ };
5746
+ }
5747
+ const availability = stepType === "ai" ? checkAIAvailability(void 0, stepName) : checkToolAvailability(stepName, cwd, tools);
5748
+ if (!availability.available) {
5749
+ return {
5750
+ runIndex: runIndex + 1,
5751
+ passed: false,
5752
+ durationMs: Date.now() - start,
5753
+ inputTokens: 0,
5754
+ outputTokens: 0,
5755
+ totalTokens: 0,
5756
+ output: null,
5757
+ trace: null,
5758
+ error: `Step unavailable: ${availability.reason}`
5759
+ };
5760
+ }
5761
+ const result = await executePortalTask(
5762
+ {
5763
+ taskId: `ci-${test.id}-run-${runIndex}`,
5764
+ type: stepType === "ai" ? "ai" : "tool",
5765
+ name: stepName,
5766
+ input: test.mock_input,
5767
+ frozenEvents: test.frozen_events
5768
+ },
5769
+ cwd,
5770
+ tools
5771
+ );
5772
+ return {
5773
+ runIndex: runIndex + 1,
5774
+ passed: result.ok,
5775
+ durationMs: result.durationMs,
5776
+ inputTokens: result.usage?.inputTokens ?? 0,
5777
+ outputTokens: result.usage?.outputTokens ?? 0,
5778
+ totalTokens: result.usage?.totalTokens ?? 0,
5779
+ output: result.output,
5780
+ trace: null,
5781
+ error: result.error
5782
+ };
5783
+ }
5784
+ async function executeFullFlow(test, cwd, tools, runIndex, start) {
5785
+ try {
5786
+ const { runWorkflow: runWorkflow2 } = await Promise.resolve().then(() => (init_workflow_runner(), workflow_runner_exports));
5787
+ const { resolveRuntimeModule: resolveRuntimeModule2 } = await Promise.resolve().then(() => (init_tool_runner(), tool_runner_exports));
5788
+ const { pathToFileURL: pathToFileURL3 } = await import("node:url");
5789
+ const workflowModulePath = resolveRuntimeModule2(cwd, "ed_workflows");
5790
+ if (!workflowModulePath) {
5791
+ return {
5792
+ runIndex: runIndex + 1,
5793
+ passed: false,
5794
+ durationMs: Date.now() - start,
5795
+ inputTokens: 0,
5796
+ outputTokens: 0,
5797
+ totalTokens: 0,
5798
+ output: null,
5799
+ trace: null,
5800
+ error: "Cannot find ed_workflows.ts/js in workspace root."
5801
+ };
5802
+ }
5803
+ const workflowModule = await import(pathToFileURL3(workflowModulePath).href);
5804
+ const workflowFns = Object.entries(workflowModule).filter(
5805
+ ([, val]) => typeof val === "function"
5806
+ );
5807
+ if (workflowFns.length === 0) {
5808
+ return {
5809
+ runIndex: runIndex + 1,
5810
+ passed: false,
5811
+ durationMs: Date.now() - start,
5812
+ inputTokens: 0,
5813
+ outputTokens: 0,
5814
+ totalTokens: 0,
5815
+ output: null,
5816
+ trace: null,
5817
+ error: "No workflow functions found in ed_workflows."
5818
+ };
5819
+ }
5820
+ const targetFn = test.target_step_name ? workflowFns.find(([name]) => name === test.target_step_name)?.[1] : workflowFns[0][1];
5821
+ const fn = targetFn ?? workflowFns[0][1];
5822
+ const frozenEvents = Array.isArray(test.frozen_events) ? test.frozen_events : [];
5823
+ const { result, trace } = await runWorkflow2(
5824
+ () => {
5825
+ const input = test.workflow_input;
5826
+ return fn(input);
5827
+ },
5828
+ {
5829
+ replayMode: frozenEvents.length > 0,
5830
+ history: frozenEvents,
5831
+ interceptHttp: true,
5832
+ interceptSideEffects: true
5833
+ }
5834
+ );
5835
+ let inputTokens = 0, outputTokens = 0, totalTokens = 0;
5836
+ if (trace?.events) {
5837
+ for (const evt of trace.events) {
5838
+ if (evt.type === "ai" && evt.usage) {
5839
+ inputTokens += evt.usage.inputTokens ?? evt.usage.input ?? 0;
5840
+ outputTokens += evt.usage.outputTokens ?? evt.usage.output ?? 0;
5841
+ totalTokens += evt.usage.totalTokens ?? evt.usage.total ?? 0;
5842
+ }
5843
+ }
5844
+ }
5845
+ return {
5846
+ runIndex: runIndex + 1,
5847
+ passed: true,
5848
+ durationMs: Date.now() - start,
5849
+ inputTokens,
5850
+ outputTokens,
5851
+ totalTokens,
5852
+ output: result,
5853
+ trace: trace ?? null
5854
+ };
5855
+ } catch (err) {
5856
+ return {
5857
+ runIndex: runIndex + 1,
5858
+ passed: false,
5859
+ durationMs: Date.now() - start,
5860
+ inputTokens: 0,
5861
+ outputTokens: 0,
5862
+ totalTokens: 0,
5863
+ output: null,
5864
+ trace: null,
5865
+ error: err instanceof Error ? err.message : String(err)
5866
+ };
5867
+ }
5868
+ }
5869
+ async function evaluateExpectations(expectations, singleRuns) {
5870
+ const results = [];
5871
+ for (const exp of expectations) {
5872
+ const result = await evaluateExpectation(exp, singleRuns);
5873
+ results.push(result);
5874
+ }
5875
+ return results;
5876
+ }
5877
+ async function evaluateExpectation(exp, singleRuns) {
5878
+ switch (exp.type) {
5879
+ case "token-budget":
5880
+ return evaluateTokenBudget(exp, singleRuns);
5881
+ case "latency-budget":
5882
+ return evaluateLatencyBudget(exp, singleRuns);
5883
+ case "output-contains":
5884
+ return evaluateOutputContains(exp, singleRuns);
5885
+ case "output-schema":
5886
+ return evaluateOutputSchema(exp, singleRuns);
5887
+ case "tool-called":
5888
+ return evaluateToolCalled(exp, singleRuns);
5889
+ case "determinism":
5890
+ return evaluateDeterminism(exp, singleRuns);
5891
+ case "llm-judge":
5892
+ return evaluateLLMJudge(exp, singleRuns);
5893
+ default:
5894
+ return {
5895
+ expectationId: exp.id,
5896
+ type: exp.type,
5897
+ passed: false,
5898
+ detail: `Unknown expectation type: ${exp.type}`
5899
+ };
5900
+ }
5901
+ }
5902
+ function evaluateTokenBudget(exp, runs) {
5903
+ const perRun = {};
5904
+ let allPassed = true;
5905
+ for (const run of runs) {
5906
+ let passed = true;
5907
+ const details = [];
5908
+ if (exp.max_tokens_per_run != null && run.totalTokens > exp.max_tokens_per_run) {
5909
+ passed = false;
5910
+ details.push(`total ${run.totalTokens} > max ${exp.max_tokens_per_run}`);
5911
+ }
5912
+ perRun[run.runIndex] = { passed, detail: details.join("; ") || void 0 };
5913
+ if (!passed) allPassed = false;
5914
+ }
5915
+ if (exp.max_total_tokens != null) {
5916
+ const total = runs.reduce((sum, r) => sum + r.totalTokens, 0);
5917
+ if (total > exp.max_total_tokens) {
5918
+ allPassed = false;
5919
+ }
5920
+ }
5921
+ return {
5922
+ expectationId: exp.id,
5923
+ type: "token-budget",
5924
+ passed: allPassed,
5925
+ detail: allPassed ? void 0 : "Token budget exceeded.",
5926
+ perRun
5927
+ };
5928
+ }
5929
+ function evaluateLatencyBudget(exp, runs) {
5930
+ const perRun = {};
5931
+ let allPassed = true;
5932
+ for (const run of runs) {
5933
+ let passed = true;
5934
+ if (exp.max_duration_ms != null && run.durationMs > exp.max_duration_ms) {
5935
+ passed = false;
5936
+ }
5937
+ perRun[run.runIndex] = { passed, detail: passed ? void 0 : `${run.durationMs}ms > ${exp.max_duration_ms}ms` };
5938
+ if (!passed) allPassed = false;
5939
+ }
5940
+ if (exp.max_total_duration_ms != null) {
5941
+ const total = runs.reduce((sum, r) => sum + r.durationMs, 0);
5942
+ if (total > exp.max_total_duration_ms) {
5943
+ allPassed = false;
5944
+ }
5945
+ }
5946
+ return {
5947
+ expectationId: exp.id,
5948
+ type: "latency-budget",
5949
+ passed: allPassed,
5950
+ detail: allPassed ? void 0 : "Latency budget exceeded.",
5951
+ perRun
5952
+ };
5953
+ }
5954
+ function evaluateOutputContains(exp, runs) {
5955
+ const perRun = {};
5956
+ let allPassed = true;
5957
+ for (const run of runs) {
5958
+ const outputStr = typeof run.output === "string" ? run.output : JSON.stringify(run.output ?? "");
5959
+ const haystack = exp.case_insensitive ? outputStr.toLowerCase() : outputStr;
5960
+ let passed = true;
5961
+ if (exp.contains_text) {
5962
+ const needle = exp.case_insensitive ? exp.contains_text.toLowerCase() : exp.contains_text;
5963
+ if (!haystack.includes(needle)) passed = false;
5964
+ }
5965
+ if (exp.not_contains_text) {
5966
+ const needle = exp.case_insensitive ? exp.not_contains_text.toLowerCase() : exp.not_contains_text;
5967
+ if (haystack.includes(needle)) passed = false;
5968
+ }
5969
+ perRun[run.runIndex] = { passed, detail: passed ? void 0 : "Output text check failed." };
5970
+ if (!passed) allPassed = false;
5971
+ }
5972
+ return {
5973
+ expectationId: exp.id,
5974
+ type: "output-contains",
5975
+ passed: allPassed,
5976
+ detail: allPassed ? void 0 : "Output contains check failed.",
5977
+ perRun
5978
+ };
5979
+ }
5980
+ function evaluateOutputSchema(exp, runs) {
5981
+ const perRun = {};
5982
+ let allPassed = true;
5983
+ const schema = exp.json_schema;
5984
+ for (const run of runs) {
5985
+ if (!schema) {
5986
+ perRun[run.runIndex] = { passed: true };
5987
+ continue;
5988
+ }
5989
+ let output = run.output;
5990
+ if (typeof output === "string") {
5991
+ try {
5992
+ output = JSON.parse(output);
5993
+ } catch {
5994
+ perRun[run.runIndex] = { passed: false, detail: "Output is not valid JSON." };
5995
+ allPassed = false;
5996
+ continue;
5997
+ }
5998
+ }
5999
+ const schemaType = schema.type;
6000
+ let passed = true;
6001
+ if (schemaType === "object" && (typeof output !== "object" || output === null || Array.isArray(output))) {
6002
+ passed = false;
6003
+ } else if (schemaType === "array" && !Array.isArray(output)) {
6004
+ passed = false;
6005
+ } else if (schemaType === "string" && typeof output !== "string") {
6006
+ passed = false;
6007
+ }
6008
+ if (passed && schemaType === "object" && Array.isArray(schema.required)) {
6009
+ for (const key of schema.required) {
6010
+ if (!(key in output)) {
6011
+ passed = false;
6012
+ break;
6013
+ }
6014
+ }
6015
+ }
6016
+ perRun[run.runIndex] = { passed, detail: passed ? void 0 : "Output does not match schema." };
6017
+ if (!passed) allPassed = false;
6018
+ }
6019
+ return {
6020
+ expectationId: exp.id,
6021
+ type: "output-schema",
6022
+ passed: allPassed,
6023
+ detail: allPassed ? void 0 : "Output schema check failed.",
6024
+ perRun
6025
+ };
6026
+ }
6027
+ function evaluateToolCalled(exp, runs) {
6028
+ const perRun = {};
6029
+ let allPassed = true;
6030
+ for (const run of runs) {
6031
+ const trace = run.trace;
6032
+ const toolNames = trace?.events?.filter((e) => e.type === "tool").map((e) => e.name) ?? [];
6033
+ let passed = true;
6034
+ if (exp.required_tools?.length) {
6035
+ for (const required of exp.required_tools) {
6036
+ if (!toolNames.includes(required)) {
6037
+ passed = false;
6038
+ break;
6039
+ }
6040
+ }
6041
+ }
6042
+ if (exp.forbidden_tools?.length) {
6043
+ for (const forbidden of exp.forbidden_tools) {
6044
+ if (toolNames.includes(forbidden)) {
6045
+ passed = false;
6046
+ break;
6047
+ }
6048
+ }
6049
+ }
6050
+ perRun[run.runIndex] = { passed, detail: passed ? void 0 : `Tools called: [${toolNames.join(", ")}]` };
6051
+ if (!passed) allPassed = false;
6052
+ }
6053
+ return {
6054
+ expectationId: exp.id,
6055
+ type: "tool-called",
6056
+ passed: allPassed,
6057
+ detail: allPassed ? void 0 : "Tool call check failed.",
6058
+ perRun
6059
+ };
6060
+ }
6061
+ function evaluateDeterminism(exp, runs) {
6062
+ if (runs.length < 2) {
6063
+ return {
6064
+ expectationId: exp.id,
6065
+ type: "determinism",
6066
+ passed: true,
6067
+ detail: "Only one run \u2014 determinism check skipped."
6068
+ };
6069
+ }
6070
+ const outputs = runs.map(
6071
+ (r) => typeof r.output === "string" ? r.output : JSON.stringify(r.output ?? "")
6072
+ );
6073
+ const reference = outputs[0];
6074
+ let allSame = true;
6075
+ const perRun = {};
6076
+ for (let i = 0; i < runs.length; i++) {
6077
+ const same = outputs[i] === reference;
6078
+ perRun[runs[i].runIndex] = { passed: same, detail: same ? void 0 : "Output differs from run 1." };
6079
+ if (!same) allSame = false;
6080
+ }
6081
+ if (!allSame && exp.similarity_threshold != null) {
6082
+ const threshold = exp.similarity_threshold;
6083
+ let allAboveThreshold = true;
6084
+ for (let i = 1; i < outputs.length; i++) {
6085
+ const similarity = computeStringSimilarity(reference, outputs[i]);
6086
+ if (similarity < threshold) {
6087
+ allAboveThreshold = false;
6088
+ perRun[runs[i].runIndex] = {
6089
+ passed: false,
6090
+ detail: `Similarity ${(similarity * 100).toFixed(1)}% < ${(threshold * 100).toFixed(1)}%`
6091
+ };
6092
+ } else {
6093
+ perRun[runs[i].runIndex] = { passed: true };
6094
+ }
6095
+ }
6096
+ return {
6097
+ expectationId: exp.id,
6098
+ type: "determinism",
6099
+ passed: allAboveThreshold,
6100
+ detail: allAboveThreshold ? void 0 : "Outputs are not sufficiently similar.",
6101
+ perRun
6102
+ };
6103
+ }
6104
+ return {
6105
+ expectationId: exp.id,
6106
+ type: "determinism",
6107
+ passed: allSame,
6108
+ detail: allSame ? void 0 : "Outputs are not identical across runs.",
6109
+ perRun
6110
+ };
6111
+ }
6112
+ function computeStringSimilarity(a, b) {
6113
+ if (a === b) return 1;
6114
+ if (!a.length || !b.length) return 0;
6115
+ const longer = a.length >= b.length ? a : b;
6116
+ const shorter = a.length < b.length ? a : b;
6117
+ let matches = 0;
6118
+ for (let i = 0; i < shorter.length; i++) {
6119
+ if (shorter[i] === longer[i]) matches++;
6120
+ }
6121
+ return matches / longer.length;
6122
+ }
6123
+ async function evaluateLLMJudge(exp, runs) {
6124
+ if (!exp.judge_prompt) {
6125
+ return {
6126
+ expectationId: exp.id,
6127
+ type: "llm-judge",
6128
+ passed: false,
6129
+ detail: "LLM judge expectation requires judge_prompt."
6130
+ };
6131
+ }
6132
+ const perRun = {};
6133
+ let allPassed = true;
6134
+ const threshold = exp.judge_score_threshold ?? 7;
6135
+ for (const run of runs) {
6136
+ const outputStr = typeof run.output === "string" ? run.output : JSON.stringify(run.output ?? "");
6137
+ const evalPrompt = `${exp.judge_prompt}
6138
+
6139
+ Output to evaluate:
6140
+ ${outputStr}
6141
+
6142
+ Score this output on a scale of 0-10. Respond with only the number.`;
6143
+ try {
6144
+ const provider = exp.judge_provider || "openai";
6145
+ const result = await callProviderLLM(
6146
+ evalPrompt,
6147
+ { provider, model: exp.judge_model ?? void 0 },
6148
+ "You are an expert test judge. Return only a number between 0 and 10.",
6149
+ 16,
6150
+ 0
6151
+ );
6152
+ const score = parseFloat(result.content.match(/-?\d+(?:\.\d+)?/)?.[0] ?? "");
6153
+ if (isNaN(score)) {
6154
+ perRun[run.runIndex] = { passed: false, detail: `Could not parse score from: "${result.content}"` };
6155
+ allPassed = false;
6156
+ } else {
6157
+ const passed = score >= threshold;
6158
+ perRun[run.runIndex] = { passed, detail: `Score: ${score}/${threshold}` };
6159
+ if (!passed) allPassed = false;
6160
+ }
6161
+ } catch (err) {
6162
+ perRun[run.runIndex] = {
6163
+ passed: false,
6164
+ detail: `LLM judge error: ${err instanceof Error ? err.message : String(err)}`
6165
+ };
6166
+ allPassed = false;
6167
+ }
6168
+ }
6169
+ return {
6170
+ expectationId: exp.id,
6171
+ type: "llm-judge",
6172
+ passed: allPassed,
6173
+ detail: allPassed ? void 0 : "LLM judge check failed.",
6174
+ perRun
6175
+ };
6176
+ }
6177
+ function determinePassFail(passThreshold, singleRuns, expectationResults) {
6178
+ const runsPassed = singleRuns.filter((r) => r.passed).length;
6179
+ const totalRuns = singleRuns.length;
6180
+ let runsPass;
6181
+ switch (passThreshold) {
6182
+ case "all":
6183
+ runsPass = runsPassed === totalRuns;
6184
+ break;
6185
+ case "majority":
6186
+ runsPass = runsPassed > totalRuns / 2;
6187
+ break;
6188
+ case "any":
6189
+ runsPass = runsPassed > 0;
6190
+ break;
6191
+ default:
6192
+ runsPass = runsPassed === totalRuns;
6193
+ }
6194
+ const expectationsPass = expectationResults.every((e) => e.passed);
6195
+ return runsPass && expectationsPass;
6196
+ }
6197
+
6198
+ // src/ci/git-info.ts
6199
+ function detectGitInfo() {
6200
+ const env = process.env;
6201
+ if (env.GITHUB_ACTIONS === "true") {
6202
+ const prNumber = env.GITHUB_EVENT_NAME === "pull_request" ? parseInt(env.GITHUB_REF?.match(/refs\/pull\/(\d+)/)?.[1] ?? "", 10) || void 0 : void 0;
6203
+ const repo = env.GITHUB_REPOSITORY;
6204
+ const prUrl = prNumber && repo ? `https://github.com/${repo}/pull/${prNumber}` : void 0;
6205
+ return {
6206
+ branch: env.GITHUB_HEAD_REF || env.GITHUB_REF_NAME,
6207
+ commit: env.GITHUB_SHA,
6208
+ commitMessage: env.GITHUB_COMMIT_MESSAGE,
6209
+ prNumber,
6210
+ prUrl
6211
+ };
6212
+ }
6213
+ if (env.GITLAB_CI === "true") {
6214
+ const prNumber = env.CI_MERGE_REQUEST_IID ? parseInt(env.CI_MERGE_REQUEST_IID, 10) || void 0 : void 0;
6215
+ return {
6216
+ branch: env.CI_MERGE_REQUEST_SOURCE_BRANCH_NAME || env.CI_COMMIT_BRANCH,
6217
+ commit: env.CI_COMMIT_SHA,
6218
+ commitMessage: env.CI_COMMIT_MESSAGE,
6219
+ prNumber,
6220
+ prUrl: env.CI_MERGE_REQUEST_PROJECT_URL && prNumber ? `${env.CI_MERGE_REQUEST_PROJECT_URL}/-/merge_requests/${prNumber}` : void 0
6221
+ };
6222
+ }
6223
+ return {
6224
+ branch: env.CIRCLE_BRANCH || env.TRAVIS_BRANCH || env.BITBUCKET_BRANCH || env.CI_BRANCH,
6225
+ commit: env.CIRCLE_SHA1 || env.TRAVIS_COMMIT || env.BITBUCKET_COMMIT || env.CI_COMMIT_SHA,
6226
+ commitMessage: env.CI_COMMIT_MESSAGE,
6227
+ prNumber: env.CIRCLE_PULL_REQUEST ? parseInt(env.CIRCLE_PULL_REQUEST.split("/").pop() ?? "", 10) || void 0 : env.TRAVIS_PULL_REQUEST && env.TRAVIS_PULL_REQUEST !== "false" ? parseInt(env.TRAVIS_PULL_REQUEST, 10) || void 0 : void 0
6228
+ };
6229
+ }
6230
+
6231
+ // src/ci/runner.ts
6232
+ async function runCI(config) {
6233
+ const { serverUrl, apiKey } = config;
6234
+ const cwd = process.cwd();
6235
+ const detected = detectGitInfo();
6236
+ const gitBranch = config.gitBranch || detected.branch;
6237
+ const gitCommit = config.gitCommit || detected.commit;
6238
+ const gitCommitMessage = config.gitCommitMessage || detected.commitMessage;
6239
+ const gitPrNumber = config.gitPrNumber || detected.prNumber;
6240
+ const gitPrUrl = config.gitPrUrl || detected.prUrl;
6241
+ const triggeredBy = config.triggeredBy || "ci";
6242
+ const overallStart = Date.now();
6243
+ console.log(import_chalk2.default.cyan("\n[elasticdash ci] Fetching test groups..."));
6244
+ const testGroups = await fetchTestGroups(serverUrl, apiKey, {
6245
+ workflowName: config.workflowName,
6246
+ tags: config.tags
6247
+ });
6248
+ if (testGroups.length === 0) {
6249
+ console.log(import_chalk2.default.yellow("[elasticdash ci] No active test groups found."));
6250
+ return {
6251
+ total: 0,
6252
+ passed: 0,
6253
+ failed: 0,
6254
+ skipped: 0,
6255
+ durationMs: Date.now() - overallStart,
6256
+ batchId: null,
6257
+ results: []
6258
+ };
6259
+ }
6260
+ const totalTests = testGroups.reduce((sum, g5) => sum + g5.tests.length, 0);
6261
+ console.log(import_chalk2.default.cyan(`[elasticdash ci] Found ${testGroups.length} test group(s), ${totalTests} test(s) total.
6262
+ `));
6263
+ const allResults = [];
6264
+ const runIds = [];
6265
+ for (const group of testGroups) {
6266
+ console.log(import_chalk2.default.white.bold(` ${group.name}`) + import_chalk2.default.gray(` (${group.tests.length} tests)`));
6267
+ for (const test of group.tests) {
6268
+ const testLabel = test.name || `${test.test_type}:${test.target_step_name || "unnamed"}`;
6269
+ process.stdout.write(import_chalk2.default.gray(` ${testLabel} ... `));
6270
+ const testStart = Date.now();
6271
+ let testResult;
6272
+ try {
6273
+ const execution = await executeTest(test, cwd);
6274
+ const payload = {
6275
+ testGroupTestId: test.id,
6276
+ triggeredBy,
6277
+ passed: execution.passed,
6278
+ summary: execution.passed ? "Passed" : "Failed",
6279
+ gitBranch,
6280
+ gitCommit,
6281
+ gitCommitMessage,
6282
+ gitPrNumber,
6283
+ gitPrUrl,
6284
+ singleRuns: execution.singleRuns,
6285
+ expectationResults: execution.expectationResults,
6286
+ startedAt: new Date(testStart).toISOString(),
6287
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
6288
+ };
6289
+ let runId = null;
6290
+ try {
6291
+ const submitted = await submitTestRun(serverUrl, apiKey, group.id, payload);
6292
+ runId = submitted.id;
6293
+ runIds.push(runId);
6294
+ } catch (submitErr) {
6295
+ console.error(import_chalk2.default.yellow(`
6296
+ [warn] Failed to submit result: ${submitErr instanceof Error ? submitErr.message : String(submitErr)}`));
6297
+ }
6298
+ testResult = {
6299
+ testGroupId: group.id,
6300
+ testGroupName: group.name,
6301
+ testId: test.id,
6302
+ testName: test.name,
6303
+ testType: test.test_type,
6304
+ passed: execution.passed,
6305
+ runId,
6306
+ singleRuns: execution.singleRuns,
6307
+ expectationResults: execution.expectationResults,
6308
+ durationMs: execution.durationMs
6309
+ };
6310
+ } catch (err) {
6311
+ testResult = {
6312
+ testGroupId: group.id,
6313
+ testGroupName: group.name,
6314
+ testId: test.id,
6315
+ testName: test.name,
6316
+ testType: test.test_type,
6317
+ passed: false,
6318
+ runId: null,
6319
+ singleRuns: [],
6320
+ expectationResults: [],
6321
+ error: err instanceof Error ? err.message : String(err),
6322
+ durationMs: Date.now() - testStart
6323
+ };
6324
+ }
6325
+ allResults.push(testResult);
6326
+ const durationStr = import_chalk2.default.gray(`(${testResult.durationMs}ms)`);
6327
+ if (testResult.passed) {
6328
+ console.log(import_chalk2.default.green("PASS") + ` ${durationStr}`);
6329
+ } else {
6330
+ console.log(import_chalk2.default.red("FAIL") + ` ${durationStr}`);
6331
+ if (testResult.error) {
6332
+ console.log(import_chalk2.default.red(` ${testResult.error}`));
6333
+ }
6334
+ for (const exp of testResult.expectationResults) {
6335
+ if (!exp.passed) {
6336
+ console.log(import_chalk2.default.red(` [${exp.type}] ${exp.detail || "failed"}`));
6337
+ }
6338
+ }
6339
+ }
6340
+ }
6341
+ console.log();
6342
+ }
6343
+ let batchId = null;
6344
+ if (runIds.length > 0) {
6345
+ try {
6346
+ const passedCount = allResults.filter((r) => r.passed).length;
6347
+ const allPassed = passedCount === allResults.length;
6348
+ const batch = await createBatch(serverUrl, apiKey, {
6349
+ testGroupRunIds: runIds,
6350
+ status: allPassed ? "success" : "failed",
6351
+ passed: allPassed,
6352
+ summary: `CI: ${passedCount}/${allResults.length} passed`,
6353
+ gitBranch,
6354
+ gitCommit,
6355
+ startedAt: new Date(overallStart).toISOString(),
6356
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
6357
+ });
6358
+ batchId = batch.id;
6359
+ } catch {
6360
+ }
6361
+ }
6362
+ const passed = allResults.filter((r) => r.passed).length;
6363
+ const failed = allResults.filter((r) => !r.passed).length;
6364
+ const durationMs = Date.now() - overallStart;
6365
+ console.log(import_chalk2.default.white.bold("\u2500".repeat(50)));
6366
+ console.log(import_chalk2.default.white.bold("Summary"));
6367
+ console.log(import_chalk2.default.white.bold("\u2500".repeat(50)));
6368
+ console.log(` Total: ${allResults.length}`);
6369
+ console.log(` ${import_chalk2.default.green(`Passed: ${passed}`)}`);
6370
+ if (failed > 0) {
6371
+ console.log(` ${import_chalk2.default.red(`Failed: ${failed}`)}`);
6372
+ }
6373
+ console.log(` Duration: ${(durationMs / 1e3).toFixed(1)}s`);
6374
+ if (batchId) {
6375
+ console.log(` Batch ID: ${batchId}`);
6376
+ }
6377
+ console.log(import_chalk2.default.white.bold("\u2500".repeat(50)));
6378
+ if (failed > 0) {
6379
+ console.log(import_chalk2.default.red(`
6380
+ [elasticdash ci] ${failed} test(s) failed.
6381
+ `));
6382
+ } else {
6383
+ console.log(import_chalk2.default.green(`
6384
+ [elasticdash ci] All tests passed.
6385
+ `));
6386
+ }
6387
+ return {
6388
+ total: allResults.length,
6389
+ passed,
6390
+ failed,
6391
+ skipped: 0,
6392
+ durationMs,
6393
+ batchId,
6394
+ results: allResults
6395
+ };
6396
+ }
6397
+
6398
+ // src/index.ts
5576
6399
  init_telemetry_push();
5577
6400
  tryAutoInitHttpContext().catch(() => {
5578
6401
  });
@@ -5593,14 +6416,17 @@ tryAutoInitHttpContext().catch(() => {
5593
6416
  clearGlobalHttpContext,
5594
6417
  clearRegistry,
5595
6418
  connectToBackend,
6419
+ createBatch,
5596
6420
  createTraceHandle,
5597
6421
  deserializeAgentState,
6422
+ detectGitInfo,
5598
6423
  disconnectFromBackend,
5599
6424
  endTrace,
5600
6425
  executePortalTask,
5601
6426
  expect,
5602
6427
  extractTaskOutputs,
5603
6428
  fetchCapturedTrace,
6429
+ fetchTestGroups,
5604
6430
  getCaptureContext,
5605
6431
  getCurrentTrace,
5606
6432
  getHttpFrozenEvent,
@@ -5629,6 +6455,7 @@ tryAutoInitHttpContext().catch(() => {
5629
6455
  restoreDateNow,
5630
6456
  restoreFetch,
5631
6457
  restoreRandom,
6458
+ runCI,
5632
6459
  runFiles,
5633
6460
  runInHttpContext,
5634
6461
  runWithInitializedHttpContext,
@@ -5644,6 +6471,7 @@ tryAutoInitHttpContext().catch(() => {
5644
6471
  startPortalServer,
5645
6472
  startTrace,
5646
6473
  startTraceSession,
6474
+ submitTestRun,
5647
6475
  tryAutoInitHttpContext,
5648
6476
  uninstallAIInterceptor,
5649
6477
  uninstallDBAutoInterceptor,