@7n/test 0.5.0 → 0.7.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.7.0] - 2026-06-27
4
+
5
+ ### Added
6
+
7
+ - Bootstrap-режим: якщо тестів нема взагалі (`files=[]`, `failingTests=[]`), `@7n/test` тепер сканує джерельні файли, оцінює потребу через LLM і генерує початкові тести — замість зупинки з попередженням. Прибрано бінарний аліас `test` із bin-поля — залишено лише `7n-test`, що усуває конфлікт з shell built-in при `npx @7n/test`.
8
+
9
+ ## [0.6.0] - 2026-06-27
10
+
11
+ ### Added
12
+
13
+ - `vitest` і `@vitest/coverage-v8` тепер є залежностями `@7n/test` — цільовий проєкт більше не потребує їх у `devDependencies`. Vitest викликається з node_modules самого пакету через `process.execPath`. Додано бінарний аліас `7n-test` поряд з `test` — усуває конфлікт з shell built-in при `npx @7n/test`.
14
+
3
15
  ## [0.5.0] - 2026-06-27
4
16
 
5
17
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@7n/test",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "CLI-утиліта @7n/test",
5
5
  "keywords": [
6
6
  "7n",
@@ -18,7 +18,7 @@
18
18
  "url": "git+https://github.com/nitra/7n-test.git"
19
19
  },
20
20
  "bin": {
21
- "test": "bin/7n-test.js"
21
+ "7n-test": "bin/7n-test.js"
22
22
  },
23
23
  "files": [
24
24
  "bin",
@@ -40,6 +40,8 @@
40
40
  },
41
41
  "dependencies": {
42
42
  "@earendil-works/pi-coding-agent": "^0.80.2",
43
+ "@vitest/coverage-v8": "^4.1.9",
44
+ "vitest": "^4.1.9",
43
45
  "zod": "^3.23.0"
44
46
  },
45
47
  "engines": {
@@ -1,15 +1,23 @@
1
1
  /**
2
2
  * Per-file coverage via vitest + lcov.
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.
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
- import { mkdtemp, rm } from 'node:fs/promises'
9
+ import { mkdtemp, readdir, 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?[/\\]/
14
22
  const MAX_ERRORS_PER_FILE = 5
15
23
  const MAX_ERROR_LINES = 10
@@ -85,9 +93,9 @@ export async function measureCoveragePerFile(dir) {
85
93
 
86
94
  try {
87
95
  spawnSync(
88
- 'bunx',
96
+ process.execPath,
89
97
  [
90
- 'vitest',
98
+ VITEST_BIN,
91
99
  'run',
92
100
  '--passWithNoTests',
93
101
  '--coverage',
@@ -125,3 +133,41 @@ export async function measureCoveragePerFile(dir) {
125
133
  export function getUncoveredFiles(files, threshold = 80) {
126
134
  return files.filter(f => f.pct < threshold)
127
135
  }
136
+
137
+ const SOURCE_EXT_RE = /\.(mjs|js|ts|vue|py)$/
138
+ const IGNORE_DIRS = new Set([
139
+ 'node_modules', 'dist', 'build', 'out', '.git', '__pycache__',
140
+ 'coverage', '.cursor', '.claude', '.pi', 'docs', 'bin', 'reports'
141
+ ])
142
+
143
+ /**
144
+ * Recursively finds source code files in a directory, excluding tests and
145
+ * ignored directories. Used for bootstrap when no coverage data exists.
146
+ *
147
+ * @param {string} dir project root
148
+ * @returns {Promise<string[]>} relative paths to source files
149
+ */
150
+ export async function findSourceFiles(dir) {
151
+ const results = []
152
+
153
+ async function walk(current, relBase) {
154
+ let entries
155
+ try {
156
+ entries = await readdir(current, { withFileTypes: true })
157
+ } catch {
158
+ return
159
+ }
160
+ for (const entry of entries) {
161
+ if (entry.name.startsWith('.')) continue
162
+ const rel = relBase ? `${relBase}/${entry.name}` : entry.name
163
+ if (entry.isDirectory()) {
164
+ if (!IGNORE_DIRS.has(entry.name)) await walk(join(current, entry.name), rel)
165
+ } else if (entry.isFile() && SOURCE_EXT_RE.test(entry.name) && !TEST_FILE_RE.test(rel)) {
166
+ results.push(rel)
167
+ }
168
+ }
169
+ }
170
+
171
+ await walk(dir, '')
172
+ return results
173
+ }
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,7 +35,7 @@ export async function getFailingTests(dir) {
28
35
  const outputFile = join(tmpDir, 'results.json')
29
36
 
30
37
  try {
31
- spawnSync('bunx', ['vitest', 'run', '--reporter=json', `--outputFile=${outputFile}`, '--passWithNoTests'], {
38
+ spawnSync(process.execPath, [VITEST_BIN, 'run', '--reporter=json', `--outputFile=${outputFile}`, '--passWithNoTests'], {
32
39
  cwd: dir,
33
40
  stdio: 'inherit',
34
41
  env
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
- console.log('⚠ Vitest coverage не повернула даних — перевір налаштування vitest.')
47
- break
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)