@mknrt/autotests-overkill 1.1.4 → 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.
@@ -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
- const linkedAutotestQuery = [...interactionHits, ...backendHits]
26
- .flatMap((item) => {
27
- const metadata = parseMetadata(item.metadata_json);
28
- return [...(metadata.consumerSignalIds ?? []), ...(metadata.evidencePaths ?? [])];
29
- })
30
- .join(' ');
31
- const linkedAssets = linkedAutotestQuery
32
- ? queryDocuments(context.database, linkedAutotestQuery, {
33
- repoKind: 'autotests2',
34
- sourceKinds: ['spec', 'command', 'helper'],
35
- limit: 8,
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);
@@ -4,6 +4,7 @@ import { 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);
7
+ const focusedFiles = focusFrontendFiles(files.allFiles, input.widgetHint);
7
8
  const warnings = [];
8
9
  const controlBody = files.controlFile ? connector.read(files.controlFile) : '';
9
10
  const singletonBody = files.singletonFile ? connector.read(files.singletonFile) : '';
@@ -12,7 +13,7 @@ export async function extractFrontendContract(input) {
12
13
  if (files.mode === 'widget' && !files.controlFile) {
13
14
  warnings.push(`No control file found for widget '${input.widgetHint}'.`);
14
15
  }
15
- const sources = files.allFiles.map((filePath) => ({
16
+ const sources = focusedFiles.map((filePath) => ({
16
17
  path: filePath,
17
18
  body: connector.read(filePath),
18
19
  kind: connector.classifyFileKind(filePath),
@@ -52,11 +53,18 @@ export async function extractFrontendContract(input) {
52
53
  events: [...new Set(events)],
53
54
  files: files.mode === 'widget'
54
55
  ? unique([files.controlFile, files.singletonFile, files.handlersFile, files.templateFile, files.dataControlFile, ...files.supportFiles].filter(Boolean))
55
- : files.allFiles,
56
+ : focusedFiles,
56
57
  snippets,
57
58
  warnings,
58
59
  };
59
60
  }
61
+ function focusFrontendFiles(filePaths, widgetHint) {
62
+ if (!/auth|login/i.test(widgetHint)) {
63
+ return filePaths;
64
+ }
65
+ const focused = filePaths.filter((filePath) => /(?:^|[\\/])auth(?:[\\/]|$)|Auth|login/i.test(filePath));
66
+ return focused.length > 0 ? focused : filePaths;
67
+ }
60
68
  function extractNamedObjectKeys(source, objectName) {
61
69
  const startMatch = new RegExp(`${objectName}\\s*:\\s*\\{`).exec(source);
62
70
  if (!startMatch || startMatch.index === undefined) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mknrt/autotests-overkill",
3
- "version": "1.1.4",
3
+ "version": "1.1.5",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "AI-assisted testing intelligence platform for autotests2, caseplatform-web, and CI artifacts",