@7n/test 0.4.3 → 0.6.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.6.0] - 2026-06-27
4
+
5
+ ### Added
6
+
7
+ - `vitest` і `@vitest/coverage-v8` тепер є залежностями `@7n/test` — цільовий проєкт більше не потребує їх у `devDependencies`. Vitest викликається з node_modules самого пакету через `process.execPath`. Додано бінарний аліас `7n-test` поряд з `test` — усуває конфлікт з shell built-in при `npx @7n/test`.
8
+
9
+ ## [0.5.0] - 2026-06-27
10
+
11
+ ### Changed
12
+
13
+ - `measureCoveragePerFile` тепер повертає `{ files, failingTests }` — vitest coverage і JSON reporter запускаються в одному виклику. `run.mjs` перевіряє `failingTests` щоразу перед покриттям (не лише коли coverage порожня), автоматично викликає pi agent і повторює ітерацію при успішному виправленні.
14
+
3
15
  ## [0.4.3] - 2026-06-26
4
16
 
5
17
  ### Changed
@@ -34,7 +46,7 @@
34
46
 
35
47
  ### Changed
36
48
 
37
- - fix: unignore npm/src/coverage/ (was blocked by **/coverage/ in .gitignore)
49
+ - fix: unignore npm/src/coverage/ (was blocked by \*\*/coverage/ in .gitignore)
38
50
 
39
51
  ## [0.3.0] - 2026-06-26
40
52
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@7n/test",
3
- "version": "0.4.3",
3
+ "version": "0.6.0",
4
4
  "description": "CLI-утиліта @7n/test",
5
5
  "keywords": [
6
6
  "7n",
@@ -18,6 +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",
21
22
  "test": "bin/7n-test.js"
22
23
  },
23
24
  "files": [
@@ -40,6 +41,8 @@
40
41
  },
41
42
  "dependencies": {
42
43
  "@earendil-works/pi-coding-agent": "^0.80.2",
44
+ "@vitest/coverage-v8": "^4.1.9",
45
+ "vitest": "^4.1.9",
43
46
  "zod": "^3.23.0"
44
47
  },
45
48
  "engines": {
@@ -1,16 +1,26 @@
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 vitest (bundled with @7n/test) in a single pass and returns
4
+ * both per-file coverage data and failing tests.
5
+ * Target projects do NOT need vitest or @vitest/coverage-v8 installed.
5
6
  */
6
7
  import { spawnSync } from 'node:child_process'
7
8
  import { existsSync, readFileSync } from 'node:fs'
8
9
  import { mkdtemp, rm } from 'node:fs/promises'
9
10
  import { tmpdir } from 'node:os'
10
- import { join, relative } from 'node:path'
11
+ import { createRequire } from 'node:module'
12
+ import { join, relative, dirname } from 'node:path'
11
13
  import { env } from 'node:process'
12
14
 
15
+ const _require = createRequire(import.meta.url)
16
+ const VITEST_BIN = join(
17
+ dirname(_require.resolve('vitest/package.json')),
18
+ 'vitest.mjs'
19
+ )
20
+
13
21
  const TEST_FILE_RE = /\.(test|spec)\.[^.]+$|[/\\]tests?[/\\]/
22
+ const MAX_ERRORS_PER_FILE = 5
23
+ const MAX_ERROR_LINES = 10
14
24
 
15
25
  /**
16
26
  * @param {string} text lcov.info content
@@ -44,31 +54,71 @@ function parseLcovPerFile(text) {
44
54
  }
45
55
 
46
56
  /**
47
- * Runs vitest coverage and returns per-file line coverage data.
57
+ * @param {string} jsonPath path to vitest JSON results file
58
+ * @param {string} dir project root for relative paths
59
+ * @returns {Array<{file: string, errors: string[]}>}
60
+ */
61
+ function parseFailingTests(jsonPath, dir) {
62
+ try {
63
+ const data = JSON.parse(readFileSync(jsonPath, 'utf8'))
64
+ return (data.testResults ?? [])
65
+ .filter(r => r.status === 'failed')
66
+ .map(r => ({
67
+ file: relative(dir, r.testFilePath),
68
+ errors: (r.assertionResults ?? [])
69
+ .filter(a => a.status === 'failed')
70
+ .slice(0, MAX_ERRORS_PER_FILE)
71
+ .map(a => {
72
+ const name = [...(a.ancestorTitles ?? []), a.title].join(' > ')
73
+ const msg = (a.failureMessages?.[0] ?? '').split('\n').slice(0, MAX_ERROR_LINES).join('\n')
74
+ return `${name}:\n${msg}`
75
+ })
76
+ }))
77
+ .filter(f => !f.file.startsWith('..') && f.errors.length > 0)
78
+ } catch {
79
+ return []
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Runs vitest coverage + JSON reporter in a single pass.
85
+ * Returns per-file coverage and any failing tests detected in the same run.
86
+ *
48
87
  * @param {string} dir project root
49
- * @returns {Promise<Array<{file: string, pct: number, linesFound: number, linesCovered: number}>>}
88
+ * @returns {Promise<{files: Array<{file: string, pct: number, linesFound: number, linesCovered: number}>, failingTests: Array<{file: string, errors: string[]}>}>}
50
89
  */
51
90
  export async function measureCoveragePerFile(dir) {
52
91
  const lcovDir = await mkdtemp(join(tmpdir(), '7n-cov-'))
92
+ const jsonResultsFile = join(lcovDir, 'test-results.json')
93
+
53
94
  try {
54
95
  spawnSync(
55
- 'bunx',
96
+ process.execPath,
56
97
  [
57
- 'vitest',
98
+ VITEST_BIN,
58
99
  'run',
59
100
  '--passWithNoTests',
60
101
  '--coverage',
61
102
  '--coverage.reporter=lcov',
62
- `--coverage.reportsDirectory=${lcovDir}`
103
+ `--coverage.reportsDirectory=${lcovDir}`,
104
+ '--reporter=verbose',
105
+ '--reporter=json',
106
+ `--outputFile=${jsonResultsFile}`
63
107
  ],
64
108
  { cwd: dir, stdio: 'inherit', env }
65
109
  )
110
+
111
+ const failingTests = existsSync(jsonResultsFile) ? parseFailingTests(jsonResultsFile, dir) : []
112
+
66
113
  const lcovPath = join(lcovDir, 'lcov.info')
67
- if (!existsSync(lcovPath)) return []
114
+ if (!existsSync(lcovPath)) return { files: [], failingTests }
115
+
68
116
  const allFiles = parseLcovPerFile(readFileSync(lcovPath, 'utf8'))
69
- return allFiles
117
+ const files = allFiles
70
118
  .map(f => ({ ...f, file: relative(dir, f.file) }))
71
119
  .filter(f => !f.file.startsWith('..') && !TEST_FILE_RE.test(f.file))
120
+
121
+ return { files, failingTests }
72
122
  } finally {
73
123
  await rm(lcovDir, { recursive: true, force: true }).catch(() => {})
74
124
  }
package/src/fix-tests.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  * fix-tests: виявляє падаючі unit-тести і викликає pi-агента для їх виправлення.
3
3
  *
4
4
  * Порядок роботи:
5
- * 1. getFailingTests(dir) — запускає `bunx vitest run --reporter=json` і повертає
5
+ * 1. getFailingTests(dir) — запускає vitest run --reporter=json і повертає
6
6
  * список падаючих файлів з повідомленнями про помилки.
7
7
  * 2. fixFailingTests(dir, opts) — якщо є падіння, будує prompt і викликає pi CLI
8
8
  * в агентному режимі; перевіряє результат повторним getFailingTests.
@@ -10,10 +10,17 @@
10
10
  import { spawnSync } from 'node:child_process'
11
11
  import { existsSync, readFileSync } from 'node:fs'
12
12
  import { mkdtemp, rm } from 'node:fs/promises'
13
+ import { createRequire } from 'node:module'
13
14
  import { tmpdir } from 'node:os'
14
- import { join, relative } from 'node:path'
15
+ import { join, relative, dirname } from 'node:path'
15
16
  import { env } from 'node:process'
16
17
 
18
+ const _require = createRequire(import.meta.url)
19
+ const VITEST_BIN = join(
20
+ dirname(_require.resolve('vitest/package.json')),
21
+ 'vitest.mjs'
22
+ )
23
+
17
24
  const MODEL = env.N_CURSOR_FIX_TESTS_MODEL ?? env.N_CLOUD_MAX_MODEL ?? ''
18
25
  const MAX_ERRORS_PER_FILE = 5
19
26
  const MAX_ERROR_LINES = 10
@@ -28,11 +35,11 @@ export async function getFailingTests(dir) {
28
35
  const outputFile = join(tmpDir, 'results.json')
29
36
 
30
37
  try {
31
- spawnSync(
32
- 'bunx',
33
- ['vitest', 'run', '--reporter=json', `--outputFile=${outputFile}`, '--passWithNoTests'],
34
- { cwd: dir, stdio: 'inherit', env }
35
- )
38
+ spawnSync(process.execPath, [VITEST_BIN, 'run', '--reporter=json', `--outputFile=${outputFile}`, '--passWithNoTests'], {
39
+ cwd: dir,
40
+ stdio: 'inherit',
41
+ env
42
+ })
36
43
 
37
44
  if (!existsSync(outputFile)) return []
38
45
 
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 {}