@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
package/README.md
CHANGED
|
@@ -45,9 +45,10 @@ Repository-local development:
|
|
|
45
45
|
1. Refresh local knowledge with `npm run index`.
|
|
46
46
|
2. Start the MCP server through the checked-in MCP manifest or by running `npx autotests-overkill mcp`.
|
|
47
47
|
3. Use `find_frontend_contract`, `find_existing_test_assets`, and `generate_spec_blueprint` for a new feature draft.
|
|
48
|
-
4. Use `
|
|
49
|
-
5. Use `
|
|
50
|
-
6.
|
|
48
|
+
4. Use `review_test_draft` to critique a draft for reuse, selector stability, setup drift, and likely gap coverage before editing `autotests2`.
|
|
49
|
+
5. Use `find_backend_test_context` when a request flow needs backend validation, params, async completion, or contract-drift evidence.
|
|
50
|
+
6. Use `summarize_ci_context`, `triage_failed_run`, and `analyze_diff_impact` for failure or change triage.
|
|
51
|
+
7. Review generated output manually before editing `autotests2`.
|
|
51
52
|
|
|
52
53
|
CLI commands:
|
|
53
54
|
|
|
@@ -55,6 +56,7 @@ CLI commands:
|
|
|
55
56
|
- `npx autotests-overkill mcp`
|
|
56
57
|
- `npx autotests-overkill acceptance coverage caption`
|
|
57
58
|
- `npx autotests-overkill acceptance blueprint dateRangeField`
|
|
59
|
+
- `npx autotests-overkill acceptance review caption`
|
|
58
60
|
|
|
59
61
|
## Verification commands
|
|
60
62
|
|
|
@@ -63,6 +65,7 @@ CLI commands:
|
|
|
63
65
|
- `npm run acceptance:coverage`
|
|
64
66
|
- `npm run acceptance:coverage:legacy`
|
|
65
67
|
- `npm run acceptance:blueprint`
|
|
68
|
+
- `npm run acceptance:review`
|
|
66
69
|
- `npm run acceptance:failure`
|
|
67
70
|
- `npm run acceptance:impact`
|
|
68
71
|
|
|
@@ -95,6 +98,7 @@ Only high-confidence links (`confidence >= 0.7`) drive blueprint, triage, and im
|
|
|
95
98
|
## MCP tools
|
|
96
99
|
|
|
97
100
|
- `find_existing_test_assets`
|
|
101
|
+
- `review_test_draft`
|
|
98
102
|
- `find_backend_test_context`
|
|
99
103
|
- `find_test_area_context`
|
|
100
104
|
- `find_frontend_contract`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type AppContext } from '../appContext.js';
|
|
2
2
|
import type { NormalizedToolOutput } from '../contracts/toolOutput.js';
|
|
3
|
-
export type AcceptanceMode = 'coverage' | 'failure' | 'impact' | 'blueprint';
|
|
3
|
+
export type AcceptanceMode = 'coverage' | 'failure' | 'impact' | 'blueprint' | 'review';
|
|
4
4
|
export type AcceptanceCheck = {
|
|
5
5
|
name: string;
|
|
6
6
|
passed: boolean;
|
|
@@ -17,5 +17,6 @@ export declare function evaluateCoverageAcceptance(contract: NormalizedToolOutpu
|
|
|
17
17
|
export declare function evaluateFailureAcceptance(triage: NormalizedToolOutput): AcceptanceCheck[];
|
|
18
18
|
export declare function evaluateImpactAcceptance(impact: NormalizedToolOutput, changedFile: string): AcceptanceCheck[];
|
|
19
19
|
export declare function evaluateBlueprintAcceptance(blueprint: NormalizedToolOutput, feature: string): AcceptanceCheck[];
|
|
20
|
+
export declare function evaluateReviewAcceptance(review: NormalizedToolOutput, feature: string): AcceptanceCheck[];
|
|
20
21
|
export declare function assertPassingChecks(report: AcceptanceReport): void;
|
|
21
22
|
export declare function runAcceptanceCli(argv?: string[]): Promise<void>;
|
|
@@ -3,6 +3,7 @@ import { analyzeDiffImpact } from '../domain/analyzeDiffImpact.js';
|
|
|
3
3
|
import { analyzeTestGaps } from '../domain/analyzeTestGaps.js';
|
|
4
4
|
import { findFrontendContract } from '../domain/findFrontendContract.js';
|
|
5
5
|
import { generateSpecBlueprint } from '../domain/generateSpecBlueprint.js';
|
|
6
|
+
import { reviewTestDraft } from '../domain/reviewTestDraft.js';
|
|
6
7
|
import { triageFailedRun } from '../domain/triageFailedRun.js';
|
|
7
8
|
import { isDirectExecution } from './shared.js';
|
|
8
9
|
export async function evaluateAcceptanceMode(mode, args, context) {
|
|
@@ -43,6 +44,25 @@ export async function evaluateAcceptanceMode(mode, args, context) {
|
|
|
43
44
|
output: { impact },
|
|
44
45
|
};
|
|
45
46
|
}
|
|
47
|
+
if (mode === 'review') {
|
|
48
|
+
const feature = args[0] ?? 'caption';
|
|
49
|
+
const draft = args[1] ?? `
|
|
50
|
+
describe('[Overkill] ${feature}', () => {
|
|
51
|
+
it('covers ${feature}', () => {
|
|
52
|
+
cy.wait(1000);
|
|
53
|
+
cy.get('.widget-row').eq(1).click({ force: true });
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
`;
|
|
57
|
+
const area = args[2];
|
|
58
|
+
const review = await reviewTestDraft({ feature, draft, area }, context);
|
|
59
|
+
return {
|
|
60
|
+
mode,
|
|
61
|
+
target: feature,
|
|
62
|
+
checks: evaluateReviewAcceptance(review, feature),
|
|
63
|
+
output: { review },
|
|
64
|
+
};
|
|
65
|
+
}
|
|
46
66
|
const feature = args[0] ?? 'caption';
|
|
47
67
|
const area = args[1];
|
|
48
68
|
const blueprint = await generateSpecBlueprint({ feature, area }, context);
|
|
@@ -91,6 +111,17 @@ export function evaluateBlueprintAcceptance(blueprint, feature) {
|
|
|
91
111
|
check('selector and reuse guidance', hasSelectorGuidance && hasReuseGuidance, `Blueprint acceptance for '${feature}' requires explicit selector and reuse guidance.`, 'Recommended actions include selector and reuse guidance.'),
|
|
92
112
|
];
|
|
93
113
|
}
|
|
114
|
+
export function evaluateReviewAcceptance(review, feature) {
|
|
115
|
+
const issueEvidence = review.evidence.filter((item) => item.label.startsWith('draft issue '));
|
|
116
|
+
const hasSeverity = issueEvidence.some((item) => item.metadata && typeof item.metadata.severity === 'string');
|
|
117
|
+
const hasHighSeveritySummary = review.summary.includes('high severity');
|
|
118
|
+
const hasActionableGuidance = review.recommended_actions.length > 0;
|
|
119
|
+
return [
|
|
120
|
+
check('review findings', issueEvidence.length > 0, `Review acceptance for '${feature}' requires evidence-backed draft findings.`, `Collected ${issueEvidence.length} review finding(s).`),
|
|
121
|
+
check('review severity summary', hasSeverity && hasHighSeveritySummary, `Review acceptance for '${feature}' requires structured severity metadata and summary counts.`, 'Review output contains structured severity metadata.'),
|
|
122
|
+
check('review actions', hasActionableGuidance, `Review acceptance for '${feature}' requires actionable remediation guidance.`, 'Review output contains actionable remediation guidance.'),
|
|
123
|
+
];
|
|
124
|
+
}
|
|
94
125
|
export function assertPassingChecks(report) {
|
|
95
126
|
const failedChecks = report.checks.filter((checkResult) => !checkResult.passed);
|
|
96
127
|
if (failedChecks.length === 0) {
|
|
@@ -109,7 +140,7 @@ function check(name, passed, failedDetail, passedDetail) {
|
|
|
109
140
|
export async function runAcceptanceCli(argv = process.argv.slice(2)) {
|
|
110
141
|
const [rawMode, ...args] = argv;
|
|
111
142
|
if (!isAcceptanceMode(rawMode)) {
|
|
112
|
-
console.error(`Unknown acceptance mode '${rawMode}'. Use coverage, failure, impact, or
|
|
143
|
+
console.error(`Unknown acceptance mode '${rawMode}'. Use coverage, failure, impact, blueprint, or review.`);
|
|
113
144
|
process.exit(1);
|
|
114
145
|
}
|
|
115
146
|
const context = await createRuntimeAppContext();
|
|
@@ -124,7 +155,7 @@ export async function runAcceptanceCli(argv = process.argv.slice(2)) {
|
|
|
124
155
|
}
|
|
125
156
|
}
|
|
126
157
|
function isAcceptanceMode(value) {
|
|
127
|
-
return value === 'coverage' || value === 'failure' || value === 'impact' || value === 'blueprint';
|
|
158
|
+
return value === 'coverage' || value === 'failure' || value === 'impact' || value === 'blueprint' || value === 'review';
|
|
128
159
|
}
|
|
129
160
|
if (isDirectExecution(import.meta.url)) {
|
|
130
161
|
await runAcceptanceCli();
|
|
@@ -8,14 +8,14 @@ export declare const evidenceSchema: z.ZodObject<{
|
|
|
8
8
|
snippet: z.ZodOptional<z.ZodString>;
|
|
9
9
|
metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
10
10
|
}, "strip", z.ZodTypeAny, {
|
|
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;
|
|
15
15
|
snippet?: string | undefined;
|
|
16
16
|
metadata?: Record<string, unknown> | undefined;
|
|
17
17
|
}, {
|
|
18
|
-
type: "symbol" | "
|
|
18
|
+
type: "symbol" | "test" | "config" | "file" | "artifact" | "report" | "diff";
|
|
19
19
|
label: string;
|
|
20
20
|
path?: string | undefined;
|
|
21
21
|
locator?: string | undefined;
|
|
@@ -32,14 +32,14 @@ export declare const toolOutputSchema: z.ZodObject<{
|
|
|
32
32
|
snippet: z.ZodOptional<z.ZodString>;
|
|
33
33
|
metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
34
34
|
}, "strip", z.ZodTypeAny, {
|
|
35
|
-
type: "symbol" | "
|
|
35
|
+
type: "symbol" | "test" | "config" | "file" | "artifact" | "report" | "diff";
|
|
36
36
|
label: string;
|
|
37
37
|
path?: string | undefined;
|
|
38
38
|
locator?: string | undefined;
|
|
39
39
|
snippet?: string | undefined;
|
|
40
40
|
metadata?: Record<string, unknown> | undefined;
|
|
41
41
|
}, {
|
|
42
|
-
type: "symbol" | "
|
|
42
|
+
type: "symbol" | "test" | "config" | "file" | "artifact" | "report" | "diff";
|
|
43
43
|
label: string;
|
|
44
44
|
path?: string | undefined;
|
|
45
45
|
locator?: string | undefined;
|
|
@@ -66,7 +66,7 @@ export declare const toolOutputSchema: z.ZodObject<{
|
|
|
66
66
|
warnings: string[];
|
|
67
67
|
summary: string;
|
|
68
68
|
evidence: {
|
|
69
|
-
type: "symbol" | "
|
|
69
|
+
type: "symbol" | "test" | "config" | "file" | "artifact" | "report" | "diff";
|
|
70
70
|
label: string;
|
|
71
71
|
path?: string | undefined;
|
|
72
72
|
locator?: string | undefined;
|
|
@@ -84,7 +84,7 @@ export declare const toolOutputSchema: z.ZodObject<{
|
|
|
84
84
|
warnings: string[];
|
|
85
85
|
summary: string;
|
|
86
86
|
evidence: {
|
|
87
|
-
type: "symbol" | "
|
|
87
|
+
type: "symbol" | "test" | "config" | "file" | "artifact" | "report" | "diff";
|
|
88
88
|
label: string;
|
|
89
89
|
path?: string | undefined;
|
|
90
90
|
locator?: string | undefined;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { confidenceFromEvidence } from './confidence.js';
|
|
3
|
+
import { toPosixPath } from '../utils/pathUtils.js';
|
|
3
4
|
export const evidenceSchema = z.object({
|
|
4
5
|
type: z.enum(['file', 'symbol', 'test', 'artifact', 'report', 'diff', 'config']),
|
|
5
6
|
label: z.string(),
|
|
@@ -22,14 +23,17 @@ export const toolOutputSchema = z.object({
|
|
|
22
23
|
});
|
|
23
24
|
export function buildToolOutput(input) {
|
|
24
25
|
const warnings = input.warnings ?? [];
|
|
25
|
-
const
|
|
26
|
-
|
|
26
|
+
const normalizedEvidence = input.evidence.map((item) => item.path
|
|
27
|
+
? { ...item, path: toPosixPath(item.path) }
|
|
28
|
+
: item);
|
|
29
|
+
const repoPaths = input.repo_paths ?? normalizedEvidence.flatMap((item) => item.path ? [item.path] : []);
|
|
30
|
+
const uniquePaths = [...new Set(repoPaths.map((repoPath) => toPosixPath(repoPath)))];
|
|
27
31
|
return {
|
|
28
32
|
summary: input.summary,
|
|
29
|
-
evidence:
|
|
33
|
+
evidence: normalizedEvidence,
|
|
30
34
|
recommended_actions: input.recommended_actions,
|
|
31
35
|
repo_paths: uniquePaths,
|
|
32
|
-
confidence: input.confidence ?? confidenceFromEvidence(
|
|
36
|
+
confidence: input.confidence ?? confidenceFromEvidence(normalizedEvidence.length, warnings),
|
|
33
37
|
warnings,
|
|
34
38
|
};
|
|
35
39
|
}
|
|
@@ -5,7 +5,7 @@ export declare function analyzeDiffImpact(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;
|
|
@@ -3,21 +3,23 @@ import fs from 'node:fs';
|
|
|
3
3
|
import { normalizeChangedFiles } from '../connectors/diffConnector.js';
|
|
4
4
|
import { queryDocuments } from '../knowledge/repositories.js';
|
|
5
5
|
import { extractTestAssets } from '../indexer/extractors/testAssetExtractor.js';
|
|
6
|
-
import { basenameWithoutExt } from '../utils/pathUtils.js';
|
|
6
|
+
import { basenameWithoutExt, featureNameCandidates } from '../utils/pathUtils.js';
|
|
7
7
|
import { ensureIndexed, evidenceFile, output } from './shared.js';
|
|
8
|
+
import { scoreTextMatch } from '../utils/textUtils.js';
|
|
8
9
|
export async function analyzeDiffImpact(input, context) {
|
|
9
10
|
const changedFiles = normalizeChangedFiles(input.changedFiles);
|
|
10
11
|
const featureHints = changedFiles.map((filePath) => deriveFeatureHint(filePath)).filter(Boolean);
|
|
11
12
|
const uniqueFeatures = [...new Set(featureHints)];
|
|
12
13
|
const evidence = changedFiles.map((filePath) => evidenceFile('changed file', filePath));
|
|
13
14
|
const impactedSpecs = new Set();
|
|
15
|
+
const warnings = [];
|
|
14
16
|
await ensureIndexed(context);
|
|
15
17
|
for (const feature of uniqueFeatures) {
|
|
16
18
|
const indexed = queryDocuments(context.database, feature, {
|
|
17
19
|
repoKind: 'autotests2',
|
|
18
20
|
sourceKinds: ['spec'],
|
|
19
21
|
limit: 12,
|
|
20
|
-
});
|
|
22
|
+
}).filter((item) => isStrongFeatureSpecMatch(item.path, item.title, feature));
|
|
21
23
|
for (const item of indexed) {
|
|
22
24
|
impactedSpecs.add(item.path);
|
|
23
25
|
}
|
|
@@ -28,7 +30,7 @@ export async function analyzeDiffImpact(input, context) {
|
|
|
28
30
|
root: context.config.repos.autotests2,
|
|
29
31
|
featureHint: feature,
|
|
30
32
|
});
|
|
31
|
-
for (const spec of assets.specs.slice(0, 8)) {
|
|
33
|
+
for (const spec of assets.specs.filter((item) => isStrongFeatureSpecMatch(item.path, item.title, feature)).slice(0, 8)) {
|
|
32
34
|
impactedSpecs.add(spec.path);
|
|
33
35
|
}
|
|
34
36
|
}
|
|
@@ -73,7 +75,11 @@ export async function analyzeDiffImpact(input, context) {
|
|
|
73
75
|
], [
|
|
74
76
|
'Rerun directly impacted specs before broader regression.',
|
|
75
77
|
'Inspect widget events and container variants when the diff touches shared widget files.',
|
|
76
|
-
], impactedSpecs.size === 0
|
|
78
|
+
], impactedSpecs.size === 0
|
|
79
|
+
? ['No impacted specs were found via naming heuristics.']
|
|
80
|
+
: changedFiles.every((item) => !isFisPlatformPath(item, context))
|
|
81
|
+
? ['Frontend impact mapping relies on feature-path heuristics; validate indirect spec candidates before rerun routing.']
|
|
82
|
+
: warnings);
|
|
77
83
|
}
|
|
78
84
|
function isFisPlatformPath(filePath, context) {
|
|
79
85
|
const fisRoot = context.config.repos.fisPlatform;
|
|
@@ -116,3 +122,7 @@ function deriveFeatureHint(filePath) {
|
|
|
116
122
|
const parent = path.basename(path.dirname(filePath));
|
|
117
123
|
return parent;
|
|
118
124
|
}
|
|
125
|
+
function isStrongFeatureSpecMatch(specPath, specTitle, feature) {
|
|
126
|
+
const searchable = `${specPath}\n${specTitle}`;
|
|
127
|
+
return featureNameCandidates(feature).some((candidate) => scoreTextMatch(searchable, candidate) > 0);
|
|
128
|
+
}
|
|
@@ -5,7 +5,7 @@ export declare function analyzeTestGaps(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;
|
|
@@ -21,7 +21,8 @@ export async function analyzeTestGaps(input, context) {
|
|
|
21
21
|
const specText = assets.specs.map((spec) => spec.body).join('\n');
|
|
22
22
|
const uncoveredMethods = contract.methods.filter((method) => !specText.includes(method));
|
|
23
23
|
const uncoveredEvents = contract.events.filter((eventName) => !specText.includes(eventName));
|
|
24
|
-
const
|
|
24
|
+
const focusedRuntimeSignals = focusRuntimeSignals(runtime.runtimeSignals, contract.files);
|
|
25
|
+
const uncoveredBranches = focusedRuntimeSignals
|
|
25
26
|
.filter((signal) => signal.kind === 'branch')
|
|
26
27
|
.filter((signal) => !specText.includes(signal.name));
|
|
27
28
|
const repoPaths = unique([
|
|
@@ -39,3 +40,14 @@ export async function analyzeTestGaps(input, context) {
|
|
|
39
40
|
'Review container variants separately when only the base widget appears covered.',
|
|
40
41
|
], warnings, repoPaths);
|
|
41
42
|
}
|
|
43
|
+
function focusRuntimeSignals(signals, contractFiles) {
|
|
44
|
+
if (contractFiles.length === 0) {
|
|
45
|
+
return signals;
|
|
46
|
+
}
|
|
47
|
+
const contractFileSet = new Set(contractFiles.map(normalizePath));
|
|
48
|
+
const focused = signals.filter((signal) => contractFileSet.has(normalizePath(signal.path)));
|
|
49
|
+
return focused.length > 0 ? focused : signals;
|
|
50
|
+
}
|
|
51
|
+
function normalizePath(filePath) {
|
|
52
|
+
return filePath.replaceAll('\\', '/').toLowerCase();
|
|
53
|
+
}
|
|
@@ -6,7 +6,7 @@ export declare function buildRerunScope(input: {
|
|
|
6
6
|
warnings: string[];
|
|
7
7
|
summary: string;
|
|
8
8
|
evidence: {
|
|
9
|
-
type: "symbol" | "
|
|
9
|
+
type: "symbol" | "test" | "config" | "file" | "artifact" | "report" | "diff";
|
|
10
10
|
label: string;
|
|
11
11
|
path?: string | undefined;
|
|
12
12
|
locator?: string | undefined;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { analyzeDiffImpact } from './analyzeDiffImpact.js';
|
|
2
2
|
import { output } from './shared.js';
|
|
3
|
+
import { unique } from '../utils/pathUtils.js';
|
|
3
4
|
export async function buildRerunScope(input, context) {
|
|
4
5
|
const stats = context.ciArtifacts.readStats(input.statsFile);
|
|
5
6
|
const diffImpact = input.changedFiles?.length
|
|
@@ -10,8 +11,12 @@ export async function buildRerunScope(input, context) {
|
|
|
10
11
|
...(diffImpact?.repo_paths.filter((repoPath) => repoPath.endsWith('.cy.js')) ?? []),
|
|
11
12
|
];
|
|
12
13
|
const uniqueSpecs = [...new Set(rerunSpecs)];
|
|
14
|
+
const warnings = unique([
|
|
15
|
+
...(diffImpact?.warnings ?? []),
|
|
16
|
+
...(uniqueSpecs.length === 0 ? ['No explicit rerun spec candidates were discovered.'] : []),
|
|
17
|
+
]);
|
|
13
18
|
return output(`Built rerun scope with ${uniqueSpecs.length} spec candidate(s).`, uniqueSpecs.map((repoPath) => ({ type: 'test', label: 'rerun spec', path: repoPath })), [
|
|
14
19
|
'Run the union of failed specs and impacted specs first.',
|
|
15
20
|
'Keep the rerun pack minimal before escalating to a broader suite.',
|
|
16
|
-
],
|
|
21
|
+
], warnings);
|
|
17
22
|
}
|
|
@@ -6,7 +6,7 @@ export declare function buildSetupTeardownPlan(input: {
|
|
|
6
6
|
warnings: string[];
|
|
7
7
|
summary: string;
|
|
8
8
|
evidence: {
|
|
9
|
-
type: "symbol" | "
|
|
9
|
+
type: "symbol" | "test" | "config" | "file" | "artifact" | "report" | "diff";
|
|
10
10
|
label: string;
|
|
11
11
|
path?: string | undefined;
|
|
12
12
|
locator?: string | undefined;
|
|
@@ -6,7 +6,7 @@ export declare function findBackendTestContext(input: FindBackendTestContextInpu
|
|
|
6
6
|
warnings: string[];
|
|
7
7
|
summary: string;
|
|
8
8
|
evidence: {
|
|
9
|
-
type: "symbol" | "
|
|
9
|
+
type: "symbol" | "test" | "config" | "file" | "artifact" | "report" | "diff";
|
|
10
10
|
label: string;
|
|
11
11
|
path?: string | undefined;
|
|
12
12
|
locator?: string | undefined;
|
|
@@ -2,11 +2,15 @@ import { queryDocuments } from '../knowledge/repositories.js';
|
|
|
2
2
|
import { ensureIndexed, evidenceFile, output } from './shared.js';
|
|
3
3
|
export async function findBackendTestContext(input, context) {
|
|
4
4
|
await ensureIndexed(context);
|
|
5
|
+
const queryTokens = normalizeTokens(input.query);
|
|
6
|
+
const queryPaths = extractQueryPaths(input.query);
|
|
5
7
|
const interactionHits = highConfidence(queryDocuments(context.database, input.query, {
|
|
6
8
|
repoKind: 'autotests2',
|
|
7
9
|
sourceKinds: ['api-interaction'],
|
|
8
10
|
limit: 6,
|
|
9
|
-
}))
|
|
11
|
+
}))
|
|
12
|
+
.filter((item) => isRelevantInteraction(item, queryTokens, queryPaths))
|
|
13
|
+
.slice(0, 3);
|
|
10
14
|
const interactionBridgeQuery = interactionHits
|
|
11
15
|
.flatMap((item) => {
|
|
12
16
|
const metadata = parseMetadata(item.metadata_json);
|
|
@@ -21,20 +25,23 @@ export async function findBackendTestContext(input, context) {
|
|
|
21
25
|
repoKind: 'fis-platform',
|
|
22
26
|
sourceKinds: ['backend-endpoint'],
|
|
23
27
|
limit: 8,
|
|
24
|
-
}))
|
|
25
|
-
|
|
26
|
-
.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
: []
|
|
28
|
+
}))
|
|
29
|
+
.filter((item) => isRelevantBackendEndpoint(item, interactionHits, queryTokens, queryPaths))
|
|
30
|
+
.slice(0, 6);
|
|
31
|
+
const evidencePaths = [...interactionHits, ...backendHits]
|
|
32
|
+
.flatMap((item) => parseMetadata(item.metadata_json).evidencePaths ?? []);
|
|
33
|
+
const exactLinkedAssets = findDocumentsByPaths(context, evidencePaths);
|
|
34
|
+
const linkedSpecQuery = [
|
|
35
|
+
input.query,
|
|
36
|
+
...interactionHits.map((item) => item.title),
|
|
37
|
+
...interactionHits.flatMap((item) => parseMetadata(item.metadata_json).normalizedPaths ?? []),
|
|
38
|
+
].join(' ');
|
|
39
|
+
const linkedSpecAssets = queryDocuments(context.database, linkedSpecQuery, {
|
|
40
|
+
repoKind: 'autotests2',
|
|
41
|
+
sourceKinds: ['spec', 'command', 'helper'],
|
|
42
|
+
limit: 8,
|
|
43
|
+
}).filter((item) => isRelevantLinkedAsset(item, queryTokens));
|
|
44
|
+
const linkedAssets = uniqueDocumentHits([...exactLinkedAssets, ...linkedSpecAssets]).slice(0, 8);
|
|
38
45
|
const evidence = [
|
|
39
46
|
...interactionHits.map((item) => evidenceFile('api interaction', item.path, truncate(item.body, 260), {
|
|
40
47
|
retrieval: 'knowledge-store',
|
|
@@ -67,6 +74,84 @@ export async function findBackendTestContext(input, context) {
|
|
|
67
74
|
function highConfidence(items) {
|
|
68
75
|
return items.filter((item) => (parseMetadata(item.metadata_json).confidence ?? 0) >= 0.7);
|
|
69
76
|
}
|
|
77
|
+
function findDocumentsByPaths(context, paths) {
|
|
78
|
+
const uniquePaths = [...new Set(paths)];
|
|
79
|
+
if (uniquePaths.length === 0) {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
const statement = context.database.prepare(`
|
|
83
|
+
SELECT id, path, title, body, metadata_json, 0 AS rank
|
|
84
|
+
FROM documents
|
|
85
|
+
WHERE repo_kind = 'autotests2'
|
|
86
|
+
AND source_kind IN ('spec', 'command', 'helper')
|
|
87
|
+
AND path = ?
|
|
88
|
+
`);
|
|
89
|
+
return uniquePaths.flatMap((filePath) => statement.all(filePath));
|
|
90
|
+
}
|
|
91
|
+
function uniqueDocumentHits(items) {
|
|
92
|
+
const seen = new Map();
|
|
93
|
+
for (const item of items) {
|
|
94
|
+
seen.set(item.id, item);
|
|
95
|
+
}
|
|
96
|
+
return [...seen.values()];
|
|
97
|
+
}
|
|
98
|
+
function isRelevantLinkedAsset(item, queryTokens) {
|
|
99
|
+
const searchable = normalizeTokens(`${item.title} ${item.path} ${item.body.slice(0, 1200)}`);
|
|
100
|
+
return queryTokens.some((token) => searchable.includes(token));
|
|
101
|
+
}
|
|
102
|
+
function isRelevantInteraction(item, queryTokens, queryPaths) {
|
|
103
|
+
const metadata = parseMetadata(item.metadata_json);
|
|
104
|
+
const normalizedPaths = metadata.normalizedPaths ?? [];
|
|
105
|
+
if (queryPaths.length > 0) {
|
|
106
|
+
return normalizedPaths.some((indexedPath) => queryPaths.some((queryPath) => pathsOverlap(indexedPath, queryPath)));
|
|
107
|
+
}
|
|
108
|
+
const searchable = normalizeTokens(`${item.title} ${item.path} ${item.body} ${normalizedPaths.join(' ')}`);
|
|
109
|
+
return queryTokens.some((token) => searchable.includes(token));
|
|
110
|
+
}
|
|
111
|
+
function isRelevantBackendEndpoint(item, interactions, queryTokens, queryPaths) {
|
|
112
|
+
const metadata = parseMetadata(item.metadata_json);
|
|
113
|
+
const endpointPath = metadata.endpointPath ?? item.title;
|
|
114
|
+
const interactionPaths = interactions.flatMap((interaction) => parseMetadata(interaction.metadata_json).normalizedPaths ?? []);
|
|
115
|
+
const endpointTokens = normalizeTokens(endpointPath);
|
|
116
|
+
if (queryPaths.length > 0 || interactionPaths.length > 0) {
|
|
117
|
+
return queryPaths.some((candidate) => pathsOverlap(endpointPath, candidate))
|
|
118
|
+
|| queryTokens.some((token) => endpointTokens.includes(token))
|
|
119
|
+
|| (queryPaths.length === 0 && interactionPaths.some((candidate) => pathsOverlap(endpointPath, candidate)));
|
|
120
|
+
}
|
|
121
|
+
const searchable = normalizeTokens(`${item.title} ${item.path} ${item.body} ${endpointPath}`);
|
|
122
|
+
return queryTokens.some((token) => searchable.includes(token));
|
|
123
|
+
}
|
|
124
|
+
function extractQueryPaths(query) {
|
|
125
|
+
return [...query.matchAll(/(?:https?:\/\/[^/\s]+)?(\/[A-Za-z0-9_/${}?.=&*-]+)+/g)]
|
|
126
|
+
.map((match) => match[0])
|
|
127
|
+
.map((value) => normalizePath(value))
|
|
128
|
+
.filter((value) => normalizeTokens(value).length >= 2)
|
|
129
|
+
.filter(Boolean);
|
|
130
|
+
}
|
|
131
|
+
function pathsOverlap(left, right) {
|
|
132
|
+
const leftTokens = normalizeTokens(normalizePath(left));
|
|
133
|
+
const rightTokens = normalizeTokens(normalizePath(right));
|
|
134
|
+
const sharedTokens = leftTokens.filter((token) => rightTokens.includes(token));
|
|
135
|
+
return normalizePath(left).includes(normalizePath(right))
|
|
136
|
+
|| normalizePath(right).includes(normalizePath(left))
|
|
137
|
+
|| (leftTokens.length > 0 && rightTokens.length > 0 && sharedTokens.length >= Math.min(2, rightTokens.length));
|
|
138
|
+
}
|
|
139
|
+
function normalizePath(value) {
|
|
140
|
+
try {
|
|
141
|
+
const parsed = new URL(value);
|
|
142
|
+
return parsed.pathname;
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return (value.split('?')[0] ?? value).replace(/^\$\{[^}]+\}/, '');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function normalizeTokens(value) {
|
|
149
|
+
return value
|
|
150
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
151
|
+
.toLowerCase()
|
|
152
|
+
.split(/[^a-z0-9]+/)
|
|
153
|
+
.filter((token) => token.length > 1 && !/^(platform|rs2|api|rest|sec)$/.test(token));
|
|
154
|
+
}
|
|
70
155
|
function parseMetadata(metadataJson) {
|
|
71
156
|
try {
|
|
72
157
|
return JSON.parse(metadataJson);
|
|
@@ -5,7 +5,7 @@ export declare function findExistingTestAssets(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;
|
|
@@ -5,7 +5,7 @@ export declare function findFrontendContract(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;
|
|
@@ -5,7 +5,7 @@ export declare function findFrontendRuntimeLogic(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;
|
|
@@ -6,7 +6,7 @@ export declare function findTestAreaContext(input: {
|
|
|
6
6
|
warnings: string[];
|
|
7
7
|
summary: string;
|
|
8
8
|
evidence: {
|
|
9
|
-
type: "symbol" | "
|
|
9
|
+
type: "symbol" | "test" | "config" | "file" | "artifact" | "report" | "diff";
|
|
10
10
|
label: string;
|
|
11
11
|
path?: string | undefined;
|
|
12
12
|
locator?: string | undefined;
|
|
@@ -6,7 +6,7 @@ export declare function generateSpecBlueprint(input: {
|
|
|
6
6
|
warnings: string[];
|
|
7
7
|
summary: string;
|
|
8
8
|
evidence: {
|
|
9
|
-
type: "symbol" | "
|
|
9
|
+
type: "symbol" | "test" | "config" | "file" | "artifact" | "report" | "diff";
|
|
10
10
|
label: string;
|
|
11
11
|
path?: string | undefined;
|
|
12
12
|
locator?: string | undefined;
|
|
@@ -5,7 +5,7 @@ export declare function mapFeatureToExistingCoverage(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;
|
|
@@ -5,7 +5,7 @@ export declare function recommendSelectorStrategy(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;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { AppContext } from '../appContext.js';
|
|
2
|
+
type ReviewTestDraftInput = {
|
|
3
|
+
feature: string;
|
|
4
|
+
draft: string;
|
|
5
|
+
area?: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function reviewTestDraft(input: ReviewTestDraftInput, context: AppContext): Promise<{
|
|
8
|
+
warnings: string[];
|
|
9
|
+
summary: string;
|
|
10
|
+
evidence: {
|
|
11
|
+
type: "symbol" | "test" | "config" | "file" | "artifact" | "report" | "diff";
|
|
12
|
+
label: string;
|
|
13
|
+
path?: string | undefined;
|
|
14
|
+
locator?: string | undefined;
|
|
15
|
+
snippet?: string | undefined;
|
|
16
|
+
metadata?: Record<string, unknown> | undefined;
|
|
17
|
+
}[];
|
|
18
|
+
recommended_actions: string[];
|
|
19
|
+
repo_paths: string[];
|
|
20
|
+
confidence: {
|
|
21
|
+
score: number;
|
|
22
|
+
level: "low" | "medium" | "high";
|
|
23
|
+
rationale: string[];
|
|
24
|
+
};
|
|
25
|
+
}>;
|
|
26
|
+
export {};
|