@detiq/crawler 1.1.6 → 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.
@@ -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 result = await askTracked("", userText, llmCfg, 256);
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,kBAAkB;AAEjD,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,SAAS,MAAM,WAAW,IAAI,UAAU,QAAQ,GAAG;AACzD,WAAO,OAAO,QAAQ;AAAA,EACxB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;",
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 }]
@@ -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": "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",
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) || 3;
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) || 3;
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 { text, usage } = await askTracked(system, user, llmCfg);
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,