ai-unit-test-generator 2.0.6 → 2.0.8
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 +60 -0
- package/lib/ai/context-builder.mjs +163 -10
- package/lib/workflows/analyze.mjs +16 -1
- package/lib/workflows/batch.mjs +24 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
@@ -5,6 +5,66 @@ All notable changes to this project will be documented in this file.
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
7
|
|
8
|
+
## [2.0.8] - 2025-01-11
|
9
|
+
|
10
|
+
### ✨ Feature: Enhanced Framework Detection
|
11
|
+
|
12
|
+
**Problem**: `context-builder.mjs` framework detection was too simplistic, only detecting basic React/Vue/Angular.
|
13
|
+
|
14
|
+
**Solution**: Completely rewritten framework detection with comprehensive support:
|
15
|
+
|
16
|
+
**Supported Frameworks & Platforms**:
|
17
|
+
- ✅ **React Native** (including Expo)
|
18
|
+
- ✅ **Next.js** (SSR/SSG)
|
19
|
+
- ✅ **React Native + Next.js** (Monorepo detection)
|
20
|
+
- ✅ **Taro** (Mini-Program + H5 + RN)
|
21
|
+
- ✅ **Vue / Nuxt.js**
|
22
|
+
- ✅ **Angular**
|
23
|
+
- ✅ **Svelte / SvelteKit**
|
24
|
+
- ✅ **Electron** (Desktop apps)
|
25
|
+
- ✅ **Node.js** (Backend)
|
26
|
+
|
27
|
+
**UI Library Detection**:
|
28
|
+
- Ant Design, Material-UI, Chakra UI, Radix UI, Tailwind CSS
|
29
|
+
- styled-components, Emotion, Bootstrap
|
30
|
+
- React Native Paper, NativeBase
|
31
|
+
|
32
|
+
**State Management Detection**:
|
33
|
+
- Jotai, Zustand, Redux, MobX, Recoil, XState, Valtio
|
34
|
+
- TanStack Query, SWR, Apollo Client
|
35
|
+
|
36
|
+
**Testing Tools Detection**:
|
37
|
+
- Jest, Vitest
|
38
|
+
- Testing Library (React/RN), Enzyme
|
39
|
+
- Cypress, Playwright
|
40
|
+
|
41
|
+
**Enhanced AI Analysis Output**:
|
42
|
+
```bash
|
43
|
+
ai-test analyze
|
44
|
+
|
45
|
+
📦 Step 4: Reading project context...
|
46
|
+
Framework: React Native
|
47
|
+
Platforms: iOS, Android
|
48
|
+
State: Jotai, SWR
|
49
|
+
Testing: Jest
|
50
|
+
```
|
51
|
+
|
52
|
+
**Benefits**:
|
53
|
+
- AI 分析时可以获得更精准的项目上下文
|
54
|
+
- 支持跨端、混合项目的识别
|
55
|
+
- 为测试生成提供更准确的技术栈信息
|
56
|
+
|
57
|
+
---
|
58
|
+
|
59
|
+
## [2.0.7] - 2025-01-11
|
60
|
+
|
61
|
+
### 🐛 Hotfix
|
62
|
+
- **Fixed**: `ai-test generate` workflow - `batch.mjs` now properly captures `prompt-builder.mjs` stdout and writes to `prompt.txt`
|
63
|
+
- Modified `sh()` helper to support stdout capture with `captureStdout` option
|
64
|
+
- Resolves missing `prompt.txt` file issue in generation workflow
|
65
|
+
|
66
|
+
---
|
67
|
+
|
8
68
|
## [2.0.6] - 2025-01-11
|
9
69
|
|
10
70
|
### 🐛 Hotfix
|
@@ -10,21 +10,46 @@ import { existsSync, readFileSync } from 'node:fs'
|
|
10
10
|
export async function buildProjectContext() {
|
11
11
|
const context = {
|
12
12
|
framework: 'Unknown',
|
13
|
+
platforms: [],
|
14
|
+
uiLibraries: [],
|
15
|
+
stateManagement: [],
|
13
16
|
criticalDeps: [],
|
14
|
-
devDeps: []
|
17
|
+
devDeps: [],
|
18
|
+
testingTools: []
|
15
19
|
}
|
16
20
|
|
17
21
|
// 读取 package.json
|
18
22
|
if (existsSync('package.json')) {
|
19
23
|
try {
|
20
24
|
const pkg = JSON.parse(readFileSync('package.json', 'utf-8'))
|
25
|
+
const allDeps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) }
|
21
26
|
|
22
27
|
context.name = pkg.name
|
23
|
-
context.framework = detectFramework(pkg.dependencies || {})
|
24
28
|
|
25
|
-
//
|
29
|
+
// 检测框架和平台
|
30
|
+
const frameworkInfo = detectFramework(allDeps)
|
31
|
+
context.framework = frameworkInfo.framework
|
32
|
+
context.platforms = frameworkInfo.platforms
|
33
|
+
|
34
|
+
// 检测 UI 库
|
35
|
+
context.uiLibraries = detectUILibraries(allDeps)
|
36
|
+
|
37
|
+
// 检测状态管理
|
38
|
+
context.stateManagement = detectStateManagement(allDeps)
|
39
|
+
|
40
|
+
// 检测测试工具
|
41
|
+
context.testingTools = detectTestingTools(allDeps)
|
42
|
+
|
43
|
+
// 识别关键业务依赖
|
26
44
|
const deps = Object.keys(pkg.dependencies || {})
|
27
|
-
const criticalKeywords = [
|
45
|
+
const criticalKeywords = [
|
46
|
+
'stripe', 'payment', 'paypal', 'checkout',
|
47
|
+
'auth', 'jwt', 'passport', 'oauth',
|
48
|
+
'prisma', 'typeorm', 'sequelize', 'mongoose', 'db', 'sql',
|
49
|
+
'axios', 'fetch', 'request', 'graphql', 'apollo',
|
50
|
+
'socket', 'websocket', 'pusher', 'firebase',
|
51
|
+
'sentry', 'datadog', 'analytics'
|
52
|
+
]
|
28
53
|
|
29
54
|
context.criticalDeps = deps.filter(dep =>
|
30
55
|
criticalKeywords.some(kw => dep.toLowerCase().includes(kw))
|
@@ -40,13 +65,141 @@ export async function buildProjectContext() {
|
|
40
65
|
}
|
41
66
|
|
42
67
|
/**
|
43
|
-
*
|
68
|
+
* 检测项目框架和平台
|
44
69
|
*/
|
45
70
|
function detectFramework(deps) {
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
71
|
+
const result = {
|
72
|
+
framework: 'Unknown',
|
73
|
+
platforms: []
|
74
|
+
}
|
75
|
+
|
76
|
+
// 检测 React Native
|
77
|
+
const hasRN = deps['react-native'] || deps['@react-native'] || deps['expo']
|
78
|
+
|
79
|
+
// 检测 Next.js
|
80
|
+
const hasNext = deps['next']
|
81
|
+
|
82
|
+
// 检测 React
|
83
|
+
const hasReact = deps['react']
|
84
|
+
|
85
|
+
// 检测 Vue
|
86
|
+
const hasVue = deps['vue'] || deps['@vue/core']
|
87
|
+
const hasNuxt = deps['nuxt']
|
88
|
+
|
89
|
+
// 检测 Angular
|
90
|
+
const hasAngular = deps['@angular/core']
|
91
|
+
|
92
|
+
// 检测 Svelte
|
93
|
+
const hasSvelte = deps['svelte']
|
94
|
+
const hasSvelteKit = deps['@sveltejs/kit']
|
95
|
+
|
96
|
+
// 检测 Taro (跨端框架)
|
97
|
+
const hasTaro = deps['@tarojs/taro'] || deps['@tarojs/runtime']
|
98
|
+
|
99
|
+
// 检测 Electron
|
100
|
+
const hasElectron = deps['electron']
|
101
|
+
|
102
|
+
// 组合判断
|
103
|
+
if (hasTaro) {
|
104
|
+
result.framework = 'Taro'
|
105
|
+
result.platforms.push('Mini-Program', 'H5', 'RN')
|
106
|
+
if (hasReact) result.framework = 'Taro (React)'
|
107
|
+
if (hasVue) result.framework = 'Taro (Vue)'
|
108
|
+
} else if (hasRN && hasNext) {
|
109
|
+
result.framework = 'React Native + Next.js (Monorepo)'
|
110
|
+
result.platforms.push('iOS', 'Android', 'Web')
|
111
|
+
} else if (hasRN) {
|
112
|
+
result.framework = 'React Native'
|
113
|
+
result.platforms.push('iOS', 'Android')
|
114
|
+
if (deps['expo']) result.framework = 'React Native (Expo)'
|
115
|
+
} else if (hasNext) {
|
116
|
+
result.framework = 'Next.js'
|
117
|
+
result.platforms.push('Web', 'SSR')
|
118
|
+
} else if (hasSvelteKit) {
|
119
|
+
result.framework = 'SvelteKit'
|
120
|
+
result.platforms.push('Web', 'SSR')
|
121
|
+
} else if (hasNuxt) {
|
122
|
+
result.framework = 'Nuxt.js'
|
123
|
+
result.platforms.push('Web', 'SSR')
|
124
|
+
} else if (hasAngular) {
|
125
|
+
result.framework = 'Angular'
|
126
|
+
result.platforms.push('Web')
|
127
|
+
} else if (hasSvelte) {
|
128
|
+
result.framework = 'Svelte'
|
129
|
+
result.platforms.push('Web')
|
130
|
+
} else if (hasVue) {
|
131
|
+
result.framework = 'Vue'
|
132
|
+
result.platforms.push('Web')
|
133
|
+
} else if (hasReact) {
|
134
|
+
result.framework = 'React'
|
135
|
+
result.platforms.push('Web')
|
136
|
+
} else if (hasElectron) {
|
137
|
+
result.framework = 'Electron'
|
138
|
+
result.platforms.push('Desktop')
|
139
|
+
} else if (Object.keys(deps).length > 0) {
|
140
|
+
result.framework = 'Node.js'
|
141
|
+
result.platforms.push('Backend')
|
142
|
+
}
|
143
|
+
|
144
|
+
return result
|
145
|
+
}
|
146
|
+
|
147
|
+
/**
|
148
|
+
* 检测 UI 库
|
149
|
+
*/
|
150
|
+
function detectUILibraries(deps) {
|
151
|
+
const uiLibs = []
|
152
|
+
|
153
|
+
if (deps['antd']) uiLibs.push('Ant Design')
|
154
|
+
if (deps['@mui/material'] || deps['@material-ui/core']) uiLibs.push('Material-UI')
|
155
|
+
if (deps['@chakra-ui/react']) uiLibs.push('Chakra UI')
|
156
|
+
if (deps['@radix-ui/react-dialog']) uiLibs.push('Radix UI')
|
157
|
+
if (deps['tailwindcss']) uiLibs.push('Tailwind CSS')
|
158
|
+
if (deps['styled-components']) uiLibs.push('styled-components')
|
159
|
+
if (deps['@emotion/react']) uiLibs.push('Emotion')
|
160
|
+
if (deps['bootstrap']) uiLibs.push('Bootstrap')
|
161
|
+
if (deps['semantic-ui-react']) uiLibs.push('Semantic UI')
|
162
|
+
if (deps['react-native-paper']) uiLibs.push('React Native Paper')
|
163
|
+
if (deps['native-base']) uiLibs.push('NativeBase')
|
164
|
+
|
165
|
+
return uiLibs
|
166
|
+
}
|
167
|
+
|
168
|
+
/**
|
169
|
+
* 检测状态管理
|
170
|
+
*/
|
171
|
+
function detectStateManagement(deps) {
|
172
|
+
const stateLibs = []
|
173
|
+
|
174
|
+
if (deps['jotai']) stateLibs.push('Jotai')
|
175
|
+
if (deps['zustand']) stateLibs.push('Zustand')
|
176
|
+
if (deps['redux'] || deps['@reduxjs/toolkit']) stateLibs.push('Redux')
|
177
|
+
if (deps['mobx'] || deps['mobx-react']) stateLibs.push('MobX')
|
178
|
+
if (deps['recoil']) stateLibs.push('Recoil')
|
179
|
+
if (deps['xstate'] || deps['@xstate/react']) stateLibs.push('XState')
|
180
|
+
if (deps['valtio']) stateLibs.push('Valtio')
|
181
|
+
if (deps['@tanstack/react-query']) stateLibs.push('TanStack Query')
|
182
|
+
if (deps['swr']) stateLibs.push('SWR')
|
183
|
+
if (deps['apollo-client'] || deps['@apollo/client']) stateLibs.push('Apollo Client')
|
184
|
+
|
185
|
+
return stateLibs
|
186
|
+
}
|
187
|
+
|
188
|
+
/**
|
189
|
+
* 检测测试工具
|
190
|
+
*/
|
191
|
+
function detectTestingTools(deps) {
|
192
|
+
const testTools = []
|
193
|
+
|
194
|
+
if (deps['jest']) testTools.push('Jest')
|
195
|
+
if (deps['vitest']) testTools.push('Vitest')
|
196
|
+
if (deps['@testing-library/react']) testTools.push('@testing-library/react')
|
197
|
+
if (deps['@testing-library/react-native']) testTools.push('@testing-library/react-native')
|
198
|
+
if (deps['enzyme']) testTools.push('Enzyme')
|
199
|
+
if (deps['cypress']) testTools.push('Cypress')
|
200
|
+
if (deps['playwright']) testTools.push('Playwright')
|
201
|
+
if (deps['@playwright/test']) testTools.push('Playwright')
|
202
|
+
|
203
|
+
return testTools
|
51
204
|
}
|
52
205
|
|
@@ -45,7 +45,22 @@ export async function analyze(options) {
|
|
45
45
|
console.log('📦 Step 4: Reading project context...')
|
46
46
|
const projectCtx = await buildProjectContext()
|
47
47
|
console.log(` Framework: ${projectCtx.framework}`)
|
48
|
-
|
48
|
+
if (projectCtx.platforms.length > 0) {
|
49
|
+
console.log(` Platforms: ${projectCtx.platforms.join(', ')}`)
|
50
|
+
}
|
51
|
+
if (projectCtx.uiLibraries.length > 0) {
|
52
|
+
console.log(` UI Libraries: ${projectCtx.uiLibraries.join(', ')}`)
|
53
|
+
}
|
54
|
+
if (projectCtx.stateManagement.length > 0) {
|
55
|
+
console.log(` State: ${projectCtx.stateManagement.join(', ')}`)
|
56
|
+
}
|
57
|
+
if (projectCtx.testingTools.length > 0) {
|
58
|
+
console.log(` Testing: ${projectCtx.testingTools.join(', ')}`)
|
59
|
+
}
|
60
|
+
if (projectCtx.criticalDeps.length > 0) {
|
61
|
+
console.log(` Critical deps: ${projectCtx.criticalDeps.join(', ')}`)
|
62
|
+
}
|
63
|
+
console.log()
|
49
64
|
|
50
65
|
// 5. 构建 AI Prompt
|
51
66
|
console.log('✍️ Step 5: Building AI analysis prompt...')
|
package/lib/workflows/batch.mjs
CHANGED
@@ -12,10 +12,24 @@ const __filename = fileURLToPath(import.meta.url)
|
|
12
12
|
const __dirname = dirname(__filename)
|
13
13
|
const pkgRoot = join(__dirname, '../..')
|
14
14
|
|
15
|
-
function sh(cmd, args = []) {
|
15
|
+
function sh(cmd, args = [], options = {}) {
|
16
16
|
return new Promise((resolve, reject) => {
|
17
|
-
const
|
18
|
-
child
|
17
|
+
const stdio = options.captureStdout ? ['inherit', 'pipe', 'inherit'] : 'inherit'
|
18
|
+
const child = spawn(cmd, args, { stdio, cwd: process.cwd() })
|
19
|
+
|
20
|
+
const chunks = []
|
21
|
+
if (options.captureStdout) {
|
22
|
+
child.stdout.on('data', d => chunks.push(Buffer.from(d)))
|
23
|
+
}
|
24
|
+
|
25
|
+
child.on('close', code => {
|
26
|
+
if (code === 0) {
|
27
|
+
const output = options.captureStdout ? Buffer.concat(chunks).toString('utf8') : null
|
28
|
+
resolve(output)
|
29
|
+
} else {
|
30
|
+
reject(new Error(`${cmd} exited ${code}`))
|
31
|
+
}
|
32
|
+
})
|
19
33
|
child.on('error', reject)
|
20
34
|
})
|
21
35
|
}
|
@@ -112,11 +126,16 @@ async function main(argv = process.argv) {
|
|
112
126
|
'--skip', String(skip),
|
113
127
|
'--only-todo' // 新增:只处理 TODO 状态
|
114
128
|
]
|
129
|
+
|
130
|
+
let promptText
|
115
131
|
try {
|
116
|
-
await sh('node', [...promptArgs, '--hints-file', 'reports/hints.txt'])
|
132
|
+
promptText = await sh('node', [...promptArgs, '--hints-file', 'reports/hints.txt'], { captureStdout: true })
|
117
133
|
} catch {
|
118
|
-
await sh('node', promptArgs)
|
134
|
+
promptText = await sh('node', promptArgs, { captureStdout: true })
|
119
135
|
}
|
136
|
+
|
137
|
+
// 写入 prompt.txt
|
138
|
+
writeFileSync('prompt.txt', promptText, 'utf-8')
|
120
139
|
|
121
140
|
// 2) 调用 AI
|
122
141
|
console.log('\n🤖 Calling AI...')
|