@7n/test 0.10.0 → 0.10.2
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 +12 -0
- package/package.json +1 -1
- package/src/docs/gen-tests.md +1 -1
- package/src/gen-tests.mjs +2 -2
- package/src/lib/docs/pi-client.md +1 -1
- package/src/lib/pi-client.mjs +72 -35
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.10.2] - 2026-07-02
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- resolveLocalModel (gen-tests.mjs) помилково трактував checkEnv() з @nitra/check-env як гетер значення змінної середовища — насправді це лише валідатор наявності, що завжди повертає undefined на успіху. Через це N_LOCAL_MIN_MODEL ігнорувався і двоярусна генерація (local+cloud) ніколи не вмикалась, навіть за коректно заданої змінної — завжди йшов fallback у single-file (cloud) режим. Замінено на пряме читання process.env.N_LOCAL_MIN_MODEL.
|
|
8
|
+
|
|
9
|
+
## [0.10.1] - 2026-07-02
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- callText і callAgent (pi-client) тепер ретраять transient-помилки з'єднання (напр. до локального omlx-сервера під навантаженням) з експоненційним backoff + jitter замість негайного провалу файлу; кількість спроб і базова затримка налаштовуються через N_PI_RETRY_ATTEMPTS / N_PI_RETRY_DELAY_MS.
|
|
14
|
+
|
|
3
15
|
## [0.10.0] - 2026-07-02
|
|
4
16
|
|
|
5
17
|
### Changed
|
package/package.json
CHANGED
package/src/docs/gen-tests.md
CHANGED
package/src/gen-tests.mjs
CHANGED
|
@@ -21,7 +21,7 @@ import { resolveVitestRun } from './lib/vitest-shim.mjs'
|
|
|
21
21
|
import { extractExportsWithComplexity } from './classify-exports.mjs'
|
|
22
22
|
import { analyzeModule } from './lib/ast-analyze.mjs'
|
|
23
23
|
import { probeModule, probeFetchCalls, probeTimeVariants, probeHelpers } from './lib/runtime-probe.mjs'
|
|
24
|
-
import {
|
|
24
|
+
import { env } from 'node:process'
|
|
25
25
|
|
|
26
26
|
const MAX_SRC_BYTES = 6000
|
|
27
27
|
|
|
@@ -1142,7 +1142,7 @@ async function generateOneTest(fileInfo, dir, callTextFn) {
|
|
|
1142
1142
|
*/
|
|
1143
1143
|
function resolveLocalModel(opts) {
|
|
1144
1144
|
if (opts.localModel !== undefined) return opts.localModel
|
|
1145
|
-
return
|
|
1145
|
+
return env.N_LOCAL_MIN_MODEL ?? null
|
|
1146
1146
|
}
|
|
1147
1147
|
|
|
1148
1148
|
/**
|
package/src/lib/pi-client.mjs
CHANGED
|
@@ -5,20 +5,54 @@
|
|
|
5
5
|
* Two modes:
|
|
6
6
|
* callText(prompt, model?) — text-only, no tools, returns string
|
|
7
7
|
* callAgent(prompt, cwd) — coding tools enabled, writes files directly
|
|
8
|
+
*
|
|
9
|
+
* Both retry transient connection failures (e.g. a shared local model server
|
|
10
|
+
* that's momentarily busy under concurrent load) with exponential backoff
|
|
11
|
+
* instead of failing the caller's file on the first hiccup.
|
|
8
12
|
*/
|
|
9
13
|
import { createAgentSession, SessionManager, ModelRegistry, AuthStorage } from '@earendil-works/pi-coding-agent'
|
|
14
|
+
import { env } from 'node:process'
|
|
15
|
+
import { setTimeout as sleep } from 'node:timers/promises'
|
|
10
16
|
|
|
11
17
|
let _registry = null
|
|
18
|
+
/**
|
|
19
|
+
*
|
|
20
|
+
*/
|
|
12
21
|
async function getRegistry() {
|
|
13
22
|
if (_registry) return _registry
|
|
14
23
|
_registry = ModelRegistry.create(AuthStorage.create())
|
|
15
24
|
return _registry
|
|
16
25
|
}
|
|
17
26
|
|
|
27
|
+
const RETRYABLE_ERROR_RE = /connection error|ECONNREFUSED|ETIMEDOUT|fetch failed|network/i
|
|
28
|
+
const MAX_ATTEMPTS = Number(env.N_PI_RETRY_ATTEMPTS) || 4
|
|
29
|
+
const BASE_DELAY_MS = Number(env.N_PI_RETRY_DELAY_MS) || 1500
|
|
30
|
+
const MAX_DELAY_MS = 15_000
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Retries `fn` with exponential backoff + jitter when it throws a transient
|
|
34
|
+
* connection-ish error (shared local model server busy/restarting); other
|
|
35
|
+
* errors (auth, malformed request) are re-thrown immediately.
|
|
36
|
+
* @template T
|
|
37
|
+
* @param {() => Promise<T>} fn operation to retry
|
|
38
|
+
* @returns {Promise<T>} result of the first successful attempt
|
|
39
|
+
*/
|
|
40
|
+
async function withRetry(fn) {
|
|
41
|
+
for (let attempt = 1; ; attempt++) {
|
|
42
|
+
try {
|
|
43
|
+
return await fn()
|
|
44
|
+
} catch (error) {
|
|
45
|
+
if (attempt >= MAX_ATTEMPTS || !RETRYABLE_ERROR_RE.test(error.message ?? '')) throw error
|
|
46
|
+
const delay = Math.min(BASE_DELAY_MS * 2 ** (attempt - 1), MAX_DELAY_MS)
|
|
47
|
+
const jitter = delay * (0.5 + Math.random() * 0.5)
|
|
48
|
+
await sleep(jitter)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
18
53
|
/**
|
|
19
54
|
* Sends a single prompt to pi in text mode (no tools) and returns the response.
|
|
20
55
|
* Reads auth/model config from ~/.pi/ same as the CLI.
|
|
21
|
-
*
|
|
22
56
|
* @param {string} prompt
|
|
23
57
|
* @param {object} [opts]
|
|
24
58
|
* @param {string} [opts.cwd]
|
|
@@ -26,51 +60,54 @@ async function getRegistry() {
|
|
|
26
60
|
* @returns {Promise<string>}
|
|
27
61
|
*/
|
|
28
62
|
export async function callText(prompt, opts = {}) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
63
|
+
return withRetry(async () => {
|
|
64
|
+
const cwd = opts.cwd ?? process.cwd()
|
|
65
|
+
const sessionOpts = {
|
|
66
|
+
tools: [],
|
|
67
|
+
sessionManager: SessionManager.inMemory(cwd),
|
|
68
|
+
cwd
|
|
69
|
+
}
|
|
70
|
+
if (opts.model) {
|
|
71
|
+
const registry = await getRegistry()
|
|
72
|
+
const slashIdx = opts.model.indexOf('/')
|
|
73
|
+
const provider = slashIdx === -1 ? null : opts.model.slice(0, slashIdx)
|
|
74
|
+
const modelId = slashIdx === -1 ? opts.model : opts.model.slice(slashIdx + 1)
|
|
75
|
+
const resolved = provider ? registry.find(provider, modelId) : null
|
|
76
|
+
sessionOpts.modelRegistry = registry
|
|
77
|
+
sessionOpts.model = resolved ?? opts.model
|
|
78
|
+
}
|
|
79
|
+
const { session } = await createAgentSession(sessionOpts)
|
|
45
80
|
|
|
46
|
-
|
|
81
|
+
await session.prompt(prompt)
|
|
47
82
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
83
|
+
const state = session.state
|
|
84
|
+
const last = state.messages.at(-1)
|
|
85
|
+
if (!last || last.role !== 'assistant') return ''
|
|
86
|
+
if (last.stopReason === 'error' || last.stopReason === 'aborted') {
|
|
87
|
+
throw new Error(`pi error: ${last.errorMessage ?? last.stopReason}`)
|
|
88
|
+
}
|
|
89
|
+
return last.content
|
|
90
|
+
.filter(c => c.type === 'text')
|
|
91
|
+
.map(c => c.text)
|
|
92
|
+
.join('')
|
|
93
|
+
})
|
|
58
94
|
}
|
|
59
95
|
|
|
60
96
|
/**
|
|
61
97
|
* Sends a prompt to pi in agent mode with full coding tools (read/write/bash/edit).
|
|
62
98
|
* The agent writes test files directly — no need to parse output.
|
|
63
|
-
*
|
|
64
99
|
* @param {string} prompt
|
|
65
100
|
* @param {string} cwd project root where files should be written
|
|
66
101
|
* @returns {Promise<void>}
|
|
67
102
|
*/
|
|
68
103
|
export async function callAgent(prompt, cwd) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
104
|
+
return withRetry(async () => {
|
|
105
|
+
const { session } = await createAgentSession({
|
|
106
|
+
tools: ['read', 'write', 'edit', 'bash', 'grep', 'find', 'ls'],
|
|
107
|
+
sessionManager: SessionManager.inMemory(cwd),
|
|
108
|
+
cwd
|
|
109
|
+
})
|
|
74
110
|
|
|
75
|
-
|
|
111
|
+
await session.prompt(prompt)
|
|
112
|
+
})
|
|
76
113
|
}
|