@taodigital/agents 0.0.0-stage → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +99 -2
- package/dist/api-test-agent.d.ts +126 -0
- package/dist/api-test-agent.js +267 -0
- package/dist/artifact-intel-agent.d.ts +11 -0
- package/dist/artifact-intel-agent.js +152 -0
- package/dist/coverage-gap-agent.d.ts +25 -0
- package/dist/coverage-gap-agent.js +269 -0
- package/dist/coverage-map-agent.d.ts +15 -0
- package/dist/coverage-map-agent.js +255 -0
- package/dist/crawl-diff-agent.d.ts +27 -0
- package/dist/crawl-diff-agent.js +412 -0
- package/dist/data-agent.d.ts +101 -0
- package/dist/data-agent.js +2284 -0
- package/dist/document-agent.d.ts +10 -0
- package/dist/document-agent.js +32 -0
- package/dist/enhancement-agent.d.ts +48 -0
- package/dist/enhancement-agent.js +342 -0
- package/dist/field-detector-agent.d.ts +35 -0
- package/dist/field-detector-agent.js +220 -0
- package/dist/flow-agent.d.ts +130 -0
- package/dist/flow-agent.js +169 -0
- package/dist/github-agent.d.ts +18 -0
- package/dist/github-agent.js +39 -0
- package/dist/github-write-agent.d.ts +67 -0
- package/dist/github-write-agent.js +249 -0
- package/dist/grouping-agent.d.ts +18 -0
- package/dist/grouping-agent.js +338 -0
- package/dist/healing-agent.d.ts +73 -0
- package/dist/healing-agent.js +137 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +67 -0
- package/dist/integration-test-agent.d.ts +118 -0
- package/dist/integration-test-agent.js +165 -0
- package/dist/label-assignment-agent.d.ts +15 -0
- package/dist/label-assignment-agent.js +97 -0
- package/dist/llm-config.d.ts +38 -0
- package/dist/llm-config.js +287 -0
- package/dist/llm-mock.d.ts +77 -0
- package/dist/llm-mock.js +192 -0
- package/dist/llm.d.ts +58 -0
- package/dist/llm.js +438 -0
- package/dist/nlp-agent.d.ts +208 -0
- package/dist/nlp-agent.js +340 -0
- package/dist/orchestrator.d.ts +285 -0
- package/dist/orchestrator.js +141 -0
- package/dist/plan-agent.d.ts +4 -0
- package/dist/plan-agent.js +200 -0
- package/dist/repo-scan-agent.d.ts +189 -0
- package/dist/repo-scan-agent.js +2032 -0
- package/dist/requirements-agent.d.ts +59 -0
- package/dist/requirements-agent.js +641 -0
- package/dist/scenario-agent.d.ts +20 -0
- package/dist/scenario-agent.js +123 -0
- package/dist/script-agent.d.ts +447 -0
- package/dist/script-agent.js +11813 -0
- package/dist/security-test-agent.d.ts +111 -0
- package/dist/security-test-agent.js +141 -0
- package/dist/skill-file-agent.d.ts +264 -0
- package/dist/skill-file-agent.js +3470 -0
- package/dist/step-coverage.d.ts +46 -0
- package/dist/step-coverage.js +288 -0
- package/dist/step-utils.d.ts +2 -0
- package/dist/step-utils.js +62 -0
- package/dist/test-case-agent.d.ts +206 -0
- package/dist/test-case-agent.js +2141 -0
- package/dist/test-import-agent.d.ts +148 -0
- package/dist/test-import-agent.js +3235 -0
- package/dist/test-script-agent.d.ts +16 -0
- package/dist/test-script-agent.js +48 -0
- package/dist/token-pricing.d.ts +14 -0
- package/dist/token-pricing.js +62 -0
- package/dist/types.d.ts +232 -0
- package/dist/types.js +7 -0
- package/dist/visual-agent.d.ts +114 -0
- package/dist/visual-agent.js +261 -0
- package/package.json +26 -4
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* ZeTa-AI NLP Test Input Agent
|
|
4
|
+
*
|
|
5
|
+
* Converts plain-English test descriptions into structured, grounded test cases.
|
|
6
|
+
* Unlike competitors (testRigor, Functionize) this grounds the output in the
|
|
7
|
+
* Zeta Core — only uses elements that ACTUALLY EXIST on the real screen.
|
|
8
|
+
*
|
|
9
|
+
* Input: "When a user logs in with valid credentials, they should see the dashboard"
|
|
10
|
+
* Output: A structured TestCase grounded in real elements, ready for review.
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.convertNlpToTest = convertNlpToTest;
|
|
14
|
+
exports.extractLocatorsWithLLM = extractLocatorsWithLLM;
|
|
15
|
+
const app_brain_1 = require("@taodigital/app-brain");
|
|
16
|
+
const llm_js_1 = require("./llm.js");
|
|
17
|
+
const llm_config_js_1 = require("./llm-config.js");
|
|
18
|
+
const core_1 = require("@taodigital/core");
|
|
19
|
+
async function convertNlpToTest(opts) {
|
|
20
|
+
// Load all project screens to find the most relevant one.
|
|
21
|
+
const project = await app_brain_1.AppBrain.getProjectMap(opts.tenantId, opts.projectId);
|
|
22
|
+
if (!project)
|
|
23
|
+
throw new Error('Project not found.');
|
|
24
|
+
const ELEMENT_LIMIT = parseInt(process.env.NLP_ELEMENT_LIMIT ?? '10', 10);
|
|
25
|
+
const screenSummaries = project.screens.map((s) => ({
|
|
26
|
+
id: s.id,
|
|
27
|
+
name: s.name,
|
|
28
|
+
url: s.url,
|
|
29
|
+
elementCount: s.elements.length,
|
|
30
|
+
elements: s.elements.slice(0, ELEMENT_LIMIT).map((e) => e.meaning),
|
|
31
|
+
}));
|
|
32
|
+
// ZETA-041: load existing test titles from the suite so the AI can avoid
|
|
33
|
+
// generating duplicate or off-topic test items.
|
|
34
|
+
let existingTitles = [];
|
|
35
|
+
if (opts.suiteId) {
|
|
36
|
+
try {
|
|
37
|
+
const suite = await app_brain_1.AppBrain.getTestSuiteWithCases(opts.tenantId, opts.suiteId);
|
|
38
|
+
existingTitles = (suite?.testCases ?? []).map((tc) => String(tc.title ?? '')).filter(Boolean);
|
|
39
|
+
}
|
|
40
|
+
catch { /* non-fatal — proceed without existing titles */ }
|
|
41
|
+
}
|
|
42
|
+
const SYSTEM_DEFAULT = [
|
|
43
|
+
'You are ZeTa-AI\'s Natural Language Test Converter.',
|
|
44
|
+
'Convert a plain-English test description into a structured test case.',
|
|
45
|
+
'ONLY use elements that exist in the provided Zeta Core context.',
|
|
46
|
+
'Never invent buttons, fields, or screens that are not listed.',
|
|
47
|
+
'The generated test case MUST be distinct from any existing test titles listed below.',
|
|
48
|
+
'Return JSON: { targetScreenId, confidence, understoodIntent, unmatchedTerms, testCase: { title, kind, steps, expectedResult, groundedOn } }',
|
|
49
|
+
].join(' ');
|
|
50
|
+
const system = await (0, core_1.getAgentPrompt)('nlp-locator-extractor', undefined, opts.tenantId) ?? SYSTEM_DEFAULT;
|
|
51
|
+
const user = [
|
|
52
|
+
`USER DESCRIPTION: "${opts.description}"`,
|
|
53
|
+
'',
|
|
54
|
+
'AVAILABLE SCREENS IN Zeta Core:',
|
|
55
|
+
screenSummaries.map((s) => `Screen ID: ${s.id}\nName: ${s.name}\nURL: ${s.url ?? 'n/a'}\nSample elements: ${s.elements.join(', ')}`).join('\n\n'),
|
|
56
|
+
'',
|
|
57
|
+
existingTitles.length > 0
|
|
58
|
+
? `EXISTING TEST CASES IN SUITE (do NOT generate a duplicate of any of these):\n${existingTitles.map((t) => `- ${t}`).join('\n')}`
|
|
59
|
+
: '',
|
|
60
|
+
'',
|
|
61
|
+
'Instructions:',
|
|
62
|
+
'1. Identify which screen the user is describing',
|
|
63
|
+
'2. Map user terms to real element meanings from that screen',
|
|
64
|
+
'3. Build structured test steps using ONLY real element names',
|
|
65
|
+
'4. Note any terms you could not match to real elements',
|
|
66
|
+
'5. Set confidence 0-100 (lower if many terms unmatched)',
|
|
67
|
+
'6. Ensure the generated test title is meaningfully different from existing test titles',
|
|
68
|
+
].filter((line) => line !== '').join('\n');
|
|
69
|
+
let parsed;
|
|
70
|
+
try {
|
|
71
|
+
const t0 = Date.now();
|
|
72
|
+
// GAP-14: NLP conversion needs full reasoning model — was using fast model
|
|
73
|
+
const llmCfg = await (0, llm_config_js_1.getTaskAgentLLMConfig)(opts.tenantId, 'testGeneration');
|
|
74
|
+
const seed = (0, llm_js_1.deterministicSeed)(`${opts.tenantId}:${opts.projectId}:nlp:${opts.description.slice(0, 500)}:${opts.suiteId ?? ''}`);
|
|
75
|
+
const { text: raw, usage } = await (0, llm_js_1.askTracked)(system, user, llmCfg, undefined, undefined, { temperature: 0, seed });
|
|
76
|
+
const latencyMs = Date.now() - t0;
|
|
77
|
+
const json = raw.slice(raw.indexOf('{'), raw.lastIndexOf('}') + 1);
|
|
78
|
+
parsed = JSON.parse(json);
|
|
79
|
+
(0, app_brain_1.recordLLMCall)({
|
|
80
|
+
tenantId: opts.tenantId,
|
|
81
|
+
projectId: opts.projectId,
|
|
82
|
+
agentType: 'nlp',
|
|
83
|
+
label: 'nlp_to_test',
|
|
84
|
+
entityType: 'test_case',
|
|
85
|
+
usage,
|
|
86
|
+
latencyMs,
|
|
87
|
+
}).catch(console.error);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return {
|
|
91
|
+
error: 'Could not parse test from description. Try being more specific about which screen and elements you want to test.',
|
|
92
|
+
description: opts.description,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
// Validate that groundedOn elements actually exist on the target screen.
|
|
96
|
+
let grounded;
|
|
97
|
+
try {
|
|
98
|
+
const screen = await app_brain_1.AppBrain.getScreenWithElements(opts.tenantId, parsed.targetScreenId);
|
|
99
|
+
const allowed = new Set((screen?.elements ?? []).map((e) => e.meaning));
|
|
100
|
+
parsed.testCase.groundedOn = parsed.testCase.groundedOn.filter((e) => allowed.has(e));
|
|
101
|
+
grounded = parsed;
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
grounded = parsed;
|
|
105
|
+
}
|
|
106
|
+
// Save as PENDING_REVIEW (human must approve NLP-generated tests).
|
|
107
|
+
const suiteName = opts.suiteId ? undefined : `NLP: ${grounded.understoodIntent.slice(0, 60)}`;
|
|
108
|
+
const saved = opts.suiteId
|
|
109
|
+
? await app_brain_1.AppBrain.addTestCaseToSuite(opts.tenantId, opts.projectId, opts.suiteId, {
|
|
110
|
+
...grounded.testCase,
|
|
111
|
+
screenId: parsed.targetScreenId,
|
|
112
|
+
expectedResult: grounded.testCase.expectedResult,
|
|
113
|
+
})
|
|
114
|
+
: await app_brain_1.AppBrain.saveGeneratedTestCases(opts.tenantId, opts.projectId, suiteName, [{
|
|
115
|
+
...grounded.testCase,
|
|
116
|
+
screenId: parsed.targetScreenId,
|
|
117
|
+
expectedResult: grounded.testCase.expectedResult,
|
|
118
|
+
}]);
|
|
119
|
+
const savedCase = opts.suiteId
|
|
120
|
+
? saved
|
|
121
|
+
: saved?.testCases?.[0] ?? saved;
|
|
122
|
+
const resolvedRef = savedCase?.ref ?? `TC-NLP-001`;
|
|
123
|
+
const finalizedTestCase = {
|
|
124
|
+
...grounded.testCase,
|
|
125
|
+
id: savedCase?.id,
|
|
126
|
+
ref: resolvedRef,
|
|
127
|
+
screenId: parsed.targetScreenId,
|
|
128
|
+
};
|
|
129
|
+
return {
|
|
130
|
+
id: savedCase?.id,
|
|
131
|
+
ref: resolvedRef,
|
|
132
|
+
title: grounded.testCase.title,
|
|
133
|
+
steps: grounded.testCase.steps,
|
|
134
|
+
expectedResult: grounded.testCase.expectedResult,
|
|
135
|
+
confidence: grounded.confidence,
|
|
136
|
+
understoodIntent: grounded.understoodIntent,
|
|
137
|
+
unmatchedTerms: grounded.unmatchedTerms,
|
|
138
|
+
testCase: finalizedTestCase,
|
|
139
|
+
saved,
|
|
140
|
+
warning: grounded.confidence < 70
|
|
141
|
+
? 'Low confidence. Review carefully — some user-described elements may not have been matched correctly.'
|
|
142
|
+
: undefined,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Use an LLM to extract Playwright locators from test case steps of any format.
|
|
147
|
+
* Handles structured (QA Wolf), plain-English (AI-generated), or mixed step formats.
|
|
148
|
+
* Falls back to empty array if no LLM is configured or extraction fails.
|
|
149
|
+
*/
|
|
150
|
+
async function extractLocatorsWithLLM(opts) {
|
|
151
|
+
const llmCfg = await (0, llm_config_js_1.getTaskAgentLLMConfig)(opts.tenantId, 'testGeneration');
|
|
152
|
+
if (!llmCfg)
|
|
153
|
+
return [];
|
|
154
|
+
const cleanStepForLLM = (s) => s.replace(/\{\{\s*fze\.env\.([^}\s]+)\s*\}\}/gi, (_, v) => `<${v}>`)
|
|
155
|
+
.replace(/\[protected\]/gi, '<credential>')
|
|
156
|
+
.replace(/\[captured:\s*([^\]]+)\]/gi, (_, v) => `<captured_${v}>`)
|
|
157
|
+
.replace(/\[random[^\]]*\]/gi, '<random_value>');
|
|
158
|
+
const stepLines = opts.steps
|
|
159
|
+
.map((s, i) => {
|
|
160
|
+
const action = typeof s === 'string'
|
|
161
|
+
? s
|
|
162
|
+
: [s.action, s.text, s.description, s.label]
|
|
163
|
+
.filter((v) => typeof v === 'string' && String(v).trim().length > 0)
|
|
164
|
+
.join(' ')
|
|
165
|
+
.trim();
|
|
166
|
+
return `${i + 1}. ${cleanStepForLLM(action)}`;
|
|
167
|
+
})
|
|
168
|
+
.filter(l => l.trim().length > 2)
|
|
169
|
+
.slice(0, 80)
|
|
170
|
+
.join('\n');
|
|
171
|
+
if (!stepLines.trim())
|
|
172
|
+
return [];
|
|
173
|
+
// Deterministic pre-extraction for credential and critical action steps
|
|
174
|
+
const deterministicLocators = [];
|
|
175
|
+
const seenDetExpr = new Set();
|
|
176
|
+
const addDet = (key, expr, desc, confidence) => {
|
|
177
|
+
if (seenDetExpr.has(expr))
|
|
178
|
+
return;
|
|
179
|
+
seenDetExpr.add(expr);
|
|
180
|
+
deterministicLocators.push({ key, expression: expr, elementType: 'textbox', confidence, source: 'llm-step-analysis', description: desc });
|
|
181
|
+
};
|
|
182
|
+
for (const rawStep of opts.steps) {
|
|
183
|
+
const stepStr = typeof rawStep === 'string' ? rawStep : String(rawStep.action ?? '');
|
|
184
|
+
if (/enter\s+username\b/i.test(stepStr))
|
|
185
|
+
addDet('usernameInput', "page.getByLabel('Username')", 'Username field', 'high');
|
|
186
|
+
if (/enter\s+password\b/i.test(stepStr) || /\[protected\]/i.test(stepStr))
|
|
187
|
+
addDet('passwordInput', "page.getByLabel('Password')", 'Password field', 'high');
|
|
188
|
+
}
|
|
189
|
+
// Build value→fieldKey map for locator key alignment
|
|
190
|
+
const dataValueMap = new Map(); // exampleValue → fieldKey
|
|
191
|
+
for (const f of opts.dataFields ?? []) {
|
|
192
|
+
if (f.exampleValue && f.exampleValue.trim().length >= 3) {
|
|
193
|
+
dataValueMap.set(f.exampleValue.trim(), f.fieldKey);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const dataFieldsHint = dataValueMap.size > 0
|
|
197
|
+
? `\nDATA FIELDS (use these fieldKey names when a locator's value matches the exampleValue):\n${Array.from(dataValueMap.entries()).map(([v, k]) => ` fieldKey="${k}" exampleValue="${v}"`).join('\n')}\nWhen an element's name/text exactly matches an exampleValue above, use the corresponding fieldKey as the locator key (append "Button", "Link", or "Text" as appropriate).`
|
|
198
|
+
: '';
|
|
199
|
+
const SYSTEM_DEFAULT = `You are a Playwright test automation expert. Your task: analyze test steps and extract Playwright locators for every UI element that is interacted with or verified.
|
|
200
|
+
|
|
201
|
+
Output ONLY a JSON array — no markdown, no explanation, no wrapper object. Each item:
|
|
202
|
+
{"key":"camelCaseName","expression":"page.getByRole(...)","elementType":"button|link|heading|textbox|combobox|checkbox|text","confidence":"high|medium|low","description":"short label"}
|
|
203
|
+
|
|
204
|
+
Playwright locator priority (use the first that applies):
|
|
205
|
+
1. getByRole('button'|'link'|'heading'|'textbox'|'combobox'|'checkbox'|'radio', { name: '...' }) — for semantic elements
|
|
206
|
+
2. getByLabel('...') — for form fields identified by label text
|
|
207
|
+
3. getByPlaceholder('...') — for inputs with placeholder
|
|
208
|
+
4. getByText('...') — for verifying visible text content
|
|
209
|
+
5. getByTestId('...') — only if a data-testid attribute is explicitly mentioned
|
|
210
|
+
|
|
211
|
+
Rules:
|
|
212
|
+
- Deduplicate by expression
|
|
213
|
+
- key must be camelCase, max 40 chars
|
|
214
|
+
- Skip pure navigation steps (URL-only, no element)
|
|
215
|
+
- confidence: high=role+name match, medium=text match, low=ambiguous
|
|
216
|
+
- If a step is too vague to produce a locator, skip it
|
|
217
|
+
- SKIP steps that verify a plain numeric value (e.g. 'Verify text: "392"', 'Verify element text: "458"') — these are dynamic dashboard counts, not stable locators
|
|
218
|
+
- SKIP multi-line element strings containing "Remove Chip" — these are filter chip combinations, not reusable locators
|
|
219
|
+
- SKIP filter buttons whose name ends with \\n(count) like "Source: All\\n(68)" or "UP Type: All\\n(4)" — the count is dynamic; if the same button appears without the count suffix use that instead
|
|
220
|
+
- For steps like "Enter username: X" → generate getByLabel('Username') or getByRole('textbox', { name: 'Username' }) with key "usernameInput"
|
|
221
|
+
- For steps like "Enter password: [protected]" → generate getByLabel('Password') with key "passwordInput" — do NOT skip protected/masked credential steps
|
|
222
|
+
- For steps like "Click login button: X" → generate getByRole('button', { name: X }) with key "signInButton"
|
|
223
|
+
- For date range buttons (element name contains a date pattern like "02/01/2025 - 02/13/2025"), use key "dateRangeButton"
|
|
224
|
+
- Do NOT strip date separators (/ and -) to create numeric keys like "0201202502132025" — use semantic names instead${dataFieldsHint}`;
|
|
225
|
+
const locatorSystemBase = await (0, core_1.getAgentPrompt)('nlp-playwright-locator', undefined, opts.tenantId) ?? SYSTEM_DEFAULT;
|
|
226
|
+
// Append the dataFieldsHint (which is dynamic per-call) only when using the fallback; DB prompt already has the static rules.
|
|
227
|
+
const SYSTEM = locatorSystemBase.includes(dataFieldsHint) || !dataFieldsHint
|
|
228
|
+
? locatorSystemBase
|
|
229
|
+
: `${locatorSystemBase}${dataFieldsHint}`;
|
|
230
|
+
// Build optional context block for scenario-derived TCs where steps may be vague BDD phrases.
|
|
231
|
+
// Gherkin text and title give the LLM enough signal to infer element names even when individual
|
|
232
|
+
// step lines say things like "the user completes the registration".
|
|
233
|
+
const ctx = opts.tcContext;
|
|
234
|
+
const contextParts = [];
|
|
235
|
+
if (ctx?.title?.trim())
|
|
236
|
+
contextParts.push(`Test case: ${ctx.title.trim()}`);
|
|
237
|
+
if (ctx?.preconditions?.trim())
|
|
238
|
+
contextParts.push(`Preconditions: ${ctx.preconditions.trim()}`);
|
|
239
|
+
if (ctx?.gherkin?.trim())
|
|
240
|
+
contextParts.push(`Full Gherkin scenario:\n${ctx.gherkin.trim().slice(0, 2000)}`);
|
|
241
|
+
const tcContextBlock = contextParts.length > 0
|
|
242
|
+
? `\n\nADDITIONAL CONTEXT (use to infer element names when steps are vague):\n${contextParts.join('\n')}`
|
|
243
|
+
: '';
|
|
244
|
+
const USER = `Test steps:\n${stepLines}${tcContextBlock}\n\nReturn JSON array of Playwright locators:`;
|
|
245
|
+
try {
|
|
246
|
+
const seed = (0, llm_js_1.deterministicSeed)(`${opts.tenantId}:nlp-locators:${stepLines.slice(0, 500)}`);
|
|
247
|
+
const result = await (0, llm_js_1.askTracked)(SYSTEM, USER, llmCfg, 2048, undefined, { temperature: 0, seed });
|
|
248
|
+
const raw = result.text.trim();
|
|
249
|
+
const json = raw.startsWith('```') ? raw.replace(/^```[^\n]*\n?/, '').replace(/\n?```$/, '') : raw;
|
|
250
|
+
const parsed = JSON.parse(json);
|
|
251
|
+
if (!Array.isArray(parsed))
|
|
252
|
+
return [];
|
|
253
|
+
const seenKeys = new Set();
|
|
254
|
+
const seenExprs = new Set();
|
|
255
|
+
const locators = [];
|
|
256
|
+
for (const l of parsed) {
|
|
257
|
+
if (!l?.key || !l?.expression || !String(l.expression).startsWith('page.'))
|
|
258
|
+
continue;
|
|
259
|
+
const expr = String(l.expression);
|
|
260
|
+
// Filter 1: skip multi-line chip combo expressions
|
|
261
|
+
// Match both real newlines (LLM JSON parsed) and literal \n (safety)
|
|
262
|
+
const newlineCount = (expr.match(/\\n|\n/g) ?? []).length;
|
|
263
|
+
if (expr.includes('Remove Chip') || newlineCount >= 2)
|
|
264
|
+
continue;
|
|
265
|
+
// Filter 2: skip pure numeric descriptions or keys (dynamic dashboard counts like 392, 458)
|
|
266
|
+
const desc = String(l.description ?? '').trim();
|
|
267
|
+
if (/^\d+$/.test(desc))
|
|
268
|
+
continue;
|
|
269
|
+
if (/^\d+$/.test(String(l.key ?? '')))
|
|
270
|
+
continue;
|
|
271
|
+
// Filter 3: skip filter-chip buttons with dynamic count suffix like "Source: All\n(68)"
|
|
272
|
+
// LLM JSON "Source: All\n(68)" parses to a real newline, so match real newline char
|
|
273
|
+
if (/\n\(\d+\)/.test(expr))
|
|
274
|
+
continue;
|
|
275
|
+
// Filter 4: skip locators that use display artifact tokens — these are never real DOM text
|
|
276
|
+
if (/\[captured:\s*|'\[random|\[dynamic\]/i.test(expr))
|
|
277
|
+
continue;
|
|
278
|
+
// Deduplicate by expression
|
|
279
|
+
if (seenExprs.has(expr))
|
|
280
|
+
continue;
|
|
281
|
+
seenExprs.add(expr);
|
|
282
|
+
let key = String(l.key).replace(/[^a-zA-Z0-9]/g, '').slice(0, 40) || 'element';
|
|
283
|
+
// Fix 1: rename all-digit keys (date string munging like "0201202502132025")
|
|
284
|
+
if (/^\d{6,}$/.test(key)) {
|
|
285
|
+
if (/\d{1,2}\/\d{1,2}\/\d{4}/.test(expr)) {
|
|
286
|
+
key = 'dateRangeButton';
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
key = `element${key.slice(0, 6)}`;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
// Fix 2: align key with data field name when expression matches a field's example value
|
|
293
|
+
for (const [val, fieldKey] of dataValueMap) {
|
|
294
|
+
if (expr.includes(val)) {
|
|
295
|
+
const suffix = String(l.elementType ?? '').toLowerCase().includes('button') ? 'Button'
|
|
296
|
+
: String(l.elementType ?? '').toLowerCase().includes('link') ? 'Link'
|
|
297
|
+
: String(l.elementType ?? '').toLowerCase().includes('text') ? 'Text'
|
|
298
|
+
: 'Element';
|
|
299
|
+
key = (fieldKey + suffix).replace(/[^a-zA-Z0-9]/g, '').slice(0, 40);
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
// Disambiguate duplicate keys
|
|
304
|
+
const baseKey = key;
|
|
305
|
+
let dedupeIdx = 2;
|
|
306
|
+
while (seenKeys.has(key)) {
|
|
307
|
+
key = `${baseKey}${dedupeIdx++}`;
|
|
308
|
+
}
|
|
309
|
+
seenKeys.add(key);
|
|
310
|
+
locators.push({
|
|
311
|
+
key,
|
|
312
|
+
expression: expr,
|
|
313
|
+
elementType: String(l.elementType ?? 'element'),
|
|
314
|
+
confidence: ['high', 'medium', 'low'].includes(l.confidence) ? l.confidence : 'medium',
|
|
315
|
+
source: 'llm-step-analysis',
|
|
316
|
+
description: desc.slice(0, 80),
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
// Merge deterministic locators first (they take priority on key), then LLM locators
|
|
320
|
+
const finalLocators = [...deterministicLocators];
|
|
321
|
+
const seenFinalExprs = new Set(deterministicLocators.map(l => l.expression));
|
|
322
|
+
const seenFinalKeys = new Set(deterministicLocators.map(l => l.key));
|
|
323
|
+
for (const loc of locators) {
|
|
324
|
+
if (seenFinalExprs.has(loc.expression))
|
|
325
|
+
continue;
|
|
326
|
+
// Disambiguate key
|
|
327
|
+
let k = loc.key;
|
|
328
|
+
let idx = 2;
|
|
329
|
+
while (seenFinalKeys.has(k))
|
|
330
|
+
k = `${loc.key}${idx++}`;
|
|
331
|
+
seenFinalKeys.add(k);
|
|
332
|
+
seenFinalExprs.add(loc.expression);
|
|
333
|
+
finalLocators.push({ ...loc, key: k });
|
|
334
|
+
}
|
|
335
|
+
return finalLocators;
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
return [];
|
|
339
|
+
}
|
|
340
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
export interface OrchestratorRequest {
|
|
2
|
+
tenantId: string;
|
|
3
|
+
projectId: string;
|
|
4
|
+
jobId?: string;
|
|
5
|
+
targetScreenId?: string;
|
|
6
|
+
targetFlowStartScreenId?: string;
|
|
7
|
+
targetFlowEndScreenId?: string;
|
|
8
|
+
viewport?: string;
|
|
9
|
+
suiteNameOverride?: string;
|
|
10
|
+
force?: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* 'append' (default) — deduplicates by content hash + grounding hash + title Jaccard.
|
|
13
|
+
* 'replace' — deletes existing AI-generated cases, then inserts fresh.
|
|
14
|
+
* 'edit' — upserts matched cases (updates steps/gherkin/expectedResult, snapshots old version).
|
|
15
|
+
*/
|
|
16
|
+
mode?: 'replace' | 'append' | 'edit';
|
|
17
|
+
/**
|
|
18
|
+
* When provided, only generates test cases of these kinds.
|
|
19
|
+
* Passed through to generateTestCases. Omit for all-kinds generation (default).
|
|
20
|
+
*/
|
|
21
|
+
kinds?: readonly string[];
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Dual-Context Orchestrator — routes test generation to the right agent.
|
|
25
|
+
*
|
|
26
|
+
* ROUTE 1: targetScreenId → Pure CAG → generateTestCases() (21-pass)
|
|
27
|
+
* ROUTE 2: targetFlowStart + targetFlowEnd → KAG BFS + CAG → generateFlowTestCases()
|
|
28
|
+
*/
|
|
29
|
+
export declare class DualContextOrchestrator {
|
|
30
|
+
static execute(req: OrchestratorRequest): Promise<{
|
|
31
|
+
status: string;
|
|
32
|
+
cases: never[];
|
|
33
|
+
saved: null;
|
|
34
|
+
screenCount?: undefined;
|
|
35
|
+
suiteName?: undefined;
|
|
36
|
+
} | {
|
|
37
|
+
status: string;
|
|
38
|
+
cases: never[];
|
|
39
|
+
saved: null;
|
|
40
|
+
screenCount: number;
|
|
41
|
+
suiteName?: undefined;
|
|
42
|
+
} | {
|
|
43
|
+
status: string;
|
|
44
|
+
cases: import("./flow-agent.js").FlowTestCase[];
|
|
45
|
+
saved: {
|
|
46
|
+
suite: {
|
|
47
|
+
tenantId: string;
|
|
48
|
+
id: string;
|
|
49
|
+
projectId: string;
|
|
50
|
+
name: string;
|
|
51
|
+
createdAt: Date;
|
|
52
|
+
genModel: string | null;
|
|
53
|
+
category: import(".prisma/client").$Enums.SuiteCategory | null;
|
|
54
|
+
parentId: string | null;
|
|
55
|
+
ownerUserId: string | null;
|
|
56
|
+
genTotalTokens: number | null;
|
|
57
|
+
genTotalCostUsd: number | null;
|
|
58
|
+
genAt: Date | null;
|
|
59
|
+
genScreenCount: number | null;
|
|
60
|
+
coverageReport: import("@prisma/client/runtime/library").JsonValue | null;
|
|
61
|
+
};
|
|
62
|
+
testCases: {
|
|
63
|
+
tenantId: string;
|
|
64
|
+
screenId: string | null;
|
|
65
|
+
id: string;
|
|
66
|
+
projectId: string;
|
|
67
|
+
source: string;
|
|
68
|
+
status: string;
|
|
69
|
+
createdAt: Date;
|
|
70
|
+
updatedAt: Date;
|
|
71
|
+
steps: import("@prisma/client/runtime/library").JsonValue;
|
|
72
|
+
kind: string;
|
|
73
|
+
tags: import("@prisma/client/runtime/library").JsonValue;
|
|
74
|
+
approvedBy: string | null;
|
|
75
|
+
suiteId: string;
|
|
76
|
+
ref: string | null;
|
|
77
|
+
title: string;
|
|
78
|
+
priority: string;
|
|
79
|
+
preconditions: string | null;
|
|
80
|
+
expectedResult: string | null;
|
|
81
|
+
gherkin: string | null;
|
|
82
|
+
approvedAt: Date | null;
|
|
83
|
+
groundedOn: import("@prisma/client/runtime/library").JsonValue;
|
|
84
|
+
caseRefs: import("@prisma/client/runtime/library").JsonValue;
|
|
85
|
+
viewports: import("@prisma/client/runtime/library").JsonValue;
|
|
86
|
+
viewportConstraint: string;
|
|
87
|
+
techniqueOfRecord: string | null;
|
|
88
|
+
traceTo: string | null;
|
|
89
|
+
traceToExtra: import("@prisma/client/runtime/library").JsonValue;
|
|
90
|
+
testDataInline: import("@prisma/client/runtime/library").JsonValue | null;
|
|
91
|
+
dataNote: string | null;
|
|
92
|
+
scriptNote: string | null;
|
|
93
|
+
structuredSteps: import("@prisma/client/runtime/library").JsonValue | null;
|
|
94
|
+
artifactId: string | null;
|
|
95
|
+
scenarioId: string | null;
|
|
96
|
+
suspect: boolean;
|
|
97
|
+
suspectReason: string | null;
|
|
98
|
+
flakyScore: number;
|
|
99
|
+
consecutiveFlakes: number;
|
|
100
|
+
lastFlakyAt: Date | null;
|
|
101
|
+
quarantined: boolean;
|
|
102
|
+
quarantinedAt: Date | null;
|
|
103
|
+
quarantineReason: string | null;
|
|
104
|
+
genPromptTokens: number | null;
|
|
105
|
+
genCompletionTokens: number | null;
|
|
106
|
+
genCostUsd: number | null;
|
|
107
|
+
genModel: string | null;
|
|
108
|
+
dataSetId: string | null;
|
|
109
|
+
repoTargetPath: string | null;
|
|
110
|
+
stepsKey: string | null;
|
|
111
|
+
gherkinKey: string | null;
|
|
112
|
+
contentHash: string | null;
|
|
113
|
+
groundingHash: string | null;
|
|
114
|
+
locatorsJson: import("@prisma/client/runtime/library").JsonValue | null;
|
|
115
|
+
locatorsExtractedAt: Date | null;
|
|
116
|
+
captureStepScreenshots: import("@prisma/client/runtime/library").JsonValue | null;
|
|
117
|
+
}[];
|
|
118
|
+
};
|
|
119
|
+
screenCount: number;
|
|
120
|
+
suiteName: string;
|
|
121
|
+
} | {
|
|
122
|
+
skipped: boolean;
|
|
123
|
+
reason: string;
|
|
124
|
+
existingCaseCount: number;
|
|
125
|
+
status?: undefined;
|
|
126
|
+
raw?: undefined;
|
|
127
|
+
cases?: undefined;
|
|
128
|
+
groundedFacts?: undefined;
|
|
129
|
+
saved?: undefined;
|
|
130
|
+
passBreakdown?: undefined;
|
|
131
|
+
coverageReport?: undefined;
|
|
132
|
+
} | {
|
|
133
|
+
status: string;
|
|
134
|
+
raw: string;
|
|
135
|
+
cases: {
|
|
136
|
+
title: string;
|
|
137
|
+
ref: string;
|
|
138
|
+
viewportConstraint: "ALL" | "MOBILE_ONLY" | "TABLET_ONLY";
|
|
139
|
+
techniqueOfRecord: string | undefined;
|
|
140
|
+
traceTo: string | undefined;
|
|
141
|
+
testDataInline: Record<string, unknown> | undefined;
|
|
142
|
+
category: string[] | undefined;
|
|
143
|
+
dataNote: string | undefined;
|
|
144
|
+
scriptNote: string | undefined;
|
|
145
|
+
structuredSteps: {
|
|
146
|
+
action: string;
|
|
147
|
+
target?: string;
|
|
148
|
+
data?: string;
|
|
149
|
+
}[] | undefined;
|
|
150
|
+
kind: import("@taodigital/core").TestKind;
|
|
151
|
+
priority: "CRITICAL" | "HIGH" | "MEDIUM" | "LOW";
|
|
152
|
+
preconditions: string;
|
|
153
|
+
expectedResult: string;
|
|
154
|
+
steps: string[];
|
|
155
|
+
groundedOn: string[];
|
|
156
|
+
gherkin: string;
|
|
157
|
+
tags: string[];
|
|
158
|
+
}[];
|
|
159
|
+
groundedFacts: import("@taodigital/core").ElementFact[];
|
|
160
|
+
saved: {
|
|
161
|
+
suite: {
|
|
162
|
+
tenantId: string;
|
|
163
|
+
id: string;
|
|
164
|
+
projectId: string;
|
|
165
|
+
name: string;
|
|
166
|
+
createdAt: Date;
|
|
167
|
+
genModel: string | null;
|
|
168
|
+
category: import(".prisma/client").$Enums.SuiteCategory | null;
|
|
169
|
+
parentId: string | null;
|
|
170
|
+
ownerUserId: string | null;
|
|
171
|
+
genTotalTokens: number | null;
|
|
172
|
+
genTotalCostUsd: number | null;
|
|
173
|
+
genAt: Date | null;
|
|
174
|
+
genScreenCount: number | null;
|
|
175
|
+
coverageReport: import("@prisma/client/runtime/library").JsonValue | null;
|
|
176
|
+
};
|
|
177
|
+
testCases: {
|
|
178
|
+
tenantId: string;
|
|
179
|
+
screenId: string | null;
|
|
180
|
+
id: string;
|
|
181
|
+
projectId: string;
|
|
182
|
+
source: string;
|
|
183
|
+
status: string;
|
|
184
|
+
createdAt: Date;
|
|
185
|
+
updatedAt: Date;
|
|
186
|
+
steps: import("@prisma/client/runtime/library").JsonValue;
|
|
187
|
+
kind: string;
|
|
188
|
+
tags: import("@prisma/client/runtime/library").JsonValue;
|
|
189
|
+
approvedBy: string | null;
|
|
190
|
+
suiteId: string;
|
|
191
|
+
ref: string | null;
|
|
192
|
+
title: string;
|
|
193
|
+
priority: string;
|
|
194
|
+
preconditions: string | null;
|
|
195
|
+
expectedResult: string | null;
|
|
196
|
+
gherkin: string | null;
|
|
197
|
+
approvedAt: Date | null;
|
|
198
|
+
groundedOn: import("@prisma/client/runtime/library").JsonValue;
|
|
199
|
+
caseRefs: import("@prisma/client/runtime/library").JsonValue;
|
|
200
|
+
viewports: import("@prisma/client/runtime/library").JsonValue;
|
|
201
|
+
viewportConstraint: string;
|
|
202
|
+
techniqueOfRecord: string | null;
|
|
203
|
+
traceTo: string | null;
|
|
204
|
+
traceToExtra: import("@prisma/client/runtime/library").JsonValue;
|
|
205
|
+
testDataInline: import("@prisma/client/runtime/library").JsonValue | null;
|
|
206
|
+
dataNote: string | null;
|
|
207
|
+
scriptNote: string | null;
|
|
208
|
+
structuredSteps: import("@prisma/client/runtime/library").JsonValue | null;
|
|
209
|
+
artifactId: string | null;
|
|
210
|
+
scenarioId: string | null;
|
|
211
|
+
suspect: boolean;
|
|
212
|
+
suspectReason: string | null;
|
|
213
|
+
flakyScore: number;
|
|
214
|
+
consecutiveFlakes: number;
|
|
215
|
+
lastFlakyAt: Date | null;
|
|
216
|
+
quarantined: boolean;
|
|
217
|
+
quarantinedAt: Date | null;
|
|
218
|
+
quarantineReason: string | null;
|
|
219
|
+
genPromptTokens: number | null;
|
|
220
|
+
genCompletionTokens: number | null;
|
|
221
|
+
genCostUsd: number | null;
|
|
222
|
+
genModel: string | null;
|
|
223
|
+
dataSetId: string | null;
|
|
224
|
+
repoTargetPath: string | null;
|
|
225
|
+
stepsKey: string | null;
|
|
226
|
+
gherkinKey: string | null;
|
|
227
|
+
contentHash: string | null;
|
|
228
|
+
groundingHash: string | null;
|
|
229
|
+
locatorsJson: import("@prisma/client/runtime/library").JsonValue | null;
|
|
230
|
+
locatorsExtractedAt: Date | null;
|
|
231
|
+
captureStepScreenshots: import("@prisma/client/runtime/library").JsonValue | null;
|
|
232
|
+
}[];
|
|
233
|
+
};
|
|
234
|
+
passBreakdown: {
|
|
235
|
+
[k: string]: number;
|
|
236
|
+
};
|
|
237
|
+
coverageReport: {
|
|
238
|
+
byKind: Record<string, number>;
|
|
239
|
+
byPriority: Record<string, number>;
|
|
240
|
+
byTechnique: Record<string, number>;
|
|
241
|
+
targetsDiscovered: number;
|
|
242
|
+
targetsCovered: number;
|
|
243
|
+
gaps: string[];
|
|
244
|
+
passBreakdown: {
|
|
245
|
+
[k: string]: number;
|
|
246
|
+
};
|
|
247
|
+
};
|
|
248
|
+
skipped?: undefined;
|
|
249
|
+
reason?: undefined;
|
|
250
|
+
existingCaseCount?: undefined;
|
|
251
|
+
}>;
|
|
252
|
+
}
|
|
253
|
+
export interface SwarmRequest {
|
|
254
|
+
tenantId: string;
|
|
255
|
+
projectId: string;
|
|
256
|
+
jobId?: string;
|
|
257
|
+
/** Run all screens in parallel — swarm mode */
|
|
258
|
+
screenIds: string[];
|
|
259
|
+
/** Max concurrent agents. Default 4. */
|
|
260
|
+
concurrency?: number;
|
|
261
|
+
}
|
|
262
|
+
export interface SwarmResult {
|
|
263
|
+
succeeded: string[];
|
|
264
|
+
failed: {
|
|
265
|
+
screenId: string;
|
|
266
|
+
error: string;
|
|
267
|
+
}[];
|
|
268
|
+
totalGenerated: number;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Swarm Orchestrator — runs multiple test-generation agents in parallel.
|
|
272
|
+
*
|
|
273
|
+
* Each agent reads shared skill files before generating (so they all share
|
|
274
|
+
* the same application knowledge). After all agents complete, COVERAGE_MATRIX
|
|
275
|
+
* skill file is updated with aggregated counts.
|
|
276
|
+
*
|
|
277
|
+
* Skill files act as the shared memory bus — any knowledge learned from
|
|
278
|
+
* artifacts (via learnFromArtifact) or from crawls (via skill-file-agent)
|
|
279
|
+
* is automatically available to every agent in the swarm.
|
|
280
|
+
*/
|
|
281
|
+
export declare class SwarmOrchestrator {
|
|
282
|
+
static execute(req: SwarmRequest): Promise<SwarmResult>;
|
|
283
|
+
/** Run swarm over ALL crawled screens in a project */
|
|
284
|
+
static executeForProject(req: Omit<SwarmRequest, 'screenIds'>): Promise<SwarmResult>;
|
|
285
|
+
}
|