opencroc 1.5.1 → 1.6.0
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 +838 -3
- package/dist/cli/index.js.map +1 -1
- 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,60 @@ 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
|
+
office.runTests().catch(() => {
|
|
4215
|
+
});
|
|
4216
|
+
return { ok: true, message: "Test execution started" };
|
|
4217
|
+
});
|
|
4218
|
+
app.get("/api/test-results", async () => {
|
|
4219
|
+
const metrics = office.getLastExecutionMetrics();
|
|
4220
|
+
if (!metrics) return { ok: false, message: "No tests have been run yet" };
|
|
4221
|
+
const total = metrics.passed + metrics.failed + metrics.skipped + metrics.timedOut;
|
|
4222
|
+
return { ok: true, metrics, total };
|
|
4223
|
+
});
|
|
4224
|
+
app.post("/api/reports/generate", async (_req, reply) => {
|
|
4225
|
+
if (office.isRunning()) {
|
|
4226
|
+
reply.code(409).send({ error: "A task is already running" });
|
|
4227
|
+
return;
|
|
4228
|
+
}
|
|
4229
|
+
office.generateReport().catch(() => {
|
|
4230
|
+
});
|
|
4231
|
+
return { ok: true, message: "Report generation started" };
|
|
4232
|
+
});
|
|
4233
|
+
app.get("/api/reports", async () => {
|
|
4234
|
+
const reports = office.getLastReports();
|
|
4235
|
+
if (reports.length === 0) return { ok: false, message: "No reports generated yet" };
|
|
4236
|
+
return {
|
|
4237
|
+
ok: true,
|
|
4238
|
+
reports: reports.map((r) => ({
|
|
4239
|
+
format: r.format,
|
|
4240
|
+
filename: r.filename,
|
|
4241
|
+
size: r.content.length
|
|
4242
|
+
}))
|
|
4243
|
+
};
|
|
4244
|
+
});
|
|
4245
|
+
app.get("/api/reports/:format", async (req, reply) => {
|
|
4246
|
+
const reports = office.getLastReports();
|
|
4247
|
+
const report2 = reports.find((r) => r.format === req.params.format);
|
|
4248
|
+
if (!report2) {
|
|
4249
|
+
reply.code(404).send({ error: `No ${req.params.format} report found` });
|
|
4250
|
+
return;
|
|
4251
|
+
}
|
|
4252
|
+
const contentType = req.params.format === "html" ? "text/html" : req.params.format === "json" ? "application/json" : "text/markdown";
|
|
4253
|
+
reply.type(contentType).send(report2.content);
|
|
4254
|
+
});
|
|
4255
|
+
app.get("/api/ci/template", async (req) => {
|
|
4256
|
+
const provider = req.query.provider || "github";
|
|
4257
|
+
const { generateGitHubActionsTemplate: generateGitHubActionsTemplate2, generateGitLabCITemplate: generateGitLabCITemplate2 } = await Promise.resolve().then(() => (init_ci(), ci_exports));
|
|
4258
|
+
if (provider === "gitlab") {
|
|
4259
|
+
return { ok: true, provider: "gitlab", template: generateGitLabCITemplate2() };
|
|
4260
|
+
}
|
|
4261
|
+
return { ok: true, provider: "github", template: generateGitHubActionsTemplate2() };
|
|
4262
|
+
});
|
|
3552
4263
|
}
|
|
3553
4264
|
var init_agents = __esm({
|
|
3554
4265
|
"src/server/routes/agents.ts"() {
|
|
@@ -3580,6 +4291,8 @@ var init_croc_office = __esm({
|
|
|
3580
4291
|
running = false;
|
|
3581
4292
|
lastPipelineResult = null;
|
|
3582
4293
|
lastGeneratedFiles = [];
|
|
4294
|
+
lastExecutionMetrics = null;
|
|
4295
|
+
lastReports = [];
|
|
3583
4296
|
constructor(config, cwd) {
|
|
3584
4297
|
this.config = config;
|
|
3585
4298
|
this.cwd = cwd;
|
|
@@ -3761,6 +4474,128 @@ var init_croc_office = __esm({
|
|
|
3761
4474
|
getGeneratedFiles() {
|
|
3762
4475
|
return this.lastGeneratedFiles;
|
|
3763
4476
|
}
|
|
4477
|
+
/** Get last execution metrics */
|
|
4478
|
+
getLastExecutionMetrics() {
|
|
4479
|
+
return this.lastExecutionMetrics;
|
|
4480
|
+
}
|
|
4481
|
+
/** Get last generated reports */
|
|
4482
|
+
getLastReports() {
|
|
4483
|
+
return this.lastReports;
|
|
4484
|
+
}
|
|
4485
|
+
/** Run generated tests with Playwright */
|
|
4486
|
+
async runTests() {
|
|
4487
|
+
if (this.running) return { ok: false, task: "execute", duration: 0, error: "Another task is running" };
|
|
4488
|
+
if (this.lastGeneratedFiles.length === 0) {
|
|
4489
|
+
return { ok: false, task: "execute", duration: 0, error: "No test files \u2014 run Pipeline first" };
|
|
4490
|
+
}
|
|
4491
|
+
this.running = true;
|
|
4492
|
+
const start = Date.now();
|
|
4493
|
+
try {
|
|
4494
|
+
const { resolve: resolvePath } = await import("path");
|
|
4495
|
+
const { execSync } = await import("child_process");
|
|
4496
|
+
const { existsSync: existsSync16 } = await import("fs");
|
|
4497
|
+
const testFiles = this.lastGeneratedFiles.map((f) => resolvePath(this.cwd, f.filePath)).filter((f) => existsSync16(f));
|
|
4498
|
+
if (testFiles.length === 0) {
|
|
4499
|
+
this.log("\u26A0\uFE0F No test files found on disk", "warn");
|
|
4500
|
+
return { ok: false, task: "execute", duration: Date.now() - start, error: "No test files found on disk" };
|
|
4501
|
+
}
|
|
4502
|
+
this.updateAgent("tester-croc", { status: "working", currentTask: `Running ${testFiles.length} test files...`, progress: 0 });
|
|
4503
|
+
this.log(`\u{1F9EA} \u6D4B\u8BD5\u9CC4 is running ${testFiles.length} Playwright tests...`);
|
|
4504
|
+
this.updateAgent("healer-croc", { status: "thinking", currentTask: "Monitoring test run...", progress: 0 });
|
|
4505
|
+
let stdout2 = "", stderr = "";
|
|
4506
|
+
try {
|
|
4507
|
+
const result = execSync(
|
|
4508
|
+
`npx playwright test ${testFiles.map((f) => `"${f}"`).join(" ")} --reporter=line 2>&1`,
|
|
4509
|
+
{ cwd: this.cwd, encoding: "utf-8", timeout: 3e5, stdio: "pipe" }
|
|
4510
|
+
);
|
|
4511
|
+
stdout2 = result;
|
|
4512
|
+
} catch (err) {
|
|
4513
|
+
const execErr = err;
|
|
4514
|
+
stdout2 = execErr.stdout || "";
|
|
4515
|
+
stderr = execErr.stderr || "";
|
|
4516
|
+
}
|
|
4517
|
+
const output = stdout2 + "\n" + stderr;
|
|
4518
|
+
const metrics = { passed: 0, failed: 0, skipped: 0, timedOut: 0 };
|
|
4519
|
+
const passedMatch = output.match(/(\d+)\s+passed/);
|
|
4520
|
+
const failedMatch = output.match(/(\d+)\s+failed/);
|
|
4521
|
+
const skippedMatch = output.match(/(\d+)\s+skipped/);
|
|
4522
|
+
const timedOutMatch = output.match(/(\d+)\s+timed?\s*out/i);
|
|
4523
|
+
if (passedMatch) metrics.passed = parseInt(passedMatch[1], 10);
|
|
4524
|
+
if (failedMatch) metrics.failed = parseInt(failedMatch[1], 10);
|
|
4525
|
+
if (skippedMatch) metrics.skipped = parseInt(skippedMatch[1], 10);
|
|
4526
|
+
if (timedOutMatch) metrics.timedOut = parseInt(timedOutMatch[1], 10);
|
|
4527
|
+
this.lastExecutionMetrics = metrics;
|
|
4528
|
+
const total = metrics.passed + metrics.failed + metrics.skipped + metrics.timedOut;
|
|
4529
|
+
if (metrics.failed > 0) {
|
|
4530
|
+
this.updateAgent("tester-croc", { status: "error", currentTask: `${metrics.failed} tests failed`, progress: 100 });
|
|
4531
|
+
this.updateAgent("healer-croc", { status: "working", currentTask: `Analyzing ${metrics.failed} failures...`, progress: 50 });
|
|
4532
|
+
this.log(`\u274C Tests: ${metrics.passed} passed, ${metrics.failed} failed, ${metrics.skipped} skipped`, "warn");
|
|
4533
|
+
const { categorizeFailure: categorizeFailure2 } = await Promise.resolve().then(() => (init_self_healing(), self_healing_exports));
|
|
4534
|
+
const failLines = output.split("\n").filter((l) => /fail|error|timeout/i.test(l)).slice(0, 5);
|
|
4535
|
+
for (const line of failLines) {
|
|
4536
|
+
const cat = categorizeFailure2(line);
|
|
4537
|
+
this.log(` \u{1F50D} ${cat.category} (${Math.round(cat.confidence * 100)}%): ${line.substring(0, 100)}`, "warn");
|
|
4538
|
+
}
|
|
4539
|
+
this.updateAgent("healer-croc", { status: "done", currentTask: "Failure analysis done", progress: 100 });
|
|
4540
|
+
} else {
|
|
4541
|
+
this.updateAgent("tester-croc", { status: "done", currentTask: `All ${metrics.passed} tests passed!`, progress: 100 });
|
|
4542
|
+
this.updateAgent("healer-croc", { status: "done", currentTask: "No failures", progress: 100 });
|
|
4543
|
+
this.log(`\u2705 All ${metrics.passed} tests passed!`);
|
|
4544
|
+
}
|
|
4545
|
+
this.updateNodeStatus("controller", metrics.failed > 0 ? "failed" : "passed");
|
|
4546
|
+
this.broadcast("test:complete", { metrics, total });
|
|
4547
|
+
const duration = Date.now() - start;
|
|
4548
|
+
this.log(`\u{1F9EA} Test execution complete in ${duration}ms`);
|
|
4549
|
+
return { ok: metrics.failed === 0, task: "execute", duration, details: metrics };
|
|
4550
|
+
} catch (err) {
|
|
4551
|
+
this.updateAgent("tester-croc", { status: "error", currentTask: String(err) });
|
|
4552
|
+
this.log(`\u274C Test execution failed: ${err}`, "error");
|
|
4553
|
+
return { ok: false, task: "execute", duration: Date.now() - start, error: String(err) };
|
|
4554
|
+
} finally {
|
|
4555
|
+
this.running = false;
|
|
4556
|
+
}
|
|
4557
|
+
}
|
|
4558
|
+
/** Generate reports (HTML/JSON/Markdown) */
|
|
4559
|
+
async generateReport() {
|
|
4560
|
+
if (this.running) return { ok: false, task: "report", duration: 0, error: "Another task is running" };
|
|
4561
|
+
if (!this.lastPipelineResult) {
|
|
4562
|
+
return { ok: false, task: "report", duration: 0, error: "No pipeline result \u2014 run Pipeline first" };
|
|
4563
|
+
}
|
|
4564
|
+
this.running = true;
|
|
4565
|
+
const start = Date.now();
|
|
4566
|
+
try {
|
|
4567
|
+
this.updateAgent("reporter-croc", { status: "working", currentTask: "Generating reports...", progress: 0 });
|
|
4568
|
+
this.log("\u{1F4CA} \u6C47\u62A5\u9CC4 is generating reports...");
|
|
4569
|
+
const { generateReports: generateReports2 } = await Promise.resolve().then(() => (init_reporters(), reporters_exports));
|
|
4570
|
+
const formats = ["html", "json", "markdown"];
|
|
4571
|
+
const reports = generateReports2(this.lastPipelineResult, formats);
|
|
4572
|
+
this.lastReports = reports;
|
|
4573
|
+
const { resolve: resolvePath } = await import("path");
|
|
4574
|
+
const { writeFileSync: writeFileSync10, mkdirSync: mkdirSync10 } = await import("fs");
|
|
4575
|
+
const outDir = resolvePath(this.cwd, this.config.outDir || "./opencroc-output");
|
|
4576
|
+
mkdirSync10(outDir, { recursive: true });
|
|
4577
|
+
for (const report2 of reports) {
|
|
4578
|
+
const fullPath = resolvePath(outDir, report2.filename);
|
|
4579
|
+
writeFileSync10(fullPath, report2.content, "utf-8");
|
|
4580
|
+
this.log(`\u{1F4C4} Generated ${report2.format} report: ${report2.filename}`);
|
|
4581
|
+
}
|
|
4582
|
+
this.updateAgent("reporter-croc", { status: "done", currentTask: `${reports.length} reports generated`, progress: 100 });
|
|
4583
|
+
this.broadcast("reports:generated", reports.map((r) => ({
|
|
4584
|
+
format: r.format,
|
|
4585
|
+
filename: r.filename,
|
|
4586
|
+
size: r.content.length
|
|
4587
|
+
})));
|
|
4588
|
+
const duration = Date.now() - start;
|
|
4589
|
+
this.log(`\u2705 Reports generated in ${duration}ms`);
|
|
4590
|
+
return { ok: true, task: "report", duration, details: { count: reports.length } };
|
|
4591
|
+
} catch (err) {
|
|
4592
|
+
this.updateAgent("reporter-croc", { status: "error", currentTask: String(err) });
|
|
4593
|
+
this.log(`\u274C Report generation failed: ${err}`, "error");
|
|
4594
|
+
return { ok: false, task: "report", duration: Date.now() - start, error: String(err) };
|
|
4595
|
+
} finally {
|
|
4596
|
+
this.running = false;
|
|
4597
|
+
}
|
|
4598
|
+
}
|
|
3764
4599
|
// ============ Graph Helpers ============
|
|
3765
4600
|
updateNodeStatus(type, status) {
|
|
3766
4601
|
if (!this.cachedGraph) return;
|
|
@@ -4236,7 +5071,7 @@ program.command("heal").description("Run self-healing loop on failed tests").opt
|
|
|
4236
5071
|
await heal2(opts);
|
|
4237
5072
|
});
|
|
4238
5073
|
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(),
|
|
5074
|
+
const { ci: ci2 } = await Promise.resolve().then(() => (init_ci2(), ci_exports2));
|
|
4240
5075
|
await ci2(opts);
|
|
4241
5076
|
});
|
|
4242
5077
|
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) => {
|