@mknrt/autotests-overkill 1.1.3 → 1.1.5
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 +17 -5
- package/dist/src/appContext.d.ts +2 -0
- package/dist/src/appContext.js +4 -0
- package/dist/src/config/consumerConfig.d.ts +5 -0
- package/dist/src/config/consumerConfig.js +5 -0
- package/dist/src/connectors/autotests2Connector.d.ts +8 -0
- package/dist/src/connectors/autotests2Connector.js +7 -0
- package/dist/src/connectors/fisPlatformConnector.d.ts +15 -0
- package/dist/src/connectors/fisPlatformConnector.js +112 -0
- package/dist/src/domain/analyzeDiffImpact.js +66 -0
- package/dist/src/domain/findBackendTestContext.d.ts +24 -0
- package/dist/src/domain/findBackendTestContext.js +165 -0
- package/dist/src/domain/generateSpecBlueprint.js +48 -0
- package/dist/src/domain/triageFailedRun.js +97 -0
- package/dist/src/indexer/extractors/apiDiscoveryExtractor.d.ts +23 -0
- package/dist/src/indexer/extractors/apiDiscoveryExtractor.js +183 -0
- package/dist/src/indexer/extractors/apiInteractionExtractor.d.ts +23 -0
- package/dist/src/indexer/extractors/apiInteractionExtractor.js +35 -0
- package/dist/src/indexer/extractors/backendContractExtractor.d.ts +26 -0
- package/dist/src/indexer/extractors/backendContractExtractor.js +66 -0
- package/dist/src/indexer/extractors/frontendContractExtractor.js +10 -2
- package/dist/src/indexer/refreshPipeline.js +40 -0
- package/dist/src/mcp/registerTools.js +8 -0
- package/docs/operator-cookbook.md +14 -3
- package/docs/superpowers/plans/2026-04-20-fis-platform-backend-integration.md +980 -0
- package/docs/tool-catalog.md +12 -3
- package/overkill.config.example.json +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Autotests Overkill
|
|
2
2
|
|
|
3
|
-
`Autotests Overkill` is a standalone AI-assisted testing platform for `autotests2`, `caseplatform-web`, and CI artifacts. It is designed for Codex-style work: inspect existing coverage, extract frontend contracts and runtime logic, generate draft specs, triage failures, and narrow rerun scope without silently mutating consumer repositories.
|
|
3
|
+
`Autotests Overkill` is a standalone AI-assisted testing platform for `autotests2`, `caseplatform-web`, optional `fis_platform` backend context, and CI artifacts. It is designed for Codex-style work: inspect existing coverage, extract frontend/backend contracts and runtime logic, generate draft specs, triage failures, and narrow rerun scope without silently mutating consumer repositories.
|
|
4
4
|
|
|
5
5
|
## What v1.1 adds
|
|
6
6
|
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
- Stronger spec blueprints that reuse real `autotests2` setup, commands, helpers, and selector patterns
|
|
9
9
|
- Acceptance coverage for both modern and legacy widgets
|
|
10
10
|
- Knowledge-store-backed retrieval for asset search, area context, impact analysis, and blueprint support
|
|
11
|
+
- Optional `fis_platform` backend knowledge through an evidence-driven `ApiDiscoveryCatalog`, `api-interaction` documents, and `backend-endpoint` contracts
|
|
11
12
|
- Optional ReportPortal and GitLab adapters with graceful artifacts-first fallback
|
|
12
13
|
- Codex plugin packaging via `.codex-plugin/plugin.json` and `.mcp.json`
|
|
13
14
|
|
|
@@ -28,14 +29,14 @@ Published npm package install:
|
|
|
28
29
|
|
|
29
30
|
1. Run `npm install -D @mknrt/autotests-overkill`.
|
|
30
31
|
2. Copy `node_modules/@mknrt/autotests-overkill/overkill.config.example.json` to `overkill.config.json`.
|
|
31
|
-
3. Set `repos.autotests2`, `repos.caseplatformWeb`, and `ci.artifactsRoot`.
|
|
32
|
+
3. Set `repos.autotests2`, `repos.caseplatformWeb`, optional `repos.fisPlatform`, and `ci.artifactsRoot`.
|
|
32
33
|
4. Run `npx autotests-overkill index`.
|
|
33
34
|
|
|
34
35
|
Repository-local development:
|
|
35
36
|
|
|
36
37
|
1. Run `npm install`.
|
|
37
38
|
2. Copy `overkill.config.example.json` to `overkill.config.json`.
|
|
38
|
-
3. Set `repos.autotests2`, `repos.caseplatformWeb`, and `ci.artifactsRoot`.
|
|
39
|
+
3. Set `repos.autotests2`, `repos.caseplatformWeb`, optional `repos.fisPlatform`, and `ci.artifactsRoot`.
|
|
39
40
|
4. Run `npm run build`.
|
|
40
41
|
5. Run `npm run index`.
|
|
41
42
|
|
|
@@ -44,8 +45,9 @@ Repository-local development:
|
|
|
44
45
|
1. Refresh local knowledge with `npm run index`.
|
|
45
46
|
2. Start the MCP server through the checked-in MCP manifest or by running `npx autotests-overkill mcp`.
|
|
46
47
|
3. Use `find_frontend_contract`, `find_existing_test_assets`, and `generate_spec_blueprint` for a new feature draft.
|
|
47
|
-
4. Use `
|
|
48
|
-
5.
|
|
48
|
+
4. Use `find_backend_test_context` when a request flow needs backend validation, params, async completion, or contract-drift evidence.
|
|
49
|
+
5. Use `summarize_ci_context`, `triage_failed_run`, and `analyze_diff_impact` for failure or change triage.
|
|
50
|
+
6. Review generated output manually before editing `autotests2`.
|
|
49
51
|
|
|
50
52
|
CLI commands:
|
|
51
53
|
|
|
@@ -70,6 +72,7 @@ The platform reads `overkill.config.json` with these keys:
|
|
|
70
72
|
|
|
71
73
|
- `repos.autotests2`
|
|
72
74
|
- `repos.caseplatformWeb`
|
|
75
|
+
- `repos.fisPlatform` optional path to `../fis_platform`
|
|
73
76
|
- `ci.artifactsRoot`
|
|
74
77
|
- `ci.reportPortal.endpoint`
|
|
75
78
|
- `ci.reportPortal.project`
|
|
@@ -81,9 +84,18 @@ The platform reads `overkill.config.json` with these keys:
|
|
|
81
84
|
|
|
82
85
|
The default path is local and artifacts-first. Missing ReportPortal or GitLab credentials do not break tools; the platform keeps using local stats, screenshots, videos, and captured API payloads.
|
|
83
86
|
|
|
87
|
+
## Backend integration
|
|
88
|
+
|
|
89
|
+
Backend indexing is discovery-first and evidence-driven, not hardcoded. The first bridge is an `ApiDiscoveryCatalog` built from `autotests2` support code such as request wrappers, endpoint constants, `cy.request`, `fetch`, `XMLHttpRequest`, and API capture helpers. Selected high-signal families become `api-interaction` documents with `normalizedPaths`, `consumerSignalIds`, `evidencePaths`, `confidence`, and `whySelected`.
|
|
90
|
+
|
|
91
|
+
When `repos.fisPlatform` points to `../fis_platform`, refresh passes that catalog explicitly into backend discovery. `fis_platform` sources are selected by REST/JAX-RS signals such as `@Path`, `@GET`, `@POST`, `@PUT`, `@DELETE`, `@QueryParam`, `@HeaderParam`, `@Consumes`, and `@Produces`; noisy directories such as `thirdparty`, `extjs`, `oryx`, `generated`, `node_modules`, and `vendor` are excluded by rule. Matching sources become `backend-endpoint` documents with endpoint paths, params, rule hints, evidence paths, confidence, and `whyMatched`.
|
|
92
|
+
|
|
93
|
+
Only high-confidence links (`confidence >= 0.7`) drive blueprint, triage, and impact recommendations. Lower-confidence backend matches may be surfaced as exploratory evidence, but they are not treated as recommendations. Runtime API captures are read on demand by `triage_failed_run` in iteration 1 and are not persisted as raw knowledge-store documents.
|
|
94
|
+
|
|
84
95
|
## MCP tools
|
|
85
96
|
|
|
86
97
|
- `find_existing_test_assets`
|
|
98
|
+
- `find_backend_test_context`
|
|
87
99
|
- `find_test_area_context`
|
|
88
100
|
- `find_frontend_contract`
|
|
89
101
|
- `find_frontend_runtime_logic`
|
package/dist/src/appContext.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Autotests2Connector } from './connectors/autotests2Connector.js';
|
|
2
2
|
import { CaseplatformConnector } from './connectors/caseplatformConnector.js';
|
|
3
3
|
import { CiArtifactsConnector } from './connectors/ciArtifactsConnector.js';
|
|
4
|
+
import { FisPlatformConnector } from './connectors/fisPlatformConnector.js';
|
|
4
5
|
import { GitLabConnector } from './connectors/gitlabConnector.js';
|
|
5
6
|
import { ReportPortalConnector } from './connectors/reportPortalConnector.js';
|
|
6
7
|
import { type ConsumerConfig, type ConsumerConfigInput } from './config/consumerConfig.js';
|
|
@@ -10,6 +11,7 @@ export type AppContext = {
|
|
|
10
11
|
database: ReturnType<typeof createKnowledgeDatabase>;
|
|
11
12
|
autotests2: Autotests2Connector;
|
|
12
13
|
caseplatform: CaseplatformConnector;
|
|
14
|
+
fisPlatform?: FisPlatformConnector;
|
|
13
15
|
ciArtifacts: CiArtifactsConnector;
|
|
14
16
|
gitlab: GitLabConnector;
|
|
15
17
|
reportPortal: ReportPortalConnector;
|
package/dist/src/appContext.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { Autotests2Connector } from './connectors/autotests2Connector.js';
|
|
4
4
|
import { CaseplatformConnector } from './connectors/caseplatformConnector.js';
|
|
5
5
|
import { CiArtifactsConnector } from './connectors/ciArtifactsConnector.js';
|
|
6
|
+
import { FisPlatformConnector } from './connectors/fisPlatformConnector.js';
|
|
6
7
|
import { GitLabConnector } from './connectors/gitlabConnector.js';
|
|
7
8
|
import { ReportPortalConnector } from './connectors/reportPortalConnector.js';
|
|
8
9
|
import { createDefaultConsumerConfig, loadConsumerConfig, parseConsumerConfig } from './config/consumerConfig.js';
|
|
@@ -19,6 +20,9 @@ export async function createAppContext(input) {
|
|
|
19
20
|
database,
|
|
20
21
|
autotests2: new Autotests2Connector(config.repos.autotests2),
|
|
21
22
|
caseplatform: new CaseplatformConnector(config.repos.caseplatformWeb),
|
|
23
|
+
fisPlatform: config.repos.fisPlatform
|
|
24
|
+
? new FisPlatformConnector(config.repos.fisPlatform)
|
|
25
|
+
: undefined,
|
|
22
26
|
ciArtifacts: new CiArtifactsConnector(),
|
|
23
27
|
gitlab: new GitLabConnector(config.ci.gitlab),
|
|
24
28
|
reportPortal: new ReportPortalConnector(config.ci.reportPortal),
|
|
@@ -3,12 +3,15 @@ export declare const consumerConfigSchema: z.ZodObject<{
|
|
|
3
3
|
repos: z.ZodObject<{
|
|
4
4
|
autotests2: z.ZodString;
|
|
5
5
|
caseplatformWeb: z.ZodString;
|
|
6
|
+
fisPlatform: z.ZodOptional<z.ZodString>;
|
|
6
7
|
}, "strict", z.ZodTypeAny, {
|
|
7
8
|
autotests2: string;
|
|
8
9
|
caseplatformWeb: string;
|
|
10
|
+
fisPlatform?: string | undefined;
|
|
9
11
|
}, {
|
|
10
12
|
autotests2: string;
|
|
11
13
|
caseplatformWeb: string;
|
|
14
|
+
fisPlatform?: string | undefined;
|
|
12
15
|
}>;
|
|
13
16
|
ci: z.ZodObject<{
|
|
14
17
|
artifactsRoot: z.ZodString;
|
|
@@ -75,6 +78,7 @@ export declare const consumerConfigSchema: z.ZodObject<{
|
|
|
75
78
|
repos: {
|
|
76
79
|
autotests2: string;
|
|
77
80
|
caseplatformWeb: string;
|
|
81
|
+
fisPlatform?: string | undefined;
|
|
78
82
|
};
|
|
79
83
|
ci: {
|
|
80
84
|
artifactsRoot: string;
|
|
@@ -97,6 +101,7 @@ export declare const consumerConfigSchema: z.ZodObject<{
|
|
|
97
101
|
repos: {
|
|
98
102
|
autotests2: string;
|
|
99
103
|
caseplatformWeb: string;
|
|
104
|
+
fisPlatform?: string | undefined;
|
|
100
105
|
};
|
|
101
106
|
ci: {
|
|
102
107
|
artifactsRoot: string;
|
|
@@ -15,6 +15,7 @@ export const consumerConfigSchema = z.object({
|
|
|
15
15
|
repos: z.object({
|
|
16
16
|
autotests2: z.string().min(1),
|
|
17
17
|
caseplatformWeb: z.string().min(1),
|
|
18
|
+
fisPlatform: z.string().min(1).optional(),
|
|
18
19
|
}).strict(),
|
|
19
20
|
ci: z.object({
|
|
20
21
|
artifactsRoot: z.string().min(1),
|
|
@@ -36,6 +37,9 @@ export function parseConsumerConfig(input, baseDir = process.cwd()) {
|
|
|
36
37
|
repos: {
|
|
37
38
|
autotests2: resolveRepoPath(parsed.repos.autotests2, baseDir),
|
|
38
39
|
caseplatformWeb: resolveRepoPath(parsed.repos.caseplatformWeb, baseDir),
|
|
40
|
+
fisPlatform: parsed.repos.fisPlatform
|
|
41
|
+
? resolveRepoPath(parsed.repos.fisPlatform, baseDir)
|
|
42
|
+
: undefined,
|
|
39
43
|
},
|
|
40
44
|
ci: {
|
|
41
45
|
artifactsRoot: resolveRepoPath(parsed.ci.artifactsRoot, baseDir),
|
|
@@ -54,6 +58,7 @@ export function createDefaultConsumerConfig(baseDir = process.cwd()) {
|
|
|
54
58
|
repos: {
|
|
55
59
|
autotests2: '../autotests2',
|
|
56
60
|
caseplatformWeb: '../caseplatform-web',
|
|
61
|
+
fisPlatform: '../fis_platform',
|
|
57
62
|
},
|
|
58
63
|
ci: {
|
|
59
64
|
artifactsRoot: '../autotests2/cypress',
|
|
@@ -14,12 +14,20 @@ export type HelperMatch = {
|
|
|
14
14
|
name: string;
|
|
15
15
|
body: string;
|
|
16
16
|
};
|
|
17
|
+
export type ApiSupportSource = {
|
|
18
|
+
path: string;
|
|
19
|
+
body: string;
|
|
20
|
+
};
|
|
17
21
|
export declare class Autotests2Connector {
|
|
18
22
|
private readonly root;
|
|
19
23
|
private readonly fs;
|
|
20
24
|
constructor(root: string);
|
|
21
25
|
findSpecs(featureHint?: string): Promise<TestSpecMatch[]>;
|
|
22
26
|
findCommands(): Promise<CommandMatch[]>;
|
|
27
|
+
findApiSupportSources(): Promise<{
|
|
28
|
+
path: string;
|
|
29
|
+
body: string;
|
|
30
|
+
}[]>;
|
|
23
31
|
findHelpers(featureHint?: string): Promise<HelperMatch[]>;
|
|
24
32
|
getAreaDocs(): {
|
|
25
33
|
contextPath: string;
|
|
@@ -40,6 +40,13 @@ export class Autotests2Connector {
|
|
|
40
40
|
}
|
|
41
41
|
return dedupeByName(results);
|
|
42
42
|
}
|
|
43
|
+
async findApiSupportSources() {
|
|
44
|
+
const files = await this.fs.glob(this.root, ['cypress/support/**/*.js']);
|
|
45
|
+
const apiSignalPattern = /cy\.request|Cypress\.Commands\.add|fetch\(|XMLHttpRequest|apiCapture|request|\/platform\/|\/rs2\//i;
|
|
46
|
+
return files
|
|
47
|
+
.map((filePath) => ({ path: filePath, body: this.fs.read(filePath) }))
|
|
48
|
+
.filter((source) => apiSignalPattern.test(source.body));
|
|
49
|
+
}
|
|
43
50
|
async findHelpers(featureHint) {
|
|
44
51
|
const files = await this.fs.glob(this.root, ['cypress/support/functions/**/*.js']);
|
|
45
52
|
const results = [];
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ApiDiscoveryCatalog } from '../indexer/extractors/apiDiscoveryExtractor.js';
|
|
2
|
+
export type FocusedFisSource = {
|
|
3
|
+
path: string;
|
|
4
|
+
body: string;
|
|
5
|
+
sourceKind: 'backend-resource' | 'backend-interface' | 'backend-openapi-config';
|
|
6
|
+
matchedFamily: string;
|
|
7
|
+
confidence: number;
|
|
8
|
+
whyMatched: string[];
|
|
9
|
+
};
|
|
10
|
+
export declare class FisPlatformConnector {
|
|
11
|
+
readonly root: string;
|
|
12
|
+
private readonly fs;
|
|
13
|
+
constructor(root: string);
|
|
14
|
+
listFocusedSources(apiCatalog: ApiDiscoveryCatalog): Promise<FocusedFisSource[]>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { FilesystemConnector } from './filesystemConnector.js';
|
|
2
|
+
const REST_SIGNAL_PATTERN = /@Path|@(GET|POST|PUT|DELETE)|@QueryParam|@HeaderParam|@Consumes|@Produces/;
|
|
3
|
+
const NOISE_PATH_PATTERN = /(?:^|[\\/])(thirdparty|extjs|oryx|generated|node_modules|vendor)(?:[\\/]|$)/i;
|
|
4
|
+
export class FisPlatformConnector {
|
|
5
|
+
root;
|
|
6
|
+
fs = new FilesystemConnector();
|
|
7
|
+
constructor(root) {
|
|
8
|
+
this.root = root;
|
|
9
|
+
}
|
|
10
|
+
async listFocusedSources(apiCatalog) {
|
|
11
|
+
const files = await this.fs.glob(this.root, [
|
|
12
|
+
'**/*.java',
|
|
13
|
+
'**/*openapi*.yaml',
|
|
14
|
+
'**/*openapi*.yml',
|
|
15
|
+
'**/*openapi*.json',
|
|
16
|
+
]);
|
|
17
|
+
const results = [];
|
|
18
|
+
const selectedFamilies = apiCatalog.families.filter((family) => family.score > 0);
|
|
19
|
+
for (const filePath of files) {
|
|
20
|
+
if (NOISE_PATH_PATTERN.test(filePath)) {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const body = this.fs.read(filePath);
|
|
24
|
+
if (!REST_SIGNAL_PATTERN.test(body) && !/openapi|swagger/i.test(filePath)) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
for (const family of selectedFamilies) {
|
|
28
|
+
const match = matchBackendSourceToFamily({ path: filePath, body }, family);
|
|
29
|
+
if (match.confidence >= 0.4) {
|
|
30
|
+
results.push({
|
|
31
|
+
path: filePath,
|
|
32
|
+
body,
|
|
33
|
+
sourceKind: inferSourceKind(filePath, body),
|
|
34
|
+
matchedFamily: family.family,
|
|
35
|
+
confidence: match.confidence,
|
|
36
|
+
whyMatched: match.whyMatched,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return results.sort((left, right) => {
|
|
42
|
+
if (right.confidence !== left.confidence) {
|
|
43
|
+
return right.confidence - left.confidence;
|
|
44
|
+
}
|
|
45
|
+
return left.path.localeCompare(right.path);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function matchBackendSourceToFamily(source, family) {
|
|
50
|
+
const backendPaths = extractBackendPaths(source.body);
|
|
51
|
+
const bodyTokens = normalizeTokens(`${source.path}\n${source.body}`);
|
|
52
|
+
const familyToken = family.family.toLowerCase();
|
|
53
|
+
const whyMatched = [];
|
|
54
|
+
let score = 0;
|
|
55
|
+
if (bodyTokens.includes(familyToken)) {
|
|
56
|
+
score += 0.25;
|
|
57
|
+
whyMatched.push(`resource text contains selected API family '${family.family}'`);
|
|
58
|
+
}
|
|
59
|
+
for (const consumerPath of family.normalizedPaths) {
|
|
60
|
+
const consumerTokens = normalizeTokens(consumerPath);
|
|
61
|
+
if (backendPaths.some((backendPath) => pathsOverlap(backendPath, consumerPath))) {
|
|
62
|
+
score += 0.55;
|
|
63
|
+
whyMatched.push(`backend @Path overlaps consumer path ${consumerPath}`);
|
|
64
|
+
}
|
|
65
|
+
else if (consumerTokens.some((token) => bodyTokens.includes(token))) {
|
|
66
|
+
score += 0.3;
|
|
67
|
+
whyMatched.push(`backend source shares path token(s) with ${consumerPath}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (/@(GET|POST|PUT|DELETE)/.test(source.body)) {
|
|
71
|
+
score += 0.15;
|
|
72
|
+
whyMatched.push('backend source declares HTTP method annotation');
|
|
73
|
+
}
|
|
74
|
+
if (/@QueryParam|@HeaderParam/.test(source.body)) {
|
|
75
|
+
score += 0.05;
|
|
76
|
+
whyMatched.push('backend source declares request parameter annotations');
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
confidence: Math.min(1, score),
|
|
80
|
+
whyMatched,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function inferSourceKind(path, body) {
|
|
84
|
+
if (/openapi|swagger/i.test(path)) {
|
|
85
|
+
return 'backend-openapi-config';
|
|
86
|
+
}
|
|
87
|
+
if (/interface\s+\w+/.test(body)) {
|
|
88
|
+
return 'backend-interface';
|
|
89
|
+
}
|
|
90
|
+
return 'backend-resource';
|
|
91
|
+
}
|
|
92
|
+
function extractBackendPaths(body) {
|
|
93
|
+
return [...body.matchAll(/@Path\(\s*"([^"]+)"\s*\)/g)]
|
|
94
|
+
.map((match) => match[1])
|
|
95
|
+
.filter((value) => Boolean(value))
|
|
96
|
+
.map((value) => normalizePath(value));
|
|
97
|
+
}
|
|
98
|
+
function normalizePath(value) {
|
|
99
|
+
return value.startsWith('/') ? value : `/${value}`;
|
|
100
|
+
}
|
|
101
|
+
function pathsOverlap(left, right) {
|
|
102
|
+
const leftTokens = normalizeTokens(left);
|
|
103
|
+
const rightTokens = normalizeTokens(right);
|
|
104
|
+
const sharedTokens = leftTokens.filter((token) => rightTokens.includes(token));
|
|
105
|
+
return sharedTokens.length >= Math.min(2, rightTokens.length);
|
|
106
|
+
}
|
|
107
|
+
function normalizeTokens(value) {
|
|
108
|
+
return value
|
|
109
|
+
.toLowerCase()
|
|
110
|
+
.split(/[^a-z0-9]+/)
|
|
111
|
+
.filter((token) => token.length > 1 && !/^(platform|rs2|api|rest|java|src|main)$/.test(token));
|
|
112
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs';
|
|
2
3
|
import { normalizeChangedFiles } from '../connectors/diffConnector.js';
|
|
3
4
|
import { queryDocuments } from '../knowledge/repositories.js';
|
|
4
5
|
import { extractTestAssets } from '../indexer/extractors/testAssetExtractor.js';
|
|
@@ -31,6 +32,41 @@ export async function analyzeDiffImpact(input, context) {
|
|
|
31
32
|
impactedSpecs.add(spec.path);
|
|
32
33
|
}
|
|
33
34
|
}
|
|
35
|
+
for (const filePath of changedFiles.filter((item) => isFisPlatformPath(item, context))) {
|
|
36
|
+
const backendFeatureText = deriveFeatureTextFromBackendSource({
|
|
37
|
+
filePath,
|
|
38
|
+
fileBody: readTextIfAvailable(filePath),
|
|
39
|
+
});
|
|
40
|
+
const interactionHits = queryDocuments(context.database, backendFeatureText, {
|
|
41
|
+
repoKind: 'autotests2',
|
|
42
|
+
sourceKinds: ['api-interaction'],
|
|
43
|
+
limit: 8,
|
|
44
|
+
}).filter((item) => (parseMetadata(item.metadata_json).confidence ?? 0) >= 0.7);
|
|
45
|
+
const linkedQuery = interactionHits.flatMap((item) => {
|
|
46
|
+
const metadata = parseMetadata(item.metadata_json);
|
|
47
|
+
return [
|
|
48
|
+
item.title,
|
|
49
|
+
...(metadata.consumerSignalIds ?? []),
|
|
50
|
+
...(metadata.evidencePaths ?? []),
|
|
51
|
+
...(metadata.normalizedPaths ?? []),
|
|
52
|
+
];
|
|
53
|
+
}).join(' ');
|
|
54
|
+
const linkedSpecs = linkedQuery
|
|
55
|
+
? queryDocuments(context.database, linkedQuery, {
|
|
56
|
+
repoKind: 'autotests2',
|
|
57
|
+
sourceKinds: ['spec'],
|
|
58
|
+
limit: 8,
|
|
59
|
+
})
|
|
60
|
+
: [];
|
|
61
|
+
for (const interaction of interactionHits) {
|
|
62
|
+
evidence.push(evidenceFile('api interaction bridge', interaction.path, undefined, {
|
|
63
|
+
confidence: parseMetadata(interaction.metadata_json).confidence,
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
66
|
+
for (const spec of linkedSpecs) {
|
|
67
|
+
impactedSpecs.add(spec.path);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
34
70
|
return output(`Diff impact analysis mapped ${changedFiles.length} changed file(s) to ${impactedSpecs.size} affected spec candidate(s).`, [
|
|
35
71
|
...evidence,
|
|
36
72
|
...[...impactedSpecs].slice(0, 10).map((filePath) => evidenceFile('impacted spec', filePath)),
|
|
@@ -39,6 +75,36 @@ export async function analyzeDiffImpact(input, context) {
|
|
|
39
75
|
'Inspect widget events and container variants when the diff touches shared widget files.',
|
|
40
76
|
], impactedSpecs.size === 0 ? ['No impacted specs were found via naming heuristics.'] : []);
|
|
41
77
|
}
|
|
78
|
+
function isFisPlatformPath(filePath, context) {
|
|
79
|
+
const fisRoot = context.config.repos.fisPlatform;
|
|
80
|
+
return Boolean(fisRoot) && path.resolve(filePath).startsWith(path.resolve(fisRoot));
|
|
81
|
+
}
|
|
82
|
+
function readTextIfAvailable(filePath) {
|
|
83
|
+
try {
|
|
84
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return '';
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function deriveFeatureTextFromBackendSource(input) {
|
|
91
|
+
const pathText = deriveFeatureHint(input.filePath) ?? input.filePath;
|
|
92
|
+
const restSignals = [
|
|
93
|
+
...input.fileBody.matchAll(/@Path\(\s*"([^"]+)"\s*\)/g),
|
|
94
|
+
...input.fileBody.matchAll(/@(GET|POST|PUT|DELETE)/g),
|
|
95
|
+
...input.fileBody.matchAll(/@(?:QueryParam|HeaderParam)\(\s*"([^"]+)"\s*\)/g),
|
|
96
|
+
].map((match) => match[1]).filter(Boolean);
|
|
97
|
+
const className = input.fileBody.match(/\b(?:class|interface)\s+([A-Za-z0-9_]+)/)?.[1];
|
|
98
|
+
return [pathText, className, ...restSignals, input.fileBody.slice(0, 2000)].filter(Boolean).join(' ');
|
|
99
|
+
}
|
|
100
|
+
function parseMetadata(metadataJson) {
|
|
101
|
+
try {
|
|
102
|
+
return JSON.parse(metadataJson);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return {};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
42
108
|
function deriveFeatureHint(filePath) {
|
|
43
109
|
const base = basenameWithoutExt(filePath);
|
|
44
110
|
if (/caption/i.test(base)) {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { AppContext } from '../appContext.js';
|
|
2
|
+
type FindBackendTestContextInput = {
|
|
3
|
+
query: string;
|
|
4
|
+
};
|
|
5
|
+
export declare function findBackendTestContext(input: FindBackendTestContextInput, context: AppContext): Promise<{
|
|
6
|
+
warnings: string[];
|
|
7
|
+
summary: string;
|
|
8
|
+
evidence: {
|
|
9
|
+
type: "symbol" | "config" | "file" | "test" | "artifact" | "report" | "diff";
|
|
10
|
+
label: string;
|
|
11
|
+
path?: string | undefined;
|
|
12
|
+
locator?: string | undefined;
|
|
13
|
+
snippet?: string | undefined;
|
|
14
|
+
metadata?: Record<string, unknown> | undefined;
|
|
15
|
+
}[];
|
|
16
|
+
recommended_actions: string[];
|
|
17
|
+
repo_paths: string[];
|
|
18
|
+
confidence: {
|
|
19
|
+
score: number;
|
|
20
|
+
level: "low" | "medium" | "high";
|
|
21
|
+
rationale: string[];
|
|
22
|
+
};
|
|
23
|
+
}>;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { queryDocuments } from '../knowledge/repositories.js';
|
|
2
|
+
import { ensureIndexed, evidenceFile, output } from './shared.js';
|
|
3
|
+
export async function findBackendTestContext(input, context) {
|
|
4
|
+
await ensureIndexed(context);
|
|
5
|
+
const queryTokens = normalizeTokens(input.query);
|
|
6
|
+
const queryPaths = extractQueryPaths(input.query);
|
|
7
|
+
const interactionHits = highConfidence(queryDocuments(context.database, input.query, {
|
|
8
|
+
repoKind: 'autotests2',
|
|
9
|
+
sourceKinds: ['api-interaction'],
|
|
10
|
+
limit: 6,
|
|
11
|
+
}))
|
|
12
|
+
.filter((item) => isRelevantInteraction(item, queryTokens, queryPaths))
|
|
13
|
+
.slice(0, 3);
|
|
14
|
+
const interactionBridgeQuery = interactionHits
|
|
15
|
+
.flatMap((item) => {
|
|
16
|
+
const metadata = parseMetadata(item.metadata_json);
|
|
17
|
+
return [
|
|
18
|
+
item.title,
|
|
19
|
+
...(metadata.normalizedPaths ?? []),
|
|
20
|
+
...(metadata.evidencePaths ?? []),
|
|
21
|
+
];
|
|
22
|
+
})
|
|
23
|
+
.join(' ');
|
|
24
|
+
const backendHits = highConfidence(queryDocuments(context.database, [input.query, interactionBridgeQuery].join(' '), {
|
|
25
|
+
repoKind: 'fis-platform',
|
|
26
|
+
sourceKinds: ['backend-endpoint'],
|
|
27
|
+
limit: 8,
|
|
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);
|
|
45
|
+
const evidence = [
|
|
46
|
+
...interactionHits.map((item) => evidenceFile('api interaction', item.path, truncate(item.body, 260), {
|
|
47
|
+
retrieval: 'knowledge-store',
|
|
48
|
+
confidence: parseMetadata(item.metadata_json).confidence,
|
|
49
|
+
whySelected: parseMetadata(item.metadata_json).whySelected,
|
|
50
|
+
})),
|
|
51
|
+
...backendHits.map((item) => evidenceFile('backend endpoint', item.path, truncate(item.body, 260), {
|
|
52
|
+
retrieval: 'knowledge-store',
|
|
53
|
+
confidence: parseMetadata(item.metadata_json).confidence,
|
|
54
|
+
whyMatched: parseMetadata(item.metadata_json).whyMatched,
|
|
55
|
+
})),
|
|
56
|
+
...linkedAssets.map((item) => evidenceFile('linked autotest asset', item.path, truncate(item.body, 200), {
|
|
57
|
+
retrieval: 'metadata-evidence-link',
|
|
58
|
+
})),
|
|
59
|
+
];
|
|
60
|
+
const backendParams = backendHits
|
|
61
|
+
.flatMap((item) => parseMetadata(item.metadata_json).paramNames ?? [])
|
|
62
|
+
.filter(Boolean);
|
|
63
|
+
const warnings = evidence.length === 0
|
|
64
|
+
? [`No high-confidence backend test context found for '${input.query}'.`]
|
|
65
|
+
: [];
|
|
66
|
+
return output(`Found ${interactionHits.length} API interaction record(s), ${backendHits.length} backend context record(s), and ${linkedAssets.length} linked autotests2 asset(s) for '${input.query}'.`, evidence, [
|
|
67
|
+
'Reuse the indexed API interaction flow before reconstructing request order by hand.',
|
|
68
|
+
backendParams.length > 0
|
|
69
|
+
? `Check required params (${[...new Set(backendParams)].join(', ')}) and backend validation hints before debugging selectors.`
|
|
70
|
+
: 'Check required params and backend validation hints before debugging selectors.',
|
|
71
|
+
'Reuse the linked autotests2 request flow instead of rebuilding API assumptions from scratch.',
|
|
72
|
+
], warnings);
|
|
73
|
+
}
|
|
74
|
+
function highConfidence(items) {
|
|
75
|
+
return items.filter((item) => (parseMetadata(item.metadata_json).confidence ?? 0) >= 0.7);
|
|
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
|
+
}
|
|
155
|
+
function parseMetadata(metadataJson) {
|
|
156
|
+
try {
|
|
157
|
+
return JSON.parse(metadataJson);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return {};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function truncate(value, maxLength) {
|
|
164
|
+
return value.length > maxLength ? `${value.slice(0, maxLength)}...` : value;
|
|
165
|
+
}
|