@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,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ZeTa-AI Bug Identification Engine — P7
|
|
3
|
+
*
|
|
4
|
+
* Four entry points:
|
|
5
|
+
* computeRequirementCoverage — LLM-based AC coverage check → RequirementCoverage + BugSignal rows
|
|
6
|
+
* analyzeTestRunFailures — cluster failed TestResults by error type → BugSignal rows
|
|
7
|
+
* promoteVisualRegressionToBugSignal — elevate FLAG/BLOCK VisualDiffs to BugSignals
|
|
8
|
+
* promoteBugSignalToJira — push a BugSignal to Jira via stored connector credentials
|
|
9
|
+
*
|
|
10
|
+
* NOTE: BugSignal and RequirementCoverage are new Prisma models (pending schema migration).
|
|
11
|
+
* prismaAdmin is cast to `any` for those two models until the migration is applied.
|
|
12
|
+
*/
|
|
13
|
+
export declare function computeRequirementCoverage(tenantId: string, projectId: string): Promise<{
|
|
14
|
+
requirementsChecked: number;
|
|
15
|
+
gapsFound: number;
|
|
16
|
+
}>;
|
|
17
|
+
export declare function analyzeTestRunFailures(tenantId: string, projectId: string, runId: string): Promise<{
|
|
18
|
+
failuresAnalyzed: number;
|
|
19
|
+
signalsCreated: number;
|
|
20
|
+
}>;
|
|
21
|
+
export declare function promoteVisualRegressionToBugSignal(tenantId: string, projectId: string, visualDiffId: string): Promise<any | null>;
|
|
22
|
+
export declare function promoteBugSignalToJira(tenantId: string, projectId: string, signalId: string): Promise<{
|
|
23
|
+
jiraIssueKey: string;
|
|
24
|
+
jiraIssueUrl: string;
|
|
25
|
+
}>;
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* ZeTa-AI Bug Identification Engine — P7
|
|
4
|
+
*
|
|
5
|
+
* Four entry points:
|
|
6
|
+
* computeRequirementCoverage — LLM-based AC coverage check → RequirementCoverage + BugSignal rows
|
|
7
|
+
* analyzeTestRunFailures — cluster failed TestResults by error type → BugSignal rows
|
|
8
|
+
* promoteVisualRegressionToBugSignal — elevate FLAG/BLOCK VisualDiffs to BugSignals
|
|
9
|
+
* promoteBugSignalToJira — push a BugSignal to Jira via stored connector credentials
|
|
10
|
+
*
|
|
11
|
+
* NOTE: BugSignal and RequirementCoverage are new Prisma models (pending schema migration).
|
|
12
|
+
* prismaAdmin is cast to `any` for those two models until the migration is applied.
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.computeRequirementCoverage = computeRequirementCoverage;
|
|
16
|
+
exports.analyzeTestRunFailures = analyzeTestRunFailures;
|
|
17
|
+
exports.promoteVisualRegressionToBugSignal = promoteVisualRegressionToBugSignal;
|
|
18
|
+
exports.promoteBugSignalToJira = promoteBugSignalToJira;
|
|
19
|
+
const database_1 = require("@taodigital/database");
|
|
20
|
+
const core_1 = require("@taodigital/core");
|
|
21
|
+
const llm_js_1 = require("./llm.js");
|
|
22
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
23
|
+
function classifyError(msg) {
|
|
24
|
+
const m = msg?.toLowerCase() ?? '';
|
|
25
|
+
if (m.includes('locator') || m.includes('selector') || m.includes('element not found') || m.includes('waitfor'))
|
|
26
|
+
return 'LOCATOR_ERROR';
|
|
27
|
+
if (m.includes('expected') || m.includes('assert') || m.includes('to equal') || m.includes('to be'))
|
|
28
|
+
return 'ASSERTION_ERROR';
|
|
29
|
+
if (m.includes('navigation') || m.includes('net::err') || m.includes('timeout'))
|
|
30
|
+
return 'NAVIGATION_ERROR';
|
|
31
|
+
if (m.includes('network') || m.includes('404') || m.includes('500') || m.includes('fetch'))
|
|
32
|
+
return 'NETWORK_ERROR';
|
|
33
|
+
return 'UNKNOWN_ERROR';
|
|
34
|
+
}
|
|
35
|
+
const db = database_1.prismaAdmin;
|
|
36
|
+
// ─── Function 1: computeRequirementCoverage ───────────────────────────────────
|
|
37
|
+
async function computeRequirementCoverage(tenantId, projectId) {
|
|
38
|
+
const requirements = await database_1.prismaAdmin.requirement.findMany({
|
|
39
|
+
where: { tenantId, projectId },
|
|
40
|
+
take: 200,
|
|
41
|
+
});
|
|
42
|
+
let gapsFound = 0;
|
|
43
|
+
for (const req of requirements) {
|
|
44
|
+
const testCases = await database_1.prismaAdmin.testCase.findMany({
|
|
45
|
+
where: {
|
|
46
|
+
tenantId,
|
|
47
|
+
projectId,
|
|
48
|
+
OR: [
|
|
49
|
+
{ traceTo: req.id },
|
|
50
|
+
{ traceToExtra: { string_contains: req.id } },
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
select: { id: true, steps: true, title: true },
|
|
54
|
+
});
|
|
55
|
+
const acceptanceCriteria = Array.isArray(req.acceptanceCriteria)
|
|
56
|
+
? req.acceptanceCriteria
|
|
57
|
+
: [];
|
|
58
|
+
if (acceptanceCriteria.length === 0)
|
|
59
|
+
continue;
|
|
60
|
+
const promptText = (await (0, core_1.getAgentPrompt)('requirement-coverage-check').catch(() => undefined)) ??
|
|
61
|
+
'Given this acceptance criterion and test steps, return JSON: { covered: boolean, coverageScore: number, missingAspects: string[] }';
|
|
62
|
+
const acScores = [];
|
|
63
|
+
for (const ac of acceptanceCriteria) {
|
|
64
|
+
if (testCases.length === 0) {
|
|
65
|
+
acScores.push(0);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const steps = testCases.flatMap((tc) => Array.isArray(tc.steps) ? tc.steps : []);
|
|
69
|
+
let score = 0;
|
|
70
|
+
try {
|
|
71
|
+
const { text: raw } = await (0, llm_js_1.askTracked)(promptText, `Acceptance criterion: ${ac}\n\nTest steps: ${JSON.stringify(steps)}`);
|
|
72
|
+
const parsed = JSON.parse(raw.match(/\{[\s\S]*\}/)?.[0] ?? '{"coverageScore":0}');
|
|
73
|
+
score = typeof parsed.coverageScore === 'number' ? parsed.coverageScore : 0;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
score = 0;
|
|
77
|
+
}
|
|
78
|
+
acScores.push(score);
|
|
79
|
+
}
|
|
80
|
+
const avgScore = acScores.length > 0
|
|
81
|
+
? acScores.reduce((a, b) => a + b, 0) / acScores.length
|
|
82
|
+
: 0;
|
|
83
|
+
await db.requirementCoverage.upsert({
|
|
84
|
+
where: {
|
|
85
|
+
tenantId_requirementId: { tenantId, requirementId: req.id },
|
|
86
|
+
},
|
|
87
|
+
create: {
|
|
88
|
+
tenantId,
|
|
89
|
+
projectId,
|
|
90
|
+
requirementId: req.id,
|
|
91
|
+
testCaseIds: testCases.map((t) => t.id),
|
|
92
|
+
coverageScore: avgScore,
|
|
93
|
+
uncoveredAcIds: [],
|
|
94
|
+
},
|
|
95
|
+
update: {
|
|
96
|
+
testCaseIds: testCases.map((t) => t.id),
|
|
97
|
+
coverageScore: avgScore,
|
|
98
|
+
uncoveredAcIds: [],
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
if (avgScore < 0.5) {
|
|
102
|
+
await db.bugSignal.upsert({
|
|
103
|
+
where: {
|
|
104
|
+
tenantId_sourceRefHash: {
|
|
105
|
+
tenantId,
|
|
106
|
+
sourceRefHash: `REQUIREMENT_GAP:${req.id}`,
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
create: {
|
|
110
|
+
tenantId,
|
|
111
|
+
projectId,
|
|
112
|
+
signalType: 'REQUIREMENT_GAP',
|
|
113
|
+
severity: avgScore < 0.2 ? 'HIGH' : 'MEDIUM',
|
|
114
|
+
title: `Requirement gap: ${req.title}`,
|
|
115
|
+
sourceRef: { requirementId: req.id },
|
|
116
|
+
sourceRefHash: `REQUIREMENT_GAP:${req.id}`,
|
|
117
|
+
},
|
|
118
|
+
update: {
|
|
119
|
+
severity: avgScore < 0.2 ? 'HIGH' : 'MEDIUM',
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
gapsFound += 1;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return { requirementsChecked: requirements.length, gapsFound };
|
|
126
|
+
}
|
|
127
|
+
// ─── Function 2: analyzeTestRunFailures ──────────────────────────────────────
|
|
128
|
+
async function analyzeTestRunFailures(tenantId, projectId, runId) {
|
|
129
|
+
const failedResults = await database_1.prismaAdmin.testResult.findMany({
|
|
130
|
+
where: { runId, tenantId, status: 'FAILED' },
|
|
131
|
+
include: {
|
|
132
|
+
testCase: { select: { id: true, priority: true, title: true } },
|
|
133
|
+
},
|
|
134
|
+
take: 200,
|
|
135
|
+
});
|
|
136
|
+
// Group by error type
|
|
137
|
+
const groups = new Map();
|
|
138
|
+
for (const result of failedResults) {
|
|
139
|
+
const errorType = classifyError(result.error ?? '');
|
|
140
|
+
if (!groups.has(errorType))
|
|
141
|
+
groups.set(errorType, []);
|
|
142
|
+
groups.get(errorType).push(result);
|
|
143
|
+
}
|
|
144
|
+
const priorityToSeverity = {
|
|
145
|
+
CRITICAL: 'CRITICAL',
|
|
146
|
+
HIGH: 'HIGH',
|
|
147
|
+
};
|
|
148
|
+
let signalsCreated = 0;
|
|
149
|
+
for (const [errorType, group] of groups) {
|
|
150
|
+
// Determine highest priority in the group
|
|
151
|
+
const hasCritical = group.some((r) => r.testCase?.priority === 'CRITICAL');
|
|
152
|
+
const hasHigh = group.some((r) => r.testCase?.priority === 'HIGH');
|
|
153
|
+
const severity = hasCritical
|
|
154
|
+
? 'CRITICAL'
|
|
155
|
+
: hasHigh
|
|
156
|
+
? 'HIGH'
|
|
157
|
+
: 'MEDIUM';
|
|
158
|
+
await db.bugSignal.create({
|
|
159
|
+
data: {
|
|
160
|
+
tenantId,
|
|
161
|
+
projectId,
|
|
162
|
+
signalType: 'RUNNER_FAILURE',
|
|
163
|
+
severity,
|
|
164
|
+
title: `${errorType} failures in run ${runId.slice(-8)}`,
|
|
165
|
+
sourceRef: {
|
|
166
|
+
runId,
|
|
167
|
+
testResultIds: group.map((r) => r.id),
|
|
168
|
+
},
|
|
169
|
+
affectedTestCaseIds: group.map((r) => r.testCaseId),
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
signalsCreated += 1;
|
|
173
|
+
}
|
|
174
|
+
return { failuresAnalyzed: failedResults.length, signalsCreated };
|
|
175
|
+
}
|
|
176
|
+
// ─── Function 3: promoteVisualRegressionToBugSignal ──────────────────────────
|
|
177
|
+
async function promoteVisualRegressionToBugSignal(tenantId, projectId, visualDiffId) {
|
|
178
|
+
const visualDiff = await database_1.prismaAdmin.visualDiff.findFirst({
|
|
179
|
+
where: { id: visualDiffId, tenantId },
|
|
180
|
+
});
|
|
181
|
+
if (!visualDiff)
|
|
182
|
+
return null;
|
|
183
|
+
if (visualDiff.bugSignalVisualDiffId)
|
|
184
|
+
return null;
|
|
185
|
+
if (visualDiff.recommendation === 'FLAG' ||
|
|
186
|
+
visualDiff.recommendation === 'BLOCK') {
|
|
187
|
+
const bugReportDraft = visualDiff.bugReportDraft;
|
|
188
|
+
const signal = await db.bugSignal.create({
|
|
189
|
+
data: {
|
|
190
|
+
tenantId,
|
|
191
|
+
projectId,
|
|
192
|
+
signalType: 'VISUAL_REGRESSION',
|
|
193
|
+
severity: visualDiff.recommendation === 'BLOCK' ? 'CRITICAL' : 'HIGH',
|
|
194
|
+
title: bugReportDraft?.title ?? 'Visual regression detected',
|
|
195
|
+
description: bugReportDraft?.description ?? null,
|
|
196
|
+
sourceRef: { visualDiffId },
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
await database_1.prismaAdmin.visualDiff.update({
|
|
200
|
+
where: { id: visualDiffId },
|
|
201
|
+
data: { bugSignalVisualDiffId: signal.id },
|
|
202
|
+
});
|
|
203
|
+
return signal;
|
|
204
|
+
}
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
// ─── Function 4: promoteBugSignalToJira ──────────────────────────────────────
|
|
208
|
+
const severityToJiraPriority = {
|
|
209
|
+
CRITICAL: 'Highest',
|
|
210
|
+
HIGH: 'High',
|
|
211
|
+
MEDIUM: 'Medium',
|
|
212
|
+
LOW: 'Low',
|
|
213
|
+
};
|
|
214
|
+
async function promoteBugSignalToJira(tenantId, projectId, signalId) {
|
|
215
|
+
const signal = await db.bugSignal.findFirstOrThrow({
|
|
216
|
+
where: { id: signalId, tenantId },
|
|
217
|
+
});
|
|
218
|
+
const connector = await database_1.prismaAdmin.connector.findFirst({
|
|
219
|
+
where: { projectId, kind: 'JIRA', tenantId },
|
|
220
|
+
});
|
|
221
|
+
if (!connector) {
|
|
222
|
+
throw new Error('No Jira connector configured for this project');
|
|
223
|
+
}
|
|
224
|
+
const cfg = (connector.config ?? {});
|
|
225
|
+
const baseUrl = (cfg.baseUrl ?? '').replace(/\/$/, '');
|
|
226
|
+
const projectKey = cfg.projectKey ?? projectId.slice(0, 8).toUpperCase();
|
|
227
|
+
const payload = {
|
|
228
|
+
fields: {
|
|
229
|
+
project: { key: projectKey },
|
|
230
|
+
summary: signal.title,
|
|
231
|
+
description: {
|
|
232
|
+
version: 1,
|
|
233
|
+
type: 'doc',
|
|
234
|
+
content: [
|
|
235
|
+
{
|
|
236
|
+
type: 'paragraph',
|
|
237
|
+
content: [
|
|
238
|
+
{
|
|
239
|
+
type: 'text',
|
|
240
|
+
text: signal.description ?? signal.title,
|
|
241
|
+
},
|
|
242
|
+
],
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
},
|
|
246
|
+
issuetype: { name: 'Bug' },
|
|
247
|
+
priority: { name: severityToJiraPriority[signal.severity] ?? 'Medium' },
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
const apiToken = cfg.apiToken ?? connector.secretRef;
|
|
251
|
+
const userEmail = cfg.userEmail ?? '';
|
|
252
|
+
const res = await fetch(`${baseUrl}/rest/api/3/issue`, {
|
|
253
|
+
method: 'POST',
|
|
254
|
+
headers: {
|
|
255
|
+
'Content-Type': 'application/json',
|
|
256
|
+
Authorization: `Basic ${Buffer.from(`${userEmail}:${apiToken}`).toString('base64')}`,
|
|
257
|
+
},
|
|
258
|
+
body: JSON.stringify(payload),
|
|
259
|
+
});
|
|
260
|
+
if (!res.ok) {
|
|
261
|
+
const body = await res.text().catch(() => '');
|
|
262
|
+
throw new Error(`Jira API error ${res.status}: ${body.slice(0, 200)}`);
|
|
263
|
+
}
|
|
264
|
+
const data = (await res.json());
|
|
265
|
+
return {
|
|
266
|
+
jiraIssueKey: data.key,
|
|
267
|
+
jiraIssueUrl: `${baseUrl}/browse/${data.key}`,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ExtractedTest } from './repo-scan-agent.js';
|
|
2
|
+
export interface MappingResult {
|
|
3
|
+
testCaseId: string;
|
|
4
|
+
status: 'MAPPED' | 'UNMAPPED' | 'AMBIGUOUS';
|
|
5
|
+
matchedFilePath?: string;
|
|
6
|
+
scriptTestName?: string;
|
|
7
|
+
lineNumber?: number;
|
|
8
|
+
confidence: number;
|
|
9
|
+
matchMethod: 'EXACT' | 'NORMALIZED' | 'FUZZY' | 'AI' | 'UNMAPPED';
|
|
10
|
+
}
|
|
11
|
+
export declare function buildCoverageMap(testCases: Array<{
|
|
12
|
+
id: string;
|
|
13
|
+
title: string;
|
|
14
|
+
ref?: string | null;
|
|
15
|
+
}>, extractedTests: ExtractedTest[], tenantId: string): Promise<MappingResult[]>;
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildCoverageMap = buildCoverageMap;
|
|
4
|
+
const core_1 = require("@taodigital/core");
|
|
5
|
+
const llm_js_1 = require("./llm.js");
|
|
6
|
+
const llm_config_js_1 = require("./llm-config.js");
|
|
7
|
+
const app_brain_1 = require("@taodigital/app-brain");
|
|
8
|
+
function normalize(s) {
|
|
9
|
+
return s.toLowerCase().replace(/[^\w\s]/g, '').replace(/\s+/g, ' ').trim();
|
|
10
|
+
}
|
|
11
|
+
function levenshtein(a, b) {
|
|
12
|
+
const m = a.length;
|
|
13
|
+
const n = b.length;
|
|
14
|
+
const dp = Array.from({ length: m + 1 }, (_, i) => Array.from({ length: n + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0)));
|
|
15
|
+
for (let i = 1; i <= m; i++) {
|
|
16
|
+
for (let j = 1; j <= n; j++) {
|
|
17
|
+
if (a[i - 1] === b[j - 1]) {
|
|
18
|
+
dp[i][j] = dp[i - 1][j - 1];
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
dp[i][j] = 1 + Math.min(dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return dp[m][n];
|
|
26
|
+
}
|
|
27
|
+
function similarity(a, b) {
|
|
28
|
+
if (a.length === 0 && b.length === 0)
|
|
29
|
+
return 1.0;
|
|
30
|
+
const maxLen = Math.max(a.length, b.length);
|
|
31
|
+
if (maxLen === 0)
|
|
32
|
+
return 1.0;
|
|
33
|
+
return 1 - levenshtein(a, b) / maxLen;
|
|
34
|
+
}
|
|
35
|
+
function assignStatus(confidence) {
|
|
36
|
+
if (confidence >= 0.95)
|
|
37
|
+
return 'MAPPED';
|
|
38
|
+
if (confidence >= 0.70)
|
|
39
|
+
return 'AMBIGUOUS';
|
|
40
|
+
return 'UNMAPPED';
|
|
41
|
+
}
|
|
42
|
+
function makeMapped(testCaseId, et, confidence, matchMethod) {
|
|
43
|
+
return {
|
|
44
|
+
testCaseId,
|
|
45
|
+
status: assignStatus(confidence),
|
|
46
|
+
matchedFilePath: et.filePath,
|
|
47
|
+
scriptTestName: et.name,
|
|
48
|
+
lineNumber: et.lineNumber,
|
|
49
|
+
confidence,
|
|
50
|
+
matchMethod,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function makeUnmapped(testCaseId) {
|
|
54
|
+
return {
|
|
55
|
+
testCaseId,
|
|
56
|
+
status: 'UNMAPPED',
|
|
57
|
+
confidence: 0,
|
|
58
|
+
matchMethod: 'UNMAPPED',
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
async function runAIPass(unmatched, pool, tenantId) {
|
|
62
|
+
const results = new Map();
|
|
63
|
+
const llmCfg = await (0, llm_config_js_1.getTaskAgentLLMConfig)(tenantId, 'coverage_mapping');
|
|
64
|
+
const BATCH_SIZE = 20;
|
|
65
|
+
for (let i = 0; i < unmatched.length; i += BATCH_SIZE) {
|
|
66
|
+
const batch = unmatched.slice(i, i + BATCH_SIZE);
|
|
67
|
+
const testCasesText = batch
|
|
68
|
+
.map((tc) => `- id: "${tc.id}", title: "${tc.title}"`)
|
|
69
|
+
.join('\n');
|
|
70
|
+
const extractedNamesText = pool.map((et) => `- "${et.name}"`).join('\n');
|
|
71
|
+
const SYSTEM_DEFAULT = 'You are a test mapping assistant. Given a list of test case titles and extracted test names from source code, ' +
|
|
72
|
+
'find the best match for each test case. Return a JSON array only, no explanation.';
|
|
73
|
+
const system = await (0, core_1.getAgentPrompt)('coverage-map', undefined, tenantId) ?? SYSTEM_DEFAULT;
|
|
74
|
+
const user = `Test cases:\n${testCasesText}\n\nExtracted test names:\n${extractedNamesText}\n\n` +
|
|
75
|
+
'Return a JSON array where each element has: { "testCaseId": string, "matchedName": string | null, "confidence": number }. ' +
|
|
76
|
+
'confidence is 0.0–1.0. Use null for matchedName if no reasonable match exists.';
|
|
77
|
+
let parsed = [];
|
|
78
|
+
try {
|
|
79
|
+
const seed = (0, llm_js_1.deterministicSeed)(`${tenantId}:cov-map:${batch.map((tc) => tc.id).join('|')}`);
|
|
80
|
+
const res = await (0, llm_js_1.askTracked)(system, user, llmCfg, 2048, undefined, { temperature: 0, seed });
|
|
81
|
+
const raw = res.text.trim();
|
|
82
|
+
const jsonMatch = raw.match(/\[[\s\S]*\]/);
|
|
83
|
+
if (jsonMatch) {
|
|
84
|
+
parsed = JSON.parse(jsonMatch[0]);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// If AI fails, leave batch unmatched
|
|
89
|
+
}
|
|
90
|
+
for (const item of parsed) {
|
|
91
|
+
if (!item.matchedName || item.confidence < 0.70) {
|
|
92
|
+
results.set(item.testCaseId, makeUnmapped(item.testCaseId));
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const et = pool.find((e) => e.name === item.matchedName);
|
|
96
|
+
if (!et) {
|
|
97
|
+
results.set(item.testCaseId, makeUnmapped(item.testCaseId));
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const confidence = Math.min(item.confidence, 0.94); // AI matches cap at AMBIGUOUS
|
|
101
|
+
results.set(item.testCaseId, {
|
|
102
|
+
testCaseId: item.testCaseId,
|
|
103
|
+
status: 'AMBIGUOUS',
|
|
104
|
+
matchedFilePath: et.filePath,
|
|
105
|
+
scriptTestName: et.name,
|
|
106
|
+
lineNumber: et.lineNumber,
|
|
107
|
+
confidence,
|
|
108
|
+
matchMethod: 'AI',
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
// Mark any not returned by AI as UNMAPPED
|
|
112
|
+
for (const tc of batch) {
|
|
113
|
+
if (!results.has(tc.id)) {
|
|
114
|
+
results.set(tc.id, makeUnmapped(tc.id));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return results;
|
|
119
|
+
}
|
|
120
|
+
async function buildCoverageMap(testCases, extractedTests, tenantId) {
|
|
121
|
+
const finalResults = new Map();
|
|
122
|
+
// Separate dynamic tests — skip all passes
|
|
123
|
+
const dynamic = testCases.filter((tc) => {
|
|
124
|
+
const et = extractedTests.find((e) => e.name.trim() === tc.title.trim());
|
|
125
|
+
return et?.dynamic === true;
|
|
126
|
+
});
|
|
127
|
+
for (const tc of dynamic) {
|
|
128
|
+
finalResults.set(tc.id, makeUnmapped(tc.id));
|
|
129
|
+
}
|
|
130
|
+
let remaining = testCases.filter((tc) => !finalResults.has(tc.id));
|
|
131
|
+
// Pool is mutable — remove used extracted tests after each pass
|
|
132
|
+
const usedExtracted = new Set();
|
|
133
|
+
const availablePool = () => extractedTests.filter((et) => !usedExtracted.has(et.name) && et.dynamic !== true);
|
|
134
|
+
// Ref-based matching (before Pass 1)
|
|
135
|
+
const afterRef = [];
|
|
136
|
+
for (const tc of remaining) {
|
|
137
|
+
if (!tc.ref) {
|
|
138
|
+
afterRef.push(tc);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const pool = availablePool();
|
|
142
|
+
const match = pool.find((et) => et.name.includes(tc.ref));
|
|
143
|
+
if (match) {
|
|
144
|
+
finalResults.set(tc.id, makeMapped(tc.id, match, 1.0, 'EXACT'));
|
|
145
|
+
usedExtracted.add(match.name);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
afterRef.push(tc);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
remaining = afterRef;
|
|
152
|
+
// Pass 1 — EXACT
|
|
153
|
+
const afterPass1 = [];
|
|
154
|
+
for (const tc of remaining) {
|
|
155
|
+
const pool = availablePool();
|
|
156
|
+
const match = pool.find((et) => et.name.trim() === tc.title.trim());
|
|
157
|
+
if (match) {
|
|
158
|
+
finalResults.set(tc.id, makeMapped(tc.id, match, 1.0, 'EXACT'));
|
|
159
|
+
usedExtracted.add(match.name);
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
afterPass1.push(tc);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
remaining = afterPass1;
|
|
166
|
+
// Pass 2 — NORMALIZED
|
|
167
|
+
const afterPass2 = [];
|
|
168
|
+
for (const tc of remaining) {
|
|
169
|
+
const normTitle = normalize(tc.title);
|
|
170
|
+
const pool = availablePool();
|
|
171
|
+
const match = pool.find((et) => et.normalizedName === normTitle);
|
|
172
|
+
if (match) {
|
|
173
|
+
finalResults.set(tc.id, makeMapped(tc.id, match, 0.90, 'NORMALIZED'));
|
|
174
|
+
usedExtracted.add(match.name);
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
afterPass2.push(tc);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
remaining = afterPass2;
|
|
181
|
+
// Pass 3 — FUZZY (Levenshtein, threshold ≥ 0.85)
|
|
182
|
+
const afterPass3 = [];
|
|
183
|
+
for (const tc of remaining) {
|
|
184
|
+
const normTitle = normalize(tc.title);
|
|
185
|
+
const pool = availablePool();
|
|
186
|
+
let best = null;
|
|
187
|
+
let secondBest = null;
|
|
188
|
+
for (const et of pool) {
|
|
189
|
+
const score = similarity(normTitle, et.normalizedName ?? normalize(et.name));
|
|
190
|
+
if (score >= 0.85) {
|
|
191
|
+
if (!best || score > best.score) {
|
|
192
|
+
secondBest = best;
|
|
193
|
+
best = { et, score };
|
|
194
|
+
}
|
|
195
|
+
else if (!secondBest || score > secondBest.score) {
|
|
196
|
+
secondBest = { et, score };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (!best) {
|
|
201
|
+
afterPass3.push(tc);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
// Ambiguous if two candidates are within 0.02 of each other
|
|
205
|
+
if (secondBest && best.score - secondBest.score <= 0.02) {
|
|
206
|
+
// Neither matched — leave for AI pass
|
|
207
|
+
afterPass3.push(tc);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
finalResults.set(tc.id, makeMapped(tc.id, best.et, best.score, 'FUZZY'));
|
|
211
|
+
usedExtracted.add(best.et.name);
|
|
212
|
+
}
|
|
213
|
+
remaining = afterPass3;
|
|
214
|
+
// Pass 3.5 — SEMANTIC (cross-encoder for paraphrases that Levenshtein misses)
|
|
215
|
+
if (remaining.length > 0) {
|
|
216
|
+
const semanticPool = availablePool();
|
|
217
|
+
if (semanticPool.length > 0) {
|
|
218
|
+
const poolTexts = semanticPool.map((et) => et.normalizedName ?? normalize(et.name));
|
|
219
|
+
const afterSemantic = [];
|
|
220
|
+
for (const tc of remaining) {
|
|
221
|
+
try {
|
|
222
|
+
const ranked = await (0, app_brain_1.rerankStrings)(normalize(tc.title), poolTexts, 1, 0);
|
|
223
|
+
const top = ranked[0];
|
|
224
|
+
if (top && top.score >= 0.65) {
|
|
225
|
+
const et = semanticPool[top.index];
|
|
226
|
+
if (et && !usedExtracted.has(et.name)) {
|
|
227
|
+
finalResults.set(tc.id, makeMapped(tc.id, et, Math.min(top.score, 0.89), 'FUZZY'));
|
|
228
|
+
usedExtracted.add(et.name);
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
// reranker unavailable — fall through to AI pass
|
|
235
|
+
}
|
|
236
|
+
afterSemantic.push(tc);
|
|
237
|
+
}
|
|
238
|
+
remaining = afterSemantic;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// Pass 4 — AI
|
|
242
|
+
if (remaining.length > 0) {
|
|
243
|
+
const aiResults = await runAIPass(remaining, availablePool(), tenantId);
|
|
244
|
+
for (const [id, result] of aiResults) {
|
|
245
|
+
finalResults.set(id, result);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// Ensure every test case has a result
|
|
249
|
+
for (const tc of testCases) {
|
|
250
|
+
if (!finalResults.has(tc.id)) {
|
|
251
|
+
finalResults.set(tc.id, makeUnmapped(tc.id));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return testCases.map((tc) => finalResults.get(tc.id));
|
|
255
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ZeTa-AI Crawl Diff Agent
|
|
3
|
+
*
|
|
4
|
+
* Compares DOM elements and navigation flows across two crawl runs to detect
|
|
5
|
+
* structural UI changes. Drives the approval gate state machine that gates
|
|
6
|
+
* script generation until all diffs are reviewed.
|
|
7
|
+
*
|
|
8
|
+
* Public entry points:
|
|
9
|
+
* computeCrawlDiffs — diff elements + flows between two crawl jobs
|
|
10
|
+
* createCrawlContextEnhancement — create ContextEnhancement record for a crawl job
|
|
11
|
+
* approveCrawlDiff — mark a diff approved, flag affected test cases suspect
|
|
12
|
+
* recomputeApprovalGateSummary — recompute contextPhase based on pending diffs
|
|
13
|
+
* computeCoverageGaps — find UI elements not covered by any test case
|
|
14
|
+
*/
|
|
15
|
+
export declare function computeCrawlDiffs(tenantId: string, projectId: string, fromJobId: string, toJobId: string): Promise<{
|
|
16
|
+
diffCount: number;
|
|
17
|
+
criticalCount: number;
|
|
18
|
+
significantCount: number;
|
|
19
|
+
}>;
|
|
20
|
+
export declare function createCrawlContextEnhancement(tenantId: string, projectId: string, toJobId: string, diffs: Array<{
|
|
21
|
+
severity: string;
|
|
22
|
+
}>): Promise<any>;
|
|
23
|
+
export declare function approveCrawlDiff(tenantId: string, projectId: string, diffId: string, reviewedBy?: string): Promise<void>;
|
|
24
|
+
export declare function recomputeApprovalGateSummary(tenantId: string, projectId: string): Promise<object>;
|
|
25
|
+
export declare function computeCoverageGaps(tenantId: string, projectId: string): Promise<{
|
|
26
|
+
gapsFound: number;
|
|
27
|
+
}>;
|