opencroc 1.5.0 → 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 +873 -17
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +34 -13
- package/dist/index.js.map +1 -1
- package/dist/web/index.html +90 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -1289,11 +1289,32 @@ function createPipeline(config) {
|
|
|
1289
1289
|
validationErrors: [],
|
|
1290
1290
|
duration: 0
|
|
1291
1291
|
};
|
|
1292
|
+
const backendRoot = path5.resolve(config.backendRoot);
|
|
1293
|
+
const findDir = (name) => {
|
|
1294
|
+
const candidates = [
|
|
1295
|
+
path5.join(backendRoot, name),
|
|
1296
|
+
// ./models
|
|
1297
|
+
path5.join(backendRoot, "src", name),
|
|
1298
|
+
// ./src/models
|
|
1299
|
+
path5.join(backendRoot, "backend", "src", name),
|
|
1300
|
+
// ./backend/src/models
|
|
1301
|
+
path5.join(backendRoot, "backend", name),
|
|
1302
|
+
// ./backend/models
|
|
1303
|
+
path5.join(backendRoot, "server", "src", name),
|
|
1304
|
+
// ./server/src/models
|
|
1305
|
+
path5.join(backendRoot, "app", name)
|
|
1306
|
+
// ./app/models
|
|
1307
|
+
];
|
|
1308
|
+
for (const c of candidates) {
|
|
1309
|
+
if (fs4.existsSync(c)) return c;
|
|
1310
|
+
}
|
|
1311
|
+
return null;
|
|
1312
|
+
};
|
|
1313
|
+
const modelsRoot = findDir("models");
|
|
1314
|
+
const controllersRoot = findDir("controllers");
|
|
1292
1315
|
if (activeSteps.includes("scan")) {
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
if (fs4.existsSync(modelsDir)) {
|
|
1296
|
-
const dirs = fs4.readdirSync(modelsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
1316
|
+
if (modelsRoot) {
|
|
1317
|
+
const dirs = fs4.readdirSync(modelsRoot, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
1297
1318
|
const moduleFilter = config.modules;
|
|
1298
1319
|
for (const dir of dirs) {
|
|
1299
1320
|
if (moduleFilter && !moduleFilter.includes(dir)) continue;
|
|
@@ -1302,20 +1323,20 @@ function createPipeline(config) {
|
|
|
1302
1323
|
if (result.modules.length === 0) {
|
|
1303
1324
|
result.modules.push("default");
|
|
1304
1325
|
} else {
|
|
1305
|
-
const rootFiles = fs4.readdirSync(
|
|
1326
|
+
const rootFiles = fs4.readdirSync(modelsRoot).filter((f) => f.endsWith(".ts") && !f.endsWith(".test.ts") && f !== "index.ts");
|
|
1306
1327
|
if (rootFiles.length > 0) {
|
|
1307
1328
|
result.modules.unshift("default");
|
|
1308
1329
|
}
|
|
1309
1330
|
}
|
|
1310
1331
|
}
|
|
1311
1332
|
}
|
|
1312
|
-
const resolveModelDir = (
|
|
1313
|
-
const resolveControllerDir = (
|
|
1333
|
+
const resolveModelDir = (_backendRoot, mod) => mod === "default" ? modelsRoot || path5.join(backendRoot, "models") : path5.join(modelsRoot || path5.join(backendRoot, "models"), mod);
|
|
1334
|
+
const resolveControllerDir = (_backendRoot, mod) => mod === "default" ? controllersRoot || path5.join(backendRoot, "controllers") : path5.join(controllersRoot || path5.join(backendRoot, "controllers"), mod);
|
|
1314
1335
|
if (activeSteps.includes("er-diagram")) {
|
|
1315
1336
|
const erGen = createERDiagramGenerator();
|
|
1316
|
-
const
|
|
1337
|
+
const backendRoot2 = path5.resolve(config.backendRoot);
|
|
1317
1338
|
for (const mod of result.modules) {
|
|
1318
|
-
const modelDir = resolveModelDir(
|
|
1339
|
+
const modelDir = resolveModelDir(backendRoot2, mod);
|
|
1319
1340
|
const tables = fs4.existsSync(modelDir) ? parseModuleModels(modelDir) : [];
|
|
1320
1341
|
const relations = [];
|
|
1321
1342
|
const assocFile = path5.join(modelDir, "associations.ts");
|
|
@@ -1338,9 +1359,9 @@ function createPipeline(config) {
|
|
|
1338
1359
|
}
|
|
1339
1360
|
if (activeSteps.includes("api-chain")) {
|
|
1340
1361
|
const chainAnalyzer = createApiChainAnalyzer();
|
|
1341
|
-
const
|
|
1362
|
+
const backendRoot2 = path5.resolve(config.backendRoot);
|
|
1342
1363
|
for (const mod of result.modules) {
|
|
1343
|
-
const controllerDir = resolveControllerDir(
|
|
1364
|
+
const controllerDir = resolveControllerDir(backendRoot2, mod);
|
|
1344
1365
|
const endpoints = fs4.existsSync(controllerDir) ? parseControllerDirectory(controllerDir) : [];
|
|
1345
1366
|
const analysis = chainAnalyzer.analyze(endpoints);
|
|
1346
1367
|
analysis.moduleName = mod;
|
|
@@ -1357,10 +1378,10 @@ function createPipeline(config) {
|
|
|
1357
1378
|
}
|
|
1358
1379
|
}
|
|
1359
1380
|
if (activeSteps.includes("plan")) {
|
|
1360
|
-
const
|
|
1381
|
+
const backendRoot2 = path5.resolve(config.backendRoot);
|
|
1361
1382
|
const chainAnalyzer = createApiChainAnalyzer();
|
|
1362
1383
|
for (const mod of result.modules) {
|
|
1363
|
-
const controllerDir = resolveControllerDir(
|
|
1384
|
+
const controllerDir = resolveControllerDir(backendRoot2, mod);
|
|
1364
1385
|
const endpoints = fs4.existsSync(controllerDir) ? parseControllerDirectory(controllerDir) : [];
|
|
1365
1386
|
const analysis = chainAnalyzer.analyze(endpoints);
|
|
1366
1387
|
const topoOrder = topologicalSort(analysis.dag);
|
|
@@ -1708,6 +1729,7 @@ var init_ollama = __esm({
|
|
|
1708
1729
|
});
|
|
1709
1730
|
|
|
1710
1731
|
// src/llm/index.ts
|
|
1732
|
+
var SYSTEM_PROMPTS;
|
|
1711
1733
|
var init_llm = __esm({
|
|
1712
1734
|
"src/llm/index.ts"() {
|
|
1713
1735
|
"use strict";
|
|
@@ -1716,36 +1738,336 @@ var init_llm = __esm({
|
|
|
1716
1738
|
init_ollama();
|
|
1717
1739
|
init_openai();
|
|
1718
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
|
+
};
|
|
1719
1751
|
}
|
|
1720
1752
|
});
|
|
1721
1753
|
|
|
1722
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;
|
|
1723
1849
|
var init_dialog_loop_runner = __esm({
|
|
1724
1850
|
"src/self-healing/dialog-loop-runner.ts"() {
|
|
1725
1851
|
"use strict";
|
|
1726
1852
|
init_esm_shims();
|
|
1853
|
+
DEFAULTS2 = {
|
|
1854
|
+
maxIterations: 3,
|
|
1855
|
+
pollIntervalMs: 1e4,
|
|
1856
|
+
sameErrorThreshold: 2,
|
|
1857
|
+
autoRerunOnFix: true
|
|
1858
|
+
};
|
|
1727
1859
|
}
|
|
1728
1860
|
});
|
|
1729
1861
|
|
|
1730
1862
|
// src/self-healing/controlled-fixer.ts
|
|
1731
1863
|
import { existsSync as existsSync8, copyFileSync, readFileSync as readFileSync2, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, unlinkSync } from "fs";
|
|
1732
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;
|
|
1733
1932
|
var init_controlled_fixer = __esm({
|
|
1734
1933
|
"src/self-healing/controlled-fixer.ts"() {
|
|
1735
1934
|
"use strict";
|
|
1736
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
|
+
};
|
|
1737
1947
|
}
|
|
1738
1948
|
});
|
|
1739
1949
|
|
|
1740
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;
|
|
1741
1997
|
var init_auto_fix_generator = __esm({
|
|
1742
1998
|
"src/self-healing/auto-fix-generator.ts"() {
|
|
1743
1999
|
"use strict";
|
|
1744
2000
|
init_esm_shims();
|
|
2001
|
+
DEFAULT_OPTIONS = {
|
|
2002
|
+
branchPrefix: "autofix/",
|
|
2003
|
+
baseBranch: "main",
|
|
2004
|
+
draftOnly: true
|
|
2005
|
+
};
|
|
1745
2006
|
}
|
|
1746
2007
|
});
|
|
1747
2008
|
|
|
1748
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
|
+
}
|
|
1749
2071
|
async function attemptConfigFix(_testResultsDir, _mode, _llm) {
|
|
1750
2072
|
return {
|
|
1751
2073
|
success: false,
|
|
@@ -1847,6 +2169,13 @@ var init_heal = __esm({
|
|
|
1847
2169
|
});
|
|
1848
2170
|
|
|
1849
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
|
+
});
|
|
1850
2179
|
function generateGitHubActionsTemplate(opts = {}) {
|
|
1851
2180
|
const nodeVersions = opts.nodeVersions ?? ["20.x"];
|
|
1852
2181
|
const install = opts.installCommand ?? "npm ci";
|
|
@@ -1976,8 +2305,8 @@ var init_ci = __esm({
|
|
|
1976
2305
|
});
|
|
1977
2306
|
|
|
1978
2307
|
// src/cli/commands/ci.ts
|
|
1979
|
-
var
|
|
1980
|
-
__export(
|
|
2308
|
+
var ci_exports2 = {};
|
|
2309
|
+
__export(ci_exports2, {
|
|
1981
2310
|
ci: () => ci
|
|
1982
2311
|
});
|
|
1983
2312
|
import * as fs5 from "fs";
|
|
@@ -2023,6 +2352,143 @@ var init_ci2 = __esm({
|
|
|
2023
2352
|
});
|
|
2024
2353
|
|
|
2025
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
|
+
}
|
|
2026
2492
|
var init_checklist_reporter = __esm({
|
|
2027
2493
|
"src/reporters/checklist-reporter.ts"() {
|
|
2028
2494
|
"use strict";
|
|
@@ -2031,6 +2497,99 @@ var init_checklist_reporter = __esm({
|
|
|
2031
2497
|
});
|
|
2032
2498
|
|
|
2033
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
|
+
}
|
|
2034
2593
|
var init_workorder_reporter = __esm({
|
|
2035
2594
|
"src/reporters/workorder-reporter.ts"() {
|
|
2036
2595
|
"use strict";
|
|
@@ -2039,14 +2598,133 @@ var init_workorder_reporter = __esm({
|
|
|
2039
2598
|
});
|
|
2040
2599
|
|
|
2041
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;
|
|
2042
2645
|
var init_token_reporter = __esm({
|
|
2043
2646
|
"src/reporters/token-reporter.ts"() {
|
|
2044
2647
|
"use strict";
|
|
2045
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
|
+
};
|
|
2046
2707
|
}
|
|
2047
2708
|
});
|
|
2048
2709
|
|
|
2049
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
|
+
});
|
|
2050
2728
|
function generateJsonReport(result) {
|
|
2051
2729
|
const serializable = {
|
|
2052
2730
|
modules: result.modules,
|
|
@@ -3528,6 +4206,60 @@ function registerAgentRoutes(app, office) {
|
|
|
3528
4206
|
duration: result.duration
|
|
3529
4207
|
};
|
|
3530
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
|
+
});
|
|
3531
4263
|
}
|
|
3532
4264
|
var init_agents = __esm({
|
|
3533
4265
|
"src/server/routes/agents.ts"() {
|
|
@@ -3559,6 +4291,8 @@ var init_croc_office = __esm({
|
|
|
3559
4291
|
running = false;
|
|
3560
4292
|
lastPipelineResult = null;
|
|
3561
4293
|
lastGeneratedFiles = [];
|
|
4294
|
+
lastExecutionMetrics = null;
|
|
4295
|
+
lastReports = [];
|
|
3562
4296
|
constructor(config, cwd) {
|
|
3563
4297
|
this.config = config;
|
|
3564
4298
|
this.cwd = cwd;
|
|
@@ -3640,7 +4374,7 @@ var init_croc_office = __esm({
|
|
|
3640
4374
|
const pipelineConfig = { ...this.config, backendRoot };
|
|
3641
4375
|
const pipeline = createPipeline2(pipelineConfig);
|
|
3642
4376
|
this.updateAgent("parser-croc", { status: "working", currentTask: "Scanning source code...", progress: 10 });
|
|
3643
|
-
this.log(
|
|
4377
|
+
this.log(`\u{1F40A} \u89E3\u6790\u9CC4 scanning from: ${backendRoot}`);
|
|
3644
4378
|
this.invalidateCache();
|
|
3645
4379
|
await this.buildKnowledgeGraph();
|
|
3646
4380
|
this.updateNodeStatus("module", "testing");
|
|
@@ -3740,6 +4474,128 @@ var init_croc_office = __esm({
|
|
|
3740
4474
|
getGeneratedFiles() {
|
|
3741
4475
|
return this.lastGeneratedFiles;
|
|
3742
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
|
+
}
|
|
3743
4599
|
// ============ Graph Helpers ============
|
|
3744
4600
|
updateNodeStatus(type, status) {
|
|
3745
4601
|
if (!this.cachedGraph) return;
|
|
@@ -4215,7 +5071,7 @@ program.command("heal").description("Run self-healing loop on failed tests").opt
|
|
|
4215
5071
|
await heal2(opts);
|
|
4216
5072
|
});
|
|
4217
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) => {
|
|
4218
|
-
const { ci: ci2 } = await Promise.resolve().then(() => (init_ci2(),
|
|
5074
|
+
const { ci: ci2 } = await Promise.resolve().then(() => (init_ci2(), ci_exports2));
|
|
4219
5075
|
await ci2(opts);
|
|
4220
5076
|
});
|
|
4221
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) => {
|