@7n/test 0.10.3 → 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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.11.1] - 2026-07-04
4
+
5
+ ### Changed
6
+
7
+ - 🤖 fix(core): omlx memory-guard помилка тепер друкує тіло запиту й завершує процес замість retry
8
+
9
+ ## [0.11.0] - 2026-07-03
10
+
11
+ ### Changed
12
+
13
+ - chore(deps): major-оновлення — @nitra/cursor 13.2.6→14.4.5 (unified lint surface + прогрес-бар), vitest 3→4.1.9 і @vitest/coverage-v8 3→4 (include замість видаленого coverage.all у config/shim/per-file), @stryker-mutator/vitest-runner 8→9.6.1 (+ explicit @stryker-mutator/core exact-peer у root), zod 3→4.4.3 (без правок коду)
14
+
3
15
  ## [0.10.3] - 2026-07-03
4
16
 
5
17
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@7n/test",
3
- "version": "0.10.3",
3
+ "version": "0.11.1",
4
4
  "description": "CLI-утиліта @7n/test",
5
5
  "keywords": [
6
6
  "7n",
@@ -39,15 +39,16 @@
39
39
  "test": "vitest run"
40
40
  },
41
41
  "dependencies": {
42
- "@earendil-works/pi-coding-agent": "^0.80.2",
42
+ "@earendil-works/pi-coding-agent": "^0.80.3",
43
+ "@nitra/check-env": "^4.2.1",
43
44
  "@vitest/coverage-v8": "^4.1.9",
44
45
  "rollup": "^4.62.2",
45
46
  "vitest": "^4.1.9",
46
- "zod": "^3.23.0",
47
- "@nitra/check-env": "^4.0.0"
47
+ "zod": "^4.4.3"
48
48
  },
49
49
  "engines": {
50
50
  "bun": ">=1.3",
51
51
  "node": ">=24"
52
- }
52
+ },
53
+ "devDependencies": {}
53
54
  }
@@ -110,6 +110,8 @@ export async function measureCoveragePerFile(dir) {
110
110
  '--passWithNoTests',
111
111
  '--coverage',
112
112
  '--coverage.reporter=lcov',
113
+ // vitest 4 прибрав coverage.all — явний include, щоб файли без тестів лишались у lcov (0%)
114
+ '--coverage.include=**/*.{js,mjs,ts,vue}',
113
115
  `--coverage.reportsDirectory=${lcovDir}`,
114
116
  '--reporter=verbose',
115
117
  '--reporter=json',
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: coverage-per-file.mjs
4
4
  resource: npm/src/coverage-per-file.mjs
5
5
  docgen:
6
- crc: edc508cb
6
+ crc: d167dff5
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  score: 100
9
9
  issues: judge:inaccurate:0.98
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: gen-tests.mjs
4
4
  resource: npm/src/gen-tests.mjs
5
5
  docgen:
6
- crc: 899f36bc
6
+ crc: 243806c4
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  score: 100
9
9
  issues: judge:inaccurate:0.99
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, reset: boolean, lastErrors: string|null}} loop-control state
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, reset: false, lastErrors: null }
711
- if (MEMORY_ERROR_RE.test(error.message)) return { stop: false, reset: true, lastErrors: null }
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
- if (failure.reset) {
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
  }
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: pi-client.mjs
4
4
  resource: npm/src/lib/pi-client.mjs
5
5
  docgen:
6
- crc: f2c45cdc
6
+ crc: 0aa22b9a
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  score: 100
9
9
  issues: judge:inaccurate:0.98
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: vitest-shim.mjs
4
4
  resource: npm/src/lib/vitest-shim.mjs
5
5
  docgen:
6
- crc: 5d7aaaf0
6
+ crc: e08618cc
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  score: 100
9
9
  issues: judge:inaccurate:0.97
@@ -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
  }
@@ -42,12 +42,17 @@ export function ensureVitestShim() {
42
42
  VITEST_SHIM_CONFIG,
43
43
  `import { defineConfig } from ${JSON.stringify('file://' + VITEST_CONFIG_ENTRY)}\n` +
44
44
  `export default defineConfig({\n` +
45
- ` test: { environment: 'node' },\n` +
46
- ` coverage: {\n` +
47
- ` provider: 'v8',\n` +
45
+ ` test: {\n` +
46
+ ` environment: 'node',\n` +
47
+ // coverage МАЄ бути всередині test — поза ним vitest блок ігнорує (виправлено разом
48
+ // із міграцією на vitest 4; include — заміна видаленого all:true).
49
+ ` coverage: {\n` +
50
+ ` provider: 'v8',\n` +
51
+ ` include: ['**/*.{js,mjs,ts,vue}'],\n` +
48
52
  // Exclude test files that use non-vitest runners (bun:test, jest) from
49
53
  // the coverage scan so they don't cause import errors or skewed data.
50
- ` exclude: ['**/node_modules/**'],\n` +
54
+ ` exclude: ['**/node_modules/**'],\n` +
55
+ ` },\n` +
51
56
  ` },\n` +
52
57
  `})\n`
53
58
  )