@detiq/crawler 1.1.5 → 1.1.7
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 +160 -16
- package/dist/crawler.js.map +2 -2
- package/dist/failure-analysis-agent.js +3 -2
- package/dist/failure-analysis-agent.js.map +2 -2
- package/dist/gap-pass.js +1 -0
- package/dist/gap-pass.js.map +2 -2
- package/dist/job-queue.js +8 -4
- package/dist/job-queue.js.map +2 -2
- package/dist/sitemap.js +5 -3
- package/dist/sitemap.js.map +2 -2
- package/dist/url-pattern-completer.js +1 -0
- package/dist/url-pattern-completer.js.map +2 -2
- package/package.json +1 -1
- package/src/crawler.ts +87 -10
- package/src/failure-analysis-agent.ts +3 -2
- package/src/gap-pass.ts +1 -0
- package/src/job-queue.ts +3 -2
- package/src/url-pattern-completer.ts +1 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
-
import { askTrackedWithImages, askTracked } from "@detiq/agents";
|
|
2
|
+
import { askTrackedWithImages, askTracked, deterministicSeed } from "@detiq/agents";
|
|
3
3
|
import { AppBrain } from "@detiq/app-brain";
|
|
4
4
|
async function analyzeFailure(ctx) {
|
|
5
5
|
try {
|
|
@@ -28,7 +28,8 @@ Error: ${ctx.error}${consoleSection}${networkSection}`;
|
|
|
28
28
|
} catch {
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
-
const
|
|
31
|
+
const failureSeed = deterministicSeed(`${ctx.tenantId ?? ""}:failure:${(ctx.stepDescription ?? "").slice(0, 100)}:${ctx.error.slice(0, 200)}`);
|
|
32
|
+
const result = await askTracked("", userText, llmCfg, 256, void 0, { temperature: 0, seed: failureSeed });
|
|
32
33
|
return result.text || null;
|
|
33
34
|
} catch {
|
|
34
35
|
return null;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/failure-analysis-agent.ts"],
|
|
4
|
-
"sourcesContent": ["import { readFileSync } from 'node:fs';\nimport { askTrackedWithImages, askTracked } from '@detiq/agents';\nimport type { LLMConfig } from '@detiq/agents';\nimport { AppBrain } from '@detiq/app-brain';\n\nexport interface FailureContext {\n tenantId?: string;\n error: string;\n stepDescription?: string;\n consoleLogs?: Array<{ type: string; text: string }>;\n networkErrors?: Array<{ url: string; status: number; method: string }>;\n screenshotPath?: string;\n}\n\nexport async function analyzeFailure(ctx: FailureContext): Promise<string | null> {\n try {\n let llmCfg: LLMConfig | undefined;\n if (ctx.tenantId) {\n llmCfg = await AppBrain.getTaskLLMConfig(ctx.tenantId, 'testRunner').catch(() => undefined) as LLMConfig | undefined ?? undefined;\n }\n if (!llmCfg) return null;\n\n const consoleSection = ctx.consoleLogs?.length\n ? `\\n\\nBrowser console (last ${ctx.consoleLogs.length} entries):\\n${ctx.consoleLogs.map(l => `[${l.type}] ${l.text}`).join('\\n')}`\n : '';\n\n const networkSection = ctx.networkErrors?.length\n ? `\\n\\nFailed network requests:\\n${ctx.networkErrors.map(n => `${n.method} ${n.url} \u2192 ${n.status}`).join('\\n')}`\n : '';\n\n const userText = `A Playwright automated test failed. Identify the root cause in 2-3 sentences. Be specific \u2014 name the failing element, API endpoint, or error type. Do not suggest fixes.\n\nTest step: ${ctx.stepDescription ?? 'unknown'}\nError: ${ctx.error}${consoleSection}${networkSection}`;\n\n if (ctx.screenshotPath) {\n try {\n const img = readFileSync(ctx.screenshotPath);\n const result = await askTrackedWithImages('', userText, [{ base64: img.toString('base64'), mediaType: 'image/png' }], llmCfg);\n return result.text || null;\n } catch { /* fall through to text-only */ }\n }\n\n const result = await askTracked('', userText, llmCfg, 256);\n return result.text || null;\n } catch {\n return null;\n }\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,sBAAsB,
|
|
4
|
+
"sourcesContent": ["import { readFileSync } from 'node:fs';\nimport { askTrackedWithImages, askTracked, deterministicSeed } from '@detiq/agents';\nimport type { LLMConfig } from '@detiq/agents';\nimport { AppBrain } from '@detiq/app-brain';\n\nexport interface FailureContext {\n tenantId?: string;\n error: string;\n stepDescription?: string;\n consoleLogs?: Array<{ type: string; text: string }>;\n networkErrors?: Array<{ url: string; status: number; method: string }>;\n screenshotPath?: string;\n}\n\nexport async function analyzeFailure(ctx: FailureContext): Promise<string | null> {\n try {\n let llmCfg: LLMConfig | undefined;\n if (ctx.tenantId) {\n llmCfg = await AppBrain.getTaskLLMConfig(ctx.tenantId, 'testRunner').catch(() => undefined) as LLMConfig | undefined ?? undefined;\n }\n if (!llmCfg) return null;\n\n const consoleSection = ctx.consoleLogs?.length\n ? `\\n\\nBrowser console (last ${ctx.consoleLogs.length} entries):\\n${ctx.consoleLogs.map(l => `[${l.type}] ${l.text}`).join('\\n')}`\n : '';\n\n const networkSection = ctx.networkErrors?.length\n ? `\\n\\nFailed network requests:\\n${ctx.networkErrors.map(n => `${n.method} ${n.url} \u2192 ${n.status}`).join('\\n')}`\n : '';\n\n const userText = `A Playwright automated test failed. Identify the root cause in 2-3 sentences. Be specific \u2014 name the failing element, API endpoint, or error type. Do not suggest fixes.\n\nTest step: ${ctx.stepDescription ?? 'unknown'}\nError: ${ctx.error}${consoleSection}${networkSection}`;\n\n if (ctx.screenshotPath) {\n try {\n const img = readFileSync(ctx.screenshotPath);\n const result = await askTrackedWithImages('', userText, [{ base64: img.toString('base64'), mediaType: 'image/png' }], llmCfg);\n return result.text || null;\n } catch { /* fall through to text-only */ }\n }\n\n const failureSeed = deterministicSeed(`${ctx.tenantId ?? ''}:failure:${(ctx.stepDescription ?? '').slice(0, 100)}:${ctx.error.slice(0, 200)}`);\n const result = await askTracked('', userText, llmCfg, 256, undefined, { temperature: 0, seed: failureSeed });\n return result.text || null;\n } catch {\n return null;\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,sBAAsB,YAAY,yBAAyB;AAEpE,SAAS,gBAAgB;AAWzB,eAAsB,eAAe,KAA6C;AAChF,MAAI;AACF,QAAI;AACJ,QAAI,IAAI,UAAU;AAChB,eAAS,MAAM,SAAS,iBAAiB,IAAI,UAAU,YAAY,EAAE,MAAM,MAAM,MAAS,KAA8B;AAAA,IAC1H;AACA,QAAI,CAAC,OAAQ,QAAO;AAEpB,UAAM,iBAAiB,IAAI,aAAa,SACpC;AAAA;AAAA,wBAA6B,IAAI,YAAY,MAAM;AAAA,EAAe,IAAI,YAAY,IAAI,OAAK,IAAI,EAAE,IAAI,KAAK,EAAE,IAAI,EAAE,EAAE,KAAK,IAAI,CAAC,KAC9H;AAEJ,UAAM,iBAAiB,IAAI,eAAe,SACtC;AAAA;AAAA;AAAA,EAAiC,IAAI,cAAc,IAAI,OAAK,GAAG,EAAE,MAAM,IAAI,EAAE,GAAG,WAAM,EAAE,MAAM,EAAE,EAAE,KAAK,IAAI,CAAC,KAC5G;AAEJ,UAAM,WAAW;AAAA;AAAA,aAER,IAAI,mBAAmB,SAAS;AAAA,SACpC,IAAI,KAAK,GAAG,cAAc,GAAG,cAAc;AAEhD,QAAI,IAAI,gBAAgB;AACtB,UAAI;AACF,cAAM,MAAM,aAAa,IAAI,cAAc;AAC3C,cAAMA,UAAS,MAAM,qBAAqB,IAAI,UAAU,CAAC,EAAE,QAAQ,IAAI,SAAS,QAAQ,GAAG,WAAW,YAAY,CAAC,GAAG,MAAM;AAC5H,eAAOA,QAAO,QAAQ;AAAA,MACxB,QAAQ;AAAA,MAAkC;AAAA,IAC5C;AAEA,UAAM,cAAc,kBAAkB,GAAG,IAAI,YAAY,EAAE,aAAa,IAAI,mBAAmB,IAAI,MAAM,GAAG,GAAG,CAAC,IAAI,IAAI,MAAM,MAAM,GAAG,GAAG,CAAC,EAAE;AAC7I,UAAM,SAAS,MAAM,WAAW,IAAI,UAAU,QAAQ,KAAK,QAAW,EAAE,aAAa,GAAG,MAAM,YAAY,CAAC;AAC3G,WAAO,OAAO,QAAQ;AAAA,EACxB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;",
|
|
6
6
|
"names": ["result"]
|
|
7
7
|
}
|
package/dist/gap-pass.js
CHANGED
|
@@ -34,6 +34,7 @@ Respond with ONLY valid JSON, no markdown:
|
|
|
34
34
|
const response = await client.messages.create({
|
|
35
35
|
model: llmCfg.model ?? "claude-haiku-4-5-20251001",
|
|
36
36
|
max_tokens: 1024,
|
|
37
|
+
temperature: 0,
|
|
37
38
|
// @ts-ignore
|
|
38
39
|
system: [{ type: "text", text: "You are a web application coverage analyst. Respond with valid JSON only.", cache_control: { type: "ephemeral" } }],
|
|
39
40
|
messages: [{ role: "user", content: prompt }]
|
package/dist/gap-pass.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/gap-pass.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * P6: Gap Pass \u2014 LLM-guided second crawl pass\n *\n * After the primary BFS crawl completes, this module:\n * 1. Sends the discovered screen list to an LLM for gap analysis\n * 2. HEAD-validates each suggested URL\n * 3. Returns confirmed gap URLs for targeted re-crawl\n *\n * The caller (crawler.ts) is responsible for actually visiting the confirmed\n * URLs with the existing browser \u2014 this module only identifies them.\n */\n\nimport { logger as rootLogger } from './logger.js';\n\nconst logger = rootLogger.child({ module: 'gap-pass' });\n\nexport interface GapCandidate {\n category: string;\n urlPatterns: string[];\n confidence: number;\n rationale: string;\n confirmedUrl: string | null;\n headStatus: number | null;\n}\n\ninterface DiscoveredScreen {\n url: string;\n name?: string;\n elements?: number;\n}\n\ninterface LlmCfg {\n provider: string;\n model: string;\n apiKey?: string;\n}\n\n/**\n * Run LLM gap analysis + HEAD validation.\n * Returns candidates with `confirmedUrl` set for those where HEAD returned 200.\n */\nexport async function runGapAnalysis(\n screens: DiscoveredScreen[],\n appUrl: string,\n llmCfg: LlmCfg | null | undefined,\n): Promise<GapCandidate[]> {\n if (!llmCfg?.apiKey || screens.length < 2) return [];\n\n const origin = (() => { try { return new URL(appUrl).origin; } catch { return ''; } })();\n if (!origin) return [];\n\n // Cap context \u2014 only send url + name, not full element data\n const screenList = screens\n .slice(0, 80)\n .map(s => {\n try { return new URL(s.url).pathname + (s.name ? ` (${s.name})` : ''); } catch { return s.url; }\n })\n .join('\\n');\n\n const prompt = `You are analyzing the screen map of a web application to find structural coverage gaps.\n\nDiscovered screens (${screens.length} total):\n${screenList}\n\nApp URL: ${appUrl}\n\nBased on these screens and common patterns for this type of web application, identify screen categories that are likely present but missing. Focus on pages that would exist in a typical SaaS or web app but are not represented above.\n\nRespond with ONLY valid JSON, no markdown:\n{\"gaps\":[{\"category\":\"User profile edit\",\"urlPatterns\":[\"/profile/edit\",\"/account/profile\"],\"confidence\":0.8,\"rationale\":\"Profile view found but no edit page\"},{\"category\":\"Billing history\",\"urlPatterns\":[\"/settings/billing\",\"/account/billing\"],\"confidence\":0.7,\"rationale\":\"Settings section found but no billing page\"}]}`;\n\n try {\n const Anthropic = (await import('@anthropic-ai/sdk')).default;\n const client = new Anthropic({ apiKey: llmCfg.apiKey });\n const response = await client.messages.create({\n model: llmCfg.model ?? 'claude-haiku-4-5-20251001',\n max_tokens: 1024,\n // @ts-ignore\n system: [{ type: 'text', text: 'You are a web application coverage analyst. Respond with valid JSON only.', cache_control: { type: 'ephemeral' } }],\n messages: [{ role: 'user', content: prompt }],\n });\n\n const text = (response.content[0] as any)?.text ?? '';\n const jsonMatch = text.match(/\\{[\\s\\S]*\\}/);\n if (!jsonMatch) return [];\n const parsed = JSON.parse(jsonMatch[0]);\n const rawGaps: Array<{ category: string; urlPatterns: string[]; confidence: number; rationale: string }> =\n parsed.gaps ?? [];\n\n if (!Array.isArray(rawGaps) || rawGaps.length === 0) return [];\n logger.info({ gaps: rawGaps.length }, '[P6] Gap analysis: LLM returned candidates');\n\n // HEAD-validate each URL pattern, take the first 200\n const results: GapCandidate[] = [];\n for (const gap of rawGaps.slice(0, 15)) {\n let confirmedUrl: string | null = null;\n let headStatus: number | null = null;\n\n for (const pattern of (gap.urlPatterns ?? []).slice(0, 4)) {\n const fullUrl = pattern.startsWith('http') ? pattern : `${origin}${pattern.startsWith('/') ? '' : '/'}${pattern}`;\n try {\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), 6_000);\n const res = await fetch(fullUrl, { method: 'HEAD', signal: controller.signal, redirect: 'follow' }).catch(() => null);\n clearTimeout(timeout);\n headStatus = res?.status ?? null;\n if (res?.ok) { confirmedUrl = fullUrl; break; }\n } catch { /* non-fatal */ }\n }\n\n results.push({ category: gap.category, urlPatterns: gap.urlPatterns, confidence: gap.confidence, rationale: gap.rationale, confirmedUrl, headStatus });\n logger.info({ category: gap.category, confirmedUrl, headStatus }, '[P6] Gap candidate checked');\n }\n\n const confirmed = results.filter(r => r.confirmedUrl).length;\n logger.info({ total: results.length, confirmed }, '[P6] Gap pass analysis complete');\n return results;\n } catch (err) {\n logger.warn({ err: String(err) }, '[P6] Gap analysis failed \u2014 non-fatal');\n return [];\n }\n}\n"],
|
|
5
|
-
"mappings": "AAYA,SAAS,UAAU,kBAAkB;AAErC,MAAM,SAAS,WAAW,MAAM,EAAE,QAAQ,WAAW,CAAC;AA2BtD,eAAsB,eACpB,SACA,QACA,QACyB;AACzB,MAAI,CAAC,QAAQ,UAAU,QAAQ,SAAS,EAAG,QAAO,CAAC;AAEnD,QAAM,UAAU,MAAM;AAAE,QAAI;AAAE,aAAO,IAAI,IAAI,MAAM,EAAE;AAAA,IAAQ,QAAQ;AAAE,aAAO;AAAA,IAAI;AAAA,EAAE,GAAG;AACvF,MAAI,CAAC,OAAQ,QAAO,CAAC;AAGrB,QAAM,aAAa,QAChB,MAAM,GAAG,EAAE,EACX,IAAI,OAAK;AACR,QAAI;AAAE,aAAO,IAAI,IAAI,EAAE,GAAG,EAAE,YAAY,EAAE,OAAO,KAAK,EAAE,IAAI,MAAM;AAAA,IAAK,QAAQ;AAAE,aAAO,EAAE;AAAA,IAAK;AAAA,EACjG,CAAC,EACA,KAAK,IAAI;AAEZ,QAAM,SAAS;AAAA;AAAA,sBAEK,QAAQ,MAAM;AAAA,EAClC,UAAU;AAAA;AAAA,WAED,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAOf,MAAI;AACF,UAAM,aAAa,MAAM,OAAO,mBAAmB,GAAG;AACtD,UAAM,SAAS,IAAI,UAAU,EAAE,QAAQ,OAAO,OAAO,CAAC;AACtD,UAAM,WAAW,MAAM,OAAO,SAAS,OAAO;AAAA,MAC5C,OAAO,OAAO,SAAS;AAAA,MACvB,YAAY;AAAA;AAAA,
|
|
4
|
+
"sourcesContent": ["/**\n * P6: Gap Pass \u2014 LLM-guided second crawl pass\n *\n * After the primary BFS crawl completes, this module:\n * 1. Sends the discovered screen list to an LLM for gap analysis\n * 2. HEAD-validates each suggested URL\n * 3. Returns confirmed gap URLs for targeted re-crawl\n *\n * The caller (crawler.ts) is responsible for actually visiting the confirmed\n * URLs with the existing browser \u2014 this module only identifies them.\n */\n\nimport { logger as rootLogger } from './logger.js';\n\nconst logger = rootLogger.child({ module: 'gap-pass' });\n\nexport interface GapCandidate {\n category: string;\n urlPatterns: string[];\n confidence: number;\n rationale: string;\n confirmedUrl: string | null;\n headStatus: number | null;\n}\n\ninterface DiscoveredScreen {\n url: string;\n name?: string;\n elements?: number;\n}\n\ninterface LlmCfg {\n provider: string;\n model: string;\n apiKey?: string;\n}\n\n/**\n * Run LLM gap analysis + HEAD validation.\n * Returns candidates with `confirmedUrl` set for those where HEAD returned 200.\n */\nexport async function runGapAnalysis(\n screens: DiscoveredScreen[],\n appUrl: string,\n llmCfg: LlmCfg | null | undefined,\n): Promise<GapCandidate[]> {\n if (!llmCfg?.apiKey || screens.length < 2) return [];\n\n const origin = (() => { try { return new URL(appUrl).origin; } catch { return ''; } })();\n if (!origin) return [];\n\n // Cap context \u2014 only send url + name, not full element data\n const screenList = screens\n .slice(0, 80)\n .map(s => {\n try { return new URL(s.url).pathname + (s.name ? ` (${s.name})` : ''); } catch { return s.url; }\n })\n .join('\\n');\n\n const prompt = `You are analyzing the screen map of a web application to find structural coverage gaps.\n\nDiscovered screens (${screens.length} total):\n${screenList}\n\nApp URL: ${appUrl}\n\nBased on these screens and common patterns for this type of web application, identify screen categories that are likely present but missing. Focus on pages that would exist in a typical SaaS or web app but are not represented above.\n\nRespond with ONLY valid JSON, no markdown:\n{\"gaps\":[{\"category\":\"User profile edit\",\"urlPatterns\":[\"/profile/edit\",\"/account/profile\"],\"confidence\":0.8,\"rationale\":\"Profile view found but no edit page\"},{\"category\":\"Billing history\",\"urlPatterns\":[\"/settings/billing\",\"/account/billing\"],\"confidence\":0.7,\"rationale\":\"Settings section found but no billing page\"}]}`;\n\n try {\n const Anthropic = (await import('@anthropic-ai/sdk')).default;\n const client = new Anthropic({ apiKey: llmCfg.apiKey });\n const response = await client.messages.create({\n model: llmCfg.model ?? 'claude-haiku-4-5-20251001',\n max_tokens: 1024,\n temperature: 0,\n // @ts-ignore\n system: [{ type: 'text', text: 'You are a web application coverage analyst. Respond with valid JSON only.', cache_control: { type: 'ephemeral' } }],\n messages: [{ role: 'user', content: prompt }],\n });\n\n const text = (response.content[0] as any)?.text ?? '';\n const jsonMatch = text.match(/\\{[\\s\\S]*\\}/);\n if (!jsonMatch) return [];\n const parsed = JSON.parse(jsonMatch[0]);\n const rawGaps: Array<{ category: string; urlPatterns: string[]; confidence: number; rationale: string }> =\n parsed.gaps ?? [];\n\n if (!Array.isArray(rawGaps) || rawGaps.length === 0) return [];\n logger.info({ gaps: rawGaps.length }, '[P6] Gap analysis: LLM returned candidates');\n\n // HEAD-validate each URL pattern, take the first 200\n const results: GapCandidate[] = [];\n for (const gap of rawGaps.slice(0, 15)) {\n let confirmedUrl: string | null = null;\n let headStatus: number | null = null;\n\n for (const pattern of (gap.urlPatterns ?? []).slice(0, 4)) {\n const fullUrl = pattern.startsWith('http') ? pattern : `${origin}${pattern.startsWith('/') ? '' : '/'}${pattern}`;\n try {\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), 6_000);\n const res = await fetch(fullUrl, { method: 'HEAD', signal: controller.signal, redirect: 'follow' }).catch(() => null);\n clearTimeout(timeout);\n headStatus = res?.status ?? null;\n if (res?.ok) { confirmedUrl = fullUrl; break; }\n } catch { /* non-fatal */ }\n }\n\n results.push({ category: gap.category, urlPatterns: gap.urlPatterns, confidence: gap.confidence, rationale: gap.rationale, confirmedUrl, headStatus });\n logger.info({ category: gap.category, confirmedUrl, headStatus }, '[P6] Gap candidate checked');\n }\n\n const confirmed = results.filter(r => r.confirmedUrl).length;\n logger.info({ total: results.length, confirmed }, '[P6] Gap pass analysis complete');\n return results;\n } catch (err) {\n logger.warn({ err: String(err) }, '[P6] Gap analysis failed \u2014 non-fatal');\n return [];\n }\n}\n"],
|
|
5
|
+
"mappings": "AAYA,SAAS,UAAU,kBAAkB;AAErC,MAAM,SAAS,WAAW,MAAM,EAAE,QAAQ,WAAW,CAAC;AA2BtD,eAAsB,eACpB,SACA,QACA,QACyB;AACzB,MAAI,CAAC,QAAQ,UAAU,QAAQ,SAAS,EAAG,QAAO,CAAC;AAEnD,QAAM,UAAU,MAAM;AAAE,QAAI;AAAE,aAAO,IAAI,IAAI,MAAM,EAAE;AAAA,IAAQ,QAAQ;AAAE,aAAO;AAAA,IAAI;AAAA,EAAE,GAAG;AACvF,MAAI,CAAC,OAAQ,QAAO,CAAC;AAGrB,QAAM,aAAa,QAChB,MAAM,GAAG,EAAE,EACX,IAAI,OAAK;AACR,QAAI;AAAE,aAAO,IAAI,IAAI,EAAE,GAAG,EAAE,YAAY,EAAE,OAAO,KAAK,EAAE,IAAI,MAAM;AAAA,IAAK,QAAQ;AAAE,aAAO,EAAE;AAAA,IAAK;AAAA,EACjG,CAAC,EACA,KAAK,IAAI;AAEZ,QAAM,SAAS;AAAA;AAAA,sBAEK,QAAQ,MAAM;AAAA,EAClC,UAAU;AAAA;AAAA,WAED,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAOf,MAAI;AACF,UAAM,aAAa,MAAM,OAAO,mBAAmB,GAAG;AACtD,UAAM,SAAS,IAAI,UAAU,EAAE,QAAQ,OAAO,OAAO,CAAC;AACtD,UAAM,WAAW,MAAM,OAAO,SAAS,OAAO;AAAA,MAC5C,OAAO,OAAO,SAAS;AAAA,MACvB,YAAY;AAAA,MACZ,aAAa;AAAA;AAAA,MAEb,QAAQ,CAAC,EAAE,MAAM,QAAQ,MAAM,6EAA6E,eAAe,EAAE,MAAM,YAAY,EAAE,CAAC;AAAA,MAClJ,UAAU,CAAC,EAAE,MAAM,QAAQ,SAAS,OAAO,CAAC;AAAA,IAC9C,CAAC;AAED,UAAM,OAAQ,SAAS,QAAQ,CAAC,GAAW,QAAQ;AACnD,UAAM,YAAY,KAAK,MAAM,aAAa;AAC1C,QAAI,CAAC,UAAW,QAAO,CAAC;AACxB,UAAM,SAAS,KAAK,MAAM,UAAU,CAAC,CAAC;AACtC,UAAM,UACJ,OAAO,QAAQ,CAAC;AAElB,QAAI,CAAC,MAAM,QAAQ,OAAO,KAAK,QAAQ,WAAW,EAAG,QAAO,CAAC;AAC7D,WAAO,KAAK,EAAE,MAAM,QAAQ,OAAO,GAAG,4CAA4C;AAGlF,UAAM,UAA0B,CAAC;AACjC,eAAW,OAAO,QAAQ,MAAM,GAAG,EAAE,GAAG;AACtC,UAAI,eAA8B;AAClC,UAAI,aAA4B;AAEhC,iBAAW,YAAY,IAAI,eAAe,CAAC,GAAG,MAAM,GAAG,CAAC,GAAG;AACzD,cAAM,UAAU,QAAQ,WAAW,MAAM,IAAI,UAAU,GAAG,MAAM,GAAG,QAAQ,WAAW,GAAG,IAAI,KAAK,GAAG,GAAG,OAAO;AAC/G,YAAI;AACF,gBAAM,aAAa,IAAI,gBAAgB;AACvC,gBAAM,UAAU,WAAW,MAAM,WAAW,MAAM,GAAG,GAAK;AAC1D,gBAAM,MAAM,MAAM,MAAM,SAAS,EAAE,QAAQ,QAAQ,QAAQ,WAAW,QAAQ,UAAU,SAAS,CAAC,EAAE,MAAM,MAAM,IAAI;AACpH,uBAAa,OAAO;AACpB,uBAAa,KAAK,UAAU;AAC5B,cAAI,KAAK,IAAI;AAAE,2BAAe;AAAS;AAAA,UAAO;AAAA,QAChD,QAAQ;AAAA,QAAkB;AAAA,MAC5B;AAEA,cAAQ,KAAK,EAAE,UAAU,IAAI,UAAU,aAAa,IAAI,aAAa,YAAY,IAAI,YAAY,WAAW,IAAI,WAAW,cAAc,WAAW,CAAC;AACrJ,aAAO,KAAK,EAAE,UAAU,IAAI,UAAU,cAAc,WAAW,GAAG,4BAA4B;AAAA,IAChG;AAEA,UAAM,YAAY,QAAQ,OAAO,OAAK,EAAE,YAAY,EAAE;AACtD,WAAO,KAAK,EAAE,OAAO,QAAQ,QAAQ,UAAU,GAAG,iCAAiC;AACnF,WAAO;AAAA,EACT,SAAS,KAAK;AACZ,WAAO,KAAK,EAAE,KAAK,OAAO,GAAG,EAAE,GAAG,2CAAsC;AACxE,WAAO,CAAC;AAAA,EACV;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/job-queue.js
CHANGED
|
@@ -83,9 +83,9 @@ const runningControllers = /* @__PURE__ */ new Map();
|
|
|
83
83
|
const WORKER_ID = `${os.hostname()}:${process.pid}`;
|
|
84
84
|
const STALE_JOB_TIMEOUT_MINUTES = Number(process.env.JOB_STALE_TIMEOUT_MINUTES) || 30;
|
|
85
85
|
const WORKER_POLL_INTERVAL_MS = Number(process.env.JOB_WORKER_POLL_INTERVAL_MS) || 15e3;
|
|
86
|
-
const WORKER_CONCURRENCY = Number(process.env.JOB_WORKER_CONCURRENCY) ||
|
|
86
|
+
const WORKER_CONCURRENCY = Number(process.env.JOB_WORKER_CONCURRENCY) || 8;
|
|
87
87
|
const JOB_HEARTBEAT_INTERVAL_MS = Number(process.env.JOB_HEARTBEAT_INTERVAL_MS) || 15e3;
|
|
88
|
-
const TEST_EXEC_CONCURRENCY = Number(process.env.TEST_EXEC_CONCURRENCY) ||
|
|
88
|
+
const TEST_EXEC_CONCURRENCY = Number(process.env.TEST_EXEC_CONCURRENCY) || 5;
|
|
89
89
|
let jobDispatchTable = null;
|
|
90
90
|
async function claimSpecificJob(jobId) {
|
|
91
91
|
const rows = await prismaAdmin.$queryRaw`
|
|
@@ -1015,7 +1015,7 @@ async function processGenerateTestsFromReqsJob(jobId, config) {
|
|
|
1015
1015
|
const { tenantId, projectId, reqIds, suiteId, suiteName } = config;
|
|
1016
1016
|
try {
|
|
1017
1017
|
await updateJobProgress(tenantId, jobId, 5, "Loading requirements");
|
|
1018
|
-
const { askTracked, getTaskAgentLLMConfig } = await import("@detiq/agents");
|
|
1018
|
+
const { askTracked, deterministicSeed, getTaskAgentLLMConfig } = await import("@detiq/agents");
|
|
1019
1019
|
const llmCfg = await getTaskAgentLLMConfig(tenantId, "testGeneration");
|
|
1020
1020
|
const all = await AppBrain.listRequirements(tenantId, projectId, {});
|
|
1021
1021
|
const reqs = reqIds?.length ? all.filter((r) => reqIds.includes(r.reqId)) : all.filter((r) => r.status === "unbuilt" || r.status === "pending");
|
|
@@ -1057,7 +1057,8 @@ Each test case must reference its reqRef (the requirement ID like REQ-001).`;
|
|
|
1057
1057
|
${reqBlock}`;
|
|
1058
1058
|
let parsed = { cases: [] };
|
|
1059
1059
|
try {
|
|
1060
|
-
const
|
|
1060
|
+
const reqSeed = deterministicSeed(`${tenantId}:${projectId}:req-cases:${reqs.map((r) => r.reqId).sort().join("|").slice(0, 300)}`);
|
|
1061
|
+
const { text, usage } = await askTracked(system, user, llmCfg, void 0, void 0, { temperature: 0, seed: reqSeed });
|
|
1061
1062
|
if (usage) recordLLMCall({ tenantId, projectId, jobId, agentType: "generate_tests_from_reqs", label: "test_case_generation", usage }).catch(() => {
|
|
1062
1063
|
});
|
|
1063
1064
|
const json = text.match(/\{[\s\S]*\}/)?.[0];
|
|
@@ -1361,6 +1362,7 @@ async function processDiscoverJob(jobId, config, opts) {
|
|
|
1361
1362
|
const maxDiscoverUrls = config.maxDiscoverUrls ?? 2e3;
|
|
1362
1363
|
const proxyUrl = config.proxyUrl ?? crawlCfg.proxyUrl;
|
|
1363
1364
|
const screenshotDir = os.tmpdir();
|
|
1365
|
+
const discoverStorageConfig = await getTenantStorageConfig(tenantId).catch(() => null);
|
|
1364
1366
|
await onProgress(3, "Seeding from sitemap/robots.txt\u2026");
|
|
1365
1367
|
const sitemapResult = await discoverProjectUrls(appUrl, {
|
|
1366
1368
|
sitemapUrl: config.sitemapUrl ?? crawlCfg.sitemapUrl,
|
|
@@ -1380,6 +1382,7 @@ async function processDiscoverJob(jobId, config, opts) {
|
|
|
1380
1382
|
maxScreens: maxDiscoverUrls,
|
|
1381
1383
|
proxyUrl,
|
|
1382
1384
|
useFirefox: crawlCfg.useFirefox,
|
|
1385
|
+
storageConfig: discoverStorageConfig,
|
|
1383
1386
|
abortSignal: controller.signal,
|
|
1384
1387
|
jobId,
|
|
1385
1388
|
onUrlsDiscovered: async (urls) => {
|
|
@@ -1436,6 +1439,7 @@ async function processDiscoverJob(jobId, config, opts) {
|
|
|
1436
1439
|
proxyUrl,
|
|
1437
1440
|
chromeProfilePath: crawlCfg.chromeProfilePath,
|
|
1438
1441
|
useFirefox: crawlCfg.useFirefox,
|
|
1442
|
+
storageConfig: discoverStorageConfig,
|
|
1439
1443
|
captchaSolverApiKey: crawlCfg.captchaSolverApiKey,
|
|
1440
1444
|
captchaSolverProvider: crawlCfg.captchaSolverProvider,
|
|
1441
1445
|
mailslurpApiKey: crawlCfg.mailslurpApiKey,
|