waypoi 0.0.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/.github/instructions/ui.instructions.md +42 -0
- package/.github/workflows/ci.yml +35 -0
- package/.github/workflows/publish.yml +71 -0
- package/.github/workflows/release.yml +48 -0
- package/.playwright-mcp/console-2026-04-04T01-41-10-746Z.log +2 -0
- package/.playwright-mcp/console-2026-04-04T01-41-28-799Z.log +3 -0
- package/.playwright-mcp/console-2026-04-05T02-26-51-909Z.log +76 -0
- package/.playwright-mcp/page-2026-04-04T01-41-10-816Z.yml +1 -0
- package/.playwright-mcp/page-2026-04-04T01-41-29-141Z.yml +77 -0
- package/.playwright-mcp/page-2026-04-04T01-41-42-633Z.yml +190 -0
- package/.playwright-mcp/page-2026-04-04T01-42-03-929Z.yml +262 -0
- package/.playwright-mcp/page-2026-04-04T02-12-54-813Z.yml +6 -0
- package/.playwright-mcp/page-2026-04-04T02-14-58-600Z.yml +190 -0
- package/.playwright-mcp/page-2026-04-04T02-15-03-923Z.yml +190 -0
- package/.playwright-mcp/page-2026-04-04T02-15-07-426Z.yml +190 -0
- package/.playwright-mcp/page-2026-04-04T02-15-25-729Z.yml +262 -0
- package/.playwright-mcp/page-2026-04-04T02-16-22-984Z.yml +262 -0
- package/.playwright-mcp/page-2026-04-04T02-17-00-599Z.yml +190 -0
- package/.playwright-mcp/page-2026-04-04T02-17-50-874Z.yml +190 -0
- package/.playwright-mcp/page-2026-04-05T02-26-55-570Z.yml +6 -0
- package/AGENTS.md +48 -0
- package/CHANGELOG.md +131 -0
- package/README.md +552 -0
- package/assets/agent-mode.png +0 -0
- package/assets/categorize.png +0 -0
- package/assets/dashboard.png +0 -0
- package/assets/endpoint-proxy.png +0 -0
- package/assets/icon.png +0 -0
- package/assets/mcp-generate-image.png +0 -0
- package/assets/mcp-understand-image.png +0 -0
- package/assets/peek-token-flow.png +0 -0
- package/assets/playground.png +0 -0
- package/assets/sankey.png +0 -0
- package/cli/index.ts +2805 -0
- package/cli/legacyRewrite.ts +108 -0
- package/cli/modelRef.ts +24 -0
- package/dist/cli/index.js +2536 -0
- package/dist/cli/legacyRewrite.js +92 -0
- package/dist/cli/modelRef.js +20 -0
- package/dist/src/benchmark/artifacts.js +131 -0
- package/dist/src/benchmark/capabilityClassifier.js +81 -0
- package/dist/src/benchmark/capabilityStore.js +144 -0
- package/dist/src/benchmark/config.js +238 -0
- package/dist/src/benchmark/gates.js +118 -0
- package/dist/src/benchmark/jobs.js +252 -0
- package/dist/src/benchmark/runner.js +1847 -0
- package/dist/src/benchmark/schema.js +353 -0
- package/dist/src/benchmark/suites.js +314 -0
- package/dist/src/benchmark/tinyQaDataset.js +422 -0
- package/dist/src/benchmark/types.js +25 -0
- package/dist/src/config.js +47 -0
- package/dist/src/index.js +178 -0
- package/dist/src/mcp/client.js +215 -0
- package/dist/src/mcp/discovery.js +226 -0
- package/dist/src/mcp/policy.js +65 -0
- package/dist/src/mcp/registry.js +129 -0
- package/dist/src/mcp/service.js +460 -0
- package/dist/src/middleware/auth.js +179 -0
- package/dist/src/middleware/requestCapture.js +192 -0
- package/dist/src/middleware/requestStats.js +118 -0
- package/dist/src/pools/builder.js +132 -0
- package/dist/src/pools/repository.js +69 -0
- package/dist/src/pools/scheduler.js +360 -0
- package/dist/src/pools/types.js +2 -0
- package/dist/src/protocols/adapters/dashscope.js +267 -0
- package/dist/src/protocols/adapters/inferenceV2.js +346 -0
- package/dist/src/protocols/adapters/openai.js +27 -0
- package/dist/src/protocols/registry.js +99 -0
- package/dist/src/protocols/types.js +2 -0
- package/dist/src/providers/health.js +153 -0
- package/dist/src/providers/importer.js +289 -0
- package/dist/src/providers/modelRegistry.js +313 -0
- package/dist/src/providers/repository.js +361 -0
- package/dist/src/providers/types.js +2 -0
- package/dist/src/routes/admin.js +531 -0
- package/dist/src/routes/audio.js +295 -0
- package/dist/src/routes/chat.js +240 -0
- package/dist/src/routes/embeddings.js +157 -0
- package/dist/src/routes/images.js +288 -0
- package/dist/src/routes/mcp.js +256 -0
- package/dist/src/routes/mcpService.js +100 -0
- package/dist/src/routes/models.js +48 -0
- package/dist/src/routes/responses.js +711 -0
- package/dist/src/routes/sessions.js +450 -0
- package/dist/src/routes/stats.js +270 -0
- package/dist/src/routes/ui.js +97 -0
- package/dist/src/routes/videos.js +107 -0
- package/dist/src/routing/router.js +338 -0
- package/dist/src/services/imageGeneration.js +280 -0
- package/dist/src/services/imageUnderstanding.js +352 -0
- package/dist/src/services/videoGeneration.js +79 -0
- package/dist/src/storage/captureRepository.js +1591 -0
- package/dist/src/storage/files.js +157 -0
- package/dist/src/storage/imageCache.js +346 -0
- package/dist/src/storage/repositories.js +388 -0
- package/dist/src/storage/sessionRepository.js +370 -0
- package/dist/src/storage/statsRepository.js +204 -0
- package/dist/src/transport/httpClient.js +126 -0
- package/dist/src/types.js +2 -0
- package/dist/src/utils/messageMedia.js +285 -0
- package/dist/src/utils/modelCapabilities.js +108 -0
- package/dist/src/utils/modelDiscovery.js +170 -0
- package/dist/src/version.js +5 -0
- package/dist/src/workers/captureRetention.js +25 -0
- package/dist/src/workers/configWatcher.js +91 -0
- package/dist/src/workers/healthChecker.js +21 -0
- package/dist/src/workers/statsRotation.js +41 -0
- package/docs/LLM/output_schema.md +312 -0
- package/docs/benchmark.md +208 -0
- package/docs/mcp-guidelines.md +125 -0
- package/docs/mcp-service.md +178 -0
- package/docs/opencode.md +86 -0
- package/docs/providers.md +79 -0
- package/examples/benchmark.config.yaml +28 -0
- package/examples/providers/alibaba-dashscope.yaml +88 -0
- package/examples/providers/alibaba-llm.yaml +64 -0
- package/examples/providers/alibaba-registry.yaml +7 -0
- package/examples/providers/inference-v2-ray.yaml +29 -0
- package/examples/scenarios/assets/omni-call-sample.wav +0 -0
- package/examples/scenarios/custom.jsonl +5 -0
- package/examples/scenarios/custom.yaml +40 -0
- package/model-form-v2.png +0 -0
- package/package.json +66 -0
- package/provider-form-v2.png +0 -0
- package/provider-form.png +0 -0
- package/scripts/manual-test.sh +11 -0
- package/scripts/version-from-git.js +23 -0
- package/src/benchmark/artifacts.ts +149 -0
- package/src/benchmark/capabilityClassifier.ts +99 -0
- package/src/benchmark/capabilityStore.ts +174 -0
- package/src/benchmark/config.ts +337 -0
- package/src/benchmark/gates.ts +164 -0
- package/src/benchmark/jobs.ts +312 -0
- package/src/benchmark/runner.ts +2519 -0
- package/src/benchmark/schema.ts +443 -0
- package/src/benchmark/suites.ts +323 -0
- package/src/benchmark/tinyQaDataset.ts +428 -0
- package/src/benchmark/types.ts +442 -0
- package/src/config.ts +44 -0
- package/src/index.ts +195 -0
- package/src/mcp/client.ts +305 -0
- package/src/mcp/discovery.ts +266 -0
- package/src/mcp/policy.ts +105 -0
- package/src/mcp/registry.ts +164 -0
- package/src/mcp/service.ts +611 -0
- package/src/middleware/auth.ts +251 -0
- package/src/middleware/requestCapture.ts +245 -0
- package/src/middleware/requestStats.ts +163 -0
- package/src/pools/builder.ts +159 -0
- package/src/pools/repository.ts +71 -0
- package/src/pools/scheduler.ts +425 -0
- package/src/pools/types.ts +117 -0
- package/src/protocols/adapters/dashscope.ts +335 -0
- package/src/protocols/adapters/inferenceV2.ts +428 -0
- package/src/protocols/adapters/openai.ts +32 -0
- package/src/protocols/registry.ts +117 -0
- package/src/protocols/types.ts +81 -0
- package/src/providers/health.ts +207 -0
- package/src/providers/importer.ts +402 -0
- package/src/providers/modelRegistry.ts +415 -0
- package/src/providers/repository.ts +439 -0
- package/src/providers/types.ts +113 -0
- package/src/routes/admin.ts +666 -0
- package/src/routes/audio.ts +372 -0
- package/src/routes/chat.ts +301 -0
- package/src/routes/embeddings.ts +197 -0
- package/src/routes/images.ts +356 -0
- package/src/routes/mcp.ts +320 -0
- package/src/routes/mcpService.ts +114 -0
- package/src/routes/models.ts +50 -0
- package/src/routes/responses.ts +872 -0
- package/src/routes/sessions.ts +558 -0
- package/src/routes/stats.ts +312 -0
- package/src/routes/ui.ts +96 -0
- package/src/routes/videos.ts +132 -0
- package/src/routing/router.ts +501 -0
- package/src/services/imageGeneration.ts +396 -0
- package/src/services/imageUnderstanding.ts +449 -0
- package/src/services/videoGeneration.ts +127 -0
- package/src/storage/captureRepository.ts +1835 -0
- package/src/storage/files.ts +178 -0
- package/src/storage/imageCache.ts +405 -0
- package/src/storage/repositories.ts +494 -0
- package/src/storage/sessionRepository.ts +419 -0
- package/src/storage/statsRepository.ts +238 -0
- package/src/transport/httpClient.ts +145 -0
- package/src/types.ts +322 -0
- package/src/utils/messageMedia.ts +293 -0
- package/src/utils/modelCapabilities.ts +161 -0
- package/src/utils/modelDiscovery.ts +203 -0
- package/src/workers/captureRetention.ts +25 -0
- package/src/workers/configWatcher.ts +115 -0
- package/src/workers/healthChecker.ts +22 -0
- package/src/workers/statsRotation.ts +49 -0
- package/tests/benchmarkAdminRoutes.test.ts +82 -0
- package/tests/benchmarkBasics.test.ts +116 -0
- package/tests/captureAdminRoutes.test.ts +420 -0
- package/tests/captureRepository.test.ts +797 -0
- package/tests/cliLegacyRewrite.test.ts +45 -0
- package/tests/imageGeneration.service.test.ts +107 -0
- package/tests/imageUnderstanding.service.test.ts +123 -0
- package/tests/mcpPolicy.test.ts +105 -0
- package/tests/mcpService.test.ts +1245 -0
- package/tests/modelRef.test.ts +23 -0
- package/tests/modelsRoutes.test.ts +154 -0
- package/tests/sessionMediaCache.test.ts +167 -0
- package/tests/statsRoutes.test.ts +323 -0
- package/tsconfig.json +15 -0
- package/ui/index.html +16 -0
- package/ui/package-lock.json +8521 -0
- package/ui/package.json +52 -0
- package/ui/postcss.config.js +6 -0
- package/ui/public/assets/apple-touch-icon.png +0 -0
- package/ui/public/assets/favicon-16.png +0 -0
- package/ui/public/assets/favicon-32.png +0 -0
- package/ui/public/assets/icon-192.png +0 -0
- package/ui/public/assets/icon-512.png +0 -0
- package/ui/src/App.tsx +27 -0
- package/ui/src/api/client.ts +1503 -0
- package/ui/src/components/EndpointUsageGuide.tsx +361 -0
- package/ui/src/components/Layout.tsx +124 -0
- package/ui/src/components/MessageContent.tsx +365 -0
- package/ui/src/components/ToolCallMessage.tsx +179 -0
- package/ui/src/components/ToolPicker.tsx +442 -0
- package/ui/src/components/messageContentParser.test.ts +41 -0
- package/ui/src/components/messageContentParser.ts +73 -0
- package/ui/src/components/thinkingPreview.test.ts +27 -0
- package/ui/src/components/thinkingPreview.ts +15 -0
- package/ui/src/components/toMermaidSankey.test.ts +78 -0
- package/ui/src/components/toMermaidSankey.ts +56 -0
- package/ui/src/components/ui/button.tsx +58 -0
- package/ui/src/components/ui/input.tsx +21 -0
- package/ui/src/components/ui/textarea.tsx +21 -0
- package/ui/src/lib/utils.ts +6 -0
- package/ui/src/main.tsx +9 -0
- package/ui/src/pages/AgentPlayground.tsx +2010 -0
- package/ui/src/pages/Benchmark.tsx +988 -0
- package/ui/src/pages/Dashboard.tsx +581 -0
- package/ui/src/pages/Peek.tsx +962 -0
- package/ui/src/pages/Settings.tsx +2013 -0
- package/ui/src/pages/agentPlaygroundPayload.test.ts +109 -0
- package/ui/src/pages/agentPlaygroundPayload.ts +97 -0
- package/ui/src/pages/agentThinkingContent.test.ts +50 -0
- package/ui/src/pages/agentThinkingContent.ts +57 -0
- package/ui/src/pages/dashboardTokenUsage.test.ts +66 -0
- package/ui/src/pages/dashboardTokenUsage.ts +36 -0
- package/ui/src/pages/imageUpload.test.ts +39 -0
- package/ui/src/pages/imageUpload.ts +71 -0
- package/ui/src/pages/peekFilters.test.ts +29 -0
- package/ui/src/pages/peekFilters.ts +13 -0
- package/ui/src/pages/peekMedia.test.ts +58 -0
- package/ui/src/pages/peekMedia.ts +148 -0
- package/ui/src/pages/sessionAutoTitle.test.ts +128 -0
- package/ui/src/pages/sessionAutoTitle.ts +106 -0
- package/ui/src/stores/settings.ts +58 -0
- package/ui/src/styles/globals.css +223 -0
- package/ui/src/vite-env.d.ts +8 -0
- package/ui/tailwind.config.js +106 -0
- package/ui/tsconfig.json +32 -0
- package/ui/vite.config.ts +37 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.resolveBenchmarkConfig = resolveBenchmarkConfig;
|
|
7
|
+
const fs_1 = require("fs");
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const yaml_1 = __importDefault(require("yaml"));
|
|
10
|
+
const DEFAULT_VERSION = 1;
|
|
11
|
+
const DEFAULT_CAP_TTL_DAYS = 7;
|
|
12
|
+
const DEFAULTS = {
|
|
13
|
+
requestTimeoutMs: 120000,
|
|
14
|
+
toolTimeoutMs: 15000,
|
|
15
|
+
maxIterations: 6,
|
|
16
|
+
temperature: 0,
|
|
17
|
+
top_p: 1,
|
|
18
|
+
max_tokens: 512,
|
|
19
|
+
presence_penalty: 0,
|
|
20
|
+
frequency_penalty: 0,
|
|
21
|
+
};
|
|
22
|
+
const DEFAULT_PROFILES = {
|
|
23
|
+
local: {
|
|
24
|
+
warmupRuns: 1,
|
|
25
|
+
measuredRuns: 3,
|
|
26
|
+
minScenarioPassRate: 1.0,
|
|
27
|
+
},
|
|
28
|
+
ci: {
|
|
29
|
+
warmupRuns: 2,
|
|
30
|
+
measuredRuns: 5,
|
|
31
|
+
minScenarioPassRate: 1.0,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
const DEFAULT_GATES = {
|
|
35
|
+
hard: {
|
|
36
|
+
smokeMinSuccessRate: 1.0,
|
|
37
|
+
},
|
|
38
|
+
soft: {
|
|
39
|
+
maxP95RegressionPct: 20,
|
|
40
|
+
maxThroughputDropPct: 20,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
async function resolveBenchmarkConfig(paths, cli) {
|
|
44
|
+
const { fileConfig, configSource } = await loadConfigFile(paths, cli.configPath);
|
|
45
|
+
const mergedDefaults = {
|
|
46
|
+
...DEFAULTS,
|
|
47
|
+
...(fileConfig?.defaults ?? {}),
|
|
48
|
+
};
|
|
49
|
+
const mergedProfiles = {
|
|
50
|
+
...DEFAULT_PROFILES,
|
|
51
|
+
};
|
|
52
|
+
for (const [profileName, profilePatch] of Object.entries(fileConfig?.profiles ?? {})) {
|
|
53
|
+
mergedProfiles[profileName] = {
|
|
54
|
+
...(mergedProfiles[profileName] ?? DEFAULT_PROFILES.local),
|
|
55
|
+
...profilePatch,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const selectedProfile = cli.profile ?? fileConfig?.run?.profile ?? "local";
|
|
59
|
+
const profileSettings = mergedProfiles[selectedProfile];
|
|
60
|
+
if (!profileSettings) {
|
|
61
|
+
const names = Object.keys(mergedProfiles).sort().join(", ");
|
|
62
|
+
throw new Error(`Unknown benchmark profile '${selectedProfile}'. Available profiles: ${names}`);
|
|
63
|
+
}
|
|
64
|
+
const mergedGates = {
|
|
65
|
+
hard: {
|
|
66
|
+
...DEFAULT_GATES.hard,
|
|
67
|
+
...(fileConfig?.gates?.hard ?? {}),
|
|
68
|
+
},
|
|
69
|
+
soft: {
|
|
70
|
+
...DEFAULT_GATES.soft,
|
|
71
|
+
...(fileConfig?.gates?.soft ?? {}),
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
const resolved = {
|
|
75
|
+
version: fileConfig?.version ?? DEFAULT_VERSION,
|
|
76
|
+
profile: selectedProfile,
|
|
77
|
+
defaults: validateDefaults(mergedDefaults),
|
|
78
|
+
profileSettings: validateProfileSettings(profileSettings, selectedProfile),
|
|
79
|
+
gates: validateGates(mergedGates),
|
|
80
|
+
run: {
|
|
81
|
+
suite: cli.suite ?? fileConfig?.run?.suite ?? "showcase",
|
|
82
|
+
exampleId: cli.exampleId ?? fileConfig?.run?.exampleId,
|
|
83
|
+
scenarioPath: cli.scenarioPath ?? fileConfig?.run?.scenarioPath,
|
|
84
|
+
modelOverride: cli.modelOverride ?? fileConfig?.run?.model,
|
|
85
|
+
outPath: cli.outPath ?? fileConfig?.run?.outPath,
|
|
86
|
+
baselinePath: cli.baselinePath ?? fileConfig?.run?.baselinePath,
|
|
87
|
+
executionMode: resolveExecutionMode(cli, fileConfig),
|
|
88
|
+
listExamples: cli.listExamples ?? fileConfig?.run?.listExamples ?? false,
|
|
89
|
+
updateCapCache: cli.updateCapCache ?? fileConfig?.run?.updateCapCache ?? false,
|
|
90
|
+
capTtlDays: intField(cli.capTtlDays ?? fileConfig?.run?.capTtlDays ?? DEFAULT_CAP_TTL_DAYS, "run.capTtlDays", 1),
|
|
91
|
+
temperature: optionalNumberField(cli.temperature ?? fileConfig?.run?.temperature, "run.temperature"),
|
|
92
|
+
top_p: optionalBoundedField(cli.top_p ?? fileConfig?.run?.top_p, "run.top_p", 0, 1),
|
|
93
|
+
max_tokens: optionalIntField(cli.max_tokens ?? fileConfig?.run?.max_tokens, "run.max_tokens", 1),
|
|
94
|
+
presence_penalty: optionalBoundedField(cli.presence_penalty ?? fileConfig?.run?.presence_penalty, "run.presence_penalty", -2, 2),
|
|
95
|
+
frequency_penalty: optionalBoundedField(cli.frequency_penalty ?? fileConfig?.run?.frequency_penalty, "run.frequency_penalty", -2, 2),
|
|
96
|
+
seed: optionalIntField(cli.seed ?? fileConfig?.run?.seed, "run.seed", 0),
|
|
97
|
+
stop: optionalStopField(cli.stop ?? fileConfig?.run?.stop, "run.stop"),
|
|
98
|
+
},
|
|
99
|
+
configSource,
|
|
100
|
+
};
|
|
101
|
+
return resolved;
|
|
102
|
+
}
|
|
103
|
+
async function loadConfigFile(paths, explicitPath) {
|
|
104
|
+
const candidatePath = explicitPath
|
|
105
|
+
? path_1.default.resolve(explicitPath)
|
|
106
|
+
: path_1.default.join(paths.baseDir, "benchmark.config.yaml");
|
|
107
|
+
try {
|
|
108
|
+
const raw = await fs_1.promises.readFile(candidatePath, "utf8");
|
|
109
|
+
const parsed = parseConfigDocument(candidatePath, raw);
|
|
110
|
+
return { fileConfig: parsed, configSource: candidatePath };
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
const code = error.code;
|
|
114
|
+
if (code === "ENOENT") {
|
|
115
|
+
if (explicitPath) {
|
|
116
|
+
throw new Error(`Benchmark config not found: ${candidatePath}`);
|
|
117
|
+
}
|
|
118
|
+
return {};
|
|
119
|
+
}
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function parseConfigDocument(filePath, raw) {
|
|
124
|
+
const ext = path_1.default.extname(filePath).toLowerCase();
|
|
125
|
+
try {
|
|
126
|
+
if (ext === ".json") {
|
|
127
|
+
return JSON.parse(raw);
|
|
128
|
+
}
|
|
129
|
+
return yaml_1.default.parse(raw);
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
throw new Error(`Failed to parse benchmark config ${filePath}: ${error.message}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function validateDefaults(defaults) {
|
|
136
|
+
return {
|
|
137
|
+
requestTimeoutMs: intField(defaults.requestTimeoutMs, "defaults.requestTimeoutMs", 1),
|
|
138
|
+
toolTimeoutMs: intField(defaults.toolTimeoutMs, "defaults.toolTimeoutMs", 1),
|
|
139
|
+
maxIterations: intField(defaults.maxIterations, "defaults.maxIterations", 1),
|
|
140
|
+
temperature: numberField(defaults.temperature, "defaults.temperature"),
|
|
141
|
+
top_p: boundedField(defaults.top_p, "defaults.top_p", 0, 1),
|
|
142
|
+
max_tokens: intField(defaults.max_tokens, "defaults.max_tokens", 1),
|
|
143
|
+
presence_penalty: boundedField(defaults.presence_penalty, "defaults.presence_penalty", -2, 2),
|
|
144
|
+
frequency_penalty: boundedField(defaults.frequency_penalty, "defaults.frequency_penalty", -2, 2),
|
|
145
|
+
seed: optionalIntField(defaults.seed, "defaults.seed", 0),
|
|
146
|
+
stop: optionalStopField(defaults.stop, "defaults.stop"),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function resolveExecutionMode(cli, fileConfig) {
|
|
150
|
+
const explicit = cli.executionMode ?? fileConfig?.run?.executionMode;
|
|
151
|
+
if (explicit === "showcase" || explicit === "diagnostic") {
|
|
152
|
+
return explicit;
|
|
153
|
+
}
|
|
154
|
+
const suite = cli.suite ?? fileConfig?.run?.suite ?? "showcase";
|
|
155
|
+
return suite === "showcase" ? "showcase" : "diagnostic";
|
|
156
|
+
}
|
|
157
|
+
function validateProfileSettings(profile, profileName) {
|
|
158
|
+
return {
|
|
159
|
+
warmupRuns: intField(profile.warmupRuns, `profiles.${profileName}.warmupRuns`, 0),
|
|
160
|
+
measuredRuns: intField(profile.measuredRuns, `profiles.${profileName}.measuredRuns`, 1),
|
|
161
|
+
minScenarioPassRate: boundedField(profile.minScenarioPassRate, `profiles.${profileName}.minScenarioPassRate`, 0, 1),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function validateGates(gates) {
|
|
165
|
+
return {
|
|
166
|
+
hard: {
|
|
167
|
+
smokeMinSuccessRate: boundedField(gates.hard.smokeMinSuccessRate, "gates.hard.smokeMinSuccessRate", 0, 1),
|
|
168
|
+
},
|
|
169
|
+
soft: {
|
|
170
|
+
maxP95RegressionPct: numberField(gates.soft.maxP95RegressionPct, "gates.soft.maxP95RegressionPct", 0),
|
|
171
|
+
maxThroughputDropPct: numberField(gates.soft.maxThroughputDropPct, "gates.soft.maxThroughputDropPct", 0),
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function intField(value, field, min) {
|
|
176
|
+
if (!Number.isInteger(value) || value < min) {
|
|
177
|
+
throw new Error(`${field} must be an integer >= ${min}`);
|
|
178
|
+
}
|
|
179
|
+
return value;
|
|
180
|
+
}
|
|
181
|
+
function numberField(value, field, min) {
|
|
182
|
+
if (!Number.isFinite(value)) {
|
|
183
|
+
throw new Error(`${field} must be a finite number`);
|
|
184
|
+
}
|
|
185
|
+
if (typeof min === "number" && value < min) {
|
|
186
|
+
throw new Error(`${field} must be >= ${min}`);
|
|
187
|
+
}
|
|
188
|
+
return value;
|
|
189
|
+
}
|
|
190
|
+
function boundedField(value, field, min, max) {
|
|
191
|
+
if (!Number.isFinite(value) || value < min || value > max) {
|
|
192
|
+
throw new Error(`${field} must be between ${min} and ${max}`);
|
|
193
|
+
}
|
|
194
|
+
return value;
|
|
195
|
+
}
|
|
196
|
+
function optionalIntField(value, field, min) {
|
|
197
|
+
if (value === undefined)
|
|
198
|
+
return undefined;
|
|
199
|
+
return intField(value, field, min);
|
|
200
|
+
}
|
|
201
|
+
function optionalNumberField(value, field, min) {
|
|
202
|
+
if (value === undefined)
|
|
203
|
+
return undefined;
|
|
204
|
+
return numberField(value, field, min);
|
|
205
|
+
}
|
|
206
|
+
function optionalBoundedField(value, field, min, max) {
|
|
207
|
+
if (value === undefined)
|
|
208
|
+
return undefined;
|
|
209
|
+
return boundedField(value, field, min, max);
|
|
210
|
+
}
|
|
211
|
+
function optionalStopField(value, field) {
|
|
212
|
+
if (value === undefined)
|
|
213
|
+
return undefined;
|
|
214
|
+
if (typeof value === "string") {
|
|
215
|
+
const trimmed = value.trim();
|
|
216
|
+
if (trimmed.length === 0) {
|
|
217
|
+
throw new Error(`${field} must not be empty`);
|
|
218
|
+
}
|
|
219
|
+
return trimmed;
|
|
220
|
+
}
|
|
221
|
+
if (Array.isArray(value)) {
|
|
222
|
+
if (value.length === 0) {
|
|
223
|
+
throw new Error(`${field} must include at least one stop sequence`);
|
|
224
|
+
}
|
|
225
|
+
const normalized = value.map((item, index) => {
|
|
226
|
+
if (typeof item !== "string") {
|
|
227
|
+
throw new Error(`${field}[${index}] must be a string`);
|
|
228
|
+
}
|
|
229
|
+
const trimmed = item.trim();
|
|
230
|
+
if (trimmed.length === 0) {
|
|
231
|
+
throw new Error(`${field}[${index}] must not be empty`);
|
|
232
|
+
}
|
|
233
|
+
return trimmed;
|
|
234
|
+
});
|
|
235
|
+
return normalized;
|
|
236
|
+
}
|
|
237
|
+
throw new Error(`${field} must be a string or string[]`);
|
|
238
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.evaluateGates = evaluateGates;
|
|
4
|
+
const fs_1 = require("fs");
|
|
5
|
+
async function evaluateGates(report, effective) {
|
|
6
|
+
const hardMessages = [];
|
|
7
|
+
const softMessages = [];
|
|
8
|
+
if (effective.run.suite === "smoke") {
|
|
9
|
+
const min = effective.gates.hard.smokeMinSuccessRate;
|
|
10
|
+
if (report.executed > 0 && report.successRate < min) {
|
|
11
|
+
hardMessages.push(`Smoke suite success rate ${toPct(report.successRate)} is below required ${toPct(min)}.`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const minScenarioPassRate = effective.profileSettings.minScenarioPassRate;
|
|
15
|
+
const failingScenarios = report.results.filter((scenario) => scenario.status !== "skipped" && scenario.passRate < minScenarioPassRate);
|
|
16
|
+
for (const scenario of failingScenarios) {
|
|
17
|
+
hardMessages.push(`Scenario '${scenario.id}' pass rate ${toPct(scenario.passRate)} is below required ${toPct(minScenarioPassRate)}.`);
|
|
18
|
+
}
|
|
19
|
+
if (effective.run.baselinePath) {
|
|
20
|
+
const baseline = await loadBaseline(effective.run.baselinePath);
|
|
21
|
+
const baselineById = new Map(baseline.results
|
|
22
|
+
.map((scenario) => normalizeBaselineScenario(scenario))
|
|
23
|
+
.filter((scenario) => scenario !== null)
|
|
24
|
+
.map((scenario) => [scenario.id, scenario]));
|
|
25
|
+
for (const current of report.results) {
|
|
26
|
+
if (current.status === "skipped") {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const ref = baselineById.get(current.id);
|
|
30
|
+
if (!ref) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const maxP95 = effective.gates.soft.maxP95RegressionPct;
|
|
34
|
+
if (ref.p95LatencyMs > 0) {
|
|
35
|
+
const p95Threshold = ref.p95LatencyMs * (1 + maxP95 / 100);
|
|
36
|
+
if (current.p95LatencyMs > p95Threshold) {
|
|
37
|
+
softMessages.push(`Scenario '${current.id}' p95 latency regressed ${pctDelta(current.p95LatencyMs, ref.p95LatencyMs)} (${current.p95LatencyMs}ms vs baseline ${ref.p95LatencyMs}ms).`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const maxThroughputDrop = effective.gates.soft.maxThroughputDropPct;
|
|
41
|
+
if (ref.avgThroughputTokensPerSec > 0) {
|
|
42
|
+
const throughputThreshold = ref.avgThroughputTokensPerSec * (1 - maxThroughputDrop / 100);
|
|
43
|
+
if (current.avgThroughputTokensPerSec < throughputThreshold) {
|
|
44
|
+
softMessages.push(`Scenario '${current.id}' throughput dropped ${throughputDropPct(current.avgThroughputTokensPerSec, ref.avgThroughputTokensPerSec)} (${current.avgThroughputTokensPerSec.toFixed(2)} t/s vs baseline ${ref.avgThroughputTokensPerSec.toFixed(2)} t/s).`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
hard: {
|
|
51
|
+
passed: hardMessages.length === 0,
|
|
52
|
+
messages: hardMessages,
|
|
53
|
+
},
|
|
54
|
+
soft: {
|
|
55
|
+
passed: softMessages.length === 0,
|
|
56
|
+
messages: softMessages,
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
async function loadBaseline(pathLike) {
|
|
61
|
+
let raw;
|
|
62
|
+
try {
|
|
63
|
+
raw = await fs_1.promises.readFile(pathLike, "utf8");
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
throw new Error(`Failed to read baseline file '${pathLike}': ${error.message}`);
|
|
67
|
+
}
|
|
68
|
+
let parsed;
|
|
69
|
+
try {
|
|
70
|
+
parsed = JSON.parse(raw);
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
throw new Error(`Failed to parse baseline file '${pathLike}' as JSON: ${error.message}`);
|
|
74
|
+
}
|
|
75
|
+
if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.results)) {
|
|
76
|
+
throw new Error(`Baseline file '${pathLike}' must contain a top-level 'results' array.`);
|
|
77
|
+
}
|
|
78
|
+
return parsed;
|
|
79
|
+
}
|
|
80
|
+
function normalizeBaselineScenario(raw) {
|
|
81
|
+
if (!raw || typeof raw !== "object") {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
const scenario = raw;
|
|
85
|
+
if (typeof scenario.id !== "string") {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
const p95LatencyMs = typeof scenario.p95LatencyMs === "number" && Number.isFinite(scenario.p95LatencyMs)
|
|
89
|
+
? scenario.p95LatencyMs
|
|
90
|
+
: 0;
|
|
91
|
+
const avgThroughputTokensPerSec = typeof scenario.avgThroughputTokensPerSec === "number" &&
|
|
92
|
+
Number.isFinite(scenario.avgThroughputTokensPerSec)
|
|
93
|
+
? scenario.avgThroughputTokensPerSec
|
|
94
|
+
: 0;
|
|
95
|
+
return {
|
|
96
|
+
id: scenario.id,
|
|
97
|
+
p95LatencyMs,
|
|
98
|
+
avgThroughputTokensPerSec,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function toPct(value) {
|
|
102
|
+
return `${(value * 100).toFixed(1)}%`;
|
|
103
|
+
}
|
|
104
|
+
function pctDelta(current, baseline) {
|
|
105
|
+
if (baseline === 0) {
|
|
106
|
+
return "n/a";
|
|
107
|
+
}
|
|
108
|
+
const delta = ((current - baseline) / baseline) * 100;
|
|
109
|
+
const sign = delta >= 0 ? "+" : "";
|
|
110
|
+
return `${sign}${delta.toFixed(1)}%`;
|
|
111
|
+
}
|
|
112
|
+
function throughputDropPct(current, baseline) {
|
|
113
|
+
if (baseline === 0) {
|
|
114
|
+
return "n/a";
|
|
115
|
+
}
|
|
116
|
+
const delta = ((baseline - current) / baseline) * 100;
|
|
117
|
+
return `${delta.toFixed(1)}%`;
|
|
118
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.hasRunningBenchmarkRun = hasRunningBenchmarkRun;
|
|
7
|
+
exports.getBenchmarkRun = getBenchmarkRun;
|
|
8
|
+
exports.listBenchmarkRunEvents = listBenchmarkRunEvents;
|
|
9
|
+
exports.subscribeBenchmarkRunEvents = subscribeBenchmarkRunEvents;
|
|
10
|
+
exports.startBenchmarkRun = startBenchmarkRun;
|
|
11
|
+
exports.listBenchmarkRuns = listBenchmarkRuns;
|
|
12
|
+
exports.getArtifactBenchmarkRun = getArtifactBenchmarkRun;
|
|
13
|
+
const fs_1 = require("fs");
|
|
14
|
+
const path_1 = __importDefault(require("path"));
|
|
15
|
+
const events_1 = require("events");
|
|
16
|
+
const crypto_1 = require("crypto");
|
|
17
|
+
const runner_1 = require("./runner");
|
|
18
|
+
const runs = new Map();
|
|
19
|
+
const runEmitter = new events_1.EventEmitter();
|
|
20
|
+
function hasRunningBenchmarkRun() {
|
|
21
|
+
return Array.from(runs.values()).some((run) => run.status === "running");
|
|
22
|
+
}
|
|
23
|
+
function getBenchmarkRun(runId) {
|
|
24
|
+
return runs.get(runId);
|
|
25
|
+
}
|
|
26
|
+
function listBenchmarkRunEvents(runId) {
|
|
27
|
+
return [...(runs.get(runId)?.events ?? [])];
|
|
28
|
+
}
|
|
29
|
+
function subscribeBenchmarkRunEvents(runId, handler) {
|
|
30
|
+
const channel = eventChannel(runId);
|
|
31
|
+
runEmitter.on(channel, handler);
|
|
32
|
+
return () => {
|
|
33
|
+
runEmitter.off(channel, handler);
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
async function startBenchmarkRun(paths, request) {
|
|
37
|
+
const runId = (0, crypto_1.randomUUID)();
|
|
38
|
+
const now = new Date().toISOString();
|
|
39
|
+
const run = {
|
|
40
|
+
id: runId,
|
|
41
|
+
status: "running",
|
|
42
|
+
createdAt: now,
|
|
43
|
+
startedAt: now,
|
|
44
|
+
request,
|
|
45
|
+
progress: {
|
|
46
|
+
totalScenarios: 0,
|
|
47
|
+
completedScenarios: 0,
|
|
48
|
+
},
|
|
49
|
+
events: [],
|
|
50
|
+
};
|
|
51
|
+
runs.set(runId, run);
|
|
52
|
+
void executeBenchmarkRun(paths, runId);
|
|
53
|
+
return run;
|
|
54
|
+
}
|
|
55
|
+
async function listBenchmarkRuns(paths) {
|
|
56
|
+
const inMemory = Array.from(runs.values()).map(toSummary);
|
|
57
|
+
const artifactBacked = await listArtifactBackedRuns(paths);
|
|
58
|
+
const merged = new Map();
|
|
59
|
+
for (const item of artifactBacked) {
|
|
60
|
+
merged.set(item.id, item);
|
|
61
|
+
}
|
|
62
|
+
for (const item of inMemory) {
|
|
63
|
+
merged.set(item.id, item);
|
|
64
|
+
}
|
|
65
|
+
return Array.from(merged.values()).sort((a, b) => {
|
|
66
|
+
const aTs = Date.parse(a.createdAt || a.startedAt || a.finishedAt || "");
|
|
67
|
+
const bTs = Date.parse(b.createdAt || b.startedAt || b.finishedAt || "");
|
|
68
|
+
return bTs - aTs;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
async function getArtifactBenchmarkRun(paths, runId) {
|
|
72
|
+
const benchmarksDir = path_1.default.join(paths.baseDir, "benchmarks");
|
|
73
|
+
let files = [];
|
|
74
|
+
try {
|
|
75
|
+
files = await fs_1.promises.readdir(benchmarksDir);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
if (error.code === "ENOENT") {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
const jsonFiles = files
|
|
84
|
+
.filter((file) => file.startsWith("bench-") && file.endsWith(".json"))
|
|
85
|
+
.sort((a, b) => b.localeCompare(a));
|
|
86
|
+
for (const file of jsonFiles) {
|
|
87
|
+
const filePath = path_1.default.join(benchmarksDir, file);
|
|
88
|
+
try {
|
|
89
|
+
const raw = await fs_1.promises.readFile(filePath, "utf8");
|
|
90
|
+
const report = JSON.parse(raw);
|
|
91
|
+
if (report.id !== runId) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
id: report.id,
|
|
96
|
+
status: "completed",
|
|
97
|
+
createdAt: report.createdAt,
|
|
98
|
+
startedAt: report.createdAt,
|
|
99
|
+
finishedAt: report.createdAt,
|
|
100
|
+
request: {
|
|
101
|
+
suite: report.suite,
|
|
102
|
+
exampleId: report.exampleId,
|
|
103
|
+
scenarioPath: report.scenarioPath,
|
|
104
|
+
modelOverride: report.modelOverride,
|
|
105
|
+
profile: report.profile,
|
|
106
|
+
executionMode: report.executionMode,
|
|
107
|
+
},
|
|
108
|
+
progress: {
|
|
109
|
+
totalScenarios: report.total,
|
|
110
|
+
completedScenarios: report.total,
|
|
111
|
+
},
|
|
112
|
+
report,
|
|
113
|
+
artifactPath: filePath,
|
|
114
|
+
textArtifactPath: filePath.replace(/\.json$/i, ".txt"),
|
|
115
|
+
events: [],
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// Ignore malformed artifact.
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
async function executeBenchmarkRun(paths, runId) {
|
|
125
|
+
const run = runs.get(runId);
|
|
126
|
+
if (!run) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
const output = await (0, runner_1.runBenchmark)(paths, run.request, {
|
|
131
|
+
runId,
|
|
132
|
+
onEvent: (event) => {
|
|
133
|
+
updateProgress(run, event);
|
|
134
|
+
pushEvent(run, event);
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
run.status = "completed";
|
|
138
|
+
run.finishedAt = new Date().toISOString();
|
|
139
|
+
run.report = output.report;
|
|
140
|
+
run.artifactPath = output.artifactPath;
|
|
141
|
+
run.textArtifactPath = output.textArtifactPath;
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
run.status = "failed";
|
|
145
|
+
run.finishedAt = new Date().toISOString();
|
|
146
|
+
run.error = error.message;
|
|
147
|
+
const failureEvent = {
|
|
148
|
+
type: "warning",
|
|
149
|
+
timestamp: new Date().toISOString(),
|
|
150
|
+
runId,
|
|
151
|
+
warning: run.error,
|
|
152
|
+
};
|
|
153
|
+
pushEvent(run, failureEvent);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function updateProgress(run, event) {
|
|
157
|
+
if (event.type === "run_started") {
|
|
158
|
+
run.progress.totalScenarios = event.totalScenarios ?? run.progress.totalScenarios;
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (event.type === "scenario_started") {
|
|
162
|
+
run.progress.currentScenarioId = event.scenarioId;
|
|
163
|
+
run.progress.currentScenarioIndex = event.scenarioIndex;
|
|
164
|
+
run.progress.totalScenarios = event.totalScenarios ?? run.progress.totalScenarios;
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (event.type === "sample_completed") {
|
|
168
|
+
run.progress.currentRunIndex = event.runIndex;
|
|
169
|
+
run.progress.totalRuns = event.totalRuns;
|
|
170
|
+
run.progress.phase = event.phase;
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (event.type === "scenario_completed") {
|
|
174
|
+
run.progress.completedScenarios += 1;
|
|
175
|
+
run.progress.currentRunIndex = undefined;
|
|
176
|
+
run.progress.totalRuns = undefined;
|
|
177
|
+
run.progress.phase = undefined;
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (event.type === "run_completed") {
|
|
181
|
+
run.progress.completedScenarios = run.progress.totalScenarios;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function pushEvent(run, event) {
|
|
185
|
+
run.events.push(event);
|
|
186
|
+
if (run.events.length > 1000) {
|
|
187
|
+
run.events.splice(0, run.events.length - 1000);
|
|
188
|
+
}
|
|
189
|
+
runEmitter.emit(eventChannel(run.id), event);
|
|
190
|
+
}
|
|
191
|
+
function eventChannel(runId) {
|
|
192
|
+
return `benchmark:${runId}`;
|
|
193
|
+
}
|
|
194
|
+
function toSummary(run) {
|
|
195
|
+
return {
|
|
196
|
+
id: run.id,
|
|
197
|
+
status: run.status,
|
|
198
|
+
createdAt: run.createdAt,
|
|
199
|
+
startedAt: run.startedAt,
|
|
200
|
+
finishedAt: run.finishedAt,
|
|
201
|
+
suite: run.request.suite,
|
|
202
|
+
exampleId: run.request.exampleId,
|
|
203
|
+
profile: run.request.profile,
|
|
204
|
+
scenarioPath: run.request.scenarioPath,
|
|
205
|
+
succeeded: run.report?.succeeded,
|
|
206
|
+
failed: run.report?.failed,
|
|
207
|
+
successRate: run.report?.successRate,
|
|
208
|
+
artifactPath: run.artifactPath,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
async function listArtifactBackedRuns(paths) {
|
|
212
|
+
const benchmarksDir = path_1.default.join(paths.baseDir, "benchmarks");
|
|
213
|
+
let files = [];
|
|
214
|
+
try {
|
|
215
|
+
files = await fs_1.promises.readdir(benchmarksDir);
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
if (error.code === "ENOENT") {
|
|
219
|
+
return [];
|
|
220
|
+
}
|
|
221
|
+
throw error;
|
|
222
|
+
}
|
|
223
|
+
const jsonFiles = files
|
|
224
|
+
.filter((file) => file.startsWith("bench-") && file.endsWith(".json"))
|
|
225
|
+
.sort((a, b) => b.localeCompare(a))
|
|
226
|
+
.slice(0, 100);
|
|
227
|
+
const summaries = [];
|
|
228
|
+
for (const file of jsonFiles) {
|
|
229
|
+
const filePath = path_1.default.join(benchmarksDir, file);
|
|
230
|
+
try {
|
|
231
|
+
const raw = await fs_1.promises.readFile(filePath, "utf8");
|
|
232
|
+
const report = JSON.parse(raw);
|
|
233
|
+
summaries.push({
|
|
234
|
+
id: report.id,
|
|
235
|
+
status: "completed",
|
|
236
|
+
createdAt: report.createdAt,
|
|
237
|
+
finishedAt: report.createdAt,
|
|
238
|
+
suite: report.suite,
|
|
239
|
+
profile: report.profile,
|
|
240
|
+
scenarioPath: report.scenarioPath,
|
|
241
|
+
succeeded: report.succeeded,
|
|
242
|
+
failed: report.failed,
|
|
243
|
+
successRate: report.successRate,
|
|
244
|
+
artifactPath: filePath,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
// Skip malformed artifacts.
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return summaries;
|
|
252
|
+
}
|