@unrdf/project-engine 5.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +53 -0
- package/package.json +58 -0
- package/src/api-contract-validator.mjs +711 -0
- package/src/auto-test-generator.mjs +444 -0
- package/src/autonomic-mapek.mjs +511 -0
- package/src/capabilities-manifest.mjs +125 -0
- package/src/code-complexity-js.mjs +368 -0
- package/src/dependency-graph.mjs +276 -0
- package/src/doc-drift-checker.mjs +172 -0
- package/src/doc-generator.mjs +229 -0
- package/src/domain-infer.mjs +966 -0
- package/src/drift-snapshot.mjs +775 -0
- package/src/file-roles.mjs +94 -0
- package/src/fs-scan.mjs +305 -0
- package/src/gap-finder.mjs +376 -0
- package/src/golden-structure.mjs +149 -0
- package/src/hotspot-analyzer.mjs +412 -0
- package/src/index.mjs +151 -0
- package/src/initialize.mjs +957 -0
- package/src/lens/project-structure.mjs +74 -0
- package/src/mapek-orchestration.mjs +665 -0
- package/src/materialize-apply.mjs +505 -0
- package/src/materialize-plan.mjs +422 -0
- package/src/materialize.mjs +137 -0
- package/src/policy-derivation.mjs +869 -0
- package/src/project-config.mjs +142 -0
- package/src/project-diff.mjs +28 -0
- package/src/project-engine/build-utils.mjs +237 -0
- package/src/project-engine/code-analyzer.mjs +248 -0
- package/src/project-engine/doc-generator.mjs +407 -0
- package/src/project-engine/infrastructure.mjs +213 -0
- package/src/project-engine/metrics.mjs +146 -0
- package/src/project-model.mjs +111 -0
- package/src/project-report.mjs +348 -0
- package/src/refactoring-guide.mjs +242 -0
- package/src/stack-detect.mjs +102 -0
- package/src/stack-linter.mjs +213 -0
- package/src/template-infer.mjs +674 -0
- package/src/type-auditor.mjs +609 -0
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Auto Test Generator - generates test suggestions from code analysis
|
|
3
|
+
* @module project-engine/auto-test-generator
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { UnrdfDataFactory as DataFactory } from '@unrdf/core/rdf/n3-justified-only';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
|
|
9
|
+
const { namedNode } = DataFactory;
|
|
10
|
+
|
|
11
|
+
const NS = {
|
|
12
|
+
rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
|
|
13
|
+
rdfs: 'http://www.w3.org/2000/01/rdf-schema#',
|
|
14
|
+
fs: 'http://example.org/unrdf/filesystem#',
|
|
15
|
+
proj: 'http://example.org/unrdf/project#',
|
|
16
|
+
dom: 'http://example.org/unrdf/domain#',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const InferTestPatternsOptionsSchema = z.object({
|
|
20
|
+
fsStore: z
|
|
21
|
+
.custom(val => val && typeof val.getQuads === 'function', {
|
|
22
|
+
message: 'fsStore must be an RDF store with getQuads method',
|
|
23
|
+
})
|
|
24
|
+
.optional(),
|
|
25
|
+
projectStore: z.custom(val => val && typeof val.getQuads === 'function', {
|
|
26
|
+
message: 'projectStore must be an RDF store with getQuads method',
|
|
27
|
+
}),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const AutoTestOptionsSchema = z.object({
|
|
31
|
+
projectStore: z.custom(val => val && typeof val.getQuads === 'function', {
|
|
32
|
+
message: 'projectStore must be an RDF store with getQuads method',
|
|
33
|
+
}),
|
|
34
|
+
domainStore: z
|
|
35
|
+
.custom(val => val && typeof val.getQuads === 'function', {
|
|
36
|
+
message: 'domainStore must be an RDF store with getQuads method',
|
|
37
|
+
})
|
|
38
|
+
.optional(),
|
|
39
|
+
stackProfile: z
|
|
40
|
+
.object({
|
|
41
|
+
testFramework: z.string().nullable().optional(),
|
|
42
|
+
})
|
|
43
|
+
.passthrough()
|
|
44
|
+
.optional(),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const TestSuggestionSchema = z.object({
|
|
48
|
+
file: z.string(),
|
|
49
|
+
testFile: z.string(),
|
|
50
|
+
testType: z.enum(['unit', 'integration', 'e2e']),
|
|
51
|
+
priority: z.enum(['critical', 'high', 'medium', 'low']),
|
|
52
|
+
reason: z.string(),
|
|
53
|
+
suggestedTests: z.array(z.string()),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Generate test suggestions based on code analysis
|
|
58
|
+
* @param {Object} options
|
|
59
|
+
* @param {Store} options.projectStore - Project RDF store
|
|
60
|
+
* @param {Store} [options.domainStore] - Domain model store
|
|
61
|
+
* @param {Object} [options.stackProfile] - Stack profile
|
|
62
|
+
* @returns {{ suggestions: Array, summary: string, coverage: number }}
|
|
63
|
+
*/
|
|
64
|
+
export function generateTestSuggestions(options) {
|
|
65
|
+
const validated = AutoTestOptionsSchema.parse(options);
|
|
66
|
+
const { projectStore, stackProfile } = validated;
|
|
67
|
+
|
|
68
|
+
const suggestions = [];
|
|
69
|
+
const fileQuads = projectStore.getQuads(null, namedNode(`${NS.fs}relativePath`), null);
|
|
70
|
+
|
|
71
|
+
let totalFiles = 0;
|
|
72
|
+
let filesWithTests = 0;
|
|
73
|
+
|
|
74
|
+
for (const quad of fileQuads) {
|
|
75
|
+
const filePath = quad.object.value;
|
|
76
|
+
if (isTestFile(filePath) || isConfigFile(filePath) || !isSourceFile(filePath)) continue;
|
|
77
|
+
|
|
78
|
+
totalFiles++;
|
|
79
|
+
const testPath = generateTestPath(filePath, stackProfile);
|
|
80
|
+
const hasTest = hasExistingTest(filePath, projectStore);
|
|
81
|
+
|
|
82
|
+
if (hasTest) {
|
|
83
|
+
filesWithTests++;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const roleQuads = projectStore.getQuads(quad.subject, namedNode(`${NS.proj}roleString`), null);
|
|
88
|
+
const role = roleQuads.length > 0 ? roleQuads[0].object.value : 'Unknown';
|
|
89
|
+
const { testType, priority } = determineTestType(filePath, role);
|
|
90
|
+
const suggestedTests = generateSuggestedTestCases(filePath, role);
|
|
91
|
+
|
|
92
|
+
suggestions.push({
|
|
93
|
+
file: filePath,
|
|
94
|
+
testFile: testPath,
|
|
95
|
+
testType,
|
|
96
|
+
priority,
|
|
97
|
+
reason: `${role} file without test`,
|
|
98
|
+
suggestedTests,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
103
|
+
suggestions.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]);
|
|
104
|
+
|
|
105
|
+
const coverage = totalFiles > 0 ? Math.round((filesWithTests / totalFiles) * 100) : 100;
|
|
106
|
+
const summary =
|
|
107
|
+
suggestions.length > 0
|
|
108
|
+
? `${suggestions.length} files need tests (${coverage}% coverage)`
|
|
109
|
+
: `All files have tests (${coverage}% coverage)`;
|
|
110
|
+
|
|
111
|
+
return { suggestions, summary, coverage };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function isTestFile(filePath) {
|
|
115
|
+
return (
|
|
116
|
+
/\.(test|spec)\.(tsx?|jsx?|mjs)$/.test(filePath) ||
|
|
117
|
+
/^(test|tests|__tests__|spec)\//.test(filePath)
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function isConfigFile(filePath) {
|
|
122
|
+
return /\.(config|rc)\.(tsx?|jsx?|mjs|json)$/.test(filePath);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function isSourceFile(filePath) {
|
|
126
|
+
return /\.(tsx?|jsx?|mjs)$/.test(filePath);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function generateTestPath(filePath, _stackProfile) {
|
|
130
|
+
const ext = filePath.match(/\.(tsx?|jsx?|mjs)$/)?.[1] || 'mjs';
|
|
131
|
+
const baseName = filePath.replace(/\.(tsx?|jsx?|mjs)$/, '');
|
|
132
|
+
return `${baseName}.test.${ext}`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function hasExistingTest(filePath, projectStore) {
|
|
136
|
+
const baseName = filePath.replace(/\.(tsx?|jsx?|mjs)$/, '');
|
|
137
|
+
const testPatterns = [`${baseName}.test.`, `${baseName}.spec.`];
|
|
138
|
+
const allPaths = projectStore
|
|
139
|
+
.getQuads(null, namedNode(`${NS.fs}relativePath`), null)
|
|
140
|
+
.map(q => q.object.value);
|
|
141
|
+
return testPatterns.some(pattern => allPaths.some(p => p.includes(pattern)));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function determineTestType(filePath, role) {
|
|
145
|
+
const roleMap = {
|
|
146
|
+
Api: { testType: 'integration', priority: 'critical' },
|
|
147
|
+
Route: { testType: 'integration', priority: 'critical' },
|
|
148
|
+
Service: { testType: 'unit', priority: 'high' },
|
|
149
|
+
Schema: { testType: 'unit', priority: 'high' },
|
|
150
|
+
Component: { testType: 'unit', priority: 'medium' },
|
|
151
|
+
};
|
|
152
|
+
return roleMap[role] || { testType: 'unit', priority: 'medium' };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function generateSuggestedTestCases(filePath, role) {
|
|
156
|
+
const baseName = filePath
|
|
157
|
+
.split('/')
|
|
158
|
+
.pop()
|
|
159
|
+
?.replace(/\.(tsx?|jsx?|mjs)$/, '');
|
|
160
|
+
const tests =
|
|
161
|
+
role === 'Api' || role === 'Route'
|
|
162
|
+
? [
|
|
163
|
+
'should handle GET requests',
|
|
164
|
+
'should handle POST requests',
|
|
165
|
+
'should return 400 for invalid input',
|
|
166
|
+
]
|
|
167
|
+
: ['should work correctly with valid input', 'should handle edge cases'];
|
|
168
|
+
return tests.map(t => `${baseName}: ${t}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Infer test patterns from existing test files
|
|
173
|
+
* @param {Object} options
|
|
174
|
+
* @param {import('n3').Store} [options.fsStore] - Filesystem RDF store
|
|
175
|
+
* @param {import('n3').Store} options.projectStore - Project RDF store
|
|
176
|
+
* @returns {Object} Test patterns object
|
|
177
|
+
*/
|
|
178
|
+
export function inferTestPatterns(options) {
|
|
179
|
+
const validated = InferTestPatternsOptionsSchema.parse(options);
|
|
180
|
+
const { fsStore, _projectStore } = validated;
|
|
181
|
+
|
|
182
|
+
// Default patterns
|
|
183
|
+
const defaultPatterns = {
|
|
184
|
+
testFramework: 'vitest',
|
|
185
|
+
fileExtension: 'mjs',
|
|
186
|
+
testSuffix: 'test',
|
|
187
|
+
assertionPatterns: ['toBe', 'toEqual', 'toBeDefined', 'toContain'],
|
|
188
|
+
describeBlocks: [],
|
|
189
|
+
setupTeardown: {
|
|
190
|
+
hasBeforeEach: false,
|
|
191
|
+
hasAfterEach: false,
|
|
192
|
+
hasBeforeAll: false,
|
|
193
|
+
hasAfterAll: false,
|
|
194
|
+
},
|
|
195
|
+
imports: [],
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// If no fsStore, return defaults
|
|
199
|
+
if (!fsStore) {
|
|
200
|
+
return defaultPatterns;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Find test files
|
|
204
|
+
const testFileQuads = fsStore.getQuads(null, namedNode(`${NS.fs}relativePath`), null);
|
|
205
|
+
|
|
206
|
+
const testFiles = testFileQuads
|
|
207
|
+
.map(q => ({
|
|
208
|
+
path: q.object.value,
|
|
209
|
+
subject: q.subject,
|
|
210
|
+
}))
|
|
211
|
+
.filter(f => isTestFile(f.path));
|
|
212
|
+
|
|
213
|
+
// If no test files found, return defaults
|
|
214
|
+
if (testFiles.length === 0) {
|
|
215
|
+
return defaultPatterns;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Analyze first test file content
|
|
219
|
+
const firstTestFile = testFiles[0];
|
|
220
|
+
const contentQuads = fsStore.getQuads(firstTestFile.subject, namedNode(`${NS.fs}content`), null);
|
|
221
|
+
|
|
222
|
+
if (contentQuads.length === 0) {
|
|
223
|
+
return defaultPatterns;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const content = contentQuads[0].object.value;
|
|
227
|
+
const patterns = { ...defaultPatterns };
|
|
228
|
+
|
|
229
|
+
// Detect test framework
|
|
230
|
+
if (content.includes("from 'vitest'") || content.includes('from "vitest"')) {
|
|
231
|
+
patterns.testFramework = 'vitest';
|
|
232
|
+
} else if (content.includes("from 'jest'") || content.includes('from "jest"')) {
|
|
233
|
+
patterns.testFramework = 'jest';
|
|
234
|
+
} else if (content.includes("from 'mocha'") || content.includes('from "mocha"')) {
|
|
235
|
+
patterns.testFramework = 'mocha';
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Detect file extension
|
|
239
|
+
const extMatch = firstTestFile.path.match(/\.(tsx?|jsx?|mjs)$/);
|
|
240
|
+
if (extMatch) {
|
|
241
|
+
patterns.fileExtension = extMatch[1];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Detect test suffix
|
|
245
|
+
if (firstTestFile.path.includes('.spec.')) {
|
|
246
|
+
patterns.testSuffix = 'spec';
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Extract describe blocks
|
|
250
|
+
const describeMatches = content.matchAll(/describe\(['"]([^'"]+)['"]/g);
|
|
251
|
+
for (const match of describeMatches) {
|
|
252
|
+
patterns.describeBlocks.push(match[1]);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Detect assertion patterns
|
|
256
|
+
const assertionPatterns = [
|
|
257
|
+
'toBe',
|
|
258
|
+
'toEqual',
|
|
259
|
+
'toBeDefined',
|
|
260
|
+
'toBeUndefined',
|
|
261
|
+
'toBeNull',
|
|
262
|
+
'toBeTruthy',
|
|
263
|
+
'toBeFalsy',
|
|
264
|
+
'toContain',
|
|
265
|
+
'toHaveLength',
|
|
266
|
+
'toThrow',
|
|
267
|
+
'toMatch',
|
|
268
|
+
];
|
|
269
|
+
for (const pattern of assertionPatterns) {
|
|
270
|
+
if (content.includes(`.${pattern}(`)) {
|
|
271
|
+
if (!patterns.assertionPatterns.includes(pattern)) {
|
|
272
|
+
patterns.assertionPatterns.push(pattern);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Detect setup/teardown
|
|
278
|
+
patterns.setupTeardown.hasBeforeEach = content.includes('beforeEach');
|
|
279
|
+
patterns.setupTeardown.hasAfterEach = content.includes('afterEach');
|
|
280
|
+
patterns.setupTeardown.hasBeforeAll = content.includes('beforeAll');
|
|
281
|
+
patterns.setupTeardown.hasAfterAll = content.includes('afterAll');
|
|
282
|
+
|
|
283
|
+
// Extract imports
|
|
284
|
+
const importMatches = content.matchAll(/import\s+.*?\s+from\s+['"]([^'"]+)['"]/g);
|
|
285
|
+
for (const match of importMatches) {
|
|
286
|
+
if (!patterns.imports.includes(match[1])) {
|
|
287
|
+
patterns.imports.push(match[1]);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return patterns;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Generate test skeleton from entity and patterns
|
|
296
|
+
* @param {Object} options
|
|
297
|
+
* @param {string} options.entity - Entity name
|
|
298
|
+
* @param {Object} options.existingTestPatterns - Test patterns from inferTestPatterns
|
|
299
|
+
* @param {import('n3').Store} [options.domainStore] - Domain model store
|
|
300
|
+
* @returns {Object} Test skeleton object
|
|
301
|
+
*/
|
|
302
|
+
export function generateTestSkeleton(options) {
|
|
303
|
+
const { entity, existingTestPatterns, domainStore } = options;
|
|
304
|
+
|
|
305
|
+
// Use provided patterns or defaults
|
|
306
|
+
const patterns = existingTestPatterns || {
|
|
307
|
+
testFramework: 'vitest',
|
|
308
|
+
fileExtension: 'mjs',
|
|
309
|
+
testSuffix: 'test',
|
|
310
|
+
assertionPatterns: ['toBe', 'toEqual', 'toBeDefined'],
|
|
311
|
+
describeBlocks: [],
|
|
312
|
+
setupTeardown: {
|
|
313
|
+
hasBeforeEach: false,
|
|
314
|
+
hasAfterEach: false,
|
|
315
|
+
hasBeforeAll: false,
|
|
316
|
+
hasAfterAll: false,
|
|
317
|
+
},
|
|
318
|
+
imports: [],
|
|
319
|
+
};
|
|
320
|
+
const ext = patterns.fileExtension || 'mjs';
|
|
321
|
+
const suffix = patterns.testSuffix || 'test';
|
|
322
|
+
const entityLower = entity.charAt(0).toLowerCase() + entity.slice(1);
|
|
323
|
+
|
|
324
|
+
const filename = `${entityLower}.${suffix}.${ext}`;
|
|
325
|
+
|
|
326
|
+
// Generate imports
|
|
327
|
+
const imports =
|
|
328
|
+
patterns.imports.length > 0
|
|
329
|
+
? patterns.imports.map(imp => `import { } from '${imp}'`).join('\n')
|
|
330
|
+
: `import { describe, it, expect${patterns.setupTeardown.hasBeforeEach ? ', beforeEach' : ''} } from '${patterns.testFramework || 'vitest'}'`;
|
|
331
|
+
|
|
332
|
+
// Generate content
|
|
333
|
+
let content = `${imports}\n`;
|
|
334
|
+
content += `import { ${entity} } from '../../src/${entityLower}.${ext}'\n\n`;
|
|
335
|
+
content += `describe('${entity}', () => {\n`;
|
|
336
|
+
|
|
337
|
+
if (patterns.setupTeardown.hasBeforeEach) {
|
|
338
|
+
content += ` let ${entityLower}\n`;
|
|
339
|
+
content += ` beforeEach(() => { ${entityLower} = new ${entity}() })\n`;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
content += ` describe('creation', () => {\n`;
|
|
343
|
+
content += ` it('should create ${entityLower} instance', () => { expect(${entityLower || `new ${entity}()`}).toBeDefined() })\n`;
|
|
344
|
+
content += ` })\n`;
|
|
345
|
+
content += `})\n`;
|
|
346
|
+
|
|
347
|
+
// Generate suggested tests
|
|
348
|
+
const suggestedTests = [`should create ${entity} instance`];
|
|
349
|
+
const fieldTests = [];
|
|
350
|
+
|
|
351
|
+
// Add field tests if domainStore provided
|
|
352
|
+
if (domainStore) {
|
|
353
|
+
const fieldQuads = domainStore.getQuads(
|
|
354
|
+
namedNode(`${NS.dom}${entity}`),
|
|
355
|
+
namedNode(`${NS.dom}hasField`),
|
|
356
|
+
null
|
|
357
|
+
);
|
|
358
|
+
for (const quad of fieldQuads) {
|
|
359
|
+
const fieldName = quad.object.value.split('.').pop();
|
|
360
|
+
const testName = `should have ${fieldName} property`;
|
|
361
|
+
suggestedTests.push(testName);
|
|
362
|
+
fieldTests.push({ name: testName, field: fieldName });
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Add field tests to content
|
|
367
|
+
if (fieldTests.length > 0) {
|
|
368
|
+
content += ` describe('properties', () => {\n`;
|
|
369
|
+
for (const fieldTest of fieldTests) {
|
|
370
|
+
content += ` it('${fieldTest.name}', () => { expect(${entityLower}.${fieldTest.field}).toBeDefined() })\n`;
|
|
371
|
+
}
|
|
372
|
+
content += ` })\n`;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
filename,
|
|
377
|
+
content,
|
|
378
|
+
suggestedTests,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Score test coverage for an entity
|
|
384
|
+
* @param {Object} options
|
|
385
|
+
* @param {string} options.entity - Entity name
|
|
386
|
+
* @param {string[]} [options.testFiles] - Array of test file paths
|
|
387
|
+
* @param {string[]} [options.sourceFiles] - Array of source file paths
|
|
388
|
+
* @returns {Object} Coverage score object
|
|
389
|
+
*/
|
|
390
|
+
export function scoreTestCoverage(options) {
|
|
391
|
+
const { entity, testFiles = [], sourceFiles = [] } = options;
|
|
392
|
+
|
|
393
|
+
const entityLower = entity.charAt(0).toLowerCase() + entity.slice(1);
|
|
394
|
+
const entityKebab = entity
|
|
395
|
+
.replace(/([A-Z])/g, '-$1')
|
|
396
|
+
.toLowerCase()
|
|
397
|
+
.slice(1);
|
|
398
|
+
|
|
399
|
+
const matchingTests = testFiles.filter(f => {
|
|
400
|
+
const lower = f.toLowerCase();
|
|
401
|
+
return (
|
|
402
|
+
lower.includes(entityLower) ||
|
|
403
|
+
lower.includes(entityKebab) ||
|
|
404
|
+
lower.includes(entity.toLowerCase())
|
|
405
|
+
);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
// If no source files provided, assume entity needs tests if no matching tests found
|
|
409
|
+
const needsTests =
|
|
410
|
+
sourceFiles.length > 0 ? matchingTests.length === 0 : matchingTests.length === 0;
|
|
411
|
+
// Coverage: if source files exist, 100% if any tests found, 0% otherwise
|
|
412
|
+
// Cap at 100% to handle cases where multiple test files match one source file
|
|
413
|
+
const coverage =
|
|
414
|
+
sourceFiles.length > 0
|
|
415
|
+
? matchingTests.length > 0
|
|
416
|
+
? 100
|
|
417
|
+
: 0
|
|
418
|
+
: matchingTests.length > 0
|
|
419
|
+
? 100
|
|
420
|
+
: 0;
|
|
421
|
+
|
|
422
|
+
return {
|
|
423
|
+
coverage,
|
|
424
|
+
needsTests,
|
|
425
|
+
existingTests: matchingTests,
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Generate test factory function
|
|
431
|
+
* @param {string} entity - Entity name
|
|
432
|
+
* @returns {string} Factory function code
|
|
433
|
+
*/
|
|
434
|
+
export function generateTestFactory(entity) {
|
|
435
|
+
const _entityLower = entity.charAt(0).toLowerCase() + entity.slice(1);
|
|
436
|
+
return `function create${entity}(overrides = {}) {
|
|
437
|
+
return {
|
|
438
|
+
id: 'test-id',
|
|
439
|
+
...overrides,
|
|
440
|
+
};
|
|
441
|
+
}`;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
export { TestSuggestionSchema };
|