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 +1128 -14
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +9 -0
- package/dist/web/index.html +90 -1
- package/package.json +1 -1
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
|
|
2001
|
-
__export(
|
|
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:
|
|
3699
|
-
const { dirname:
|
|
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
|
-
|
|
3704
|
-
|
|
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
|
|
3990
|
-
import { existsSync as
|
|
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 =
|
|
3995
|
-
if (
|
|
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 =
|
|
4017
|
-
if (
|
|
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 =
|
|
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(),
|
|
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) => {
|