|
| 1 | +/** |
| 2 | + * TestPilot AI — 编程AI规则文件注入器 |
| 3 | + * |
| 4 | + * 插件激活时自动在用户项目中创建规则文件, |
| 5 | + * 让所有主流IDE的编程AI都能读到蓝本生成规则。 |
| 6 | + * |
| 7 | + * 覆盖9种IDE: |
| 8 | + * - AGENTS.md(跨工具通用:Cursor/Cline/Augment/Kilo Code) |
| 9 | + * - .github/copilot-instructions.md(VS Code Copilot) |
| 10 | + * - .cursor/rules/testpilot.md(Cursor) |
| 11 | + * - .windsurf/rules/testpilot.md(Windsurf) |
| 12 | + * - .trae/rules/testpilot.md(Trae / 字节跳动) |
| 13 | + * - .clinerules/testpilot.md(Cline) |
| 14 | + * - .aiassistant/rules/testpilot.md(JetBrains AI) |
| 15 | + * - .augment/rules/testpilot.md(Augment Code) |
| 16 | + * - CLAUDE.md(Claude Code / Anthropic CLI) |
| 17 | + */ |
| 18 | + |
| 19 | +import * as vscode from "vscode"; |
| 20 | +import * as fs from "fs"; |
| 21 | +import * as path from "path"; |
| 22 | + |
| 23 | +/** 需要注入的规则文件相对路径列表 */ |
| 24 | +const RULES_FILES: string[] = [ |
| 25 | + "AGENTS.md", |
| 26 | + "CLAUDE.md", |
| 27 | + ".github/copilot-instructions.md", |
| 28 | + ".cursor/rules/testpilot.md", |
| 29 | + ".windsurf/rules/testpilot.md", |
| 30 | + ".trae/rules/testpilot.md", |
| 31 | + ".clinerules/testpilot.md", |
| 32 | + ".aiassistant/rules/testpilot.md", |
| 33 | + ".augment/rules/testpilot.md", |
| 34 | +]; |
| 35 | + |
| 36 | +/** 规则模板内容(内嵌,避免依赖外部文件) */ |
| 37 | +function getTemplateContent(): string { |
| 38 | + return `# TestPilot AI — 编程AI蓝本自动生成规则 |
| 39 | +
|
| 40 | +> 本文件由 TestPilot AI 插件自动注入,指导编程AI在开发过程中自动生成和维护测试蓝本。 |
| 41 | +> 你可以根据项目需求自由修改本文件内容。 |
| 42 | +
|
| 43 | +--- |
| 44 | +
|
| 45 | +## 一、蓝本自动生成(最重要) |
| 46 | +
|
| 47 | +当项目中**不存在** \`testpilot.json\` 或 \`testpilot/\` 目录时,你在完成功能开发后**必须**自动生成蓝本文件。 |
| 48 | +
|
| 49 | +### 生成时机 |
| 50 | +
|
| 51 | +| 时机 | 动作 | |
| 52 | +|------|------| |
| 53 | +| 新项目创建完成 | 生成完整蓝本,覆盖所有已实现功能 | |
| 54 | +| 新增一个完整功能模块 | 在蓝本中追加对应场景 | |
| 55 | +| 项目初始蓝本不存在 | 扫描现有代码,生成功能全覆盖的蓝本 | |
| 56 | +
|
| 57 | +### 生成规则 |
| 58 | +
|
| 59 | +1. 蓝本文件固定命名 \`testpilot.json\`,放在项目根目录或 \`testpilot/\` 子目录下 |
| 60 | +2. 必须覆盖项目的**所有用户可交互功能**,不允许遗漏 |
| 61 | +3. 每个功能模块对应一个 \`page\`,每个用户场景对应一个 \`scenario\` |
| 62 | +4. 如果项目功能较多,按模块拆分为多个蓝本文件放在 \`testpilot/\` 目录下: |
| 63 | + - \`testpilot/auth.testpilot.json\` — 登录注册 |
| 64 | + - \`testpilot/product.testpilot.json\` — 商品管理 |
| 65 | + - \`testpilot/order.testpilot.json\` — 订单流程 |
| 66 | +5. 每个蓝本必须包含 \`app_name\`、\`description\`、\`platform\`、\`base_url\` 字段 |
| 67 | +
|
| 68 | +--- |
| 69 | +
|
| 70 | +## 二、蓝本增量维护(日常开发) |
| 71 | +
|
| 72 | +当项目已有蓝本文件时,以下6种代码变更**必须**同步更新蓝本: |
| 73 | +
|
| 74 | +| # | 触发条件 | 蓝本更新动作 | |
| 75 | +|---|---------|------------| |
| 76 | +| 1 | 新增/删除 UI 元素 | 添加/删除对应场景和步骤 | |
| 77 | +| 2 | 修改元素 id/class/选择器 | 更新蓝本中所有用到该选择器的 \`target\` | |
| 78 | +| 3 | 修改文本内容(按钮文字/提示/错误信息) | 更新 \`assert_text\` 的 \`expected\` | |
| 79 | +| 4 | 修改业务逻辑(表单验证/跳转/计算) | 更新断言和预期结果 | |
| 80 | +| 5 | 修复 Bug | 更新蓝本中对应断言,确保能检测到该Bug | |
| 81 | +| 6 | 修改应用配置(URL/端口/路由/启动命令) | 更新 \`base_url\` / \`start_command\` | |
| 82 | +
|
| 83 | +**不触发更新的情况**:纯CSS样式调整、代码注释修改、内部重构(不影响用户可见行为)、测试文件修改。 |
| 84 | +
|
| 85 | +--- |
| 86 | +
|
| 87 | +## 三、蓝本基本结构 |
| 88 | +
|
| 89 | +\`\`\`json |
| 90 | +{ |
| 91 | + "app_name": "应用名称", |
| 92 | + "description": "应用功能的完整描述(50-200字)", |
| 93 | + "base_url": "http://localhost:3000", |
| 94 | + "platform": "web", |
| 95 | + "start_command": "npm start", |
| 96 | + "start_cwd": ".", |
| 97 | + "pages": [ |
| 98 | + { |
| 99 | + "url": "/", |
| 100 | + "name": "首页", |
| 101 | + "scenarios": [ |
| 102 | + { |
| 103 | + "name": "场景名称", |
| 104 | + "description": "测试目标", |
| 105 | + "steps": [ |
| 106 | + {"action": "navigate", "value": "/", "description": "打开首页"}, |
| 107 | + {"action": "fill", "target": "#username", "value": "testuser", "description": "在用户名输入框输入"}, |
| 108 | + {"action": "click", "target": "#loginBtn", "description": "点击登录按钮"}, |
| 109 | + {"action": "assert_text", "expected": "欢迎", "description": "验证登录成功显示欢迎信息"}, |
| 110 | + {"action": "screenshot", "description": "登录成功后的页面"} |
| 111 | + ] |
| 112 | + } |
| 113 | + ] |
| 114 | + } |
| 115 | + ] |
| 116 | +} |
| 117 | +\`\`\` |
| 118 | +
|
| 119 | +### platform 取值 |
| 120 | +
|
| 121 | +| 值 | 适用场景 | |
| 122 | +|----|---------| |
| 123 | +| \`web\` | 网页应用(React/Vue/Angular/纯HTML) | |
| 124 | +| \`desktop\` | Windows桌面应用(需额外填 \`window_title\`) | |
| 125 | +| \`android\` | Android应用(需额外填 \`app_package\`、\`app_activity\`) | |
| 126 | +| \`ios\` | iOS应用(需额外填 \`bundle_id\`) | |
| 127 | +| \`miniprogram\` | 微信小程序(\`base_url\` 格式为 \`miniprogram://项目路径\`) | |
| 128 | +
|
| 129 | +### 步骤动作 |
| 130 | +
|
| 131 | +| 动作 | 必填参数 | 说明 | |
| 132 | +|------|---------|------| |
| 133 | +| \`click\` | \`target\`, \`description\` | 点击元素 | |
| 134 | +| \`fill\` | \`target\`, \`value\`, \`description\` | 输入文本 | |
| 135 | +| \`screenshot\` | \`description\` | 截图 | |
| 136 | +| \`assert_text\` | \`expected\`, \`description\` | 断言页面包含文本 | |
| 137 | +| \`wait\` | \`description\` | 等待(可用 \`value\` 指定毫秒) | |
| 138 | +| \`navigate\` | \`value\`(URL), \`description\` | 页面跳转 | |
| 139 | +
|
| 140 | +### target 写法 |
| 141 | +
|
| 142 | +- **Web/小程序**:CSS选择器,如 \`#loginBtn\`、\`.submit-btn\`、\`input[name="email"]\` |
| 143 | +- **桌面应用**:\`name:屏幕上可见的原文\`,如 \`name:Login\`、\`name:确定\` |
| 144 | + - ⚠️ **禁止**在 \`name:\` 后加中文后缀(按钮/输入框/列表项等) |
| 145 | +- **Android/iOS**:\`accessibility_id:xxx\` 或 \`id:xxx\` |
| 146 | +
|
| 147 | +### description 最佳实践 |
| 148 | +
|
| 149 | +每个步骤的 \`description\` 应包含: |
| 150 | +1. **位置**:上方/下方/左侧/右侧 |
| 151 | +2. **预期变化**:点击后页面会发生什么(编程AI已读过源码,应预测页面变化) |
| 152 | +
|
| 153 | +\`\`\` |
| 154 | +✅ "点击提交按钮,点击后表单数据提交到后端,页面显示'提交成功'提示" |
| 155 | +❌ "点击按钮" |
| 156 | +\`\`\` |
| 157 | +
|
| 158 | +--- |
| 159 | +
|
| 160 | +## 四、边写代码边维护蓝本(推荐工作流) |
| 161 | +
|
| 162 | +**最佳实践**:不要等项目全部写完才生成蓝本,而是**每实现一个功能就追加一个场景**。 |
| 163 | +
|
| 164 | +\`\`\` |
| 165 | +实现登录功能 → 立即在蓝本中添加"登录成功"和"登录失败"两个场景 |
| 166 | +实现商品列表 → 立即添加"商品展示"和"搜索过滤"场景 |
| 167 | +实现购物车 → 立即添加"加入购物车"和"修改数量"和"删除商品"场景 |
| 168 | +\`\`\` |
| 169 | +
|
| 170 | +这样做的好处: |
| 171 | +- 功能不会遗漏(写一个测一个) |
| 172 | +- 蓝本的选择器和预期值一定是准确的(刚写完代码,记忆最清晰) |
| 173 | +- 用户随时可以跑测试验证 |
| 174 | +
|
| 175 | +--- |
| 176 | +
|
| 177 | +## 五、蓝本自检清单 |
| 178 | +
|
| 179 | +生成或修改蓝本后,逐项检查: |
| 180 | +
|
| 181 | +- [ ] 所有用户可交互功能都有对应场景 |
| 182 | +- [ ] \`target\` 选择器与实际代码中的 id/class 一致 |
| 183 | +- [ ] \`expected\` 是界面上实际会显示的文字(不是描述性文字) |
| 184 | +- [ ] 页面切换后有 \`wait\` 步骤 |
| 185 | +- [ ] 场景间状态连贯(后一个场景基于前一个的结束状态) |
| 186 | +- [ ] \`platform\` 字段正确 |
| 187 | +- [ ] \`base_url\` 和 \`start_command\` 填写正确 |
| 188 | +`; |
| 189 | +} |
| 190 | + |
| 191 | +/** |
| 192 | + * 在指定工作区目录中注入规则文件 |
| 193 | + * @param workspaceRoot 工作区根目录绝对路径 |
| 194 | + * @param outputChannel 日志输出通道 |
| 195 | + * @returns 注入结果 { created: string[], skipped: string[] } |
| 196 | + */ |
| 197 | +export function injectRules( |
| 198 | + workspaceRoot: string, |
| 199 | + outputChannel?: vscode.OutputChannel, |
| 200 | +): { created: string[]; skipped: string[] } { |
| 201 | + const template = getTemplateContent(); |
| 202 | + const created: string[] = []; |
| 203 | + const skipped: string[] = []; |
| 204 | + |
| 205 | + for (const relPath of RULES_FILES) { |
| 206 | + const fullPath = path.join(workspaceRoot, relPath); |
| 207 | + |
| 208 | + // 已存在则跳过(不覆盖用户自定义内容) |
| 209 | + if (fs.existsSync(fullPath)) { |
| 210 | + skipped.push(relPath); |
| 211 | + continue; |
| 212 | + } |
| 213 | + |
| 214 | + try { |
| 215 | + // 创建目录 |
| 216 | + const dir = path.dirname(fullPath); |
| 217 | + if (!fs.existsSync(dir)) { |
| 218 | + fs.mkdirSync(dir, { recursive: true }); |
| 219 | + } |
| 220 | + |
| 221 | + // 写入规则文件 |
| 222 | + fs.writeFileSync(fullPath, template, "utf-8"); |
| 223 | + created.push(relPath); |
| 224 | + } catch (err) { |
| 225 | + const msg = err instanceof Error ? err.message : String(err); |
| 226 | + outputChannel?.appendLine(`[TestPilot AI] ⚠️ 创建 ${relPath} 失败: ${msg}`); |
| 227 | + } |
| 228 | + } |
| 229 | + |
| 230 | + if (created.length > 0) { |
| 231 | + outputChannel?.appendLine( |
| 232 | + `[TestPilot AI] ✅ 已注入 ${created.length} 个规则文件: ${created.join(", ")}`, |
| 233 | + ); |
| 234 | + } |
| 235 | + if (skipped.length > 0) { |
| 236 | + outputChannel?.appendLine( |
| 237 | + `[TestPilot AI] ⏭️ 跳过 ${skipped.length} 个已存在文件: ${skipped.join(", ")}`, |
| 238 | + ); |
| 239 | + } |
| 240 | + |
| 241 | + return { created, skipped }; |
| 242 | +} |
| 243 | + |
| 244 | +/** |
| 245 | + * 检查工作区是否需要注入规则(没有任何规则文件时才需要) |
| 246 | + */ |
| 247 | +export function needsInjection(workspaceRoot: string): boolean { |
| 248 | + return !RULES_FILES.some((relPath) => |
| 249 | + fs.existsSync(path.join(workspaceRoot, relPath)), |
| 250 | + ); |
| 251 | +} |
| 252 | + |
| 253 | +/** |
| 254 | + * 插件激活时自动注入规则到所有工作区 |
| 255 | + * 只在首次(工作区没有任何规则文件时)自动注入,不会重复打扰用户 |
| 256 | + */ |
| 257 | +export async function autoInjectOnActivate( |
| 258 | + outputChannel?: vscode.OutputChannel, |
| 259 | +): Promise<void> { |
| 260 | + const folders = vscode.workspace.workspaceFolders; |
| 261 | + if (!folders) { return; } |
| 262 | + |
| 263 | + for (const folder of folders) { |
| 264 | + const root = folder.uri.fsPath; |
| 265 | + |
| 266 | + // 跳过 TestPilotAI 项目本身(我们自己的项目已有规则) |
| 267 | + if (fs.existsSync(path.join(root, "cli.py")) && fs.existsSync(path.join(root, "src", "app.py"))) { |
| 268 | + outputChannel?.appendLine(`[TestPilot AI] 跳过 TestPilotAI 项目本身: ${root}`); |
| 269 | + continue; |
| 270 | + } |
| 271 | + |
| 272 | + // 检查是否需要注入 |
| 273 | + if (needsInjection(root)) { |
| 274 | + outputChannel?.appendLine(`[TestPilot AI] 检测到 ${folder.name} 没有蓝本规则,自动注入中...`); |
| 275 | + const result = injectRules(root, outputChannel); |
| 276 | + |
| 277 | + if (result.created.length > 0) { |
| 278 | + vscode.window.showInformationMessage( |
| 279 | + `TestPilot AI: 已为 ${folder.name} 注入 ${result.created.length} 个编程AI规则文件,编程AI将自动生成测试蓝本`, |
| 280 | + "查看详情", |
| 281 | + ).then((action) => { |
| 282 | + if (action === "查看详情") { |
| 283 | + // 打开 AGENTS.md 让用户看看注入了什么 |
| 284 | + const agentsPath = path.join(root, "AGENTS.md"); |
| 285 | + if (fs.existsSync(agentsPath)) { |
| 286 | + vscode.workspace.openTextDocument(agentsPath).then((doc) => { |
| 287 | + vscode.window.showTextDocument(doc, { preview: true }); |
| 288 | + }); |
| 289 | + } |
| 290 | + } |
| 291 | + }); |
| 292 | + } |
| 293 | + } else { |
| 294 | + outputChannel?.appendLine(`[TestPilot AI] ${folder.name} 已有规则文件,跳过注入`); |
| 295 | + } |
| 296 | + } |
| 297 | +} |
0 commit comments