@detiq/crawler 1.1.10 → 1.1.12
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/crawler.js +52 -6
- package/dist/crawler.js.map +2 -2
- package/dist/job-queue.js +499 -10
- package/dist/job-queue.js.map +2 -2
- package/dist/locator-extractor.js +35 -1
- package/dist/locator-extractor.js.map +2 -2
- package/dist/test-executor.js +102 -0
- package/dist/test-executor.js.map +2 -2
- package/package.json +1 -1
- package/src/crawler.ts +59 -7
- package/src/job-queue.ts +469 -7
- package/src/locator-extractor.ts +41 -3
- package/src/test-executor.ts +122 -0
- package/types/crawler.d.ts +9 -0
- package/types/locator-extractor.d.ts +2 -0
- package/types/test-executor.d.ts +7 -0
package/dist/job-queue.js
CHANGED
|
@@ -30,13 +30,14 @@ async function notifyAdmins(tenantId, data) {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
import { AppBrain, buildArtifactKey, getTenantStorageConfig, uploadScreenshotToTenantStorage, uploadFileToTenantStorage, notifyRunCompletion, recordLLMCall } from "@detiq/app-brain";
|
|
33
|
-
import { generateTaxonomySkillFiles, groupScreensWithAI, generateExecutionPlan, refreshTestCasesSkillFile, refreshTestDataSkillFile, refreshTestScriptSkillFile, refreshRequirementsCoverageSkillFile, detectFieldsFromHtml, mergeIntoCatalog } from "@detiq/agents";
|
|
34
|
-
import { crawlProject } from "./crawler.js";
|
|
33
|
+
import { generateTaxonomySkillFiles, groupScreensWithAI, generateExecutionPlan, refreshTestCasesSkillFile, refreshTestDataSkillFile, refreshTestScriptSkillFile, refreshRequirementsCoverageSkillFile, refreshTestExecutionHistorySkillFile, refreshVisualRegressionHistorySkillFile, refreshLocatorStrategyWithHealingData, refreshFieldCatalogSkillFile, detectFieldsFromHtml, mergeIntoCatalog } from "@detiq/agents";
|
|
34
|
+
import { crawlProject, domFallbackElements } from "./crawler.js";
|
|
35
35
|
import { httpCrawl } from "./http-engine.js";
|
|
36
36
|
import { hashContent, normalizeText } from "./incremental-diff.js";
|
|
37
37
|
import { discoverProjectUrls } from "./sitemap.js";
|
|
38
|
-
import { executeTestPlan } from "./test-executor.js";
|
|
38
|
+
import { executeExecutionStep, executeTestPlan } from "./test-executor.js";
|
|
39
39
|
import { runNativeCrawl } from "./native-crawler.js";
|
|
40
|
+
import { elementsToPlaywrightLocators } from "./locator-extractor.js";
|
|
40
41
|
import { jira, confluence, figma } from "@detiq/connectors";
|
|
41
42
|
import { decrypt, config as platformConfig, dispatchWebhook, isFeatureEnabled, interpolatePlaceholders } from "@detiq/core";
|
|
42
43
|
import { createHmac } from "node:crypto";
|
|
@@ -76,7 +77,7 @@ function makeJobName(type) {
|
|
|
76
77
|
const now = /* @__PURE__ */ new Date();
|
|
77
78
|
const date = now.toISOString().slice(0, 10);
|
|
78
79
|
const ts = now.getTime();
|
|
79
|
-
const label = type === "CRAWL" ? "Crawl" : type === "NATIVE_CRAWL" ? "NativeCrawl" : type === "GENERATE_TESTS" ? "GenerateTests" : type === "VISUAL_REGRESSION" ? "VisualRegression" : type === "AI_EDIT" ? "AIEdit" : type === "API_TESTS" ? "ApiTests" : type === "SECURITY_TESTS" ? "SecurityTests" : type === "INTEGRATION_TESTS" ? "IntegrationTests" : type === "GENERATE_TESTS_FROM_REQS" ? "GenerateTestsFromReqs" : type === "GENERATE_DATA" ? "GenerateData" : type === "GENERATE_SCRIPT" ? "GenerateScript" : type === "GENERATE_TEST_PLAN" ? "GenerateTestPlan" : type === "IMPORT_TESTS" ? "ImportTests" : type === "GENERATE_SCREEN_TESTS" ? "GenerateScreenTests" : type === "BATCH_GENERATE_ALIGNED" ? "BatchGenerateAligned" : type === "CREATE_PR" ? "CreatePR" : type === "NEW_PAGES_PIPELINE" ? "NewPagesPipeline" : type.charAt(0) + type.slice(1).toLowerCase().replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
80
|
+
const label = type === "CRAWL" ? "Crawl" : type === "CAPTURE_STEPS" ? "CaptureSteps" : type === "NATIVE_CRAWL" ? "NativeCrawl" : type === "GENERATE_TESTS" ? "GenerateTests" : type === "VISUAL_REGRESSION" ? "VisualRegression" : type === "AI_EDIT" ? "AIEdit" : type === "API_TESTS" ? "ApiTests" : type === "SECURITY_TESTS" ? "SecurityTests" : type === "INTEGRATION_TESTS" ? "IntegrationTests" : type === "GENERATE_TESTS_FROM_REQS" ? "GenerateTestsFromReqs" : type === "GENERATE_DATA" ? "GenerateData" : type === "GENERATE_SCRIPT" ? "GenerateScript" : type === "GENERATE_TEST_PLAN" ? "GenerateTestPlan" : type === "IMPORT_TESTS" ? "ImportTests" : type === "GENERATE_SCREEN_TESTS" ? "GenerateScreenTests" : type === "BATCH_GENERATE_ALIGNED" ? "BatchGenerateAligned" : type === "CREATE_PR" ? "CreatePR" : type === "NEW_PAGES_PIPELINE" ? "NewPagesPipeline" : type.charAt(0) + type.slice(1).toLowerCase().replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
80
81
|
return `${label}_${date}_${ts}`;
|
|
81
82
|
}
|
|
82
83
|
const runningControllers = /* @__PURE__ */ new Map();
|
|
@@ -548,6 +549,7 @@ async function processJob(jobId, config, opts) {
|
|
|
548
549
|
}
|
|
549
550
|
if (fieldDetectAdded > 0 || detectLimit > 0) {
|
|
550
551
|
logger.info({ screens: detectLimit, added: fieldDetectAdded, projectId }, "[field-detect] catalog populated from crawl");
|
|
552
|
+
refreshFieldCatalogSkillFile(tenantId, projectId).catch(console.error);
|
|
551
553
|
}
|
|
552
554
|
} catch (fdErr) {
|
|
553
555
|
logger.warn({ err: fdErr }, "[field-detect] post-crawl field detection failed");
|
|
@@ -1234,6 +1236,7 @@ async function processRunTestSuiteJob(jobId, config) {
|
|
|
1234
1236
|
{ passed, failed, total: testCases.length },
|
|
1235
1237
|
runResults
|
|
1236
1238
|
).catch(console.error);
|
|
1239
|
+
refreshTestExecutionHistorySkillFile(tenantId, projectId).catch(console.error);
|
|
1237
1240
|
} catch (err) {
|
|
1238
1241
|
const msg = String(err?.message ?? err).slice(0, 500);
|
|
1239
1242
|
logger.error({ jobId, testRunId, err: msg }, "processRunTestSuiteJob failed");
|
|
@@ -1273,7 +1276,11 @@ async function processGenerateSkillFilesJob(jobId, config) {
|
|
|
1273
1276
|
refreshTestCasesSkillFile(tenantId, projectId),
|
|
1274
1277
|
refreshTestDataSkillFile(tenantId, projectId),
|
|
1275
1278
|
refreshTestScriptSkillFile(tenantId, projectId),
|
|
1276
|
-
refreshRequirementsCoverageSkillFile(tenantId, projectId)
|
|
1279
|
+
refreshRequirementsCoverageSkillFile(tenantId, projectId),
|
|
1280
|
+
refreshTestExecutionHistorySkillFile(tenantId, projectId),
|
|
1281
|
+
refreshVisualRegressionHistorySkillFile(tenantId, projectId),
|
|
1282
|
+
refreshLocatorStrategyWithHealingData(tenantId, projectId),
|
|
1283
|
+
refreshFieldCatalogSkillFile(tenantId, projectId)
|
|
1277
1284
|
]);
|
|
1278
1285
|
await withTenant(
|
|
1279
1286
|
tenantId,
|
|
@@ -1803,8 +1810,241 @@ async function processGenerateScreenTestsJob(jobId, config) {
|
|
|
1803
1810
|
});
|
|
1804
1811
|
}
|
|
1805
1812
|
}
|
|
1813
|
+
function captureStepText(step) {
|
|
1814
|
+
if (typeof step === "string") return step;
|
|
1815
|
+
if (!step || typeof step !== "object") return "";
|
|
1816
|
+
const obj = step;
|
|
1817
|
+
return [obj.action, obj.text, obj.label, obj.data, obj.expected].filter((v) => typeof v === "string" && v.trim().length > 0).join(" -> ");
|
|
1818
|
+
}
|
|
1819
|
+
function firstQuoted(text) {
|
|
1820
|
+
return (text.match(/"([^"]+)"/)?.[1] ?? text.match(/'([^']+)'/)?.[1])?.trim();
|
|
1821
|
+
}
|
|
1822
|
+
function fallbackExecutionPlan(rawSteps) {
|
|
1823
|
+
const plan = [];
|
|
1824
|
+
for (const raw of rawSteps) {
|
|
1825
|
+
const text = captureStepText(raw).trim();
|
|
1826
|
+
if (!text) continue;
|
|
1827
|
+
const stepNumber = plan.length + 1;
|
|
1828
|
+
const url = text.match(/https?:\/\/[^\s"']+/)?.[0];
|
|
1829
|
+
if (/^navigate\b|^go to\b|^open\b/i.test(text) && url) {
|
|
1830
|
+
plan.push({ stepNumber, description: text, action: "navigate", url });
|
|
1831
|
+
continue;
|
|
1832
|
+
}
|
|
1833
|
+
const urlContains = text.match(/verify url contains:\s*["']?([^"']+)["']?/i);
|
|
1834
|
+
if (urlContains) {
|
|
1835
|
+
plan.push({ stepNumber, description: text, action: "assert_url", value: urlContains[1].trim() });
|
|
1836
|
+
continue;
|
|
1837
|
+
}
|
|
1838
|
+
const inputMatch = text.match(/(?:enter|type|fill)\s+["']([^"']+)["']\s+(?:in|into)\s+(.+?)(?:\s+field|\s+input|\s+box|\s+area)?(?:\s*→.*)?$/i);
|
|
1839
|
+
if (inputMatch) {
|
|
1840
|
+
const selector = inputMatch[2].replace(/^(the\s+)?/i, "").replace(/\s+(field|input|box|area)$/i, "").trim();
|
|
1841
|
+
plan.push({ stepNumber, description: text, action: "fill", selectorStrategy: "label", selector, value: inputMatch[1].trim() });
|
|
1842
|
+
continue;
|
|
1843
|
+
}
|
|
1844
|
+
const quoted = firstQuoted(text);
|
|
1845
|
+
if (/^(click|tap|navigate menu|check|select)\b/i.test(text) && quoted) {
|
|
1846
|
+
plan.push({ stepNumber, description: text, action: "click", selectorStrategy: "text", selector: quoted });
|
|
1847
|
+
continue;
|
|
1848
|
+
}
|
|
1849
|
+
if (/^(verify|assert|expect)\b/i.test(text) && quoted) {
|
|
1850
|
+
plan.push({ stepNumber, description: text, action: "assert_visible", selectorStrategy: "text", selector: quoted });
|
|
1851
|
+
continue;
|
|
1852
|
+
}
|
|
1853
|
+
if (/execute script/i.test(text)) {
|
|
1854
|
+
const script = text.split(":").slice(1).join(":").replace(/\s*→.*$/, "").trim();
|
|
1855
|
+
if (script) plan.push({ stepNumber, description: text, action: "script", value: script });
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
return plan;
|
|
1859
|
+
}
|
|
1860
|
+
function screenNameFromUrl(url, fallback) {
|
|
1861
|
+
try {
|
|
1862
|
+
const u = new URL(url);
|
|
1863
|
+
return u.pathname.replace(/\/+$/, "").split("/").filter(Boolean).pop() || u.hostname || fallback;
|
|
1864
|
+
} catch {
|
|
1865
|
+
return fallback;
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
async function processCaptureStepsJob(jobId, config) {
|
|
1869
|
+
const { tenantId, projectId, testCaseId } = config;
|
|
1870
|
+
const screenshotDir = config.screenshotDir || platformConfig.screenshotDir || process.env.SCREENSHOT_DIR || "/tmp/zeta-screenshots";
|
|
1871
|
+
await fs.mkdir(screenshotDir, { recursive: true });
|
|
1872
|
+
await withTenant(
|
|
1873
|
+
tenantId,
|
|
1874
|
+
(tx) => tx.platformJob.update({ where: { id: jobId }, data: { status: "RUNNING", startedAt: /* @__PURE__ */ new Date(), progress: 2 } })
|
|
1875
|
+
);
|
|
1876
|
+
let browser;
|
|
1877
|
+
try {
|
|
1878
|
+
await updateJobProgress(tenantId, jobId, 5, "Loading test case");
|
|
1879
|
+
const [tc, crawlCfg, tenantStorageCfg] = await Promise.all([
|
|
1880
|
+
AppBrain.getTestCaseForExecution(tenantId, testCaseId),
|
|
1881
|
+
AppBrain.getCrawlConfig(tenantId, projectId).catch(() => null),
|
|
1882
|
+
getTenantStorageConfig(tenantId).catch(() => null)
|
|
1883
|
+
]);
|
|
1884
|
+
if (!tc) throw new Error("Test case not found");
|
|
1885
|
+
const rawSteps = Array.isArray(tc.steps) ? tc.steps : [];
|
|
1886
|
+
const stepTexts = rawSteps.map(captureStepText).filter(Boolean);
|
|
1887
|
+
if (stepTexts.length === 0) throw new Error("Test case has no steps to capture");
|
|
1888
|
+
const baseUrl = config.baseUrl || tc?.screen?.project?.appUrl || crawlCfg?.appUrl || "";
|
|
1889
|
+
let plan = await generateExecutionPlan(tenantId, { ...tc, steps: stepTexts }, baseUrl).catch(() => []);
|
|
1890
|
+
plan = Array.isArray(plan) ? plan.filter(Boolean).map((step, idx) => ({ ...step, stepNumber: idx + 1 })) : [];
|
|
1891
|
+
if (plan.length === 0) plan = fallbackExecutionPlan(rawSteps);
|
|
1892
|
+
if (plan.length === 0) throw new Error("Could not generate execution plan from steps");
|
|
1893
|
+
const { chromium } = await import("playwright");
|
|
1894
|
+
browser = await chromium.launch({
|
|
1895
|
+
headless: true,
|
|
1896
|
+
...process.env.PLAYWRIGHT_EXECUTABLE_PATH ? { executablePath: process.env.PLAYWRIGHT_EXECUTABLE_PATH } : {}
|
|
1897
|
+
});
|
|
1898
|
+
let parsedStorageState;
|
|
1899
|
+
try {
|
|
1900
|
+
parsedStorageState = config.storageState ? JSON.parse(config.storageState) : crawlCfg?.storageState ? JSON.parse(crawlCfg.storageState) : void 0;
|
|
1901
|
+
} catch {
|
|
1902
|
+
parsedStorageState = void 0;
|
|
1903
|
+
}
|
|
1904
|
+
const context = await browser.newContext({ viewport: { width: 1280, height: 800 }, ...parsedStorageState ? { storageState: parsedStorageState } : {} });
|
|
1905
|
+
const page = await context.newPage();
|
|
1906
|
+
const aiHealingEnabled = await isFeatureEnabled(tenantId, "AI_SELF_HEALING").catch(() => false);
|
|
1907
|
+
const stepResults = [];
|
|
1908
|
+
const screens = [];
|
|
1909
|
+
const locators = [];
|
|
1910
|
+
const seenLocatorExpressions = /* @__PURE__ */ new Set();
|
|
1911
|
+
const seenScreenKeys = /* @__PURE__ */ new Set();
|
|
1912
|
+
let lastStateKey = "";
|
|
1913
|
+
for (let i = 0; i < plan.length; i++) {
|
|
1914
|
+
const step = plan[i];
|
|
1915
|
+
const description = step.description || stepTexts[i] || `Step ${i + 1}`;
|
|
1916
|
+
const stepNumber = i + 1;
|
|
1917
|
+
const result = { stepNumber, description, status: "FAILED" };
|
|
1918
|
+
await withTenant(
|
|
1919
|
+
tenantId,
|
|
1920
|
+
(tx) => tx.platformJob.update({
|
|
1921
|
+
where: { id: jobId },
|
|
1922
|
+
data: {
|
|
1923
|
+
progress: Math.min(90, Math.round(i / Math.max(plan.length, 1) * 90)),
|
|
1924
|
+
result: { currentStep: { stepNumber, description } },
|
|
1925
|
+
heartbeatAt: /* @__PURE__ */ new Date()
|
|
1926
|
+
}
|
|
1927
|
+
})
|
|
1928
|
+
).catch(() => {
|
|
1929
|
+
});
|
|
1930
|
+
try {
|
|
1931
|
+
const heal = await executeExecutionStep(page, step, aiHealingEnabled ? { healCtx: { tenantId, projectId } } : {});
|
|
1932
|
+
if (heal.healed) {
|
|
1933
|
+
result.healed = true;
|
|
1934
|
+
result.healTier = heal.healedVia;
|
|
1935
|
+
result.healReasoning = heal.healReasoning;
|
|
1936
|
+
}
|
|
1937
|
+
await page.waitForLoadState("networkidle", { timeout: parseInt(process.env.TEST_NETWORK_IDLE_TIMEOUT_MS ?? "2000", 10) }).catch(() => {
|
|
1938
|
+
});
|
|
1939
|
+
result.status = result.healed ? "HEALED" : "PASSED";
|
|
1940
|
+
} catch (err) {
|
|
1941
|
+
result.status = step.optional ? "SKIPPED" : "FAILED";
|
|
1942
|
+
result.error = String(err?.message ?? err).slice(0, 300);
|
|
1943
|
+
}
|
|
1944
|
+
const currentUrl = page.url();
|
|
1945
|
+
result.url = currentUrl;
|
|
1946
|
+
const ssFilename = `${testCaseId}-capture-step-${stepNumber}${result.status === "FAILED" ? "-err" : ""}.png`;
|
|
1947
|
+
const ssPath = path.join(screenshotDir, ssFilename);
|
|
1948
|
+
await page.screenshot({ path: ssPath, fullPage: false }).catch(() => {
|
|
1949
|
+
});
|
|
1950
|
+
result.screenshotPath = ssPath;
|
|
1951
|
+
let cloudUrl;
|
|
1952
|
+
if (tenantStorageCfg) {
|
|
1953
|
+
const key = buildArtifactKey(tenantId, projectId, "screenshots", ssFilename, tenantStorageCfg.prefix);
|
|
1954
|
+
cloudUrl = await uploadScreenshotToTenantStorage(tenantStorageCfg, ssPath, key).catch(() => void 0) ?? void 0;
|
|
1955
|
+
if (cloudUrl) result.screenshotUrl = cloudUrl;
|
|
1956
|
+
}
|
|
1957
|
+
const elements = await domFallbackElements(page).catch(() => []);
|
|
1958
|
+
result.elementsCount = elements.length;
|
|
1959
|
+
const stateHash = hashContent(elements.map((el) => `${el.role}:${el.meaning}:${el.notes ?? ""}`).join("|"));
|
|
1960
|
+
const screenKey = `${currentUrl || "about:blank"}:${stateHash}`;
|
|
1961
|
+
const shouldPersistScreen = currentUrl && currentUrl !== "about:blank" && (i === 0 || screenKey !== lastStateKey || !seenScreenKeys.has(screenKey));
|
|
1962
|
+
lastStateKey = screenKey;
|
|
1963
|
+
if (shouldPersistScreen) {
|
|
1964
|
+
const screenName = screenNameFromUrl(currentUrl, tc.title ?? `Step ${stepNumber}`);
|
|
1965
|
+
const screen = await AppBrain.saveScreen(tenantId, projectId, screenName, currentUrl, jobId, i === 0 ? "ENTRY_POINT" : "LINK_FOLLOW").catch(() => null);
|
|
1966
|
+
if (screen?.id) {
|
|
1967
|
+
result.screenId = screen.id;
|
|
1968
|
+
await AppBrain.saveScreenshot(tenantId, projectId, screen.id, ssPath, screen.version ?? 1, "DESKTOP", cloudUrl).catch(() => {
|
|
1969
|
+
});
|
|
1970
|
+
for (const el of elements) {
|
|
1971
|
+
await AppBrain.saveElement(
|
|
1972
|
+
tenantId,
|
|
1973
|
+
projectId,
|
|
1974
|
+
screen.id,
|
|
1975
|
+
el.meaning,
|
|
1976
|
+
el.role,
|
|
1977
|
+
el.expectedData,
|
|
1978
|
+
el.notes,
|
|
1979
|
+
void 0,
|
|
1980
|
+
true,
|
|
1981
|
+
el.boundingRect,
|
|
1982
|
+
el.ariaState,
|
|
1983
|
+
el.parentLandmark
|
|
1984
|
+
).catch(() => {
|
|
1985
|
+
});
|
|
1986
|
+
}
|
|
1987
|
+
for (const loc of elementsToPlaywrightLocators(elements)) {
|
|
1988
|
+
if (seenLocatorExpressions.has(loc.expression)) continue;
|
|
1989
|
+
seenLocatorExpressions.add(loc.expression);
|
|
1990
|
+
locators.push(loc);
|
|
1991
|
+
}
|
|
1992
|
+
if (!seenScreenKeys.has(screenKey)) {
|
|
1993
|
+
screens.push({ stepNumber, screenId: screen.id, url: currentUrl, screenshotPath: ssPath, screenshotUrl: cloudUrl, isNewScreen: true });
|
|
1994
|
+
seenScreenKeys.add(screenKey);
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
stepResults.push(result);
|
|
1999
|
+
if (result.status === "FAILED") break;
|
|
2000
|
+
}
|
|
2001
|
+
if (locators.length > 0) {
|
|
2002
|
+
await AppBrain.saveLocatorsJson(tenantId, testCaseId, locators).catch(() => {
|
|
2003
|
+
});
|
|
2004
|
+
}
|
|
2005
|
+
const firstScreen = screens[0];
|
|
2006
|
+
await withTenant(
|
|
2007
|
+
tenantId,
|
|
2008
|
+
(tx) => tx.testCase.update({
|
|
2009
|
+
where: { id: testCaseId },
|
|
2010
|
+
data: {
|
|
2011
|
+
...firstScreen?.screenId ? { screenId: firstScreen.screenId } : {},
|
|
2012
|
+
captureStepScreenshots: stepResults
|
|
2013
|
+
}
|
|
2014
|
+
})
|
|
2015
|
+
).catch(() => {
|
|
2016
|
+
});
|
|
2017
|
+
const finalResult = {
|
|
2018
|
+
stepResults,
|
|
2019
|
+
screens,
|
|
2020
|
+
locators,
|
|
2021
|
+
totalSteps: plan.length,
|
|
2022
|
+
capturedScreens: screens.length
|
|
2023
|
+
};
|
|
2024
|
+
await withTenant(
|
|
2025
|
+
tenantId,
|
|
2026
|
+
(tx) => tx.platformJob.update({
|
|
2027
|
+
where: { id: jobId },
|
|
2028
|
+
data: { status: "DONE", progress: 100, result: finalResult, finishedAt: /* @__PURE__ */ new Date(), heartbeatAt: /* @__PURE__ */ new Date() }
|
|
2029
|
+
})
|
|
2030
|
+
);
|
|
2031
|
+
} catch (err) {
|
|
2032
|
+
const error = String(err?.message ?? err);
|
|
2033
|
+
await withTenant(
|
|
2034
|
+
tenantId,
|
|
2035
|
+
(tx) => tx.platformJob.update({ where: { id: jobId }, data: { status: "FAILED", error, finishedAt: /* @__PURE__ */ new Date(), heartbeatAt: /* @__PURE__ */ new Date() } })
|
|
2036
|
+
).catch(() => {
|
|
2037
|
+
});
|
|
2038
|
+
logger.error({ jobId, testCaseId, err: error }, "processCaptureStepsJob failed");
|
|
2039
|
+
throw err;
|
|
2040
|
+
} finally {
|
|
2041
|
+
await browser?.close?.().catch(() => {
|
|
2042
|
+
});
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
1806
2045
|
jobDispatchTable = {
|
|
1807
2046
|
CRAWL: processJob,
|
|
2047
|
+
CAPTURE_STEPS: processCaptureStepsJob,
|
|
1808
2048
|
DISCOVER: processDiscoverJob,
|
|
1809
2049
|
MONITOR: processMonitorJob,
|
|
1810
2050
|
NATIVE_CRAWL: processNativeCrawlJob,
|
|
@@ -1833,7 +2073,8 @@ async function processRepoScanJob(jobId, config) {
|
|
|
1833
2073
|
detectFramework,
|
|
1834
2074
|
extractTestIds,
|
|
1835
2075
|
extractStyleProfile,
|
|
1836
|
-
buildPageObjectInventory
|
|
2076
|
+
buildPageObjectInventory,
|
|
2077
|
+
resolveFixtureAliasesFromImports
|
|
1837
2078
|
} = await import("@detiq/agents");
|
|
1838
2079
|
const conn = await withTenant(
|
|
1839
2080
|
tenantId,
|
|
@@ -1848,6 +2089,9 @@ async function processRepoScanJob(jobId, config) {
|
|
|
1848
2089
|
);
|
|
1849
2090
|
await updateJobProgress(tenantId, jobId, 15, "Fetching repo tree");
|
|
1850
2091
|
const treeResult = await fetchRepoTree(token, conn.repoFullName, "HEAD");
|
|
2092
|
+
if (treeResult.truncated) {
|
|
2093
|
+
console.warn(`[repo-scan] ${conn.repoFullName}: tree truncated (>100k blobs) \u2014 detection may be incomplete`);
|
|
2094
|
+
}
|
|
1851
2095
|
const frameworks = detectFramework(treeResult.paths);
|
|
1852
2096
|
const primary = frameworks[0];
|
|
1853
2097
|
await updateJobProgress(tenantId, jobId, 30, `Detected framework: ${primary?.framework ?? "unknown"}`);
|
|
@@ -1865,6 +2109,22 @@ async function processRepoScanJob(jobId, config) {
|
|
|
1865
2109
|
return p.endsWith(".spec.ts") || p.endsWith(".spec.js") || p.endsWith(".test.ts") || p.endsWith(".test.js") || p.endsWith("_test.py") || p.includes("/test_");
|
|
1866
2110
|
}).slice(0, 500);
|
|
1867
2111
|
await updateJobProgress(tenantId, jobId, 40, `Found ${testPaths.length} test files`);
|
|
2112
|
+
if (testPaths.length === 0) {
|
|
2113
|
+
logger.warn({ jobId, repoConnectionId, framework }, "[repo-scan] no test files matched glob \u2014 scan complete with 0 results");
|
|
2114
|
+
await withTenant(
|
|
2115
|
+
tenantId,
|
|
2116
|
+
(tx) => tx.gitHubRepoConnection.update({
|
|
2117
|
+
where: { id: repoConnectionId },
|
|
2118
|
+
data: { scanStatus: "DONE", testFileCount: 0, extractedIdCount: 0, lastScannedAt: /* @__PURE__ */ new Date(), lastSyncAt: /* @__PURE__ */ new Date() }
|
|
2119
|
+
})
|
|
2120
|
+
);
|
|
2121
|
+
await updateJobProgress(tenantId, jobId, 100, "Done: 0 test files found");
|
|
2122
|
+
await withTenant(
|
|
2123
|
+
tenantId,
|
|
2124
|
+
(tx) => tx.platformJob.update({ where: { id: jobId }, data: { status: "DONE", finishedAt: /* @__PURE__ */ new Date(), result: { testFileCount: 0, extractedIdCount: 0 } } })
|
|
2125
|
+
);
|
|
2126
|
+
return;
|
|
2127
|
+
}
|
|
1868
2128
|
const allExtracted = [];
|
|
1869
2129
|
const styleSampleFiles = [];
|
|
1870
2130
|
let processed = 0;
|
|
@@ -1872,7 +2132,13 @@ async function processRepoScanJob(jobId, config) {
|
|
|
1872
2132
|
for (const filePath of testPaths) {
|
|
1873
2133
|
const content = await fetchFileContent(token, conn.repoFullName, filePath);
|
|
1874
2134
|
if (!content) continue;
|
|
1875
|
-
const
|
|
2135
|
+
const extraAliases = await resolveFixtureAliasesFromImports(
|
|
2136
|
+
content,
|
|
2137
|
+
token,
|
|
2138
|
+
conn.repoFullName,
|
|
2139
|
+
filePath
|
|
2140
|
+
).catch(() => /* @__PURE__ */ new Set());
|
|
2141
|
+
const extracted = extractTestIds(content, filePath, framework, extraAliases);
|
|
1876
2142
|
allExtracted.push(...extracted);
|
|
1877
2143
|
await withTenant(
|
|
1878
2144
|
tenantId,
|
|
@@ -1895,6 +2161,214 @@ async function processRepoScanJob(jobId, config) {
|
|
|
1895
2161
|
await updateJobProgress(tenantId, jobId, 40 + Math.floor(processed / testPaths.length * 40), `Processed ${processed}/${testPaths.length} files`);
|
|
1896
2162
|
}
|
|
1897
2163
|
}
|
|
2164
|
+
const configPaths = {};
|
|
2165
|
+
try {
|
|
2166
|
+
const pathSet = new Set(treeResult.paths);
|
|
2167
|
+
const lang = framework.includes("java") ? "java" : framework.includes("py") || framework === "pytest" ? "python" : "js";
|
|
2168
|
+
const configFilesToFetch = [];
|
|
2169
|
+
const tryAdd = (candidates, type) => {
|
|
2170
|
+
for (const c of candidates) {
|
|
2171
|
+
if (pathSet.has(c)) {
|
|
2172
|
+
configFilesToFetch.push({ path: c, type, dirPrefix: "" });
|
|
2173
|
+
break;
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
};
|
|
2177
|
+
const scanTree = (pattern, type) => {
|
|
2178
|
+
if (configFilesToFetch.some((f) => f.type === type)) return;
|
|
2179
|
+
const all = treeResult.paths.filter((p) => pattern.test(p)).sort((a, b) => a.split("/").length - b.split("/").length);
|
|
2180
|
+
if (all[0]) {
|
|
2181
|
+
const lastSlash = all[0].lastIndexOf("/");
|
|
2182
|
+
const dirPrefix = lastSlash > 0 ? all[0].slice(0, lastSlash + 1) : "";
|
|
2183
|
+
configFilesToFetch.push({ path: all[0], type, dirPrefix });
|
|
2184
|
+
}
|
|
2185
|
+
};
|
|
2186
|
+
if (lang === "js") {
|
|
2187
|
+
if (framework.startsWith("playwright")) {
|
|
2188
|
+
tryAdd(["playwright.config.ts", "playwright.config.js"], "playwright");
|
|
2189
|
+
scanTree(/(?:^|\/)playwright\.config\.(ts|js|mts)$/, "playwright");
|
|
2190
|
+
}
|
|
2191
|
+
if (framework.startsWith("cypress")) {
|
|
2192
|
+
tryAdd(["cypress.config.ts", "cypress.config.js"], "cypress");
|
|
2193
|
+
scanTree(/(?:^|\/)cypress\.config\.(ts|js)$/, "cypress");
|
|
2194
|
+
}
|
|
2195
|
+
if (framework === "jest-ts" || framework === "jest-js") {
|
|
2196
|
+
tryAdd(["jest.config.ts", "jest.config.js", "jest.config.mjs"], "jest");
|
|
2197
|
+
scanTree(/(?:^|\/)jest\.config\.(ts|js|mjs|cjs)$/, "jest");
|
|
2198
|
+
tryAdd(["package.json"], "package-json");
|
|
2199
|
+
}
|
|
2200
|
+
if (framework === "webdriverio") {
|
|
2201
|
+
tryAdd(["wdio.conf.ts", "wdio.conf.js", "wdio.config.ts", "wdio.config.js"], "wdio");
|
|
2202
|
+
scanTree(/(?:^|\/)wdio\.(?:conf|config)\.(ts|js|mjs)$/, "wdio");
|
|
2203
|
+
}
|
|
2204
|
+
if (framework === "cucumber" || framework === "gherkin") {
|
|
2205
|
+
tryAdd([".cucumber.js", ".cucumber.cjs", "cucumber.json", ".cucumber.yaml", ".cucumber.yml"], "cucumber");
|
|
2206
|
+
tryAdd(["package.json"], "package-json");
|
|
2207
|
+
}
|
|
2208
|
+
tryAdd(["tsconfig.json"], "tsconfig");
|
|
2209
|
+
scanTree(/(?:^|\/)tsconfig(?:\.base)?\.json$/, "tsconfig");
|
|
2210
|
+
} else if (lang === "python") {
|
|
2211
|
+
tryAdd(["pytest.ini"], "pytest-ini");
|
|
2212
|
+
scanTree(/(?:^|\/)pytest\.ini$/, "pytest-ini");
|
|
2213
|
+
tryAdd(["pyproject.toml"], "pyproject-toml");
|
|
2214
|
+
scanTree(/(?:^|\/)pyproject\.toml$/, "pyproject-toml");
|
|
2215
|
+
tryAdd(["setup.cfg"], "setup-cfg");
|
|
2216
|
+
} else if (lang === "java") {
|
|
2217
|
+
tryAdd(["pom.xml"], "pom-xml");
|
|
2218
|
+
scanTree(/(?:^|\/)pom\.xml$/, "pom-xml");
|
|
2219
|
+
tryAdd(["build.gradle.kts", "build.gradle"], "build-gradle");
|
|
2220
|
+
scanTree(/(?:^|\/)build\.gradle(?:\.kts)?$/, "build-gradle");
|
|
2221
|
+
tryAdd(["testng.xml", "src/test/resources/testng.xml"], "testng-xml");
|
|
2222
|
+
}
|
|
2223
|
+
const normDir = (s) => s.replace(/^\.\//, "").replace(/\/$/, "");
|
|
2224
|
+
const globRoot = (pattern) => {
|
|
2225
|
+
const root = pattern.replace(/\/\*\*.*$/, "").replace(/\/\*[^*].*$/, "");
|
|
2226
|
+
return root && !root.startsWith("*") && !root.startsWith("{") ? normDir(root) : null;
|
|
2227
|
+
};
|
|
2228
|
+
const fetched = await Promise.all(
|
|
2229
|
+
configFilesToFetch.map(async ({ path: cfgPath, type, dirPrefix }) => {
|
|
2230
|
+
const content = await fetchFileContent(token, conn.repoFullName, cfgPath).catch(() => null);
|
|
2231
|
+
if (!content) return null;
|
|
2232
|
+
const r = {};
|
|
2233
|
+
if (type === "playwright") {
|
|
2234
|
+
const m = content.match(/\btestDir\s*:\s*['"`]([^'"`]+)['"`]/);
|
|
2235
|
+
if (m) r.testDir = normDir(m[1]);
|
|
2236
|
+
const bm = content.match(/\bbaseURL\s*:\s*['"`]([^'"`]+)['"`]/);
|
|
2237
|
+
if (bm) r.baseURL = bm[1];
|
|
2238
|
+
} else if (type === "cypress") {
|
|
2239
|
+
const sm = content.match(/\bspecPattern\s*:\s*['"`]([^'"`]+)['"`]/);
|
|
2240
|
+
if (sm) {
|
|
2241
|
+
const gr = globRoot(sm[1]);
|
|
2242
|
+
if (gr) r.testDir = gr;
|
|
2243
|
+
}
|
|
2244
|
+
const fm = content.match(/\bfixturesFolder\s*:\s*['"`]([^'"`]+)['"`]/);
|
|
2245
|
+
if (fm) r.fixturesFolder = normDir(fm[1]);
|
|
2246
|
+
const bm = content.match(/\bbaseUrl\s*:\s*['"`]([^'"`]+)['"`]/);
|
|
2247
|
+
if (bm) r.baseURL = bm[1];
|
|
2248
|
+
} else if (type === "jest") {
|
|
2249
|
+
const rm = content.match(/\broots\s*:\s*\[([^\]]+)\]/);
|
|
2250
|
+
if (rm) {
|
|
2251
|
+
const fr = rm[1].match(/['"`]([^'"`]+)['"`]/)?.[1];
|
|
2252
|
+
if (fr) r.testDir = fr.replace(/<rootDir>\/?/, "").replace(/\/$/, "") || "src";
|
|
2253
|
+
}
|
|
2254
|
+
if (!r.testDir) {
|
|
2255
|
+
const tm = content.match(/\btestMatch\s*:\s*\[([^\]]+)\]/);
|
|
2256
|
+
if (tm) {
|
|
2257
|
+
const fp = tm[1].match(/['"`]([^'"`]+)['"`]/)?.[1];
|
|
2258
|
+
if (fp) {
|
|
2259
|
+
const gr = globRoot(fp.replace(/<rootDir>\/?/, ""));
|
|
2260
|
+
if (gr) r.testDir = gr;
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
const mm = content.match(/\bmoduleNameMapper\s*:\s*\{([^}]+)\}/s);
|
|
2265
|
+
if (mm) {
|
|
2266
|
+
const map = {};
|
|
2267
|
+
for (const [, k, v] of mm[1].matchAll(/['"`]([^'"`]+)['"`]\s*:\s*['"`]([^'"`]+)['"`]/g)) map[k] = v;
|
|
2268
|
+
if (Object.keys(map).length) r.jestModuleNameMapper = map;
|
|
2269
|
+
}
|
|
2270
|
+
} else if (type === "package-json") {
|
|
2271
|
+
try {
|
|
2272
|
+
const p = JSON.parse(content);
|
|
2273
|
+
const j = p?.jest ?? {};
|
|
2274
|
+
if (Array.isArray(j.roots) && j.roots[0]) r.testDir = String(j.roots[0]).replace(/<rootDir>\/?/, "").replace(/\/$/, "") || "src";
|
|
2275
|
+
if (!r.testDir && Array.isArray(j.testMatch) && j.testMatch[0]) {
|
|
2276
|
+
const gr = globRoot(String(j.testMatch[0]).replace(/<rootDir>\/?/, ""));
|
|
2277
|
+
if (gr) r.testDir = gr;
|
|
2278
|
+
}
|
|
2279
|
+
if (j.moduleNameMapper && typeof j.moduleNameMapper === "object") r.jestModuleNameMapper = j.moduleNameMapper;
|
|
2280
|
+
} catch {
|
|
2281
|
+
}
|
|
2282
|
+
} else if (type === "tsconfig") {
|
|
2283
|
+
try {
|
|
2284
|
+
const stripped = content.replace(/\/\/[^\n]*/g, "").replace(/,(\s*[}\]])/g, "$1");
|
|
2285
|
+
const p = JSON.parse(stripped);
|
|
2286
|
+
const opts = p?.compilerOptions ?? {};
|
|
2287
|
+
if (opts.paths && typeof opts.paths === "object") r.tsconfigPaths = opts.paths;
|
|
2288
|
+
if (typeof opts.baseUrl === "string") r.tsconfigBaseUrl = opts.baseUrl;
|
|
2289
|
+
} catch {
|
|
2290
|
+
}
|
|
2291
|
+
} else if (type === "pytest-ini") {
|
|
2292
|
+
const m = content.match(/^\s*testpaths\s*=\s*(.+)$/m);
|
|
2293
|
+
if (m) {
|
|
2294
|
+
const first = m[1].trim().split(/\s+/)[0];
|
|
2295
|
+
if (first) r.testDir = normDir(first);
|
|
2296
|
+
}
|
|
2297
|
+
} else if (type === "pyproject-toml") {
|
|
2298
|
+
const m = content.match(/testpaths\s*=\s*\[([^\]]+)\]/);
|
|
2299
|
+
if (m) {
|
|
2300
|
+
const first = m[1].match(/['"]([^'"]+)['"]/)?.[1];
|
|
2301
|
+
if (first) r.testDir = normDir(first);
|
|
2302
|
+
}
|
|
2303
|
+
if (!r.testDir) {
|
|
2304
|
+
const m2 = content.match(/testpaths\s*=\s*['"]([^'"]+)['"]/);
|
|
2305
|
+
if (m2) r.testDir = normDir(m2[1]);
|
|
2306
|
+
}
|
|
2307
|
+
} else if (type === "setup-cfg") {
|
|
2308
|
+
const m = content.match(/^\s*testpaths\s*=\s*(.+)$/m);
|
|
2309
|
+
if (m) {
|
|
2310
|
+
const first = m[1].trim().split(/[\s,]+/)[0];
|
|
2311
|
+
if (first) r.testDir = normDir(first);
|
|
2312
|
+
}
|
|
2313
|
+
} else if (type === "pom-xml") {
|
|
2314
|
+
const m = content.match(/<testSourceDirectory>\s*([^<]+)\s*<\/testSourceDirectory>/);
|
|
2315
|
+
if (m) r.javaTestSourceDir = normDir(m[1].trim());
|
|
2316
|
+
} else if (type === "build-gradle") {
|
|
2317
|
+
const m = content.match(/srcDir[s]?\s*[=([]?\s*['"`]([^'"`]+)['"`]/) ?? content.match(/srcDir\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/);
|
|
2318
|
+
if (m) r.javaTestSourceDir = normDir(m[1]);
|
|
2319
|
+
} else if (type === "wdio") {
|
|
2320
|
+
const sm = content.match(/\bspecs\s*:\s*\[([^\]]+)\]/);
|
|
2321
|
+
if (sm) {
|
|
2322
|
+
const fp = sm[1].match(/['"`]([^'"`]+)['"`]/)?.[1];
|
|
2323
|
+
if (fp) {
|
|
2324
|
+
const gr = globRoot(fp);
|
|
2325
|
+
if (gr) r.testDir = gr;
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
const bm = content.match(/\bbaseUrl\s*:\s*['"`]([^'"`]+)['"`]/);
|
|
2329
|
+
if (bm) r.baseURL = bm[1];
|
|
2330
|
+
} else if (type === "cucumber") {
|
|
2331
|
+
const pm = content.match(/\bpaths\s*:\s*\[([^\]]+)\]/);
|
|
2332
|
+
if (pm) {
|
|
2333
|
+
const fp = pm[1].match(/['"`]([^'"`]+)['"`]/)?.[1];
|
|
2334
|
+
if (fp) {
|
|
2335
|
+
const gr = globRoot(fp);
|
|
2336
|
+
if (gr) r.testDir = gr;
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
if (!r.testDir) {
|
|
2340
|
+
try {
|
|
2341
|
+
const p = JSON.parse(content);
|
|
2342
|
+
const paths = p?.default?.paths ?? p?.paths;
|
|
2343
|
+
if (Array.isArray(paths) && paths[0]) {
|
|
2344
|
+
const gr = globRoot(String(paths[0]));
|
|
2345
|
+
if (gr) r.testDir = gr;
|
|
2346
|
+
}
|
|
2347
|
+
} catch {
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
if (dirPrefix && r.testDir && !r.testDir.startsWith(dirPrefix)) {
|
|
2352
|
+
r.testDir = dirPrefix + r.testDir;
|
|
2353
|
+
}
|
|
2354
|
+
if (dirPrefix && r.javaTestSourceDir && !r.javaTestSourceDir.startsWith(dirPrefix)) {
|
|
2355
|
+
r.javaTestSourceDir = dirPrefix + r.javaTestSourceDir;
|
|
2356
|
+
}
|
|
2357
|
+
return r;
|
|
2358
|
+
})
|
|
2359
|
+
);
|
|
2360
|
+
for (const parsed of fetched) {
|
|
2361
|
+
if (!parsed) continue;
|
|
2362
|
+
if (parsed.testDir && !configPaths.testDir) configPaths.testDir = parsed.testDir;
|
|
2363
|
+
if (parsed.fixturesFolder && !configPaths.fixturesFolder) configPaths.fixturesFolder = parsed.fixturesFolder;
|
|
2364
|
+
if (parsed.baseURL && !configPaths.baseURL) configPaths.baseURL = parsed.baseURL;
|
|
2365
|
+
if (parsed.tsconfigPaths && !configPaths.tsconfigPaths) configPaths.tsconfigPaths = parsed.tsconfigPaths;
|
|
2366
|
+
if (parsed.tsconfigBaseUrl && !configPaths.tsconfigBaseUrl) configPaths.tsconfigBaseUrl = parsed.tsconfigBaseUrl;
|
|
2367
|
+
if (parsed.javaTestSourceDir && !configPaths.javaTestSourceDir) configPaths.javaTestSourceDir = parsed.javaTestSourceDir;
|
|
2368
|
+
if (parsed.jestModuleNameMapper && !configPaths.jestModuleNameMapper) configPaths.jestModuleNameMapper = parsed.jestModuleNameMapper;
|
|
2369
|
+
}
|
|
2370
|
+
} catch {
|
|
2371
|
+
}
|
|
1898
2372
|
let styleProfile = conn.styleProfile ?? null;
|
|
1899
2373
|
if (styleSampleFiles.length > 0) {
|
|
1900
2374
|
try {
|
|
@@ -1902,11 +2376,23 @@ async function processRepoScanJob(jobId, config) {
|
|
|
1902
2376
|
const testPathSet = new Set(testPaths);
|
|
1903
2377
|
const sourceFilePaths = language === "typescript" || language === "javascript" ? treeResult.paths.filter((p) => (p.endsWith(".ts") || p.endsWith(".js")) && !testPathSet.has(p) && !p.endsWith(".d.ts") && !p.includes("node_modules/") && !p.includes("dist/") && !p.includes(".config.")).slice(0, 60) : [];
|
|
1904
2378
|
const allPathsForProfile = [...testPaths, ...mainSourcePaths, ...sourceFilePaths].slice(0, 200);
|
|
1905
|
-
const freshProfile = await extractStyleProfile(styleSampleFiles, framework, language, tenantId, projectId, jobId, allPathsForProfile);
|
|
2379
|
+
const freshProfile = await extractStyleProfile(styleSampleFiles, framework, language, tenantId, projectId, jobId, allPathsForProfile, treeResult.paths);
|
|
1906
2380
|
if (freshProfile) styleProfile = freshProfile;
|
|
1907
2381
|
} catch {
|
|
1908
2382
|
}
|
|
1909
2383
|
}
|
|
2384
|
+
if (styleProfile && Object.keys(configPaths).length > 0) {
|
|
2385
|
+
if (configPaths.testDir) styleProfile.configTestDir = configPaths.testDir;
|
|
2386
|
+
if (configPaths.javaTestSourceDir) {
|
|
2387
|
+
styleProfile.configTestDir = configPaths.javaTestSourceDir;
|
|
2388
|
+
if (!styleProfile.testLocation) styleProfile.testLocation = configPaths.javaTestSourceDir;
|
|
2389
|
+
}
|
|
2390
|
+
if (configPaths.fixturesFolder) styleProfile.configFixturesFolder = configPaths.fixturesFolder;
|
|
2391
|
+
if (configPaths.baseURL) styleProfile.configBaseURL = configPaths.baseURL;
|
|
2392
|
+
if (configPaths.tsconfigPaths) styleProfile.tsconfigPaths = configPaths.tsconfigPaths;
|
|
2393
|
+
if (configPaths.tsconfigBaseUrl) styleProfile.tsconfigBaseUrl = configPaths.tsconfigBaseUrl;
|
|
2394
|
+
if (configPaths.jestModuleNameMapper) styleProfile.jestModuleNameMapper = configPaths.jestModuleNameMapper;
|
|
2395
|
+
}
|
|
1910
2396
|
let pageObjectInventory = conn.pageObjectInventory ?? [];
|
|
1911
2397
|
try {
|
|
1912
2398
|
const freshInventory = await buildPageObjectInventory(token, conn.repoFullName, treeResult.paths, framework);
|
|
@@ -1985,7 +2471,9 @@ async function processRepoScanJob(jobId, config) {
|
|
|
1985
2471
|
styleProfile: resolvedStyleProfile,
|
|
1986
2472
|
pageObjectInventory,
|
|
1987
2473
|
testPaths,
|
|
1988
|
-
extractedTestCount: allExtracted.length
|
|
2474
|
+
extractedTestCount: allExtracted.length,
|
|
2475
|
+
treeWasTruncated: treeResult.truncated,
|
|
2476
|
+
extractedTests: allExtracted
|
|
1989
2477
|
});
|
|
1990
2478
|
} catch (e) {
|
|
1991
2479
|
logger.warn({ jobId, repoConnectionId, err: e?.message }, "[repo-scan] repo skill files generation failed (non-fatal)");
|
|
@@ -1997,7 +2485,8 @@ async function processRepoScanJob(jobId, config) {
|
|
|
1997
2485
|
);
|
|
1998
2486
|
aggregateJobTokens(tenantId, jobId, projectId, jobStart).catch(console.error);
|
|
1999
2487
|
} catch (err) {
|
|
2000
|
-
const
|
|
2488
|
+
const cause = err?.cause?.message ?? (typeof err?.cause === "string" ? err.cause : "");
|
|
2489
|
+
const msg = `${String(err?.message ?? err)}${cause ? ` \u2014 ${cause}` : ""}`;
|
|
2001
2490
|
logger.error({ jobId, repoConnectionId, err: msg }, "[repo-scan] failed");
|
|
2002
2491
|
await withTenant(
|
|
2003
2492
|
tenantId,
|