@7n/test 0.8.0 → 0.10.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 +28 -0
- package/package.json +4 -2
- package/src/assess-need.mjs +4 -1
- package/src/classify-exports.mjs +108 -0
- package/src/coverage-classify/docs/apply.md +28 -0
- package/src/coverage-classify/docs/cache.md +34 -0
- package/src/coverage-classify/docs/index.md +40 -0
- package/src/coverage-classify/docs/prompt.md +30 -0
- package/src/coverage-classify/docs/verdict-schema.md +29 -0
- package/src/coverage-fix-extract.mjs +10 -10
- package/src/coverage-per-file.mjs +68 -43
- package/src/docs/assess-need.md +31 -0
- package/src/docs/classify-exports.md +30 -0
- package/src/docs/coverage-fix-extract.md +35 -0
- package/src/docs/coverage-fix.md +31 -0
- package/src/docs/coverage-per-file.md +34 -0
- package/src/docs/fix-tests.md +32 -0
- package/src/docs/gen-tests.md +38 -0
- package/src/docs/index.md +34 -0
- package/src/docs/run.md +33 -0
- package/src/fix-tests.mjs +233 -87
- package/src/gen-tests.mjs +1102 -49
- package/src/lib/ast-analyze.mjs +287 -0
- package/src/lib/docs/ast-analyze.md +45 -0
- package/src/lib/docs/index.md +14 -0
- package/src/lib/docs/pi-client.md +29 -0
- package/src/lib/docs/runtime-probe.md +36 -0
- package/src/lib/docs/vitest-shim.md +31 -0
- package/src/lib/pi-client.mjs +18 -3
- package/src/lib/runtime-probe.mjs +390 -0
- package/src/lib/vitest-shim.mjs +73 -0
- package/src/run.mjs +30 -26
- package/src/scripts/lib/changed-files.mjs +5 -5
- package/src/scripts/lib/docs/changed-files.md +32 -0
- package/src/scripts/lib/docs/read-n-cursor-config-lite.md +39 -0
- package/src/scripts/utils/docs/index.md +13 -0
- package/src/scripts/utils/docs/lock-cache-dir.md +32 -0
- package/src/scripts/utils/docs/with-lock.md +38 -0
- package/src/scripts/utils/docs/worktree-fingerprint.md +34 -0
- package/src/scripts/utils/lock-cache-dir.mjs +1 -1
- package/src/scripts/utils/with-lock.mjs +2 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.10.0] - 2026-07-02
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- Оновлено залежність @nitra/check-env до ^4.0.0
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Coverage/gen-tests/fix-tests тепер віддають перевагу локально встановленому vitest цільового проєкту замість підміненого шим-конфіга: власний vitest.config.js (setupFiles, environment, plugins) і провайдери оточення (happy-dom тощо) з node_modules цільового проєкту більше не ігноруються.
|
|
12
|
+
|
|
13
|
+
## [0.9.1] - 2026-07-02
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- docs
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- Виніс спільний парсинг vitest failure у `coverage-per-file.mjs` і перевів `fix-tests.mjs` на його повторне використання, щоб прибрати jscpd-дубль.
|
|
22
|
+
- - Зменшено cognitive complexity генерації тестів через винесення підготовки контексту та tiered block generation у менші helper-и.
|
|
23
|
+
- - Виправлено JSDoc-описи та спрощено цикл генерації тестових блоків, щоб пройти JS lint для `gen-tests.mjs`.
|
|
24
|
+
|
|
25
|
+
## [0.9.0] - 2026-06-28
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- В нормальному циклі `assessNeed` (LLM-оцінка) повністю прибрано: coverage % < порогу є достатнім сигналом для генерації тестів. У bootstrap-режимі тепер використовується тільки `quickClassify` (локально, без LLM). Таким чином нульова кількість LLM-викликів до `assessNeed` у будь-якому сценарії.
|
|
30
|
+
|
|
3
31
|
## [0.8.0] - 2026-06-28
|
|
4
32
|
|
|
5
33
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@7n/test",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "CLI-утиліта @7n/test",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"7n",
|
|
@@ -41,8 +41,10 @@
|
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@earendil-works/pi-coding-agent": "^0.80.2",
|
|
43
43
|
"@vitest/coverage-v8": "^4.1.9",
|
|
44
|
+
"rollup": "^4.62.2",
|
|
44
45
|
"vitest": "^4.1.9",
|
|
45
|
-
"zod": "^3.23.0"
|
|
46
|
+
"zod": "^3.23.0",
|
|
47
|
+
"@nitra/check-env": "^4.0.0"
|
|
46
48
|
},
|
|
47
49
|
"engines": {
|
|
48
50
|
"bun": ">=1.3",
|
package/src/assess-need.mjs
CHANGED
|
@@ -48,7 +48,10 @@ function stripComments(src) {
|
|
|
48
48
|
*/
|
|
49
49
|
export function quickClassify(content) {
|
|
50
50
|
const stripped = stripComments(content)
|
|
51
|
-
const lines = stripped
|
|
51
|
+
const lines = stripped
|
|
52
|
+
.split('\n')
|
|
53
|
+
.map(l => l.trim())
|
|
54
|
+
.filter(Boolean)
|
|
52
55
|
|
|
53
56
|
// All lines are imports/re-exports → no testable logic
|
|
54
57
|
if (lines.length > 0 && lines.every(l => WIRING_RE.test(l))) {
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Classifies named exports of a JS/MJS source file by test-generation complexity.
|
|
3
|
+
* Used to route: trivial/simple → local LLM, complex → cloud LLM.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const EXPORT_RE = /^export\s+(?:async\s+)?(?:const|function|class|let)\s+(\w+)/gm
|
|
7
|
+
const PRIMITIVE_LITERAL_RE = /^(?:\d[\d_]*(?:\.\d+)?|'[^']*'|"[^"]*"|true|false|null)\s*$/
|
|
8
|
+
const NEXT_EXPORT_RE = /\nexport\s/m
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Patterns that flag an export as too complex for local model.
|
|
12
|
+
* Matched against the export's extracted body (up to 3000 chars).
|
|
13
|
+
*/
|
|
14
|
+
const COMPLEX_SIGNALS = [
|
|
15
|
+
/\bfetch\b/,
|
|
16
|
+
/\bnew\s+Date\b/,
|
|
17
|
+
/\bprocess\.env\b/,
|
|
18
|
+
/\benv\.[A-Z_]{2,}/,
|
|
19
|
+
/\bFormData\b/,
|
|
20
|
+
/\bcheckEnv\b/,
|
|
21
|
+
/\bXMLHttpRequest\b/,
|
|
22
|
+
/\bWebSocket\b/,
|
|
23
|
+
/\bsetTimeout\b|\bsetInterval\b/
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {'trivial'|'simple'|'complex'} ExportComplexity
|
|
28
|
+
* @typedef {{ name: string, complexity: ExportComplexity }} ExportInfo
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Extracts all named exports and classifies each by test complexity.
|
|
33
|
+
* @param {string} content source file text
|
|
34
|
+
* @returns {ExportInfo[]} named exports with complexity labels
|
|
35
|
+
*/
|
|
36
|
+
export function extractExportsWithComplexity(content) {
|
|
37
|
+
const names = Array.from(content.matchAll(EXPORT_RE), m => m[1])
|
|
38
|
+
return names.map(name => ({ name, complexity: classifyExport(name, content) }))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Classifies one export by inspecting the code region that defines it.
|
|
43
|
+
* @param {string} name export name
|
|
44
|
+
* @param {string} content source file text
|
|
45
|
+
* @returns {ExportComplexity} complexity label
|
|
46
|
+
*/
|
|
47
|
+
function classifyExport(name, content) {
|
|
48
|
+
if (isPrimitiveConstExport(name, content)) return 'trivial'
|
|
49
|
+
|
|
50
|
+
const body = extractBody(name, content)
|
|
51
|
+
if (!body) return 'simple'
|
|
52
|
+
if (COMPLEX_SIGNALS.some(re => re.test(body))) return 'complex'
|
|
53
|
+
return 'simple'
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Checks whether the export is a primitive `export const NAME = <literal>`.
|
|
58
|
+
* @param {string} name export name
|
|
59
|
+
* @param {string} content source file text
|
|
60
|
+
* @returns {boolean} true when the export is a primitive constant
|
|
61
|
+
*/
|
|
62
|
+
function isPrimitiveConstExport(name, content) {
|
|
63
|
+
const prefix = `export const ${name} =`
|
|
64
|
+
for (const line of content.split('\n')) {
|
|
65
|
+
if (!line.startsWith(prefix)) continue
|
|
66
|
+
return PRIMITIVE_LITERAL_RE.test(line.slice(prefix.length).trim())
|
|
67
|
+
}
|
|
68
|
+
return false
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Finds the declaration start for a named export.
|
|
73
|
+
* @param {string} name export name
|
|
74
|
+
* @param {string} content source file text
|
|
75
|
+
* @returns {number} start index or `-1`
|
|
76
|
+
*/
|
|
77
|
+
function findExportStart(name, content) {
|
|
78
|
+
const prefixes = [
|
|
79
|
+
`export async function ${name}`,
|
|
80
|
+
`export function ${name}`,
|
|
81
|
+
`export const ${name}`,
|
|
82
|
+
`export class ${name}`,
|
|
83
|
+
`export let ${name}`
|
|
84
|
+
]
|
|
85
|
+
let start = -1
|
|
86
|
+
for (const prefix of prefixes) {
|
|
87
|
+
const idx = content.indexOf(prefix)
|
|
88
|
+
if (idx !== -1 && (start === -1 || idx < start)) start = idx
|
|
89
|
+
}
|
|
90
|
+
return start
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Extracts the code region from the export declaration to the next export.
|
|
95
|
+
* Used for complexity signal matching only — not exact AST.
|
|
96
|
+
* @param {string} name export name
|
|
97
|
+
* @param {string} content source file text
|
|
98
|
+
* @returns {string|null} declaration snippet or `null`
|
|
99
|
+
*/
|
|
100
|
+
function extractBody(name, content) {
|
|
101
|
+
const start = findExportStart(name, content)
|
|
102
|
+
if (start === -1) return null
|
|
103
|
+
|
|
104
|
+
const after = content.slice(start)
|
|
105
|
+
const nextExport = after.search(NEXT_EXPORT_RE)
|
|
106
|
+
const end = nextExport === -1 ? Math.min(after.length, 3000) : nextExport
|
|
107
|
+
return after.slice(0, end)
|
|
108
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: apply.mjs
|
|
4
|
+
resource: npm/src/coverage-classify/apply.mjs
|
|
5
|
+
docgen:
|
|
6
|
+
crc: 0f54e6a0
|
|
7
|
+
model: omlx/gemma-4-e4b-it-OptiQ-4bit
|
|
8
|
+
score: 100
|
|
9
|
+
judgeModel: openai-codex/gpt-5.4-mini
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Огляд
|
|
13
|
+
|
|
14
|
+
Файл відповідає за застосування вердиктів до рядків покриття, фільтруючи виживших мутантів. Застосовується Skip rule, якщо `verdict` належить до `{equivalent, defensive, glue, wrapper}` і `confidence` досягає заданого порогу. Для інших мутантів, включаючи ті, що мають `worth-testing` або низьку впевненість, мутанти залишаються в наборі виживших. Крім того, загальна кількість мутантів (`mutation.total`) декрементується на кількість `allowed-gaps`, а окремий список `allowedGaps` повертається для візуалізації в `COVERAGE.md`.
|
|
15
|
+
|
|
16
|
+
## Поведінка
|
|
17
|
+
|
|
18
|
+
isAllowedGap визначає, чи мутант відповідає критеріям для віднесення до категорії allowed-gap, що дозволяє його ігнорувати при оцінці покриття.
|
|
19
|
+
applyVerdicts фільтрує виживших мутантів на основі наданих verdict-ів, зменшує загальну кількість мутантів та повертає новий набір рядків покриття разом зі списком allowedGaps.
|
|
20
|
+
|
|
21
|
+
## Публічний API
|
|
22
|
+
|
|
23
|
+
isAllowedGap — Визначає, чи слід вважати мутанта прийнятним пропуском (виключає його з категорії потенційно "вбиваних" мутантів).
|
|
24
|
+
applyVerdicts — Призначає рішення (verdicts) до рядків покриття. Відфільтровує мутанти, які вижили, якщо вони кваліфікуються як прийнятний пропуск, і зменшує загальну кількість мутантів у системі відповідно до кількості таких прийнятних пропусків.
|
|
25
|
+
|
|
26
|
+
## Гарантії поведінки
|
|
27
|
+
|
|
28
|
+
- (специфічних машинно-виведених гарантій немає)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: cache.mjs
|
|
4
|
+
resource: npm/src/coverage-classify/cache.mjs
|
|
5
|
+
docgen:
|
|
6
|
+
crc: 53b251b1
|
|
7
|
+
model: omlx/gemma-4-e4b-it-OptiQ-4bit
|
|
8
|
+
score: 100
|
|
9
|
+
issues: judge:inaccurate:0.98
|
|
10
|
+
judgeModel: openai-codex/gpt-5.4-mini
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Огляд
|
|
14
|
+
|
|
15
|
+
Механізм кешування фіксує вердикти класифікації мутантів для прискорення аналізу. Він використовує кеш, описаний у `cache.json`, де ключами є ідентифікатори контенту у форматі `<blob-hash>:<line>:<col>:<base64url>`, що динамічно формується на основі хешу файлу (`git hash-object` або `sha1`). Збережені дані включають вердикт, рівень впевненості та причину, а інвалідація кешу спрацьовує при зміні джерела коду, що призводить до нового хешу.
|
|
16
|
+
|
|
17
|
+
## Поведінка
|
|
18
|
+
|
|
19
|
+
deriveBlobHash обчислює унікальний хеш контенту файлу, використовуючи `git hash-object` або SHA256 контенту у випадку відсутності Git.
|
|
20
|
+
deriveCacheKey створює унікальний ключ кешу для мутанта на основі хешу файлу, номера рядка, колонки та заміни.
|
|
21
|
+
readCache зчитує дані кешу з файлу, повертаючи порожній кеш у разі відсутності або некоректності.
|
|
22
|
+
writeCache зберігає об'єкт кешу на диск, автоматично створюючи необхідні батьківські директорії.
|
|
23
|
+
|
|
24
|
+
## Публічний API
|
|
25
|
+
|
|
26
|
+
deriveBlobHash — Створює унікальний ідентифікатор вмісту файлу (SHA1 хеш).
|
|
27
|
+
deriveCacheKey — Генерує ключ для кешування, що описує зміну файлу у певному стані.
|
|
28
|
+
readCache — Завантажує кешовані дані з диска; повертає порожній кеш при будь-якій помилці зчитування.
|
|
29
|
+
writeCache — Зберігає кешовані дані на диск, створюючи необхідну структуру каталогів.
|
|
30
|
+
|
|
31
|
+
## Гарантії поведінки
|
|
32
|
+
|
|
33
|
+
- Перехоплює помилки і не пропускає винятків назовні (fail-safe).
|
|
34
|
+
- Кешує результати в межах одного прогону.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: index.mjs
|
|
4
|
+
resource: npm/src/coverage-classify/index.mjs
|
|
5
|
+
docgen:
|
|
6
|
+
crc: b7eff232
|
|
7
|
+
model: omlx/gemma-4-e4b-it-OptiQ-4bit
|
|
8
|
+
score: 100
|
|
9
|
+
issues: judge:inaccurate:0.99
|
|
10
|
+
judgeModel: openai-codex/gpt-5.4-mini
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Огляд
|
|
14
|
+
|
|
15
|
+
Класифікатор визначає, чи слід вважати мутант, переданий у межах `classify`, мутантом, згенерованим тестом. Механізм працює через маршрутизацію через pi SDK (callText). Процес включає перевірку в кеші, пошук у локальній (Tier 1) моделі, пошук у хмарній (Tier 2) моделі, а у разі повної відмови систем — застосування консервативного результату. Усі результати класифікації зберігаються у кеші, який контролюється конфігурацією у `coverage-classify.cache.json`.
|
|
16
|
+
|
|
17
|
+
Ключовий механізм роботи:
|
|
18
|
+
Перевірка кешу з файлу `coverage-classify.cache.json`. Якщо відповідний запис знайдено, повертається збережений verdicts[]. Якщо кеш не містить результату, ініціюється послідовна класифікація: спочатку за допомогою локальної моделі (`N_LOCAL_MIN_MODEL`), а після невдачі — за допомогою хмарної моделі (`N_CLOUD_MIN_MODEL`). У випадку невдачі обох рівнів, застосовується консервативний fallback (worth-testing/confidence=0). Після успішної класифікації результат додається до кешу.
|
|
19
|
+
|
|
20
|
+
## Поведінка
|
|
21
|
+
|
|
22
|
+
1. Отримується кеш з файлу `coverage-classify.cache.json`. Якщо конфігурація моделей у кеші не відповідає поточній, кеш ініціалізується.
|
|
23
|
+
2. Для кожного скомпільованого мутанта у наданому наборі виконується перевірка в кеші за унікальним ключем, що включає шлях до файлу, рядок, стовпець та заміну.
|
|
24
|
+
3. Якщо знайдено відповідний запис у кеші, він повертається як результат класифікації.
|
|
25
|
+
4. Якщо в кеші запису немає, ініціюється класифікація мутанта.
|
|
26
|
+
5. Класифікація здійснюється послідовно через джерела ШІ: спочатку через модель, визначену у змінній `N_LOCAL_MIN_MODEL`, а якщо це не вдається, то через модель, визначену у змінній `N_CLOUD_MIN_MODEL`.
|
|
27
|
+
6. У разі невдачі обох моделей, встановлюється консервативний результат, що вважається мутантом, вартим тестування з низькою впевненістю.
|
|
28
|
+
7. Успішно класифікований результат додається до кешу з часовою відміткою.
|
|
29
|
+
8. Після обробки всіх мутантів, оновлений кеш записується у файл `coverage-classify.cache.json`.
|
|
30
|
+
9. Повертається список результатів класифікації для всіх мутантів.
|
|
31
|
+
|
|
32
|
+
## Публічний API
|
|
33
|
+
|
|
34
|
+
- classify — Визначає, які мутанти вижили, використовуючи модель для локальних даних, потім для хмарних, а якщо це не вдається, застосовує запасний варіант.
|
|
35
|
+
|
|
36
|
+
## Гарантії поведінки
|
|
37
|
+
|
|
38
|
+
- Read-only: не виконує операцій запису (ФС/БД).
|
|
39
|
+
- Перехоплює помилки і не пропускає винятків назовні (fail-safe).
|
|
40
|
+
- Кешує результати в межах одного прогону.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: prompt.mjs
|
|
4
|
+
resource: npm/src/coverage-classify/prompt.mjs
|
|
5
|
+
docgen:
|
|
6
|
+
crc: 12bfb99a
|
|
7
|
+
model: omlx/gemma-4-e4b-it-OptiQ-4bit
|
|
8
|
+
score: 100
|
|
9
|
+
issues: judge:inaccurate:0.98
|
|
10
|
+
judgeModel: openai-codex/gpt-5.4-mini
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Огляд
|
|
14
|
+
|
|
15
|
+
Промпт-builder для `coverage-classify` генерує контекстні інструкції. Він визначає статичну роль класифікатора (`SYSTEM_PROMPT`), яка кешується через `cache_control: ephemeral` при виклику API. Динамічно він збирає деталі кожного мутанта (location, source $\pm$10, tests, git) у промпт, що здійснюється функцією `buildUserPrompt`. Процес працює з механізмом fail-safe, запобігаючи виникненню винятків, а кешування відбувається в межах одного прогону.
|
|
16
|
+
|
|
17
|
+
## Поведінка
|
|
18
|
+
|
|
19
|
+
SYSTEM_PROMPT визначає роль класифікатора мутацій і встановлює чіткий JSON-схему для відповіді, де мутації класифікуються як 'worth-testing', 'equivalent', 'defensive', 'glue' або 'wrapper'.
|
|
20
|
+
buildUserPrompt збирає повний контекст для класифікації конкретного мутанта, включаючи фрагмент вихідного коду навколо місця мутації, вміст супутніх тестів та останні зміни у Git для створення деталізованого промпта.
|
|
21
|
+
|
|
22
|
+
## Публічний API
|
|
23
|
+
|
|
24
|
+
SYSTEM_PROMPT — Визначає основні інструкції та обмеження для AI-агента.
|
|
25
|
+
buildUserPrompt — Формує запит від користувача для розпізнавання конкретного мутагенту.
|
|
26
|
+
|
|
27
|
+
## Гарантії поведінки
|
|
28
|
+
|
|
29
|
+
- Перехоплює помилки і не пропускає винятків назовні (fail-safe).
|
|
30
|
+
- Кешує результати в межах одного прогону.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: verdict-schema.mjs
|
|
4
|
+
resource: npm/src/coverage-classify/verdict-schema.mjs
|
|
5
|
+
docgen:
|
|
6
|
+
crc: ecf5dfe1
|
|
7
|
+
model: omlx/gemma-4-e4b-it-OptiQ-4bit
|
|
8
|
+
score: 100
|
|
9
|
+
issues: judge:inaccurate:0.98
|
|
10
|
+
judgeModel: openai-codex/gpt-5.4-mini
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Огляд
|
|
14
|
+
|
|
15
|
+
Цей файл визначає `VerdictSchema` для структурованого відображення результатів класифікації, які генерує LLM-класифікатор (coverage-classify). Функція `parseVerdict` відповідає за вилучення та валідацію JSON-структури з необробленого тексту відповіді LLM, що дозволяє класифікувати вихід у категорії: `worth-testing`, `equivalent`, `defensive`, `glue` або `wrapper`.
|
|
16
|
+
|
|
17
|
+
## Поведінка
|
|
18
|
+
|
|
19
|
+
VerdictSchema: Визначає структуру даних для результатів класифікації, отриманих від LLM.
|
|
20
|
+
parseVerdict: Витягує та перевіряє відповідність JSON-об'єкта з текстової відповіді LLM визначеній схемі.
|
|
21
|
+
|
|
22
|
+
## Публічний API
|
|
23
|
+
|
|
24
|
+
VerdictSchema — схема, що описує очікувану структуру відповіді від LLM.
|
|
25
|
+
parseVerdict — видобуває та перевіряє відповідь від LLM щодо відповідності схемі VerdictSchema.
|
|
26
|
+
|
|
27
|
+
## Гарантії поведінки
|
|
28
|
+
|
|
29
|
+
- Read-only: не виконує операцій запису (ФС/БД).
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* спалює сотні тисяч токенів лише на парсинг. Натомість важкий парсинг несе цей
|
|
8
8
|
* скрипт (для JS — мілісекунди, 0 токенів), а агенту віддається рівно потрібна
|
|
9
9
|
* порція:
|
|
10
|
-
* - `index` — крихітний `
|
|
10
|
+
* - `index` — крихітний масив записів `file`/`mutants` для рішення про фан-аут;
|
|
11
11
|
* - `slice --file <path>` — промпт лише для одного файлу (контекст ±3 рядки),
|
|
12
12
|
* рівно під когнітивне навантаження одного субагента.
|
|
13
13
|
*
|
|
@@ -23,7 +23,7 @@ import { buildFixPrompt } from './coverage-fix.mjs'
|
|
|
23
23
|
const SURVIVED_SECTION = '## Вцілілі мутанти'
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
|
-
* Огорожа json-блоку: ≥3
|
|
26
|
+
* Огорожа json-блоку: ≥3 зворотних апострофи, далі `json` і решта рядка до `\n`. Довжина
|
|
27
27
|
* захоплюється в групу 1 — renderMarkdown пише 3, але oxfmt підвищує до 4+, коли
|
|
28
28
|
* сам JSON-вміст містить ``` (типово для original/replacement мутантів).
|
|
29
29
|
*/
|
|
@@ -31,9 +31,9 @@ const FENCE_OPEN_RE = /(`{3,8})json[^\n]{0,200}\n/
|
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* Витягує JSON-масив вцілілих мутантів із тексту COVERAGE.md: знаходить секцію
|
|
34
|
-
* `## Вцілілі мутанти`, перший огороджений
|
|
34
|
+
* `## Вцілілі мутанти`, перший огороджений JSON-блок під нею і парсить.
|
|
35
35
|
* @param {string} md повний текст COVERAGE.md
|
|
36
|
-
* @returns {
|
|
36
|
+
* @returns {object[]} групи вцілілих по файлах (порожньо, якщо секції/блоку немає або JSON невалідний)
|
|
37
37
|
*/
|
|
38
38
|
export function parseSurvivedBlock(md) {
|
|
39
39
|
const sectionAt = md.indexOf(SURVIVED_SECTION)
|
|
@@ -44,9 +44,9 @@ export function parseSurvivedBlock(md) {
|
|
|
44
44
|
const fence = open[1]
|
|
45
45
|
const bodyStart = open.index + open[0].length
|
|
46
46
|
const rest = after.slice(bodyStart)
|
|
47
|
-
// Закриття — рядок із тих самих
|
|
47
|
+
// Закриття — рядок із тих самих зворотних апострофів. Усередині JSON реальних переводів
|
|
48
48
|
// рядка немає (JSON.stringify екранує їх як `\n`), тож `\n<fence>` унікально
|
|
49
|
-
// позначає кінець блоку навіть якщо значення містять
|
|
49
|
+
// позначає кінець блоку навіть якщо значення містять зворотні апострофи.
|
|
50
50
|
const closeAt = rest.indexOf(`\n${fence}`)
|
|
51
51
|
const json = closeAt === -1 ? rest : rest.slice(0, closeAt)
|
|
52
52
|
try {
|
|
@@ -60,7 +60,7 @@ export function parseSurvivedBlock(md) {
|
|
|
60
60
|
/**
|
|
61
61
|
* Читає `COVERAGE.md` із кореня проєкту і повертає структуровані групи вцілілих.
|
|
62
62
|
* @param {string} cwd корінь проєкту
|
|
63
|
-
* @returns {Promise<
|
|
63
|
+
* @returns {Promise<object[]>} групи вцілілих по файлах
|
|
64
64
|
*/
|
|
65
65
|
export async function readSurvived(cwd) {
|
|
66
66
|
let md
|
|
@@ -73,9 +73,9 @@ export async function readSurvived(cwd) {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
/**
|
|
76
|
-
* Згортає групи вцілілих у компактний index `
|
|
77
|
-
* @param {
|
|
78
|
-
* @returns {
|
|
76
|
+
* Згортає групи вцілілих у компактний index із полями `file` і `mutants`.
|
|
77
|
+
* @param {object[]} survived групи вцілілих
|
|
78
|
+
* @returns {object[]} файл → кількість вцілілих мутантів
|
|
79
79
|
*/
|
|
80
80
|
export function buildIndex(survived) {
|
|
81
81
|
return survived
|
|
@@ -1,27 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Per-file coverage via vitest + lcov.
|
|
3
|
-
* Runs vitest (bundled with
|
|
3
|
+
* Runs vitest (bundled with \@7n/test) in a single pass and returns
|
|
4
4
|
* both per-file coverage data and failing tests.
|
|
5
|
-
* Target projects do NOT need vitest or
|
|
5
|
+
* Target projects do NOT need vitest or \@vitest/coverage-v8 installed.
|
|
6
6
|
*/
|
|
7
7
|
import { spawnSync } from 'node:child_process'
|
|
8
8
|
import { existsSync, readFileSync } from 'node:fs'
|
|
9
9
|
import { mkdtemp, readdir, rm } from 'node:fs/promises'
|
|
10
10
|
import { tmpdir } from 'node:os'
|
|
11
|
-
import {
|
|
12
|
-
import { join, relative, dirname } from 'node:path'
|
|
11
|
+
import { join, relative } from 'node:path'
|
|
13
12
|
import { env } from 'node:process'
|
|
13
|
+
import { resolveVitestRun } from './lib/vitest-shim.mjs'
|
|
14
14
|
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
const TEST_FILE_RE = /\.(test|spec)\.[^.]+$|[/\\]tests?[/\\]/
|
|
15
|
+
const TEST_FILE_RE = /\.(test|spec)\.[^.]+$|(?:^|[/\\])tests?[/\\]/
|
|
16
|
+
const VITEST_UNSUPPORTED_TEST_RE = /bun:test|Cannot find package 'bun/i
|
|
19
17
|
const MAX_ERRORS_PER_FILE = 5
|
|
20
18
|
const MAX_ERROR_LINES = 10
|
|
21
19
|
|
|
22
20
|
/**
|
|
23
21
|
* @param {string} text lcov.info content
|
|
24
|
-
* @returns {Array<{file: string, pct: number, linesFound: number, linesCovered: number}>}
|
|
22
|
+
* @returns {Array<{file: string, pct: number, linesFound: number, linesCovered: number}>} per-file coverage rows parsed from lcov
|
|
25
23
|
*/
|
|
26
24
|
function parseLcovPerFile(text) {
|
|
27
25
|
const files = []
|
|
@@ -53,32 +51,37 @@ function parseLcovPerFile(text) {
|
|
|
53
51
|
/**
|
|
54
52
|
* @param {string} jsonPath path to vitest JSON results file
|
|
55
53
|
* @param {string} dir project root for relative paths
|
|
56
|
-
* @returns {Array<{file: string, errors: string[]}>}
|
|
54
|
+
* @returns {Array<{file: string, errors: string[]}>} список failing test-файлів з короткими помилками
|
|
57
55
|
*/
|
|
58
|
-
function parseFailingTests(jsonPath, dir) {
|
|
56
|
+
export function parseFailingTests(jsonPath, dir) {
|
|
59
57
|
try {
|
|
60
58
|
const data = JSON.parse(readFileSync(jsonPath, 'utf8'))
|
|
61
|
-
return (
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
59
|
+
return (
|
|
60
|
+
(data.testResults ?? [])
|
|
61
|
+
.filter(r => r.status === 'failed')
|
|
62
|
+
.map(r => {
|
|
63
|
+
const assertionErrors = (r.assertionResults ?? [])
|
|
64
|
+
.filter(a => a.status === 'failed')
|
|
65
|
+
.slice(0, MAX_ERRORS_PER_FILE)
|
|
66
|
+
.map(a => {
|
|
67
|
+
const name = [...(a.ancestorTitles ?? []), a.title].join(' > ')
|
|
68
|
+
const msg = (a.failureMessages?.[0] ?? '').split('\n').slice(0, MAX_ERROR_LINES).join('\n')
|
|
69
|
+
return `${name}:\n${msg}`
|
|
70
|
+
})
|
|
71
|
+
// Module-level errors (import/syntax) produce no assertionResults
|
|
72
|
+
const errors =
|
|
73
|
+
assertionErrors.length > 0
|
|
74
|
+
? assertionErrors
|
|
75
|
+
: [
|
|
76
|
+
`Suite error: ${(r.message ?? r.failureMessage ?? 'module-level failure').split('\n').slice(0, MAX_ERROR_LINES).join('\n')}`
|
|
77
|
+
]
|
|
78
|
+
return { file: relative(dir, r.testFilePath ?? r.name), errors }
|
|
79
|
+
})
|
|
80
|
+
.filter(f => !f.file.startsWith('..'))
|
|
81
|
+
// Skip test files that use a non-vitest runner (bun:test, jest, etc.)
|
|
82
|
+
// They are expected to fail and cannot be fixed by this tool.
|
|
83
|
+
.filter(f => f.errors.every(e => !VITEST_UNSUPPORTED_TEST_RE.test(e)))
|
|
84
|
+
)
|
|
82
85
|
} catch {
|
|
83
86
|
return []
|
|
84
87
|
}
|
|
@@ -87,20 +90,23 @@ function parseFailingTests(jsonPath, dir) {
|
|
|
87
90
|
/**
|
|
88
91
|
* Runs vitest coverage + JSON reporter in a single pass.
|
|
89
92
|
* Returns per-file coverage and any failing tests detected in the same run.
|
|
90
|
-
*
|
|
91
93
|
* @param {string} dir project root
|
|
92
|
-
* @returns {Promise<{files: Array<{file: string, pct: number, linesFound: number, linesCovered: number}>, failingTests: Array<{file: string, errors: string[]}>}>}
|
|
94
|
+
* @returns {Promise<{files: Array<{file: string, pct: number, linesFound: number, linesCovered: number}>, failingTests: Array<{file: string, errors: string[]}>}>} coverage rows and failing tests from one vitest run
|
|
93
95
|
*/
|
|
94
96
|
export async function measureCoveragePerFile(dir) {
|
|
95
97
|
const lcovDir = await mkdtemp(join(tmpdir(), '7n-cov-'))
|
|
96
98
|
const jsonResultsFile = join(lcovDir, 'test-results.json')
|
|
97
99
|
|
|
100
|
+
const { bin, configArgs } = resolveVitestRun(dir)
|
|
98
101
|
try {
|
|
99
102
|
spawnSync(
|
|
100
103
|
process.execPath,
|
|
101
104
|
[
|
|
102
|
-
|
|
105
|
+
bin,
|
|
103
106
|
'run',
|
|
107
|
+
...configArgs,
|
|
108
|
+
'--root',
|
|
109
|
+
dir,
|
|
104
110
|
'--passWithNoTests',
|
|
105
111
|
'--coverage',
|
|
106
112
|
'--coverage.reporter=lcov',
|
|
@@ -124,15 +130,19 @@ export async function measureCoveragePerFile(dir) {
|
|
|
124
130
|
|
|
125
131
|
return { files, failingTests }
|
|
126
132
|
} finally {
|
|
127
|
-
|
|
133
|
+
try {
|
|
134
|
+
await rm(lcovDir, { recursive: true, force: true })
|
|
135
|
+
} catch {
|
|
136
|
+
/* ignore cleanup failures */
|
|
137
|
+
}
|
|
128
138
|
}
|
|
129
139
|
}
|
|
130
140
|
|
|
131
141
|
/**
|
|
132
142
|
* Files below the coverage threshold.
|
|
133
|
-
* @param {Array<{file: string, pct: number}>} files
|
|
134
|
-
* @param {number} [threshold
|
|
135
|
-
* @returns {Array<{file: string, pct: number}>}
|
|
143
|
+
* @param {Array<{file: string, pct: number}>} files список файлів із coverage-метриками
|
|
144
|
+
* @param {number} [threshold] coverage floor; за замовчуванням `80`
|
|
145
|
+
* @returns {Array<{file: string, pct: number}>} файли, чий coverage нижчий за threshold
|
|
136
146
|
*/
|
|
137
147
|
export function getUncoveredFiles(files, threshold = 80) {
|
|
138
148
|
return files.filter(f => f.pct < threshold)
|
|
@@ -152,19 +162,27 @@ const IGNORE_DIRS = new Set([
|
|
|
152
162
|
'.pi',
|
|
153
163
|
'docs',
|
|
154
164
|
'bin',
|
|
155
|
-
'reports'
|
|
165
|
+
'reports',
|
|
166
|
+
'types'
|
|
156
167
|
])
|
|
157
168
|
|
|
169
|
+
// Config/tooling files that are not unit-testable source files.
|
|
170
|
+
const CONFIG_FILE_RE =
|
|
171
|
+
/^(?:vitest|jest|eslint|prettier|stryker|babel|webpack|vite|rollup|tsconfig|jsconfig|knip)\.config\./
|
|
172
|
+
|
|
158
173
|
/**
|
|
159
174
|
* Recursively finds source code files in a directory, excluding tests and
|
|
160
175
|
* ignored directories. Used for bootstrap when no coverage data exists.
|
|
161
|
-
*
|
|
162
176
|
* @param {string} dir project root
|
|
163
|
-
* @returns {Promise<string[]>} relative paths to source files
|
|
177
|
+
* @returns {Promise<string[]>} relative paths to source files that look unit-testable
|
|
164
178
|
*/
|
|
165
179
|
export async function findSourceFiles(dir) {
|
|
166
180
|
const results = []
|
|
167
181
|
|
|
182
|
+
/**
|
|
183
|
+
* @param {string} current absolute directory path
|
|
184
|
+
* @param {string} relBase relative path prefix
|
|
185
|
+
*/
|
|
168
186
|
async function walk(current, relBase) {
|
|
169
187
|
let entries
|
|
170
188
|
try {
|
|
@@ -177,7 +195,14 @@ export async function findSourceFiles(dir) {
|
|
|
177
195
|
const rel = relBase ? `${relBase}/${entry.name}` : entry.name
|
|
178
196
|
if (entry.isDirectory()) {
|
|
179
197
|
if (!IGNORE_DIRS.has(entry.name)) await walk(join(current, entry.name), rel)
|
|
180
|
-
} else if (
|
|
198
|
+
} else if (
|
|
199
|
+
entry.isFile() &&
|
|
200
|
+
SOURCE_EXT_RE.test(entry.name) &&
|
|
201
|
+
!entry.name.endsWith('.d.ts') &&
|
|
202
|
+
!entry.name.endsWith('.d.mts') &&
|
|
203
|
+
!CONFIG_FILE_RE.test(entry.name) &&
|
|
204
|
+
!TEST_FILE_RE.test(rel)
|
|
205
|
+
) {
|
|
181
206
|
results.push(rel)
|
|
182
207
|
}
|
|
183
208
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: assess-need.mjs
|
|
4
|
+
resource: npm/src/assess-need.mjs
|
|
5
|
+
docgen:
|
|
6
|
+
crc: 02dd5f01
|
|
7
|
+
model: omlx/gemma-4-e4b-it-OptiQ-4bit
|
|
8
|
+
score: 90
|
|
9
|
+
issues: internal-name:callText,judge:inaccurate:0.98
|
|
10
|
+
judgeModel: openai-codex/gpt-5.4-mini
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Огляд
|
|
14
|
+
|
|
15
|
+
Модуль оцінює необхідність написання юніт-тестів для файлів, які не покриті тестами, застосовуючи спочатку швидкі локальні евристичні перевірки. У випадку невизначеності, для отримання структурованої JSON-відповіді, викликається `callText` (без інструментів). Функціонал керується публічними методами `quickClassify` та `assessNeed`. Модуль працює у read-only режимі, не взаємодіючи з ФС чи БД, та використовує механізм fail-safe для обробки помилок.
|
|
16
|
+
|
|
17
|
+
## Поведінка
|
|
18
|
+
|
|
19
|
+
Поведінка
|
|
20
|
+
quickClassify визначає, чи виправдано написання юніт-тестів для коду на основі швидких локальних евристик, повертаючи рішення або `null`, якщо випадок неоднозначний.
|
|
21
|
+
assessNeed оцінює перелік файлів з низьким покриттям, вирішуючи очевидні випадки локально та передаючи неоднозначні файли для оцінки за допомогою моделі.
|
|
22
|
+
|
|
23
|
+
## Публічний API
|
|
24
|
+
|
|
25
|
+
quickClassify — Швидко визначає клас об'єкта локально, не використовуючи зовнішніх ресурсів (I/O або LLM), повертаючи результат для очевидних випадків або null у випадку невизначеності.
|
|
26
|
+
assessNeed — Визначає, чи потребують тестування перераховані файли, локально розв'язуючи очевидні випадки (наприклад, ре-експорти чи функції з умовами) та ініціюючи виклик LLM лише для неоднозначних файлів.
|
|
27
|
+
|
|
28
|
+
## Гарантії поведінки
|
|
29
|
+
|
|
30
|
+
- Read-only: не виконує операцій запису (ФС/БД).
|
|
31
|
+
- Перехоплює помилки і не пропускає винятків назовні (fail-safe).
|