elasticdash-sdk 0.2.0
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/LICENSE +21 -0
- package/README.md +775 -0
- package/dist/browser-ui.d.ts +43 -0
- package/dist/browser-ui.d.ts.map +1 -0
- package/dist/browser-ui.js +246 -0
- package/dist/browser-ui.js.map +1 -0
- package/dist/capture/event.d.ts +33 -0
- package/dist/capture/event.d.ts.map +1 -0
- package/dist/capture/event.js +2 -0
- package/dist/capture/event.js.map +1 -0
- package/dist/capture/index.d.ts +4 -0
- package/dist/capture/index.d.ts.map +1 -0
- package/dist/capture/index.js +4 -0
- package/dist/capture/index.js.map +1 -0
- package/dist/capture/recorder.d.ts +24 -0
- package/dist/capture/recorder.d.ts.map +1 -0
- package/dist/capture/recorder.js +46 -0
- package/dist/capture/recorder.js.map +1 -0
- package/dist/capture/replay.d.ts +20 -0
- package/dist/capture/replay.d.ts.map +1 -0
- package/dist/capture/replay.js +47 -0
- package/dist/capture/replay.js.map +1 -0
- package/dist/ci/api-client.d.ts +38 -0
- package/dist/ci/api-client.d.ts.map +1 -0
- package/dist/ci/api-client.js +96 -0
- package/dist/ci/api-client.js.map +1 -0
- package/dist/ci/benchmark.d.ts +33 -0
- package/dist/ci/benchmark.d.ts.map +1 -0
- package/dist/ci/benchmark.js +213 -0
- package/dist/ci/benchmark.js.map +1 -0
- package/dist/ci/ed-runner.d.ts +48 -0
- package/dist/ci/ed-runner.d.ts.map +1 -0
- package/dist/ci/ed-runner.js +260 -0
- package/dist/ci/ed-runner.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 +542 -0
- package/dist/ci/executor.js.map +1 -0
- package/dist/ci/git-info.d.ts +17 -0
- package/dist/ci/git-info.d.ts.map +1 -0
- package/dist/ci/git-info.js +102 -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/measurement.d.ts +9 -0
- package/dist/ci/measurement.d.ts.map +1 -0
- package/dist/ci/measurement.js +15 -0
- package/dist/ci/measurement.js.map +1 -0
- package/dist/ci/replay.d.ts +31 -0
- package/dist/ci/replay.d.ts.map +1 -0
- package/dist/ci/replay.js +96 -0
- package/dist/ci/replay.js.map +1 -0
- package/dist/ci/reporters/default.d.ts +8 -0
- package/dist/ci/reporters/default.d.ts.map +1 -0
- package/dist/ci/reporters/default.js +46 -0
- package/dist/ci/reporters/default.js.map +1 -0
- package/dist/ci/reporters/index.d.ts +8 -0
- package/dist/ci/reporters/index.d.ts.map +1 -0
- package/dist/ci/reporters/index.js +14 -0
- package/dist/ci/reporters/index.js.map +1 -0
- package/dist/ci/reporters/json.d.ts +8 -0
- package/dist/ci/reporters/json.d.ts.map +1 -0
- package/dist/ci/reporters/json.js +14 -0
- package/dist/ci/reporters/json.js.map +1 -0
- package/dist/ci/reporters/junit.d.ts +8 -0
- package/dist/ci/reporters/junit.d.ts.map +1 -0
- package/dist/ci/reporters/junit.js +48 -0
- package/dist/ci/reporters/junit.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 +187 -0
- package/dist/ci/runner.js.map +1 -0
- package/dist/ci/test-discovery.d.ts +5 -0
- package/dist/ci/test-discovery.d.ts.map +1 -0
- package/dist/ci/test-discovery.js +11 -0
- package/dist/ci/test-discovery.js.map +1 -0
- package/dist/ci/test-loader.d.ts +19 -0
- package/dist/ci/test-loader.d.ts.map +1 -0
- package/dist/ci/test-loader.js +149 -0
- package/dist/ci/test-loader.js.map +1 -0
- package/dist/ci/test-registry.d.ts +42 -0
- package/dist/ci/test-registry.d.ts.map +1 -0
- package/dist/ci/test-registry.js +18 -0
- package/dist/ci/test-registry.js.map +1 -0
- package/dist/ci/trace-schema.d.ts +30 -0
- package/dist/ci/trace-schema.d.ts.map +1 -0
- package/dist/ci/trace-schema.js +66 -0
- package/dist/ci/trace-schema.js.map +1 -0
- package/dist/ci/trace-writer.d.ts +16 -0
- package/dist/ci/trace-writer.d.ts.map +1 -0
- package/dist/ci/trace-writer.js +108 -0
- package/dist/ci/trace-writer.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/ci/upload-client.d.ts +74 -0
- package/dist/ci/upload-client.d.ts.map +1 -0
- package/dist/ci/upload-client.js +195 -0
- package/dist/ci/upload-client.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +716 -0
- package/dist/cli.js.map +1 -0
- package/dist/core/agent-state.d.ts +47 -0
- package/dist/core/agent-state.d.ts.map +1 -0
- package/dist/core/agent-state.js +137 -0
- package/dist/core/agent-state.js.map +1 -0
- package/dist/core/judge-utils.d.ts +22 -0
- package/dist/core/judge-utils.d.ts.map +1 -0
- package/dist/core/judge-utils.js +211 -0
- package/dist/core/judge-utils.js.map +1 -0
- package/dist/core/registry.d.ts +28 -0
- package/dist/core/registry.d.ts.map +1 -0
- package/dist/core/registry.js +52 -0
- package/dist/core/registry.js.map +1 -0
- package/dist/dashboard-server.d.ts +65 -0
- package/dist/dashboard-server.d.ts.map +1 -0
- package/dist/dashboard-server.js +3940 -0
- package/dist/dashboard-server.js.map +1 -0
- package/dist/execution/tool-runner.d.ts +26 -0
- package/dist/execution/tool-runner.d.ts.map +1 -0
- package/dist/execution/tool-runner.js +316 -0
- package/dist/execution/tool-runner.js.map +1 -0
- package/dist/html/dashboard.html +2218 -0
- package/dist/http.d.ts +14 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +13 -0
- package/dist/http.js.map +1 -0
- package/dist/index.cjs +8102 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +67 -0
- package/dist/index.js.map +1 -0
- package/dist/interceptors/ai-interceptor.d.ts +26 -0
- package/dist/interceptors/ai-interceptor.d.ts.map +1 -0
- package/dist/interceptors/ai-interceptor.js +756 -0
- package/dist/interceptors/ai-interceptor.js.map +1 -0
- package/dist/interceptors/db-auto.d.ts +8 -0
- package/dist/interceptors/db-auto.d.ts.map +1 -0
- package/dist/interceptors/db-auto.js +217 -0
- package/dist/interceptors/db-auto.js.map +1 -0
- package/dist/interceptors/db.d.ts +23 -0
- package/dist/interceptors/db.d.ts.map +1 -0
- package/dist/interceptors/db.js +137 -0
- package/dist/interceptors/db.js.map +1 -0
- package/dist/interceptors/http.d.ts +28 -0
- package/dist/interceptors/http.d.ts.map +1 -0
- package/dist/interceptors/http.js +356 -0
- package/dist/interceptors/http.js.map +1 -0
- package/dist/interceptors/side-effects.d.ts +7 -0
- package/dist/interceptors/side-effects.d.ts.map +1 -0
- package/dist/interceptors/side-effects.js +72 -0
- package/dist/interceptors/side-effects.js.map +1 -0
- package/dist/interceptors/telemetry-push.d.ts +142 -0
- package/dist/interceptors/telemetry-push.d.ts.map +1 -0
- package/dist/interceptors/telemetry-push.js +463 -0
- package/dist/interceptors/telemetry-push.js.map +1 -0
- package/dist/interceptors/tool.d.ts +2 -0
- package/dist/interceptors/tool.d.ts.map +1 -0
- package/dist/interceptors/tool.js +274 -0
- package/dist/interceptors/tool.js.map +1 -0
- package/dist/interceptors/workflow-ai.d.ts +5 -0
- package/dist/interceptors/workflow-ai.d.ts.map +1 -0
- package/dist/interceptors/workflow-ai.js +382 -0
- package/dist/interceptors/workflow-ai.js.map +1 -0
- package/dist/internals/conditional-recorder.d.ts +21 -0
- package/dist/internals/conditional-recorder.d.ts.map +1 -0
- package/dist/internals/conditional-recorder.js +54 -0
- package/dist/internals/conditional-recorder.js.map +1 -0
- package/dist/internals/mock-resolver.d.ts +146 -0
- package/dist/internals/mock-resolver.d.ts.map +1 -0
- package/dist/internals/mock-resolver.js +427 -0
- package/dist/internals/mock-resolver.js.map +1 -0
- package/dist/matchers/index.d.ts +96 -0
- package/dist/matchers/index.d.ts.map +1 -0
- package/dist/matchers/index.js +668 -0
- package/dist/matchers/index.js.map +1 -0
- package/dist/observability.d.ts +82 -0
- package/dist/observability.d.ts.map +1 -0
- package/dist/observability.js +471 -0
- package/dist/observability.js.map +1 -0
- package/dist/portal-executor.d.ts +30 -0
- package/dist/portal-executor.d.ts.map +1 -0
- package/dist/portal-executor.js +324 -0
- package/dist/portal-executor.js.map +1 -0
- package/dist/portal-server.d.ts +3 -0
- package/dist/portal-server.d.ts.map +1 -0
- package/dist/portal-server.js +279 -0
- package/dist/portal-server.js.map +1 -0
- package/dist/proxy/llm-capture.d.ts +14 -0
- package/dist/proxy/llm-capture.d.ts.map +1 -0
- package/dist/proxy/llm-capture.js +264 -0
- package/dist/proxy/llm-capture.js.map +1 -0
- package/dist/reporter.d.ts +3 -0
- package/dist/reporter.d.ts.map +1 -0
- package/dist/reporter.js +72 -0
- package/dist/reporter.js.map +1 -0
- package/dist/runWorkflowSubprocess.d.ts +14 -0
- package/dist/runWorkflowSubprocess.d.ts.map +1 -0
- package/dist/runWorkflowSubprocess.js +66 -0
- package/dist/runWorkflowSubprocess.js.map +1 -0
- package/dist/runner.d.ts +16 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +138 -0
- package/dist/runner.js.map +1 -0
- package/dist/socket-connector.d.ts +22 -0
- package/dist/socket-connector.d.ts.map +1 -0
- package/dist/socket-connector.js +104 -0
- package/dist/socket-connector.js.map +1 -0
- package/dist/telemetry-batcher.d.ts +56 -0
- package/dist/telemetry-batcher.d.ts.map +1 -0
- package/dist/telemetry-batcher.js +143 -0
- package/dist/telemetry-batcher.js.map +1 -0
- package/dist/test-setup.d.ts +12 -0
- package/dist/test-setup.d.ts.map +1 -0
- package/dist/test-setup.js +13 -0
- package/dist/test-setup.js.map +1 -0
- package/dist/tool-registry.d.ts +31 -0
- package/dist/tool-registry.d.ts.map +1 -0
- package/dist/tool-registry.js +73 -0
- package/dist/tool-registry.js.map +1 -0
- package/dist/tool-runner-worker.d.ts +2 -0
- package/dist/tool-runner-worker.d.ts.map +1 -0
- package/dist/tool-runner-worker.js +215 -0
- package/dist/tool-runner-worker.js.map +1 -0
- package/dist/trace-adapter/context.d.ts +72 -0
- package/dist/trace-adapter/context.d.ts.map +1 -0
- package/dist/trace-adapter/context.js +80 -0
- package/dist/trace-adapter/context.js.map +1 -0
- package/dist/tracing.d.ts +2 -0
- package/dist/tracing.d.ts.map +1 -0
- package/dist/tracing.js +59 -0
- package/dist/tracing.js.map +1 -0
- package/dist/trigger-executor.d.ts +12 -0
- package/dist/trigger-executor.d.ts.map +1 -0
- package/dist/trigger-executor.js +130 -0
- package/dist/trigger-executor.js.map +1 -0
- package/dist/types/portal.d.ts +76 -0
- package/dist/types/portal.d.ts.map +1 -0
- package/dist/types/portal.js +2 -0
- package/dist/types/portal.js.map +1 -0
- package/dist/utils/debug.d.ts +3 -0
- package/dist/utils/debug.d.ts.map +1 -0
- package/dist/utils/debug.js +8 -0
- package/dist/utils/debug.js.map +1 -0
- package/dist/utils/license-error.d.ts +23 -0
- package/dist/utils/license-error.d.ts.map +1 -0
- package/dist/utils/license-error.js +42 -0
- package/dist/utils/license-error.js.map +1 -0
- package/dist/utils/redact.d.ts +7 -0
- package/dist/utils/redact.d.ts.map +1 -0
- package/dist/utils/redact.js +26 -0
- package/dist/utils/redact.js.map +1 -0
- package/dist/workflow-runner-worker.d.ts +2 -0
- package/dist/workflow-runner-worker.d.ts.map +1 -0
- package/dist/workflow-runner-worker.js +329 -0
- package/dist/workflow-runner-worker.js.map +1 -0
- package/dist/workflow-runner.d.ts +14 -0
- package/dist/workflow-runner.d.ts.map +1 -0
- package/dist/workflow-runner.js +34 -0
- package/dist/workflow-runner.js.map +1 -0
- package/docs/agent-coding-instructions.md +138 -0
- package/docs/agent-integration-guide.md +564 -0
- package/docs/agents.md +140 -0
- package/docs/dashboard.md +394 -0
- package/docs/deno.md +69 -0
- package/docs/instrumentation.md +424 -0
- package/docs/langfuse-trace-structure.md +145 -0
- package/docs/matchers.md +173 -0
- package/docs/observability_contract.md +192 -0
- package/docs/observability_mode.md +195 -0
- package/docs/quickstart.md +621 -0
- package/docs/security-compliance.md +566 -0
- package/docs/test-writing-guidelines.md +444 -0
- package/docs/tools.md +165 -0
- package/docs/workflow-modes.md +253 -0
- package/package.json +76 -0
- package/src/browser-ui.ts +281 -0
- package/src/capture/event.ts +30 -0
- package/src/capture/index.ts +3 -0
- package/src/capture/recorder.ts +62 -0
- package/src/capture/replay.ts +55 -0
- package/src/ci/api-client.ts +136 -0
- package/src/ci/benchmark.ts +257 -0
- package/src/ci/ed-runner.ts +351 -0
- package/src/ci/executor.ts +671 -0
- package/src/ci/git-info.ts +127 -0
- package/src/ci/index.ts +5 -0
- package/src/ci/measurement.ts +25 -0
- package/src/ci/replay.ts +127 -0
- package/src/ci/reporters/default.ts +50 -0
- package/src/ci/reporters/index.ts +21 -0
- package/src/ci/reporters/json.ts +18 -0
- package/src/ci/reporters/junit.ts +61 -0
- package/src/ci/runner.ts +208 -0
- package/src/ci/test-discovery.ts +16 -0
- package/src/ci/test-loader.ts +187 -0
- package/src/ci/test-registry.ts +62 -0
- package/src/ci/trace-schema.ts +96 -0
- package/src/ci/trace-writer.ts +107 -0
- package/src/ci/types.ts +115 -0
- package/src/ci/upload-client.ts +300 -0
- package/src/cli.ts +811 -0
- package/src/core/agent-state.ts +162 -0
- package/src/core/judge-utils.ts +232 -0
- package/src/core/registry.ts +92 -0
- package/src/dashboard-server.ts +2047 -0
- package/src/execution/tool-runner.ts +352 -0
- package/src/html/dashboard.html +2218 -0
- package/src/http.ts +13 -0
- package/src/index.ts +138 -0
- package/src/interceptors/ai-interceptor.ts +798 -0
- package/src/interceptors/db-auto.ts +243 -0
- package/src/interceptors/db.ts +156 -0
- package/src/interceptors/http.ts +393 -0
- package/src/interceptors/side-effects.ts +83 -0
- package/src/interceptors/telemetry-push.ts +537 -0
- package/src/interceptors/tool.ts +287 -0
- package/src/interceptors/workflow-ai.ts +419 -0
- package/src/internals/conditional-recorder.ts +63 -0
- package/src/internals/mock-resolver.ts +492 -0
- package/src/matchers/index.ts +824 -0
- package/src/observability.ts +501 -0
- package/src/portal-executor.ts +355 -0
- package/src/portal-server.ts +304 -0
- package/src/proxy/llm-capture.ts +301 -0
- package/src/reporter.ts +81 -0
- package/src/runWorkflowSubprocess.ts +74 -0
- package/src/runner.ts +178 -0
- package/src/socket-connector.ts +117 -0
- package/src/telemetry-batcher.ts +191 -0
- package/src/test-setup.ts +16 -0
- package/src/tool-registry.ts +94 -0
- package/src/tool-runner-worker.ts +244 -0
- package/src/trace-adapter/context.ts +156 -0
- package/src/tracing.ts +62 -0
- package/src/trigger-executor.ts +171 -0
- package/src/types/agent.d.ts +63 -0
- package/src/types/expect.d.ts +81 -0
- package/src/types/modules.d.ts +2 -0
- package/src/types/portal.ts +69 -0
- package/src/utils/debug.ts +8 -0
- package/src/utils/license-error.ts +43 -0
- package/src/utils/redact.ts +25 -0
- package/src/workflow-runner-worker.ts +386 -0
- package/src/workflow-runner.ts +58 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import http from 'node:http'
|
|
2
|
+
import { URL } from 'node:url'
|
|
3
|
+
import { Readable } from 'node:stream'
|
|
4
|
+
import type { LLMStep } from '../trace-adapter/context.js'
|
|
5
|
+
|
|
6
|
+
const DEFAULT_PORT = 8787
|
|
7
|
+
const HEADER_TRACE_ID = 'x-trace-id'
|
|
8
|
+
|
|
9
|
+
type Provider = 'openai' | 'gemini' | 'grok' | 'anthropic'
|
|
10
|
+
|
|
11
|
+
const DEFAULT_UPSTREAM: Record<Provider, string> = {
|
|
12
|
+
openai: 'https://api.openai.com',
|
|
13
|
+
gemini: 'https://generativelanguage.googleapis.com',
|
|
14
|
+
grok: 'https://api.x.ai',
|
|
15
|
+
anthropic: 'https://api.anthropic.com',
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const AI_PATTERNS: Record<Provider, RegExp> = {
|
|
19
|
+
openai: /\/v1\/(chat\/)?completions/, // also covers legacy /v1/completions
|
|
20
|
+
gemini: /\/v1beta\/models\/[^/:]+:(generateContent|streamGenerateContent)/,
|
|
21
|
+
grok: /\/v1\/(chat\/)?completions/,
|
|
22
|
+
anthropic: /\/v1\/messages/,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function detectProvider(pathname: string): Provider | null {
|
|
26
|
+
for (const [provider, pattern] of Object.entries(AI_PATTERNS) as Array<[Provider, RegExp]>) {
|
|
27
|
+
if (pattern.test(pathname)) return provider
|
|
28
|
+
}
|
|
29
|
+
return null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function extractModel(provider: Provider, body: Record<string, unknown>, url: string): string {
|
|
33
|
+
if (provider === 'gemini') {
|
|
34
|
+
const match = /\/models\/([^/:]+):/.exec(url)
|
|
35
|
+
return match ? match[1] : 'unknown'
|
|
36
|
+
}
|
|
37
|
+
if (provider === 'anthropic') {
|
|
38
|
+
return typeof body.model === 'string' ? body.model : 'unknown'
|
|
39
|
+
}
|
|
40
|
+
return typeof body.model === 'string' ? body.model : 'unknown'
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function extractPrompt(provider: Provider, body: Record<string, unknown>): string {
|
|
44
|
+
if (provider === 'openai' || provider === 'grok') {
|
|
45
|
+
const messages = body.messages
|
|
46
|
+
if (Array.isArray(messages)) {
|
|
47
|
+
return messages
|
|
48
|
+
.map((m: unknown) => {
|
|
49
|
+
if (m && typeof m === 'object') {
|
|
50
|
+
const msg = m as Record<string, unknown>
|
|
51
|
+
return `${msg.role}: ${msg.content}`
|
|
52
|
+
}
|
|
53
|
+
return String(m)
|
|
54
|
+
})
|
|
55
|
+
.join('\n')
|
|
56
|
+
}
|
|
57
|
+
return typeof body.prompt === 'string' ? body.prompt : ''
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (provider === 'gemini') {
|
|
61
|
+
const contents = body.contents
|
|
62
|
+
if (Array.isArray(contents)) {
|
|
63
|
+
return contents
|
|
64
|
+
.flatMap((c: unknown) => {
|
|
65
|
+
if (c && typeof c === 'object') {
|
|
66
|
+
const parts = (c as Record<string, unknown>).parts
|
|
67
|
+
if (Array.isArray(parts)) {
|
|
68
|
+
return parts.map((p: unknown) => {
|
|
69
|
+
if (p && typeof p === 'object') {
|
|
70
|
+
return String((p as Record<string, unknown>).text ?? '')
|
|
71
|
+
}
|
|
72
|
+
return ''
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return []
|
|
77
|
+
})
|
|
78
|
+
.join('\n')
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (provider === 'anthropic') {
|
|
83
|
+
const messages = body.messages
|
|
84
|
+
if (Array.isArray(messages)) {
|
|
85
|
+
return messages
|
|
86
|
+
.map((m: unknown) => {
|
|
87
|
+
if (m && typeof m === 'object') {
|
|
88
|
+
const msg = m as Record<string, unknown>
|
|
89
|
+
return `${msg.role ?? 'user'}: ${msg.content ?? ''}`
|
|
90
|
+
}
|
|
91
|
+
return String(m)
|
|
92
|
+
})
|
|
93
|
+
.join('\n')
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return ''
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function extractCompletion(provider: Provider, responseBody: Record<string, unknown>): string {
|
|
101
|
+
if (provider === 'openai' || provider === 'grok') {
|
|
102
|
+
const choices = responseBody.choices
|
|
103
|
+
if (Array.isArray(choices) && choices.length > 0) {
|
|
104
|
+
const first = choices[0] as Record<string, unknown>
|
|
105
|
+
if (first.message && typeof first.message === 'object') {
|
|
106
|
+
return String((first.message as Record<string, unknown>).content ?? '')
|
|
107
|
+
}
|
|
108
|
+
if (typeof first.text === 'string') return first.text
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (provider === 'gemini') {
|
|
113
|
+
const candidates = responseBody.candidates
|
|
114
|
+
if (Array.isArray(candidates) && candidates.length > 0) {
|
|
115
|
+
const first = candidates[0] as Record<string, unknown>
|
|
116
|
+
if (first.content && typeof first.content === 'object') {
|
|
117
|
+
const parts = (first.content as Record<string, unknown>).parts
|
|
118
|
+
if (Array.isArray(parts) && parts.length > 0) {
|
|
119
|
+
return String((parts[0] as Record<string, unknown>).text ?? '')
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (provider === 'anthropic') {
|
|
126
|
+
const content = responseBody.content
|
|
127
|
+
if (Array.isArray(content) && content.length > 0) {
|
|
128
|
+
const first = content[0] as Record<string, unknown>
|
|
129
|
+
if (typeof first.text === 'string') return first.text
|
|
130
|
+
if (first.type === 'text' && typeof first.text === 'string') return first.text
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return ''
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function cloneHeaders(headers: http.IncomingHttpHeaders): Record<string, string> {
|
|
138
|
+
const result: Record<string, string> = {}
|
|
139
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
140
|
+
if (Array.isArray(value)) {
|
|
141
|
+
result[key] = value.join(', ')
|
|
142
|
+
} else if (typeof value === 'string') {
|
|
143
|
+
result[key] = value
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return result
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function normalizeUpstream(provider: Provider, userBase?: string): string {
|
|
150
|
+
const base = userBase || DEFAULT_UPSTREAM[provider]
|
|
151
|
+
return base.endsWith('/') ? base.slice(0, -1) : base
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
type Store = Map<string, LLMStep[]>
|
|
155
|
+
|
|
156
|
+
function recordStep(store: Store, traceId: string, step: LLMStep): void {
|
|
157
|
+
if (!store.has(traceId)) {
|
|
158
|
+
store.set(traceId, [])
|
|
159
|
+
}
|
|
160
|
+
store.get(traceId)!.push(step)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function readBody(req: http.IncomingMessage): Promise<Buffer> {
|
|
164
|
+
const chunks: Buffer[] = []
|
|
165
|
+
for await (const chunk of req) {
|
|
166
|
+
chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk)
|
|
167
|
+
}
|
|
168
|
+
return Buffer.concat(chunks)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function sendUpstreamResponse(upstreamRes: Response, res: http.ServerResponse): void {
|
|
172
|
+
res.statusCode = upstreamRes.status
|
|
173
|
+
for (const [key, value] of upstreamRes.headers.entries()) {
|
|
174
|
+
res.setHeader(key, value)
|
|
175
|
+
}
|
|
176
|
+
const body = upstreamRes.body
|
|
177
|
+
if (!body) {
|
|
178
|
+
res.end()
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
const nodeStream = Readable.fromWeb(body as unknown as ReadableStream)
|
|
182
|
+
nodeStream.pipe(res)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export interface StartedProxy {
|
|
186
|
+
url: string
|
|
187
|
+
stop(): Promise<void>
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export interface ProxyOptions {
|
|
191
|
+
port?: number
|
|
192
|
+
upstream?: Partial<Record<Provider, string>>
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export async function startLLMProxy(options: ProxyOptions = {}): Promise<StartedProxy> {
|
|
196
|
+
const port = options.port ?? DEFAULT_PORT
|
|
197
|
+
const store: Store = new Map()
|
|
198
|
+
const upstreamOverride = options.upstream || {}
|
|
199
|
+
|
|
200
|
+
const server = http.createServer(async (req, res) => {
|
|
201
|
+
try {
|
|
202
|
+
if (!req.url || !req.method) {
|
|
203
|
+
res.statusCode = 400
|
|
204
|
+
res.end('Bad request')
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const parsed = new URL(req.url, `http://localhost:${port}`)
|
|
209
|
+
|
|
210
|
+
if (req.method === 'GET' && parsed.pathname.startsWith('/traces/')) {
|
|
211
|
+
const traceId = decodeURIComponent(parsed.pathname.replace('/traces/', ''))
|
|
212
|
+
const steps = store.get(traceId) ?? []
|
|
213
|
+
store.delete(traceId)
|
|
214
|
+
res.setHeader('content-type', 'application/json')
|
|
215
|
+
res.end(JSON.stringify({ steps }))
|
|
216
|
+
return
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (req.method === 'GET' && parsed.pathname === '/health') {
|
|
220
|
+
res.statusCode = 200
|
|
221
|
+
res.end('ok')
|
|
222
|
+
return
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const bodyBuf = await readBody(req)
|
|
226
|
+
const bodyText = bodyBuf.toString() || '{}'
|
|
227
|
+
const requestBody = (() => {
|
|
228
|
+
try {
|
|
229
|
+
return JSON.parse(bodyText) as Record<string, unknown>
|
|
230
|
+
} catch {
|
|
231
|
+
return {} as Record<string, unknown>
|
|
232
|
+
}
|
|
233
|
+
})()
|
|
234
|
+
|
|
235
|
+
const provider = detectProvider(parsed.pathname)
|
|
236
|
+
const traceId = (req.headers[HEADER_TRACE_ID] as string | undefined)?.toString()
|
|
237
|
+
const isStreaming = requestBody && typeof requestBody === 'object' ? (requestBody as any).stream === true : false
|
|
238
|
+
const headers = cloneHeaders(req.headers)
|
|
239
|
+
|
|
240
|
+
const upstreamBase = provider ? normalizeUpstream(provider, upstreamOverride[provider]) : undefined
|
|
241
|
+
if (!provider || !upstreamBase) {
|
|
242
|
+
// Fallback passthrough without capture
|
|
243
|
+
const passthrough = await fetch(parsed.toString(), {
|
|
244
|
+
method: req.method,
|
|
245
|
+
headers,
|
|
246
|
+
body: bodyBuf.length > 0 ? bodyBuf : undefined,
|
|
247
|
+
})
|
|
248
|
+
sendUpstreamResponse(passthrough, res)
|
|
249
|
+
return
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const targetUrl = `${upstreamBase}${parsed.pathname}${parsed.search}`
|
|
253
|
+
const upstreamRes = await fetch(targetUrl, {
|
|
254
|
+
method: req.method,
|
|
255
|
+
headers,
|
|
256
|
+
body: bodyBuf.length > 0 ? bodyBuf : undefined,
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
if (traceId) {
|
|
260
|
+
const model = extractModel(provider, requestBody, targetUrl)
|
|
261
|
+
const prompt = extractPrompt(provider, requestBody)
|
|
262
|
+
if (isStreaming) {
|
|
263
|
+
recordStep(store, traceId, { model, provider, prompt, completion: '(streamed)' })
|
|
264
|
+
} else {
|
|
265
|
+
try {
|
|
266
|
+
const clone = upstreamRes.clone()
|
|
267
|
+
const responseBody = (await clone.json()) as Record<string, unknown>
|
|
268
|
+
const completion = extractCompletion(provider, responseBody)
|
|
269
|
+
recordStep(store, traceId, { model, provider, prompt, completion })
|
|
270
|
+
} catch {
|
|
271
|
+
recordStep(store, traceId, { model, provider, prompt, completion: '' })
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
sendUpstreamResponse(upstreamRes, res)
|
|
277
|
+
} catch (err) {
|
|
278
|
+
res.statusCode = 500
|
|
279
|
+
res.end(`proxy error: ${(err as Error).message}`)
|
|
280
|
+
}
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
await new Promise<void>((resolve) => server.listen(port, resolve))
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
url: `http://localhost:${port}`,
|
|
287
|
+
async stop() {
|
|
288
|
+
await new Promise<void>((resolve) => server.close(() => resolve()))
|
|
289
|
+
},
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export async function fetchCapturedTrace(proxyUrl: string, traceId: string): Promise<LLMStep[]> {
|
|
294
|
+
const url = `${proxyUrl.replace(/\/$/, '')}/traces/${encodeURIComponent(traceId)}`
|
|
295
|
+
const res = await fetch(url)
|
|
296
|
+
if (!res.ok) {
|
|
297
|
+
throw new Error(`failed to fetch trace ${traceId} from proxy: ${res.status}`)
|
|
298
|
+
}
|
|
299
|
+
const data = (await res.json()) as { steps?: LLMStep[] }
|
|
300
|
+
return data.steps || []
|
|
301
|
+
}
|
package/src/reporter.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import type { TestResult, FileResult } from './runner.js'
|
|
3
|
+
|
|
4
|
+
export function reportResults(fileResults: FileResult[]): void {
|
|
5
|
+
let totalPassed = 0
|
|
6
|
+
let totalFailed = 0
|
|
7
|
+
let totalDurationMs = 0
|
|
8
|
+
|
|
9
|
+
for (const fileResult of fileResults) {
|
|
10
|
+
if (fileResults.length > 1) {
|
|
11
|
+
console.log(chalk.dim(`\n${fileResult.file}`))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
for (const result of fileResult.results) {
|
|
15
|
+
printTestResult(result)
|
|
16
|
+
totalDurationMs += result.durationMs
|
|
17
|
+
if (result.passed) {
|
|
18
|
+
totalPassed++
|
|
19
|
+
} else {
|
|
20
|
+
totalFailed++
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
printSummary(totalPassed, totalFailed, totalDurationMs)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function printTestResult(result: TestResult): void {
|
|
29
|
+
const duration = chalk.dim(`(${formatDuration(result.durationMs)})`)
|
|
30
|
+
|
|
31
|
+
if (result.passed) {
|
|
32
|
+
console.log(` ${chalk.green('✓')} ${result.name} ${duration}`)
|
|
33
|
+
} else {
|
|
34
|
+
console.log(` ${chalk.red('✗')} ${result.name} ${duration}`)
|
|
35
|
+
if (result.error) {
|
|
36
|
+
const errorLines = formatError(result.error)
|
|
37
|
+
for (const line of errorLines) {
|
|
38
|
+
console.log(` ${chalk.red('→')} ${line}`)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function printSummary(passed: number, failed: number, totalMs: number): void {
|
|
45
|
+
const total = passed + failed
|
|
46
|
+
console.log('')
|
|
47
|
+
|
|
48
|
+
if (passed > 0) {
|
|
49
|
+
console.log(chalk.green(`${passed} passed`))
|
|
50
|
+
}
|
|
51
|
+
if (failed > 0) {
|
|
52
|
+
console.log(chalk.red(`${failed} failed`))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
console.log(chalk.dim(`Total: ${total}`))
|
|
56
|
+
console.log(chalk.dim(`Duration: ${formatDuration(totalMs)}`))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function formatDuration(ms: number): string {
|
|
60
|
+
if (ms >= 1000) {
|
|
61
|
+
return `${(ms / 1000).toFixed(1)}s`
|
|
62
|
+
}
|
|
63
|
+
return `${ms}ms`
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function formatError(error: Error): string[] {
|
|
67
|
+
const lines: string[] = []
|
|
68
|
+
if (error.message) {
|
|
69
|
+
lines.push(error.message)
|
|
70
|
+
}
|
|
71
|
+
if (error.stack) {
|
|
72
|
+
const stackLines = error.stack
|
|
73
|
+
.split('\n')
|
|
74
|
+
.slice(1)
|
|
75
|
+
.map((l) => l.trim())
|
|
76
|
+
.filter((l) => l.startsWith('at '))
|
|
77
|
+
.slice(0, 3)
|
|
78
|
+
lines.push(...stackLines)
|
|
79
|
+
}
|
|
80
|
+
return lines
|
|
81
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { Readable } from 'stream';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Run workflow logic in a subprocess using the workflow-runner-worker (fd3 IPC).
|
|
7
|
+
* This utility is designed for use in Next.js API routes or any parent process.
|
|
8
|
+
*
|
|
9
|
+
* @param {object} input - The workflow input (requestBody, userToken, testCaseId, testCaseRunRecordId, etc)
|
|
10
|
+
* @returns {Promise<any>} - Resolves with the result from the worker, or throws on error.
|
|
11
|
+
*
|
|
12
|
+
* Usage (in your Next.js API route):
|
|
13
|
+
* import { runWorkflowInSubprocess } from './src/runWorkflowSubprocess';
|
|
14
|
+
* const result = await runWorkflowInSubprocess({ ... });
|
|
15
|
+
* return NextResponse.json(result);
|
|
16
|
+
*/
|
|
17
|
+
export async function runWorkflowInSubprocess(input: Record<string, any>): Promise<any> {
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
// Path to the workflow worker entry point
|
|
20
|
+
const workerPath = join(process.cwd(), 'src', 'workflow-runner-worker.ts');
|
|
21
|
+
|
|
22
|
+
// Spawn the worker as a subprocess with fd3 pipe
|
|
23
|
+
const child = spawn(
|
|
24
|
+
process.execPath,
|
|
25
|
+
[workerPath],
|
|
26
|
+
{
|
|
27
|
+
stdio: ['pipe', 'pipe', 'pipe', 'pipe'],
|
|
28
|
+
env: { ...process.env },
|
|
29
|
+
}
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
// Write the input as JSON to the worker's stdin
|
|
33
|
+
child.stdin.write(JSON.stringify(input));
|
|
34
|
+
child.stdin.end();
|
|
35
|
+
|
|
36
|
+
let resultData = '';
|
|
37
|
+
let errorData = '';
|
|
38
|
+
|
|
39
|
+
// Read result from fd3 (child.stdio[3])
|
|
40
|
+
if (child.stdio[3] && child.stdio[3] instanceof Readable) {
|
|
41
|
+
child.stdio[3].setEncoding('utf8');
|
|
42
|
+
child.stdio[3].on('data', (chunk) => {
|
|
43
|
+
resultData += chunk;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Optionally, collect logs from stdout/stderr for debugging
|
|
48
|
+
child.stdout?.on('data', (chunk) => {
|
|
49
|
+
// Optionally log or buffer
|
|
50
|
+
});
|
|
51
|
+
child.stderr?.on('data', (chunk) => {
|
|
52
|
+
errorData += chunk;
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
child.on('error', (err) => {
|
|
56
|
+
reject(err);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
child.on('close', (code) => {
|
|
60
|
+
if (resultData) {
|
|
61
|
+
try {
|
|
62
|
+
const parsed = JSON.parse(resultData);
|
|
63
|
+
resolve(parsed);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
reject(new Error('Failed to parse worker result: ' + err));
|
|
66
|
+
}
|
|
67
|
+
} else if (errorData) {
|
|
68
|
+
reject(new Error('Worker error: ' + errorData));
|
|
69
|
+
} else {
|
|
70
|
+
reject(new Error('Worker exited with code ' + code + ' and no result.'));
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
}
|
package/src/runner.ts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { clearRegistry, getRegistry } from './core/registry.js'
|
|
2
|
+
import { startTraceSession, setCurrentTrace } from './trace-adapter/context.js'
|
|
3
|
+
import { startLLMProxy, fetchCapturedTrace } from './proxy/llm-capture.js'
|
|
4
|
+
import type { RunnerHooks } from './trace-adapter/context.js'
|
|
5
|
+
import { pathToFileURL } from 'node:url'
|
|
6
|
+
import { randomUUID } from 'node:crypto'
|
|
7
|
+
import path from 'node:path'
|
|
8
|
+
|
|
9
|
+
export interface TestResult {
|
|
10
|
+
name: string
|
|
11
|
+
passed: boolean
|
|
12
|
+
durationMs: number
|
|
13
|
+
error?: Error
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface FileResult {
|
|
17
|
+
file: string
|
|
18
|
+
results: TestResult[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface RunnerOptions {
|
|
22
|
+
hooks?: RunnerHooks
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function runFiles(files: string[], options: RunnerOptions = {}): Promise<FileResult[]> {
|
|
26
|
+
// Optional local LLM capture proxy (opt-in via env). Default behavior stays unchanged when disabled.
|
|
27
|
+
const proxyOptIn = process.env.ELASTICDASH_LLM_PROXY === '1' || Boolean(process.env.ELASTICDASH_LLM_PROXY_URL)
|
|
28
|
+
const proxyPort = Number.parseInt(process.env.ELASTICDASH_LLM_PROXY_PORT || '8787', 10)
|
|
29
|
+
let proxyUrl = process.env.ELASTICDASH_LLM_PROXY_URL
|
|
30
|
+
const proxyHandle = proxyOptIn && !proxyUrl ? await startLLMProxy({ port: proxyPort }) : null
|
|
31
|
+
if (proxyHandle) {
|
|
32
|
+
proxyUrl = proxyHandle.url
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const fileResults: FileResult[] = []
|
|
36
|
+
|
|
37
|
+
for (const file of files) {
|
|
38
|
+
const result = await runFile(file, options, { proxyOptIn, proxyUrl })
|
|
39
|
+
fileResults.push(result)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (proxyHandle) {
|
|
43
|
+
await proxyHandle.stop()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return fileResults
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function runFile(file: string, options: RunnerOptions, proxyCtx: { proxyOptIn: boolean; proxyUrl: string | null | undefined }): Promise<FileResult> {
|
|
50
|
+
const { hooks = {} } = options
|
|
51
|
+
|
|
52
|
+
// 1. Clear the global registry before loading the file
|
|
53
|
+
clearRegistry()
|
|
54
|
+
|
|
55
|
+
// 2. Dynamically import the test file (triggers aiTest() registrations)
|
|
56
|
+
const resolvedPath = file.startsWith('file://')
|
|
57
|
+
? file
|
|
58
|
+
: pathToFileURL(path.resolve(file)).href
|
|
59
|
+
|
|
60
|
+
if (resolvedPath.endsWith('.ts') && typeof (globalThis as any).Deno === 'undefined') {
|
|
61
|
+
await import('tsx/esm')
|
|
62
|
+
await import('tsx/cjs')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
await import(resolvedPath)
|
|
66
|
+
|
|
67
|
+
const registry = getRegistry()
|
|
68
|
+
const results: TestResult[] = []
|
|
69
|
+
|
|
70
|
+
// Shared unhandled error trap for this file's test run
|
|
71
|
+
let currentTestName: string | null = null
|
|
72
|
+
let pendingUnhandled: Error | undefined
|
|
73
|
+
const onUnhandled = (reason: unknown) => {
|
|
74
|
+
if (!pendingUnhandled) pendingUnhandled = reason instanceof Error ? reason : new Error(String(reason))
|
|
75
|
+
}
|
|
76
|
+
process.on('unhandledRejection', onUnhandled)
|
|
77
|
+
process.on('uncaughtException', onUnhandled)
|
|
78
|
+
|
|
79
|
+
// 3. Run beforeAll hooks
|
|
80
|
+
for (const hook of registry.beforeAllHooks) {
|
|
81
|
+
await hook()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 4. Execute each test sequentially
|
|
85
|
+
for (const entry of registry.tests) {
|
|
86
|
+
const { context, finalise } = startTraceSession()
|
|
87
|
+
const traceId = proxyCtx.proxyOptIn ? randomUUID() : null
|
|
88
|
+
setCurrentTrace(context.trace)
|
|
89
|
+
if (traceId) {
|
|
90
|
+
process.env.ELASTICDASH_TRACE_ID = traceId
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (hooks.onTestStart) {
|
|
94
|
+
await hooks.onTestStart(entry.name)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const startTime = Date.now()
|
|
98
|
+
let passed = false
|
|
99
|
+
let error: Error | undefined
|
|
100
|
+
|
|
101
|
+
// Reset per-test unhandled capture and mark current test name
|
|
102
|
+
pendingUnhandled = undefined
|
|
103
|
+
currentTestName = entry.name
|
|
104
|
+
|
|
105
|
+
setCurrentTrace(context.trace)
|
|
106
|
+
try {
|
|
107
|
+
for (const hook of registry.beforeEachHooks) {
|
|
108
|
+
await hook()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
await entry.fn(context)
|
|
112
|
+
passed = true
|
|
113
|
+
} catch (err) {
|
|
114
|
+
error = err instanceof Error ? err : new Error(String(err))
|
|
115
|
+
} finally {
|
|
116
|
+
try {
|
|
117
|
+
for (const hook of registry.afterEachHooks) {
|
|
118
|
+
await hook()
|
|
119
|
+
}
|
|
120
|
+
} catch (afterErr) {
|
|
121
|
+
if (!error) {
|
|
122
|
+
error = afterErr instanceof Error ? afterErr : new Error(String(afterErr))
|
|
123
|
+
passed = false
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
setCurrentTrace(undefined)
|
|
128
|
+
if (!error && pendingUnhandled) {
|
|
129
|
+
error = pendingUnhandled
|
|
130
|
+
passed = false
|
|
131
|
+
}
|
|
132
|
+
currentTestName = null
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const durationMs = Date.now() - startTime
|
|
136
|
+
|
|
137
|
+
if (hooks.onTestFinish) {
|
|
138
|
+
await hooks.onTestFinish(entry.name, passed, durationMs, error)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (hooks.onTraceComplete) {
|
|
142
|
+
await hooks.onTraceComplete(entry.name, context.trace)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// If proxy mode is enabled, pull captured LLM steps and fold into the trace
|
|
146
|
+
if (traceId && proxyCtx.proxyUrl) {
|
|
147
|
+
try {
|
|
148
|
+
const captured = await fetchCapturedTrace(proxyCtx.proxyUrl, traceId)
|
|
149
|
+
for (const step of captured) {
|
|
150
|
+
context.trace.recordLLMStep(step)
|
|
151
|
+
}
|
|
152
|
+
} catch (proxyErr) {
|
|
153
|
+
// Non-fatal: keep test result as-is
|
|
154
|
+
// eslint-disable-next-line no-console
|
|
155
|
+
console.warn('[elasticdash] Failed to fetch proxy-captured steps:', proxyErr)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
finalise()
|
|
160
|
+
setCurrentTrace(undefined)
|
|
161
|
+
if (traceId) {
|
|
162
|
+
delete process.env.ELASTICDASH_TRACE_ID
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
results.push({ name: entry.name, passed, durationMs, error })
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// 5. Run afterAll hooks
|
|
169
|
+
for (const hook of registry.afterAllHooks) {
|
|
170
|
+
await hook()
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Cleanup shared handlers
|
|
174
|
+
process.off('unhandledRejection', onUnhandled)
|
|
175
|
+
process.off('uncaughtException', onUnhandled)
|
|
176
|
+
|
|
177
|
+
return { file, results }
|
|
178
|
+
}
|