opencroc 1.6.0 → 1.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -4206,12 +4206,17 @@ function registerAgentRoutes(app, office) {
4206
4206
  duration: result.duration
4207
4207
  };
4208
4208
  });
4209
- app.post("/api/run-tests", async (_req, reply) => {
4209
+ app.post("/api/run-tests", async (req, reply) => {
4210
4210
  if (office.isRunning()) {
4211
4211
  reply.code(409).send({ error: "A task is already running" });
4212
4212
  return;
4213
4213
  }
4214
- office.runTests().catch(() => {
4214
+ const mode = req.body?.mode;
4215
+ if (mode && !["auto", "reuse", "managed"].includes(mode)) {
4216
+ reply.code(400).send({ error: "Invalid mode. Valid values: auto, reuse, managed" });
4217
+ return;
4218
+ }
4219
+ office.runTests({ mode }).catch(() => {
4215
4220
  });
4216
4221
  return { ok: true, message: "Test execution started" };
4217
4222
  });
@@ -4268,6 +4273,264 @@ var init_agents = __esm({
4268
4273
  }
4269
4274
  });
4270
4275
 
4276
+ // src/execution/coordinator.ts
4277
+ var coordinator_exports = {};
4278
+ __export(coordinator_exports, {
4279
+ createExecutionCoordinator: () => createExecutionCoordinator
4280
+ });
4281
+ import { execSync as nodeExecSync } from "child_process";
4282
+ function parseMetrics(output) {
4283
+ const metrics = { passed: 0, failed: 0, skipped: 0, timedOut: 0 };
4284
+ const passedMatch = output.match(/(\d+)\s+passed/);
4285
+ const failedMatch = output.match(/(\d+)\s+failed/);
4286
+ const skippedMatch = output.match(/(\d+)\s+skipped/);
4287
+ const timedOutMatch = output.match(/(\d+)\s+timed?\s*out/i);
4288
+ if (passedMatch) metrics.passed = parseInt(passedMatch[1], 10);
4289
+ if (failedMatch) metrics.failed = parseInt(failedMatch[1], 10);
4290
+ if (skippedMatch) metrics.skipped = parseInt(skippedMatch[1], 10);
4291
+ if (timedOutMatch) metrics.timedOut = parseInt(timedOutMatch[1], 10);
4292
+ return metrics;
4293
+ }
4294
+ function getFailureLines(output) {
4295
+ return output.split(/\r?\n/).filter((line) => /fail|error|timeout/i.test(line)).slice(0, 5);
4296
+ }
4297
+ function createExecutionCoordinator(deps = {}) {
4298
+ const execSync = deps.execSync ?? nodeExecSync;
4299
+ const categorizeFailure2 = deps.categorizeFailure;
4300
+ return {
4301
+ async run(request) {
4302
+ const mode = request.mode ?? "auto";
4303
+ const timeoutMs = request.timeoutMs ?? 3e5;
4304
+ const command = `npx playwright test ${request.testFiles.map((file) => `"${file}"`).join(" ")} --reporter=line 2>&1`;
4305
+ let output;
4306
+ try {
4307
+ output = String(execSync(command, {
4308
+ cwd: request.cwd,
4309
+ encoding: "utf-8",
4310
+ timeout: timeoutMs,
4311
+ stdio: "pipe"
4312
+ }));
4313
+ } catch (err) {
4314
+ const execErr = err;
4315
+ output = `${execErr.stdout || ""}
4316
+ ${execErr.stderr || ""}`;
4317
+ }
4318
+ output = output.trim();
4319
+ const metrics = parseMetrics(output);
4320
+ if (metrics.passed === 0 && metrics.failed === 0 && metrics.skipped === 0 && metrics.timedOut === 0) {
4321
+ metrics.failed = request.testFiles.length;
4322
+ }
4323
+ const failureHints = getFailureLines(output).map((line) => {
4324
+ const analyzed = categorizeFailure2 ? categorizeFailure2(line) : { category: "unknown", confidence: 0.5 };
4325
+ return {
4326
+ line,
4327
+ category: analyzed.category,
4328
+ confidence: analyzed.confidence
4329
+ };
4330
+ });
4331
+ return {
4332
+ mode,
4333
+ metrics,
4334
+ output,
4335
+ failureHints
4336
+ };
4337
+ }
4338
+ };
4339
+ }
4340
+ var init_coordinator = __esm({
4341
+ "src/execution/coordinator.ts"() {
4342
+ "use strict";
4343
+ init_esm_shims();
4344
+ }
4345
+ });
4346
+
4347
+ // src/runtime/resilient-fetch.ts
4348
+ function sleep(ms) {
4349
+ return new Promise((r) => setTimeout(r, ms));
4350
+ }
4351
+ async function waitForBackend(baseUrl, options = {}) {
4352
+ const { timeoutMs = 3e4, intervalMs = 1e3, healthPath = "/health" } = options;
4353
+ const healthUrl = new URL(healthPath, baseUrl).href;
4354
+ const start = Date.now();
4355
+ while (Date.now() - start < timeoutMs) {
4356
+ try {
4357
+ const resp = await fetch(healthUrl, { method: "GET", signal: AbortSignal.timeout(3e3) });
4358
+ if (resp.ok) return;
4359
+ } catch {
4360
+ }
4361
+ await sleep(intervalMs);
4362
+ }
4363
+ throw new Error(`Backend not ready: ${healthUrl} timed out after ${timeoutMs}ms`);
4364
+ }
4365
+ var init_resilient_fetch = __esm({
4366
+ "src/runtime/resilient-fetch.ts"() {
4367
+ "use strict";
4368
+ init_esm_shims();
4369
+ }
4370
+ });
4371
+
4372
+ // src/execution/backend-manager.ts
4373
+ var backend_manager_exports = {};
4374
+ __export(backend_manager_exports, {
4375
+ createBackendManager: () => createBackendManager
4376
+ });
4377
+ import { resolve as resolve8 } from "path";
4378
+ import { spawn as nodeSpawn } from "child_process";
4379
+ function normalizeHealthUrl(server, baseURL) {
4380
+ if (server?.healthUrl) return server.healthUrl;
4381
+ if (baseURL) return new URL("/health", baseURL).href;
4382
+ return "http://localhost:3000/health";
4383
+ }
4384
+ function splitHealthUrl(healthUrl) {
4385
+ const parsed = new URL(healthUrl);
4386
+ const baseUrl = `${parsed.protocol}//${parsed.host}`;
4387
+ const healthPath = `${parsed.pathname}${parsed.search}`;
4388
+ return { baseUrl, healthPath };
4389
+ }
4390
+ async function waitReady(waitForBackend2, healthUrl, timeoutMs, intervalMs) {
4391
+ const { baseUrl, healthPath } = splitHealthUrl(healthUrl);
4392
+ await waitForBackend2(baseUrl, {
4393
+ timeoutMs,
4394
+ intervalMs,
4395
+ healthPath
4396
+ });
4397
+ }
4398
+ function createBackendManager(deps = {}) {
4399
+ const waitForBackend2 = deps.waitForBackend ?? waitForBackend;
4400
+ const spawn = deps.spawn ?? nodeSpawn;
4401
+ return {
4402
+ async ensureReady(request) {
4403
+ const server = request.server ?? {};
4404
+ const healthUrl = normalizeHealthUrl(server, request.baseURL);
4405
+ const quickTimeoutMs = 1500;
4406
+ const startTimeoutMs = server.startTimeoutMs ?? 3e4;
4407
+ const pollIntervalMs = server.pollIntervalMs ?? 800;
4408
+ const tryReuse = request.mode !== "managed" || server.reuseExisting !== false;
4409
+ if (tryReuse) {
4410
+ try {
4411
+ await waitReady(waitForBackend2, healthUrl, quickTimeoutMs, 300);
4412
+ return {
4413
+ mode: request.mode,
4414
+ status: "reused",
4415
+ healthUrl,
4416
+ cleanup: async () => {
4417
+ }
4418
+ };
4419
+ } catch (err) {
4420
+ void err;
4421
+ }
4422
+ }
4423
+ if (request.mode === "reuse") {
4424
+ throw new Error(`HEALTH_FAIL: backend is not reachable at ${healthUrl} in reuse mode`);
4425
+ }
4426
+ if (!server.command) {
4427
+ if (request.mode === "auto") {
4428
+ return {
4429
+ mode: request.mode,
4430
+ status: "skipped",
4431
+ healthUrl,
4432
+ cleanup: async () => {
4433
+ }
4434
+ };
4435
+ }
4436
+ throw new Error("BOOT_CONFIG_MISSING: runtime.server.command is required for managed mode");
4437
+ }
4438
+ const child = spawn(server.command, server.args ?? [], {
4439
+ cwd: resolve8(request.cwd, server.cwd ?? "."),
4440
+ shell: true,
4441
+ stdio: "pipe",
4442
+ env: process.env
4443
+ });
4444
+ try {
4445
+ await waitReady(waitForBackend2, healthUrl, startTimeoutMs, pollIntervalMs);
4446
+ } catch {
4447
+ if (child.exitCode === null) child.kill();
4448
+ throw new Error(`BOOT_TIMEOUT: backend did not become healthy at ${healthUrl}`);
4449
+ }
4450
+ return {
4451
+ mode: request.mode,
4452
+ status: "started",
4453
+ healthUrl,
4454
+ cleanup: async () => {
4455
+ if (child.exitCode === null) child.kill();
4456
+ }
4457
+ };
4458
+ }
4459
+ };
4460
+ }
4461
+ var init_backend_manager = __esm({
4462
+ "src/execution/backend-manager.ts"() {
4463
+ "use strict";
4464
+ init_esm_shims();
4465
+ init_resilient_fetch();
4466
+ }
4467
+ });
4468
+
4469
+ // src/execution/runtime-bootstrap.ts
4470
+ var runtime_bootstrap_exports = {};
4471
+ __export(runtime_bootstrap_exports, {
4472
+ createRuntimeBootstrap: () => createRuntimeBootstrap
4473
+ });
4474
+ import { existsSync as existsSync15, mkdirSync as mkdirSync10, writeFileSync as writeFileSync10 } from "fs";
4475
+ import { dirname as dirname5, join as join13 } from "path";
4476
+ function ensureFile(filePath, content, force) {
4477
+ if (existsSync15(filePath) && !force) {
4478
+ return false;
4479
+ }
4480
+ mkdirSync10(dirname5(filePath), { recursive: true });
4481
+ writeFileSync10(filePath, content, "utf-8");
4482
+ return true;
4483
+ }
4484
+ function createRuntimeBootstrap(config) {
4485
+ return {
4486
+ async ensure(request) {
4487
+ const force = request.force ?? false;
4488
+ const files = [
4489
+ {
4490
+ name: "playwright.config.ts",
4491
+ content: generatePlaywrightConfig(config)
4492
+ },
4493
+ {
4494
+ name: "global-setup.ts",
4495
+ content: generateGlobalSetup(config)
4496
+ },
4497
+ {
4498
+ name: "global-teardown.ts",
4499
+ content: generateGlobalTeardown(config)
4500
+ }
4501
+ ];
4502
+ if (request.hasAuth) {
4503
+ files.push({
4504
+ name: "auth.setup.ts",
4505
+ content: generateAuthSetup(config)
4506
+ });
4507
+ }
4508
+ const writtenFiles = [];
4509
+ const skippedFiles = [];
4510
+ for (const file of files) {
4511
+ const filePath = join13(request.cwd, file.name);
4512
+ const written = ensureFile(filePath, file.content, force);
4513
+ if (written) writtenFiles.push(file.name);
4514
+ else skippedFiles.push(file.name);
4515
+ }
4516
+ return {
4517
+ writtenFiles,
4518
+ skippedFiles
4519
+ };
4520
+ }
4521
+ };
4522
+ }
4523
+ var init_runtime_bootstrap = __esm({
4524
+ "src/execution/runtime-bootstrap.ts"() {
4525
+ "use strict";
4526
+ init_esm_shims();
4527
+ init_playwright_config_generator();
4528
+ init_global_setup_generator();
4529
+ init_global_teardown_generator();
4530
+ init_auth_setup_generator();
4531
+ }
4532
+ });
4533
+
4271
4534
  // src/server/croc-office.ts
4272
4535
  var DEFAULT_AGENTS, CrocOffice;
4273
4536
  var init_croc_office = __esm({
@@ -4408,13 +4671,13 @@ var init_croc_office = __esm({
4408
4671
  const fullResult = await pipeline.run(["scan", "er-diagram", "api-chain", "plan", "codegen"]);
4409
4672
  this.lastPipelineResult = fullResult;
4410
4673
  this.lastGeneratedFiles = fullResult.generatedFiles;
4411
- const { writeFileSync: writeFileSync10, mkdirSync: mkdirSync10 } = await import("fs");
4412
- const { dirname: dirname6 } = await import("path");
4674
+ const { writeFileSync: writeFileSync11, mkdirSync: mkdirSync11 } = await import("fs");
4675
+ const { dirname: dirname7 } = await import("path");
4413
4676
  let filesWritten = 0;
4414
4677
  for (const file of fullResult.generatedFiles) {
4415
4678
  const fullPath = resolvePath(this.cwd, file.filePath);
4416
- mkdirSync10(dirname6(fullPath), { recursive: true });
4417
- writeFileSync10(fullPath, file.content, "utf-8");
4679
+ mkdirSync11(dirname7(fullPath), { recursive: true });
4680
+ writeFileSync11(fullPath, file.content, "utf-8");
4418
4681
  filesWritten++;
4419
4682
  }
4420
4683
  this.updateNodeStatus("controller", "passed");
@@ -4483,58 +4746,66 @@ var init_croc_office = __esm({
4483
4746
  return this.lastReports;
4484
4747
  }
4485
4748
  /** Run generated tests with Playwright */
4486
- async runTests() {
4749
+ async runTests(options = {}) {
4487
4750
  if (this.running) return { ok: false, task: "execute", duration: 0, error: "Another task is running" };
4488
4751
  if (this.lastGeneratedFiles.length === 0) {
4489
4752
  return { ok: false, task: "execute", duration: 0, error: "No test files \u2014 run Pipeline first" };
4490
4753
  }
4491
4754
  this.running = true;
4492
4755
  const start = Date.now();
4756
+ let cleanupBackend = null;
4493
4757
  try {
4494
4758
  const { resolve: resolvePath } = await import("path");
4495
- const { execSync } = await import("child_process");
4496
- const { existsSync: existsSync16 } = await import("fs");
4497
- const testFiles = this.lastGeneratedFiles.map((f) => resolvePath(this.cwd, f.filePath)).filter((f) => existsSync16(f));
4759
+ const { existsSync: existsSync17 } = await import("fs");
4760
+ const { createExecutionCoordinator: createExecutionCoordinator2 } = await Promise.resolve().then(() => (init_coordinator(), coordinator_exports));
4761
+ const { createBackendManager: createBackendManager2 } = await Promise.resolve().then(() => (init_backend_manager(), backend_manager_exports));
4762
+ const { createRuntimeBootstrap: createRuntimeBootstrap2 } = await Promise.resolve().then(() => (init_runtime_bootstrap(), runtime_bootstrap_exports));
4763
+ const { categorizeFailure: categorizeFailure2 } = await Promise.resolve().then(() => (init_self_healing(), self_healing_exports));
4764
+ const testFiles = this.lastGeneratedFiles.map((f) => resolvePath(this.cwd, f.filePath)).filter((f) => existsSync17(f));
4498
4765
  if (testFiles.length === 0) {
4499
4766
  this.log("\u26A0\uFE0F No test files found on disk", "warn");
4500
4767
  return { ok: false, task: "execute", duration: Date.now() - start, error: "No test files found on disk" };
4501
4768
  }
4502
- this.updateAgent("tester-croc", { status: "working", currentTask: `Running ${testFiles.length} test files...`, progress: 0 });
4503
- this.log(`\u{1F9EA} \u6D4B\u8BD5\u9CC4 is running ${testFiles.length} Playwright tests...`);
4504
- this.updateAgent("healer-croc", { status: "thinking", currentTask: "Monitoring test run...", progress: 0 });
4505
- let stdout2 = "", stderr = "";
4506
- try {
4507
- const result = execSync(
4508
- `npx playwright test ${testFiles.map((f) => `"${f}"`).join(" ")} --reporter=line 2>&1`,
4509
- { cwd: this.cwd, encoding: "utf-8", timeout: 3e5, stdio: "pipe" }
4510
- );
4511
- stdout2 = result;
4512
- } catch (err) {
4513
- const execErr = err;
4514
- stdout2 = execErr.stdout || "";
4515
- stderr = execErr.stderr || "";
4769
+ const mode = options.mode ?? "auto";
4770
+ this.updateAgent("tester-croc", { status: "working", currentTask: `Running ${testFiles.length} test files (${mode})...`, progress: 0 });
4771
+ this.log(`\u{1F9EA} \u6D4B\u8BD5\u9CC4 is running ${testFiles.length} Playwright tests (${mode})...`);
4772
+ const runtimeBootstrap = createRuntimeBootstrap2(this.config);
4773
+ const runtimeResult = await runtimeBootstrap.ensure({
4774
+ cwd: this.cwd,
4775
+ hasAuth: !!this.config.runtime?.auth?.loginUrl
4776
+ });
4777
+ if (runtimeResult.writtenFiles.length > 0) {
4778
+ this.log(`\u{1F9E9} Runtime assets prepared: ${runtimeResult.writtenFiles.join(", ")}`);
4779
+ }
4780
+ const backendManager = createBackendManager2();
4781
+ const backendReady = await backendManager.ensureReady({
4782
+ mode,
4783
+ cwd: this.cwd,
4784
+ server: this.config.runtime?.server,
4785
+ baseURL: this.config.playwright?.baseURL
4786
+ });
4787
+ cleanupBackend = backendReady.cleanup;
4788
+ if (backendReady.status === "started") {
4789
+ this.log(`\u{1F680} Managed backend started (${backendReady.healthUrl})`);
4790
+ } else if (backendReady.status === "reused") {
4791
+ this.log(`\u{1F501} Reusing backend (${backendReady.healthUrl})`);
4516
4792
  }
4517
- const output = stdout2 + "\n" + stderr;
4518
- const metrics = { passed: 0, failed: 0, skipped: 0, timedOut: 0 };
4519
- const passedMatch = output.match(/(\d+)\s+passed/);
4520
- const failedMatch = output.match(/(\d+)\s+failed/);
4521
- const skippedMatch = output.match(/(\d+)\s+skipped/);
4522
- const timedOutMatch = output.match(/(\d+)\s+timed?\s*out/i);
4523
- if (passedMatch) metrics.passed = parseInt(passedMatch[1], 10);
4524
- if (failedMatch) metrics.failed = parseInt(failedMatch[1], 10);
4525
- if (skippedMatch) metrics.skipped = parseInt(skippedMatch[1], 10);
4526
- if (timedOutMatch) metrics.timedOut = parseInt(timedOutMatch[1], 10);
4793
+ this.updateAgent("healer-croc", { status: "thinking", currentTask: "Monitoring test run...", progress: 0 });
4794
+ const coordinator = createExecutionCoordinator2({ categorizeFailure: categorizeFailure2 });
4795
+ const execResult = await coordinator.run({
4796
+ cwd: this.cwd,
4797
+ testFiles,
4798
+ mode
4799
+ });
4800
+ const metrics = execResult.metrics;
4527
4801
  this.lastExecutionMetrics = metrics;
4528
4802
  const total = metrics.passed + metrics.failed + metrics.skipped + metrics.timedOut;
4529
4803
  if (metrics.failed > 0) {
4530
4804
  this.updateAgent("tester-croc", { status: "error", currentTask: `${metrics.failed} tests failed`, progress: 100 });
4531
4805
  this.updateAgent("healer-croc", { status: "working", currentTask: `Analyzing ${metrics.failed} failures...`, progress: 50 });
4532
4806
  this.log(`\u274C Tests: ${metrics.passed} passed, ${metrics.failed} failed, ${metrics.skipped} skipped`, "warn");
4533
- const { categorizeFailure: categorizeFailure2 } = await Promise.resolve().then(() => (init_self_healing(), self_healing_exports));
4534
- const failLines = output.split("\n").filter((l) => /fail|error|timeout/i.test(l)).slice(0, 5);
4535
- for (const line of failLines) {
4536
- const cat = categorizeFailure2(line);
4537
- this.log(` \u{1F50D} ${cat.category} (${Math.round(cat.confidence * 100)}%): ${line.substring(0, 100)}`, "warn");
4807
+ for (const hint of execResult.failureHints) {
4808
+ this.log(` \u{1F50D} ${hint.category} (${Math.round(hint.confidence * 100)}%): ${hint.line.substring(0, 100)}`, "warn");
4538
4809
  }
4539
4810
  this.updateAgent("healer-croc", { status: "done", currentTask: "Failure analysis done", progress: 100 });
4540
4811
  } else {
@@ -4552,6 +4823,14 @@ var init_croc_office = __esm({
4552
4823
  this.log(`\u274C Test execution failed: ${err}`, "error");
4553
4824
  return { ok: false, task: "execute", duration: Date.now() - start, error: String(err) };
4554
4825
  } finally {
4826
+ if (cleanupBackend) {
4827
+ try {
4828
+ await cleanupBackend();
4829
+ this.log("\u{1F9F9} Managed backend stopped");
4830
+ } catch (err) {
4831
+ this.log(`\u26A0\uFE0F Backend cleanup failed: ${err}`, "warn");
4832
+ }
4833
+ }
4555
4834
  this.running = false;
4556
4835
  }
4557
4836
  }
@@ -4571,12 +4850,12 @@ var init_croc_office = __esm({
4571
4850
  const reports = generateReports2(this.lastPipelineResult, formats);
4572
4851
  this.lastReports = reports;
4573
4852
  const { resolve: resolvePath } = await import("path");
4574
- const { writeFileSync: writeFileSync10, mkdirSync: mkdirSync10 } = await import("fs");
4853
+ const { writeFileSync: writeFileSync11, mkdirSync: mkdirSync11 } = await import("fs");
4575
4854
  const outDir = resolvePath(this.cwd, this.config.outDir || "./opencroc-output");
4576
- mkdirSync10(outDir, { recursive: true });
4855
+ mkdirSync11(outDir, { recursive: true });
4577
4856
  for (const report2 of reports) {
4578
4857
  const fullPath = resolvePath(outDir, report2.filename);
4579
- writeFileSync10(fullPath, report2.content, "utf-8");
4858
+ writeFileSync11(fullPath, report2.content, "utf-8");
4580
4859
  this.log(`\u{1F4C4} Generated ${report2.format} report: ${report2.filename}`);
4581
4860
  }
4582
4861
  this.updateAgent("reporter-croc", { status: "done", currentTask: `${reports.length} reports generated`, progress: 100 });
@@ -4821,13 +5100,13 @@ import Fastify from "fastify";
4821
5100
  import fastifyStatic from "@fastify/static";
4822
5101
  import fastifyWebsocket from "@fastify/websocket";
4823
5102
  import { fileURLToPath as fileURLToPath2 } from "url";
4824
- import { dirname as dirname5, join as join13, resolve as resolve8 } from "path";
4825
- import { existsSync as existsSync15 } from "fs";
5103
+ import { dirname as dirname6, join as join14, resolve as resolve9 } from "path";
5104
+ import { existsSync as existsSync16 } from "fs";
4826
5105
  async function startServer(opts) {
4827
5106
  const app = Fastify({ logger: false });
4828
5107
  await app.register(fastifyWebsocket);
4829
- const webDir = resolve8(__dirname2, "../web");
4830
- if (existsSync15(webDir)) {
5108
+ const webDir = resolve9(__dirname2, "../web");
5109
+ if (existsSync16(webDir)) {
4831
5110
  await app.register(fastifyStatic, {
4832
5111
  root: webDir,
4833
5112
  prefix: "/",
@@ -4848,8 +5127,8 @@ async function startServer(opts) {
4848
5127
  reply.code(404).send({ error: "Not found" });
4849
5128
  return;
4850
5129
  }
4851
- const indexPath = join13(webDir, "index.html");
4852
- if (existsSync15(indexPath)) {
5130
+ const indexPath = join14(webDir, "index.html");
5131
+ if (existsSync16(indexPath)) {
4853
5132
  reply.sendFile("index.html");
4854
5133
  } else {
4855
5134
  reply.code(200).header("content-type", "text/html").send(getEmbeddedHtml());
@@ -5000,7 +5279,7 @@ var init_server = __esm({
5000
5279
  init_agents();
5001
5280
  init_croc_office();
5002
5281
  __filename2 = fileURLToPath2(import.meta.url);
5003
- __dirname2 = dirname5(__filename2);
5282
+ __dirname2 = dirname6(__filename2);
5004
5283
  }
5005
5284
  });
5006
5285