@mknrt/autotests-overkill 1.1.4 → 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 +7 -3
- package/dist/src/cli/runAcceptance.d.ts +2 -1
- package/dist/src/cli/runAcceptance.js +33 -2
- package/dist/src/contracts/toolOutput.d.ts +6 -6
- package/dist/src/contracts/toolOutput.js +8 -4
- package/dist/src/domain/analyzeDiffImpact.d.ts +1 -1
- package/dist/src/domain/analyzeDiffImpact.js +14 -4
- package/dist/src/domain/analyzeTestGaps.d.ts +1 -1
- package/dist/src/domain/analyzeTestGaps.js +13 -1
- package/dist/src/domain/buildRerunScope.d.ts +1 -1
- package/dist/src/domain/buildRerunScope.js +6 -1
- package/dist/src/domain/buildSetupTeardownPlan.d.ts +1 -1
- package/dist/src/domain/findBackendTestContext.d.ts +1 -1
- package/dist/src/domain/findBackendTestContext.js +100 -15
- package/dist/src/domain/findExistingTestAssets.d.ts +1 -1
- package/dist/src/domain/findFrontendContract.d.ts +1 -1
- package/dist/src/domain/findFrontendRuntimeLogic.d.ts +1 -1
- package/dist/src/domain/findTestAreaContext.d.ts +1 -1
- package/dist/src/domain/generateSpecBlueprint.d.ts +1 -1
- package/dist/src/domain/mapFeatureToExistingCoverage.d.ts +1 -1
- package/dist/src/domain/recommendSelectorStrategy.d.ts +1 -1
- package/dist/src/domain/reviewTestDraft.d.ts +26 -0
- package/dist/src/domain/reviewTestDraft.js +226 -0
- package/dist/src/domain/shared.js +15 -2
- package/dist/src/domain/summarizeCiContext.d.ts +1 -1
- package/dist/src/domain/triageFailedRun.d.ts +1 -1
- package/dist/src/domain/triageFailedRun.js +14 -3
- package/dist/src/indexer/extractors/frontendContractExtractor.js +47 -3
- package/dist/src/indexer/refreshPipeline.d.ts +10 -0
- package/dist/src/indexer/refreshPipeline.js +38 -8
- package/dist/src/mcp/registerTools.js +12 -0
- package/dist/src/utils/pathUtils.d.ts +3 -0
- package/dist/src/utils/pathUtils.js +29 -0
- package/docs/audits/2026-04-20-iteration-1.md +251 -0
- package/docs/audits/2026-04-20-iteration-2-comprehensive.md +563 -0
- package/package.json +2 -1
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { unique } from '../utils/pathUtils.js';
|
|
2
|
+
import { analyzeTestGaps } from './analyzeTestGaps.js';
|
|
3
|
+
import { buildSetupTeardownPlan } from './buildSetupTeardownPlan.js';
|
|
4
|
+
import { findBackendTestContext } from './findBackendTestContext.js';
|
|
5
|
+
import { findExistingTestAssets } from './findExistingTestAssets.js';
|
|
6
|
+
import { findTestAreaContext } from './findTestAreaContext.js';
|
|
7
|
+
import { output } from './shared.js';
|
|
8
|
+
import { recommendSelectorStrategy } from './recommendSelectorStrategy.js';
|
|
9
|
+
export async function reviewTestDraft(input, context) {
|
|
10
|
+
const area = input.area ?? inferArea(input.feature, input.draft);
|
|
11
|
+
const backendContextPromise = shouldQueryBackendContext(input.feature, input.draft, area)
|
|
12
|
+
? findBackendTestContext({ query: input.feature }, context)
|
|
13
|
+
: Promise.resolve(emptyToolOutput(`No backend context review was needed for '${input.feature}'.`));
|
|
14
|
+
const [assets, selectorStrategy, areaContext, setupPlan, gaps, backendContext] = await Promise.all([
|
|
15
|
+
findExistingTestAssets({ query: input.feature }, context),
|
|
16
|
+
recommendSelectorStrategy({ feature: input.feature }, context),
|
|
17
|
+
findTestAreaContext({ area, feature: input.feature }, context),
|
|
18
|
+
buildSetupTeardownPlan({ area, feature: input.feature }, context),
|
|
19
|
+
analyzeTestGaps({ widget: input.feature }, context),
|
|
20
|
+
backendContextPromise,
|
|
21
|
+
]);
|
|
22
|
+
const issues = reviewDraft({
|
|
23
|
+
draft: input.draft,
|
|
24
|
+
area,
|
|
25
|
+
gaps,
|
|
26
|
+
backendContext,
|
|
27
|
+
});
|
|
28
|
+
const evidence = [
|
|
29
|
+
...issues.map((issue) => ({
|
|
30
|
+
type: 'symbol',
|
|
31
|
+
label: `draft issue ${issue.kind}`,
|
|
32
|
+
snippet: issue.message,
|
|
33
|
+
metadata: { action: issue.action, area, severity: issue.severity },
|
|
34
|
+
})),
|
|
35
|
+
...supportingEvidence(assets, 'find_existing_test_assets', 3),
|
|
36
|
+
...supportingEvidence(selectorStrategy, 'recommend_selector_strategy', 2),
|
|
37
|
+
...supportingEvidence(areaContext, 'find_test_area_context', 2),
|
|
38
|
+
...supportingEvidence(setupPlan, 'build_setup_teardown_plan', 2),
|
|
39
|
+
...supportingEvidence(gaps, 'analyze_test_gaps', 2),
|
|
40
|
+
...supportingEvidence(backendContext, 'find_backend_test_context', 3),
|
|
41
|
+
];
|
|
42
|
+
const recommendedActions = unique([
|
|
43
|
+
...issues.map((issue) => issue.action),
|
|
44
|
+
...selectorStrategy.recommended_actions.slice(0, 1),
|
|
45
|
+
...setupPlan.recommended_actions.slice(0, 2),
|
|
46
|
+
...backendContext.recommended_actions.slice(0, 2),
|
|
47
|
+
...(gaps.evidence.length > 0
|
|
48
|
+
? ['Cross-check uncovered runtime signals before considering the draft complete.']
|
|
49
|
+
: []),
|
|
50
|
+
]);
|
|
51
|
+
const warnings = unique([
|
|
52
|
+
'Draft review is heuristic and checks Cypress patterns by string analysis; validate the final edits against the referenced project files.',
|
|
53
|
+
...assets.warnings,
|
|
54
|
+
...selectorStrategy.warnings,
|
|
55
|
+
...areaContext.warnings,
|
|
56
|
+
...setupPlan.warnings,
|
|
57
|
+
...gaps.warnings,
|
|
58
|
+
...backendContext.warnings,
|
|
59
|
+
]);
|
|
60
|
+
const repoPaths = unique([
|
|
61
|
+
...assets.repo_paths,
|
|
62
|
+
...selectorStrategy.repo_paths,
|
|
63
|
+
...areaContext.repo_paths,
|
|
64
|
+
...setupPlan.repo_paths,
|
|
65
|
+
...gaps.repo_paths,
|
|
66
|
+
...backendContext.repo_paths,
|
|
67
|
+
]);
|
|
68
|
+
return output(buildReviewSummary(input.feature, area, issues), evidence, recommendedActions, warnings, repoPaths);
|
|
69
|
+
}
|
|
70
|
+
function reviewDraft(input) {
|
|
71
|
+
const issues = [];
|
|
72
|
+
const draft = input.draft;
|
|
73
|
+
if (input.area === 'constructor') {
|
|
74
|
+
if (!/\binitConf\s*\(/.test(draft)) {
|
|
75
|
+
issues.push({
|
|
76
|
+
kind: 'bootstrap',
|
|
77
|
+
message: 'Constructor draft does not bootstrap with initConf().',
|
|
78
|
+
action: 'Switch bootstrap to initConf() and expose appNameConf at the describe level.',
|
|
79
|
+
severity: 'high',
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
if (!/cy\.selectAppInConstructor\s*\(/.test(draft)) {
|
|
83
|
+
issues.push({
|
|
84
|
+
kind: 'bootstrap',
|
|
85
|
+
message: 'Constructor draft bypasses cy.selectAppInConstructor(...) navigation reuse.',
|
|
86
|
+
action: 'Reuse cy.selectAppInConstructor(...) instead of rebuilding constructor navigation.',
|
|
87
|
+
severity: 'high',
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
if (!/\binit\s*\(/.test(draft)) {
|
|
93
|
+
issues.push({
|
|
94
|
+
kind: 'bootstrap',
|
|
95
|
+
message: 'formRunner draft does not call init().',
|
|
96
|
+
action: 'Add init() at the describe level before reviewing assertions.',
|
|
97
|
+
severity: 'high',
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
if (!/cy\.selectForm\s*\(/.test(draft)) {
|
|
101
|
+
issues.push({
|
|
102
|
+
kind: 'bootstrap',
|
|
103
|
+
message: 'formRunner draft does not reuse cy.selectForm(...) navigation.',
|
|
104
|
+
action: 'Reuse cy.selectForm(...) from project patterns instead of custom navigation steps.',
|
|
105
|
+
severity: 'high',
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
if (!/cy\.waitVcmForm\s*\(/.test(draft)) {
|
|
109
|
+
issues.push({
|
|
110
|
+
kind: 'bootstrap',
|
|
111
|
+
message: 'formRunner draft does not reuse cy.waitVcmForm() around form navigation.',
|
|
112
|
+
action: 'Reuse cy.waitVcmForm() around form navigation instead of relying on incidental readiness.',
|
|
113
|
+
severity: 'medium',
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (/cy\.wait\(\s*\d+\s*\)/.test(draft)) {
|
|
118
|
+
issues.push({
|
|
119
|
+
kind: 'wait',
|
|
120
|
+
message: 'Draft uses ad-hoc cy.wait(number) sleeps.',
|
|
121
|
+
action: 'Replace cy.wait(number) with project wait helpers or explicit readiness assertions.',
|
|
122
|
+
severity: 'high',
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
if (hasBrittleSelector(draft) || !/cy\.getWidget\s*\(/.test(draft)) {
|
|
126
|
+
issues.push({
|
|
127
|
+
kind: 'selector',
|
|
128
|
+
message: 'Draft leans on brittle selectors or skips cy.getWidget(...) reuse.',
|
|
129
|
+
action: 'Prefer cy.getWidget(...) plus selector-tree or testguid-backed hooks over DOM-order selectors.',
|
|
130
|
+
severity: 'medium',
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
if (/click\(\s*\{\s*force\s*:\s*true/.test(draft) || hasSilentMutation(draft)) {
|
|
134
|
+
issues.push({
|
|
135
|
+
kind: 'mutation',
|
|
136
|
+
message: 'Draft relies on force clicks or direct DOM mutation patterns.',
|
|
137
|
+
action: 'Remove force clicks and silent DOM mutation patterns until widget readiness is proven explicitly.',
|
|
138
|
+
severity: 'high',
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
appendBackendIssues(issues, input.backendContext, draft);
|
|
142
|
+
const gapSignal = input.gaps.evidence
|
|
143
|
+
.find((item) => item.label.startsWith('uncovered branch') || item.label.startsWith('uncovered method') || item.label.startsWith('uncovered event'));
|
|
144
|
+
if (gapSignal) {
|
|
145
|
+
const gapHint = gapSignal.snippet ?? gapSignal.label.replace(/^uncovered (?:branch|method|event)\s+/i, '');
|
|
146
|
+
if (gapHint && !draft.includes(gapHint)) {
|
|
147
|
+
issues.push({
|
|
148
|
+
kind: 'coverage',
|
|
149
|
+
message: `Draft does not mention the current gap signal '${gapHint}'.`,
|
|
150
|
+
action: `Add an assertion or scenario covering '${gapHint}' before considering the draft complete.`,
|
|
151
|
+
severity: 'medium',
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return issues;
|
|
156
|
+
}
|
|
157
|
+
function supportingEvidence(result, sourceTool, limit) {
|
|
158
|
+
return result.evidence.slice(0, limit).map((item) => ({
|
|
159
|
+
...item,
|
|
160
|
+
metadata: {
|
|
161
|
+
...item.metadata,
|
|
162
|
+
sourceTool,
|
|
163
|
+
},
|
|
164
|
+
}));
|
|
165
|
+
}
|
|
166
|
+
function inferArea(feature, draft) {
|
|
167
|
+
if (/\binitConf\s*\(|cy\.selectAppInConstructor\s*\(|@constructor/i.test(draft) || /constructor|auth|repository|application/i.test(feature)) {
|
|
168
|
+
return 'constructor';
|
|
169
|
+
}
|
|
170
|
+
return 'formRunner';
|
|
171
|
+
}
|
|
172
|
+
function shouldQueryBackendContext(feature, draft, area) {
|
|
173
|
+
return area === 'constructor'
|
|
174
|
+
|| /\bcy\.request\s*\(|\bfetch\s*\(|\/platform\/|\/rs2\/|api-heavy|backend|request flow|taskId|poll/i.test(`${feature}\n${draft}`);
|
|
175
|
+
}
|
|
176
|
+
function emptyToolOutput(summary) {
|
|
177
|
+
return {
|
|
178
|
+
summary,
|
|
179
|
+
evidence: [],
|
|
180
|
+
recommended_actions: [],
|
|
181
|
+
repo_paths: [],
|
|
182
|
+
confidence: {
|
|
183
|
+
score: 0,
|
|
184
|
+
level: 'low',
|
|
185
|
+
rationale: ['Backend review branch was intentionally skipped for this draft.'],
|
|
186
|
+
},
|
|
187
|
+
warnings: [],
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
function buildReviewSummary(feature, area, issues) {
|
|
191
|
+
const highCount = issues.filter((issue) => issue.severity === 'high').length;
|
|
192
|
+
const mediumCount = issues.filter((issue) => issue.severity === 'medium').length;
|
|
193
|
+
return `Reviewed draft for ${feature} in ${area}: found ${issues.length} issue(s) with ${highCount} high severity and ${mediumCount} medium severity findings across project reuse, selector stability, setup patterns, and runtime gap hints.`;
|
|
194
|
+
}
|
|
195
|
+
function appendBackendIssues(issues, backendContext, draft) {
|
|
196
|
+
const hasBackendEvidence = backendContext.evidence.some((item) => item.label === 'api interaction' || item.label === 'backend endpoint');
|
|
197
|
+
if (!hasBackendEvidence) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const usesRawRequest = /\bcy\.request\s*\(|\bfetch\s*\(/.test(draft);
|
|
201
|
+
const reusesKnownFlow = /\bcy\.(sampleAction|waitForSampleAction|authConf|getCurrentAuthUser|logoutFromConstructor)\s*\(/.test(draft);
|
|
202
|
+
const hasCompletionCheck = /\bwaitFor[A-Z]|poll|status\b|its\(\s*['"]status['"]\s*\)|should\(\s*['"]eq['"]\s*,\s*(200|201|202)/.test(draft);
|
|
203
|
+
if (usesRawRequest && !reusesKnownFlow) {
|
|
204
|
+
issues.push({
|
|
205
|
+
kind: 'backend',
|
|
206
|
+
message: 'Draft calls backend requests directly instead of reusing the indexed project request flow.',
|
|
207
|
+
action: 'Reuse the linked autotests2 request flow instead of rebuilding API assumptions from scratch.',
|
|
208
|
+
severity: 'high',
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
if (!hasCompletionCheck) {
|
|
212
|
+
issues.push({
|
|
213
|
+
kind: 'backend',
|
|
214
|
+
message: 'Draft does not verify backend completion, response status, or polling for the indexed API-heavy flow.',
|
|
215
|
+
action: 'Preserve backend request order, required params, and completion polling from the indexed API interaction before adding UI assertions.',
|
|
216
|
+
severity: 'high',
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function hasBrittleSelector(draft) {
|
|
221
|
+
return /nth-child|\.eq\(|\.first\(|\.last\(|\.children\(|>\s*div|>\s*span/.test(draft);
|
|
222
|
+
}
|
|
223
|
+
function hasSilentMutation(draft) {
|
|
224
|
+
return /\.invoke\(\s*['"](show|hide|remove|removeAttr|attr|prop|val|html)['"]/.test(draft)
|
|
225
|
+
|| /\.trigger\(\s*['"](input|change|blur)['"]/.test(draft);
|
|
226
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
1
2
|
import { buildToolOutput } from '../contracts/toolOutput.js';
|
|
2
3
|
import { confidenceFromEvidence } from '../contracts/confidence.js';
|
|
3
|
-
import { refreshKnowledge } from '../indexer/refreshPipeline.js';
|
|
4
|
+
import { getRefreshSignature, isRefreshStateExpired, readRefreshState, refreshKnowledge } from '../indexer/refreshPipeline.js';
|
|
4
5
|
import { countDocuments } from '../knowledge/repositories.js';
|
|
5
6
|
import { unique } from '../utils/pathUtils.js';
|
|
6
7
|
let indexedContexts = new WeakSet();
|
|
@@ -8,13 +9,25 @@ export async function ensureIndexed(context) {
|
|
|
8
9
|
if (indexedContexts.has(context)) {
|
|
9
10
|
return;
|
|
10
11
|
}
|
|
11
|
-
|
|
12
|
+
const refreshState = readRefreshState(context.config.cacheDir);
|
|
13
|
+
const currentSignature = getRefreshSignature(context);
|
|
14
|
+
if (countDocuments(context.database) > 0
|
|
15
|
+
&& ((refreshState && refreshState.repoSignature === currentSignature && !isRefreshStateExpired(refreshState))
|
|
16
|
+
|| (!refreshState && configuredReposUnavailable(context)))) {
|
|
12
17
|
indexedContexts.add(context);
|
|
13
18
|
return;
|
|
14
19
|
}
|
|
15
20
|
await refreshKnowledge(context);
|
|
16
21
|
indexedContexts.add(context);
|
|
17
22
|
}
|
|
23
|
+
function configuredReposUnavailable(context) {
|
|
24
|
+
const repoRoots = [
|
|
25
|
+
context.config.repos.autotests2,
|
|
26
|
+
context.config.repos.caseplatformWeb,
|
|
27
|
+
context.config.repos.fisPlatform,
|
|
28
|
+
].filter((value) => Boolean(value));
|
|
29
|
+
return repoRoots.every((repoRoot) => !existsSync(repoRoot));
|
|
30
|
+
}
|
|
18
31
|
export function evidenceFile(label, filePath, snippet, metadata) {
|
|
19
32
|
return {
|
|
20
33
|
type: 'file',
|
|
@@ -5,7 +5,7 @@ export declare function summarizeCiContext(input: {
|
|
|
5
5
|
warnings: string[];
|
|
6
6
|
summary: string;
|
|
7
7
|
evidence: {
|
|
8
|
-
type: "symbol" | "
|
|
8
|
+
type: "symbol" | "test" | "config" | "file" | "artifact" | "report" | "diff";
|
|
9
9
|
label: string;
|
|
10
10
|
path?: string | undefined;
|
|
11
11
|
locator?: string | undefined;
|
|
@@ -8,7 +8,7 @@ export declare function triageFailedRun(input: {
|
|
|
8
8
|
warnings: string[];
|
|
9
9
|
summary: string;
|
|
10
10
|
evidence: {
|
|
11
|
-
type: "symbol" | "
|
|
11
|
+
type: "symbol" | "test" | "config" | "file" | "artifact" | "report" | "diff";
|
|
12
12
|
label: string;
|
|
13
13
|
path?: string | undefined;
|
|
14
14
|
locator?: string | undefined;
|
|
@@ -44,9 +44,15 @@ export async function triageFailedRun(input, context) {
|
|
|
44
44
|
})),
|
|
45
45
|
...apiBackendContext.interactionHits.map((item) => evidenceFile('api interaction', item.path, item.title, {
|
|
46
46
|
confidence: parseMetadata(item.metadata_json).confidence,
|
|
47
|
+
normalizedPaths: parseMetadata(item.metadata_json).normalizedPaths,
|
|
48
|
+
whySelected: parseMetadata(item.metadata_json).whySelected,
|
|
47
49
|
})),
|
|
48
50
|
...apiBackendContext.backendHits.map((item) => evidenceFile('backend endpoint', item.path, item.title, {
|
|
49
51
|
confidence: parseMetadata(item.metadata_json).confidence,
|
|
52
|
+
endpointPath: parseMetadata(item.metadata_json).endpointPath,
|
|
53
|
+
whyMatched: parseMetadata(item.metadata_json).whyMatched,
|
|
54
|
+
paramNames: parseMetadata(item.metadata_json).paramNames,
|
|
55
|
+
ruleHints: parseMetadata(item.metadata_json).ruleHints,
|
|
50
56
|
})),
|
|
51
57
|
...(reportPortal.launchLink ? [{
|
|
52
58
|
type: 'report',
|
|
@@ -104,12 +110,17 @@ async function matchApiCapturesToBackendContext(captureSignals, context) {
|
|
|
104
110
|
return (metadata.confidence ?? 0) >= 0.7
|
|
105
111
|
&& (metadata.normalizedPaths ?? []).some((indexedPath) => captureSignals.some((signal) => pathsOverlap(indexedPath, signal.path)));
|
|
106
112
|
});
|
|
107
|
-
const
|
|
108
|
-
|
|
113
|
+
const interactionPaths = [...new Set(interactionHits.flatMap((item) => parseMetadata(item.metadata_json).normalizedPaths ?? []))];
|
|
114
|
+
const backendHits = interactionPaths.length > 0
|
|
115
|
+
? queryDocuments(context.database, interactionPaths.join(' '), {
|
|
109
116
|
repoKind: 'fis-platform',
|
|
110
117
|
sourceKinds: ['backend-endpoint'],
|
|
111
118
|
limit: 6,
|
|
112
|
-
}).filter((item) =>
|
|
119
|
+
}).filter((item) => {
|
|
120
|
+
const metadata = parseMetadata(item.metadata_json);
|
|
121
|
+
return (metadata.confidence ?? 0) >= 0.7
|
|
122
|
+
&& interactionPaths.some((candidate) => pathsOverlap(metadata.endpointPath ?? item.title, candidate));
|
|
123
|
+
})
|
|
113
124
|
: [];
|
|
114
125
|
return { interactionHits, backendHits };
|
|
115
126
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CaseplatformConnector } from '../../connectors/caseplatformConnector.js';
|
|
2
2
|
import { extractMatches, truncate } from '../../utils/textUtils.js';
|
|
3
|
-
import { unique } from '../../utils/pathUtils.js';
|
|
3
|
+
import { featureIntentTokens, pathIntentTokens, textIntentTokens, unique } from '../../utils/pathUtils.js';
|
|
4
4
|
export async function extractFrontendContract(input) {
|
|
5
5
|
const connector = new CaseplatformConnector(input.root);
|
|
6
6
|
const files = await connector.resolveFeatureFiles(input.widgetHint);
|
|
@@ -12,11 +12,15 @@ export async function extractFrontendContract(input) {
|
|
|
12
12
|
if (files.mode === 'widget' && !files.controlFile) {
|
|
13
13
|
warnings.push(`No control file found for widget '${input.widgetHint}'.`);
|
|
14
14
|
}
|
|
15
|
-
const
|
|
15
|
+
const allSources = files.allFiles.map((filePath) => ({
|
|
16
16
|
path: filePath,
|
|
17
17
|
body: connector.read(filePath),
|
|
18
18
|
kind: connector.classifyFileKind(filePath),
|
|
19
19
|
}));
|
|
20
|
+
const sources = files.mode === 'conf'
|
|
21
|
+
? focusFrontendSources(allSources, input.widgetHint)
|
|
22
|
+
: allSources;
|
|
23
|
+
const focusedFiles = sources.map((source) => source.path);
|
|
20
24
|
const properties = files.mode === 'conf'
|
|
21
25
|
? unique(sources.flatMap((source) => extractConfProperties(source.body)))
|
|
22
26
|
: extractMatches(controlBody, /qname:\s*'([^']+)'/g);
|
|
@@ -52,11 +56,51 @@ export async function extractFrontendContract(input) {
|
|
|
52
56
|
events: [...new Set(events)],
|
|
53
57
|
files: files.mode === 'widget'
|
|
54
58
|
? unique([files.controlFile, files.singletonFile, files.handlersFile, files.templateFile, files.dataControlFile, ...files.supportFiles].filter(Boolean))
|
|
55
|
-
:
|
|
59
|
+
: focusedFiles,
|
|
56
60
|
snippets,
|
|
57
61
|
warnings,
|
|
58
62
|
};
|
|
59
63
|
}
|
|
64
|
+
function focusFrontendSources(sources, widgetHint) {
|
|
65
|
+
const intentTokens = featureIntentTokens(widgetHint);
|
|
66
|
+
if (intentTokens.length === 0) {
|
|
67
|
+
return sources;
|
|
68
|
+
}
|
|
69
|
+
const scoredSources = sources.map((source) => ({
|
|
70
|
+
source,
|
|
71
|
+
pathTokens: pathIntentTokens(source.path),
|
|
72
|
+
score: scoreSourceIntent(source, intentTokens),
|
|
73
|
+
}));
|
|
74
|
+
const matchedSources = scoredSources.filter((item) => item.score > 0);
|
|
75
|
+
if (matchedSources.length === 0) {
|
|
76
|
+
return sources;
|
|
77
|
+
}
|
|
78
|
+
const viewIntentDirectories = new Set(matchedSources
|
|
79
|
+
.filter((item) => item.source.kind === 'view')
|
|
80
|
+
.flatMap((item) => directoryIntentTokens(item.source.path))
|
|
81
|
+
.filter((token) => intentTokens.includes(token)));
|
|
82
|
+
const focused = matchedSources
|
|
83
|
+
.filter((item) => {
|
|
84
|
+
if (item.source.kind !== 'view' || viewIntentDirectories.size === 0) {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
return directoryIntentTokens(item.source.path).some((token) => viewIntentDirectories.has(token));
|
|
88
|
+
})
|
|
89
|
+
.map((item) => item.source);
|
|
90
|
+
return focused.length > 0 ? focused : sources;
|
|
91
|
+
}
|
|
92
|
+
function scoreSourceIntent(source, intentTokens) {
|
|
93
|
+
const pathTokens = pathIntentTokens(source.path);
|
|
94
|
+
const bodyTokens = new Set(textIntentTokens(source.body));
|
|
95
|
+
return intentTokens.reduce((score, token) => {
|
|
96
|
+
const pathScore = pathTokens.includes(token) ? 4 : pathTokens.some((pathToken) => pathToken.includes(token)) ? 2 : 0;
|
|
97
|
+
const bodyScore = bodyTokens.has(token) ? 1 : 0;
|
|
98
|
+
return score + pathScore + bodyScore;
|
|
99
|
+
}, 0);
|
|
100
|
+
}
|
|
101
|
+
function directoryIntentTokens(filePath) {
|
|
102
|
+
return pathIntentTokens(filePath.replace(/[\\/][^\\/]+$/, ''));
|
|
103
|
+
}
|
|
60
104
|
function extractNamedObjectKeys(source, objectName) {
|
|
61
105
|
const startMatch = new RegExp(`${objectName}\\s*:\\s*\\{`).exec(source);
|
|
62
106
|
if (!startMatch || startMatch.index === undefined) {
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import type { AppContext } from '../appContext.js';
|
|
2
|
+
export declare const REFRESH_STATE_FILE = "refresh-state.json";
|
|
3
|
+
export declare const REFRESH_STATE_TTL_MS: number;
|
|
4
|
+
type RefreshState = {
|
|
5
|
+
repoSignature: string;
|
|
6
|
+
updatedAt: string;
|
|
7
|
+
};
|
|
2
8
|
export declare function refreshKnowledge(context: AppContext): Promise<{
|
|
3
9
|
documentsIndexed: number;
|
|
4
10
|
stats: {
|
|
@@ -10,3 +16,7 @@ export declare function refreshKnowledge(context: AppContext): Promise<{
|
|
|
10
16
|
};
|
|
11
17
|
warnings: string[];
|
|
12
18
|
}>;
|
|
19
|
+
export declare function getRefreshSignature(context: AppContext): string;
|
|
20
|
+
export declare function readRefreshState(cacheDir: string): RefreshState | null;
|
|
21
|
+
export declare function isRefreshStateExpired(refreshState: RefreshState): boolean;
|
|
22
|
+
export {};
|
|
@@ -7,16 +7,17 @@ import { extractApiInteractions } from './extractors/apiInteractionExtractor.js'
|
|
|
7
7
|
import { extractBackendContracts } from './extractors/backendContractExtractor.js';
|
|
8
8
|
import { extractFrontendContract } from './extractors/frontendContractExtractor.js';
|
|
9
9
|
import { SnapshotStore } from './snapshotStore.js';
|
|
10
|
-
import { ensureDirectory } from '../utils/fileUtils.js';
|
|
10
|
+
import { ensureDirectory, readJson, writeJson } from '../utils/fileUtils.js';
|
|
11
|
+
export const REFRESH_STATE_FILE = 'refresh-state.json';
|
|
12
|
+
export const REFRESH_STATE_TTL_MS = 5 * 60 * 1000;
|
|
11
13
|
export async function refreshKnowledge(context) {
|
|
12
14
|
ensureDirectory(context.config.cacheDir);
|
|
13
15
|
const snapshotStore = new SnapshotStore(path.join(context.config.cacheDir, 'snapshots'));
|
|
14
16
|
const documents = [];
|
|
15
17
|
const now = new Date().toISOString();
|
|
16
|
-
const repoKindsToSync = [];
|
|
18
|
+
const repoKindsToSync = ['autotests2', 'caseplatform-web', 'fis-platform'];
|
|
17
19
|
const warnings = [];
|
|
18
20
|
if (fs.existsSync(context.config.repos.autotests2)) {
|
|
19
|
-
repoKindsToSync.push('autotests2');
|
|
20
21
|
const docs = context.autotests2.getAreaDocs();
|
|
21
22
|
const apiCatalog = await buildApiDiscoveryCatalog({
|
|
22
23
|
root: context.config.repos.autotests2,
|
|
@@ -106,7 +107,6 @@ export async function refreshKnowledge(context) {
|
|
|
106
107
|
}
|
|
107
108
|
warnings.push(...apiCatalog.warnings, ...apiInteractions.warnings);
|
|
108
109
|
if (context.fisPlatform && context.config.repos.fisPlatform && fs.existsSync(context.config.repos.fisPlatform)) {
|
|
109
|
-
repoKindsToSync.push('fis-platform');
|
|
110
110
|
const backendSources = await context.fisPlatform.listFocusedSources(apiCatalog);
|
|
111
111
|
const backendContracts = extractBackendContracts({ sources: backendSources });
|
|
112
112
|
snapshotStore.write('backend-contracts', backendContracts);
|
|
@@ -128,7 +128,6 @@ export async function refreshKnowledge(context) {
|
|
|
128
128
|
warnings.push(`Skipped autotests2 refresh because '${context.config.repos.autotests2}' is unavailable.`);
|
|
129
129
|
}
|
|
130
130
|
if (fs.existsSync(context.config.repos.caseplatformWeb)) {
|
|
131
|
-
repoKindsToSync.push('caseplatform-web');
|
|
132
131
|
const frontendContract = await extractFrontendContract({
|
|
133
132
|
root: context.config.repos.caseplatformWeb,
|
|
134
133
|
widgetHint: context.config.projectHints?.defaultWidget ?? 'caption',
|
|
@@ -152,9 +151,8 @@ export async function refreshKnowledge(context) {
|
|
|
152
151
|
else {
|
|
153
152
|
warnings.push(`Skipped caseplatform-web refresh because '${context.config.repos.caseplatformWeb}' is unavailable.`);
|
|
154
153
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
154
|
+
syncDocumentsByRepoKinds(context.database, documents, repoKindsToSync);
|
|
155
|
+
writeRefreshState(context, now);
|
|
158
156
|
const uniqueDocuments = dedupeDocuments(documents);
|
|
159
157
|
const stats = {
|
|
160
158
|
attemptedDocuments: documents.length,
|
|
@@ -169,6 +167,38 @@ export async function refreshKnowledge(context) {
|
|
|
169
167
|
warnings,
|
|
170
168
|
};
|
|
171
169
|
}
|
|
170
|
+
export function getRefreshSignature(context) {
|
|
171
|
+
return JSON.stringify({
|
|
172
|
+
autotests2: repoState(context.config.repos.autotests2),
|
|
173
|
+
caseplatformWeb: repoState(context.config.repos.caseplatformWeb),
|
|
174
|
+
fisPlatform: context.config.repos.fisPlatform ? repoState(context.config.repos.fisPlatform) : null,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
export function readRefreshState(cacheDir) {
|
|
178
|
+
const filePath = path.join(cacheDir, REFRESH_STATE_FILE);
|
|
179
|
+
try {
|
|
180
|
+
return readJson(filePath);
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
export function isRefreshStateExpired(refreshState) {
|
|
187
|
+
return Date.now() - Date.parse(refreshState.updatedAt) > REFRESH_STATE_TTL_MS;
|
|
188
|
+
}
|
|
189
|
+
function writeRefreshState(context, updatedAt) {
|
|
190
|
+
const filePath = path.join(context.config.cacheDir, REFRESH_STATE_FILE);
|
|
191
|
+
writeJson(filePath, {
|
|
192
|
+
repoSignature: getRefreshSignature(context),
|
|
193
|
+
updatedAt,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
function repoState(root) {
|
|
197
|
+
return {
|
|
198
|
+
root: path.resolve(root),
|
|
199
|
+
exists: fs.existsSync(root),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
172
202
|
function documentFrom(input) {
|
|
173
203
|
return {
|
|
174
204
|
id: crypto.createHash('sha1').update(`${input.sourceKind}:${input.path}`).digest('hex'),
|
|
@@ -11,6 +11,7 @@ import { findTestAreaContext } from '../domain/findTestAreaContext.js';
|
|
|
11
11
|
import { generateSpecBlueprint } from '../domain/generateSpecBlueprint.js';
|
|
12
12
|
import { mapFeatureToExistingCoverage } from '../domain/mapFeatureToExistingCoverage.js';
|
|
13
13
|
import { recommendSelectorStrategy } from '../domain/recommendSelectorStrategy.js';
|
|
14
|
+
import { reviewTestDraft } from '../domain/reviewTestDraft.js';
|
|
14
15
|
import { summarizeCiContext } from '../domain/summarizeCiContext.js';
|
|
15
16
|
import { triageFailedRun } from '../domain/triageFailedRun.js';
|
|
16
17
|
import { toolOutputSchema } from '../contracts/toolOutput.js';
|
|
@@ -23,6 +24,17 @@ export function buildToolRegistry() {
|
|
|
23
24
|
outputSchema: toolOutputSchema,
|
|
24
25
|
execute: findExistingTestAssets,
|
|
25
26
|
},
|
|
27
|
+
{
|
|
28
|
+
name: 'review_test_draft',
|
|
29
|
+
description: 'Reviews a draft test for reuse, brittle selectors, setup drift, and likely uncovered signals.',
|
|
30
|
+
inputSchema: z.object({
|
|
31
|
+
feature: z.string().min(1),
|
|
32
|
+
draft: z.string().min(1),
|
|
33
|
+
area: z.string().optional(),
|
|
34
|
+
}),
|
|
35
|
+
outputSchema: toolOutputSchema,
|
|
36
|
+
execute: reviewTestDraft,
|
|
37
|
+
},
|
|
26
38
|
{
|
|
27
39
|
name: 'find_backend_test_context',
|
|
28
40
|
description: 'Finds autotests2 API interaction flows plus backend endpoint, validation, async-task, and error hints.',
|
|
@@ -5,3 +5,6 @@ export declare function safeRelative(from: string, to: string): string;
|
|
|
5
5
|
export declare function basenameWithoutExt(filePath: string): string;
|
|
6
6
|
export declare function widgetNameCandidates(widgetHint: string): string[];
|
|
7
7
|
export declare function featureNameCandidates(featureHint: string): string[];
|
|
8
|
+
export declare function featureIntentTokens(featureHint: string): string[];
|
|
9
|
+
export declare function pathIntentTokens(filePath: string): string[];
|
|
10
|
+
export declare function textIntentTokens(value: string): string[];
|
|
@@ -57,3 +57,32 @@ export function featureNameCandidates(featureHint) {
|
|
|
57
57
|
}
|
|
58
58
|
return unique(candidates.filter(Boolean));
|
|
59
59
|
}
|
|
60
|
+
export function featureIntentTokens(featureHint) {
|
|
61
|
+
return unique(featureNameCandidates(featureHint)
|
|
62
|
+
.flatMap(splitIntentTokens)
|
|
63
|
+
.map((token) => token.toLowerCase())
|
|
64
|
+
.filter((token) => token.length >= 3)
|
|
65
|
+
.filter((token) => !INTENT_STOP_WORDS.has(token)));
|
|
66
|
+
}
|
|
67
|
+
export function pathIntentTokens(filePath) {
|
|
68
|
+
const normalized = toPosixPath(filePath).replace(/\.[A-Za-z0-9]+$/, '');
|
|
69
|
+
return unique(splitIntentTokens(normalized).map((token) => token.toLowerCase()));
|
|
70
|
+
}
|
|
71
|
+
export function textIntentTokens(value) {
|
|
72
|
+
return unique(splitIntentTokens(value).map((token) => token.toLowerCase()));
|
|
73
|
+
}
|
|
74
|
+
function splitIntentTokens(value) {
|
|
75
|
+
return value
|
|
76
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
77
|
+
.split(/[^A-Za-z0-9]+/)
|
|
78
|
+
.filter(Boolean);
|
|
79
|
+
}
|
|
80
|
+
const INTENT_STOP_WORDS = new Set([
|
|
81
|
+
'and',
|
|
82
|
+
'constructor',
|
|
83
|
+
'cypress',
|
|
84
|
+
'spec',
|
|
85
|
+
'test',
|
|
86
|
+
'tests',
|
|
87
|
+
'the',
|
|
88
|
+
]);
|