opencroc 1.5.1 → 1.6.1

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
@@ -1729,6 +1729,7 @@ var init_ollama = __esm({
1729
1729
  });
1730
1730
 
1731
1731
  // src/llm/index.ts
1732
+ var SYSTEM_PROMPTS;
1732
1733
  var init_llm = __esm({
1733
1734
  "src/llm/index.ts"() {
1734
1735
  "use strict";
@@ -1737,36 +1738,336 @@ var init_llm = __esm({
1737
1738
  init_ollama();
1738
1739
  init_openai();
1739
1740
  init_ollama();
1741
+ SYSTEM_PROMPTS = {
1742
+ failureAnalysis: `You are an expert test failure analyst for an E2E testing framework.
1743
+ Given a test failure error message and its context, analyze the root cause and suggest a fix.
1744
+ Respond in JSON format: { "rootCause": string, "category": string, "suggestedFix": string, "confidence": number }
1745
+ Categories: backend-5xx, timeout, endpoint-not-found, data-constraint, network, frontend-render, test-script, unknown.`,
1746
+ chainPlanning: `You are an API test chain planner.
1747
+ Given a list of API endpoints and their dependencies, generate an optimal test execution order.
1748
+ Consider data dependencies, authentication requirements, and cleanup steps.
1749
+ Respond in JSON format: { "chains": [{ "name": string, "steps": [{ "endpoint": string, "method": string, "description": string }] }] }`
1750
+ };
1740
1751
  }
1741
1752
  });
1742
1753
 
1743
1754
  // src/self-healing/dialog-loop-runner.ts
1755
+ function createJsonResultParser() {
1756
+ return {
1757
+ parse(stdout2) {
1758
+ const failures = [];
1759
+ const lines = stdout2.split("\n");
1760
+ for (const line of lines) {
1761
+ const failMatch = line.match(/[✘✗×]\s+.*?›\s+(.+)/);
1762
+ if (failMatch) {
1763
+ failures.push({
1764
+ title: failMatch[1].trim(),
1765
+ error: failMatch[1].trim()
1766
+ });
1767
+ }
1768
+ }
1769
+ return failures;
1770
+ },
1771
+ countTotal(stdout2) {
1772
+ let total = 0;
1773
+ const passMatch = stdout2.match(/(\d+)\s+passed/);
1774
+ const failMatch = stdout2.match(/(\d+)\s+failed/);
1775
+ if (passMatch) total += parseInt(passMatch[1], 10);
1776
+ if (failMatch) total += parseInt(failMatch[1], 10);
1777
+ return total || 1;
1778
+ }
1779
+ };
1780
+ }
1781
+ async function runDialogLoop(options) {
1782
+ const cfg = { ...DEFAULTS2, ...options.config };
1783
+ const { runner, parser, fixer } = options;
1784
+ const history = [];
1785
+ const errorTracker = /* @__PURE__ */ new Map();
1786
+ for (let iteration = 1; iteration <= cfg.maxIterations + 1; iteration++) {
1787
+ const iterStart = Date.now();
1788
+ const { stdout: stdout2 } = await runner.run();
1789
+ const failures = parser.parse(stdout2);
1790
+ const totalTests = parser.countTotal(stdout2);
1791
+ const passed = totalTests - failures.length;
1792
+ const iterResult = {
1793
+ iteration,
1794
+ totalTests,
1795
+ passed,
1796
+ failed: failures.length,
1797
+ failedTests: failures.map((f) => f.title),
1798
+ fixesApplied: [],
1799
+ durationMs: 0
1800
+ };
1801
+ if (failures.length === 0) {
1802
+ iterResult.durationMs = Date.now() - iterStart;
1803
+ history.push(iterResult);
1804
+ options.onIteration?.(iterResult);
1805
+ break;
1806
+ }
1807
+ if (iteration > cfg.maxIterations) {
1808
+ iterResult.durationMs = Date.now() - iterStart;
1809
+ history.push(iterResult);
1810
+ options.onIteration?.(iterResult);
1811
+ break;
1812
+ }
1813
+ const newFailures = failures.filter((f) => {
1814
+ const key = `${f.title}::${f.error}`;
1815
+ const count = (errorTracker.get(key) ?? 0) + 1;
1816
+ errorTracker.set(key, count);
1817
+ return count <= cfg.sameErrorThreshold;
1818
+ });
1819
+ if (newFailures.length === 0) {
1820
+ iterResult.durationMs = Date.now() - iterStart;
1821
+ history.push(iterResult);
1822
+ options.onIteration?.(iterResult);
1823
+ break;
1824
+ }
1825
+ for (const failure of newFailures) {
1826
+ const outcome = await fixer.apply(failure);
1827
+ if (outcome.success) {
1828
+ iterResult.fixesApplied.push(failure.title);
1829
+ }
1830
+ }
1831
+ iterResult.durationMs = Date.now() - iterStart;
1832
+ history.push(iterResult);
1833
+ options.onIteration?.(iterResult);
1834
+ if (iterResult.fixesApplied.length === 0 || !cfg.autoRerunOnFix) {
1835
+ break;
1836
+ }
1837
+ }
1838
+ const final = history[history.length - 1];
1839
+ const totalFixesApplied = history.reduce((sum, h) => sum + h.fixesApplied.length, 0);
1840
+ return {
1841
+ iterations: history,
1842
+ finalPassed: final?.passed ?? 0,
1843
+ finalFailed: final?.failed ?? 0,
1844
+ totalFixesApplied,
1845
+ success: (final?.failed ?? 1) === 0
1846
+ };
1847
+ }
1848
+ var DEFAULTS2;
1744
1849
  var init_dialog_loop_runner = __esm({
1745
1850
  "src/self-healing/dialog-loop-runner.ts"() {
1746
1851
  "use strict";
1747
1852
  init_esm_shims();
1853
+ DEFAULTS2 = {
1854
+ maxIterations: 3,
1855
+ pollIntervalMs: 1e4,
1856
+ sameErrorThreshold: 2,
1857
+ autoRerunOnFix: true
1858
+ };
1748
1859
  }
1749
1860
  });
1750
1861
 
1751
1862
  // src/self-healing/controlled-fixer.ts
1752
1863
  import { existsSync as existsSync8, copyFileSync, readFileSync as readFileSync2, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, unlinkSync } from "fs";
1753
1864
  import { dirname as dirname2 } from "path";
1865
+ async function applyControlledFix(opts) {
1866
+ const fs8 = opts.fs ?? defaultFs;
1867
+ const scope = opts.options?.scope ?? "config-only";
1868
+ const dryRun = opts.options?.dryRun ?? true;
1869
+ const verify = opts.options?.verify ?? true;
1870
+ const configPath = opts.configPath;
1871
+ const backupPath = configPath + ".backup";
1872
+ if (!fs8.exists(configPath)) {
1873
+ return { success: false, scope, fixedItems: [], rolledBack: false, error: `Config file not found: ${configPath}` };
1874
+ }
1875
+ const originalContent = fs8.read(configPath);
1876
+ fs8.write(backupPath, originalContent);
1877
+ const validation = opts.validator.validate(originalContent);
1878
+ if (validation.passed) {
1879
+ cleanup(fs8, backupPath);
1880
+ return { success: true, scope, fixedItems: [], rolledBack: false };
1881
+ }
1882
+ let fixResult;
1883
+ try {
1884
+ fixResult = opts.fixer.fix(originalContent, validation.errors);
1885
+ } catch (err) {
1886
+ rollback(fs8, backupPath, configPath);
1887
+ return { success: false, scope, fixedItems: [], rolledBack: true, error: `Fix threw: ${err instanceof Error ? err.message : String(err)}` };
1888
+ }
1889
+ if (!fixResult.success) {
1890
+ rollback(fs8, backupPath, configPath);
1891
+ return { success: false, scope, fixedItems: fixResult.fixedItems, rolledBack: true, error: `Remaining errors: ${fixResult.remainingErrors.join("; ")}` };
1892
+ }
1893
+ if (dryRun) {
1894
+ const dryValidation = opts.validator.validate(fixResult.fixedContent);
1895
+ if (!dryValidation.passed) {
1896
+ rollback(fs8, backupPath, configPath);
1897
+ return { success: false, scope, fixedItems: fixResult.fixedItems, rolledBack: true, error: `Dry-run validation failed: ${dryValidation.errors.join("; ")}` };
1898
+ }
1899
+ }
1900
+ fs8.write(configPath, fixResult.fixedContent);
1901
+ if (verify) {
1902
+ const reloaded = fs8.read(configPath);
1903
+ const postValidation = opts.validator.validate(reloaded);
1904
+ if (!postValidation.passed) {
1905
+ rollback(fs8, backupPath, configPath);
1906
+ return { success: false, scope, fixedItems: fixResult.fixedItems, rolledBack: true, error: `Post-write verification failed: ${postValidation.errors.join("; ")}` };
1907
+ }
1908
+ }
1909
+ cleanup(fs8, backupPath);
1910
+ let prUrl;
1911
+ if (scope === "config-and-source" && opts.attribution && opts.prGenerator) {
1912
+ try {
1913
+ prUrl = await opts.prGenerator.generate(opts.attribution);
1914
+ } catch {
1915
+ }
1916
+ }
1917
+ return { success: true, scope, fixedItems: fixResult.fixedItems, rolledBack: false, prUrl };
1918
+ }
1919
+ function rollback(fs8, backupPath, configPath) {
1920
+ if (fs8.exists(backupPath)) {
1921
+ const backup = fs8.read(backupPath);
1922
+ fs8.write(configPath, backup);
1923
+ fs8.remove(backupPath);
1924
+ }
1925
+ }
1926
+ function cleanup(fs8, backupPath) {
1927
+ if (fs8.exists(backupPath)) {
1928
+ fs8.remove(backupPath);
1929
+ }
1930
+ }
1931
+ var defaultFs;
1754
1932
  var init_controlled_fixer = __esm({
1755
1933
  "src/self-healing/controlled-fixer.ts"() {
1756
1934
  "use strict";
1757
1935
  init_esm_shims();
1936
+ defaultFs = {
1937
+ exists: existsSync8,
1938
+ read: (p) => readFileSync2(p, "utf-8"),
1939
+ write: (p, c) => {
1940
+ mkdirSync3(dirname2(p), { recursive: true });
1941
+ writeFileSync3(p, c, "utf-8");
1942
+ },
1943
+ copy: copyFileSync,
1944
+ remove: unlinkSync,
1945
+ mkdirp: (d) => mkdirSync3(d, { recursive: true })
1946
+ };
1758
1947
  }
1759
1948
  });
1760
1949
 
1761
1950
  // src/self-healing/auto-fix-generator.ts
1951
+ async function generateFixPR(attribution, git, patchWriter, options) {
1952
+ const opts = { ...DEFAULT_OPTIONS, ...options };
1953
+ const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
1954
+ const branch = `${opts.branchPrefix}${ts}`;
1955
+ const patchFile = `report/patch-${ts}.patch`;
1956
+ await git.exec("git", ["checkout", "-b", branch]);
1957
+ await patchWriter.mkdir("report");
1958
+ await patchWriter.write(patchFile, attribution.fixSuggestion.codePatch);
1959
+ try {
1960
+ await git.exec("git", ["apply", patchFile]);
1961
+ } catch {
1962
+ }
1963
+ await git.exec("git", ["add", "."]);
1964
+ await git.exec("git", ["commit", "-m", `fix: AI auto-patch for "${attribution.testName}"`]);
1965
+ await git.exec("git", ["push", "origin", branch]);
1966
+ const prArgs = [
1967
+ "pr",
1968
+ "create",
1969
+ "--draft",
1970
+ "--title",
1971
+ `[AI Fix] ${attribution.testName}`,
1972
+ "--body",
1973
+ buildPRBody(attribution)
1974
+ ];
1975
+ const { stdout: prUrl } = await git.exec("gh", prArgs);
1976
+ await git.exec("git", ["checkout", opts.baseBranch]);
1977
+ return { prUrl: prUrl.trim(), branch, patchFile };
1978
+ }
1979
+ function buildPRBody(a) {
1980
+ return [
1981
+ "## AI Auto-Fix PR",
1982
+ "",
1983
+ `**Test:** ${a.testName}`,
1984
+ `**Category:** ${a.category} | **Severity:** ${a.severity} | **Confidence:** ${(a.confidence * 100).toFixed(0)}%`,
1985
+ "",
1986
+ "### Root Cause",
1987
+ a.rootCause,
1988
+ "",
1989
+ "### Fix",
1990
+ a.fixSuggestion.description,
1991
+ "",
1992
+ "---",
1993
+ "> This PR was auto-generated by AI and **must be reviewed before merging**."
1994
+ ].join("\n");
1995
+ }
1996
+ var DEFAULT_OPTIONS;
1762
1997
  var init_auto_fix_generator = __esm({
1763
1998
  "src/self-healing/auto-fix-generator.ts"() {
1764
1999
  "use strict";
1765
2000
  init_esm_shims();
2001
+ DEFAULT_OPTIONS = {
2002
+ branchPrefix: "autofix/",
2003
+ baseBranch: "main",
2004
+ draftOnly: true
2005
+ };
1766
2006
  }
1767
2007
  });
1768
2008
 
1769
2009
  // src/self-healing/index.ts
2010
+ var self_healing_exports = {};
2011
+ __export(self_healing_exports, {
2012
+ analyzeFailureWithLLM: () => analyzeFailureWithLLM,
2013
+ applyControlledFix: () => applyControlledFix,
2014
+ categorizeFailure: () => categorizeFailure,
2015
+ createJsonResultParser: () => createJsonResultParser,
2016
+ createSelfHealingLoop: () => createSelfHealingLoop,
2017
+ generateFixPR: () => generateFixPR,
2018
+ runDialogLoop: () => runDialogLoop
2019
+ });
2020
+ function categorizeFailure(errorMessage) {
2021
+ const msg = errorMessage.toLowerCase();
2022
+ if (/5\d{2}|internal server error/.test(msg))
2023
+ return { category: "backend-5xx", confidence: 0.9 };
2024
+ if (/timeout|timed?\s*out/.test(msg))
2025
+ return { category: "timeout", confidence: 0.8 };
2026
+ if (/404|not found/.test(msg))
2027
+ return { category: "endpoint-not-found", confidence: 0.85 };
2028
+ if (/4[0-2]\d|validation|constraint/.test(msg))
2029
+ return { category: "data-constraint", confidence: 0.75 };
2030
+ if (/econnrefused|enotfound|network/.test(msg))
2031
+ return { category: "network", confidence: 0.9 };
2032
+ if (/selector|locator|element/.test(msg))
2033
+ return { category: "frontend-render", confidence: 0.7 };
2034
+ if (/storage\s*state|auth|login/.test(msg))
2035
+ return { category: "test-script", confidence: 0.8 };
2036
+ return { category: "unknown", confidence: 0.5 };
2037
+ }
2038
+ async function analyzeFailureWithLLM(errorMessage, llm) {
2039
+ const heuristic = categorizeFailure(errorMessage);
2040
+ if (!llm) {
2041
+ return {
2042
+ rootCause: errorMessage,
2043
+ category: heuristic.category,
2044
+ suggestedFix: "",
2045
+ confidence: heuristic.confidence
2046
+ };
2047
+ }
2048
+ try {
2049
+ const response = await llm.chat([
2050
+ { role: "system", content: SYSTEM_PROMPTS.failureAnalysis },
2051
+ { role: "user", content: `Analyze this test failure:
2052
+
2053
+ ${errorMessage}` }
2054
+ ]);
2055
+ const parsed = JSON.parse(response);
2056
+ return {
2057
+ rootCause: parsed.rootCause || errorMessage,
2058
+ category: parsed.category || heuristic.category,
2059
+ suggestedFix: parsed.suggestedFix || "",
2060
+ confidence: parsed.confidence || heuristic.confidence
2061
+ };
2062
+ } catch {
2063
+ return {
2064
+ rootCause: errorMessage,
2065
+ category: heuristic.category,
2066
+ suggestedFix: "",
2067
+ confidence: heuristic.confidence
2068
+ };
2069
+ }
2070
+ }
1770
2071
  async function attemptConfigFix(_testResultsDir, _mode, _llm) {
1771
2072
  return {
1772
2073
  success: false,
@@ -1868,6 +2169,13 @@ var init_heal = __esm({
1868
2169
  });
1869
2170
 
1870
2171
  // src/ci/index.ts
2172
+ var ci_exports = {};
2173
+ __export(ci_exports, {
2174
+ generateCiTemplate: () => generateCiTemplate,
2175
+ generateGitHubActionsTemplate: () => generateGitHubActionsTemplate,
2176
+ generateGitLabCITemplate: () => generateGitLabCITemplate,
2177
+ listCiPlatforms: () => listCiPlatforms
2178
+ });
1871
2179
  function generateGitHubActionsTemplate(opts = {}) {
1872
2180
  const nodeVersions = opts.nodeVersions ?? ["20.x"];
1873
2181
  const install = opts.installCommand ?? "npm ci";
@@ -1997,8 +2305,8 @@ var init_ci = __esm({
1997
2305
  });
1998
2306
 
1999
2307
  // src/cli/commands/ci.ts
2000
- var ci_exports = {};
2001
- __export(ci_exports, {
2308
+ var ci_exports2 = {};
2309
+ __export(ci_exports2, {
2002
2310
  ci: () => ci
2003
2311
  });
2004
2312
  import * as fs5 from "fs";
@@ -2044,6 +2352,143 @@ var init_ci2 = __esm({
2044
2352
  });
2045
2353
 
2046
2354
  // src/reporters/checklist-reporter.ts
2355
+ function classifyFailure(error) {
2356
+ if (!error) return "other";
2357
+ if (error.includes("[BACKEND_5XX]")) return "backend-5xx";
2358
+ if (error.includes("[MIXED_5XX]")) return "mixed-5xx";
2359
+ if (error.includes("[SLOW_API_FATAL]")) return "slow-api";
2360
+ if (error.includes("[LOG_COMPLETION_FAIL]")) return "log-fail";
2361
+ if (error.includes("[LOG_COMPLETION_TIMEOUT]")) return "log-timeout";
2362
+ if (/waitForSelector|toHaveURL|Timeout/i.test(error)) return "frontend-load";
2363
+ return "other";
2364
+ }
2365
+ function buildFailureSummary(records) {
2366
+ const failed = records.filter((r) => r.status === "failed");
2367
+ const cats = failed.map((r) => classifyFailure(r.error));
2368
+ return {
2369
+ totalFailed: failed.length,
2370
+ backend5xx: cats.filter((c) => c === "backend-5xx").length,
2371
+ mixed5xx: cats.filter((c) => c === "mixed-5xx").length,
2372
+ slowApi: cats.filter((c) => c === "slow-api").length,
2373
+ logFail: cats.filter((c) => c === "log-fail").length,
2374
+ logTimeout: cats.filter((c) => c === "log-timeout").length,
2375
+ frontendLoad: cats.filter((c) => c === "frontend-load").length,
2376
+ other: cats.filter((c) => c === "other").length
2377
+ };
2378
+ }
2379
+ function aggregateLogCompletion(records) {
2380
+ let totalCandidates = 0;
2381
+ let succeeded = 0;
2382
+ let failed = 0;
2383
+ let timedOut = 0;
2384
+ const timedOutFreq = /* @__PURE__ */ new Map();
2385
+ for (const r of records) {
2386
+ const lc = r.logCompletion;
2387
+ if (!lc || lc.candidateCount === 0) continue;
2388
+ totalCandidates += lc.candidateCount;
2389
+ succeeded += lc.succeeded.length;
2390
+ failed += lc.failed.length;
2391
+ timedOut += lc.timedOut.length;
2392
+ for (const item of lc.timedOut) {
2393
+ const key = `${item.method}:${item.path}`;
2394
+ const existing = timedOutFreq.get(key);
2395
+ if (existing) existing.count += 1;
2396
+ else timedOutFreq.set(key, { method: item.method, path: item.path, count: 1 });
2397
+ }
2398
+ }
2399
+ const timedOutTop5 = Array.from(timedOutFreq.values()).sort((a, b) => b.count - a.count).slice(0, 5).map((t) => ({ method: t.method, path: t.path, occurrences: t.count }));
2400
+ const matchRate = totalCandidates > 0 ? (succeeded + failed) / totalCandidates * 100 : 0;
2401
+ const effectiveRate = totalCandidates > 0 ? succeeded / totalCandidates * 100 : 0;
2402
+ return { totalCandidates, succeeded, failed, timedOut, matchRate, effectiveRate, timedOutTop5 };
2403
+ }
2404
+ function extractUrls(error) {
2405
+ if (!error) return [];
2406
+ const matched = error.match(/https?:\/\/[^\s\n]+/g);
2407
+ return matched ? Array.from(new Set(matched)) : [];
2408
+ }
2409
+ function parseApiDomain(urlStr) {
2410
+ try {
2411
+ const u = new URL(urlStr);
2412
+ const segments = u.pathname.split("/").filter(Boolean);
2413
+ const v1Index = segments.findIndex((s) => s === "v1");
2414
+ if (v1Index === -1 || v1Index + 1 >= segments.length) return segments[0] || null;
2415
+ const afterV1 = segments.slice(v1Index + 1);
2416
+ if (afterV1.length === 0) return null;
2417
+ const first = afterV1[0];
2418
+ if (/^\d+$/.test(first) && afterV1.length > 1) return afterV1[1];
2419
+ return first;
2420
+ } catch {
2421
+ return null;
2422
+ }
2423
+ }
2424
+ function buildBackendChecklist(records) {
2425
+ const domainMap = /* @__PURE__ */ new Map();
2426
+ const failed = records.filter((r) => r.status === "failed");
2427
+ for (const item of failed) {
2428
+ const cat = classifyFailure(item.error);
2429
+ if (cat !== "backend-5xx" && cat !== "mixed-5xx") continue;
2430
+ const urls = extractUrls(item.error);
2431
+ for (const url of urls) {
2432
+ const domain = parseApiDomain(url);
2433
+ if (!domain) continue;
2434
+ const current = domainMap.get(domain) ?? { tests: /* @__PURE__ */ new Set(), endpoints: /* @__PURE__ */ new Set() };
2435
+ current.tests.add(item.title);
2436
+ current.endpoints.add(url);
2437
+ domainMap.set(domain, current);
2438
+ }
2439
+ }
2440
+ return Array.from(domainMap.entries()).map(([domain, v]) => ({
2441
+ domain,
2442
+ tests: Array.from(v.tests).sort(),
2443
+ endpoints: Array.from(v.endpoints).sort()
2444
+ })).sort((a, b) => b.tests.length - a.tests.length || a.domain.localeCompare(b.domain));
2445
+ }
2446
+ function renderChecklistMarkdown(items, summary, logSummary) {
2447
+ const lines = [
2448
+ "# Backend Fix Checklist",
2449
+ ""
2450
+ ];
2451
+ if (logSummary) {
2452
+ lines.push(
2453
+ `- Log match rate: ${logSummary.matchRate.toFixed(2)}% (candidates=${logSummary.totalCandidates}, succeeded=${logSummary.succeeded}, failed=${logSummary.failed}, timedOut=${logSummary.timedOut})`,
2454
+ `- Effective success rate: ${logSummary.effectiveRate.toFixed(2)}%`
2455
+ );
2456
+ }
2457
+ lines.push(
2458
+ `- Backend 5xx: ${summary.backend5xx}`,
2459
+ `- Mixed 5xx: ${summary.mixed5xx}`,
2460
+ `- Slow API: ${summary.slowApi}`,
2461
+ `- Log fail: ${summary.logFail}`,
2462
+ `- Log timeout: ${summary.logTimeout}`,
2463
+ `- Frontend load: ${summary.frontendLoad}`,
2464
+ `- Other: ${summary.other}`,
2465
+ ""
2466
+ );
2467
+ if (logSummary && logSummary.timedOutTop5.length > 0) {
2468
+ lines.push("### Timed-out APIs Top 5", "", "| # | Method | Path | Occurrences |", "|---|--------|------|-------------|");
2469
+ logSummary.timedOutTop5.forEach((t, i) => {
2470
+ lines.push(`| ${i + 1} | ${t.method} | ${t.path} | ${t.occurrences} |`);
2471
+ });
2472
+ lines.push("");
2473
+ }
2474
+ if (items.length === 0) {
2475
+ lines.push("No backend 5xx failures this run.");
2476
+ return lines.join("\n") + "\n";
2477
+ }
2478
+ for (const item of items) {
2479
+ lines.push(
2480
+ `## ${item.domain}`,
2481
+ "",
2482
+ `- Failed tests: ${item.tests.length}`,
2483
+ "- Affected tests:"
2484
+ );
2485
+ for (const t of item.tests) lines.push(` - ${t}`);
2486
+ lines.push("- Failed endpoints:");
2487
+ for (const e of item.endpoints) lines.push(` - ${e}`);
2488
+ lines.push("");
2489
+ }
2490
+ return lines.join("\n") + "\n";
2491
+ }
2047
2492
  var init_checklist_reporter = __esm({
2048
2493
  "src/reporters/checklist-reporter.ts"() {
2049
2494
  "use strict";
@@ -2052,6 +2497,99 @@ var init_checklist_reporter = __esm({
2052
2497
  });
2053
2498
 
2054
2499
  // src/reporters/workorder-reporter.ts
2500
+ function assignPriority(item, isLogRate) {
2501
+ if (isLogRate) return "P0";
2502
+ if (item.tests.length >= 3) return "P0";
2503
+ if (item.tests.length === 2) return "P1";
2504
+ return "P2";
2505
+ }
2506
+ function buildWorkorders(opts) {
2507
+ const { checklist, logSummary, logRateThreshold = 90 } = opts;
2508
+ const result = [];
2509
+ let idx = 1;
2510
+ if (logSummary && logSummary.totalCandidates > 0 && logSummary.matchRate < logRateThreshold) {
2511
+ const logItem = {
2512
+ domain: "Log Completion Standards",
2513
+ tests: logSummary.timedOutTop5.map((t) => `${t.method} ${t.path} (\xD7${t.occurrences})`),
2514
+ endpoints: logSummary.timedOutTop5.map((t) => t.path)
2515
+ };
2516
+ result.push({
2517
+ index: idx++,
2518
+ domain: logItem.domain,
2519
+ priority: "P0",
2520
+ tests: logItem.tests,
2521
+ endpoints: logItem.endpoints,
2522
+ objective: "Add missing end-phase structured logs for timed-out APIs",
2523
+ acceptanceCriteria: [
2524
+ `Log completion match rate \u2265 ${logRateThreshold}%`,
2525
+ "TimedOut API count drops to 0 or only SSE/long-polling endpoints remain"
2526
+ ]
2527
+ });
2528
+ }
2529
+ for (const item of checklist) {
2530
+ const priority = assignPriority(item, false);
2531
+ result.push({
2532
+ index: idx++,
2533
+ domain: item.domain,
2534
+ priority,
2535
+ tests: item.tests,
2536
+ endpoints: item.endpoints,
2537
+ objective: "Fix 500 errors and return a valid business response",
2538
+ acceptanceCriteria: [
2539
+ "HTTP status returns 2xx for affected endpoints",
2540
+ "No [BACKEND_5XX] errors in corresponding page traversal",
2541
+ "All covered tests pass"
2542
+ ]
2543
+ });
2544
+ }
2545
+ return result;
2546
+ }
2547
+ function renderWorkordersMarkdown(workorders, summary, logSummary) {
2548
+ const lines = [
2549
+ "# Backend Work Orders",
2550
+ ""
2551
+ ];
2552
+ if (logSummary) {
2553
+ lines.push(
2554
+ `- Log match rate: ${logSummary.matchRate.toFixed(2)}%`,
2555
+ `- Effective success rate: ${logSummary.effectiveRate.toFixed(2)}%`
2556
+ );
2557
+ }
2558
+ lines.push(
2559
+ `- Total failed: ${summary.totalFailed}`,
2560
+ `- Backend 5xx: ${summary.backend5xx}`,
2561
+ `- Mixed 5xx: ${summary.mixed5xx}`,
2562
+ `- Slow API: ${summary.slowApi}`,
2563
+ `- Log fail: ${summary.logFail}`,
2564
+ `- Log timeout: ${summary.logTimeout}`,
2565
+ `- Frontend load: ${summary.frontendLoad}`,
2566
+ `- Other: ${summary.other}`,
2567
+ ""
2568
+ );
2569
+ if (workorders.length === 0) {
2570
+ lines.push("No backend work orders this run.");
2571
+ return lines.join("\n") + "\n";
2572
+ }
2573
+ for (const wo of workorders) {
2574
+ lines.push(
2575
+ `## Workorder ${wo.index} - ${wo.domain}`,
2576
+ "",
2577
+ `- Priority: ${wo.priority}`,
2578
+ `- Affected tests: ${wo.tests.length}`,
2579
+ "- Scope:"
2580
+ );
2581
+ for (const t of wo.tests) lines.push(` - ${t}`);
2582
+ if (wo.endpoints.length > 0) {
2583
+ lines.push("- Endpoints:");
2584
+ for (const e of wo.endpoints) lines.push(` - ${e}`);
2585
+ }
2586
+ lines.push(`- Objective: ${wo.objective}`);
2587
+ lines.push("- Acceptance criteria:");
2588
+ for (const c of wo.acceptanceCriteria) lines.push(` - ${c}`);
2589
+ lines.push("");
2590
+ }
2591
+ return lines.join("\n") + "\n";
2592
+ }
2055
2593
  var init_workorder_reporter = __esm({
2056
2594
  "src/reporters/workorder-reporter.ts"() {
2057
2595
  "use strict";
@@ -2060,14 +2598,133 @@ var init_workorder_reporter = __esm({
2060
2598
  });
2061
2599
 
2062
2600
  // src/reporters/token-reporter.ts
2601
+ function renderTokenReportMarkdown(summary) {
2602
+ const lines = [
2603
+ "# AI Token Usage Report",
2604
+ "",
2605
+ `- Total requests: ${summary.totalRequests}`,
2606
+ `- Total tokens: ${summary.totalTokens.toLocaleString()}`,
2607
+ `- Prompt tokens: ${summary.totalPromptTokens.toLocaleString()}`,
2608
+ `- Completion tokens: ${summary.totalCompletionTokens.toLocaleString()}`,
2609
+ `- Estimated cost: \xA5${summary.totalEstimatedCost.toFixed(4)}`,
2610
+ `- Average latency: ${summary.avgLatencyMs}ms`
2611
+ ];
2612
+ if (summary.budgetUsedPercent !== null) {
2613
+ lines.push(`- Budget used: ${summary.budgetUsedPercent}%${summary.budgetExceeded ? " **EXCEEDED**" : ""}`);
2614
+ }
2615
+ lines.push("");
2616
+ const cats = Object.entries(summary.byCategory).sort((a, b) => b[1].totalTokens - a[1].totalTokens);
2617
+ if (cats.length > 0) {
2618
+ lines.push(
2619
+ "## By Category",
2620
+ "",
2621
+ "| Category | Requests | Prompt | Completion | Total | Cost |",
2622
+ "|----------|----------|--------|------------|-------|------|"
2623
+ );
2624
+ for (const [cat, d] of cats) {
2625
+ lines.push(`| ${cat} | ${d.requests} | ${d.promptTokens.toLocaleString()} | ${d.completionTokens.toLocaleString()} | ${d.totalTokens.toLocaleString()} | \xA5${d.estimatedCost.toFixed(4)} |`);
2626
+ }
2627
+ lines.push("");
2628
+ }
2629
+ const models = Object.entries(summary.byModel);
2630
+ if (models.length > 0) {
2631
+ lines.push(
2632
+ "## By Model",
2633
+ "",
2634
+ "| Model | Requests | Total Tokens | Cost |",
2635
+ "|-------|----------|-------------|------|"
2636
+ );
2637
+ for (const [model, d] of models) {
2638
+ lines.push(`| ${model} | ${d.requests} | ${d.totalTokens.toLocaleString()} | \xA5${d.estimatedCost.toFixed(4)} |`);
2639
+ }
2640
+ lines.push("");
2641
+ }
2642
+ return lines.join("\n") + "\n";
2643
+ }
2644
+ var TokenTracker;
2063
2645
  var init_token_reporter = __esm({
2064
2646
  "src/reporters/token-reporter.ts"() {
2065
2647
  "use strict";
2066
2648
  init_esm_shims();
2649
+ TokenTracker = class {
2650
+ entries = [];
2651
+ budget = null;
2652
+ setBudget(maxTokens) {
2653
+ this.budget = maxTokens;
2654
+ }
2655
+ record(entry) {
2656
+ this.entries.push(entry);
2657
+ }
2658
+ reset() {
2659
+ this.entries = [];
2660
+ }
2661
+ getSummary() {
2662
+ const totalRequests = this.entries.length;
2663
+ let totalPromptTokens = 0;
2664
+ let totalCompletionTokens = 0;
2665
+ let totalEstimatedCost = 0;
2666
+ let totalLatency = 0;
2667
+ const byCategory = {};
2668
+ const byModel = {};
2669
+ for (const e of this.entries) {
2670
+ totalPromptTokens += e.promptTokens;
2671
+ totalCompletionTokens += e.completionTokens;
2672
+ totalEstimatedCost += e.estimatedCost;
2673
+ totalLatency += e.latencyMs;
2674
+ if (!byCategory[e.category]) {
2675
+ byCategory[e.category] = { requests: 0, promptTokens: 0, completionTokens: 0, totalTokens: 0, estimatedCost: 0 };
2676
+ }
2677
+ const cat = byCategory[e.category];
2678
+ cat.requests++;
2679
+ cat.promptTokens += e.promptTokens;
2680
+ cat.completionTokens += e.completionTokens;
2681
+ cat.totalTokens += e.promptTokens + e.completionTokens;
2682
+ cat.estimatedCost += e.estimatedCost;
2683
+ if (!byModel[e.model]) {
2684
+ byModel[e.model] = { requests: 0, totalTokens: 0, estimatedCost: 0 };
2685
+ }
2686
+ const mod = byModel[e.model];
2687
+ mod.requests++;
2688
+ mod.totalTokens += e.promptTokens + e.completionTokens;
2689
+ mod.estimatedCost += e.estimatedCost;
2690
+ }
2691
+ const totalTokens = totalPromptTokens + totalCompletionTokens;
2692
+ const budgetUsedPercent = this.budget && this.budget > 0 ? Math.round(totalTokens / this.budget * 1e4) / 100 : null;
2693
+ return {
2694
+ totalRequests,
2695
+ totalTokens,
2696
+ totalPromptTokens,
2697
+ totalCompletionTokens,
2698
+ totalEstimatedCost,
2699
+ avgLatencyMs: totalRequests > 0 ? Math.round(totalLatency / totalRequests) : 0,
2700
+ byCategory,
2701
+ byModel,
2702
+ budgetUsedPercent,
2703
+ budgetExceeded: budgetUsedPercent !== null && budgetUsedPercent > 100
2704
+ };
2705
+ }
2706
+ };
2067
2707
  }
2068
2708
  });
2069
2709
 
2070
2710
  // src/reporters/index.ts
2711
+ var reporters_exports = {};
2712
+ __export(reporters_exports, {
2713
+ TokenTracker: () => TokenTracker,
2714
+ aggregateLogCompletion: () => aggregateLogCompletion,
2715
+ buildBackendChecklist: () => buildBackendChecklist,
2716
+ buildFailureSummary: () => buildFailureSummary,
2717
+ buildWorkorders: () => buildWorkorders,
2718
+ classifyFailure: () => classifyFailure,
2719
+ generateHtmlReport: () => generateHtmlReport,
2720
+ generateJsonReport: () => generateJsonReport,
2721
+ generateMarkdownReport: () => generateMarkdownReport,
2722
+ generateReports: () => generateReports,
2723
+ parseApiDomain: () => parseApiDomain,
2724
+ renderChecklistMarkdown: () => renderChecklistMarkdown,
2725
+ renderTokenReportMarkdown: () => renderTokenReportMarkdown,
2726
+ renderWorkordersMarkdown: () => renderWorkordersMarkdown
2727
+ });
2071
2728
  function generateJsonReport(result) {
2072
2729
  const serializable = {
2073
2730
  modules: result.modules,
@@ -3549,6 +4206,65 @@ function registerAgentRoutes(app, office) {
3549
4206
  duration: result.duration
3550
4207
  };
3551
4208
  });
4209
+ app.post("/api/run-tests", async (req, reply) => {
4210
+ if (office.isRunning()) {
4211
+ reply.code(409).send({ error: "A task is already running" });
4212
+ return;
4213
+ }
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(() => {
4220
+ });
4221
+ return { ok: true, message: "Test execution started" };
4222
+ });
4223
+ app.get("/api/test-results", async () => {
4224
+ const metrics = office.getLastExecutionMetrics();
4225
+ if (!metrics) return { ok: false, message: "No tests have been run yet" };
4226
+ const total = metrics.passed + metrics.failed + metrics.skipped + metrics.timedOut;
4227
+ return { ok: true, metrics, total };
4228
+ });
4229
+ app.post("/api/reports/generate", async (_req, reply) => {
4230
+ if (office.isRunning()) {
4231
+ reply.code(409).send({ error: "A task is already running" });
4232
+ return;
4233
+ }
4234
+ office.generateReport().catch(() => {
4235
+ });
4236
+ return { ok: true, message: "Report generation started" };
4237
+ });
4238
+ app.get("/api/reports", async () => {
4239
+ const reports = office.getLastReports();
4240
+ if (reports.length === 0) return { ok: false, message: "No reports generated yet" };
4241
+ return {
4242
+ ok: true,
4243
+ reports: reports.map((r) => ({
4244
+ format: r.format,
4245
+ filename: r.filename,
4246
+ size: r.content.length
4247
+ }))
4248
+ };
4249
+ });
4250
+ app.get("/api/reports/:format", async (req, reply) => {
4251
+ const reports = office.getLastReports();
4252
+ const report2 = reports.find((r) => r.format === req.params.format);
4253
+ if (!report2) {
4254
+ reply.code(404).send({ error: `No ${req.params.format} report found` });
4255
+ return;
4256
+ }
4257
+ const contentType = req.params.format === "html" ? "text/html" : req.params.format === "json" ? "application/json" : "text/markdown";
4258
+ reply.type(contentType).send(report2.content);
4259
+ });
4260
+ app.get("/api/ci/template", async (req) => {
4261
+ const provider = req.query.provider || "github";
4262
+ const { generateGitHubActionsTemplate: generateGitHubActionsTemplate2, generateGitLabCITemplate: generateGitLabCITemplate2 } = await Promise.resolve().then(() => (init_ci(), ci_exports));
4263
+ if (provider === "gitlab") {
4264
+ return { ok: true, provider: "gitlab", template: generateGitLabCITemplate2() };
4265
+ }
4266
+ return { ok: true, provider: "github", template: generateGitHubActionsTemplate2() };
4267
+ });
3552
4268
  }
3553
4269
  var init_agents = __esm({
3554
4270
  "src/server/routes/agents.ts"() {
@@ -3557,6 +4273,264 @@ var init_agents = __esm({
3557
4273
  }
3558
4274
  });
3559
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
+
3560
4534
  // src/server/croc-office.ts
3561
4535
  var DEFAULT_AGENTS, CrocOffice;
3562
4536
  var init_croc_office = __esm({
@@ -3580,6 +4554,8 @@ var init_croc_office = __esm({
3580
4554
  running = false;
3581
4555
  lastPipelineResult = null;
3582
4556
  lastGeneratedFiles = [];
4557
+ lastExecutionMetrics = null;
4558
+ lastReports = [];
3583
4559
  constructor(config, cwd) {
3584
4560
  this.config = config;
3585
4561
  this.cwd = cwd;
@@ -3695,13 +4671,13 @@ var init_croc_office = __esm({
3695
4671
  const fullResult = await pipeline.run(["scan", "er-diagram", "api-chain", "plan", "codegen"]);
3696
4672
  this.lastPipelineResult = fullResult;
3697
4673
  this.lastGeneratedFiles = fullResult.generatedFiles;
3698
- const { writeFileSync: writeFileSync10, mkdirSync: mkdirSync10 } = await import("fs");
3699
- const { dirname: dirname6 } = await import("path");
4674
+ const { writeFileSync: writeFileSync11, mkdirSync: mkdirSync11 } = await import("fs");
4675
+ const { dirname: dirname7 } = await import("path");
3700
4676
  let filesWritten = 0;
3701
4677
  for (const file of fullResult.generatedFiles) {
3702
4678
  const fullPath = resolvePath(this.cwd, file.filePath);
3703
- mkdirSync10(dirname6(fullPath), { recursive: true });
3704
- writeFileSync10(fullPath, file.content, "utf-8");
4679
+ mkdirSync11(dirname7(fullPath), { recursive: true });
4680
+ writeFileSync11(fullPath, file.content, "utf-8");
3705
4681
  filesWritten++;
3706
4682
  }
3707
4683
  this.updateNodeStatus("controller", "passed");
@@ -3761,6 +4737,144 @@ var init_croc_office = __esm({
3761
4737
  getGeneratedFiles() {
3762
4738
  return this.lastGeneratedFiles;
3763
4739
  }
4740
+ /** Get last execution metrics */
4741
+ getLastExecutionMetrics() {
4742
+ return this.lastExecutionMetrics;
4743
+ }
4744
+ /** Get last generated reports */
4745
+ getLastReports() {
4746
+ return this.lastReports;
4747
+ }
4748
+ /** Run generated tests with Playwright */
4749
+ async runTests(options = {}) {
4750
+ if (this.running) return { ok: false, task: "execute", duration: 0, error: "Another task is running" };
4751
+ if (this.lastGeneratedFiles.length === 0) {
4752
+ return { ok: false, task: "execute", duration: 0, error: "No test files \u2014 run Pipeline first" };
4753
+ }
4754
+ this.running = true;
4755
+ const start = Date.now();
4756
+ let cleanupBackend = null;
4757
+ try {
4758
+ const { resolve: resolvePath } = await import("path");
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));
4765
+ if (testFiles.length === 0) {
4766
+ this.log("\u26A0\uFE0F No test files found on disk", "warn");
4767
+ return { ok: false, task: "execute", duration: Date.now() - start, error: "No test files found on disk" };
4768
+ }
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})`);
4792
+ }
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;
4801
+ this.lastExecutionMetrics = metrics;
4802
+ const total = metrics.passed + metrics.failed + metrics.skipped + metrics.timedOut;
4803
+ if (metrics.failed > 0) {
4804
+ this.updateAgent("tester-croc", { status: "error", currentTask: `${metrics.failed} tests failed`, progress: 100 });
4805
+ this.updateAgent("healer-croc", { status: "working", currentTask: `Analyzing ${metrics.failed} failures...`, progress: 50 });
4806
+ this.log(`\u274C Tests: ${metrics.passed} passed, ${metrics.failed} failed, ${metrics.skipped} skipped`, "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");
4809
+ }
4810
+ this.updateAgent("healer-croc", { status: "done", currentTask: "Failure analysis done", progress: 100 });
4811
+ } else {
4812
+ this.updateAgent("tester-croc", { status: "done", currentTask: `All ${metrics.passed} tests passed!`, progress: 100 });
4813
+ this.updateAgent("healer-croc", { status: "done", currentTask: "No failures", progress: 100 });
4814
+ this.log(`\u2705 All ${metrics.passed} tests passed!`);
4815
+ }
4816
+ this.updateNodeStatus("controller", metrics.failed > 0 ? "failed" : "passed");
4817
+ this.broadcast("test:complete", { metrics, total });
4818
+ const duration = Date.now() - start;
4819
+ this.log(`\u{1F9EA} Test execution complete in ${duration}ms`);
4820
+ return { ok: metrics.failed === 0, task: "execute", duration, details: metrics };
4821
+ } catch (err) {
4822
+ this.updateAgent("tester-croc", { status: "error", currentTask: String(err) });
4823
+ this.log(`\u274C Test execution failed: ${err}`, "error");
4824
+ return { ok: false, task: "execute", duration: Date.now() - start, error: String(err) };
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
+ }
4834
+ this.running = false;
4835
+ }
4836
+ }
4837
+ /** Generate reports (HTML/JSON/Markdown) */
4838
+ async generateReport() {
4839
+ if (this.running) return { ok: false, task: "report", duration: 0, error: "Another task is running" };
4840
+ if (!this.lastPipelineResult) {
4841
+ return { ok: false, task: "report", duration: 0, error: "No pipeline result \u2014 run Pipeline first" };
4842
+ }
4843
+ this.running = true;
4844
+ const start = Date.now();
4845
+ try {
4846
+ this.updateAgent("reporter-croc", { status: "working", currentTask: "Generating reports...", progress: 0 });
4847
+ this.log("\u{1F4CA} \u6C47\u62A5\u9CC4 is generating reports...");
4848
+ const { generateReports: generateReports2 } = await Promise.resolve().then(() => (init_reporters(), reporters_exports));
4849
+ const formats = ["html", "json", "markdown"];
4850
+ const reports = generateReports2(this.lastPipelineResult, formats);
4851
+ this.lastReports = reports;
4852
+ const { resolve: resolvePath } = await import("path");
4853
+ const { writeFileSync: writeFileSync11, mkdirSync: mkdirSync11 } = await import("fs");
4854
+ const outDir = resolvePath(this.cwd, this.config.outDir || "./opencroc-output");
4855
+ mkdirSync11(outDir, { recursive: true });
4856
+ for (const report2 of reports) {
4857
+ const fullPath = resolvePath(outDir, report2.filename);
4858
+ writeFileSync11(fullPath, report2.content, "utf-8");
4859
+ this.log(`\u{1F4C4} Generated ${report2.format} report: ${report2.filename}`);
4860
+ }
4861
+ this.updateAgent("reporter-croc", { status: "done", currentTask: `${reports.length} reports generated`, progress: 100 });
4862
+ this.broadcast("reports:generated", reports.map((r) => ({
4863
+ format: r.format,
4864
+ filename: r.filename,
4865
+ size: r.content.length
4866
+ })));
4867
+ const duration = Date.now() - start;
4868
+ this.log(`\u2705 Reports generated in ${duration}ms`);
4869
+ return { ok: true, task: "report", duration, details: { count: reports.length } };
4870
+ } catch (err) {
4871
+ this.updateAgent("reporter-croc", { status: "error", currentTask: String(err) });
4872
+ this.log(`\u274C Report generation failed: ${err}`, "error");
4873
+ return { ok: false, task: "report", duration: Date.now() - start, error: String(err) };
4874
+ } finally {
4875
+ this.running = false;
4876
+ }
4877
+ }
3764
4878
  // ============ Graph Helpers ============
3765
4879
  updateNodeStatus(type, status) {
3766
4880
  if (!this.cachedGraph) return;
@@ -3986,13 +5100,13 @@ import Fastify from "fastify";
3986
5100
  import fastifyStatic from "@fastify/static";
3987
5101
  import fastifyWebsocket from "@fastify/websocket";
3988
5102
  import { fileURLToPath as fileURLToPath2 } from "url";
3989
- import { dirname as dirname5, join as join13, resolve as resolve8 } from "path";
3990
- 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";
3991
5105
  async function startServer(opts) {
3992
5106
  const app = Fastify({ logger: false });
3993
5107
  await app.register(fastifyWebsocket);
3994
- const webDir = resolve8(__dirname2, "../web");
3995
- if (existsSync15(webDir)) {
5108
+ const webDir = resolve9(__dirname2, "../web");
5109
+ if (existsSync16(webDir)) {
3996
5110
  await app.register(fastifyStatic, {
3997
5111
  root: webDir,
3998
5112
  prefix: "/",
@@ -4013,8 +5127,8 @@ async function startServer(opts) {
4013
5127
  reply.code(404).send({ error: "Not found" });
4014
5128
  return;
4015
5129
  }
4016
- const indexPath = join13(webDir, "index.html");
4017
- if (existsSync15(indexPath)) {
5130
+ const indexPath = join14(webDir, "index.html");
5131
+ if (existsSync16(indexPath)) {
4018
5132
  reply.sendFile("index.html");
4019
5133
  } else {
4020
5134
  reply.code(200).header("content-type", "text/html").send(getEmbeddedHtml());
@@ -4165,7 +5279,7 @@ var init_server = __esm({
4165
5279
  init_agents();
4166
5280
  init_croc_office();
4167
5281
  __filename2 = fileURLToPath2(import.meta.url);
4168
- __dirname2 = dirname5(__filename2);
5282
+ __dirname2 = dirname6(__filename2);
4169
5283
  }
4170
5284
  });
4171
5285
 
@@ -4236,7 +5350,7 @@ program.command("heal").description("Run self-healing loop on failed tests").opt
4236
5350
  await heal2(opts);
4237
5351
  });
4238
5352
  program.command("ci").description("Generate CI/CD pipeline template").option("-p, --platform <name>", "CI platform (github, gitlab)", "github").option("--self-heal", "Include self-healing step").option("--node <versions>", "Node.js versions (comma-separated)", "20.x").action(async (opts) => {
4239
- const { ci: ci2 } = await Promise.resolve().then(() => (init_ci2(), ci_exports));
5353
+ const { ci: ci2 } = await Promise.resolve().then(() => (init_ci2(), ci_exports2));
4240
5354
  await ci2(opts);
4241
5355
  });
4242
5356
  program.command("report").description("Generate pipeline report (HTML/JSON/Markdown)").option("-f, --format <formats>", "Report formats (comma-separated)", "html").option("-o, --output <dir>", "Output directory").action(async (opts) => {