@taodigital/crawler 1.2.1 → 1.2.2
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/ai-login.js.map +1 -1
- package/dist/crawler.js.map +2 -2
- package/dist/extractors/summary-extractor.js.map +2 -2
- package/dist/failure-analysis-agent.js.map +1 -1
- package/dist/http-engine.js.map +2 -2
- package/dist/interactive-probe.js.map +1 -1
- package/dist/job-queue.js +60 -18
- package/dist/job-queue.js.map +2 -2
- package/dist/native-crawler.js.map +1 -1
- package/dist/sitemap.js +1 -1
- package/dist/sitemap.js.map +2 -2
- package/dist/test-executor.js.map +2 -2
- package/dist/visual-diff-verdict.js.map +1 -1
- package/dist/worker.js.map +1 -1
- package/package.json +1 -1
- package/src/http-engine.ts +12 -7
- package/src/job-queue.ts +104 -32
- package/src/sitemap.ts +1 -1
- package/types/job-queue.d.ts +5 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/test-executor.ts"],
|
|
4
|
-
"sourcesContent": ["import { chromium, firefox, webkit, type Page, type Locator } from 'playwright';\nimport type { ExecutionStep } from '@detiq/agents';\nimport { assertCrawlUrlSafe } from '@detiq/core/dist/ssrf-guard.js';\n\nexport interface StepResult {\n stepNumber: number;\n description: string;\n status: 'PASSED' | 'FAILED' | 'SKIPPED';\n duration: number;\n error?: string;\n screenshotPath?: string;\n healed?: boolean;\n /** 'mechanical' = same selector text, different Playwright locator strategy.\n * 'ai' = AI Self-Healing Agent matched a semantically different element\n * (the app's selector text itself changed, e.g. \"Sign In\" -> \"Log In\"). */\n healedVia?: 'mechanical' | 'ai';\n healReasoning?: string;\n healConfidence?: number;\n elementBoundingRect?: { x: number; y: number; width: number; height: number } | null;\n elementSelector?: string;\n}\n\nexport interface ExecutionResult {\n stepResults: StepResult[];\n status: 'PASSED' | 'FAILED' | 'ERROR' | 'FLAKY';\n errorStep?: number;\n totalDuration: number;\n finalScreenshotPath?: string;\n retryCount?: number;\n consoleLogs?: Array<{ type: string; text: string; timestamp: number }>;\n networkErrors?: Array<{ url: string; status: number; method: string; timestamp: number }>;\n rootCauseSummary?: string;\n networkTimings?: Array<{ url: string; method: string; status: number; duration: number }>;\n}\n\nconst LOCATOR_STRATEGIES: ExecutionStep['selectorStrategy'][] = ['text', 'label', 'role', 'css'];\n\nfunction buildLocator(page: Page, selector: string, strategy: ExecutionStep['selectorStrategy'] = 'text'): Locator {\n switch (strategy) {\n case 'css': return page.locator(selector);\n case 'text': return page.getByText(selector, { exact: false });\n case 'role': return page.getByRole(selector as any);\n case 'label': return page.getByLabel(selector);\n default: return page.locator(selector);\n }\n}\n\n/** Scans the live page for visible interactive elements \u2014 the \"current page elements\"\n * fed to the AI Self-Healing Agent when mechanical locator strategies fail. */\nasync function extractCurrentPageElements(page: Page): Promise<Array<{ meaning: string; role: string; ariaLabel?: string; textContent?: string }>> {\n return page.evaluate(() => {\n const els = Array.from(document.querySelectorAll<HTMLElement>(\n 'a, button, input, select, textarea, [role=\"button\"], [role=\"link\"], [role=\"tab\"], [role=\"menuitem\"], [aria-label]'\n ));\n return els\n .filter((el) => {\n const rect = el.getBoundingClientRect();\n return rect.width > 0 && rect.height > 0;\n })\n .slice(0, 200)\n .map((el) => {\n const ariaLabel = el.getAttribute('aria-label') ?? undefined;\n const textContent = (el.textContent ?? '').replace(/\\s+/g, ' ').trim().slice(0, 100) || undefined;\n const role = el.getAttribute('role') ?? el.tagName.toLowerCase();\n return { meaning: ariaLabel || textContent || role, role, ariaLabel, textContent };\n });\n });\n}\n\n/**\n * Resolves a step's target element in three tiers:\n * 1. Exact selector + strategy as generated.\n * 2. Mechanical: same selector text, other Playwright locator strategies\n * (handles a strategy mismatch, not an actual UI change).\n * 3. AI: when tenantId/projectId are supplied, asks the Self-Healing Agent to find\n * the current page's best semantic match for the failed step description \u2014 the\n * tier that actually survives the app's selector text changing.\n */\nasync function resolveLocator(\n page: Page,\n step: ExecutionStep,\n healCtx?: { tenantId: string; projectId: string },\n): Promise<{ loc: Locator; healed: boolean; healedVia?: 'mechanical' | 'ai'; healReasoning?: string; healConfidence?: number } | null> {\n const { selector, selectorStrategy = 'text' } = step;\n if (!selector) return null;\n\n const primary = buildLocator(page, selector, selectorStrategy);\n const attached = await primary.first().isVisible({ timeout: parseInt(process.env.TEST_WAIT_TIMEOUT_MS ?? '1500', 10) }).catch(() => false);\n if (attached) return { loc: primary, healed: false };\n\n // Tier 2 \u2014 mechanical: try other strategies before giving up\n for (const strategy of LOCATOR_STRATEGIES) {\n if (strategy === selectorStrategy) continue;\n const loc = buildLocator(page, selector, strategy);\n const visible = await loc.first().isVisible({ timeout: parseInt(process.env.TEST_SHORT_TIMEOUT_MS ?? '800', 10) }).catch(() => false);\n if (visible) return { loc, healed: true, healedVia: 'mechanical' };\n }\n\n // Tier 3 \u2014 AI: the selector text itself may no longer match anything on the page.\n if (healCtx) {\n try {\n const { healElement } = await import('@detiq/agents');\n const currentPageElements = await extractCurrentPageElements(page);\n const result = await healElement({\n tenantId: healCtx.tenantId,\n projectId: healCtx.projectId,\n failedElementMeaning: step.description || selector,\n currentPageElements,\n });\n if (result.healed && result.updatedTest) {\n const healedLoc = page.getByText(result.updatedTest.elementMeaning, { exact: false });\n const visible = await healedLoc.first().isVisible({ timeout: parseInt(process.env.TEST_WAIT_TIMEOUT_MS ?? '1500', 10) }).catch(() => false);\n if (visible) {\n return {\n loc: healedLoc,\n healed: true,\n healedVia: 'ai',\n healReasoning: result.reasoning,\n healConfidence: result.confidence,\n };\n }\n }\n } catch {\n // AI healing is a best-effort last resort \u2014 any failure here (LLM error, bad\n // JSON, etc.) just falls through to the caller's normal Playwright error below.\n }\n }\n\n return null;\n}\n\nfunction applyHealInfo(result: StepResult, r: { healed: boolean; healedVia?: 'mechanical' | 'ai'; healReasoning?: string; healConfidence?: number }): void {\n result.healed = r.healed;\n result.healedVia = r.healedVia;\n result.healReasoning = r.healReasoning;\n result.healConfidence = r.healConfidence;\n}\n\nfunction assertExecutionUrlSafe(url: string, allowUnsafeLocalUrlsForTests?: boolean): void {\n if (!allowUnsafeLocalUrlsForTests) assertCrawlUrlSafe(url);\n}\n\nexport async function executeExecutionStep(\n page: Page,\n step: ExecutionStep,\n options: { healCtx?: { tenantId: string; projectId: string }; allowUnsafeLocalUrlsForTests?: boolean } = {},\n): Promise<Pick<StepResult, 'healed' | 'healedVia' | 'healReasoning' | 'healConfidence' | 'elementBoundingRect' | 'elementSelector'>> {\n const result: Pick<StepResult, 'healed' | 'healedVia' | 'healReasoning' | 'healConfidence' | 'elementBoundingRect' | 'elementSelector'> = {};\n const timeout = step.timeout ?? parseInt(process.env.TEST_STEP_TIMEOUT_MS ?? '8000', 10);\n\n switch (step.action) {\n case 'navigate':\n if (!step.url) throw new Error('Navigate step missing url');\n assertExecutionUrlSafe(step.url, options.allowUnsafeLocalUrlsForTests);\n await page.goto(step.url, { waitUntil: 'domcontentloaded', timeout });\n break;\n\n case 'click': {\n const r = await resolveLocator(page, step, options.healCtx);\n if (!r) throw new Error('Could not resolve locator');\n result.elementBoundingRect = await r.loc.first().boundingBox().catch(() => null);\n result.elementSelector = step.selector;\n await r.loc.first().click({ timeout });\n applyHealInfo(result as StepResult, r);\n break;\n }\n\n case 'fill': {\n const r = await resolveLocator(page, step, options.healCtx);\n if (!r) throw new Error('Could not resolve locator');\n result.elementBoundingRect = await r.loc.first().boundingBox().catch(() => null);\n result.elementSelector = step.selector;\n await r.loc.first().fill(step.value ?? '', { timeout });\n applyHealInfo(result as StepResult, r);\n break;\n }\n\n case 'select': {\n const r = await resolveLocator(page, step, options.healCtx);\n if (!r) throw new Error('Could not resolve locator');\n result.elementBoundingRect = await r.loc.first().boundingBox().catch(() => null);\n result.elementSelector = step.selector;\n await r.loc.first().selectOption(step.value ?? '', { timeout });\n applyHealInfo(result as StepResult, r);\n break;\n }\n\n case 'hover': {\n const r = await resolveLocator(page, step, options.healCtx);\n if (!r) throw new Error('Could not resolve locator');\n result.elementBoundingRect = await r.loc.first().boundingBox().catch(() => null);\n result.elementSelector = step.selector;\n await r.loc.first().hover({ timeout });\n applyHealInfo(result as StepResult, r);\n break;\n }\n\n case 'press_key':\n await page.keyboard.press(step.value ?? 'Enter');\n break;\n\n case 'assert_visible': {\n const r = await resolveLocator(page, step, options.healCtx);\n if (!r) throw new Error('Could not resolve locator');\n await r.loc.first().waitFor({ state: 'visible', timeout });\n result.elementBoundingRect = await r.loc.first().boundingBox().catch(() => null);\n result.elementSelector = step.selector;\n applyHealInfo(result as StepResult, r);\n break;\n }\n\n case 'assert_text': {\n const r = await resolveLocator(page, step, options.healCtx);\n if (!r) throw new Error('Could not resolve locator');\n const text = await r.loc.first().textContent({ timeout });\n if (!text?.includes(step.value ?? '')) {\n throw new Error(`Expected text \"${step.value}\" not found. Got: \"${text?.slice(0, 100)}\"`);\n }\n result.elementBoundingRect = await r.loc.first().boundingBox().catch(() => null);\n result.elementSelector = step.selector;\n applyHealInfo(result as StepResult, r);\n break;\n }\n\n case 'assert_url': {\n const url = page.url();\n if (!url.includes(step.value ?? '')) {\n throw new Error(`Expected URL to contain \"${step.value}\". Got: \"${url}\"`);\n }\n break;\n }\n\n case 'wait':\n await page.waitForTimeout(step.timeout ?? parseInt(process.env.TEST_STEP_DELAY_MS ?? '2000', 10));\n break;\n\n case 'screenshot':\n break;\n\n case 'script':\n case 'setVariable':\n if (step.value) {\n await page.evaluate(step.value).catch(err => {\n throw new Error(`Script execution failed: ${err.message}`);\n });\n }\n break;\n }\n\n return result;\n}\n\nasync function runPlan(\n page: Page,\n plan: ExecutionStep[],\n options: { screenshotDir: string; testRunId: string; onStepComplete?: (result: StepResult) => void; healCtx?: { tenantId: string; projectId: string }; allowUnsafeLocalUrlsForTests?: boolean },\n consoleLogs: Array<{ type: string; text: string; timestamp: number }>,\n networkErrors: Array<{ url: string; status: number; method: string; timestamp: number }>,\n): Promise<{ stepResults: StepResult[]; errorStep?: number; finalScreenshotPath?: string; consoleLogs: typeof consoleLogs; networkErrors: typeof networkErrors }> {\n const stepResults: StepResult[] = [];\n let errorStep: number | undefined;\n let finalScreenshotPath: string | undefined;\n\n for (const step of plan) {\n const stepStart = Date.now();\n const result: StepResult = {\n stepNumber: step.stepNumber,\n description: step.description,\n status: 'FAILED',\n duration: 0,\n };\n\n try {\n const timeout = step.timeout ?? parseInt(process.env.TEST_STEP_TIMEOUT_MS ?? '8000', 10);\n\n switch (step.action) {\n case 'navigate':\n if (!step.url) throw new Error('Navigate step missing url');\n assertExecutionUrlSafe(step.url, options.allowUnsafeLocalUrlsForTests);\n await page.goto(step.url, { waitUntil: 'domcontentloaded', timeout });\n break;\n\n case 'click': {\n const r = await resolveLocator(page, step, options.healCtx);\n if (!r) throw new Error('Could not resolve locator');\n result.elementBoundingRect = await r.loc.first().boundingBox().catch(() => null);\n result.elementSelector = step.selector;\n await r.loc.first().click({ timeout });\n applyHealInfo(result, r);\n break;\n }\n\n case 'fill': {\n const r = await resolveLocator(page, step, options.healCtx);\n if (!r) throw new Error('Could not resolve locator');\n result.elementBoundingRect = await r.loc.first().boundingBox().catch(() => null);\n result.elementSelector = step.selector;\n await r.loc.first().fill(step.value ?? '', { timeout });\n applyHealInfo(result, r);\n break;\n }\n\n case 'select': {\n const r = await resolveLocator(page, step, options.healCtx);\n if (!r) throw new Error('Could not resolve locator');\n result.elementBoundingRect = await r.loc.first().boundingBox().catch(() => null);\n result.elementSelector = step.selector;\n await r.loc.first().selectOption(step.value ?? '', { timeout });\n applyHealInfo(result, r);\n break;\n }\n\n case 'hover': {\n const r = await resolveLocator(page, step, options.healCtx);\n if (!r) throw new Error('Could not resolve locator');\n result.elementBoundingRect = await r.loc.first().boundingBox().catch(() => null);\n result.elementSelector = step.selector;\n await r.loc.first().hover({ timeout });\n applyHealInfo(result, r);\n break;\n }\n\n case 'press_key':\n await page.keyboard.press(step.value ?? 'Enter');\n break;\n\n case 'assert_visible': {\n const r = await resolveLocator(page, step, options.healCtx);\n if (!r) throw new Error('Could not resolve locator');\n await r.loc.first().waitFor({ state: 'visible', timeout });\n result.elementBoundingRect = await r.loc.first().boundingBox().catch(() => null);\n result.elementSelector = step.selector;\n applyHealInfo(result, r);\n break;\n }\n\n case 'assert_text': {\n const r = await resolveLocator(page, step, options.healCtx);\n if (!r) throw new Error('Could not resolve locator');\n const text = await r.loc.first().textContent({ timeout });\n if (!text?.includes(step.value ?? '')) {\n throw new Error(`Expected text \"${step.value}\" not found. Got: \"${text?.slice(0, 100)}\"`);\n }\n result.elementBoundingRect = await r.loc.first().boundingBox().catch(() => null);\n result.elementSelector = step.selector;\n applyHealInfo(result, r);\n break;\n }\n\n case 'assert_url': {\n const url = page.url();\n if (!url.includes(step.value ?? '')) {\n throw new Error(`Expected URL to contain \"${step.value}\". Got: \"${url}\"`);\n }\n break;\n }\n\n case 'wait':\n await page.waitForTimeout(step.timeout ?? parseInt(process.env.TEST_STEP_DELAY_MS ?? '2000', 10));\n break;\n\n case 'screenshot': {\n const ssPath = `${options.screenshotDir}/${options.testRunId}-step-${step.stepNumber}.png`;\n await page.screenshot({ path: ssPath, fullPage: true });\n result.screenshotPath = ssPath;\n break;\n }\n\n case 'script':\n case 'setVariable': {\n if (step.value) {\n // Execute arbitrary JS string. Often used by manual test builder extension for custom variables\n // or assertions.\n await page.evaluate(step.value).catch(err => {\n throw new Error(`Script execution failed: ${err.message}`);\n });\n }\n break;\n }\n }\n\n result.status = 'PASSED';\n } catch (err: any) {\n result.status = step.optional ? 'SKIPPED' : 'FAILED';\n result.error = err.message;\n if (!step.optional) {\n errorStep = step.stepNumber;\n }\n }\n\n try {\n await page.waitForLoadState('networkidle', { timeout: parseInt(process.env.TEST_NETWORK_IDLE_TIMEOUT_MS ?? '1500', 10) }).catch(() => {});\n if (!result.screenshotPath) {\n const ssPath = `${options.screenshotDir}/${options.testRunId}-step-${step.stepNumber}.png`;\n await page.screenshot({ path: ssPath, fullPage: true });\n result.screenshotPath = ssPath;\n }\n } catch { /* Screenshot evidence is best-effort and should not fail the run. */ }\n\n result.duration = Date.now() - stepStart;\n stepResults.push(result);\n options.onStepComplete?.(result);\n\n if (result.status === 'FAILED') break;\n }\n\n try {\n const finalPath = `${options.screenshotDir}/${options.testRunId}-final.png`;\n await page.screenshot({ path: finalPath, fullPage: true });\n finalScreenshotPath = finalPath;\n } catch { /* non-fatal */ }\n\n return { stepResults, errorStep, finalScreenshotPath, consoleLogs, networkErrors };\n}\n\nfunction interpolate(template: string | undefined, data: Record<string, string>): string | undefined {\n if (!template) return template;\n return template.replace(/\\{\\{(\\w+)\\}\\}/g, (_, key) => data[key] ?? `{{${key}}}`);\n}\n\nexport async function executeTestPlan(\n plan: ExecutionStep[],\n options: {\n screenshotDir: string;\n testRunId: string;\n viewport?: { width: number; height: number };\n onStepComplete?: (result: StepResult) => void;\n /** Run the test up to this many times to detect flakiness (default 1). */\n maxRuns?: number;\n browser?: 'chromium' | 'firefox' | 'webkit';\n /** Data row for parameterized execution: substitutes {{varname}} in step fields. */\n dataRow?: Record<string, string>;\n /** Enables the AI Self-Healing Agent tier when a step's selector can't be resolved\n * even after trying alternate locator strategies. Omit to keep mechanical-only\n * healing (no LLM calls during execution). */\n healCtx?: { tenantId: string; projectId: string };\n /** Decrypted Playwright storageState JSON \u2014 restores a logged-in session so\n * steps don't have to re-authenticate. Without this, any test case behind a\n * login wall fails at the first authenticated step. */\n storageState?: string;\n /** Test fixtures may run on loopback; production callers must leave this unset. */\n allowUnsafeLocalUrlsForTests?: boolean;\n },\n): Promise<ExecutionResult> {\n if (options.dataRow && Object.keys(options.dataRow).length > 0) {\n const data = options.dataRow;\n plan = plan.map((step) => ({\n ...step,\n url: interpolate(step.url, data),\n selector: interpolate(step.selector, data),\n value: interpolate(step.value, data),\n description: interpolate(step.description, data) ?? step.description,\n }));\n }\n const maxRuns = options.maxRuns ?? 1;\n const browserEngine = { chromium, firefox, webkit }[options.browser ?? 'chromium'] ?? chromium;\n const start = Date.now();\n\n const outcomes: ('PASSED' | 'FAILED')[] = [];\n let lastResult: Awaited<ReturnType<typeof runPlan>> | null = null;\n let lastNetworkTimings: Array<{ url: string; method: string; status: number; duration: number }> = [];\n\n for (let run = 0; run < maxRuns; run++) {\n const browser = await browserEngine.launch({\n headless: true,\n // Optional override for environments with a non-standard/pre-provisioned\n // browser install (e.g. a custom base image with only system Chromium).\n ...(process.env.PLAYWRIGHT_EXECUTABLE_PATH ? { executablePath: process.env.PLAYWRIGHT_EXECUTABLE_PATH } : {}),\n });\n let parsedStorageState: any;\n try {\n parsedStorageState = options.storageState ? JSON.parse(options.storageState) : undefined;\n } catch {\n parsedStorageState = undefined;\n }\n const context = await browser.newContext({\n viewport: options.viewport ?? { width: 1440, height: 900 },\n ...(parsedStorageState ? { storageState: parsedStorageState } : {}),\n });\n const page = await context.newPage();\n\n await page\n .addStyleTag({ content: '*, *::before, *::after { animation-duration: 0s !important; transition-duration: 0s !important; }' })\n .catch(() => {});\n\n const consoleLogs: Array<{ type: string; text: string; timestamp: number }> = [];\n const networkErrors: Array<{ url: string; status: number; method: string; timestamp: number }> = [];\n const networkTimings: Array<{ url: string; method: string; status: number; duration: number }> = [];\n const requestStartTimes = new Map<string, number>();\n\n page.on('console', msg => {\n if (consoleLogs.length < 100) {\n consoleLogs.push({ type: msg.type(), text: msg.text().slice(0, 500), timestamp: Date.now() });\n }\n });\n page.on('request', request => {\n const url = request.url();\n if (url.includes('/api/') || url.match(/\\/(graphql|rest|v\\d)\\//)) {\n requestStartTimes.set(request.url(), Date.now());\n }\n });\n page.on('response', response => {\n const url = response.url();\n const start = requestStartTimes.get(url);\n if (start && networkTimings.length < 200) {\n networkTimings.push({ url: url.slice(0, 300), method: response.request().method(), status: response.status(), duration: Date.now() - start });\n requestStartTimes.delete(url);\n }\n if (!response.ok() && !url.includes('favicon') && networkErrors.length < 50) {\n networkErrors.push({\n url: url.slice(0, 300),\n status: response.status(),\n method: response.request().method(),\n timestamp: Date.now(),\n });\n }\n });\n\n try {\n const TEST_EXECUTION_TIMEOUT_MS = 5 * 60_000;\n lastResult = await Promise.race([\n runPlan(page, plan, options, consoleLogs, networkErrors),\n new Promise<never>((_, reject) =>\n setTimeout(() => reject(new Error('Test execution timed out')), TEST_EXECUTION_TIMEOUT_MS)\n ),\n ]);\n lastNetworkTimings = networkTimings;\n } finally {\n await context.close().catch(() => {});\n await browser.close().catch(() => {});\n }\n\n const passed = !lastResult.stepResults.some((r) => r.status === 'FAILED');\n outcomes.push(passed ? 'PASSED' : 'FAILED');\n\n // If all subsequent runs are consistent with first, no need to keep going\n if (run > 0 && outcomes.every((o) => o === outcomes[0])) break;\n }\n\n const passCount = outcomes.filter((o) => o === 'PASSED').length;\n const failCount = outcomes.filter((o) => o === 'FAILED').length;\n\n let status: ExecutionResult['status'];\n if (maxRuns > 1 && passCount > 0 && failCount > 0) {\n status = 'FLAKY';\n } else if (passCount === outcomes.length) {\n status = 'PASSED';\n } else {\n status = 'FAILED';\n }\n\n let rootCauseSummary: string | undefined;\n if ((status === 'FAILED' || status === 'FLAKY') && options.healCtx?.tenantId) {\n const failedStep = lastResult!.stepResults.find(s => s.status === 'FAILED');\n const { analyzeFailure } = await import('./failure-analysis-agent.js');\n rootCauseSummary = await analyzeFailure({\n tenantId: options.healCtx?.tenantId,\n error: failedStep?.error ?? 'Unknown error',\n stepDescription: failedStep?.description,\n consoleLogs: lastResult!.consoleLogs,\n networkErrors: lastResult!.networkErrors,\n screenshotPath: lastResult!.finalScreenshotPath,\n }).catch(() => undefined) ?? undefined;\n }\n\n return {\n stepResults: lastResult!.stepResults,\n status,\n errorStep: lastResult!.errorStep,\n totalDuration: Date.now() - start,\n finalScreenshotPath: lastResult!.finalScreenshotPath,\n retryCount: outcomes.length - 1,\n consoleLogs: lastResult!.consoleLogs,\n networkErrors: lastResult!.networkErrors,\n rootCauseSummary,\n networkTimings: lastNetworkTimings,\n };\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import { chromium, firefox, webkit, type Page, type Locator } from 'playwright';\nimport type { ExecutionStep } from '@taodigital/agents';\nimport { assertCrawlUrlSafe } from '@taodigital/core/dist/ssrf-guard.js';\n\nexport interface StepResult {\n stepNumber: number;\n description: string;\n status: 'PASSED' | 'FAILED' | 'SKIPPED';\n duration: number;\n error?: string;\n screenshotPath?: string;\n healed?: boolean;\n /** 'mechanical' = same selector text, different Playwright locator strategy.\n * 'ai' = AI Self-Healing Agent matched a semantically different element\n * (the app's selector text itself changed, e.g. \"Sign In\" -> \"Log In\"). */\n healedVia?: 'mechanical' | 'ai';\n healReasoning?: string;\n healConfidence?: number;\n elementBoundingRect?: { x: number; y: number; width: number; height: number } | null;\n elementSelector?: string;\n}\n\nexport interface ExecutionResult {\n stepResults: StepResult[];\n status: 'PASSED' | 'FAILED' | 'ERROR' | 'FLAKY';\n errorStep?: number;\n totalDuration: number;\n finalScreenshotPath?: string;\n retryCount?: number;\n consoleLogs?: Array<{ type: string; text: string; timestamp: number }>;\n networkErrors?: Array<{ url: string; status: number; method: string; timestamp: number }>;\n rootCauseSummary?: string;\n networkTimings?: Array<{ url: string; method: string; status: number; duration: number }>;\n}\n\nconst LOCATOR_STRATEGIES: ExecutionStep['selectorStrategy'][] = ['text', 'label', 'role', 'css'];\n\nfunction buildLocator(page: Page, selector: string, strategy: ExecutionStep['selectorStrategy'] = 'text'): Locator {\n switch (strategy) {\n case 'css': return page.locator(selector);\n case 'text': return page.getByText(selector, { exact: false });\n case 'role': return page.getByRole(selector as any);\n case 'label': return page.getByLabel(selector);\n default: return page.locator(selector);\n }\n}\n\n/** Scans the live page for visible interactive elements \u2014 the \"current page elements\"\n * fed to the AI Self-Healing Agent when mechanical locator strategies fail. */\nasync function extractCurrentPageElements(page: Page): Promise<Array<{ meaning: string; role: string; ariaLabel?: string; textContent?: string }>> {\n return page.evaluate(() => {\n const els = Array.from(document.querySelectorAll<HTMLElement>(\n 'a, button, input, select, textarea, [role=\"button\"], [role=\"link\"], [role=\"tab\"], [role=\"menuitem\"], [aria-label]'\n ));\n return els\n .filter((el) => {\n const rect = el.getBoundingClientRect();\n return rect.width > 0 && rect.height > 0;\n })\n .slice(0, 200)\n .map((el) => {\n const ariaLabel = el.getAttribute('aria-label') ?? undefined;\n const textContent = (el.textContent ?? '').replace(/\\s+/g, ' ').trim().slice(0, 100) || undefined;\n const role = el.getAttribute('role') ?? el.tagName.toLowerCase();\n return { meaning: ariaLabel || textContent || role, role, ariaLabel, textContent };\n });\n });\n}\n\n/**\n * Resolves a step's target element in three tiers:\n * 1. Exact selector + strategy as generated.\n * 2. Mechanical: same selector text, other Playwright locator strategies\n * (handles a strategy mismatch, not an actual UI change).\n * 3. AI: when tenantId/projectId are supplied, asks the Self-Healing Agent to find\n * the current page's best semantic match for the failed step description \u2014 the\n * tier that actually survives the app's selector text changing.\n */\nasync function resolveLocator(\n page: Page,\n step: ExecutionStep,\n healCtx?: { tenantId: string; projectId: string },\n): Promise<{ loc: Locator; healed: boolean; healedVia?: 'mechanical' | 'ai'; healReasoning?: string; healConfidence?: number } | null> {\n const { selector, selectorStrategy = 'text' } = step;\n if (!selector) return null;\n\n const primary = buildLocator(page, selector, selectorStrategy);\n const attached = await primary.first().isVisible({ timeout: parseInt(process.env.TEST_WAIT_TIMEOUT_MS ?? '1500', 10) }).catch(() => false);\n if (attached) return { loc: primary, healed: false };\n\n // Tier 2 \u2014 mechanical: try other strategies before giving up\n for (const strategy of LOCATOR_STRATEGIES) {\n if (strategy === selectorStrategy) continue;\n const loc = buildLocator(page, selector, strategy);\n const visible = await loc.first().isVisible({ timeout: parseInt(process.env.TEST_SHORT_TIMEOUT_MS ?? '800', 10) }).catch(() => false);\n if (visible) return { loc, healed: true, healedVia: 'mechanical' };\n }\n\n // Tier 3 \u2014 AI: the selector text itself may no longer match anything on the page.\n if (healCtx) {\n try {\n const { healElement } = await import('@taodigital/agents');\n const currentPageElements = await extractCurrentPageElements(page);\n const result = await healElement({\n tenantId: healCtx.tenantId,\n projectId: healCtx.projectId,\n failedElementMeaning: step.description || selector,\n currentPageElements,\n });\n if (result.healed && result.updatedTest) {\n const healedLoc = page.getByText(result.updatedTest.elementMeaning, { exact: false });\n const visible = await healedLoc.first().isVisible({ timeout: parseInt(process.env.TEST_WAIT_TIMEOUT_MS ?? '1500', 10) }).catch(() => false);\n if (visible) {\n return {\n loc: healedLoc,\n healed: true,\n healedVia: 'ai',\n healReasoning: result.reasoning,\n healConfidence: result.confidence,\n };\n }\n }\n } catch {\n // AI healing is a best-effort last resort \u2014 any failure here (LLM error, bad\n // JSON, etc.) just falls through to the caller's normal Playwright error below.\n }\n }\n\n return null;\n}\n\nfunction applyHealInfo(result: StepResult, r: { healed: boolean; healedVia?: 'mechanical' | 'ai'; healReasoning?: string; healConfidence?: number }): void {\n result.healed = r.healed;\n result.healedVia = r.healedVia;\n result.healReasoning = r.healReasoning;\n result.healConfidence = r.healConfidence;\n}\n\nfunction assertExecutionUrlSafe(url: string, allowUnsafeLocalUrlsForTests?: boolean): void {\n if (!allowUnsafeLocalUrlsForTests) assertCrawlUrlSafe(url);\n}\n\nexport async function executeExecutionStep(\n page: Page,\n step: ExecutionStep,\n options: { healCtx?: { tenantId: string; projectId: string }; allowUnsafeLocalUrlsForTests?: boolean } = {},\n): Promise<Pick<StepResult, 'healed' | 'healedVia' | 'healReasoning' | 'healConfidence' | 'elementBoundingRect' | 'elementSelector'>> {\n const result: Pick<StepResult, 'healed' | 'healedVia' | 'healReasoning' | 'healConfidence' | 'elementBoundingRect' | 'elementSelector'> = {};\n const timeout = step.timeout ?? parseInt(process.env.TEST_STEP_TIMEOUT_MS ?? '8000', 10);\n\n switch (step.action) {\n case 'navigate':\n if (!step.url) throw new Error('Navigate step missing url');\n assertExecutionUrlSafe(step.url, options.allowUnsafeLocalUrlsForTests);\n await page.goto(step.url, { waitUntil: 'domcontentloaded', timeout });\n break;\n\n case 'click': {\n const r = await resolveLocator(page, step, options.healCtx);\n if (!r) throw new Error('Could not resolve locator');\n result.elementBoundingRect = await r.loc.first().boundingBox().catch(() => null);\n result.elementSelector = step.selector;\n await r.loc.first().click({ timeout });\n applyHealInfo(result as StepResult, r);\n break;\n }\n\n case 'fill': {\n const r = await resolveLocator(page, step, options.healCtx);\n if (!r) throw new Error('Could not resolve locator');\n result.elementBoundingRect = await r.loc.first().boundingBox().catch(() => null);\n result.elementSelector = step.selector;\n await r.loc.first().fill(step.value ?? '', { timeout });\n applyHealInfo(result as StepResult, r);\n break;\n }\n\n case 'select': {\n const r = await resolveLocator(page, step, options.healCtx);\n if (!r) throw new Error('Could not resolve locator');\n result.elementBoundingRect = await r.loc.first().boundingBox().catch(() => null);\n result.elementSelector = step.selector;\n await r.loc.first().selectOption(step.value ?? '', { timeout });\n applyHealInfo(result as StepResult, r);\n break;\n }\n\n case 'hover': {\n const r = await resolveLocator(page, step, options.healCtx);\n if (!r) throw new Error('Could not resolve locator');\n result.elementBoundingRect = await r.loc.first().boundingBox().catch(() => null);\n result.elementSelector = step.selector;\n await r.loc.first().hover({ timeout });\n applyHealInfo(result as StepResult, r);\n break;\n }\n\n case 'press_key':\n await page.keyboard.press(step.value ?? 'Enter');\n break;\n\n case 'assert_visible': {\n const r = await resolveLocator(page, step, options.healCtx);\n if (!r) throw new Error('Could not resolve locator');\n await r.loc.first().waitFor({ state: 'visible', timeout });\n result.elementBoundingRect = await r.loc.first().boundingBox().catch(() => null);\n result.elementSelector = step.selector;\n applyHealInfo(result as StepResult, r);\n break;\n }\n\n case 'assert_text': {\n const r = await resolveLocator(page, step, options.healCtx);\n if (!r) throw new Error('Could not resolve locator');\n const text = await r.loc.first().textContent({ timeout });\n if (!text?.includes(step.value ?? '')) {\n throw new Error(`Expected text \"${step.value}\" not found. Got: \"${text?.slice(0, 100)}\"`);\n }\n result.elementBoundingRect = await r.loc.first().boundingBox().catch(() => null);\n result.elementSelector = step.selector;\n applyHealInfo(result as StepResult, r);\n break;\n }\n\n case 'assert_url': {\n const url = page.url();\n if (!url.includes(step.value ?? '')) {\n throw new Error(`Expected URL to contain \"${step.value}\". Got: \"${url}\"`);\n }\n break;\n }\n\n case 'wait':\n await page.waitForTimeout(step.timeout ?? parseInt(process.env.TEST_STEP_DELAY_MS ?? '2000', 10));\n break;\n\n case 'screenshot':\n break;\n\n case 'script':\n case 'setVariable':\n if (step.value) {\n await page.evaluate(step.value).catch(err => {\n throw new Error(`Script execution failed: ${err.message}`);\n });\n }\n break;\n }\n\n return result;\n}\n\nasync function runPlan(\n page: Page,\n plan: ExecutionStep[],\n options: { screenshotDir: string; testRunId: string; onStepComplete?: (result: StepResult) => void; healCtx?: { tenantId: string; projectId: string }; allowUnsafeLocalUrlsForTests?: boolean },\n consoleLogs: Array<{ type: string; text: string; timestamp: number }>,\n networkErrors: Array<{ url: string; status: number; method: string; timestamp: number }>,\n): Promise<{ stepResults: StepResult[]; errorStep?: number; finalScreenshotPath?: string; consoleLogs: typeof consoleLogs; networkErrors: typeof networkErrors }> {\n const stepResults: StepResult[] = [];\n let errorStep: number | undefined;\n let finalScreenshotPath: string | undefined;\n\n for (const step of plan) {\n const stepStart = Date.now();\n const result: StepResult = {\n stepNumber: step.stepNumber,\n description: step.description,\n status: 'FAILED',\n duration: 0,\n };\n\n try {\n const timeout = step.timeout ?? parseInt(process.env.TEST_STEP_TIMEOUT_MS ?? '8000', 10);\n\n switch (step.action) {\n case 'navigate':\n if (!step.url) throw new Error('Navigate step missing url');\n assertExecutionUrlSafe(step.url, options.allowUnsafeLocalUrlsForTests);\n await page.goto(step.url, { waitUntil: 'domcontentloaded', timeout });\n break;\n\n case 'click': {\n const r = await resolveLocator(page, step, options.healCtx);\n if (!r) throw new Error('Could not resolve locator');\n result.elementBoundingRect = await r.loc.first().boundingBox().catch(() => null);\n result.elementSelector = step.selector;\n await r.loc.first().click({ timeout });\n applyHealInfo(result, r);\n break;\n }\n\n case 'fill': {\n const r = await resolveLocator(page, step, options.healCtx);\n if (!r) throw new Error('Could not resolve locator');\n result.elementBoundingRect = await r.loc.first().boundingBox().catch(() => null);\n result.elementSelector = step.selector;\n await r.loc.first().fill(step.value ?? '', { timeout });\n applyHealInfo(result, r);\n break;\n }\n\n case 'select': {\n const r = await resolveLocator(page, step, options.healCtx);\n if (!r) throw new Error('Could not resolve locator');\n result.elementBoundingRect = await r.loc.first().boundingBox().catch(() => null);\n result.elementSelector = step.selector;\n await r.loc.first().selectOption(step.value ?? '', { timeout });\n applyHealInfo(result, r);\n break;\n }\n\n case 'hover': {\n const r = await resolveLocator(page, step, options.healCtx);\n if (!r) throw new Error('Could not resolve locator');\n result.elementBoundingRect = await r.loc.first().boundingBox().catch(() => null);\n result.elementSelector = step.selector;\n await r.loc.first().hover({ timeout });\n applyHealInfo(result, r);\n break;\n }\n\n case 'press_key':\n await page.keyboard.press(step.value ?? 'Enter');\n break;\n\n case 'assert_visible': {\n const r = await resolveLocator(page, step, options.healCtx);\n if (!r) throw new Error('Could not resolve locator');\n await r.loc.first().waitFor({ state: 'visible', timeout });\n result.elementBoundingRect = await r.loc.first().boundingBox().catch(() => null);\n result.elementSelector = step.selector;\n applyHealInfo(result, r);\n break;\n }\n\n case 'assert_text': {\n const r = await resolveLocator(page, step, options.healCtx);\n if (!r) throw new Error('Could not resolve locator');\n const text = await r.loc.first().textContent({ timeout });\n if (!text?.includes(step.value ?? '')) {\n throw new Error(`Expected text \"${step.value}\" not found. Got: \"${text?.slice(0, 100)}\"`);\n }\n result.elementBoundingRect = await r.loc.first().boundingBox().catch(() => null);\n result.elementSelector = step.selector;\n applyHealInfo(result, r);\n break;\n }\n\n case 'assert_url': {\n const url = page.url();\n if (!url.includes(step.value ?? '')) {\n throw new Error(`Expected URL to contain \"${step.value}\". Got: \"${url}\"`);\n }\n break;\n }\n\n case 'wait':\n await page.waitForTimeout(step.timeout ?? parseInt(process.env.TEST_STEP_DELAY_MS ?? '2000', 10));\n break;\n\n case 'screenshot': {\n const ssPath = `${options.screenshotDir}/${options.testRunId}-step-${step.stepNumber}.png`;\n await page.screenshot({ path: ssPath, fullPage: true });\n result.screenshotPath = ssPath;\n break;\n }\n\n case 'script':\n case 'setVariable': {\n if (step.value) {\n // Execute arbitrary JS string. Often used by manual test builder extension for custom variables\n // or assertions.\n await page.evaluate(step.value).catch(err => {\n throw new Error(`Script execution failed: ${err.message}`);\n });\n }\n break;\n }\n }\n\n result.status = 'PASSED';\n } catch (err: any) {\n result.status = step.optional ? 'SKIPPED' : 'FAILED';\n result.error = err.message;\n if (!step.optional) {\n errorStep = step.stepNumber;\n }\n }\n\n try {\n await page.waitForLoadState('networkidle', { timeout: parseInt(process.env.TEST_NETWORK_IDLE_TIMEOUT_MS ?? '1500', 10) }).catch(() => {});\n if (!result.screenshotPath) {\n const ssPath = `${options.screenshotDir}/${options.testRunId}-step-${step.stepNumber}.png`;\n await page.screenshot({ path: ssPath, fullPage: true });\n result.screenshotPath = ssPath;\n }\n } catch { /* Screenshot evidence is best-effort and should not fail the run. */ }\n\n result.duration = Date.now() - stepStart;\n stepResults.push(result);\n options.onStepComplete?.(result);\n\n if (result.status === 'FAILED') break;\n }\n\n try {\n const finalPath = `${options.screenshotDir}/${options.testRunId}-final.png`;\n await page.screenshot({ path: finalPath, fullPage: true });\n finalScreenshotPath = finalPath;\n } catch { /* non-fatal */ }\n\n return { stepResults, errorStep, finalScreenshotPath, consoleLogs, networkErrors };\n}\n\nfunction interpolate(template: string | undefined, data: Record<string, string>): string | undefined {\n if (!template) return template;\n return template.replace(/\\{\\{(\\w+)\\}\\}/g, (_, key) => data[key] ?? `{{${key}}}`);\n}\n\nexport async function executeTestPlan(\n plan: ExecutionStep[],\n options: {\n screenshotDir: string;\n testRunId: string;\n viewport?: { width: number; height: number };\n onStepComplete?: (result: StepResult) => void;\n /** Run the test up to this many times to detect flakiness (default 1). */\n maxRuns?: number;\n browser?: 'chromium' | 'firefox' | 'webkit';\n /** Data row for parameterized execution: substitutes {{varname}} in step fields. */\n dataRow?: Record<string, string>;\n /** Enables the AI Self-Healing Agent tier when a step's selector can't be resolved\n * even after trying alternate locator strategies. Omit to keep mechanical-only\n * healing (no LLM calls during execution). */\n healCtx?: { tenantId: string; projectId: string };\n /** Decrypted Playwright storageState JSON \u2014 restores a logged-in session so\n * steps don't have to re-authenticate. Without this, any test case behind a\n * login wall fails at the first authenticated step. */\n storageState?: string;\n /** Test fixtures may run on loopback; production callers must leave this unset. */\n allowUnsafeLocalUrlsForTests?: boolean;\n },\n): Promise<ExecutionResult> {\n if (options.dataRow && Object.keys(options.dataRow).length > 0) {\n const data = options.dataRow;\n plan = plan.map((step) => ({\n ...step,\n url: interpolate(step.url, data),\n selector: interpolate(step.selector, data),\n value: interpolate(step.value, data),\n description: interpolate(step.description, data) ?? step.description,\n }));\n }\n const maxRuns = options.maxRuns ?? 1;\n const browserEngine = { chromium, firefox, webkit }[options.browser ?? 'chromium'] ?? chromium;\n const start = Date.now();\n\n const outcomes: ('PASSED' | 'FAILED')[] = [];\n let lastResult: Awaited<ReturnType<typeof runPlan>> | null = null;\n let lastNetworkTimings: Array<{ url: string; method: string; status: number; duration: number }> = [];\n\n for (let run = 0; run < maxRuns; run++) {\n const browser = await browserEngine.launch({\n headless: true,\n // Optional override for environments with a non-standard/pre-provisioned\n // browser install (e.g. a custom base image with only system Chromium).\n ...(process.env.PLAYWRIGHT_EXECUTABLE_PATH ? { executablePath: process.env.PLAYWRIGHT_EXECUTABLE_PATH } : {}),\n });\n let parsedStorageState: any;\n try {\n parsedStorageState = options.storageState ? JSON.parse(options.storageState) : undefined;\n } catch {\n parsedStorageState = undefined;\n }\n const context = await browser.newContext({\n viewport: options.viewport ?? { width: 1440, height: 900 },\n ...(parsedStorageState ? { storageState: parsedStorageState } : {}),\n });\n const page = await context.newPage();\n\n await page\n .addStyleTag({ content: '*, *::before, *::after { animation-duration: 0s !important; transition-duration: 0s !important; }' })\n .catch(() => {});\n\n const consoleLogs: Array<{ type: string; text: string; timestamp: number }> = [];\n const networkErrors: Array<{ url: string; status: number; method: string; timestamp: number }> = [];\n const networkTimings: Array<{ url: string; method: string; status: number; duration: number }> = [];\n const requestStartTimes = new Map<string, number>();\n\n page.on('console', msg => {\n if (consoleLogs.length < 100) {\n consoleLogs.push({ type: msg.type(), text: msg.text().slice(0, 500), timestamp: Date.now() });\n }\n });\n page.on('request', request => {\n const url = request.url();\n if (url.includes('/api/') || url.match(/\\/(graphql|rest|v\\d)\\//)) {\n requestStartTimes.set(request.url(), Date.now());\n }\n });\n page.on('response', response => {\n const url = response.url();\n const start = requestStartTimes.get(url);\n if (start && networkTimings.length < 200) {\n networkTimings.push({ url: url.slice(0, 300), method: response.request().method(), status: response.status(), duration: Date.now() - start });\n requestStartTimes.delete(url);\n }\n if (!response.ok() && !url.includes('favicon') && networkErrors.length < 50) {\n networkErrors.push({\n url: url.slice(0, 300),\n status: response.status(),\n method: response.request().method(),\n timestamp: Date.now(),\n });\n }\n });\n\n try {\n const TEST_EXECUTION_TIMEOUT_MS = 5 * 60_000;\n lastResult = await Promise.race([\n runPlan(page, plan, options, consoleLogs, networkErrors),\n new Promise<never>((_, reject) =>\n setTimeout(() => reject(new Error('Test execution timed out')), TEST_EXECUTION_TIMEOUT_MS)\n ),\n ]);\n lastNetworkTimings = networkTimings;\n } finally {\n await context.close().catch(() => {});\n await browser.close().catch(() => {});\n }\n\n const passed = !lastResult.stepResults.some((r) => r.status === 'FAILED');\n outcomes.push(passed ? 'PASSED' : 'FAILED');\n\n // If all subsequent runs are consistent with first, no need to keep going\n if (run > 0 && outcomes.every((o) => o === outcomes[0])) break;\n }\n\n const passCount = outcomes.filter((o) => o === 'PASSED').length;\n const failCount = outcomes.filter((o) => o === 'FAILED').length;\n\n let status: ExecutionResult['status'];\n if (maxRuns > 1 && passCount > 0 && failCount > 0) {\n status = 'FLAKY';\n } else if (passCount === outcomes.length) {\n status = 'PASSED';\n } else {\n status = 'FAILED';\n }\n\n let rootCauseSummary: string | undefined;\n if ((status === 'FAILED' || status === 'FLAKY') && options.healCtx?.tenantId) {\n const failedStep = lastResult!.stepResults.find(s => s.status === 'FAILED');\n const { analyzeFailure } = await import('./failure-analysis-agent.js');\n rootCauseSummary = await analyzeFailure({\n tenantId: options.healCtx?.tenantId,\n error: failedStep?.error ?? 'Unknown error',\n stepDescription: failedStep?.description,\n consoleLogs: lastResult!.consoleLogs,\n networkErrors: lastResult!.networkErrors,\n screenshotPath: lastResult!.finalScreenshotPath,\n }).catch(() => undefined) ?? undefined;\n }\n\n return {\n stepResults: lastResult!.stepResults,\n status,\n errorStep: lastResult!.errorStep,\n totalDuration: Date.now() - start,\n finalScreenshotPath: lastResult!.finalScreenshotPath,\n retryCount: outcomes.length - 1,\n consoleLogs: lastResult!.consoleLogs,\n networkErrors: lastResult!.networkErrors,\n rootCauseSummary,\n networkTimings: lastNetworkTimings,\n };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["start"]
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/visual-diff-verdict.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Pure visual-diff verdict logic, split out of crawler.ts so it can be unit-tested\n * without pulling in crawler.ts's full import graph (Playwright, Stagehand, and\n * transitively @
|
|
4
|
+
"sourcesContent": ["/**\n * Pure visual-diff verdict logic, split out of crawler.ts so it can be unit-tested\n * without pulling in crawler.ts's full import graph (Playwright, Stagehand, and\n * transitively @taodigital/database, which instantiates a PrismaClient at module load and\n * so requires a live DATABASE_URL just to import the module).\n */\n\nexport interface VisualDiffVerdict {\n changeType: 'NONE' | 'MINOR' | 'SIGNIFICANT' | 'CRITICAL';\n recommendation: 'APPROVE' | 'REVIEW' | 'FLAG' | 'BLOCK';\n changes: Array<{ location: string; severity: string; description: string }>;\n bugReportDraft?: { title: string; severity: string; steps: string[]; expected: string; actual: string };\n aiSummary?: string;\n}\n\n/** Cheap, always-on first-pass verdict from perceptual hash distance + layout/element diffs. */\nexport function pHashVerdict(\n pHashDistance: number,\n changeLevel: 'IDENTICAL' | 'MINOR' | 'MODERATE' | 'SIGNIFICANT',\n changes: Array<{ location: string; severity: string; description: string }>,\n): VisualDiffVerdict {\n const changeType: VisualDiffVerdict['changeType'] = changeLevel === 'IDENTICAL' ? 'NONE'\n : changeLevel === 'MINOR' ? 'MINOR'\n : changeLevel === 'MODERATE' ? 'SIGNIFICANT'\n : 'CRITICAL';\n const approveThreshold = parseInt(process.env.CRAWLER_VISUAL_DIFF_APPROVE_THRESHOLD ?? '') || 5;\n const reviewThreshold = parseInt(process.env.CRAWLER_VISUAL_DIFF_REVIEW_THRESHOLD ?? '') || 20;\n const recommendation: VisualDiffVerdict['recommendation'] = pHashDistance === 0 ? 'APPROVE'\n : pHashDistance < approveThreshold ? 'APPROVE'\n : pHashDistance < reviewThreshold ? 'REVIEW'\n : 'FLAG';\n return { changeType, recommendation, changes };\n}\n\n/** Replaces a pHash-only verdict with the AI's semantic judgment once it's available,\n * and drafts a bug report for anything still CRITICAL/SIGNIFICANT after AI review\n * (pHash flags plenty of anti-aliasing/font-rendering noise the AI correctly discards\n * as NONE/MINOR \u2014 only what survives AI review is worth a draft). */\nexport function mergeAiVisualVerdict(\n pHashResult: VisualDiffVerdict,\n aiResult: { changeType: string; changes: any[]; recommendation: string; diffScore: number; summary?: string },\n screenName: string,\n): VisualDiffVerdict {\n if (!aiResult.changeType) return pHashResult;\n const changeType = aiResult.changeType as VisualDiffVerdict['changeType'];\n const recommendation = aiResult.recommendation as VisualDiffVerdict['recommendation'];\n const changes = Array.isArray(aiResult.changes) && aiResult.changes.length > 0 ? aiResult.changes : pHashResult.changes;\n const aiSummary = aiResult.summary ?? undefined;\n const bugReportDraft = (changeType === 'CRITICAL' || changeType === 'SIGNIFICANT')\n ? {\n title: `Visual regression on \"${screenName}\"`,\n severity: changeType === 'CRITICAL' ? 'High' : 'Medium',\n steps: ['Navigate to the screen', 'Compare current view to baseline screenshot'],\n expected: 'Screen appearance should match the stored baseline.',\n actual: aiSummary || `Screen has changed (diff score: ${aiResult.diffScore.toFixed(1)}%)`,\n }\n : undefined;\n return { changeType, recommendation, changes, bugReportDraft, aiSummary };\n}\n"],
|
|
5
5
|
"mappings": "AAgBO,SAAS,aACd,eACA,aACA,SACmB;AACnB,QAAM,aAA8C,gBAAgB,cAAc,SAC9E,gBAAgB,UAAU,UACxB,gBAAgB,aAAa,gBAC3B;AACR,QAAM,mBAAmB,SAAS,QAAQ,IAAI,yCAAyC,EAAE,KAAK;AAC9F,QAAM,kBAAkB,SAAS,QAAQ,IAAI,wCAAwC,EAAE,KAAK;AAC5F,QAAM,iBAAsD,kBAAkB,IAAI,YAC9E,gBAAgB,mBAAmB,YACjC,gBAAgB,kBAAkB,WAChC;AACR,SAAO,EAAE,YAAY,gBAAgB,QAAQ;AAC/C;AAMO,SAAS,qBACd,aACA,UACA,YACmB;AACnB,MAAI,CAAC,SAAS,WAAY,QAAO;AACjC,QAAM,aAAa,SAAS;AAC5B,QAAM,iBAAiB,SAAS;AAChC,QAAM,UAAU,MAAM,QAAQ,SAAS,OAAO,KAAK,SAAS,QAAQ,SAAS,IAAI,SAAS,UAAU,YAAY;AAChH,QAAM,YAAY,SAAS,WAAW;AACtC,QAAM,iBAAkB,eAAe,cAAc,eAAe,gBAChE;AAAA,IACE,OAAO,yBAAyB,UAAU;AAAA,IAC1C,UAAU,eAAe,aAAa,SAAS;AAAA,IAC/C,OAAO,CAAC,0BAA0B,6CAA6C;AAAA,IAC/E,UAAU;AAAA,IACV,QAAQ,aAAa,mCAAmC,SAAS,UAAU,QAAQ,CAAC,CAAC;AAAA,EACvF,IACA;AACJ,SAAO,EAAE,YAAY,gBAAgB,SAAS,gBAAgB,UAAU;AAC1E;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/worker.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/worker.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Standalone worker process entrypoint for a dedicated crawler/worker container,\n * separate from the API server (see infrastructure/ecs-task-definitions/crawler.json).\n *\n * index.ts is a side-effect-free library barrel imported by @
|
|
4
|
+
"sourcesContent": ["/**\n * Standalone worker process entrypoint for a dedicated crawler/worker container,\n * separate from the API server (see infrastructure/ecs-task-definitions/crawler.json).\n *\n * index.ts is a side-effect-free library barrel imported by @taodigital/api \u2014 it must NOT\n * start any background loop itself, or importing a single helper from @taodigital/crawler\n * would silently spin up a second worker inside the API process too. This file is\n * the actual entrypoint that keeps a dedicated worker container's process alive and\n * consuming jobs: `node dist/worker.js`.\n */\nimport { startJobWorkerLoop, processDataRetentionPurge } from './job-queue.js';\nimport { logger } from './logger.js';\n\nasync function main() {\n logger.info('[worker] starting standalone crawler worker process');\n\n if (process.env.JOB_QUEUE_BACKEND === 'sqs') {\n // SQS mode: messages arrive via long-poll; claimSpecificJob atomically moves\n // QUEUED\u2192RUNNING before dispatch, so multiple worker replicas are safe.\n // DB poll loop also runs as a safety net for jobs whose SQS message was lost.\n const { startSqsWorker } = await import('./sqs-queue.js');\n startSqsWorker().catch((err) => {\n logger.error({ err: String(err) }, '[worker] SQS worker crashed');\n process.exit(1);\n });\n }\n\n // DB-backed poll loop \u2014 primary in non-SQS mode, safety net in SQS mode.\n // Claims all job types. Safe to run in many worker replicas (FOR UPDATE SKIP LOCKED).\n startJobWorkerLoop();\n\n // Daily data-retention purge: runs immediately on startup then every 24 h.\n // Only runs in the dedicated worker container (this file), not in the API\n // process, so there is exactly one purge sweep regardless of API replica count.\n const runPurge = () => {\n processDataRetentionPurge().catch((err) => {\n logger.error({ err: String(err) }, '[worker] data-retention purge failed');\n });\n };\n runPurge();\n setInterval(runPurge, 24 * 60 * 60 * 1000);\n}\n\nprocess.on('SIGTERM', () => { logger.info('[worker] SIGTERM received, exiting'); process.exit(0); });\nprocess.on('SIGINT', () => { logger.info('[worker] SIGINT received, exiting'); process.exit(0); });\n\nmain().catch((err) => {\n logger.error({ err: String(err) }, '[worker] fatal startup error');\n process.exit(1);\n});\n"],
|
|
5
5
|
"mappings": "AAUA,SAAS,oBAAoB,iCAAiC;AAC9D,SAAS,cAAc;AAEvB,eAAe,OAAO;AACpB,SAAO,KAAK,qDAAqD;AAEjE,MAAI,QAAQ,IAAI,sBAAsB,OAAO;AAI3C,UAAM,EAAE,eAAe,IAAI,MAAM,OAAO,gBAAgB;AACxD,mBAAe,EAAE,MAAM,CAAC,QAAQ;AAC9B,aAAO,MAAM,EAAE,KAAK,OAAO,GAAG,EAAE,GAAG,6BAA6B;AAChE,cAAQ,KAAK,CAAC;AAAA,IAChB,CAAC;AAAA,EACH;AAIA,qBAAmB;AAKnB,QAAM,WAAW,MAAM;AACrB,8BAA0B,EAAE,MAAM,CAAC,QAAQ;AACzC,aAAO,MAAM,EAAE,KAAK,OAAO,GAAG,EAAE,GAAG,sCAAsC;AAAA,IAC3E,CAAC;AAAA,EACH;AACA,WAAS;AACT,cAAY,UAAU,KAAK,KAAK,KAAK,GAAI;AAC3C;AAEA,QAAQ,GAAG,WAAW,MAAM;AAAE,SAAO,KAAK,oCAAoC;AAAG,UAAQ,KAAK,CAAC;AAAG,CAAC;AACnG,QAAQ,GAAG,UAAU,MAAM;AAAE,SAAO,KAAK,mCAAmC;AAAG,UAAQ,KAAK,CAAC;AAAG,CAAC;AAEjG,KAAK,EAAE,MAAM,CAAC,QAAQ;AACpB,SAAO,MAAM,EAAE,KAAK,OAAO,GAAG,EAAE,GAAG,8BAA8B;AACjE,UAAQ,KAAK,CAAC;AAChB,CAAC;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
package/src/http-engine.ts
CHANGED
|
@@ -19,9 +19,6 @@
|
|
|
19
19
|
* - Streaming progress via onProgress callback
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
import { createHash } from 'node:crypto';
|
|
23
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
24
|
-
import { join } from 'node:path';
|
|
25
22
|
import { Readability } from '@mozilla/readability';
|
|
26
23
|
import TurndownService from 'turndown';
|
|
27
24
|
import { parseHTML } from 'linkedom';
|
|
@@ -120,6 +117,7 @@ export interface HttpCrawlOpts {
|
|
|
120
117
|
includeRawHtml?: boolean;
|
|
121
118
|
timeoutMs?: number; // per-request timeout, default 15000
|
|
122
119
|
concurrency?: number; // default 5 parallel fetches
|
|
120
|
+
proxyUrl?: string; // HTTP/HTTPS/SOCKS5 proxy URL
|
|
123
121
|
onProgress?: (update: { url: string; screensFound: number; total: number; cached: boolean }) => void;
|
|
124
122
|
}
|
|
125
123
|
|
|
@@ -173,7 +171,7 @@ function extractLinks(html: string, baseUrl: string): string[] {
|
|
|
173
171
|
return links;
|
|
174
172
|
}
|
|
175
173
|
|
|
176
|
-
function extractMarkdown(html: string,
|
|
174
|
+
function extractMarkdown(html: string, _url: string): { markdown: string; title: string | null } {
|
|
177
175
|
try {
|
|
178
176
|
const { document } = parseHTML(html);
|
|
179
177
|
// Remove script/style tags before Readability
|
|
@@ -211,7 +209,7 @@ interface FetchResult {
|
|
|
211
209
|
async function fetchWithBackoff(
|
|
212
210
|
url: string,
|
|
213
211
|
cache: ResponseCache,
|
|
214
|
-
opts: { userAgent: string; timeoutMs: number; extraHeaders?: Record<string, string
|
|
212
|
+
opts: { userAgent: string; timeoutMs: number; extraHeaders?: Record<string, string>; dispatcher?: any },
|
|
215
213
|
retryCount = 0,
|
|
216
214
|
httpCfg?: Awaited<ReturnType<typeof getHttpConfig>>
|
|
217
215
|
): Promise<FetchResult> {
|
|
@@ -245,7 +243,7 @@ async function fetchWithBackoff(
|
|
|
245
243
|
|
|
246
244
|
let response: Response;
|
|
247
245
|
try {
|
|
248
|
-
const dispatcher = await getChromeFetchDispatcher();
|
|
246
|
+
const dispatcher = opts.dispatcher ?? await getChromeFetchDispatcher();
|
|
249
247
|
response = await (fetch as any)(url, {
|
|
250
248
|
headers,
|
|
251
249
|
signal: AbortSignal.timeout(opts.timeoutMs),
|
|
@@ -313,6 +311,13 @@ export async function httpCrawl(opts: HttpCrawlOpts): Promise<HttpCrawlResult> {
|
|
|
313
311
|
const userAgent = opts.userAgent ?? 'ZeTa-Crawler/1.0 (+https://zeta.taodigitalsolutions.com/crawler)';
|
|
314
312
|
const cache = new ResponseCache(cacheDir);
|
|
315
313
|
const robots = opts.respectRobotsTxt !== false ? new RobotsCache() : null;
|
|
314
|
+
|
|
315
|
+
// Build proxy dispatcher when proxyUrl is provided — uses undici ProxyAgent.
|
|
316
|
+
let proxyDispatcher: any;
|
|
317
|
+
if (opts.proxyUrl) {
|
|
318
|
+
const { ProxyAgent } = await import('undici');
|
|
319
|
+
proxyDispatcher = new ProxyAgent(opts.proxyUrl);
|
|
320
|
+
}
|
|
316
321
|
const urlFilterRe = opts.urlFilter ? (() => { try { return new RegExp(opts.urlFilter!, 'i'); } catch { return null; } })() : null;
|
|
317
322
|
|
|
318
323
|
const visited = new Set<string>();
|
|
@@ -354,7 +359,7 @@ export async function httpCrawl(opts: HttpCrawlOpts): Promise<HttpCrawlResult> {
|
|
|
354
359
|
|
|
355
360
|
let fetchResult: FetchResult;
|
|
356
361
|
try {
|
|
357
|
-
fetchResult = await fetchWithBackoff(url, cache, { userAgent, timeoutMs, extraHeaders: opts.headers }, 0, httpCfg);
|
|
362
|
+
fetchResult = await fetchWithBackoff(url, cache, { userAgent, timeoutMs, extraHeaders: opts.headers, dispatcher: proxyDispatcher }, 0, httpCfg);
|
|
358
363
|
if (fetchResult.statusCode === 429 || fetchResult.statusCode === 503) rateLimitedCount++;
|
|
359
364
|
} catch (err: any) {
|
|
360
365
|
console.error(`[http-engine] error fetching ${url}: ${err.message}`);
|
package/src/job-queue.ts
CHANGED
|
@@ -765,28 +765,36 @@ export async function getJob(tenantId: string, jobId: string) {
|
|
|
765
765
|
}
|
|
766
766
|
|
|
767
767
|
/** List recent jobs for a project. */
|
|
768
|
-
export async function
|
|
769
|
-
return withTenant(tenantId,
|
|
770
|
-
|
|
768
|
+
export async function countJobs(tenantId: string, projectId: string): Promise<number> {
|
|
769
|
+
return withTenant(tenantId, (tx) => tx.platformJob.count({ where: { tenantId, projectId } }));
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
export async function listJobs(tenantId: string, projectId: string, { skip = 0, take = 50 }: { skip?: number; take?: number } = {}) {
|
|
773
|
+
const jobs = await withTenant(tenantId, (tx) =>
|
|
774
|
+
tx.platformJob.findMany({
|
|
771
775
|
where: { tenantId, projectId },
|
|
772
776
|
orderBy: { queuedAt: 'desc' },
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
777
|
+
skip,
|
|
778
|
+
take,
|
|
779
|
+
})
|
|
780
|
+
);
|
|
781
|
+
if (jobs.length === 0) return jobs;
|
|
782
|
+
// groupBy runs outside the RLS transaction to avoid aborted-transaction cascade
|
|
783
|
+
let modelByJobId = new Map<string, string>();
|
|
784
|
+
try {
|
|
776
785
|
const jobIds = jobs.map((j) => j.id);
|
|
777
|
-
|
|
778
|
-
const modelRows = await (tx as any).lLMCallLog.groupBy({
|
|
786
|
+
const modelRows = await (prismaAdmin as any).lLMCallLog.groupBy({
|
|
779
787
|
by: ['jobId'],
|
|
780
788
|
where: { tenantId, jobId: { in: jobIds } },
|
|
781
789
|
_max: { model: true, calledAt: true },
|
|
782
790
|
});
|
|
783
|
-
|
|
791
|
+
modelByJobId = new Map<string, string>(
|
|
784
792
|
modelRows
|
|
785
793
|
.filter((r: any) => r.jobId && r._max?.model)
|
|
786
794
|
.map((r: any) => [r.jobId as string, r._max.model as string])
|
|
787
795
|
);
|
|
788
|
-
|
|
789
|
-
});
|
|
796
|
+
} catch { /* best-effort */ }
|
|
797
|
+
return jobs.map((j) => ({ ...j, llmModel: modelByJobId.get(j.id) ?? null }));
|
|
790
798
|
}
|
|
791
799
|
|
|
792
800
|
/** Cancel a RUNNING or QUEUED job.
|
|
@@ -1007,6 +1015,15 @@ export async function processJob(jobId: string, config: any, opts?: ProcessJobOp
|
|
|
1007
1015
|
}
|
|
1008
1016
|
: undefined;
|
|
1009
1017
|
|
|
1018
|
+
// Resolve workspace proxy: project-level proxyUrl takes priority; fall back to
|
|
1019
|
+
// workspace (tenant) proxy when neither the job config nor project crawlConfig set one.
|
|
1020
|
+
if (!config.proxyUrl) {
|
|
1021
|
+
const tenantProxy = await AppBrain.getTenantProxyConfig(tenantId).catch(() => null);
|
|
1022
|
+
if (tenantProxy?.mode === 'agent') config.proxyUrl = 'agent';
|
|
1023
|
+
else if (tenantProxy?.mode === 'auto') config.proxyUrl = 'auto';
|
|
1024
|
+
else if (tenantProxy?.mode === 'custom' && tenantProxy.proxyUrl) config.proxyUrl = tenantProxy.proxyUrl;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1010
1027
|
const storageConfigForCrawl = await getTenantStorageConfig(tenantId).catch(() => null);
|
|
1011
1028
|
const result = await crawlProject({
|
|
1012
1029
|
...config, credentials: runtimeCreds, abortSignal: controller.signal, jobId,
|
|
@@ -2157,7 +2174,16 @@ async function processDiscoverJob(jobId: string, config: any, opts?: ProcessJobO
|
|
|
2157
2174
|
const { getSystemConfig } = await import('@taodigital/core');
|
|
2158
2175
|
const crawlTuning = await getSystemConfig('crawl_tuning');
|
|
2159
2176
|
const maxDiscoverUrls: number = config.maxDiscoverUrls ?? parseInt(String(crawlTuning?.maxDiscoverUrls ?? '2000'), 10);
|
|
2160
|
-
|
|
2177
|
+
// Resolve proxy: job config → project crawlConfig → workspace (tenant) proxy.
|
|
2178
|
+
const projectProxyUrl: string | undefined = config.proxyUrl ?? crawlCfg.proxyUrl;
|
|
2179
|
+
const proxyUrl: string | undefined = await (async () => {
|
|
2180
|
+
if (projectProxyUrl) return projectProxyUrl;
|
|
2181
|
+
const tenantProxy = await AppBrain.getTenantProxyConfig(tenantId).catch(() => null);
|
|
2182
|
+
if (tenantProxy?.mode === 'agent') return 'agent';
|
|
2183
|
+
if (tenantProxy?.mode === 'auto') return 'auto';
|
|
2184
|
+
if (tenantProxy?.mode === 'custom' && tenantProxy.proxyUrl) return tenantProxy.proxyUrl;
|
|
2185
|
+
return undefined;
|
|
2186
|
+
})();
|
|
2161
2187
|
const screenshotDir = os.tmpdir();
|
|
2162
2188
|
const discoverStorageConfig = await getTenantStorageConfig(tenantId).catch(() => null);
|
|
2163
2189
|
|
|
@@ -2373,6 +2399,11 @@ async function processMonitorJob(jobId: string, config: any, opts?: ProcessJobOp
|
|
|
2373
2399
|
|
|
2374
2400
|
await updateJobProgress(tenantId, jobId, 10, `Monitoring ${monitorUrls.length} URL(s)...`);
|
|
2375
2401
|
|
|
2402
|
+
// Resolve proxy: job config takes priority; fall back to workspace (tenant) proxy.
|
|
2403
|
+
const wsProxy = await AppBrain.getTenantProxyConfig(tenantId).catch(() => null);
|
|
2404
|
+
const monitorProxyUrl: string | undefined = config.proxyUrl ??
|
|
2405
|
+
(wsProxy?.mode === 'custom' && wsProxy.proxyUrl ? wsProxy.proxyUrl : undefined);
|
|
2406
|
+
|
|
2376
2407
|
// Baseline: stored baselines from previous monitor run take priority;
|
|
2377
2408
|
// project screens' elementHash is the fallback for first-run.
|
|
2378
2409
|
const storedBaselines: Record<string, string> = crawlCfg.monitorBaselines ?? {};
|
|
@@ -2399,7 +2430,7 @@ async function processMonitorJob(jobId: string, config: any, opts?: ProcessJobOp
|
|
|
2399
2430
|
opts?.onProgress?.(pct);
|
|
2400
2431
|
|
|
2401
2432
|
try {
|
|
2402
|
-
const crawlResult = await httpCrawl({ appUrl: url, maxScreens: 1, respectRobotsTxt: false, timeoutMs: parseInt(process.env.CRAWL_TIMEOUT_MS ?? '20000', 10) });
|
|
2433
|
+
const crawlResult = await httpCrawl({ appUrl: url, maxScreens: 1, respectRobotsTxt: false, timeoutMs: parseInt(process.env.CRAWL_TIMEOUT_MS ?? '20000', 10), proxyUrl: monitorProxyUrl });
|
|
2403
2434
|
const screen = crawlResult.screens[0];
|
|
2404
2435
|
if (!screen) {
|
|
2405
2436
|
results.push({ url, changed: false, reason: 'fetch_empty', currentHash: '', previousHash: null });
|
|
@@ -2977,6 +3008,17 @@ async function processMultiRoleCrawlJob(jobId: string, config: any) {
|
|
|
2977
3008
|
const credentialProfiles: Array<{ id: string; name: string; username: string; password: string }> =
|
|
2978
3009
|
config.credentialProfiles ?? [];
|
|
2979
3010
|
|
|
3011
|
+
// Resolve workspace proxy for child jobs
|
|
3012
|
+
const multiRoleWsProxy = await AppBrain.getTenantProxyConfig(tenantId).catch(() => null);
|
|
3013
|
+
const multiRoleResolvedProxyUrl: string | undefined = (() => {
|
|
3014
|
+
const projectProxy = (savedCrawlConfig as any)?.proxyUrl;
|
|
3015
|
+
if (projectProxy === 'agent' || multiRoleWsProxy?.mode === 'agent') return 'agent';
|
|
3016
|
+
if (projectProxy === 'auto' || multiRoleWsProxy?.mode === 'auto') return 'auto';
|
|
3017
|
+
if (projectProxy && projectProxy !== 'auto' && projectProxy !== 'agent') return projectProxy;
|
|
3018
|
+
if (multiRoleWsProxy?.mode === 'custom' && multiRoleWsProxy.proxyUrl) return multiRoleWsProxy.proxyUrl;
|
|
3019
|
+
return undefined;
|
|
3020
|
+
})();
|
|
3021
|
+
|
|
2980
3022
|
// ── Step 1: enqueue primary CRAWL ────────────────────────────────────────
|
|
2981
3023
|
await updateJobProgress(tenantId, jobId, 10, 'Enqueuing primary crawl…');
|
|
2982
3024
|
const primaryJobId = await enqueueJob('CRAWL', {
|
|
@@ -2991,7 +3033,7 @@ async function processMultiRoleCrawlJob(jobId: string, config: any) {
|
|
|
2991
3033
|
startUrls: savedCrawlConfig.seedUrls?.length ? savedCrawlConfig.seedUrls : undefined,
|
|
2992
3034
|
deviceProfiles: savedCrawlConfig.deviceProfiles ?? ['DESKTOP'],
|
|
2993
3035
|
loginInstructions: savedCrawlConfig.loginInstructions,
|
|
2994
|
-
proxyUrl:
|
|
3036
|
+
proxyUrl: multiRoleResolvedProxyUrl,
|
|
2995
3037
|
useSitemap: true,
|
|
2996
3038
|
sitemapUrl: savedCrawlConfig.sitemapUrl,
|
|
2997
3039
|
});
|
|
@@ -3048,7 +3090,7 @@ async function processMultiRoleCrawlJob(jobId: string, config: any) {
|
|
|
3048
3090
|
storageState: savedCrawlConfig.storageState,
|
|
3049
3091
|
deviceProfiles: savedCrawlConfig.deviceProfiles ?? ['DESKTOP'],
|
|
3050
3092
|
loginInstructions: savedCrawlConfig.loginInstructions,
|
|
3051
|
-
proxyUrl:
|
|
3093
|
+
proxyUrl: multiRoleResolvedProxyUrl,
|
|
3052
3094
|
});
|
|
3053
3095
|
roleJobs.push({ roleName: profile.name, jobId: roleJobId });
|
|
3054
3096
|
logger.info({ roleJobId, roleName: profile.name, projectId }, '[multi-role-crawl] ROLE_CRAWL enqueued');
|
|
@@ -3140,6 +3182,17 @@ async function processRoleCrawlJob(jobId: string, config: any) {
|
|
|
3140
3182
|
) as { crawlConfig: any } | null;
|
|
3141
3183
|
const savedCrawlConfig = proj?.crawlConfig ?? {};
|
|
3142
3184
|
|
|
3185
|
+
// Resolve workspace proxy for this role crawl
|
|
3186
|
+
const roleCrawlWsProxy = await AppBrain.getTenantProxyConfig(tenantId).catch(() => null);
|
|
3187
|
+
const roleCrawlResolvedProxyUrl: string | undefined = (() => {
|
|
3188
|
+
const projectProxy = config.proxyUrl ?? (savedCrawlConfig as any)?.proxyUrl;
|
|
3189
|
+
if (projectProxy === 'agent' || roleCrawlWsProxy?.mode === 'agent') return 'agent';
|
|
3190
|
+
if (projectProxy === 'auto' || roleCrawlWsProxy?.mode === 'auto') return 'auto';
|
|
3191
|
+
if (projectProxy && projectProxy !== 'auto' && projectProxy !== 'agent') return projectProxy;
|
|
3192
|
+
if (roleCrawlWsProxy?.mode === 'custom' && roleCrawlWsProxy.proxyUrl) return roleCrawlWsProxy.proxyUrl;
|
|
3193
|
+
return undefined;
|
|
3194
|
+
})();
|
|
3195
|
+
|
|
3143
3196
|
// Build runtime credentials with TOTP support
|
|
3144
3197
|
const runtimeCreds = config.credentials
|
|
3145
3198
|
? {
|
|
@@ -3156,7 +3209,7 @@ async function processRoleCrawlJob(jobId: string, config: any) {
|
|
|
3156
3209
|
...config,
|
|
3157
3210
|
credentials: runtimeCreds,
|
|
3158
3211
|
storageState: config.storageState ?? savedCrawlConfig.storageState,
|
|
3159
|
-
proxyUrl:
|
|
3212
|
+
proxyUrl: roleCrawlResolvedProxyUrl,
|
|
3160
3213
|
loginInstructions: config.loginInstructions ?? savedCrawlConfig.loginInstructions,
|
|
3161
3214
|
abortSignal: controller.signal,
|
|
3162
3215
|
jobId,
|
|
@@ -3698,19 +3751,27 @@ async function processRepoScanJob(jobId: string, config: any) {
|
|
|
3698
3751
|
/[A-Z]\w*Tests?\.java$/.test(p) ||
|
|
3699
3752
|
(/\/tests?\//.test(p) && /[A-Z][a-z]/.test(p.split('/').pop() ?? ''))
|
|
3700
3753
|
);
|
|
3701
|
-
// Build list of globs to try — primary + common variants so we don't miss
|
|
3754
|
+
// Build list of globs to try — primary + common variants so we don't miss alternative test file
|
|
3755
|
+
// naming conventions (.test.ts alongside .spec.ts, .e2e.ts, .integration.ts, .smoke.ts, .steps.ts,
|
|
3756
|
+
// .cy.ts for Cypress, .tsx variants, .feature for Gherkin).
|
|
3702
3757
|
const testGlobs = minimatch
|
|
3703
3758
|
? Array.from(new Set([
|
|
3704
3759
|
testGlob,
|
|
3705
|
-
|
|
3706
|
-
...(testGlob
|
|
3760
|
+
// TS variants: add all common suffixes whenever the primary glob is any *.spec.ts or *.test.ts
|
|
3761
|
+
...(testGlob.endsWith('.spec.ts') ? ['**/*.test.ts', '**/*.test.tsx', '**/*.spec.tsx', '**/*.cy.ts', '**/*.e2e.ts', '**/*.integration.ts', '**/*.smoke.ts', '**/*.steps.ts'] : []),
|
|
3762
|
+
...(testGlob.endsWith('.test.ts') ? ['**/*.spec.ts', '**/*.test.tsx', '**/*.spec.tsx', '**/*.cy.ts', '**/*.e2e.ts', '**/*.integration.ts', '**/*.smoke.ts', '**/*.steps.ts'] : []),
|
|
3763
|
+
// JS variants
|
|
3764
|
+
...(testGlob.endsWith('.spec.js') ? ['**/*.test.js', '**/*.cy.js', '**/*.e2e.js', '**/*.integration.js', '**/*.smoke.js', '**/*.steps.js'] : []),
|
|
3765
|
+
...(testGlob.endsWith('.test.js') ? ['**/*.spec.js', '**/*.cy.js', '**/*.e2e.js', '**/*.integration.js', '**/*.smoke.js', '**/*.steps.js'] : []),
|
|
3766
|
+
// Gherkin feature files are always relevant regardless of primary glob
|
|
3767
|
+
...(framework === 'cucumber' || framework === 'gherkin' ? ['**/*.feature'] : []),
|
|
3707
3768
|
]))
|
|
3708
3769
|
: [];
|
|
3709
3770
|
const testPaths = treeResult.paths.filter((p) => {
|
|
3710
3771
|
if (language === 'java') return isJavaTestFile(p);
|
|
3711
3772
|
if (language === 'python') return isPythonTestFile(p);
|
|
3712
3773
|
if (testGlobs.length) return testGlobs.some(g => (minimatch as any)(p, g, { matchBase: true }));
|
|
3713
|
-
return p.endsWith('.spec.ts') || p.endsWith('.spec.js') || p.endsWith('.test.ts') || p.endsWith('.test.js') || p.endsWith('.cy.ts') || p.endsWith('.cy.js') || p.endsWith('_test.py') || p.includes('/test_');
|
|
3774
|
+
return p.endsWith('.spec.ts') || p.endsWith('.spec.js') || p.endsWith('.test.ts') || p.endsWith('.test.js') || p.endsWith('.cy.ts') || p.endsWith('.cy.js') || p.endsWith('.e2e.ts') || p.endsWith('.e2e.js') || p.endsWith('.integration.ts') || p.endsWith('.integration.js') || p.endsWith('.smoke.ts') || p.endsWith('.steps.ts') || p.endsWith('.feature') || p.endsWith('_test.py') || p.includes('/test_');
|
|
3714
3775
|
}).slice(0, 2000); // cap at 2000 test files
|
|
3715
3776
|
|
|
3716
3777
|
await updateJobProgress(tenantId, jobId, 40, `Found ${testPaths.length} test files`);
|
|
@@ -3936,21 +3997,32 @@ async function processRepoScanJob(jobId: string, config: any) {
|
|
|
3936
3997
|
const mainSourcePaths = language === 'java'
|
|
3937
3998
|
? treeResult.paths.filter((p: string) => /^src\/main\/java\//.test(p)).slice(0, 50)
|
|
3938
3999
|
: [];
|
|
3939
|
-
// For TS/JS, also include non-test source files so LLM can detect pageObjectLocation
|
|
3940
|
-
//
|
|
4000
|
+
// For TS/JS, also include non-test source files so LLM can detect pageObjectLocation,
|
|
4001
|
+
// utilsDir, fixtureDir, configDir, etc. Priority given to framework support file types
|
|
4002
|
+
// (.page.ts, .fixture.ts, .helper.ts, .util.ts, .data.ts) so they are not crowded out.
|
|
3941
4003
|
const testPathSet = new Set(testPaths);
|
|
4004
|
+
const isFrameworkSupportFile = (p: string) =>
|
|
4005
|
+
/\.(page|fixture|helper|util|utils|data|actions|assertions|locator|locators|steps)\.(ts|js|tsx|jsx)$/.test(p);
|
|
4006
|
+
const isGenericSourceFile = (p: string) =>
|
|
4007
|
+
(p.endsWith('.ts') || p.endsWith('.js') || p.endsWith('.tsx') || p.endsWith('.jsx')) &&
|
|
4008
|
+
!p.endsWith('.d.ts') &&
|
|
4009
|
+
!p.includes('node_modules/') &&
|
|
4010
|
+
!p.includes('dist/') &&
|
|
4011
|
+
!p.includes('.next/');
|
|
4012
|
+
const candidateSourcePaths = treeResult.paths.filter((p: string) =>
|
|
4013
|
+
isGenericSourceFile(p) && !testPathSet.has(p)
|
|
4014
|
+
);
|
|
4015
|
+
// Framework support files first (page objects, fixtures, helpers, etc.), then generic source
|
|
4016
|
+
const supportFilePaths = candidateSourcePaths.filter(isFrameworkSupportFile).slice(0, 80);
|
|
4017
|
+
const supportPathSet = new Set(supportFilePaths);
|
|
4018
|
+
const otherSourcePaths = candidateSourcePaths
|
|
4019
|
+
.filter((p: string) => !supportPathSet.has(p))
|
|
4020
|
+
.slice(0, 60);
|
|
3942
4021
|
const sourceFilePaths = (language === 'typescript' || language === 'javascript')
|
|
3943
|
-
?
|
|
3944
|
-
.filter((p: string) =>
|
|
3945
|
-
(p.endsWith('.ts') || p.endsWith('.js')) &&
|
|
3946
|
-
!testPathSet.has(p) &&
|
|
3947
|
-
!p.endsWith('.d.ts') &&
|
|
3948
|
-
!p.includes('node_modules/') &&
|
|
3949
|
-
!p.includes('dist/') &&
|
|
3950
|
-
!p.includes('.config.'))
|
|
3951
|
-
.slice(0, 60)
|
|
4022
|
+
? [...supportFilePaths, ...otherSourcePaths]
|
|
3952
4023
|
: [];
|
|
3953
|
-
|
|
4024
|
+
// Allow up to 400 paths total so test files + rich source file sets both fit
|
|
4025
|
+
const allPathsForProfile = [...testPaths, ...mainSourcePaths, ...sourceFilePaths].slice(0, 400);
|
|
3954
4026
|
// Pass full tree paths separately so detectDataDirs can find testdata/*.json, fixtures/*.yaml etc.
|
|
3955
4027
|
const freshProfile = await extractStyleProfile(styleSampleFiles, framework, language, tenantId, projectId, jobId, allPathsForProfile, treeResult.paths);
|
|
3956
4028
|
if (freshProfile) styleProfile = freshProfile;
|
package/src/sitemap.ts
CHANGED
|
@@ -153,7 +153,7 @@ export async function discoverSitemapUrls(appUrl: string, sitemapUrl?: string):
|
|
|
153
153
|
* Returns undefined for no/auto proxy — the caller then fetches directly.
|
|
154
154
|
*/
|
|
155
155
|
export function buildProxyDispatcher(proxyUrl?: string): ProxyAgent | undefined {
|
|
156
|
-
if (!proxyUrl || proxyUrl === 'auto') return undefined;
|
|
156
|
+
if (!proxyUrl || proxyUrl === 'auto' || proxyUrl === 'agent') return undefined;
|
|
157
157
|
try {
|
|
158
158
|
const m = proxyUrl.match(/^(https?):\/\/(?:([^:@]*)(?::([^@]*))?@)?(.+)$/);
|
|
159
159
|
if (!m) return new ProxyAgent(proxyUrl);
|
package/types/job-queue.d.ts
CHANGED
|
@@ -237,7 +237,11 @@ export declare function getJob(tenantId: string, jobId: string): Promise<{
|
|
|
237
237
|
priority: number;
|
|
238
238
|
} | null>;
|
|
239
239
|
/** List recent jobs for a project. */
|
|
240
|
-
export declare function
|
|
240
|
+
export declare function countJobs(tenantId: string, projectId: string): Promise<number>;
|
|
241
|
+
export declare function listJobs(tenantId: string, projectId: string, { skip, take }?: {
|
|
242
|
+
skip?: number;
|
|
243
|
+
take?: number;
|
|
244
|
+
}): Promise<{
|
|
241
245
|
progress: number;
|
|
242
246
|
id: string;
|
|
243
247
|
name: string | null;
|