@taodigital/agents 0.0.0-stage → 1.2.1
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,412 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* ZeTa-AI Crawl Diff Agent
|
|
4
|
+
*
|
|
5
|
+
* Compares DOM elements and navigation flows across two crawl runs to detect
|
|
6
|
+
* structural UI changes. Drives the approval gate state machine that gates
|
|
7
|
+
* script generation until all diffs are reviewed.
|
|
8
|
+
*
|
|
9
|
+
* Public entry points:
|
|
10
|
+
* computeCrawlDiffs — diff elements + flows between two crawl jobs
|
|
11
|
+
* createCrawlContextEnhancement — create ContextEnhancement record for a crawl job
|
|
12
|
+
* approveCrawlDiff — mark a diff approved, flag affected test cases suspect
|
|
13
|
+
* recomputeApprovalGateSummary — recompute contextPhase based on pending diffs
|
|
14
|
+
* computeCoverageGaps — find UI elements not covered by any test case
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.computeCrawlDiffs = computeCrawlDiffs;
|
|
18
|
+
exports.createCrawlContextEnhancement = createCrawlContextEnhancement;
|
|
19
|
+
exports.approveCrawlDiff = approveCrawlDiff;
|
|
20
|
+
exports.recomputeApprovalGateSummary = recomputeApprovalGateSummary;
|
|
21
|
+
exports.computeCoverageGaps = computeCoverageGaps;
|
|
22
|
+
const database_1 = require("@taodigital/database");
|
|
23
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
24
|
+
const PHASE_ORDER = [
|
|
25
|
+
'PENDING_CONTEXT',
|
|
26
|
+
'CONTEXT_PARTIAL',
|
|
27
|
+
'CONTEXT_ENRICHED',
|
|
28
|
+
'DIFF_PENDING_APPROVAL',
|
|
29
|
+
'ALL_DIFFS_APPROVED',
|
|
30
|
+
'READY_FOR_SCRIPT_GEN',
|
|
31
|
+
];
|
|
32
|
+
const CONTEXT_THRESHOLDS = {
|
|
33
|
+
WEB: 80,
|
|
34
|
+
API: 40,
|
|
35
|
+
NATIVE: 60,
|
|
36
|
+
HYBRID: 70,
|
|
37
|
+
};
|
|
38
|
+
// Non-interactive roles to skip in coverage gap analysis
|
|
39
|
+
const NON_INTERACTIVE_ROLES = new Set(['link', 'heading', 'image', 'text']);
|
|
40
|
+
// ─── 1. computeCrawlDiffs ─────────────────────────────────────────────────────
|
|
41
|
+
async function computeCrawlDiffs(tenantId, projectId, fromJobId, toJobId) {
|
|
42
|
+
// Load screens for each job run, keyed by url (fallback to name)
|
|
43
|
+
const [fromScreens, toScreens] = await Promise.all([
|
|
44
|
+
database_1.prismaAdmin.screen.findMany({
|
|
45
|
+
where: { tenantId, projectId, lastCrawlJobId: fromJobId },
|
|
46
|
+
}),
|
|
47
|
+
database_1.prismaAdmin.screen.findMany({
|
|
48
|
+
where: { tenantId, projectId, lastCrawlJobId: toJobId },
|
|
49
|
+
}),
|
|
50
|
+
]);
|
|
51
|
+
const fromScreenMap = new Map(fromScreens.map((s) => [s.url ?? s.name, s]));
|
|
52
|
+
const toScreenMap = new Map(toScreens.map((s) => [s.url ?? s.name, s]));
|
|
53
|
+
// Find screens present in both runs
|
|
54
|
+
const commonKeys = [...fromScreenMap.keys()].filter((k) => toScreenMap.has(k));
|
|
55
|
+
const diffsToCreate = [];
|
|
56
|
+
// ── Element diffs for common screens ──────────────────────────────────────
|
|
57
|
+
for (const key of commonKeys) {
|
|
58
|
+
const fromScreen = fromScreenMap.get(key);
|
|
59
|
+
const toScreen = toScreenMap.get(key);
|
|
60
|
+
const [fromElements, toElements] = await Promise.all([
|
|
61
|
+
database_1.prismaAdmin.element.findMany({ where: { tenantId, screenId: fromScreen.id } }),
|
|
62
|
+
database_1.prismaAdmin.element.findMany({ where: { tenantId, screenId: toScreen.id } }),
|
|
63
|
+
]);
|
|
64
|
+
// Key elements by role+meaning for comparison
|
|
65
|
+
const makeElementKey = (e) => `${e.role ?? ''}::${e.meaning ?? e.text ?? ''}`;
|
|
66
|
+
const fromElemMap = new Map(fromElements.map((e) => [makeElementKey(e), e]));
|
|
67
|
+
const toElemMap = new Map(toElements.map((e) => [makeElementKey(e), e]));
|
|
68
|
+
// Removed elements (in from, not in to)
|
|
69
|
+
for (const [eKey, elem] of fromElemMap) {
|
|
70
|
+
if (!toElemMap.has(eKey)) {
|
|
71
|
+
const affectedTestCaseIds = await findAffectedTestCases(tenantId, projectId, [elem.id]);
|
|
72
|
+
const severity = computeSeverity(affectedTestCaseIds.length);
|
|
73
|
+
diffsToCreate.push({
|
|
74
|
+
tenantId,
|
|
75
|
+
projectId,
|
|
76
|
+
fromJobId,
|
|
77
|
+
toJobId,
|
|
78
|
+
diffType: 'ELEMENT_REMOVED',
|
|
79
|
+
severity,
|
|
80
|
+
status: severity === 'NONE' ? 'APPROVED' : 'PENDING',
|
|
81
|
+
fromScreenId: fromScreen.id,
|
|
82
|
+
toScreenId: toScreen.id,
|
|
83
|
+
elementRole: elem.role,
|
|
84
|
+
elementMeaning: elem.meaning,
|
|
85
|
+
affectedTestCaseIds,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// Added elements (in to, not in from)
|
|
90
|
+
for (const [eKey, elem] of toElemMap) {
|
|
91
|
+
if (!fromElemMap.has(eKey)) {
|
|
92
|
+
const affectedTestCaseIds = await findAffectedTestCases(tenantId, projectId, [elem.id]);
|
|
93
|
+
const severity = computeSeverity(affectedTestCaseIds.length);
|
|
94
|
+
diffsToCreate.push({
|
|
95
|
+
tenantId,
|
|
96
|
+
projectId,
|
|
97
|
+
fromJobId,
|
|
98
|
+
toJobId,
|
|
99
|
+
diffType: 'ELEMENT_ADDED',
|
|
100
|
+
severity,
|
|
101
|
+
status: severity === 'NONE' ? 'APPROVED' : 'PENDING',
|
|
102
|
+
fromScreenId: fromScreen.id,
|
|
103
|
+
toScreenId: toScreen.id,
|
|
104
|
+
elementRole: elem.role,
|
|
105
|
+
elementMeaning: elem.meaning,
|
|
106
|
+
affectedTestCaseIds,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// ── Flow diffs (DiscoveredLink) ────────────────────────────────────────────
|
|
112
|
+
const [fromLinks, toLinks] = await Promise.all([
|
|
113
|
+
database_1.prismaAdmin.discoveredLink.findMany({ where: { tenantId, projectId, jobId: fromJobId } }),
|
|
114
|
+
database_1.prismaAdmin.discoveredLink.findMany({ where: { tenantId, projectId, jobId: toJobId } }),
|
|
115
|
+
]);
|
|
116
|
+
const fromLinkSet = new Set(fromLinks.map((l) => l.href));
|
|
117
|
+
const toLinkSet = new Set(toLinks.map((l) => l.href));
|
|
118
|
+
// Links added in toJob (not in fromJob)
|
|
119
|
+
for (const link of toLinks) {
|
|
120
|
+
if (!fromLinkSet.has(link.href)) {
|
|
121
|
+
diffsToCreate.push({
|
|
122
|
+
tenantId,
|
|
123
|
+
projectId,
|
|
124
|
+
fromJobId,
|
|
125
|
+
toJobId,
|
|
126
|
+
diffType: 'FLOW_ADDED',
|
|
127
|
+
severity: 'MINOR',
|
|
128
|
+
status: 'PENDING',
|
|
129
|
+
fromScreenId: null,
|
|
130
|
+
toScreenId: link.toScreenId,
|
|
131
|
+
fromHref: null,
|
|
132
|
+
toHref: link.href,
|
|
133
|
+
affectedTestCaseIds: [],
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// Links removed (in fromJob, not in toJob)
|
|
138
|
+
for (const link of fromLinks) {
|
|
139
|
+
if (!toLinkSet.has(link.href)) {
|
|
140
|
+
diffsToCreate.push({
|
|
141
|
+
tenantId,
|
|
142
|
+
projectId,
|
|
143
|
+
fromJobId,
|
|
144
|
+
toJobId,
|
|
145
|
+
diffType: 'FLOW_REMOVED',
|
|
146
|
+
severity: 'SIGNIFICANT',
|
|
147
|
+
status: 'PENDING',
|
|
148
|
+
fromScreenId: link.toScreenId,
|
|
149
|
+
toScreenId: null,
|
|
150
|
+
fromHref: link.href,
|
|
151
|
+
toHref: null,
|
|
152
|
+
affectedTestCaseIds: [],
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// ── Batch-create all diffs ─────────────────────────────────────────────────
|
|
157
|
+
if (diffsToCreate.length > 0) {
|
|
158
|
+
await database_1.prismaAdmin.crawlDiff.createMany({
|
|
159
|
+
data: diffsToCreate.map((d) => ({
|
|
160
|
+
...d,
|
|
161
|
+
affectedTestCaseIds: JSON.stringify(d.affectedTestCaseIds),
|
|
162
|
+
createdAt: new Date(),
|
|
163
|
+
})),
|
|
164
|
+
skipDuplicates: true,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
const criticalCount = diffsToCreate.filter((d) => d.severity === 'CRITICAL').length;
|
|
168
|
+
const significantCount = diffsToCreate.filter((d) => d.severity === 'SIGNIFICANT').length;
|
|
169
|
+
// ── Create context enhancement record ────────────────────────────────────
|
|
170
|
+
await createCrawlContextEnhancement(tenantId, projectId, toJobId, diffsToCreate.map((d) => ({ severity: d.severity })));
|
|
171
|
+
return {
|
|
172
|
+
diffCount: diffsToCreate.length,
|
|
173
|
+
criticalCount,
|
|
174
|
+
significantCount,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
// ─── 2. createCrawlContextEnhancement ────────────────────────────────────────
|
|
178
|
+
async function createCrawlContextEnhancement(tenantId, projectId, toJobId, diffs) {
|
|
179
|
+
const hasPendingDiffs = diffs.filter((d) => d.severity !== 'NONE').length > 0;
|
|
180
|
+
const status = hasPendingDiffs ? 'DIFF_READY' : 'APPROVED';
|
|
181
|
+
return database_1.prismaAdmin.contextEnhancement.create({
|
|
182
|
+
data: {
|
|
183
|
+
tenantId,
|
|
184
|
+
projectId,
|
|
185
|
+
sourceType: 'CRAWL',
|
|
186
|
+
crawlJobId: toJobId,
|
|
187
|
+
status,
|
|
188
|
+
proposedChanges: [],
|
|
189
|
+
createdAt: new Date(),
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
// ─── 3. approveCrawlDiff ──────────────────────────────────────────────────────
|
|
194
|
+
async function approveCrawlDiff(tenantId, projectId, diffId, reviewedBy) {
|
|
195
|
+
const diff = await database_1.prismaAdmin.crawlDiff.findFirstOrThrow({
|
|
196
|
+
where: { id: diffId, tenantId },
|
|
197
|
+
});
|
|
198
|
+
await database_1.prismaAdmin.$transaction([
|
|
199
|
+
database_1.prismaAdmin.crawlDiff.update({
|
|
200
|
+
where: { id: diffId },
|
|
201
|
+
data: {
|
|
202
|
+
status: 'APPROVED',
|
|
203
|
+
reviewedBy,
|
|
204
|
+
reviewedAt: new Date(),
|
|
205
|
+
},
|
|
206
|
+
}),
|
|
207
|
+
]);
|
|
208
|
+
// Flag affected test cases as suspect
|
|
209
|
+
const affectedTestCaseIds = (() => {
|
|
210
|
+
try {
|
|
211
|
+
const raw = diff.affectedTestCaseIds;
|
|
212
|
+
if (Array.isArray(raw))
|
|
213
|
+
return raw;
|
|
214
|
+
if (typeof raw === 'string')
|
|
215
|
+
return JSON.parse(raw);
|
|
216
|
+
return [];
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
return [];
|
|
220
|
+
}
|
|
221
|
+
})();
|
|
222
|
+
if (affectedTestCaseIds.length > 0) {
|
|
223
|
+
await database_1.prismaAdmin.testCase.updateMany({
|
|
224
|
+
where: { id: { in: affectedTestCaseIds }, tenantId },
|
|
225
|
+
data: { suspect: true },
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
await recomputeApprovalGateSummary(tenantId, projectId);
|
|
229
|
+
}
|
|
230
|
+
// ─── 4. recomputeApprovalGateSummary ─────────────────────────────────────────
|
|
231
|
+
async function recomputeApprovalGateSummary(tenantId, projectId) {
|
|
232
|
+
const [screenshotPending, figmaPending, crawlPending, apiSpecPending] = await Promise.all([
|
|
233
|
+
database_1.prismaAdmin.contextEnhancement.count({
|
|
234
|
+
where: { tenantId, projectId, sourceType: 'SCREENSHOT', status: { in: ['DIFF_READY', 'PENDING'] } },
|
|
235
|
+
}),
|
|
236
|
+
database_1.prismaAdmin.contextEnhancement.count({
|
|
237
|
+
where: { tenantId, projectId, sourceType: 'FIGMA', status: { in: ['DIFF_READY', 'PENDING'] } },
|
|
238
|
+
}),
|
|
239
|
+
database_1.prismaAdmin.crawlDiff.count({
|
|
240
|
+
where: { tenantId, projectId, status: 'PENDING' },
|
|
241
|
+
}),
|
|
242
|
+
database_1.prismaAdmin.contextEnhancement.count({
|
|
243
|
+
where: { tenantId, projectId, sourceType: 'API_SPEC', status: { in: ['DIFF_READY', 'PENDING'] } },
|
|
244
|
+
}),
|
|
245
|
+
]);
|
|
246
|
+
const project = await database_1.prismaAdmin.project.findFirstOrThrow({
|
|
247
|
+
where: { id: projectId, tenantId },
|
|
248
|
+
select: {
|
|
249
|
+
projectKind: true,
|
|
250
|
+
contextScore: true,
|
|
251
|
+
contextPhase: true,
|
|
252
|
+
contextSourceRequirements: true,
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
const req = project.contextSourceRequirements ?? {};
|
|
256
|
+
const summary = {
|
|
257
|
+
screenshotsDone: screenshotPending === 0,
|
|
258
|
+
figmaDone: figmaPending === 0,
|
|
259
|
+
crawlDone: crawlPending === 0,
|
|
260
|
+
apiSpecDone: apiSpecPending === 0,
|
|
261
|
+
pendingCounts: {
|
|
262
|
+
screenshot: screenshotPending,
|
|
263
|
+
figma: figmaPending,
|
|
264
|
+
crawl: crawlPending,
|
|
265
|
+
apiSpec: apiSpecPending,
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
const allRequiredDone = Object.entries(req).every(([src, level]) => {
|
|
269
|
+
if (level !== 'REQUIRED')
|
|
270
|
+
return true;
|
|
271
|
+
const key = src === 'apiSpec' ? 'apiSpecDone' : src + 'Done';
|
|
272
|
+
return summary[key] === true;
|
|
273
|
+
});
|
|
274
|
+
const projectKind = project.projectKind ?? 'WEB';
|
|
275
|
+
const contextScore = project.contextScore ?? 0;
|
|
276
|
+
const currentPhase = project.contextPhase ?? 'PENDING_CONTEXT';
|
|
277
|
+
const threshold = CONTEXT_THRESHOLDS[projectKind] ?? 80;
|
|
278
|
+
const totalPending = screenshotPending + figmaPending + crawlPending + apiSpecPending;
|
|
279
|
+
const currentIdx = PHASE_ORDER.indexOf(currentPhase);
|
|
280
|
+
let newPhase = currentPhase;
|
|
281
|
+
if (currentPhase === 'CONTEXT_ENRICHED' &&
|
|
282
|
+
totalPending > 0) {
|
|
283
|
+
newPhase = 'DIFF_PENDING_APPROVAL';
|
|
284
|
+
}
|
|
285
|
+
else if (currentIdx <= PHASE_ORDER.indexOf('ALL_DIFFS_APPROVED') &&
|
|
286
|
+
allRequiredDone &&
|
|
287
|
+
totalPending === 0) {
|
|
288
|
+
newPhase = 'ALL_DIFFS_APPROVED';
|
|
289
|
+
}
|
|
290
|
+
if (newPhase === 'ALL_DIFFS_APPROVED' && contextScore >= threshold) {
|
|
291
|
+
newPhase = 'READY_FOR_SCRIPT_GEN';
|
|
292
|
+
}
|
|
293
|
+
// Never regress to an earlier phase
|
|
294
|
+
const newIdx = PHASE_ORDER.indexOf(newPhase);
|
|
295
|
+
const finalPhase = newIdx > currentIdx ? newPhase : currentPhase;
|
|
296
|
+
await database_1.prismaAdmin.project.update({
|
|
297
|
+
where: { id: projectId },
|
|
298
|
+
data: {
|
|
299
|
+
approvalGateSummary: summary,
|
|
300
|
+
contextPhase: finalPhase,
|
|
301
|
+
},
|
|
302
|
+
});
|
|
303
|
+
return { ...summary, allRequiredDone, contextPhase: finalPhase };
|
|
304
|
+
}
|
|
305
|
+
// ─── 5. computeCoverageGaps ───────────────────────────────────────────────────
|
|
306
|
+
async function computeCoverageGaps(tenantId, projectId) {
|
|
307
|
+
const [elements, testCases] = await Promise.all([
|
|
308
|
+
database_1.prismaAdmin.element.findMany({
|
|
309
|
+
where: { tenantId, projectId },
|
|
310
|
+
take: 500,
|
|
311
|
+
}),
|
|
312
|
+
database_1.prismaAdmin.testCase.findMany({
|
|
313
|
+
where: { tenantId, projectId },
|
|
314
|
+
select: { id: true, groundedOn: true, steps: true },
|
|
315
|
+
}),
|
|
316
|
+
]);
|
|
317
|
+
// Build set of element IDs referenced by any test case
|
|
318
|
+
const coveredElementIds = new Set();
|
|
319
|
+
for (const tc of testCases) {
|
|
320
|
+
const groundedOn = (() => {
|
|
321
|
+
try {
|
|
322
|
+
const raw = tc.groundedOn;
|
|
323
|
+
if (Array.isArray(raw))
|
|
324
|
+
return raw;
|
|
325
|
+
if (typeof raw === 'string')
|
|
326
|
+
return JSON.parse(raw);
|
|
327
|
+
return [];
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
return [];
|
|
331
|
+
}
|
|
332
|
+
})();
|
|
333
|
+
for (const id of groundedOn)
|
|
334
|
+
coveredElementIds.add(id);
|
|
335
|
+
}
|
|
336
|
+
// Find interactive elements not covered by any test case
|
|
337
|
+
const uncoveredElements = elements.filter((e) => !coveredElementIds.has(e.id) &&
|
|
338
|
+
e.role != null &&
|
|
339
|
+
!NON_INTERACTIVE_ROLES.has(e.role.toLowerCase()));
|
|
340
|
+
// Upsert BugSignal for each uncovered element
|
|
341
|
+
for (const element of uncoveredElements) {
|
|
342
|
+
await database_1.prismaAdmin.bugSignal.upsert({
|
|
343
|
+
where: {
|
|
344
|
+
tenantId_elementId_signalType: {
|
|
345
|
+
tenantId,
|
|
346
|
+
elementId: element.id,
|
|
347
|
+
signalType: 'ELEMENT_NOT_IN_TESTS',
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
update: {
|
|
351
|
+
updatedAt: new Date(),
|
|
352
|
+
},
|
|
353
|
+
create: {
|
|
354
|
+
tenantId,
|
|
355
|
+
projectId,
|
|
356
|
+
elementId: element.id,
|
|
357
|
+
signalType: 'ELEMENT_NOT_IN_TESTS',
|
|
358
|
+
severity: 'LOW',
|
|
359
|
+
title: `UI element not covered: ${element.role ?? 'unknown'}/${element.meaning ?? element.text ?? element.id}`,
|
|
360
|
+
status: 'OPEN',
|
|
361
|
+
createdAt: new Date(),
|
|
362
|
+
updatedAt: new Date(),
|
|
363
|
+
},
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
return { gapsFound: uncoveredElements.length };
|
|
367
|
+
}
|
|
368
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
369
|
+
/**
|
|
370
|
+
* Find test case IDs whose groundedOn array contains any of the given element IDs.
|
|
371
|
+
*/
|
|
372
|
+
async function findAffectedTestCases(tenantId, projectId, elementIds) {
|
|
373
|
+
if (elementIds.length === 0)
|
|
374
|
+
return [];
|
|
375
|
+
const testCases = await database_1.prismaAdmin.testCase.findMany({
|
|
376
|
+
where: { tenantId, projectId },
|
|
377
|
+
select: { id: true, groundedOn: true },
|
|
378
|
+
});
|
|
379
|
+
const affected = [];
|
|
380
|
+
for (const tc of testCases) {
|
|
381
|
+
const groundedOn = (() => {
|
|
382
|
+
try {
|
|
383
|
+
const raw = tc.groundedOn;
|
|
384
|
+
if (Array.isArray(raw))
|
|
385
|
+
return raw;
|
|
386
|
+
if (typeof raw === 'string')
|
|
387
|
+
return JSON.parse(raw);
|
|
388
|
+
return [];
|
|
389
|
+
}
|
|
390
|
+
catch {
|
|
391
|
+
return [];
|
|
392
|
+
}
|
|
393
|
+
})();
|
|
394
|
+
if (elementIds.some((id) => groundedOn.includes(id))) {
|
|
395
|
+
affected.push(tc.id);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return affected;
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Compute diff severity based on how many test cases are affected.
|
|
402
|
+
* > 5 → CRITICAL
|
|
403
|
+
* 1-5 → SIGNIFICANT
|
|
404
|
+
* 0 → MINOR (auto-approve as NONE would hide it; keep MINOR for awareness)
|
|
405
|
+
*/
|
|
406
|
+
function computeSeverity(affectedCount) {
|
|
407
|
+
if (affectedCount > 5)
|
|
408
|
+
return 'CRITICAL';
|
|
409
|
+
if (affectedCount >= 1)
|
|
410
|
+
return 'SIGNIFICANT';
|
|
411
|
+
return 'NONE';
|
|
412
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ZeTa-AI Test Data Generation Agent
|
|
3
|
+
*
|
|
4
|
+
* Field-schema grounded: loads typed AppField catalog before generating.
|
|
5
|
+
* If all grounded elements are non-data-bearing (buttons/links/toggles)
|
|
6
|
+
* → returns interaction variations, NOT fabricated field:value datasets.
|
|
7
|
+
*
|
|
8
|
+
* Four generation modes:
|
|
9
|
+
* FORM — ≥1 data-bearing field → typed, constraint-aware parameterized datasets
|
|
10
|
+
* STATE — all non-data-bearing → precondition-state datasets
|
|
11
|
+
* VOLUME — PERFORMANCE / LOAD kinds → data-setup specs (row counts, concurrency, seed)
|
|
12
|
+
* SKIP — kinds not suited to any parameterized data (EXPLORATORY, MUTATION, AB_TEST, etc.)
|
|
13
|
+
*/
|
|
14
|
+
import type { LLMConfig } from './llm.js';
|
|
15
|
+
export declare function scheduleSkillFileRefresh(tenantId: string, projectId: string, delayMs?: number): void;
|
|
16
|
+
export declare function generateTestData(opts: {
|
|
17
|
+
tenantId: string;
|
|
18
|
+
projectId: string;
|
|
19
|
+
testCaseId: string;
|
|
20
|
+
jobId?: string;
|
|
21
|
+
targetCount?: number;
|
|
22
|
+
}): Promise<{
|
|
23
|
+
dataSets: never[];
|
|
24
|
+
saved: never[];
|
|
25
|
+
skipped: boolean;
|
|
26
|
+
reason: string;
|
|
27
|
+
mode?: undefined;
|
|
28
|
+
source?: undefined;
|
|
29
|
+
} | {
|
|
30
|
+
dataSets: {
|
|
31
|
+
label: string;
|
|
32
|
+
category: string;
|
|
33
|
+
values: Record<string, string>;
|
|
34
|
+
expectedOutcome: string;
|
|
35
|
+
}[];
|
|
36
|
+
saved: {
|
|
37
|
+
tenantId: string;
|
|
38
|
+
id: string;
|
|
39
|
+
projectId: string;
|
|
40
|
+
source: string;
|
|
41
|
+
kind: string;
|
|
42
|
+
testCaseId: string;
|
|
43
|
+
payload: import("@prisma/client/runtime/library").JsonValue;
|
|
44
|
+
supersededAt: Date | null;
|
|
45
|
+
}[];
|
|
46
|
+
mode: string;
|
|
47
|
+
source: string;
|
|
48
|
+
skipped?: undefined;
|
|
49
|
+
reason?: undefined;
|
|
50
|
+
}>;
|
|
51
|
+
/**
|
|
52
|
+
* Generates test data for up to 5 test cases in a single LLM call.
|
|
53
|
+
*
|
|
54
|
+
* Caller is responsible for chunking: pass batches of up to 5 cases.
|
|
55
|
+
* Returns a Map keyed by the original case id, value is the dataSets array
|
|
56
|
+
* using the same per-dataset shape as generateTestData().
|
|
57
|
+
*
|
|
58
|
+
* Cases that the LLM omits from its response are absent from the returned Map
|
|
59
|
+
* (caller should fall back to generateTestData() for those).
|
|
60
|
+
*/
|
|
61
|
+
export declare function generateTestDataBatch(cases: Array<{
|
|
62
|
+
id: string;
|
|
63
|
+
title: string;
|
|
64
|
+
steps: string[];
|
|
65
|
+
expectedResult: string;
|
|
66
|
+
preconditions?: string;
|
|
67
|
+
testDataInline?: object;
|
|
68
|
+
}>, options?: {
|
|
69
|
+
llmCfg?: LLMConfig;
|
|
70
|
+
skillFile?: string;
|
|
71
|
+
tenantId?: string;
|
|
72
|
+
projectId?: string;
|
|
73
|
+
}): Promise<Map<string, unknown>>;
|
|
74
|
+
/**
|
|
75
|
+
* Shape used by parseCsvToDatasets / mergeExternalDatasets.
|
|
76
|
+
* Matches the inline dataset shape used throughout this module.
|
|
77
|
+
*/
|
|
78
|
+
type CsvDataSet = {
|
|
79
|
+
label: string;
|
|
80
|
+
category: string;
|
|
81
|
+
values: Record<string, string>;
|
|
82
|
+
expectedOutcome: string;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Parse a CSV string (with header row) into VALID/INVALID/EDGE test datasets.
|
|
86
|
+
* Column names map to dataset field names.
|
|
87
|
+
* Rows are assigned categories based on a 'category' column (if present) or
|
|
88
|
+
* positionally: first 40% VALID, next 40% INVALID, last 20% EDGE.
|
|
89
|
+
*
|
|
90
|
+
* @param csvContent - Raw CSV string content
|
|
91
|
+
* @param fieldMap - Optional alias map: { csvColumnName: normalizedFieldName }
|
|
92
|
+
* @returns Array of dataset objects ready for use in test data generation
|
|
93
|
+
*/
|
|
94
|
+
export declare function parseCsvToDatasets(csvContent: string, fieldMap?: Record<string, string>): CsvDataSet[];
|
|
95
|
+
/**
|
|
96
|
+
* Merge externally-provided datasets (from CSV upload) with LLM-generated datasets.
|
|
97
|
+
* External datasets take priority for matching categories.
|
|
98
|
+
* Deduplicates by category label.
|
|
99
|
+
*/
|
|
100
|
+
export declare function mergeExternalDatasets(generated: CsvDataSet[], external: CsvDataSet[]): CsvDataSet[];
|
|
101
|
+
export {};
|