@7n/test 0.6.0 → 0.7.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/CHANGELOG.md +12 -0
- package/package.json +2 -3
- package/src/coverage-per-file.mjs +48 -6
- package/src/run.mjs +23 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.7.1] - 2026-06-27
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- `parseFailingTests` тепер коректно обробляє module-level помилки (import/syntax errors) — тести, що падають ще до запуску (`assertionResults: []`), більше не пропускаються, що усувало нескінченну bootstrap-петлю.
|
|
8
|
+
|
|
9
|
+
## [0.7.0] - 2026-06-27
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Bootstrap-режим: якщо тестів нема взагалі (`files=[]`, `failingTests=[]`), `@7n/test` тепер сканує джерельні файли, оцінює потребу через LLM і генерує початкові тести — замість зупинки з попередженням. Прибрано бінарний аліас `test` із bin-поля — залишено лише `7n-test`, що усуває конфлікт з shell built-in при `npx @7n/test`.
|
|
14
|
+
|
|
3
15
|
## [0.6.0] - 2026-06-27
|
|
4
16
|
|
|
5
17
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@7n/test",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "CLI-утиліта @7n/test",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"7n",
|
|
@@ -18,8 +18,7 @@
|
|
|
18
18
|
"url": "git+https://github.com/nitra/7n-test.git"
|
|
19
19
|
},
|
|
20
20
|
"bin": {
|
|
21
|
-
"7n-test": "bin/7n-test.js"
|
|
22
|
-
"test": "bin/7n-test.js"
|
|
21
|
+
"7n-test": "bin/7n-test.js"
|
|
23
22
|
},
|
|
24
23
|
"files": [
|
|
25
24
|
"bin",
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { spawnSync } from 'node:child_process'
|
|
8
8
|
import { existsSync, readFileSync } from 'node:fs'
|
|
9
|
-
import { mkdtemp, rm } from 'node:fs/promises'
|
|
9
|
+
import { mkdtemp, readdir, rm } from 'node:fs/promises'
|
|
10
10
|
import { tmpdir } from 'node:os'
|
|
11
11
|
import { createRequire } from 'node:module'
|
|
12
12
|
import { join, relative, dirname } from 'node:path'
|
|
@@ -63,9 +63,8 @@ function parseFailingTests(jsonPath, dir) {
|
|
|
63
63
|
const data = JSON.parse(readFileSync(jsonPath, 'utf8'))
|
|
64
64
|
return (data.testResults ?? [])
|
|
65
65
|
.filter(r => r.status === 'failed')
|
|
66
|
-
.map(r =>
|
|
67
|
-
|
|
68
|
-
errors: (r.assertionResults ?? [])
|
|
66
|
+
.map(r => {
|
|
67
|
+
const assertionErrors = (r.assertionResults ?? [])
|
|
69
68
|
.filter(a => a.status === 'failed')
|
|
70
69
|
.slice(0, MAX_ERRORS_PER_FILE)
|
|
71
70
|
.map(a => {
|
|
@@ -73,8 +72,13 @@ function parseFailingTests(jsonPath, dir) {
|
|
|
73
72
|
const msg = (a.failureMessages?.[0] ?? '').split('\n').slice(0, MAX_ERROR_LINES).join('\n')
|
|
74
73
|
return `${name}:\n${msg}`
|
|
75
74
|
})
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
// Module-level errors (import/syntax) produce no assertionResults
|
|
76
|
+
const errors = assertionErrors.length > 0
|
|
77
|
+
? assertionErrors
|
|
78
|
+
: [`Suite error: ${(r.message ?? r.failureMessage ?? 'module-level failure').split('\n').slice(0, MAX_ERROR_LINES).join('\n')}`]
|
|
79
|
+
return { file: relative(dir, r.testFilePath), errors }
|
|
80
|
+
})
|
|
81
|
+
.filter(f => !f.file.startsWith('..'))
|
|
78
82
|
} catch {
|
|
79
83
|
return []
|
|
80
84
|
}
|
|
@@ -133,3 +137,41 @@ export async function measureCoveragePerFile(dir) {
|
|
|
133
137
|
export function getUncoveredFiles(files, threshold = 80) {
|
|
134
138
|
return files.filter(f => f.pct < threshold)
|
|
135
139
|
}
|
|
140
|
+
|
|
141
|
+
const SOURCE_EXT_RE = /\.(mjs|js|ts|vue|py)$/
|
|
142
|
+
const IGNORE_DIRS = new Set([
|
|
143
|
+
'node_modules', 'dist', 'build', 'out', '.git', '__pycache__',
|
|
144
|
+
'coverage', '.cursor', '.claude', '.pi', 'docs', 'bin', 'reports'
|
|
145
|
+
])
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Recursively finds source code files in a directory, excluding tests and
|
|
149
|
+
* ignored directories. Used for bootstrap when no coverage data exists.
|
|
150
|
+
*
|
|
151
|
+
* @param {string} dir project root
|
|
152
|
+
* @returns {Promise<string[]>} relative paths to source files
|
|
153
|
+
*/
|
|
154
|
+
export async function findSourceFiles(dir) {
|
|
155
|
+
const results = []
|
|
156
|
+
|
|
157
|
+
async function walk(current, relBase) {
|
|
158
|
+
let entries
|
|
159
|
+
try {
|
|
160
|
+
entries = await readdir(current, { withFileTypes: true })
|
|
161
|
+
} catch {
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
for (const entry of entries) {
|
|
165
|
+
if (entry.name.startsWith('.')) continue
|
|
166
|
+
const rel = relBase ? `${relBase}/${entry.name}` : entry.name
|
|
167
|
+
if (entry.isDirectory()) {
|
|
168
|
+
if (!IGNORE_DIRS.has(entry.name)) await walk(join(current, entry.name), rel)
|
|
169
|
+
} else if (entry.isFile() && SOURCE_EXT_RE.test(entry.name) && !TEST_FILE_RE.test(rel)) {
|
|
170
|
+
results.push(rel)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
await walk(dir, '')
|
|
176
|
+
return results
|
|
177
|
+
}
|
package/src/run.mjs
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* 5. Mutation testing via existing coverage providers (requires .n-cursor.json#rules).
|
|
12
12
|
* 6. Auto-fix survived mutants via pi agent.
|
|
13
13
|
*/
|
|
14
|
-
import { measureCoveragePerFile, getUncoveredFiles } from './coverage-per-file.mjs'
|
|
14
|
+
import { measureCoveragePerFile, getUncoveredFiles, findSourceFiles } from './coverage-per-file.mjs'
|
|
15
15
|
import { assessNeed } from './assess-need.mjs'
|
|
16
16
|
import { generateTests } from './gen-tests.mjs'
|
|
17
17
|
import { fixFailingTests } from './fix-tests.mjs'
|
|
@@ -43,8 +43,28 @@ export async function runAutoTest(dir, opts = {}) {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
if (allFiles.length === 0) {
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
if (failingTests.length > 0) {
|
|
47
|
+
console.log('⚠ Тести падають, але coverage все одно порожня — зупиняю цикл.')
|
|
48
|
+
break
|
|
49
|
+
}
|
|
50
|
+
// Bootstrap: no tests at all — scan source files and generate initial tests
|
|
51
|
+
const sourceFiles = await findSourceFiles(dir)
|
|
52
|
+
if (sourceFiles.length === 0) {
|
|
53
|
+
console.log('⚠ Vitest coverage не повернула даних — перевір налаштування vitest.')
|
|
54
|
+
break
|
|
55
|
+
}
|
|
56
|
+
console.log(`\n── Bootstrap: ${sourceFiles.length} файлів без тестів — оцінюю потребу ──\n`)
|
|
57
|
+
const bootstrapFiles = sourceFiles.map(f => ({ file: f, pct: 0 }))
|
|
58
|
+
const assessed = await assessNeed(bootstrapFiles, dir)
|
|
59
|
+
const needsTests = assessed.filter(f => f.needsTests)
|
|
60
|
+
if (needsTests.length === 0) {
|
|
61
|
+
console.log('✓ LLM вирішила: жоден файл не потребує unit-тестів.')
|
|
62
|
+
break
|
|
63
|
+
}
|
|
64
|
+
console.log(`\n→ Bootstrap-тести для (${needsTests.length}):`)
|
|
65
|
+
for (const f of needsTests) console.log(` • ${f.file} — ${f.reason}`)
|
|
66
|
+
await generateTests(needsTests, dir)
|
|
67
|
+
continue
|
|
48
68
|
}
|
|
49
69
|
|
|
50
70
|
const uncovered = getUncoveredFiles(allFiles, COVERAGE_THRESHOLD)
|