@7n/test 0.13.0 → 0.14.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 +10 -0
- package/package.json +2 -2
- package/src/coverage-classify/docs/index.md +1 -1
- package/src/coverage-classify/index.mjs +39 -13
- package/src/coverage-fix.mjs +2 -2
- package/src/docs/assess-need.md +1 -1
- package/src/docs/coverage-fix.md +1 -1
- package/src/docs/fix-tests.md +1 -1
- package/src/docs/gen-tests.md +1 -1
- package/src/fix-tests.mjs +73 -47
- package/src/gen-tests.mjs +23 -8
- package/src/index.js +13 -0
- package/src/lib/docs/llm.md +2 -2
- package/src/lib/llm.mjs +10 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.14.0] - 2026-07-11
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Ланцюжки (chains) @nitra/llm-lib ^1.1.0: mutant-classify — chain per mutant (tier1/tier2 = кроки, cache hit без chain), test-generate — chain per file (header + local/cloud спроби + vitest/length-retry = кроки), test-fix — chain per прогін (batch-виклики = кроки); адаптер callText/callAgent прокидає opts.chain. Аналітика ланцюжків — n-llm-chains-report і вкладка «Ланцюжки» в myllm.
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- підняти @7n/llm-lib до ^2.0.2 (rename @nitra/llm-lib → @7n/llm-lib уже застосовано в dacc051; нова мажорна версія без змін API — chains/компресія/body-capture, лише перейменування пакета)
|
|
12
|
+
|
|
3
13
|
## [0.13.0] - 2026-07-05
|
|
4
14
|
|
|
5
15
|
### Changed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@7n/test",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "CLI-утиліта @7n/test",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"7n",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@earendil-works/pi-coding-agent": "^0.80.3",
|
|
43
43
|
"@nitra/check-env": "^4.2.1",
|
|
44
|
-
"@
|
|
44
|
+
"@7n/llm-lib": "^2.0.2",
|
|
45
45
|
"@vitest/coverage-v8": "^4.1.9",
|
|
46
46
|
"rollup": "^4.62.2",
|
|
47
47
|
"vitest": "^4.1.9",
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
import { join } from 'node:path'
|
|
11
11
|
|
|
12
12
|
import { callText } from '../lib/llm.mjs'
|
|
13
|
-
import { CLOUD_MIN, LOCAL_MIN } from '@
|
|
13
|
+
import { CLOUD_MIN, LOCAL_MIN } from '@7n/llm-lib/model-tiers'
|
|
14
|
+
import { startChain } from '@7n/llm-lib/chain'
|
|
14
15
|
import { deriveCacheKey, readCache, writeCache } from './cache.mjs'
|
|
15
16
|
import { buildUserPrompt, SYSTEM_PROMPT } from './prompt.mjs'
|
|
16
17
|
import { parseVerdict } from './verdict-schema.mjs'
|
|
@@ -26,52 +27,77 @@ const FALLBACK_VERDICT = {
|
|
|
26
27
|
* @param {string} prompt
|
|
27
28
|
* @param {string} model provider/model-id або '' для pi-дефолту
|
|
28
29
|
* @param {string} cwd
|
|
30
|
+
* @param {{chain?: object}} [callOpts] chain handle поточного мутанта
|
|
29
31
|
* @returns {Promise<string>}
|
|
30
32
|
*/
|
|
31
|
-
function callModel(prompt, model, cwd) {
|
|
32
|
-
return callText(prompt, { cwd, ...(model && { model }) })
|
|
33
|
+
function callModel(prompt, model, cwd, { chain } = {}) {
|
|
34
|
+
return callText(prompt, { cwd, chain, ...(model && { model }) })
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
/**
|
|
36
38
|
* Два тири: tier1 (local-min) → tier2 (cloud-min) → FALLBACK_VERDICT.
|
|
39
|
+
* Кожен мутант — окремий ланцюжок (kind: mutant-classify): tier1 = крок 1,
|
|
40
|
+
* tier2 = крок 2; fallback-вердикт = outcome:'fail' (LLM не впорався).
|
|
37
41
|
* @param {{file: string, mutants: object[]}} group
|
|
38
42
|
* @param {object} mutant
|
|
39
43
|
* @param {string} cwd
|
|
40
|
-
* @param {(prompt: string, model: string, cwd: string) => Promise<string>} callModelFn
|
|
44
|
+
* @param {(prompt: string, model: string, cwd: string, callOpts?: {chain?: object}) => Promise<string>} callModelFn
|
|
41
45
|
* @param {string} tier1 model-spec першого тиру ('' = pi-дефолт)
|
|
42
46
|
* @param {string} tier2 model-spec другого тиру ('' = pi-дефолт)
|
|
47
|
+
* @param {typeof startChain} makeChain фабрика ланцюжка
|
|
43
48
|
* @returns {Promise<object>} verdict
|
|
44
49
|
*/
|
|
45
|
-
async function classifyOne(group, mutant, cwd, callModelFn, tier1, tier2) {
|
|
50
|
+
async function classifyOne(group, mutant, cwd, callModelFn, tier1, tier2, makeChain) {
|
|
46
51
|
const prompt = `${SYSTEM_PROMPT}\n\n${buildUserPrompt({ ...mutant, file: group.file }, cwd)}`
|
|
47
52
|
const loc = `${group.file}:${mutant.line}:${mutant.col}`
|
|
53
|
+
const chain = makeChain({ kind: 'mutant-classify', unit: loc, cwd })
|
|
48
54
|
|
|
49
55
|
try {
|
|
50
|
-
const text = await callModelFn(prompt, tier1, cwd)
|
|
51
|
-
|
|
56
|
+
const text = await callModelFn(prompt, tier1, cwd, { chain })
|
|
57
|
+
const verdict = parseVerdict(text)
|
|
58
|
+
chain.end({ outcome: 'success', extra: verdictExtra(verdict, mutant) })
|
|
59
|
+
return verdict
|
|
52
60
|
} catch {
|
|
53
61
|
try {
|
|
54
|
-
const text = await callModelFn(prompt, tier2, cwd)
|
|
55
|
-
|
|
62
|
+
const text = await callModelFn(prompt, tier2, cwd, { chain })
|
|
63
|
+
const verdict = parseVerdict(text)
|
|
64
|
+
chain.end({ outcome: 'success', extra: verdictExtra(verdict, mutant) })
|
|
65
|
+
return verdict
|
|
56
66
|
} catch (error) {
|
|
57
67
|
console.warn(`⚠ coverage classify: ${loc} both tiers failed: ${error.message}`)
|
|
68
|
+
chain.end({ outcome: 'fail', extra: { error: String(error.message ?? error).slice(0, 200) } })
|
|
58
69
|
return { ...FALLBACK_VERDICT }
|
|
59
70
|
}
|
|
60
71
|
}
|
|
61
72
|
}
|
|
62
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Extra-поля фінального chain-запису мутанта.
|
|
76
|
+
* @param {{verdict: string, confidence: number}} verdict розпарсений вердикт
|
|
77
|
+
* @param {{replacement?: string}} mutant мутант
|
|
78
|
+
* @returns {object} extra
|
|
79
|
+
*/
|
|
80
|
+
function verdictExtra(verdict, mutant) {
|
|
81
|
+
return {
|
|
82
|
+
verdict: verdict.verdict,
|
|
83
|
+
confidence: verdict.confidence,
|
|
84
|
+
replacement: String(mutant.replacement ?? '').slice(0, 120)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
63
88
|
/**
|
|
64
89
|
* Класифікує survived мутантів через pi (N_LOCAL_MIN_MODEL → N_CLOUD_MIN_MODEL → fallback).
|
|
65
90
|
* @param {Array<{file: string, mutants: object[], exampleTest?: object|null, recommendationText?: string|null}>} survived
|
|
66
91
|
* @param {string} cwd
|
|
67
|
-
* @param {{cachePath?: string, callModel?: (prompt: string, model: string, cwd: string) => Promise<string>,
|
|
68
|
-
* tier1?: string, tier2?: string}} [opts] `tier1`/`tier2` — явні model-specs (дефолт: LOCAL_MIN/CLOUD_MIN пакета;
|
|
69
|
-
* інжектовні, бо тир-константи фіксуються при імпорті й у тестах не стабляться через env)
|
|
92
|
+
* @param {{cachePath?: string, callModel?: (prompt: string, model: string, cwd: string, callOpts?: {chain?: object}) => Promise<string>,
|
|
93
|
+
* tier1?: string, tier2?: string, startChain?: typeof startChain}} [opts] `tier1`/`tier2` — явні model-specs (дефолт: LOCAL_MIN/CLOUD_MIN пакета;
|
|
94
|
+
* інжектовні, бо тир-константи фіксуються при імпорті й у тестах не стабляться через env); `startChain` — фабрика ланцюжка (інжект для тестів)
|
|
70
95
|
* @returns {Promise<Array<{key: string, verdict: object}>>}
|
|
71
96
|
*/
|
|
72
97
|
export async function classify(survived, cwd, opts = {}) {
|
|
73
98
|
const cachePath = opts.cachePath ?? join(cwd, 'npm/reports/coverage-classify.cache.json')
|
|
74
99
|
const callModelFn = opts.callModel ?? callModel
|
|
100
|
+
const makeChain = opts.startChain ?? startChain
|
|
75
101
|
const tier1 = opts.tier1 ?? LOCAL_MIN
|
|
76
102
|
const tier2 = opts.tier2 ?? CLOUD_MIN
|
|
77
103
|
const cacheModel = `${tier1 || 'default'}+${tier2 || 'cloud'}`
|
|
@@ -99,7 +125,7 @@ export async function classify(survived, cwd, opts = {}) {
|
|
|
99
125
|
}
|
|
100
126
|
}
|
|
101
127
|
if (!verdict) {
|
|
102
|
-
verdict = await classifyOne(group, mutant, cwd, callModelFn, tier1, tier2)
|
|
128
|
+
verdict = await classifyOne(group, mutant, cwd, callModelFn, tier1, tier2, makeChain)
|
|
103
129
|
if (cacheKey) {
|
|
104
130
|
cache.entries[cacheKey] = { ...verdict, classifiedAt: new Date().toISOString() }
|
|
105
131
|
}
|
package/src/coverage-fix.mjs
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { readFile } from 'node:fs/promises'
|
|
10
10
|
import { join } from 'node:path'
|
|
11
11
|
import { env } from 'node:process'
|
|
12
|
-
import { CLOUD_MAX } from '@
|
|
12
|
+
import { CLOUD_MAX } from '@7n/llm-lib/model-tiers'
|
|
13
13
|
import { callAgent } from './lib/llm.mjs'
|
|
14
14
|
|
|
15
15
|
const MODEL = env.N_CURSOR_COVERAGE_FIX_MODEL ?? CLOUD_MAX
|
|
@@ -41,7 +41,7 @@ export async function fixSurvivedMutants(survived, projectRoot, opts = {}) {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
|
-
* Викликає агента через
|
|
44
|
+
* Викликає агента через `@7n/llm-lib` (SDK-embed, live-output до stdout) —
|
|
45
45
|
* заміна колишнього spawnSync pi CLI.
|
|
46
46
|
* @param {string} prompt текст промпта
|
|
47
47
|
* @param {string} model provider/model-id або '' для pi-дефолту
|
package/src/docs/assess-need.md
CHANGED
package/src/docs/coverage-fix.md
CHANGED
package/src/docs/fix-tests.md
CHANGED
package/src/docs/gen-tests.md
CHANGED
package/src/fix-tests.mjs
CHANGED
|
@@ -20,8 +20,9 @@ import { tmpdir } from 'node:os'
|
|
|
20
20
|
import { join, relative } from 'node:path'
|
|
21
21
|
import { env } from 'node:process'
|
|
22
22
|
import { callText, MEMORY_ERROR_RE } from './lib/llm.mjs'
|
|
23
|
-
import { CLOUD_MAX } from '@
|
|
24
|
-
import {
|
|
23
|
+
import { CLOUD_MAX } from '@7n/llm-lib/model-tiers'
|
|
24
|
+
import { startChain } from '@7n/llm-lib/chain'
|
|
25
|
+
import { budgetFor, capText, packBatch } from '@7n/llm-lib/prompt-budget'
|
|
25
26
|
import { findTestRules } from './gen-tests.mjs'
|
|
26
27
|
import { parseFailingTests } from './coverage-per-file.mjs'
|
|
27
28
|
import { resolveVitestRun } from './lib/vitest-shim.mjs'
|
|
@@ -322,17 +323,28 @@ function writeFixedFiles(fixed, remaining, dir) {
|
|
|
322
323
|
* @param {string} dir project root
|
|
323
324
|
* @param {{
|
|
324
325
|
* failures?: Array<{file: string, errors: string[]}>,
|
|
325
|
-
* callTextFn?: (prompt: string, opts?: object) => Promise<string
|
|
326
|
-
*
|
|
326
|
+
* callTextFn?: (prompt: string, opts?: object) => Promise<string>,
|
|
327
|
+
* startChain?: typeof startChain
|
|
328
|
+
* }} [opts] overrides for callText, pre-fetched failures і фабрика ланцюжка (інжект для тестів)
|
|
327
329
|
* @returns {Promise<{count: number, fixed: number, remaining: number}>} fix result summary
|
|
328
330
|
*/
|
|
329
331
|
export async function fixFailingTests(dir, opts = {}) {
|
|
330
|
-
const callTextFn =
|
|
331
|
-
opts.callTextFn ?? (prompt => callText(prompt, { model: MODEL, cwd: dir, maxTokens: budgetFor('fix').maxTokens }))
|
|
332
332
|
const failures = opts.failures ?? (await getFailingTests(dir))
|
|
333
333
|
|
|
334
334
|
if (failures.length === 0) return { count: 0, fixed: 0, remaining: 0 }
|
|
335
335
|
|
|
336
|
+
// Один ланцюжок на прогін: батчі змішують файли, per-file chain неможливий
|
|
337
|
+
// без ламання батчингу; кожен batch-виклик = крок.
|
|
338
|
+
const chain = (opts.startChain ?? startChain)({
|
|
339
|
+
kind: 'test-fix',
|
|
340
|
+
unit: `fix:${failures.length}files`,
|
|
341
|
+
cwd: dir
|
|
342
|
+
})
|
|
343
|
+
let batches = 0
|
|
344
|
+
const callTextFn =
|
|
345
|
+
opts.callTextFn ??
|
|
346
|
+
(prompt => callText(prompt, { model: MODEL, cwd: dir, maxTokens: budgetFor('fix').maxTokens, chain }))
|
|
347
|
+
|
|
336
348
|
console.log(`\n🔧 Виправляю ${failures.length} падаючих test-файлів (pi text mode)...\n`)
|
|
337
349
|
for (const f of failures) {
|
|
338
350
|
console.log(` • ${f.file} (${f.errors.length} помилок)`)
|
|
@@ -342,50 +354,64 @@ export async function fixFailingTests(dir, opts = {}) {
|
|
|
342
354
|
let remaining = failures
|
|
343
355
|
const attempts = new Map()
|
|
344
356
|
let prevDeferred = new Set()
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
357
|
+
try {
|
|
358
|
+
for (;;) {
|
|
359
|
+
// Спроби рахуються per-file (лише коли файл реально був у батчі),
|
|
360
|
+
// тож deferred-черга не з'їдає ліміт файлів, які ще не пробували
|
|
361
|
+
const eligible = remaining
|
|
362
|
+
.filter(f => (attempts.get(f.file) ?? 0) < MAX_FIX_ATTEMPTS)
|
|
363
|
+
// Анти-starvation: відкладені минулого разу файли йдуть першими
|
|
364
|
+
.toSorted((a, b) => (prevDeferred.has(b.file) ? 1 : 0) - (prevDeferred.has(a.file) ? 1 : 0))
|
|
365
|
+
if (eligible.length === 0) break
|
|
366
|
+
|
|
367
|
+
const batch = buildFixTestsBatch(eligible, dir)
|
|
368
|
+
if (batch.deferred.length) {
|
|
369
|
+
console.log(
|
|
370
|
+
` 📦 батч: ${batch.included.length} файлів, відкладено на наступний прохід: ${batch.deferred.length}`
|
|
371
|
+
)
|
|
372
|
+
}
|
|
373
|
+
for (const file of batch.included) attempts.set(file, (attempts.get(file) ?? 0) + 1)
|
|
374
|
+
prevDeferred = new Set(batch.deferred)
|
|
375
|
+
|
|
376
|
+
let response
|
|
377
|
+
try {
|
|
378
|
+
batches++
|
|
379
|
+
response = await callTextFn(batch.prompt)
|
|
380
|
+
} catch (error) {
|
|
381
|
+
// memory-guard: не звичайна per-file помилка — RAM-стеля фіксована, продовжувати
|
|
382
|
+
// до наступного файлу немає сенсу. Пробиваємо нагору до CLI, аби процес завершився.
|
|
383
|
+
if (MEMORY_ERROR_RE.test(error.message ?? '')) throw error
|
|
384
|
+
console.error(` ✗ pi помилка: ${error.message}`)
|
|
385
|
+
break
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const fixed = parseFixedFiles(response)
|
|
389
|
+
|
|
390
|
+
if (fixed.length === 0) {
|
|
391
|
+
console.error(' ✗ pi не повернула виправлений код')
|
|
392
|
+
break
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const includedSet = new Set(batch.included)
|
|
396
|
+
writeFixedFiles(
|
|
397
|
+
fixed,
|
|
398
|
+
eligible.filter(f => includedSet.has(f.file)),
|
|
399
|
+
dir
|
|
358
400
|
)
|
|
359
|
-
}
|
|
360
|
-
for (const file of batch.included) attempts.set(file, (attempts.get(file) ?? 0) + 1)
|
|
361
|
-
prevDeferred = new Set(batch.deferred)
|
|
362
401
|
|
|
363
|
-
|
|
364
|
-
try {
|
|
365
|
-
response = await callTextFn(batch.prompt)
|
|
366
|
-
} catch (error) {
|
|
367
|
-
// memory-guard: не звичайна per-file помилка — RAM-стеля фіксована, продовжувати
|
|
368
|
-
// до наступного файлу немає сенсу. Пробиваємо нагору до CLI, аби процес завершився.
|
|
369
|
-
if (MEMORY_ERROR_RE.test(error.message ?? '')) throw error
|
|
370
|
-
console.error(` ✗ pi помилка: ${error.message}`)
|
|
371
|
-
break
|
|
402
|
+
remaining = await getFailingTests(dir)
|
|
372
403
|
}
|
|
373
|
-
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
eligible.filter(f => includedSet.has(f.file)),
|
|
385
|
-
dir
|
|
386
|
-
)
|
|
387
|
-
|
|
388
|
-
remaining = await getFailingTests(dir)
|
|
404
|
+
} finally {
|
|
405
|
+
const fixedSoFar = failures.length - remaining.length
|
|
406
|
+
chain.end({
|
|
407
|
+
outcome: remaining.length === 0 ? 'success' : fixedSoFar > 0 ? 'partial' : 'fail',
|
|
408
|
+
extra: {
|
|
409
|
+
files: failures.map(f => f.file),
|
|
410
|
+
batches,
|
|
411
|
+
fixed: fixedSoFar,
|
|
412
|
+
remaining: remaining.length
|
|
413
|
+
}
|
|
414
|
+
})
|
|
389
415
|
}
|
|
390
416
|
|
|
391
417
|
const fixedCount = failures.length - remaining.length
|
package/src/gen-tests.mjs
CHANGED
|
@@ -18,7 +18,8 @@ import { spawnSync } from 'node:child_process'
|
|
|
18
18
|
import { join, relative, dirname } from 'node:path'
|
|
19
19
|
import { callText, MEMORY_ERROR_RE } from './lib/llm.mjs'
|
|
20
20
|
import { env } from 'node:process'
|
|
21
|
-
import { budgetFor } from '@
|
|
21
|
+
import { budgetFor } from '@7n/llm-lib/prompt-budget'
|
|
22
|
+
import { startChain } from '@7n/llm-lib/chain'
|
|
22
23
|
import { resolveVitestRun } from './lib/vitest-shim.mjs'
|
|
23
24
|
import { extractExportsWithComplexity } from './classify-exports.mjs'
|
|
24
25
|
import { analyzeModule } from './lib/ast-analyze.mjs'
|
|
@@ -1149,9 +1150,10 @@ function resolveLocalModel(opts) {
|
|
|
1149
1150
|
* @param {PiCallFn} callTextFn cloud LLM caller
|
|
1150
1151
|
* @param {PiCallFn | null} localFn local LLM caller
|
|
1151
1152
|
* @param {GenerateOneFn | undefined} generateOne custom single-file generator
|
|
1153
|
+
* @param {typeof startChain} [makeChain] фабрика ланцюжка (інжект для тестів)
|
|
1152
1154
|
* @returns {Promise<void>} resolves after generation for this file completes
|
|
1153
1155
|
*/
|
|
1154
|
-
async function generateTestsForFile(fileInfo, dir, callTextFn, localFn, generateOne) {
|
|
1156
|
+
async function generateTestsForFile(fileInfo, dir, callTextFn, localFn, generateOne, makeChain = startChain) {
|
|
1155
1157
|
console.log(` → ${fileInfo.file} (${fileInfo.pct.toFixed(1)}%)`)
|
|
1156
1158
|
|
|
1157
1159
|
if (generateOne) {
|
|
@@ -1159,13 +1161,26 @@ async function generateTestsForFile(fileInfo, dir, callTextFn, localFn, generate
|
|
|
1159
1161
|
return
|
|
1160
1162
|
}
|
|
1161
1163
|
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
}
|
|
1164
|
+
// Ланцюжок файлу: усі виклики (header, per-export local/cloud спроби,
|
|
1165
|
+
// vitest-retry, length-retry) — кроки одного chain.
|
|
1166
|
+
const chain = makeChain({ kind: 'test-generate', unit: fileInfo.file, cwd: dir })
|
|
1167
|
+
const chainedCloud = (prompt, callOpts = {}) => callTextFn(prompt, { ...callOpts, chain })
|
|
1168
|
+
const chainedLocal = localFn ? (prompt, callOpts = {}) => localFn(prompt, { ...callOpts, chain }) : null
|
|
1169
|
+
let failed = null
|
|
1170
|
+
try {
|
|
1171
|
+
const exportsInfo = extractExportsWithComplexity(readSourceSnippet(join(dir, fileInfo.file)))
|
|
1172
|
+
if (chainedLocal && exportsInfo.length > 0) {
|
|
1173
|
+
await generatePerExport(fileInfo, dir, chainedCloud, chainedLocal)
|
|
1174
|
+
return
|
|
1175
|
+
}
|
|
1167
1176
|
|
|
1168
|
-
|
|
1177
|
+
await generateOneTest(fileInfo, dir, chainedCloud)
|
|
1178
|
+
} catch (error) {
|
|
1179
|
+
failed = String(error.message ?? error).slice(0, 200)
|
|
1180
|
+
throw error
|
|
1181
|
+
} finally {
|
|
1182
|
+
chain.end({ outcome: failed ? 'fail' : 'success', extra: failed ? { error: failed } : {} })
|
|
1183
|
+
}
|
|
1169
1184
|
}
|
|
1170
1185
|
|
|
1171
1186
|
/**
|
package/src/index.js
CHANGED
|
@@ -11,9 +11,22 @@ export async function run(args) {
|
|
|
11
11
|
console.log(' Runs coverage analysis, generates missing tests, then mutation testing.')
|
|
12
12
|
console.log(' --no-mutation Skip mutation testing phase.')
|
|
13
13
|
console.log(' Defaults to current directory when no argument is given.')
|
|
14
|
+
console.log('Usage: n coverage [--fix] [--changed]')
|
|
15
|
+
console.log(' Coverage + mutation testing only (no test generation) — writes COVERAGE.md.')
|
|
16
|
+
console.log(' --fix Agent fixes survived mutants, then re-runs coverage.')
|
|
17
|
+
console.log(' --changed Scope to files changed vs merge-base with main — no COVERAGE.md write.')
|
|
14
18
|
return 0
|
|
15
19
|
}
|
|
16
20
|
|
|
21
|
+
if (first === 'coverage') {
|
|
22
|
+
const { runCoverageCli } = await import('./coverage/coverage.mjs')
|
|
23
|
+
return runCoverageCli({
|
|
24
|
+
cwd: getCwd(),
|
|
25
|
+
fix: flags.includes('--fix'),
|
|
26
|
+
changed: flags.includes('--changed')
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
17
30
|
const dir = first ? resolve(first) : getCwd()
|
|
18
31
|
const noMutation = flags.includes('--no-mutation')
|
|
19
32
|
const { runAutoTest } = await import('./run.mjs')
|
package/src/lib/docs/llm.md
CHANGED
|
@@ -3,12 +3,12 @@ type: JS Module
|
|
|
3
3
|
title: llm.mjs
|
|
4
4
|
resource: npm/src/lib/llm.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: b126a1aa
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
## Огляд
|
|
10
10
|
|
|
11
|
-
Тонкий адаптер @7n/test над пакетом `@
|
|
11
|
+
Тонкий адаптер @7n/test над пакетом `@7n/llm-lib` (Ф3 спеки llm-lib-extraction у репо cursor): зберігає звичний контракт `callText`/`callAgent` для внутрішніх колерів (gen-tests, fix-tests, assess-need, coverage-classify, coverage-fix), а транспорт, registry і трасування повністю живуть у пакеті. Замінив колишній `pi-client.mjs` з власним SDK-плюмбінгом, retry/backoff і прямим omlx-HTTP.
|
|
12
12
|
|
|
13
13
|
## Поведінка
|
|
14
14
|
|
package/src/lib/llm.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Тонкий адаптер
|
|
2
|
+
* Тонкий адаптер `@7n/test` над `@7n/llm-lib` (Ф3 спеки llm-lib-extraction):
|
|
3
3
|
* зберігає звичний контракт `callText`/`callAgent` для внутрішніх колерів,
|
|
4
4
|
* але транспорт/registry/trace повністю живуть у пакеті.
|
|
5
5
|
*
|
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
* подвоєння `maxTokens` на обрізаній відповіді (`stopReason: 'length'`):
|
|
11
11
|
* це семантичний повтор без пауз, а не очікування зайнятого сервера.
|
|
12
12
|
*/
|
|
13
|
-
import {
|
|
14
|
-
import { runAgentSkill } from '@
|
|
13
|
+
import { runOneShot } from '@7n/llm-lib/one-shot'
|
|
14
|
+
import { runAgentSkill } from '@7n/llm-lib/agent-skill'
|
|
15
|
+
|
|
15
16
|
|
|
16
|
-
export { MEMORY_ERROR_RE }
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* Стеля відповіді моделі для подвоєння на `stopReason: 'length'` — межа
|
|
@@ -33,6 +33,7 @@ const AGENT_TIMEOUT_MS = 900_000
|
|
|
33
33
|
* @param {string} [opts.model] provider/model-id (напр. "openai/gpt-5.5"); без значення — default pi
|
|
34
34
|
* @param {number} [opts.maxTokens] стеля відповіді для цього виклику; на
|
|
35
35
|
* `stopReason: 'length'` виклик повторюється один раз із подвоєною стелею
|
|
36
|
+
* @param {object} [opts.chain] chain handle (`@7n/llm-lib/chain`) — виклик стає кроком ланцюжка
|
|
36
37
|
* @param {object} [opts.deps] інжекти для тестів (прокидаються у runOneShot)
|
|
37
38
|
* @returns {Promise<string>} текстова відповідь моделі
|
|
38
39
|
*/
|
|
@@ -44,6 +45,7 @@ export async function callText(prompt, opts = {}) {
|
|
|
44
45
|
timeoutMs: 0,
|
|
45
46
|
cwd: opts.cwd,
|
|
46
47
|
caller: '7n-test:text',
|
|
48
|
+
chain: opts.chain ?? null,
|
|
47
49
|
deps: opts.deps
|
|
48
50
|
})
|
|
49
51
|
if (r.error) throw new Error(r.error)
|
|
@@ -66,6 +68,7 @@ export async function callText(prompt, opts = {}) {
|
|
|
66
68
|
* @param {string} cwd робоча директорія, куди агент може писати файли
|
|
67
69
|
* @param {object} [opts] додаткові параметри
|
|
68
70
|
* @param {string} [opts.model] provider/model-id або '' для pi-дефолту
|
|
71
|
+
* @param {object} [opts.chain] chain handle (`@7n/llm-lib/chain`) — виклик стає кроком ланцюжка
|
|
69
72
|
* @param {object} [opts.deps] інжекти для тестів (прокидаються у runAgentSkill)
|
|
70
73
|
* @returns {Promise<void>} проміс завершується після виконання агента
|
|
71
74
|
*/
|
|
@@ -77,7 +80,10 @@ export async function callAgent(prompt, cwd, opts = {}) {
|
|
|
77
80
|
timeoutMs: AGENT_TIMEOUT_MS,
|
|
78
81
|
maxTokens: 0, // без стелі: агент пише цілі тест-файли (паритет зі старим CLI-шляхом)
|
|
79
82
|
caller: 'agent:7n-test',
|
|
83
|
+
chain: opts.chain ?? null,
|
|
80
84
|
deps: opts.deps
|
|
81
85
|
})
|
|
82
86
|
if (r.error) throw new Error(r.error)
|
|
83
87
|
}
|
|
88
|
+
|
|
89
|
+
export {MEMORY_ERROR_RE} from '@7n/llm-lib/one-shot'
|