viberadar 0.3.208 → 0.3.210

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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAO7B,OAAO,EAAE,UAAU,EAA4H,MAAM,YAAY,CAAC;AAOlK,UAAU,aAAa;IACrB,IAAI,EAAE,UAAU,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC;CACrB;AA0vED,wBAAgB,WAAW,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CA+gG1G"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAO7B,OAAO,EAAE,UAAU,EAA4H,MAAM,YAAY,CAAC;AAOlK,UAAU,aAAa;IACrB,IAAI,EAAE,UAAU,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC;CACrB;AA0vED,wBAAgB,WAAW,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CAuzG1G"}
@@ -2095,7 +2095,7 @@ function startServer({ data: initialData, port, projectRoot }) {
2095
2095
  let loadRunning = false;
2096
2096
  let loadProc = null;
2097
2097
  let loadState = {
2098
- status: 'idle', startTime: 0, buckets: [], totalRequests: 0,
2098
+ runId: null, status: 'idle', startTime: 0, buckets: [], totalRequests: 0,
2099
2099
  totalErrors: 0, logs: [], script: '', config: null, summary: null,
2100
2100
  };
2101
2101
  let probeRunning = false;
@@ -2184,36 +2184,222 @@ function startServer({ data: initialData, port, projectRoot }) {
2184
2184
  probeRunning = false;
2185
2185
  }
2186
2186
  }
2187
- function parseK6Dur(s) {
2188
- let m;
2189
- if ((m = s.match(/^([\d.]+)µs$/)))
2190
- return parseFloat(m[1]) / 1000;
2191
- if ((m = s.match(/^([\d.]+)ms$/)))
2192
- return parseFloat(m[1]);
2193
- if ((m = s.match(/^([\d.]+)s$/)))
2194
- return parseFloat(m[1]) * 1000;
2195
- if ((m = s.match(/^(\d+)m([\d.]+)s$/)))
2196
- return parseInt(m[1]) * 60000 + parseFloat(m[2]) * 1000;
2197
- return 0;
2187
+ const loadRunsDir = path.join(projectRoot, '.viberadar', 'load-runs');
2188
+ const MAX_LOAD_RUN_HISTORY = 50;
2189
+ function sanitizeLoadScriptName(name) {
2190
+ const raw = typeof name === 'string' && name.trim() ? name.trim() : 'Без названия';
2191
+ return raw.replace(/[^a-zA-Zа-яА-ЯёЁ0-9_\- .]/g, '_').slice(0, 80);
2198
2192
  }
2199
- function parseK6Summary(text) {
2200
- const s = {};
2201
- const dur = text.match(/http_req_duration[^:]*:\s+avg=([\w.µ]+)[^\n]*p\(90\)=([\w.µ]+)[^\n]*p\(95\)=([\w.µ]+)/);
2202
- if (dur) {
2203
- s.avgDuration = parseK6Dur(dur[1]);
2204
- s.p90Duration = parseK6Dur(dur[2]);
2205
- s.p95Duration = parseK6Dur(dur[3]);
2206
- }
2207
- const reqs = text.match(/\bhttp_reqs[^:]*:\s+(\d+)\s+([\d.]+)\/s/);
2208
- if (reqs) {
2209
- s.totalRequests = parseInt(reqs[1]);
2210
- s.rps = parseFloat(reqs[2]);
2211
- }
2212
- const fail = text.match(/http_req_failed[^:]*:\s+([\d.]+)%/);
2213
- if (fail)
2214
- s.errorPct = parseFloat(fail[1]);
2215
- return s;
2193
+ function normalizeLoadDuration(value) {
2194
+ const raw = typeof value === 'string' && value.trim() ? value.trim() : '30s';
2195
+ if (!/^(\d+(ms|s|m|h))+$/.test(raw))
2196
+ return '30s';
2197
+ return raw;
2216
2198
  }
2199
+ function normalizeLoadVus(value) {
2200
+ const n = typeof value === 'number' ? value : parseInt(String(value || '10'), 10);
2201
+ if (!Number.isFinite(n) || n < 1)
2202
+ return 10;
2203
+ return Math.min(Math.floor(n), 10000);
2204
+ }
2205
+ function sanitizeLoadEnvVars(value) {
2206
+ if (!value || typeof value !== 'object')
2207
+ return {};
2208
+ const out = {};
2209
+ for (const [key, val] of Object.entries(value)) {
2210
+ const envKey = key.trim();
2211
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(envKey))
2212
+ continue;
2213
+ if (val == null)
2214
+ continue;
2215
+ out[envKey] = String(val);
2216
+ }
2217
+ return out;
2218
+ }
2219
+ function redactLoadEnvVars(envVars) {
2220
+ const out = {};
2221
+ for (const key of Object.keys(envVars)) {
2222
+ out[key] = /token|secret|password|key/i.test(key) ? '***' : envVars[key];
2223
+ }
2224
+ return out;
2225
+ }
2226
+ function resolveLoadLocalPath(value) {
2227
+ if (typeof value !== 'string' || !value.trim())
2228
+ return undefined;
2229
+ const raw = value.trim();
2230
+ return path.resolve(path.isAbsolute(raw) ? raw : path.join(projectRoot, raw));
2231
+ }
2232
+ function copyLoadDataFiles(sourcePath, workDir) {
2233
+ if (!sourcePath || !fs.existsSync(sourcePath))
2234
+ return 0;
2235
+ const stat = fs.statSync(sourcePath);
2236
+ let copied = 0;
2237
+ const copyOne = (src, dst) => {
2238
+ const st = fs.statSync(src);
2239
+ if (st.isDirectory()) {
2240
+ fs.mkdirSync(dst, { recursive: true });
2241
+ for (const name of fs.readdirSync(src)) {
2242
+ if (name === '.git' || name === 'node_modules' || name === 'dist')
2243
+ continue;
2244
+ copyOne(path.join(src, name), path.join(dst, name));
2245
+ }
2246
+ return;
2247
+ }
2248
+ if (!st.isFile())
2249
+ return;
2250
+ fs.mkdirSync(path.dirname(dst), { recursive: true });
2251
+ fs.copyFileSync(src, dst);
2252
+ copied++;
2253
+ };
2254
+ if (stat.isDirectory()) {
2255
+ for (const name of fs.readdirSync(sourcePath)) {
2256
+ if (name === '.git' || name === 'node_modules' || name === 'dist')
2257
+ continue;
2258
+ copyOne(path.join(sourcePath, name), path.join(workDir, name));
2259
+ }
2260
+ }
2261
+ else if (stat.isFile()) {
2262
+ copyOne(sourcePath, path.join(workDir, path.basename(sourcePath)));
2263
+ }
2264
+ return copied;
2265
+ }
2266
+ function buildLoadConfig(cfg, paths) {
2267
+ const envVars = sanitizeLoadEnvVars(cfg.envVars);
2268
+ const baseUrl = typeof cfg.baseUrl === 'string' && cfg.baseUrl.trim() ? cfg.baseUrl.trim() : 'http://localhost:5000';
2269
+ const dataDir = resolveLoadLocalPath(cfg.dataDir);
2270
+ const resultDir = resolveLoadLocalPath(cfg.resultDir);
2271
+ envVars.BASE_URL = baseUrl;
2272
+ const config = {
2273
+ vus: normalizeLoadVus(cfg.vus),
2274
+ duration: normalizeLoadDuration(cfg.duration),
2275
+ baseUrl,
2276
+ scriptName: sanitizeLoadScriptName(cfg.scriptName),
2277
+ dataDir,
2278
+ resultDir,
2279
+ runDir: paths?.runDir,
2280
+ workDir: paths?.workDir,
2281
+ resultPath: paths?.resultPath,
2282
+ dataFilesCopied: paths?.dataFilesCopied,
2283
+ envVars: redactLoadEnvVars(envVars),
2284
+ };
2285
+ return { config, envVars, dataDir, resultDir };
2286
+ }
2287
+ function flattenK6Checks(group) {
2288
+ let passes = 0;
2289
+ let fails = 0;
2290
+ for (const check of group?.checks || []) {
2291
+ passes += Number(check.passes || 0);
2292
+ fails += Number(check.fails || 0);
2293
+ }
2294
+ for (const child of group?.groups || []) {
2295
+ const nested = flattenK6Checks(child);
2296
+ passes += nested.passes;
2297
+ fails += nested.fails;
2298
+ }
2299
+ return { passes, fails };
2300
+ }
2301
+ function normalizeK6Summary(raw, exitCode) {
2302
+ const metrics = raw?.metrics || {};
2303
+ const duration = metrics.http_req_duration?.values || {};
2304
+ const reqs = metrics.http_reqs?.values || {};
2305
+ const failed = metrics.http_req_failed?.values || {};
2306
+ const checks = flattenK6Checks(raw?.root_group);
2307
+ let thresholdsPassed = 0;
2308
+ let thresholdsFailed = 0;
2309
+ for (const metric of Object.values(metrics)) {
2310
+ for (const threshold of Object.values(metric?.thresholds || {})) {
2311
+ if (threshold?.ok === false)
2312
+ thresholdsFailed++;
2313
+ else if (threshold?.ok === true)
2314
+ thresholdsPassed++;
2315
+ }
2316
+ }
2317
+ return {
2318
+ totalRequests: typeof reqs.count === 'number' ? reqs.count : undefined,
2319
+ rps: typeof reqs.rate === 'number' ? reqs.rate : undefined,
2320
+ avgDuration: typeof duration.avg === 'number' ? duration.avg : undefined,
2321
+ p90Duration: typeof duration['p(90)'] === 'number' ? duration['p(90)'] : undefined,
2322
+ p95Duration: typeof duration['p(95)'] === 'number' ? duration['p(95)'] : undefined,
2323
+ p99Duration: typeof duration['p(99)'] === 'number' ? duration['p(99)'] : undefined,
2324
+ errorPct: typeof failed.rate === 'number' ? failed.rate * 100 : undefined,
2325
+ testRunDurationMs: typeof raw?.state?.testRunDurationMs === 'number' ? raw.state.testRunDurationMs : undefined,
2326
+ checksPassed: checks.passes,
2327
+ checksFailed: checks.fails,
2328
+ thresholdsPassed,
2329
+ thresholdsFailed,
2330
+ exitCode,
2331
+ };
2332
+ }
2333
+ function readK6Summary(summaryPath, exitCode) {
2334
+ try {
2335
+ if (!fs.existsSync(summaryPath))
2336
+ return { exitCode };
2337
+ return normalizeK6Summary(JSON.parse(fs.readFileSync(summaryPath, 'utf-8')), exitCode);
2338
+ }
2339
+ catch {
2340
+ return { exitCode };
2341
+ }
2342
+ }
2343
+ function readLoadRunIndex() {
2344
+ try {
2345
+ const p = path.join(loadRunsDir, 'index.json');
2346
+ const parsed = JSON.parse(fs.readFileSync(p, 'utf-8'));
2347
+ return Array.isArray(parsed) ? parsed : [];
2348
+ }
2349
+ catch {
2350
+ return [];
2351
+ }
2352
+ }
2353
+ function writeLoadRunIndex(items) {
2354
+ fs.mkdirSync(loadRunsDir, { recursive: true });
2355
+ fs.writeFileSync(path.join(loadRunsDir, 'index.json'), JSON.stringify(items, null, 2), 'utf-8');
2356
+ }
2357
+ function saveLoadRun() {
2358
+ if (!loadState.runId)
2359
+ return;
2360
+ fs.mkdirSync(loadRunsDir, { recursive: true });
2361
+ const record = {
2362
+ ...loadState,
2363
+ scriptName: loadState.config?.scriptName,
2364
+ createdAt: new Date(loadState.startTime || Date.now()).toISOString(),
2365
+ };
2366
+ fs.writeFileSync(path.join(loadRunsDir, `${loadState.runId}.json`), JSON.stringify(record, null, 2), 'utf-8');
2367
+ const index = readLoadRunIndex().filter(i => i.runId !== loadState.runId);
2368
+ index.unshift({
2369
+ runId: loadState.runId,
2370
+ scriptName: loadState.config?.scriptName || 'Без названия',
2371
+ createdAt: record.createdAt,
2372
+ status: loadState.status,
2373
+ startTime: loadState.startTime,
2374
+ endTime: loadState.endTime,
2375
+ config: loadState.config,
2376
+ summary: loadState.summary,
2377
+ });
2378
+ const compact = index.slice(0, MAX_LOAD_RUN_HISTORY);
2379
+ writeLoadRunIndex(compact);
2380
+ for (const old of index.slice(MAX_LOAD_RUN_HISTORY)) {
2381
+ try {
2382
+ fs.unlinkSync(path.join(loadRunsDir, `${old.runId}.json`));
2383
+ }
2384
+ catch { }
2385
+ try {
2386
+ fs.rmSync(path.join(loadRunsDir, old.runId), { recursive: true, force: true });
2387
+ }
2388
+ catch { }
2389
+ }
2390
+ }
2391
+ function loadLastRunIntoState() {
2392
+ const latest = readLoadRunIndex()[0];
2393
+ if (!latest)
2394
+ return;
2395
+ try {
2396
+ const record = JSON.parse(fs.readFileSync(path.join(loadRunsDir, `${latest.runId}.json`), 'utf-8'));
2397
+ if (record && typeof record === 'object')
2398
+ loadState = record;
2399
+ }
2400
+ catch { }
2401
+ }
2402
+ loadLastRunIntoState();
2217
2403
  // ── SSE clients ────────────────────────────────────────────────────────────
2218
2404
  const sseClients = new Set();
2219
2405
  function broadcast(event, payload = {}) {
@@ -4911,7 +5097,7 @@ a{color:var(--blue)}
4911
5097
  req.on('data', (d) => { body += d; });
4912
5098
  req.on('end', () => {
4913
5099
  try {
4914
- const { name, script } = JSON.parse(body);
5100
+ const { name, script, vus, duration, baseUrl, dataDir, resultDir } = JSON.parse(body);
4915
5101
  if (!name || !script) {
4916
5102
  res.writeHead(400, jsonH);
4917
5103
  res.end(JSON.stringify({ error: 'name and script required' }));
@@ -4921,7 +5107,17 @@ a{color:var(--blue)}
4921
5107
  const safeName = name.replace(/[^a-zA-Zа-яА-ЯёЁ0-9_\- ]/g, '_').slice(0, 80);
4922
5108
  const date = new Date().toISOString().slice(0, 16).replace('T', ' ');
4923
5109
  const fileName = `${Date.now()}-${safeName.replace(/\s+/g, '_')}.json`;
4924
- const entry = { name: safeName, date, script, fileName };
5110
+ const entry = {
5111
+ name: safeName,
5112
+ date,
5113
+ script,
5114
+ fileName,
5115
+ vus: normalizeLoadVus(vus),
5116
+ duration: normalizeLoadDuration(duration),
5117
+ baseUrl: typeof baseUrl === 'string' && baseUrl.trim() ? baseUrl.trim() : 'http://localhost:5000',
5118
+ dataDir: resolveLoadLocalPath(dataDir),
5119
+ resultDir: resolveLoadLocalPath(resultDir),
5120
+ };
4925
5121
  // overwrite if same name exists
4926
5122
  const existing = fs.readdirSync(scriptsDir).find(f => {
4927
5123
  try {
@@ -5011,6 +5207,31 @@ a{color:var(--blue)}
5011
5207
  res.end(JSON.stringify(loadState));
5012
5208
  return;
5013
5209
  }
5210
+ if (url === '/api/load/runs' && req.method === 'GET') {
5211
+ res.writeHead(200, jsonH);
5212
+ res.end(JSON.stringify(readLoadRunIndex()));
5213
+ return;
5214
+ }
5215
+ const loadRunMatch = url.match(/^\/api\/load\/runs\/([^/]+)$/);
5216
+ if (loadRunMatch && req.method === 'GET') {
5217
+ try {
5218
+ const runId = decodeURIComponent(loadRunMatch[1]);
5219
+ if (!/^[a-zA-Z0-9_-]+$/.test(runId)) {
5220
+ res.writeHead(400, jsonH);
5221
+ res.end(JSON.stringify({ error: 'Bad run id' }));
5222
+ return;
5223
+ }
5224
+ const runPath = path.join(loadRunsDir, `${runId}.json`);
5225
+ const record = JSON.parse(fs.readFileSync(runPath, 'utf-8'));
5226
+ res.writeHead(200, jsonH);
5227
+ res.end(JSON.stringify(record));
5228
+ }
5229
+ catch {
5230
+ res.writeHead(404, jsonH);
5231
+ res.end(JSON.stringify({ error: 'Run not found' }));
5232
+ }
5233
+ return;
5234
+ }
5014
5235
  if (url === '/api/load/stop' && req.method === 'POST') {
5015
5236
  if (loadProc) {
5016
5237
  try {
@@ -5024,7 +5245,7 @@ a{color:var(--blue)}
5024
5245
  loadState.status = 'stopped';
5025
5246
  loadState.endTime = Date.now();
5026
5247
  }
5027
- broadcast('load-done', { status: loadState.status, summary: loadState.summary });
5248
+ broadcast('load-done', { runId: loadState.runId, status: loadState.status, summary: loadState.summary });
5028
5249
  res.writeHead(200, jsonH);
5029
5250
  res.end(JSON.stringify({ ok: true }));
5030
5251
  return;
@@ -5053,9 +5274,20 @@ a{color:var(--blue)}
5053
5274
  res.end(JSON.stringify({ error: 'No script provided' }));
5054
5275
  return;
5055
5276
  }
5056
- const scriptPath = path.join(os.tmpdir(), `viberadar-k6-${Date.now()}.js`);
5057
- const jsonOutPath = path.join(os.tmpdir(), `viberadar-k6-out-${Date.now()}.ndjson`);
5277
+ const runId = `load-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
5278
+ const runDir = path.join(loadRunsDir, runId);
5279
+ const workDir = path.join(runDir, 'work');
5280
+ const defaultResultDir = path.join(runDir, 'results');
5281
+ const initial = buildLoadConfig(cfg);
5282
+ const resultPath = initial.resultDir ? path.join(initial.resultDir, runId) : defaultResultDir;
5283
+ const scriptPath = path.join(workDir, 'script.js');
5284
+ const jsonOutPath = path.join(resultPath, 'metrics.ndjson');
5285
+ const summaryPath = path.join(resultPath, 'summary.json');
5286
+ let dataFilesCopied = 0;
5058
5287
  try {
5288
+ fs.mkdirSync(workDir, { recursive: true });
5289
+ fs.mkdirSync(resultPath, { recursive: true });
5290
+ dataFilesCopied = copyLoadDataFiles(initial.dataDir, workDir);
5059
5291
  fs.writeFileSync(scriptPath, script, 'utf-8');
5060
5292
  }
5061
5293
  catch (e) {
@@ -5063,25 +5295,36 @@ a{color:var(--blue)}
5063
5295
  res.end(JSON.stringify({ error: e.message }));
5064
5296
  return;
5065
5297
  }
5298
+ const { config: loadConfig, envVars } = buildLoadConfig(cfg, {
5299
+ runDir,
5300
+ workDir,
5301
+ resultPath,
5302
+ dataFilesCopied,
5303
+ });
5066
5304
  loadRunning = true;
5067
5305
  loadState = {
5068
- status: 'running', startTime: Date.now(), buckets: [], totalRequests: 0,
5069
- totalErrors: 0, logs: [], script, config: cfg, summary: null,
5306
+ runId, status: 'running', startTime: Date.now(), buckets: [], totalRequests: 0,
5307
+ totalErrors: 0, logs: [], script, config: loadConfig, summary: null,
5070
5308
  };
5071
- broadcast('load-started', { config: cfg });
5309
+ broadcast('load-started', { runId, config: loadConfig });
5072
5310
  res.writeHead(200, jsonH);
5073
- res.end(JSON.stringify({ ok: true }));
5074
- // Build --env flags from cfg.envVars (e.g. { TOKEN: 'abc', BASE_URL: '...' })
5075
- const envVars = (typeof cfg.envVars === 'object' && cfg.envVars !== null)
5076
- ? cfg.envVars
5077
- : {};
5311
+ res.end(JSON.stringify({ ok: true, runId }));
5078
5312
  const envFlags = [];
5079
5313
  for (const [k, v] of Object.entries(envVars)) {
5080
5314
  if (k && v !== undefined && v !== '')
5081
5315
  envFlags.push('--env', `${k}=${v}`);
5082
5316
  }
5083
- loadProc = (0, child_process_1.spawn)('k6', ['run', ...envFlags, '--out', `json=${jsonOutPath}`, scriptPath], {
5084
- cwd: projectRoot, env: { ...process.env }, shell: WIN, stdio: 'pipe',
5317
+ const args = [
5318
+ 'run',
5319
+ '--vus', String(loadConfig.vus),
5320
+ '--duration', loadConfig.duration,
5321
+ ...envFlags,
5322
+ '--summary-export', summaryPath,
5323
+ '--out', `json=${jsonOutPath}`,
5324
+ scriptPath,
5325
+ ];
5326
+ loadProc = (0, child_process_1.spawn)('k6', args, {
5327
+ cwd: workDir, env: { ...process.env }, shell: WIN, stdio: 'pipe',
5085
5328
  });
5086
5329
  const addLog = (line) => {
5087
5330
  loadState.logs.push(line);
@@ -5156,7 +5399,7 @@ a{color:var(--blue)}
5156
5399
  }
5157
5400
  if (changed) {
5158
5401
  const slice = loadState.buckets.slice(-30);
5159
- broadcast('load-progress', { buckets: slice, total: loadState.totalRequests, errors: loadState.totalErrors });
5402
+ broadcast('load-progress', { runId: loadState.runId, buckets: slice, total: loadState.totalRequests, errors: loadState.totalErrors });
5160
5403
  }
5161
5404
  }
5162
5405
  catch { }
@@ -5166,17 +5409,23 @@ a{color:var(--blue)}
5166
5409
  loadRunning = false;
5167
5410
  loadProc = null;
5168
5411
  if (loadState.status === 'running') {
5169
- loadState.status = (code === 0 || code === null) ? 'done' : 'done';
5412
+ loadState.status = code === 0 ? 'done' : 'error';
5170
5413
  }
5171
5414
  loadState.endTime = Date.now();
5172
- loadState.summary = parseK6Summary(loadState.logs.join('\n'));
5173
- broadcast('load-done', { status: loadState.status, summary: loadState.summary });
5415
+ loadState.summary = readK6Summary(summaryPath, code);
5416
+ if (loadState.summary?.totalRequests != null)
5417
+ loadState.totalRequests = loadState.summary.totalRequests;
5418
+ if (loadState.summary?.errorPct != null && loadState.summary.totalRequests != null) {
5419
+ loadState.totalErrors = Math.round(loadState.summary.totalRequests * (loadState.summary.errorPct / 100));
5420
+ }
5421
+ saveLoadRun();
5422
+ broadcast('load-done', { runId: loadState.runId, status: loadState.status, summary: loadState.summary });
5174
5423
  try {
5175
- fs.unlinkSync(scriptPath);
5424
+ fs.writeFileSync(path.join(resultPath, 'k6.log'), loadState.logs.join('\n'), 'utf-8');
5176
5425
  }
5177
5426
  catch { }
5178
5427
  try {
5179
- fs.unlinkSync(jsonOutPath);
5428
+ fs.writeFileSync(path.join(resultPath, 'config.json'), JSON.stringify(loadState.config, null, 2), 'utf-8');
5180
5429
  }
5181
5430
  catch { }
5182
5431
  });
@@ -5187,9 +5436,15 @@ a{color:var(--blue)}
5187
5436
  loadState.status = 'error';
5188
5437
  loadState.endTime = Date.now();
5189
5438
  addLog(`❌ k6 не запустился: ${err.message}`);
5190
- broadcast('load-done', { status: 'error', summary: null });
5439
+ loadState.summary = { exitCode: null };
5440
+ saveLoadRun();
5441
+ broadcast('load-done', { runId: loadState.runId, status: 'error', summary: loadState.summary });
5442
+ try {
5443
+ fs.writeFileSync(path.join(resultPath, 'k6.log'), loadState.logs.join('\n'), 'utf-8');
5444
+ }
5445
+ catch { }
5191
5446
  try {
5192
- fs.unlinkSync(scriptPath);
5447
+ fs.writeFileSync(path.join(resultPath, 'config.json'), JSON.stringify(loadState.config, null, 2), 'utf-8');
5193
5448
  }
5194
5449
  catch { }
5195
5450
  });