@7n/test 0.7.2 → 0.8.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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.8.0] - 2026-06-28
4
+
5
+ ### Added
6
+
7
+ - `assessNeed` тепер запускає локальні JS-евристики перед LLM-викликом: файли-реекспорти (`export { } from`) класифікуються як `needsTests:false`, файли з функціями та гілками — як `needsTests:true`, без будь-яких витрат на API. LLM викликається тільки для неоднозначних файлів. Нова публічна функція `quickClassify(content)` доступна для тестування/розширення.
8
+
3
9
  ## [0.7.2] - 2026-06-27
4
10
 
5
11
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@7n/test",
3
- "version": "0.7.2",
3
+ "version": "0.8.0",
4
4
  "description": "CLI-утиліта @7n/test",
5
5
  "keywords": [
6
6
  "7n",
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * LLM assessment via pi SDK: does an uncovered file actually need unit tests?
3
3
  * Uses callText() (no tools) for structured JSON replies.
4
+ *
5
+ * Quick local heuristics run first — LLM is called only for ambiguous files.
4
6
  */
5
7
  import { existsSync, readFileSync } from 'node:fs'
6
8
  import { join } from 'node:path'
@@ -25,17 +27,60 @@ needsTests: true when:
25
27
  - Business logic with conditions or non-trivial contracts
26
28
  - Pure functions that can be unit-tested cheaply`
27
29
 
30
+ // Lines that are purely module wiring — no testable logic
31
+ const WIRING_RE = /^(import\b|export\s+(?:\{[^}]*\}|\*|type\b|interface\b|enum\b))/
32
+
33
+ /**
34
+ * Strip JS/TS comments for heuristic analysis.
35
+ * @param {string} src
36
+ * @returns {string}
37
+ */
38
+ function stripComments(src) {
39
+ return src.replace(/\/\*[\s\S]*?\*\//g, ' ').replace(/\/\/[^\n]*/g, '')
40
+ }
41
+
42
+ /**
43
+ * Fast local classifier — no I/O, no LLM.
44
+ * Returns a result object for obvious cases, or null when ambiguous.
45
+ *
46
+ * @param {string} content file source
47
+ * @returns {{ needsTests: boolean, reason: string } | null}
48
+ */
49
+ export function quickClassify(content) {
50
+ const stripped = stripComments(content)
51
+ const lines = stripped.split('\n').map(l => l.trim()).filter(Boolean)
52
+
53
+ // All lines are imports/re-exports → no testable logic
54
+ if (lines.length > 0 && lines.every(l => WIRING_RE.test(l))) {
55
+ return { needsTests: false, reason: 'лише імпорти/реекспорти без логіки' }
56
+ }
57
+
58
+ // Has control flow AND function bodies → obviously needs tests
59
+ const hasBranches = /\bif\s*\(|\bswitch\s*\(/.test(stripped)
60
+ const hasFunctions = /\bfunction\s*\w*\s*\(|=>\s*\{/.test(stripped)
61
+ if (hasBranches && hasFunctions) {
62
+ return { needsTests: true, reason: 'містить функції з розгалуженнями' }
63
+ }
64
+
65
+ return null
66
+ }
67
+
28
68
  /**
29
69
  * @param {{file: string, pct: number}} fileInfo
30
70
  * @param {string} dir project root
31
- * @param {Function} [callTextFn] injectable for tests
71
+ * @param {Function} callTextFn
32
72
  * @returns {Promise<{file: string, pct: number, needsTests: boolean, reason: string}>}
33
73
  */
34
74
  async function assessOne(fileInfo, dir, callTextFn) {
35
75
  const absPath = join(dir, fileInfo.file)
36
76
  if (!existsSync(absPath)) return { ...fileInfo, needsTests: false, reason: 'файл недоступний' }
37
77
 
38
- let content = readFileSync(absPath, 'utf8')
78
+ const rawContent = readFileSync(absPath, 'utf8')
79
+
80
+ const quick = quickClassify(rawContent)
81
+ if (quick !== null) return { ...fileInfo, ...quick }
82
+
83
+ let content = rawContent
39
84
  if (content.length > MAX_CONTENT_BYTES) content = content.slice(0, MAX_CONTENT_BYTES) + '\n...(truncated)'
40
85
 
41
86
  const prompt =
@@ -48,23 +93,20 @@ async function assessOne(fileInfo, dir, callTextFn) {
48
93
  const match = text.match(/\{[\s\S]*?"needsTests"[\s\S]*?\}/)
49
94
  const parsed = JSON.parse(match?.[0] ?? '{}')
50
95
  return {
51
- file: fileInfo.file,
52
- pct: fileInfo.pct,
96
+ ...fileInfo,
53
97
  needsTests: parsed.needsTests !== false,
54
98
  reason: typeof parsed.reason === 'string' ? parsed.reason : ''
55
99
  }
56
100
  } catch {
57
- return {
58
- file: fileInfo.file,
59
- pct: fileInfo.pct,
60
- needsTests: true,
61
- reason: 'оцінка не вдалась — вважаємо що потрібні тести'
62
- }
101
+ return { ...fileInfo, needsTests: true, reason: 'оцінка не вдалась — вважаємо що потрібні тести' }
63
102
  }
64
103
  }
65
104
 
66
105
  /**
67
106
  * Assess a list of uncovered files: do they need tests?
107
+ * Obvious cases (re-exports, functions-with-branches) are resolved locally.
108
+ * Only ambiguous files trigger an LLM call.
109
+ *
68
110
  * @param {Array<{file: string, pct: number}>} files
69
111
  * @param {string} dir project root
70
112
  * @param {{ callText?: Function }} [opts]