@7n/test 0.11.0 → 0.11.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 +6 -0
- package/package.json +1 -1
- package/src/docs/gen-tests.md +1 -1
- package/src/fix-tests.mjs +4 -1
- package/src/gen-tests.mjs +10 -14
- package/src/lib/docs/pi-client.md +1 -1
- package/src/lib/pi-client.mjs +30 -5
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
package/src/docs/gen-tests.md
CHANGED
package/src/fix-tests.mjs
CHANGED
|
@@ -19,7 +19,7 @@ import { mkdtemp, rm } from 'node:fs/promises'
|
|
|
19
19
|
import { tmpdir } from 'node:os'
|
|
20
20
|
import { join, relative } from 'node:path'
|
|
21
21
|
import { env } from 'node:process'
|
|
22
|
-
import { callText } from './lib/pi-client.mjs'
|
|
22
|
+
import { callText, MEMORY_ERROR_RE } from './lib/pi-client.mjs'
|
|
23
23
|
import { findTestRules } from './gen-tests.mjs'
|
|
24
24
|
import { parseFailingTests } from './coverage-per-file.mjs'
|
|
25
25
|
import { resolveVitestRun } from './lib/vitest-shim.mjs'
|
|
@@ -278,6 +278,9 @@ export async function fixFailingTests(dir, opts = {}) {
|
|
|
278
278
|
try {
|
|
279
279
|
response = await callTextFn(prompt)
|
|
280
280
|
} catch (error) {
|
|
281
|
+
// memory-guard: не звичайна per-file помилка — RAM-стеля фіксована, продовжувати
|
|
282
|
+
// до наступного файлу немає сенсу. Пробиваємо нагору до CLI, аби процес завершився.
|
|
283
|
+
if (MEMORY_ERROR_RE.test(error.message ?? '')) throw error
|
|
281
284
|
console.error(` ✗ pi помилка: ${error.message}`)
|
|
282
285
|
break
|
|
283
286
|
}
|
package/src/gen-tests.mjs
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
17
17
|
import { spawnSync } from 'node:child_process'
|
|
18
18
|
import { join, relative, dirname } from 'node:path'
|
|
19
|
-
import { callText } from './lib/pi-client.mjs'
|
|
19
|
+
import { callText, MEMORY_ERROR_RE } from './lib/pi-client.mjs'
|
|
20
20
|
import { resolveVitestRun } from './lib/vitest-shim.mjs'
|
|
21
21
|
import { extractExportsWithComplexity } from './classify-exports.mjs'
|
|
22
22
|
import { analyzeModule } from './lib/ast-analyze.mjs'
|
|
@@ -51,7 +51,6 @@ const MOCK_TYPE_RE = /:\s*\w*Mock\b/
|
|
|
51
51
|
const FETCH_CALL_RE = /\bfetch\s*\(/
|
|
52
52
|
const TIME_DEPS_RE = /\bnew\s+Date\b|\bgetHours\b|\bgetDay\b|\bgetMinutes\b|\bDate\.now\b/
|
|
53
53
|
const VITEST_FAIL_RE = /Failed Tests|FAIL /
|
|
54
|
-
const MEMORY_ERROR_RE = /memory guard|memory limit|prefill would require/i
|
|
55
54
|
const EXPECTED_LINE_RE = /Expected:\s+"([^"]+)"/
|
|
56
55
|
const RECEIVED_LINE_RE = /Received:\s+"([^"]+)"/
|
|
57
56
|
const TO_CONTAIN_RE = /to contain '([^='\s]+)=([^']+)'/
|
|
@@ -703,13 +702,15 @@ function buildRetryDiagnostics(lastErrors, prevErrorSig, staleCount, label) {
|
|
|
703
702
|
* @param {number} attempt current attempt number
|
|
704
703
|
* @param {number} maxAttempts retry limit
|
|
705
704
|
* @param {string} label display name for logging
|
|
706
|
-
* @returns {{stop: boolean,
|
|
705
|
+
* @returns {{stop: boolean, lastErrors: string|null}} loop-control state
|
|
707
706
|
*/
|
|
708
707
|
function resolveLoopCallFailure(error, attempt, maxAttempts, label) {
|
|
708
|
+
// memory-guard: не звичайна per-file помилка — RAM-стеля фіксована, продовжувати
|
|
709
|
+
// до наступного файлу немає сенсу. Пробиваємо нагору до CLI, аби процес завершився.
|
|
710
|
+
if (MEMORY_ERROR_RE.test(error.message ?? '')) throw error
|
|
709
711
|
console.log(` ${label} ✗ LLM error (спроба ${attempt}): ${error.message}`)
|
|
710
|
-
if (attempt >= maxAttempts) return { stop: true,
|
|
711
|
-
|
|
712
|
-
return { stop: false, reset: false, lastErrors: `LLM error: ${error.message}` }
|
|
712
|
+
if (attempt >= maxAttempts) return { stop: true, lastErrors: null }
|
|
713
|
+
return { stop: false, lastErrors: `LLM error: ${error.message}` }
|
|
713
714
|
}
|
|
714
715
|
|
|
715
716
|
/**
|
|
@@ -760,14 +761,7 @@ async function generateBlockWithLoop(
|
|
|
760
761
|
} catch (error) {
|
|
761
762
|
const failure = resolveLoopCallFailure(error, attempt, maxAttempts, label)
|
|
762
763
|
if (failure.stop) break
|
|
763
|
-
|
|
764
|
-
lastBlock = null
|
|
765
|
-
lastErrors = null
|
|
766
|
-
prevErrorSig = null
|
|
767
|
-
staleCount = 0
|
|
768
|
-
} else {
|
|
769
|
-
lastErrors = failure.lastErrors
|
|
770
|
-
}
|
|
764
|
+
lastErrors = failure.lastErrors
|
|
771
765
|
continue
|
|
772
766
|
}
|
|
773
767
|
|
|
@@ -879,6 +873,7 @@ async function generateSharedHeader(ctx, dir, callTextFn) {
|
|
|
879
873
|
if (header) return header
|
|
880
874
|
console.error(` ✗ cloud не повернув header для ${file}`)
|
|
881
875
|
} catch (error) {
|
|
876
|
+
if (MEMORY_ERROR_RE.test(error.message ?? '')) throw error
|
|
882
877
|
console.error(` ✗ cloud header error: ${error.message}`)
|
|
883
878
|
}
|
|
884
879
|
return null
|
|
@@ -1116,6 +1111,7 @@ async function generateOneTest(fileInfo, dir, callTextFn) {
|
|
|
1116
1111
|
try {
|
|
1117
1112
|
response = await callTextFn(prompt, { cwd: dir })
|
|
1118
1113
|
} catch (error) {
|
|
1114
|
+
if (MEMORY_ERROR_RE.test(error.message ?? '')) throw error
|
|
1119
1115
|
console.error(` ✗ pi помилка для ${fileInfo.file}: ${error.message}`)
|
|
1120
1116
|
return null
|
|
1121
1117
|
}
|
package/src/lib/pi-client.mjs
CHANGED
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Both retry transient connection failures (e.g. a shared local model server
|
|
10
10
|
* that's momentarily busy under concurrent load) with exponential backoff
|
|
11
|
-
* instead of failing the caller's file on the first hiccup.
|
|
11
|
+
* instead of failing the caller's file on the first hiccup. A memory-guard
|
|
12
|
+
* rejection (the shared machine can't fit the prompt in RAM) is not
|
|
13
|
+
* retryable — retrying against a fixed RAM ceiling can't succeed, so it
|
|
14
|
+
* prints the request body to stdout and terminates the process instead.
|
|
12
15
|
*/
|
|
13
16
|
import { createAgentSession, SessionManager, ModelRegistry, AuthStorage } from '@earendil-works/pi-coding-agent'
|
|
14
17
|
import { env } from 'node:process'
|
|
@@ -25,23 +28,45 @@ async function getRegistry() {
|
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
const RETRYABLE_ERROR_RE = /connection error|ECONNREFUSED|ETIMEDOUT|fetch failed|network/i
|
|
31
|
+
/** Matches a local model server (e.g. oMLX) rejecting a prompt for lack of RAM. */
|
|
32
|
+
export const MEMORY_ERROR_RE = /memory guard|memory limit|prefill would require/i
|
|
28
33
|
const MAX_ATTEMPTS = Number(env.N_PI_RETRY_ATTEMPTS) || 4
|
|
29
34
|
const BASE_DELAY_MS = Number(env.N_PI_RETRY_DELAY_MS) || 1500
|
|
30
35
|
const MAX_DELAY_MS = 15_000
|
|
31
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Prints the request body that triggered a memory-guard rejection to stdout
|
|
39
|
+
* and throws — there's no RAM to retry into. Callers that own the process
|
|
40
|
+
* lifecycle (CLI entrypoints) must let this propagate uncaught instead of
|
|
41
|
+
* swallowing it like a normal per-file error, so the process exits instead
|
|
42
|
+
* of quietly continuing against a RAM ceiling that won't change.
|
|
43
|
+
* @param {Error} error the memory-guard error thrown by the model server
|
|
44
|
+
* @param {string} requestBody prompt sent to the model
|
|
45
|
+
* @returns {never} always throws
|
|
46
|
+
*/
|
|
47
|
+
function failOnMemoryGuard(error, requestBody) {
|
|
48
|
+
console.log('--- omlx memory-guard: тіло запиту ---')
|
|
49
|
+
console.log(requestBody)
|
|
50
|
+
console.log(`✗ omlx memory-guard: ${error.message}`)
|
|
51
|
+
throw new Error(`omlx memory-guard: ${error.message}`)
|
|
52
|
+
}
|
|
53
|
+
|
|
32
54
|
/**
|
|
33
55
|
* Retries `fn` with exponential backoff + jitter when it throws a transient
|
|
34
56
|
* connection-ish error (shared local model server busy/restarting); other
|
|
35
|
-
* errors (auth, malformed request) are re-thrown immediately.
|
|
57
|
+
* errors (auth, malformed request) are re-thrown immediately. A memory-guard
|
|
58
|
+
* rejection throws via `failOnMemoryGuard` instead of retrying — see its docs.
|
|
36
59
|
* @template T
|
|
37
60
|
* @param {() => Promise<T>} fn operation to retry
|
|
61
|
+
* @param {string} requestBody prompt sent to the model, printed if a memory-guard error terminates the process
|
|
38
62
|
* @returns {Promise<T>} result of the first successful attempt
|
|
39
63
|
*/
|
|
40
|
-
async function withRetry(fn) {
|
|
64
|
+
async function withRetry(fn, requestBody) {
|
|
41
65
|
for (let attempt = 1; ; attempt++) {
|
|
42
66
|
try {
|
|
43
67
|
return await fn()
|
|
44
68
|
} catch (error) {
|
|
69
|
+
if (MEMORY_ERROR_RE.test(error.message ?? '')) failOnMemoryGuard(error, requestBody)
|
|
45
70
|
if (attempt >= MAX_ATTEMPTS || !RETRYABLE_ERROR_RE.test(error.message ?? '')) throw error
|
|
46
71
|
const delay = Math.min(BASE_DELAY_MS * 2 ** (attempt - 1), MAX_DELAY_MS)
|
|
47
72
|
const jitter = delay * (0.5 + Math.random() * 0.5)
|
|
@@ -90,7 +115,7 @@ export async function callText(prompt, opts = {}) {
|
|
|
90
115
|
.filter(c => c.type === 'text')
|
|
91
116
|
.map(c => c.text)
|
|
92
117
|
.join('')
|
|
93
|
-
})
|
|
118
|
+
}, prompt)
|
|
94
119
|
}
|
|
95
120
|
|
|
96
121
|
/**
|
|
@@ -109,5 +134,5 @@ export async function callAgent(prompt, cwd) {
|
|
|
109
134
|
})
|
|
110
135
|
|
|
111
136
|
await session.prompt(prompt)
|
|
112
|
-
})
|
|
137
|
+
}, prompt)
|
|
113
138
|
}
|