opencroc 1.6.2 → 1.6.4

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
@@ -2725,7 +2725,7 @@ __export(reporters_exports, {
2725
2725
  renderTokenReportMarkdown: () => renderTokenReportMarkdown,
2726
2726
  renderWorkordersMarkdown: () => renderWorkordersMarkdown
2727
2727
  });
2728
- function generateJsonReport(result) {
2728
+ function generateJsonReport(result, context) {
2729
2729
  const serializable = {
2730
2730
  modules: result.modules,
2731
2731
  erDiagrams: Object.fromEntries(
@@ -2745,6 +2745,10 @@ function generateJsonReport(result) {
2745
2745
  module: f.module,
2746
2746
  chain: f.chain
2747
2747
  })),
2748
+ execution: context ? {
2749
+ metrics: context.metrics ?? null,
2750
+ quality: context.quality ?? null
2751
+ } : void 0,
2748
2752
  validationErrors: result.validationErrors,
2749
2753
  duration: result.duration
2750
2754
  };
@@ -2754,7 +2758,7 @@ function generateJsonReport(result) {
2754
2758
  filename: "opencroc-report.json"
2755
2759
  };
2756
2760
  }
2757
- function generateMarkdownReport(result) {
2761
+ function generateMarkdownReport(result, context) {
2758
2762
  const lines = [
2759
2763
  "# OpenCroc Report",
2760
2764
  "",
@@ -2798,6 +2802,31 @@ function generateMarkdownReport(result) {
2798
2802
  }
2799
2803
  }
2800
2804
  }
2805
+ if (context?.metrics || context?.quality) {
2806
+ lines.push("", "## Execution Quality", "");
2807
+ if (context.metrics) {
2808
+ lines.push(
2809
+ `- Passed: ${context.metrics.passed}`,
2810
+ `- Failed: ${context.metrics.failed}`,
2811
+ `- Skipped: ${context.metrics.skipped}`,
2812
+ `- TimedOut: ${context.metrics.timedOut}`
2813
+ );
2814
+ }
2815
+ if (context.quality) {
2816
+ lines.push(
2817
+ `- Gate Level: ${context.quality.level}`,
2818
+ `- Setup Fail: ${context.quality.setupFail}`,
2819
+ `- Skip Ratio: ${(context.quality.skipRatio * 100).toFixed(2)}%`,
2820
+ `- Auth Fail Ratio: ${(context.quality.authFailRatio * 100).toFixed(2)}%`,
2821
+ `- Effective Execution Rate: ${(context.quality.effectiveExecutionRate * 100).toFixed(2)}%`,
2822
+ `- Auth Status: ${context.quality.authStatus}`,
2823
+ `- Backend Status: ${context.quality.backendStatus}`
2824
+ );
2825
+ if (context.quality.reasons.length > 0) {
2826
+ lines.push(`- Reasons: ${context.quality.reasons.join(", ")}`);
2827
+ }
2828
+ }
2829
+ }
2801
2830
  lines.push("", "---", "*Generated by [OpenCroc](https://github.com/opencroc/opencroc)*");
2802
2831
  return {
2803
2832
  format: "markdown",
@@ -2830,7 +2859,7 @@ function validationRows(errors) {
2830
2859
  (e) => `<tr class="${e.severity}"><td><span class="badge ${e.severity}">${e.severity}</span></td><td>${escapeHtml(e.module)}</td><td>${escapeHtml(e.field)}</td><td>${escapeHtml(e.message)}</td></tr>`
2831
2860
  ).join("\n");
2832
2861
  }
2833
- function generateHtmlReport(result) {
2862
+ function generateHtmlReport(result, context) {
2834
2863
  const totalTables = Array.from(result.erDiagrams.values()).reduce((s, e) => s + e.tables.length, 0);
2835
2864
  const totalRelations = Array.from(result.erDiagrams.values()).reduce((s, e) => s + e.relations.length, 0);
2836
2865
  const totalChains = Array.from(result.chainPlans.values()).reduce((s, p) => s + p.chains.length, 0);
@@ -2909,6 +2938,17 @@ function generateHtmlReport(result) {
2909
2938
  </table>
2910
2939
  </section>
2911
2940
 
2941
+ ${context?.quality || context?.metrics ? `<section>
2942
+ <h2>Execution Quality</h2>
2943
+ <table>
2944
+ <thead><tr><th>Item</th><th>Value</th></tr></thead>
2945
+ <tbody>
2946
+ ${context.metrics ? `<tr><td>Passed</td><td>${context.metrics.passed}</td></tr><tr><td>Failed</td><td>${context.metrics.failed}</td></tr><tr><td>Skipped</td><td>${context.metrics.skipped}</td></tr><tr><td>TimedOut</td><td>${context.metrics.timedOut}</td></tr>` : ""}
2947
+ ${context.quality ? `<tr><td>Gate Level</td><td>${escapeHtml(context.quality.level)}</td></tr><tr><td>Setup Fail</td><td>${String(context.quality.setupFail)}</td></tr><tr><td>Skip Ratio</td><td>${(context.quality.skipRatio * 100).toFixed(2)}%</td></tr><tr><td>Auth Fail Ratio</td><td>${(context.quality.authFailRatio * 100).toFixed(2)}%</td></tr><tr><td>Effective Execution Rate</td><td>${(context.quality.effectiveExecutionRate * 100).toFixed(2)}%</td></tr><tr><td>Auth Status</td><td>${escapeHtml(context.quality.authStatus)}</td></tr><tr><td>Backend Status</td><td>${escapeHtml(context.quality.backendStatus)}</td></tr><tr><td>Reasons</td><td>${escapeHtml(context.quality.reasons.join(", ") || "-")}</td></tr>` : ""}
2948
+ </tbody>
2949
+ </table>
2950
+ </section>` : ""}
2951
+
2912
2952
  ${result.validationErrors.length > 0 ? `<section>
2913
2953
  <h2>Validation Issues (${result.validationErrors.length})</h2>
2914
2954
  <table>
@@ -2928,11 +2968,11 @@ ${result.validationErrors.length > 0 ? `<section>
2928
2968
  filename: "opencroc-report.html"
2929
2969
  };
2930
2970
  }
2931
- function generateReports(result, formats = ["html"]) {
2971
+ function generateReports(result, formats = ["html"], context) {
2932
2972
  return formats.map((fmt) => {
2933
2973
  const gen = REPORTERS[fmt];
2934
2974
  if (!gen) throw new Error(`Unknown report format: "${fmt}". Available: ${Object.keys(REPORTERS).join(", ")}`);
2935
- return gen(result);
2975
+ return gen(result, context);
2936
2976
  });
2937
2977
  }
2938
2978
  var REPORTERS;
@@ -4222,9 +4262,13 @@ function registerAgentRoutes(app, office) {
4222
4262
  });
4223
4263
  app.get("/api/test-results", async () => {
4224
4264
  const metrics = office.getLastExecutionMetrics();
4225
- if (!metrics) return { ok: false, message: "No tests have been run yet" };
4265
+ const quality = office.getLastExecutionQuality();
4266
+ if (!metrics && !quality) return { ok: false, message: "No tests have been run yet" };
4267
+ if (!metrics) {
4268
+ return { ok: true, metrics: null, total: 0, quality };
4269
+ }
4226
4270
  const total = metrics.passed + metrics.failed + metrics.skipped + metrics.timedOut;
4227
- return { ok: true, metrics, total };
4271
+ return { ok: true, metrics, total, quality };
4228
4272
  });
4229
4273
  app.post("/api/reports/generate", async (_req, reply) => {
4230
4274
  if (office.isRunning()) {
@@ -4308,7 +4352,8 @@ function createExecutionCoordinator(deps = {}) {
4308
4352
  cwd: request.cwd,
4309
4353
  encoding: "utf-8",
4310
4354
  timeout: timeoutMs,
4311
- stdio: "pipe"
4355
+ stdio: "pipe",
4356
+ env: request.env
4312
4357
  }));
4313
4358
  } catch (err) {
4314
4359
  const execErr = err;
@@ -4531,6 +4576,111 @@ var init_runtime_bootstrap = __esm({
4531
4576
  }
4532
4577
  });
4533
4578
 
4579
+ // src/execution/auth-provisioner.ts
4580
+ var auth_provisioner_exports = {};
4581
+ __export(auth_provisioner_exports, {
4582
+ createAuthProvisioner: () => createAuthProvisioner
4583
+ });
4584
+ function selectBaseUrl(configBaseUrl) {
4585
+ if (configBaseUrl && /^https?:\/\//i.test(configBaseUrl)) return configBaseUrl;
4586
+ const envBaseUrl = process.env.BASE_URL || "";
4587
+ if (/^https?:\/\//i.test(envBaseUrl)) return envBaseUrl;
4588
+ return "";
4589
+ }
4590
+ function resolveLoginUrl(loginUrl, baseUrl) {
4591
+ if (/^https?:\/\//i.test(loginUrl)) return loginUrl;
4592
+ if (!baseUrl) {
4593
+ throw new Error("AUTH_FAIL: playwright.baseURL or BASE_URL is required for relative runtime.auth.loginUrl");
4594
+ }
4595
+ try {
4596
+ return new URL(loginUrl, baseUrl).href;
4597
+ } catch {
4598
+ throw new Error("AUTH_FAIL: invalid login URL or base URL");
4599
+ }
4600
+ }
4601
+ function createAuthProvisioner(config, deps = {}) {
4602
+ const fetchFn = deps.fetchFn ?? fetch;
4603
+ return {
4604
+ async provision() {
4605
+ const auth = config.runtime?.auth;
4606
+ const loginUrl = auth?.loginUrl;
4607
+ const baseURL = selectBaseUrl(config.playwright?.baseURL);
4608
+ if (!loginUrl) {
4609
+ return { status: "skipped", env: {} };
4610
+ }
4611
+ const username = process.env.AUTH_USERNAME || auth?.username || "admin";
4612
+ const password = process.env.AUTH_PASSWORD || auth?.password || "";
4613
+ if (!password) {
4614
+ throw new Error("AUTH_FAIL: runtime.auth.password or AUTH_PASSWORD is required");
4615
+ }
4616
+ const resolvedLoginUrl = resolveLoginUrl(loginUrl, baseURL);
4617
+ const response = await fetchFn(resolvedLoginUrl, {
4618
+ method: "POST",
4619
+ headers: { "content-type": "application/json" },
4620
+ body: JSON.stringify({ username, password }),
4621
+ signal: AbortSignal.timeout(8e3)
4622
+ });
4623
+ if (!response.ok) {
4624
+ throw new Error(`AUTH_FAIL: login request failed with status ${response.status}`);
4625
+ }
4626
+ const env = {
4627
+ AUTH_LOGIN_URL: resolvedLoginUrl,
4628
+ AUTH_USERNAME: username,
4629
+ AUTH_PASSWORD: password
4630
+ };
4631
+ if (baseURL) env.BASE_URL = baseURL;
4632
+ return {
4633
+ status: "ready",
4634
+ env
4635
+ };
4636
+ }
4637
+ };
4638
+ }
4639
+ var init_auth_provisioner = __esm({
4640
+ "src/execution/auth-provisioner.ts"() {
4641
+ "use strict";
4642
+ init_esm_shims();
4643
+ }
4644
+ });
4645
+
4646
+ // src/execution/quality-gate.ts
4647
+ var quality_gate_exports = {};
4648
+ __export(quality_gate_exports, {
4649
+ buildExecutionQualityGate: () => buildExecutionQualityGate
4650
+ });
4651
+ function buildExecutionQualityGate(input) {
4652
+ const metrics = input.metrics ?? { passed: 0, failed: 0, skipped: 0, timedOut: 0 };
4653
+ const total = metrics.passed + metrics.failed + metrics.skipped + metrics.timedOut;
4654
+ const skipRatio = total > 0 ? metrics.skipped / total : 0;
4655
+ const effectiveExecutionRate = total > 0 ? metrics.passed / total : 0;
4656
+ const authFailRatio = input.authStatus === "failed" ? 1 : 0;
4657
+ const setupFail = input.authStatus === "failed" || input.backendStatus === "failed" || total === 0 && (input.authStatus !== "skipped" || input.backendStatus !== "skipped");
4658
+ const reasons = [];
4659
+ if (setupFail) reasons.push("setup-failed");
4660
+ if (skipRatio >= 0.5) reasons.push("high-skip-ratio");
4661
+ if (metrics.failed > 0) reasons.push("tests-failed");
4662
+ if (metrics.timedOut > 0) reasons.push("tests-timeout");
4663
+ let level = "pass";
4664
+ if (setupFail || metrics.failed > 0) level = "fail";
4665
+ else if (skipRatio >= 0.5 || metrics.timedOut > 0) level = "warn";
4666
+ return {
4667
+ setupFail,
4668
+ skipRatio,
4669
+ authFailRatio,
4670
+ effectiveExecutionRate,
4671
+ level,
4672
+ reasons,
4673
+ authStatus: input.authStatus,
4674
+ backendStatus: input.backendStatus
4675
+ };
4676
+ }
4677
+ var init_quality_gate = __esm({
4678
+ "src/execution/quality-gate.ts"() {
4679
+ "use strict";
4680
+ init_esm_shims();
4681
+ }
4682
+ });
4683
+
4534
4684
  // src/server/croc-office.ts
4535
4685
  var DEFAULT_AGENTS, CrocOffice;
4536
4686
  var init_croc_office = __esm({
@@ -4555,6 +4705,7 @@ var init_croc_office = __esm({
4555
4705
  lastPipelineResult = null;
4556
4706
  lastGeneratedFiles = [];
4557
4707
  lastExecutionMetrics = null;
4708
+ lastExecutionQuality = null;
4558
4709
  lastReports = [];
4559
4710
  constructor(config, cwd) {
4560
4711
  this.config = config;
@@ -4741,6 +4892,9 @@ var init_croc_office = __esm({
4741
4892
  getLastExecutionMetrics() {
4742
4893
  return this.lastExecutionMetrics;
4743
4894
  }
4895
+ getLastExecutionQuality() {
4896
+ return this.lastExecutionQuality;
4897
+ }
4744
4898
  /** Get last generated reports */
4745
4899
  getLastReports() {
4746
4900
  return this.lastReports;
@@ -4754,12 +4908,16 @@ var init_croc_office = __esm({
4754
4908
  this.running = true;
4755
4909
  const start = Date.now();
4756
4910
  let cleanupBackend = null;
4911
+ let authStatus = "skipped";
4912
+ let backendStatus;
4757
4913
  try {
4758
4914
  const { resolve: resolvePath } = await import("path");
4759
4915
  const { existsSync: existsSync17 } = await import("fs");
4760
4916
  const { createExecutionCoordinator: createExecutionCoordinator2 } = await Promise.resolve().then(() => (init_coordinator(), coordinator_exports));
4761
4917
  const { createBackendManager: createBackendManager2 } = await Promise.resolve().then(() => (init_backend_manager(), backend_manager_exports));
4762
4918
  const { createRuntimeBootstrap: createRuntimeBootstrap2 } = await Promise.resolve().then(() => (init_runtime_bootstrap(), runtime_bootstrap_exports));
4919
+ const { createAuthProvisioner: createAuthProvisioner2 } = await Promise.resolve().then(() => (init_auth_provisioner(), auth_provisioner_exports));
4920
+ const { buildExecutionQualityGate: buildExecutionQualityGate2 } = await Promise.resolve().then(() => (init_quality_gate(), quality_gate_exports));
4763
4921
  const { categorizeFailure: categorizeFailure2 } = await Promise.resolve().then(() => (init_self_healing(), self_healing_exports));
4764
4922
  const testFiles = this.lastGeneratedFiles.map((f) => resolvePath(this.cwd, f.filePath)).filter((f) => existsSync17(f));
4765
4923
  if (testFiles.length === 0) {
@@ -4778,27 +4936,61 @@ var init_croc_office = __esm({
4778
4936
  this.log(`\u{1F9E9} Runtime assets prepared: ${runtimeResult.writtenFiles.join(", ")}`);
4779
4937
  }
4780
4938
  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})`);
4939
+ try {
4940
+ const backendReady = await backendManager.ensureReady({
4941
+ mode,
4942
+ cwd: this.cwd,
4943
+ server: this.config.runtime?.server,
4944
+ baseURL: this.config.playwright?.baseURL
4945
+ });
4946
+ backendStatus = backendReady.status;
4947
+ cleanupBackend = backendReady.cleanup;
4948
+ if (backendReady.status === "started") {
4949
+ this.log(`\u{1F680} Managed backend started (${backendReady.healthUrl})`);
4950
+ } else if (backendReady.status === "reused") {
4951
+ this.log(`\u{1F501} Reusing backend (${backendReady.healthUrl})`);
4952
+ }
4953
+ } catch (err) {
4954
+ backendStatus = "failed";
4955
+ this.lastExecutionQuality = buildExecutionQualityGate2({
4956
+ metrics: null,
4957
+ authStatus,
4958
+ backendStatus
4959
+ });
4960
+ throw err;
4961
+ }
4962
+ const authProvisioner = createAuthProvisioner2(this.config);
4963
+ let authResult;
4964
+ try {
4965
+ authResult = await authProvisioner.provision();
4966
+ authStatus = authResult.status;
4967
+ if (authResult.status === "ready") {
4968
+ this.log("\u{1F510} Auth environment prepared");
4969
+ }
4970
+ } catch (err) {
4971
+ authStatus = "failed";
4972
+ this.lastExecutionQuality = buildExecutionQualityGate2({
4973
+ metrics: null,
4974
+ authStatus,
4975
+ backendStatus
4976
+ });
4977
+ throw err;
4792
4978
  }
4793
4979
  this.updateAgent("healer-croc", { status: "thinking", currentTask: "Monitoring test run...", progress: 0 });
4794
4980
  const coordinator = createExecutionCoordinator2({ categorizeFailure: categorizeFailure2 });
4795
4981
  const execResult = await coordinator.run({
4796
4982
  cwd: this.cwd,
4797
4983
  testFiles,
4798
- mode
4984
+ mode,
4985
+ env: authResult.env
4799
4986
  });
4800
4987
  const metrics = execResult.metrics;
4801
4988
  this.lastExecutionMetrics = metrics;
4989
+ this.lastExecutionQuality = buildExecutionQualityGate2({
4990
+ metrics,
4991
+ authStatus,
4992
+ backendStatus
4993
+ });
4802
4994
  const total = metrics.passed + metrics.failed + metrics.skipped + metrics.timedOut;
4803
4995
  if (metrics.failed > 0) {
4804
4996
  this.updateAgent("tester-croc", { status: "error", currentTask: `${metrics.failed} tests failed`, progress: 100 });
@@ -4814,7 +5006,7 @@ var init_croc_office = __esm({
4814
5006
  this.log(`\u2705 All ${metrics.passed} tests passed!`);
4815
5007
  }
4816
5008
  this.updateNodeStatus("controller", metrics.failed > 0 ? "failed" : "passed");
4817
- this.broadcast("test:complete", { metrics, total });
5009
+ this.broadcast("test:complete", { metrics, total, quality: this.lastExecutionQuality });
4818
5010
  const duration = Date.now() - start;
4819
5011
  this.log(`\u{1F9EA} Test execution complete in ${duration}ms`);
4820
5012
  return { ok: metrics.failed === 0, task: "execute", duration, details: metrics };
@@ -4847,7 +5039,10 @@ var init_croc_office = __esm({
4847
5039
  this.log("\u{1F4CA} \u6C47\u62A5\u9CC4 is generating reports...");
4848
5040
  const { generateReports: generateReports2 } = await Promise.resolve().then(() => (init_reporters(), reporters_exports));
4849
5041
  const formats = ["html", "json", "markdown"];
4850
- const reports = generateReports2(this.lastPipelineResult, formats);
5042
+ const reports = generateReports2(this.lastPipelineResult, formats, {
5043
+ metrics: this.lastExecutionMetrics,
5044
+ quality: this.lastExecutionQuality
5045
+ });
4851
5046
  this.lastReports = reports;
4852
5047
  const { resolve: resolvePath } = await import("path");
4853
5048
  const { writeFileSync: writeFileSync11, mkdirSync: mkdirSync11 } = await import("fs");