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.
- package/README.md +167 -0
- package/dist/ci/api-client.d.ts +23 -0
- package/dist/ci/api-client.d.ts.map +1 -0
- package/dist/ci/api-client.js +64 -0
- package/dist/ci/api-client.js.map +1 -0
- package/dist/ci/executor.d.ts +13 -0
- package/dist/ci/executor.d.ts.map +1 -0
- package/dist/ci/executor.js +539 -0
- package/dist/ci/executor.js.map +1 -0
- package/dist/ci/git-info.d.ts +13 -0
- package/dist/ci/git-info.d.ts.map +1 -0
- package/dist/ci/git-info.js +52 -0
- package/dist/ci/git-info.js.map +1 -0
- package/dist/ci/index.d.ts +6 -0
- package/dist/ci/index.d.ts.map +1 -0
- package/dist/ci/index.js +4 -0
- package/dist/ci/index.js.map +1 -0
- package/dist/ci/runner.d.ts +3 -0
- package/dist/ci/runner.d.ts.map +1 -0
- package/dist/ci/runner.js +178 -0
- package/dist/ci/runner.js.map +1 -0
- package/dist/ci/types.d.ts +108 -0
- package/dist/ci/types.d.ts.map +1 -0
- package/dist/ci/types.js +3 -0
- package/dist/ci/types.js.map +1 -0
- package/dist/cli.js +40 -0
- package/dist/cli.js.map +1 -1
- package/dist/dashboard-server.d.ts.map +1 -1
- package/dist/dashboard-server.js +37 -3
- package/dist/dashboard-server.js.map +1 -1
- package/dist/index.cjs +951 -123
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/portal-server.d.ts.map +1 -1
- package/dist/portal-server.js +15 -1
- package/dist/portal-server.js.map +1 -1
- package/dist/telemetry-batcher.d.ts.map +1 -1
- package/dist/telemetry-batcher.js +5 -1
- package/dist/telemetry-batcher.js.map +1 -1
- package/package.json +1 -1
- package/src/ci/api-client.ts +87 -0
- package/src/ci/executor.ts +668 -0
- package/src/ci/git-info.ts +66 -0
- package/src/ci/index.ts +5 -0
- package/src/ci/runner.ts +198 -0
- package/src/ci/types.ts +115 -0
- package/src/cli.ts +55 -0
- package/src/dashboard-server.ts +37 -3
- package/src/index.ts +7 -0
- package/src/portal-server.ts +15 -1
- 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(
|
|
1200
|
-
if (!
|
|
1201
|
-
if (
|
|
1250
|
+
function normalizeHeaders(headers2) {
|
|
1251
|
+
if (!headers2) return void 0;
|
|
1252
|
+
if (headers2 instanceof Headers) {
|
|
1202
1253
|
const obj = {};
|
|
1203
|
-
|
|
1254
|
+
headers2.forEach((v, k) => {
|
|
1204
1255
|
obj[k] = v;
|
|
1205
1256
|
});
|
|
1206
1257
|
return obj;
|
|
1207
1258
|
}
|
|
1208
|
-
if (Array.isArray(
|
|
1209
|
-
return
|
|
1259
|
+
if (Array.isArray(headers2)) return Object.fromEntries(headers2);
|
|
1260
|
+
return headers2;
|
|
1210
1261
|
}
|
|
1211
|
-
function pickReplayResponseHeaders(
|
|
1212
|
-
if (!
|
|
1262
|
+
function pickReplayResponseHeaders(headers2) {
|
|
1263
|
+
if (!headers2) return { "Content-Type": "application/json" };
|
|
1213
1264
|
const out = {};
|
|
1214
|
-
for (const [key, value] of Object.entries(
|
|
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(
|
|
1221
|
-
const ct =
|
|
1222
|
-
return ct.includes("text/event-stream") || ct.includes("application/x-ndjson") || ct.includes("application/stream+json") || ct.includes("application/jsonl") ||
|
|
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
|
|
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
|
|
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
|
-
...
|
|
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
|
|
2424
|
-
|
|
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
|
|
3068
|
-
if (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,
|
|
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,
|
|
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
|
|
3343
|
-
if (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,
|
|
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,
|
|
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
|
|
3498
|
+
var import_node_crypto4, heartbeatTimer, shutdownRegistered;
|
|
3435
3499
|
var init_observability = __esm({
|
|
3436
3500
|
"src/observability.ts"() {
|
|
3437
3501
|
"use strict";
|
|
3438
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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(
|
|
4222
|
+
function cloneHeaders(headers2) {
|
|
4110
4223
|
const result = {};
|
|
4111
|
-
for (const [key, value] of Object.entries(
|
|
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
|
|
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/
|
|
5247
|
-
|
|
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
|
|
5455
|
-
if (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(
|
|
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,
|