@7n/test 0.4.2 → 0.5.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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.5.0] - 2026-06-27
4
+
5
+ ### Changed
6
+
7
+ - `measureCoveragePerFile` тепер повертає `{ files, failingTests }` — vitest coverage і JSON reporter запускаються в одному виклику. `run.mjs` перевіряє `failingTests` щоразу перед покриттям (не лише коли coverage порожня), автоматично викликає pi agent і повторює ітерацію при успішному виправленні.
8
+
9
+ ## [0.4.3] - 2026-06-26
10
+
11
+ ### Changed
12
+
13
+ - Видалено зайвий бінарний аліас `7n-test` — залишено лише `test` для `npx @7n/test .`.
14
+
3
15
  ## [0.4.2] - 2026-06-26
4
16
 
5
17
  ### Changed
@@ -28,7 +40,7 @@
28
40
 
29
41
  ### Changed
30
42
 
31
- - fix: unignore npm/src/coverage/ (was blocked by **/coverage/ in .gitignore)
43
+ - fix: unignore npm/src/coverage/ (was blocked by \*\*/coverage/ in .gitignore)
32
44
 
33
45
  ## [0.3.0] - 2026-06-26
34
46
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@7n/test",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
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
- "test": "bin/7n-test.js",
22
- "7n-test": "bin/7n-test.js"
21
+ "test": "bin/7n-test.js"
23
22
  },
24
23
  "files": [
25
24
  "bin",
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Per-file coverage via vitest + lcov.
3
- * Runs `bunx vitest run --coverage --coverage.reporter=lcov` and parses lcov.info
4
- * per source file. Returns relative paths (to dir) with line coverage %.
3
+ * Runs `bunx vitest run --coverage --coverage.reporter=lcov --reporter=json`
4
+ * in a single pass and returns both per-file coverage data and failing tests.
5
5
  */
6
6
  import { spawnSync } from 'node:child_process'
7
7
  import { existsSync, readFileSync } from 'node:fs'
@@ -11,6 +11,8 @@ import { join, relative } from 'node:path'
11
11
  import { env } from 'node:process'
12
12
 
13
13
  const TEST_FILE_RE = /\.(test|spec)\.[^.]+$|[/\\]tests?[/\\]/
14
+ const MAX_ERRORS_PER_FILE = 5
15
+ const MAX_ERROR_LINES = 10
14
16
 
15
17
  /**
16
18
  * @param {string} text lcov.info content
@@ -44,12 +46,43 @@ function parseLcovPerFile(text) {
44
46
  }
45
47
 
46
48
  /**
47
- * Runs vitest coverage and returns per-file line coverage data.
49
+ * @param {string} jsonPath path to vitest JSON results file
50
+ * @param {string} dir project root for relative paths
51
+ * @returns {Array<{file: string, errors: string[]}>}
52
+ */
53
+ function parseFailingTests(jsonPath, dir) {
54
+ try {
55
+ const data = JSON.parse(readFileSync(jsonPath, 'utf8'))
56
+ return (data.testResults ?? [])
57
+ .filter(r => r.status === 'failed')
58
+ .map(r => ({
59
+ file: relative(dir, r.testFilePath),
60
+ errors: (r.assertionResults ?? [])
61
+ .filter(a => a.status === 'failed')
62
+ .slice(0, MAX_ERRORS_PER_FILE)
63
+ .map(a => {
64
+ const name = [...(a.ancestorTitles ?? []), a.title].join(' > ')
65
+ const msg = (a.failureMessages?.[0] ?? '').split('\n').slice(0, MAX_ERROR_LINES).join('\n')
66
+ return `${name}:\n${msg}`
67
+ })
68
+ }))
69
+ .filter(f => !f.file.startsWith('..') && f.errors.length > 0)
70
+ } catch {
71
+ return []
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Runs vitest coverage + JSON reporter in a single pass.
77
+ * Returns per-file coverage and any failing tests detected in the same run.
78
+ *
48
79
  * @param {string} dir project root
49
- * @returns {Promise<Array<{file: string, pct: number, linesFound: number, linesCovered: number}>>}
80
+ * @returns {Promise<{files: Array<{file: string, pct: number, linesFound: number, linesCovered: number}>, failingTests: Array<{file: string, errors: string[]}>}>}
50
81
  */
51
82
  export async function measureCoveragePerFile(dir) {
52
83
  const lcovDir = await mkdtemp(join(tmpdir(), '7n-cov-'))
84
+ const jsonResultsFile = join(lcovDir, 'test-results.json')
85
+
53
86
  try {
54
87
  spawnSync(
55
88
  'bunx',
@@ -59,16 +92,25 @@ export async function measureCoveragePerFile(dir) {
59
92
  '--passWithNoTests',
60
93
  '--coverage',
61
94
  '--coverage.reporter=lcov',
62
- `--coverage.reportsDirectory=${lcovDir}`
95
+ `--coverage.reportsDirectory=${lcovDir}`,
96
+ '--reporter=verbose',
97
+ '--reporter=json',
98
+ `--outputFile=${jsonResultsFile}`
63
99
  ],
64
100
  { cwd: dir, stdio: 'inherit', env }
65
101
  )
102
+
103
+ const failingTests = existsSync(jsonResultsFile) ? parseFailingTests(jsonResultsFile, dir) : []
104
+
66
105
  const lcovPath = join(lcovDir, 'lcov.info')
67
- if (!existsSync(lcovPath)) return []
106
+ if (!existsSync(lcovPath)) return { files: [], failingTests }
107
+
68
108
  const allFiles = parseLcovPerFile(readFileSync(lcovPath, 'utf8'))
69
- return allFiles
109
+ const files = allFiles
70
110
  .map(f => ({ ...f, file: relative(dir, f.file) }))
71
111
  .filter(f => !f.file.startsWith('..') && !TEST_FILE_RE.test(f.file))
112
+
113
+ return { files, failingTests }
72
114
  } finally {
73
115
  await rm(lcovDir, { recursive: true, force: true }).catch(() => {})
74
116
  }
package/src/fix-tests.mjs CHANGED
@@ -28,11 +28,11 @@ export async function getFailingTests(dir) {
28
28
  const outputFile = join(tmpDir, 'results.json')
29
29
 
30
30
  try {
31
- spawnSync(
32
- 'bunx',
33
- ['vitest', 'run', '--reporter=json', `--outputFile=${outputFile}`, '--passWithNoTests'],
34
- { cwd: dir, stdio: 'inherit', env }
35
- )
31
+ spawnSync('bunx', ['vitest', 'run', '--reporter=json', `--outputFile=${outputFile}`, '--passWithNoTests'], {
32
+ cwd: dir,
33
+ stdio: 'inherit',
34
+ env
35
+ })
36
36
 
37
37
  if (!existsSync(outputFile)) return []
38
38
 
package/src/run.mjs CHANGED
@@ -14,7 +14,7 @@
14
14
  import { measureCoveragePerFile, getUncoveredFiles } from './coverage-per-file.mjs'
15
15
  import { assessNeed } from './assess-need.mjs'
16
16
  import { generateTests } from './gen-tests.mjs'
17
- import { fixFailingTests, getFailingTests } from './fix-tests.mjs'
17
+ import { fixFailingTests } from './fix-tests.mjs'
18
18
  import { runCoverageSteps } from './coverage/coverage.mjs'
19
19
  import { withLock } from './scripts/utils/with-lock.mjs'
20
20
 
@@ -34,14 +34,15 @@ export async function runAutoTest(dir, opts = {}) {
34
34
  for (let i = 1; i <= MAX_ITERATIONS; i++) {
35
35
  console.log(`\n── Ітерація ${i}/${MAX_ITERATIONS}: coverage ──\n`)
36
36
 
37
- const allFiles = await measureCoveragePerFile(dir)
37
+ const { files: allFiles, failingTests } = await measureCoveragePerFile(dir)
38
+
39
+ if (failingTests.length > 0) {
40
+ console.log(`\n── ${failingTests.length} тестів падають — виправляю (pi agent) ──\n`)
41
+ const { remaining } = await fixFailingTests(dir, { failures: failingTests })
42
+ if (remaining === 0) continue
43
+ }
44
+
38
45
  if (allFiles.length === 0) {
39
- const failures = await getFailingTests(dir)
40
- if (failures.length > 0) {
41
- console.log(`\n── ${failures.length} тестів падають — виправляю (pi agent) ──\n`)
42
- const { remaining } = await fixFailingTests(dir, { failures })
43
- if (remaining === 0) continue
44
- }
45
46
  console.log('⚠ Vitest coverage не повернула даних — перевір налаштування vitest.')
46
47
  break
47
48
  }
@@ -0,0 +1 @@
1
+ export {}