@7n/test 0.9.0 → 0.10.1
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 +80 -28
- package/src/lib/runtime-probe.mjs +390 -0
- package/src/lib/vitest-shim.mjs +73 -0
- package/src/run.mjs +3 -1
- 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/src/fix-tests.mjs
CHANGED
|
@@ -1,157 +1,303 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* fix-tests: виявляє падаючі unit-тести і
|
|
2
|
+
* fix-tests: виявляє падаючі unit-тести і виправляє їх через pi SDK (text mode).
|
|
3
3
|
*
|
|
4
4
|
* Порядок роботи:
|
|
5
5
|
* 1. getFailingTests(dir) — запускає vitest run --reporter=json і повертає
|
|
6
6
|
* список падаючих файлів з повідомленнями про помилки.
|
|
7
|
-
* 2. fixFailingTests(dir, opts) —
|
|
8
|
-
*
|
|
7
|
+
* 2. fixFailingTests(dir, opts) — для кожного падаючого файлу:
|
|
8
|
+
* a. Читає поточний вміст тест-файлу та source-файлу
|
|
9
|
+
* b. callText → отримує виправлений код у ```js блоці
|
|
10
|
+
* c. writeFileSync → записує файл напряму (без Edit інструментів агента)
|
|
11
|
+
* d. Повторює до MAX_FIX_ATTEMPTS якщо ще залишились падіння
|
|
12
|
+
*
|
|
13
|
+
* Перехід з callAgent (Edit-інструменти) на callText+write усуває проблему
|
|
14
|
+
* точного матчу рядків що містять URL, backtick-рядки та спецсимволи.
|
|
9
15
|
*/
|
|
10
16
|
import { spawnSync } from 'node:child_process'
|
|
11
|
-
import { existsSync, readFileSync } from 'node:fs'
|
|
17
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
12
18
|
import { mkdtemp, rm } from 'node:fs/promises'
|
|
13
|
-
import { createRequire } from 'node:module'
|
|
14
19
|
import { tmpdir } from 'node:os'
|
|
15
|
-
import { join, relative
|
|
20
|
+
import { join, relative } from 'node:path'
|
|
16
21
|
import { env } from 'node:process'
|
|
22
|
+
import { callText } from './lib/pi-client.mjs'
|
|
23
|
+
import { findTestRules } from './gen-tests.mjs'
|
|
24
|
+
import { parseFailingTests } from './coverage-per-file.mjs'
|
|
25
|
+
import { resolveVitestRun } from './lib/vitest-shim.mjs'
|
|
17
26
|
|
|
18
|
-
const
|
|
19
|
-
const
|
|
27
|
+
const MODEL = env.N_CURSOR_FIX_TESTS_MODEL ?? env.N_CLOUD_MAX_MODEL ?? undefined
|
|
28
|
+
const MAX_SRC_BYTES = 4000
|
|
29
|
+
const TEST_DIR_MARKERS = ['/tests/', '\\tests\\']
|
|
30
|
+
const TEST_FILE_SUFFIX = '.test.mjs'
|
|
31
|
+
const SOURCE_FILE_SUFFIX = '.js'
|
|
32
|
+
const FILE_MARKER_PREFIX = '<!--'
|
|
33
|
+
const FILE_MARKER_SUFFIX = '-->'
|
|
34
|
+
const FILE_MARKER_LABEL = 'file:'
|
|
35
|
+
const CODE_FENCE_START_RE = /^```(?:js|javascript|mjs|ts)?$/
|
|
36
|
+
const CODE_FENCE_END = '```'
|
|
20
37
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Infers a source file path from a conventional `tests/<name>.test.mjs` path.
|
|
40
|
+
* @param {string} absPath absolute test file path
|
|
41
|
+
* @returns {string|null} inferred source path or null when convention does not match
|
|
42
|
+
*/
|
|
43
|
+
function inferSourcePath(absPath) {
|
|
44
|
+
const marker = TEST_DIR_MARKERS.find(item => absPath.includes(item))
|
|
45
|
+
if (!marker || !absPath.endsWith(TEST_FILE_SUFFIX)) return null
|
|
46
|
+
const markerAt = absPath.lastIndexOf(marker)
|
|
47
|
+
const dir = absPath.slice(0, markerAt)
|
|
48
|
+
const stem = absPath.slice(markerAt + marker.length, -TEST_FILE_SUFFIX.length)
|
|
49
|
+
if (!stem || stem.includes('/') || stem.includes('\\')) return null
|
|
50
|
+
return `${dir}${marker[0]}${stem}${SOURCE_FILE_SUFFIX}`
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Parses an HTML file marker from a single LLM response line.
|
|
55
|
+
* @param {string} line response line
|
|
56
|
+
* @returns {string|null} file path from marker or null
|
|
57
|
+
*/
|
|
58
|
+
function parseFileMarker(line) {
|
|
59
|
+
const trimmed = line.trim()
|
|
60
|
+
if (!trimmed.startsWith(FILE_MARKER_PREFIX) || !trimmed.endsWith(FILE_MARKER_SUFFIX)) return null
|
|
61
|
+
const inner = trimmed.slice(FILE_MARKER_PREFIX.length, -FILE_MARKER_SUFFIX.length).trim()
|
|
62
|
+
if (!inner.startsWith(FILE_MARKER_LABEL)) return null
|
|
63
|
+
const file = inner.slice(FILE_MARKER_LABEL.length).trim()
|
|
64
|
+
return file || null
|
|
65
|
+
}
|
|
24
66
|
|
|
25
67
|
/**
|
|
26
68
|
* Runs vitest in JSON mode and returns failing test files with error messages.
|
|
27
69
|
* @param {string} dir project root
|
|
28
|
-
* @returns {Promise<Array<{file: string, errors: string[]}>>}
|
|
70
|
+
* @returns {Promise<Array<{file: string, errors: string[]}>>} failing test files with error messages
|
|
29
71
|
*/
|
|
30
72
|
export async function getFailingTests(dir) {
|
|
31
73
|
const tmpDir = await mkdtemp(join(tmpdir(), '7n-fix-'))
|
|
32
74
|
const outputFile = join(tmpDir, 'results.json')
|
|
33
75
|
|
|
76
|
+
const { bin, configArgs } = resolveVitestRun(dir)
|
|
34
77
|
try {
|
|
35
78
|
spawnSync(
|
|
36
79
|
process.execPath,
|
|
37
|
-
[
|
|
38
|
-
{
|
|
39
|
-
cwd: dir,
|
|
40
|
-
stdio: 'inherit',
|
|
41
|
-
env
|
|
42
|
-
}
|
|
80
|
+
[bin, 'run', ...configArgs, '--root', dir, '--reporter=json', `--outputFile=${outputFile}`, '--passWithNoTests'],
|
|
81
|
+
{ cwd: dir, stdio: 'inherit', env }
|
|
43
82
|
)
|
|
44
83
|
|
|
45
84
|
if (!existsSync(outputFile)) return []
|
|
46
85
|
|
|
47
|
-
|
|
86
|
+
return parseFailingTests(outputFile, dir)
|
|
87
|
+
} finally {
|
|
48
88
|
try {
|
|
49
|
-
|
|
89
|
+
await rm(tmpDir, { recursive: true, force: true })
|
|
50
90
|
} catch {
|
|
51
|
-
|
|
91
|
+
/* ignore cleanup errors */
|
|
52
92
|
}
|
|
53
|
-
|
|
54
|
-
return (data.testResults ?? [])
|
|
55
|
-
.filter(r => r.status === 'failed')
|
|
56
|
-
.map(r => {
|
|
57
|
-
const assertionErrors = (r.assertionResults ?? [])
|
|
58
|
-
.filter(a => a.status === 'failed')
|
|
59
|
-
.slice(0, MAX_ERRORS_PER_FILE)
|
|
60
|
-
.map(a => {
|
|
61
|
-
const name = [...(a.ancestorTitles ?? []), a.title].join(' > ')
|
|
62
|
-
const msg = (a.failureMessages?.[0] ?? '').split('\n').slice(0, MAX_ERROR_LINES).join('\n')
|
|
63
|
-
return `${name}:\n${msg}`
|
|
64
|
-
})
|
|
65
|
-
const errors = assertionErrors.length > 0
|
|
66
|
-
? assertionErrors
|
|
67
|
-
: [`Suite error: ${(r.message ?? r.failureMessage ?? 'module-level failure').split('\n').slice(0, MAX_ERROR_LINES).join('\n')}`]
|
|
68
|
-
return { file: relative(dir, r.testFilePath ?? r.name), errors }
|
|
69
|
-
})
|
|
70
|
-
.filter(f => !f.file.startsWith('..'))
|
|
71
|
-
} finally {
|
|
72
|
-
await rm(tmpDir, { recursive: true, force: true }).catch(() => {})
|
|
73
93
|
}
|
|
74
94
|
}
|
|
75
95
|
|
|
76
96
|
/**
|
|
77
|
-
*
|
|
78
|
-
* @param {
|
|
79
|
-
* @returns {string}
|
|
97
|
+
* Extracts the first fenced JavaScript block from LLM text output.
|
|
98
|
+
* @param {string} text LLM response text.
|
|
99
|
+
* @returns {string} Extracted code or an empty string.
|
|
80
100
|
*/
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
101
|
+
function extractCode(text) {
|
|
102
|
+
const start = text.indexOf('```')
|
|
103
|
+
if (start === -1) return ''
|
|
104
|
+
const bodyStart = text.indexOf('\n', start)
|
|
105
|
+
if (bodyStart === -1) return ''
|
|
106
|
+
const end = text.indexOf('\n```', bodyStart + 1)
|
|
107
|
+
if (end === -1) return ''
|
|
108
|
+
return text.slice(bodyStart + 1, end).trim()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Builds a prompt to fix one failing test file.
|
|
113
|
+
* Includes current file content so the LLM doesn't need file-read tools.
|
|
114
|
+
* @param {Array<{file: string, errors: string[]}>} failures failing test files
|
|
115
|
+
* @param {string} [dir] project root
|
|
116
|
+
* @returns {string} prompt text for the LLM
|
|
117
|
+
*/
|
|
118
|
+
export function buildFixTestsPrompt(failures, dir) {
|
|
119
|
+
const testRules = dir ? findTestRules(dir) : null
|
|
120
|
+
|
|
121
|
+
const sections = failures.map(({ file, errors }) => {
|
|
122
|
+
const absPath = dir ? join(dir, file) : file
|
|
123
|
+
const testCode = existsSync(absPath) ? readFileSync(absPath, 'utf8').slice(0, 6000) : '(файл не знайдено)'
|
|
124
|
+
|
|
125
|
+
// Heuristically find source file: strip tests/ prefix from path
|
|
126
|
+
const sourcePath = inferSourcePath(absPath)
|
|
127
|
+
const sourceCode =
|
|
128
|
+
sourcePath && existsSync(sourcePath) ? readFileSync(sourcePath, 'utf8').slice(0, MAX_SRC_BYTES) : null
|
|
129
|
+
|
|
130
|
+
const errBlock = errors.join('\n\n')
|
|
131
|
+
|
|
132
|
+
return [
|
|
133
|
+
`### \`${file}\``,
|
|
134
|
+
'',
|
|
135
|
+
'**Помилки:**',
|
|
136
|
+
'```',
|
|
137
|
+
errBlock,
|
|
138
|
+
'```',
|
|
139
|
+
'',
|
|
140
|
+
'**Поточний тест-файл:**',
|
|
141
|
+
'```js',
|
|
142
|
+
testCode,
|
|
143
|
+
'```',
|
|
144
|
+
...(sourceCode ? ['', '**Source (для довідки, не міняй):**', '```js', sourceCode, '```'] : [])
|
|
145
|
+
].join('\n')
|
|
146
|
+
})
|
|
88
147
|
|
|
89
148
|
return [
|
|
90
|
-
'Виправ падаючі unit-тести.
|
|
91
|
-
'1. Прочитай тест-файл і source-файл що він тестує',
|
|
92
|
-
'2. Запусти: `bunx vitest run <testFile>` — переконайся в точній помилці',
|
|
93
|
-
'3. Виправ ЛИШЕ тест-файл — source-файли не чіпай',
|
|
94
|
-
'4. Запусти тест ще раз — переконайся що зелений',
|
|
149
|
+
'Виправ падаючі unit-тести. Поверни ПОВНИЙ вміст КОЖНОГО виправленого тест-файлу.',
|
|
95
150
|
'',
|
|
96
151
|
'## Правила:',
|
|
97
|
-
'- Міняй виключно тест-файли',
|
|
98
|
-
'- Зберігай структуру тестів (describe/it/expect)',
|
|
152
|
+
'- Міняй виключно тест-файли — source-файли не чіпай',
|
|
153
|
+
'- Зберігай структуру тестів (describe/it/expect) — НЕ видаляй тести',
|
|
99
154
|
'- Якщо тест перевіряє неіснуючий API — адаптуй до реального',
|
|
100
|
-
'- НЕ
|
|
101
|
-
'-
|
|
155
|
+
'- Файл .mjs = чистий JavaScript, НЕ TypeScript. НІКОЛИ: `as Type`, generics',
|
|
156
|
+
'- `import { vi, describe, it, expect, beforeEach, afterEach, beforeAll, afterAll } from "vitest"` — імпортуй ВСЕ що використовуєш',
|
|
157
|
+
'- Замість `fn as vi.Mock` → `vi.mocked(fn)`',
|
|
158
|
+
'- `vi.spyOn(process, "env")` НЕ ПРАЦЮЄ — use `vi.stubEnv("KEY", "val")` + `afterEach(() => vi.unstubAllEnvs())`',
|
|
159
|
+
'- `vi.spyOn(Date).mockReturnValue(...)` НЕ ПРАЦЮЄ з `new Date()` — use `vi.useFakeTimers()` + `vi.setSystemTime(new Date(...))` + `afterEach(() => vi.useRealTimers())`',
|
|
160
|
+
"- `sendMessage` викликає fetch з одним аргументом (URL-рядком) — перевіряй через `expect.stringContaining(...)`, НЕ через об'єктний matcher",
|
|
161
|
+
'- `describe()` callback НЕ може бути async. `await` тільки у: top-level async IIFE, `beforeAll(async () => {})`, або `it(async () => {})`',
|
|
162
|
+
"- `vi.mock` hoisting: фабрика виконується до будь-якого `const`/`let` у модулі. Якщо потрібен спільний mock-об'єкт — оголошуй його через `vi.hoisted()`: `const { mockFn } = vi.hoisted(() => ({ mockFn: vi.fn() }))`; потім використовуй в factory та тестах",
|
|
163
|
+
'- Якщо AssertionError показує `Expected: "A" Received: "B"` — функція реально повертає B. ВИПРАВ expected на точне значення з рядка "Received:", не переосмислюй логіку функції',
|
|
164
|
+
'- Для regex/escape функцій: тестуй по одному символу (`expect(esc("*")).toBe("\\\\*")`), НЕ комбінований рядок — легко помилитись в подвійному екрануванні',
|
|
165
|
+
...(testRules ? ['', '## Конвенції тестів цього проєкту (.cursor/rules/n-test.mdc):', testRules] : []),
|
|
166
|
+
'',
|
|
167
|
+
'## Падаючі файли:',
|
|
102
168
|
'',
|
|
103
|
-
|
|
169
|
+
...sections,
|
|
104
170
|
'',
|
|
105
|
-
|
|
171
|
+
'Для кожного файлу поверни його ПОВНИЙ вміст у блоці:',
|
|
172
|
+
'<!-- file: <шлях до файлу> -->',
|
|
173
|
+
'```js',
|
|
174
|
+
'... повний вміст ...',
|
|
175
|
+
'```'
|
|
106
176
|
].join('\n')
|
|
107
177
|
}
|
|
108
178
|
|
|
109
179
|
/**
|
|
110
|
-
*
|
|
111
|
-
* @param {string}
|
|
112
|
-
* @
|
|
113
|
-
* @param {string} cwd
|
|
180
|
+
* Parses fenced JavaScript blocks with file markers from LLM response.
|
|
181
|
+
* @param {string} text LLM response text.
|
|
182
|
+
* @returns {Array<{file: string|null, code: string}>} Parsed file/code pairs.
|
|
114
183
|
*/
|
|
115
|
-
function
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
184
|
+
function parseFixedFiles(text) {
|
|
185
|
+
const results = []
|
|
186
|
+
const lines = text.split('\n')
|
|
187
|
+
|
|
188
|
+
let lineIndex = 0
|
|
189
|
+
while (lineIndex < lines.length) {
|
|
190
|
+
const file = parseFileMarker(lines[lineIndex])
|
|
191
|
+
if (!file) {
|
|
192
|
+
lineIndex++
|
|
193
|
+
continue
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const fenceStart = lines.findIndex((line, index) => index > lineIndex && CODE_FENCE_START_RE.test(line.trim()))
|
|
197
|
+
if (fenceStart === -1) break
|
|
198
|
+
|
|
199
|
+
const fenceEnd = lines.findIndex((line, index) => index > fenceStart && line.trim() === CODE_FENCE_END)
|
|
200
|
+
if (fenceEnd === -1) break
|
|
201
|
+
|
|
202
|
+
const code = lines
|
|
203
|
+
.slice(fenceStart + 1, fenceEnd)
|
|
204
|
+
.join('\n')
|
|
205
|
+
.trim()
|
|
206
|
+
if (code) results.push({ file, code })
|
|
207
|
+
lineIndex = fenceEnd + 1
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Fallback: single unnamed block when only one file is being fixed
|
|
211
|
+
if (results.length === 0) {
|
|
212
|
+
const code = extractCode(text)
|
|
213
|
+
if (code) results.push({ file: null, code })
|
|
214
|
+
}
|
|
215
|
+
return results
|
|
122
216
|
}
|
|
123
217
|
|
|
218
|
+
const MAX_FIX_ATTEMPTS = 3
|
|
219
|
+
|
|
124
220
|
/**
|
|
125
|
-
*
|
|
221
|
+
* Writes generated fixes to existing test files.
|
|
222
|
+
* @param {Array<{file: string|null, code: string}>} fixed Parsed fixes.
|
|
223
|
+
* @param {Array<{file: string, errors: string[]}>} remaining Current failing files.
|
|
224
|
+
* @param {string} dir Project root.
|
|
225
|
+
* @returns {void}
|
|
226
|
+
*/
|
|
227
|
+
function writeFixedFiles(fixed, remaining, dir) {
|
|
228
|
+
for (const { file, code } of fixed) {
|
|
229
|
+
let absPath = null
|
|
230
|
+
if (file) {
|
|
231
|
+
absPath = join(dir, file)
|
|
232
|
+
} else if (remaining.length === 1) {
|
|
233
|
+
absPath = join(dir, remaining[0].file)
|
|
234
|
+
}
|
|
235
|
+
if (!absPath) {
|
|
236
|
+
console.error(' ✗ не вдалось визначити файл для запису (немає маркера <!-- file: ... -->)')
|
|
237
|
+
continue
|
|
238
|
+
}
|
|
239
|
+
if (!existsSync(absPath)) {
|
|
240
|
+
console.error(` ✗ файл не існує: ${relative(dir, absPath)}`)
|
|
241
|
+
continue
|
|
242
|
+
}
|
|
243
|
+
writeFileSync(absPath, code + '\n', 'utf8')
|
|
244
|
+
console.log(` ✓ Записано: ${relative(dir, absPath)}`)
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Detects and fixes failing tests using pi SDK text mode + direct writeFileSync.
|
|
126
250
|
* Returns immediately with count=0 if all tests are already passing.
|
|
127
|
-
*
|
|
128
251
|
* @param {string} dir project root
|
|
129
252
|
* @param {{
|
|
130
253
|
* failures?: Array<{file: string, errors: string[]}>,
|
|
131
|
-
*
|
|
132
|
-
* }} [opts]
|
|
133
|
-
* @returns {Promise<{count: number, fixed: number, remaining: number}>}
|
|
254
|
+
* callTextFn?: (prompt: string, opts?: object) => Promise<string>
|
|
255
|
+
* }} [opts] overrides for callText and pre-fetched failures
|
|
256
|
+
* @returns {Promise<{count: number, fixed: number, remaining: number}>} fix result summary
|
|
134
257
|
*/
|
|
135
258
|
export async function fixFailingTests(dir, opts = {}) {
|
|
136
|
-
const
|
|
259
|
+
const callTextFn = opts.callTextFn ?? (prompt => callText(prompt, { model: MODEL, cwd: dir }))
|
|
137
260
|
const failures = opts.failures ?? (await getFailingTests(dir))
|
|
138
261
|
|
|
139
262
|
if (failures.length === 0) return { count: 0, fixed: 0, remaining: 0 }
|
|
140
263
|
|
|
141
|
-
console.log(`\n🔧 Виправляю ${failures.length} падаючих test-файлів (pi
|
|
264
|
+
console.log(`\n🔧 Виправляю ${failures.length} падаючих test-файлів (pi text mode)...\n`)
|
|
142
265
|
for (const f of failures) {
|
|
143
266
|
console.log(` • ${f.file} (${f.errors.length} помилок)`)
|
|
144
267
|
}
|
|
145
268
|
console.log()
|
|
146
269
|
|
|
147
|
-
|
|
148
|
-
|
|
270
|
+
let remaining = failures
|
|
271
|
+
for (let attempt = 1; attempt <= MAX_FIX_ATTEMPTS && remaining.length > 0; attempt++) {
|
|
272
|
+
if (attempt > 1) {
|
|
273
|
+
console.log(`\n🔄 Спроба ${attempt}/${MAX_FIX_ATTEMPTS}: залишилось ${remaining.length} файлів...\n`)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const prompt = buildFixTestsPrompt(remaining, dir)
|
|
277
|
+
let response
|
|
278
|
+
try {
|
|
279
|
+
response = await callTextFn(prompt)
|
|
280
|
+
} catch (error) {
|
|
281
|
+
console.error(` ✗ pi помилка: ${error.message}`)
|
|
282
|
+
break
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const fixed = parseFixedFiles(response)
|
|
286
|
+
|
|
287
|
+
if (fixed.length === 0) {
|
|
288
|
+
console.error(' ✗ pi не повернула виправлений код')
|
|
289
|
+
break
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
writeFixedFiles(fixed, remaining, dir)
|
|
293
|
+
|
|
294
|
+
remaining = await getFailingTests(dir)
|
|
295
|
+
}
|
|
149
296
|
|
|
150
|
-
const
|
|
151
|
-
const fixed = failures.length - after.length
|
|
297
|
+
const fixedCount = failures.length - remaining.length
|
|
152
298
|
|
|
153
|
-
if (
|
|
154
|
-
if (
|
|
299
|
+
if (fixedCount > 0) console.log(`✓ Виправлено: ${fixedCount}/${failures.length} файлів`)
|
|
300
|
+
if (remaining.length > 0) console.log(`⚠ Залишились падати: ${remaining.length} файлів`)
|
|
155
301
|
|
|
156
|
-
return { count: failures.length, fixed, remaining:
|
|
302
|
+
return { count: failures.length, fixed: fixedCount, remaining: remaining.length }
|
|
157
303
|
}
|