digital-intern 1.0.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/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # digital-intern
2
+
3
+ 一键把 digital-intern 的自动化开发/测试提示词,安装到主流 AI 编程工具的命令目录下。
4
+
5
+ ## 使用
6
+
7
+ ```bash
8
+ npx digital-intern@latest init # 交互式选择工具
9
+ npx digital-intern@latest init claude # 只装到 .claude/
10
+ npx digital-intern@latest init claude cursor # 同时装多个
11
+ npx digital-intern@latest init --all # 装到所有支持的工具
12
+ ```
13
+
14
+ ## 支持的工具
15
+
16
+ 工具的"命令系统"实现方式并不统一,所以分成两档:
17
+
18
+ ### ✅ 完全兼容(目录名不同,文件格式相同,转换 100% 可靠)
19
+
20
+ | 工具参数 | 安装目录 |
21
+ |----------|----------|
22
+ | claude | `.claude/commands/` |
23
+ | codebuddy | `.codebuddy/commands/` |
24
+ | cursor | `.cursor/commands/` |
25
+ | trae | `.trae/commands/` |
26
+ | qwen | `.qwen/commands/`(参数占位符 `{{args}}`) |
27
+
28
+ ### ⚠️ 格式转换(自动转换后可用,但和源工具的格式差异较大,建议装完打开看一眼)
29
+
30
+ | 工具参数 | 安装目录 | 差异点 |
31
+ |----------|----------|--------|
32
+ | gemini | `.gemini/commands/*.toml` | Gemini CLI 用 TOML 而不是 Markdown |
33
+ | copilot | `.github/prompts/*.prompt.md` | 文件名后缀、frontmatter 字段、参数语法(`${input:arguments}`)都不同 |
34
+ | windsurf | `.windsurf/workflows/*.md` | 目录叫 workflows 不叫 commands,且不支持 frontmatter / 参数占位符 |
35
+
36
+ ### 不支持
37
+
38
+ - **iFlow CLI**:已于 2026 年 4 月 17 日官方停止服务,不再适配。
39
+
40
+ ## 模板编写规则(重要)
41
+
42
+ `templates/commands/*.md` 里,两个地方不要写死:
43
+
44
+ 1. 用户输入 —— 写 `$ARGUMENTS`(各工具的语法差异由 CLI 自动转换)
45
+ 2. 指向"自己所在工具目录"的路径 —— 写 `{{TOOL_DIR}}`,比如
46
+ `` `{{TOOL_DIR}}/prompts/xxx.md` ``,装到 `.codebuddy` 时会自动变成
47
+ `.codebuddy/prompts/xxx.md`。
48
+
49
+ `templates/prompts/*.md` 是被 commands 按需读取的规则文件,不需要 frontmatter,
50
+ 原样复制到每个工具的 `<工具目录>/prompts/` 下即可。
51
+
52
+ ## 新增工具支持
53
+
54
+ 编辑 `tools.config.js`:
55
+
56
+ - 如果新工具的命令格式和 Claude Code 一样(`commands/*.md` + frontmatter + `$ARGUMENTS`),
57
+ 只需加一行 `family: "native"`。
58
+ - 如果只是参数占位符语法不同,用 `family: "args-only"` + `argPlaceholder`。
59
+ - 如果格式差异更大(比如另一种文件格式),在 `bin/renderers.js` 里加一个新的渲染函数,
60
+ 参考 `toml` / `copilot` / `windsurf` 的写法。
61
+
62
+ ```js
63
+ export const TOOLS = {
64
+ // ...已有工具
65
+ aider: { label: "Aider", dir: ".aider", family: "native" }, // 示例
66
+ };
67
+ ```
68
+
69
+ ## 发布
70
+
71
+ ```bash
72
+ npm login
73
+ npm publish --access public
74
+ ```
package/bin/cli.js ADDED
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env node
2
+ import fs from "fs";
3
+ import os from "os";
4
+ import path from "path";
5
+ import readline from "readline";
6
+ import { fileURLToPath } from "url";
7
+ import { TOOLS, DEFAULT_TOOL, EXPERIMENTAL_FAMILIES } from "../tools.config.js";
8
+ import { render } from "./renderers.js";
9
+
10
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
+ const TEMPLATES_DIR = path.join(__dirname, "..", "templates");
12
+
13
+ const [, , command, ...rest] = process.argv;
14
+
15
+ // ---------- 工具函数 ----------
16
+
17
+ function rootDir(tool) {
18
+ return tool.scope === "user" ? os.homedir() : process.cwd();
19
+ }
20
+
21
+ function installDir(tool, kind) {
22
+ // kind: "commands" | "prompts"
23
+ const subdir = kind === "commands" ? tool.commandsSubdir || "commands" : tool.promptsSubdir || "prompts";
24
+ return path.join(rootDir(tool), tool.dir, subdir);
25
+ }
26
+
27
+ function displayPath(tool, kind) {
28
+ const subdir = kind === "commands" ? tool.commandsSubdir || "commands" : tool.promptsSubdir || "prompts";
29
+ const prefix = tool.scope === "user" ? "~" : ".";
30
+ return `${prefix}/${tool.dir}/${subdir}`;
31
+ }
32
+
33
+ function copyKind(srcDir, tool, kind) {
34
+ if (!fs.existsSync(srcDir)) return;
35
+ const targetDir = installDir(tool, kind);
36
+ fs.mkdirSync(targetDir, { recursive: true });
37
+
38
+ for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
39
+ if (!entry.isFile()) continue; // 模板目前是平铺的,不处理子目录
40
+ const raw = fs.readFileSync(path.join(srcDir, entry.name), "utf8");
41
+ const { fileName, content } = render(tool.family, entry.name, raw, tool, kind);
42
+ fs.writeFileSync(path.join(targetDir, fileName), content, "utf8");
43
+ }
44
+ }
45
+
46
+ function askQuestion(query) {
47
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
48
+ return new Promise((resolve) => rl.question(query, (answer) => { rl.close(); resolve(answer.trim()); }));
49
+ }
50
+
51
+ async function resolveTools(args) {
52
+ if (args.includes("--all")) return Object.keys(TOOLS);
53
+
54
+ const named = args.filter((a) => !a.startsWith("--") && TOOLS[a]);
55
+ const unknown = args.filter((a) => !a.startsWith("--") && !TOOLS[a]);
56
+
57
+ if (unknown.length > 0) {
58
+ console.log(`⚠️ 未识别的工具参数:${unknown.join(", ")}`);
59
+ console.log(` 当前支持:${Object.keys(TOOLS).join(", ")}`);
60
+ }
61
+ if (named.length > 0) return named;
62
+
63
+ if (!process.stdin.isTTY) {
64
+ console.log(`未指定工具,默认安装到 ${TOOLS[DEFAULT_TOOL].dir}(${TOOLS[DEFAULT_TOOL].label})`);
65
+ return [DEFAULT_TOOL];
66
+ }
67
+
68
+ const keys = Object.keys(TOOLS);
69
+ const list = keys
70
+ .map((key, i) => {
71
+ const tool = TOOLS[key];
72
+ const tags = [
73
+ EXPERIMENTAL_FAMILIES.has(tool.family) ? "格式转换" : null,
74
+ tool.scope === "user" ? "装到用户目录" : null,
75
+ ].filter(Boolean);
76
+ const tag = tags.length ? `(${tags.join(",")})` : "";
77
+ return ` ${i + 1}. ${tool.label} (${key}) ${tag}`;
78
+ })
79
+ .join("\n");
80
+ console.log(`请选择要安装的工具(可用空格分隔多个编号,直接回车 = 默认 ${DEFAULT_TOOL}):\n${list}`);
81
+ const answer = await askQuestion("> ");
82
+ if (!answer) return [DEFAULT_TOOL];
83
+
84
+ const picked = answer.split(/[\s,]+/).map((s) => keys[Number(s) - 1]).filter(Boolean);
85
+ return picked.length > 0 ? picked : [DEFAULT_TOOL];
86
+ }
87
+
88
+ // ---------- 主逻辑 ----------
89
+
90
+ async function init(args) {
91
+ const selectedTools = await resolveTools(args);
92
+
93
+ for (const toolKey of selectedTools) {
94
+ const tool = TOOLS[toolKey];
95
+ if (!tool) continue;
96
+
97
+ copyKind(path.join(TEMPLATES_DIR, "commands"), tool, "commands");
98
+ copyKind(path.join(TEMPLATES_DIR, "prompts"), tool, "prompts");
99
+
100
+ const cmdPath = displayPath(tool, "commands");
101
+ console.log(`✅ ${tool.label} -> 已安装到 ${cmdPath}`);
102
+ if (tool.note) {
103
+ console.log(` ℹ️ ${tool.note}`);
104
+ } else if (EXPERIMENTAL_FAMILIES.has(tool.family)) {
105
+ console.log(` ⚠️ 该工具的格式与源模板不同,内容已自动转换,建议打开确认一下`);
106
+ }
107
+ }
108
+ }
109
+
110
+ function printHelp() {
111
+ console.log(`
112
+ 用法:
113
+ npx digital-intern@latest init [tool...] [--all]
114
+
115
+ 支持的工具:
116
+ ${Object.entries(TOOLS)
117
+ .map(([k, v]) => ` ${k.padEnd(10)} -> ${v.scope === "user" ? "~" : "."}/${v.dir}`)
118
+ .join("\n")}
119
+
120
+ 示例:
121
+ npx digital-intern@latest init # 交互式选择
122
+ npx digital-intern@latest init claude
123
+ npx digital-intern@latest init claude cursor qwen
124
+ npx digital-intern@latest init --all
125
+ `);
126
+ }
127
+
128
+ if (command === "init") {
129
+ await init(rest);
130
+ } else {
131
+ printHelp();
132
+ }
@@ -0,0 +1,113 @@
1
+ // 把 templates/ 下统一格式的 markdown(YAML frontmatter + 正文,
2
+ // 正文里用 $ARGUMENTS 表示用户输入、{{TOOL_DIR}} 表示"当前工具的安装根目录"、
3
+ // {{PROMPTS_DIR}} 表示"当前工具存放规则文件的目录")转换成各工具实际需要的文件。
4
+
5
+ const ARG_TOKEN = "$ARGUMENTS";
6
+ const DIR_TOKEN = "{{TOOL_DIR}}";
7
+ const PROMPTS_TOKEN = "{{PROMPTS_DIR}}";
8
+
9
+ export function parseFrontmatter(content) {
10
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
11
+ if (!match) return { meta: {}, body: content };
12
+ const meta = {};
13
+ for (const line of match[1].split(/\r?\n/)) {
14
+ const idx = line.indexOf(":");
15
+ if (idx === -1) continue;
16
+ meta[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
17
+ }
18
+ return { meta, body: match[2] };
19
+ }
20
+
21
+ function promptsDirOf(tool) {
22
+ return `${tool.dir}/${tool.promptsSubdir || "prompts"}`;
23
+ }
24
+
25
+ function replacePathTokens(text, tool) {
26
+ return text.split(DIR_TOKEN).join(tool.dir).split(PROMPTS_TOKEN).join(promptsDirOf(tool));
27
+ }
28
+
29
+ function replaceArgToken(text, tool) {
30
+ if (!tool.argPlaceholder) return text;
31
+ return text.split(ARG_TOKEN).join(tool.argPlaceholder);
32
+ }
33
+
34
+ function tomlEscapeBasicString(str) {
35
+ return str.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
36
+ }
37
+
38
+ function tomlEscapeMultiline(str) {
39
+ // 避免正文中偶然出现的 """ 提前结束多行字符串
40
+ return str.replace(/"""/g, '\\"\\"\\"');
41
+ }
42
+
43
+ // ---------- 各 family 的渲染器 ----------
44
+ // 每个渲染器接收 (fileName, content, tool, kind),返回 { fileName, content }
45
+ // kind: "commands" | "prompts"
46
+
47
+ const renderers = {
48
+ // Claude Code / CodeBuddy / Cursor / Trae:格式完全一致,只换目录 token
49
+ native(fileName, content, tool) {
50
+ return { fileName, content: replacePathTokens(content, tool) };
51
+ },
52
+
53
+ // Qwen Code:格式一致,只是 $ARGUMENTS 语法不同
54
+ "args-only"(fileName, content, tool) {
55
+ let out = replacePathTokens(content, tool);
56
+ out = replaceArgToken(out, tool);
57
+ return { fileName, content: out };
58
+ },
59
+
60
+ // Gemini CLI:commands 需要转成 .toml;规则文件保持 .md 不变
61
+ toml(fileName, content, tool, kind) {
62
+ if (kind !== "commands") {
63
+ return { fileName, content: replacePathTokens(content, tool) };
64
+ }
65
+ const { meta, body } = parseFrontmatter(content);
66
+ let promptBody = replacePathTokens(body, tool);
67
+ promptBody = replaceArgToken(promptBody, tool).trim();
68
+ const description = meta.description || "";
69
+ const lines = [];
70
+ if (description) {
71
+ lines.push(`description = "${tomlEscapeBasicString(description)}"`);
72
+ }
73
+ lines.push(`prompt = """`);
74
+ lines.push(tomlEscapeMultiline(promptBody));
75
+ lines.push(`"""`);
76
+ const newName = fileName.replace(/\.md$/, ".toml");
77
+ return { fileName: newName, content: lines.join("\n") + "\n" };
78
+ },
79
+
80
+ // GitHub Copilot:commands -> .prompt.md,frontmatter 精简,$ARGUMENTS -> ${input:arguments}
81
+ copilot(fileName, content, tool, kind) {
82
+ if (kind !== "commands") {
83
+ return { fileName, content: replacePathTokens(content, tool) };
84
+ }
85
+ const { meta, body } = parseFrontmatter(content);
86
+ let promptBody = replacePathTokens(body, tool);
87
+ promptBody = replaceArgToken(promptBody, tool);
88
+ const frontmatterLines = ["---", `description: ${meta.description || ""}`, "agent: 'agent'", "---"];
89
+ const newName = fileName.replace(/\.md$/, ".prompt.md");
90
+ return {
91
+ fileName: newName,
92
+ content: frontmatterLines.join("\n") + "\n\n" + promptBody.trim() + "\n",
93
+ };
94
+ },
95
+
96
+ // Windsurf:不支持 frontmatter / 参数占位符,直接剥离 frontmatter,
97
+ // $ARGUMENTS 换成一句人类可读的提示
98
+ windsurf(fileName, content, tool, kind) {
99
+ if (kind !== "commands") {
100
+ return { fileName, content: replacePathTokens(content, tool) };
101
+ }
102
+ const { meta, body } = parseFrontmatter(content);
103
+ let out = replacePathTokens(body, tool);
104
+ out = out.split(ARG_TOKEN).join("[在触发本工作流时,把你的具体需求写在这里]");
105
+ const title = meta.description ? `# ${meta.description}\n\n` : "";
106
+ return { fileName, content: title + out.trim() + "\n" };
107
+ },
108
+ };
109
+
110
+ export function render(family, fileName, content, tool, kind) {
111
+ const fn = renderers[family] || renderers.native;
112
+ return fn(fileName, content, tool, kind);
113
+ }
package/package.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "digital-intern",
3
+ "version": "1.0.0",
4
+ "description": "一键将 digital-intern 的自动化开发/测试提示词安装到 .claude / .codebuddy 等目录",
5
+ "type": "module",
6
+ "bin": {
7
+ "digital-intern": "./bin/cli.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "templates",
12
+ "tools.config.js"
13
+ ],
14
+ "license": "MIT"
15
+ }
@@ -0,0 +1,74 @@
1
+ ---
2
+ description: 严格按TDD(红-绿-重构)方式,根据需求编号/内容/卡片路径驱动实现
3
+ argument-hint: <需求编号 | 需求内容 | 需求卡片路径>
4
+ ---
5
+
6
+ # 开发工程师
7
+
8
+ 你是一名严格遵循测试驱动开发(TDD)的资深工程师。用户输入是:
9
+
10
+ ```
11
+ $ARGUMENTS
12
+ ```
13
+
14
+ 你的任务是:先在代码仓库中定位/解析出这条需求,再把它的验收标准(AC)与实例化示例,逐条转化为自动化测试并驱动出实现代码。**不能跳过红灯阶段,也不能一次性写完所有代码。**
15
+
16
+ ## 第 0 阶段:需求定位与解析(先做,不计入TDD循环)
17
+
18
+ 根据 `$ARGUMENTS` 的形式分三种情况处理:
19
+
20
+ 1. **看起来是路径**(包含 `/`、以 `.md` / `.feature` 等结尾,或用户明确说"路径")
21
+ → 直接读取该文件内容。找不到文件就如实报告路径不存在,不要臆造内容。
22
+
23
+ 2. **看起来是需求编号**(如 `STORY-123`、`PROJ-456`、纯编号等)
24
+ → 在仓库内搜索匹配项,按优先级尝试常见目录/位置:`docs/stories/`、`stories/`、`requirements/`、`features/`、`user_stories`、项目管理工具导出文件等,用编号做全文/文件名检索。
25
+ → 找到多个候选时列出来源路径请用户确认,不要自行猜测选一个。
26
+ → 一个都没找到:明确说明"未在仓库中找到该编号对应的需求卡片",列出你搜索过的位置,然后停下来问用户需求内容或正确路径,**不要虚构需求内容**。
27
+
28
+ 3. **既不是路径也不是编号,是一段完整文本**
29
+ → 视为直接粘贴的需求描述,直接使用。
30
+
31
+ 解析出需求后,检查是否已经包含结构化的 **AC(验收标准)** 和 **实例化示例表格**(Given-When-Then + 具体数据):
32
+ - 如果已经有,直接采用,不做改写、不做泛化。
33
+ - 如果 AC 是自然语言、缺少实例化示例,先基于 AC 补全出具体数据的 Given-When-Then 示例表格,并在每一条补全数据旁标注 `TODO: AI 草拟,待与 PO 核实`。**不要把补全的数据当成最终业务需求擅自扩展范围。**
34
+
35
+ 同时探测项目技术信息(不要向用户索要,自己在仓库里找):
36
+ - 编程语言、测试框架:查 `package.json` / `pyproject.toml` / `requirements.txt` / `pom.xml` / `go.mod` / `Cargo.toml` 等
37
+ - 项目结构约定:现有测试目录、命名规范、既有测试的写法
38
+ - 已有相关代码位置:与本需求相关的现有模块/类/接口
39
+
40
+ 如果依赖的前置 Story(如后端骨架、种子数据)尚未就绪,先说明缺口,不要臆造依赖项的实现。
41
+
42
+ 完成第 0 阶段后,向用户简要汇报:找到的需求来源、识别到的技术栈、AC 数量、实例化示例数量(含几条是 AI 草拟待核实的),再进入 TDD 循环。
43
+
44
+ ## 拆解与排期规则
45
+
46
+ - 将每一条"实例化示例"视为一个独立的红-绿-重构循环单元;每次只选择一个尚未通过的示例作为本轮目标,禁止一次性把多个示例的测试都写出来。
47
+ - 选择顺序:先覆盖 AC 中最核心、最简单的正向路径,再覆盖边界场景、异常场景和防护性场景(例如"防枚举""统一错误提示"这类反例)。
48
+ - 测试断言必须直接来自 AC 的 Then 描述与实例化示例中的具体数据(用户名、密码、提示文案等),不得泛化、简化或替换为近似断言。
49
+ - 涉及安全或隐私类要求(如"不区分、不透露具体是哪一项错误")必须有专门的反例测试来验证——即验证"没有做某件事",而不仅验证"做对了某件事"。
50
+ - 标记为 `TODO: 待PO确认` 的数据,测试中可以直接使用,但保留该标记,不要当成最终业务需求擅自扩展。
51
+
52
+ ## TDD 循环:三个阶段
53
+
54
+ 每次代码更改都属于以下三个阶段之一,按顺序进行。运行任何测试之后,都要展示完整测试结果。
55
+
56
+ ### 1. 新增测试
57
+ 本阶段只新增/修改测试代码,目标是得到一个**失败信息足以说明代码行为不符合预期**的红灯。这一步绝不能改动生产代码。改完测试可直接运行,无需确认。
58
+
59
+ ### 2. 增加功能
60
+ 修改实现代码,使新测试和所有已有测试全部通过。必须获取真实测试运行结果并确认通过。**只写能让测试通过的最少实现代码**,不要顺带实现测试还没要求的行为。改完可直接运行测试并确认通过。
61
+
62
+ ### 3. 重构
63
+ 在不改变代码行为的前提下消除代码异味,主要做:
64
+ - 消除重复代码
65
+ - 消除"特性嫉妒",把函数移到拥有其所需数据最多的类中去
66
+
67
+ 重构过程中不能破坏现有测试;每次改动后都要运行测试确认通过。重构完成后运行**全部**测试,不要只做构建。
68
+
69
+ ## 沟通与展示要求
70
+
71
+ - 每次动作前,先声明:"当前处于【新增测试/增加功能/重构】阶段,本轮目标是:xxx"。
72
+ - 每次运行测试后,完整展示测试运行结果(用例名称、通过/失败状态),不得只汇总"N 个通过、M 个失败"。
73
+ - 严格增量推进:一次只推进一个示例的一个阶段,完成后再询问或自行推进下一个。
74
+ - 全部 AC 与实例化示例都有对应测试并通过后,做一次全量测试运行作为收尾确认,并总结本 Story 的测试覆盖情况(哪些示例覆盖了正向路径、哪些是防护性反例、哪些数据项仍待 PO 确认)。
@@ -0,0 +1,56 @@
1
+ ---
2
+ description: 需求澄清访谈官 —— 通过结构化提问,把一句话需求打磨成可直接开发的规格说明
3
+ argument-hint: [一句话需求,例如:我想做个电商网站]
4
+ ---
5
+
6
+ 你现在是我的"需求澄清访谈官"。在我说出一句话需求之后,
7
+ 你不能直接开始写代码或输出技术方案,必须先通过结构化提问,
8
+ 把需求打磨成一份开发者可以直接拿去开发的规格说明,并导出为 .md 文件,保存在当前项目目录下。
9
+
10
+ 请严格遵守以下规则:
11
+
12
+ 【一次只问一个问题】
13
+ 每次只问我一个问题,等我回答后,再基于我的回答提出下一个问题。
14
+ 不要一次性抛出多个问题清单。
15
+
16
+ 【必须覆盖的四个要素】
17
+ 在结束访谈前,你必须帮我确认清楚以下四点,缺一不可:
18
+ 1. [角色] 这个功能主要给谁用?
19
+ 2. [行为] 用户具体会做什么操作?
20
+ 3. [目标] 我们希望通过这个功能达成什么效果?
21
+ 4. [验收标准] 怎样才算"做完了"?
22
+
23
+ 【每个问题都要给出你的专业判断,而不是甩问题给我】
24
+ 遇到需要我做选择的地方,不要只抛问题,
25
+ 请先给出2-3个可选方案,用一个小表格列出每个方案的优点和缺点,
26
+ 并明确给出你的推荐方案和理由,最后让我确认或提出修改。
27
+
28
+ 格式示例:
29
+ 问题:XXX?
30
+ 选项:
31
+ | 方案 | 优点 | 缺点 |
32
+ |---|---|---|
33
+ | A | ... | ... |
34
+ | B | ... | ... |
35
+ 推荐:✅ 方案 X,理由是……
36
+ 你的选择是?(直接回复 A / B / 或你的想法)
37
+
38
+ 【验收标准三问自检】
39
+ 在最终确认验收标准之前,请自己先检查一遍,确保每一条验收标准都满足:
40
+ 1. 是否是具体动作?(而不是"好用""正常""基本完成"这类模糊词)
41
+ 2. 是否有可观察的结果?
42
+ 3. 不懂代码的人能否直接判断是否达标?
43
+ 如果不满足,必须重新追问,不能替我"脑补"答案。
44
+
45
+ 【结束条件】
46
+ 当四要素都已确认清楚,且验收标准通过三问自检后,
47
+ 请输出一份完整的《需求规格摘要》,包含:
48
+ - 背景摘要
49
+ - 已确认的决策清单(每条决策及最终选择)
50
+ - 最终验收标准列表
51
+ 并明确告诉我:"需求已澄清完毕,可以进入方案设计阶段"。
52
+
53
+ 在这之前,你绝对不能开始写任何代码或给出技术架构方案。
54
+
55
+ ---
56
+ 我的一句话需求是:$ARGUMENTS
@@ -0,0 +1,83 @@
1
+ ---
2
+ description: 支持三种用法——(1) 生成 API 层 .feature (2) 生成 UI 层 .feature (3) 基于已有 .feature 文件生成对应的自动化测试代码
3
+ argument-hint: <需求编号/内容/卡片路径> 生成 api/ui 测试用例 | 请帮我为 feature <feature文件路径> 生成测试代码
4
+ ---
5
+
6
+ # /test —— User Story → .feature → 测试代码
7
+
8
+ 你现在要扮演一名资深 QA 自动化工程师。用户传入的参数是:
9
+
10
+ ```
11
+ $ARGUMENTS
12
+ ```
13
+
14
+ 本命令本身只负责**判断意图、定位输入、决定加载哪份规则文件**;具体的生成规则全部在下面提到的独立规则文件里,只在对应模式下才读取,不要提前加载不需要的规则文件,避免不必要的上下文占用。
15
+
16
+ ## 0. 先判断本次意图
17
+
18
+ - 若 `$ARGUMENTS` 中出现"feature"字样 + 一个文件路径(或明显指向某个已存在 `.feature` 文件),并且提到"生成测试代码"、"生成自动化代码"、"实现代码"、"step definition"等词 → **模式 C**,跳到本文件第三部分。
19
+ - 否则视为**模式 A/B(从需求生成 feature 文件)**,继续走第一部分,并在其中判断是 API 层还是 UI 层。
20
+ - 两种意图的关键词都出现,或都没出现、无法判断 → 向用户确认到底要"生成 feature 文件"还是"基于已有 feature 生成测试代码",不要自行猜测。
21
+
22
+ ---
23
+
24
+ # 第一部分:从需求生成 .feature 文件(模式 A / B)
25
+
26
+ ## 1. 确定目标层 + 定位需求卡片
27
+
28
+ **先定层**:
29
+ - 出现"api"、"接口"、"后端测试"等关键词 → API 层
30
+ - 出现"ui"、"e2e"、"端到端"、"前端测试"、"页面测试"等关键词 → UI 层
31
+ - 两层都提到,或完全没提 → 向用户确认要生成哪一层,不要自行猜测默认哪个
32
+
33
+ **再定位卡片**:
34
+ - 若参数中包含类似 `FT-01-US-02` 这种编号格式,在仓库中用 grep/glob 搜索包含该编号的 `.md` 文件(常见目录:`docs/`、`stories/`、`requirements/`、`specs/`,具体以本仓库实际结构为准,先用 `find`/`grep -r` 探一遍)。
35
+ - 若参数中包含一个存在的文件路径,直接读取该文件作为卡片。
36
+ - 若以上都不匹配,把描述性文字当作需求内容/模糊描述:先尝试用关键词在仓库全文检索定位卡片;找不到时直接以用户输入文本作为卡片内容继续,但要在最终输出中注明"未在仓库中找到对应卡片文件,以用户输入内容为准"。
37
+ - 命中多个候选卡片时列出候选,向用户确认,不要自行臆断。
38
+
39
+ ## 2. 加载规则并生成
40
+
41
+ 按第 1 步确定的层,依次读取:
42
+ 1. 通用规则:`{{PROMPTS_DIR}}/automation-test-base.md`
43
+ 2. 对应层 profile:`{{PROMPTS_DIR}}/automation-test-api-profile.md`(API 层)或 `{{PROMPTS_DIR}}/automation-test-ui-profile.md`(UI 层)
44
+
45
+ 只读取本次用得到的那一份 profile,不要两份都读。找不到规则文件时尝试 `find . -iname "automation-test-*"`;仍找不到就明确告知用户缺少规则文件,不要凭空编造规则继续。
46
+
47
+ 读到规则后,严格按其中的信源优先级判断、header 规范、Tag 体系、Scenario 组织方式、命名规则等逐条执行,本命令不重复这些细节。执行时额外做两件规则文件里提到但需要你主动去查的事:
48
+ - 判断代码已实现/未实现(API 层查后端 controller/service/DTO;UI 层查前端组件/路由/种子数据文件)
49
+ - 在仓库测试目录中查找同一 Story 编号的**另一层** `.feature` 文件,复用已认可的实例化数据(API 找 UI 文件、UI 找 API 文件)
50
+
51
+ 生成完成后按规则文件里约定的文件名(`<StoryID>-<kebab-name>_api.feature` / `<StoryID>-<kebab-name>.feature`)保存;输出路径优先复用仓库已有测试目录约定,找不到约定就先问用户。
52
+
53
+ ## 收尾汇总(模式 A/B)
54
+ 汇报:生成的层、卡片来源、信源模式(代码已实现/草稿)、是否复用了姊妹层数据、生成文件路径、所有 `# 待人工确认:` 清单。
55
+
56
+ ---
57
+
58
+ # 第二部分:基于已有 .feature 生成测试代码(模式 C)
59
+
60
+ ## 3. 定位目标 feature 文件
61
+
62
+ - 参数中给出了具体路径 → 直接读取该文件。
63
+ - 只给了 Story 编号/模糊描述、没给路径 → 在仓库测试目录中搜索匹配的 `.feature` 文件,命中多个(比如同一 Story 的 API 层和 UI 层文件都在)时列出来让用户确认要为哪一个生成代码。
64
+ - 找不到对应 `.feature` 文件 → 告知用户需要先有 feature 文件才能生成代码,终止执行,不要凭空编一份场景再生成代码。
65
+
66
+ ## 4. 加载规则并生成代码
67
+
68
+ 读取 `{{PROMPTS_DIR}}/automation-test-code-generate.md`(找不到时尝试 `find . -iname "automation-test-code-generate.md"`;仍找不到就明确告知用户缺少规则文件,不要凭空编造流程继续)。
69
+
70
+ 按该文件里的步骤逐条执行:解析 feature 内容与所属层、探测仓库现有技术栈与约定、复用已有 step definitions、生成测试代码(含 UI 层 selector 定位优先级与溯源质量门槛、Given 造数据的清理机制)、尝试运行验证。本命令不重复这些细节。
71
+
72
+ ## 收尾汇总(模式 C)
73
+
74
+ 按 `automation-test-code-generate.md` 里约定的收尾汇总格式汇报(技术栈、代码文件清单、step 复用情况、selector 溯源情况、数据清理情况、运行验证结果、待人工确认清单)。
75
+
76
+ ---
77
+
78
+ ## 用法示例
79
+ - `/test FT-01-US-02 生成api测试用例`
80
+ - `/test 请帮我为"用户登录"这个需求生成ui测试用例`
81
+ - `/test docs/stories/user-login.md 生成api测试`
82
+ - `/test 请帮我为 feature features/api/FT-01-US-02-user-login_api.feature 生成测试代码`
83
+ - `/test 帮我给 FT-01-US-02-user-login.feature 生成对应的自动化实现`
@@ -0,0 +1,161 @@
1
+ # API Profile 提示词:与 automation-test-base.md 配合使用
2
+
3
+ > 本文件只定义 API 层专属规则。通用规则(信源优先级、Scenario 命名、跨层拆分、header 规范、`# source:`/`# 待人工确认:` 格式等)见 `automation-test-base.md`,此处不重复。
4
+
5
+ ## 适用场景
6
+
7
+ 基于后端代码(或代码未实现时基于卡片)生成 API 层 `.feature` 文件,文件名建议 `<StoryID>-<kebab-name>_api.feature`(`<kebab-name>` 为业务名称转 kebab-case,如 `FT-01-US-02-user-login_api.feature`)。
8
+
9
+ ## Feature 声明
10
+
11
+ - 第二行 Tag 固定为 `@layer:api`
12
+ - Feature 标题在业务名称后加 `" - API 层"` 后缀,如 `Feature: 用户登录 - API 层`
13
+
14
+ ## Header 里的"测试关注点"行
15
+
16
+ 在 base 规定的 header 基础上,API profile 额外固定一行,声明本文件用到的子标签含义:
17
+ ```
18
+ # 测试关注点: 接口契约(@api:contract) / 字段边界(@api:boundary) / 错误码(@api:error)
19
+ ```
20
+ 每个 Scenario 除 `@AC-N` 外,从这三个子标签中选一个(互斥,一个场景选一个最贴切的):
21
+ - `@api:contract`:正向流程,验证接口按契约返回预期数据
22
+ - `@api:boundary`:字段边界/格式校验(多为绕开前端后的后端兜底校验)
23
+ - `@api:error`:业务错误路径(如凭据错误、资源冲突)
24
+
25
+ ## Background
26
+
27
+ 声明请求基础地址:
28
+ ```gherkin
29
+ Background:
30
+ Given 请求基础地址为 "http://localhost:8080"
31
+ ```
32
+ 代码未实现、端口未知时:`Given 请求基础地址为 "<待确认>"` 并在 header 中加 `# 待人工确认: 服务实际监听地址/端口`。
33
+
34
+ ## Given:认证状态 + 数据表
35
+
36
+ - 认证状态:`Given 请求认证状态为 "未登录"` / `"已登录"`
37
+ - 需要预置数据时一律用数据表,字段名对齐真实 DTO/数据模型(代码未实现时对齐卡片给出的最小字段集):
38
+ ```gherkin
39
+ And 存在如下注册用户:
40
+ | id | username | password | display_name | avatar |
41
+ | 2 | woolenwhimsy | knit123 | Sarah Chen | https://... |
42
+ ```
43
+
44
+ ## When:请求
45
+
46
+ 固定句式 + docstring 完整 JSON,不用内联缩写:
47
+ ```gherkin
48
+ When 发送 POST 请求 "/api/v1/auth/login",请求体如下
49
+ """
50
+ {
51
+ "username": "woolenwhimsy",
52
+ "password": "wool456"
53
+ }
54
+ """
55
+ ```
56
+
57
+ ## Then:响应断言
58
+
59
+ 1. **状态码**:`Then 响应状态码为 <code>`
60
+ 2. **响应体**:
61
+ - 结构固定、字段值完全可预期(正常业务响应、固定错误文案)→ `And 返回如下<用户信息/错误信息/XX信息>` + docstring 完整 JSON,字段一个不漏:
62
+ ```gherkin
63
+ And 返回如下错误信息
64
+ """
65
+ { "message": "用户名或密码错误" }
66
+ """
67
+ ```
68
+ - 含不可预测值(自增 id 若非种子数据、生成时间戳、token)→ 改为逐字段 `And` 断言,不做整体 JSON 比对
69
+ 3. **响应头**:对含动态值的 header(如 Set-Cookie)用**部分匹配**、逐条断言,不做整段全等:
70
+ ```gherkin
71
+ And 响应头 Set-Cookie 包含 "sid="
72
+ And 响应头 Set-Cookie 包含 "HttpOnly"
73
+ And 响应头 Set-Cookie 包含 "SameSite=Lax"
74
+ ```
75
+
76
+ ## 边界类场景(@api:boundary)的枚举模式
77
+
78
+ 当某条 AC 在 UI 层已经由前端拦截(如必填校验),API 层要改写为"绕过前端直接调用后端"的边界验证,尽量覆盖以下典型边界(按代码实际校验注解决定要覆盖哪些):
79
+ - 空字符串
80
+ - 纯空格(若代码用 trim 校验)
81
+ - 字段缺失(不传该字段)
82
+ - 字段为 null(如适用)
83
+
84
+ 每个边界值展开为一个独立 Scenario,参考命名:`AC-N 字段边界 - <具体边界情况>`。若代码没有自定义异常处理器、400 响应体结构不确定,在该 Scenario 上方加:
85
+ ```
86
+ # 待人工确认: 400 响应体结构为框架默认校验失败响应(源码无自定义异常处理器),具体字段结构待联调确认
87
+ ```
88
+
89
+ ## 精简示例
90
+
91
+ ```gherkin
92
+ @FT-01-US-02
93
+ @layer:api
94
+ Feature: 用户登录 - API 层
95
+
96
+ 作为一个已注册的织友,
97
+ 我想要用用户名和密码登录,
98
+ 以便进入我的个人空间并持续使用 KnitHub。
99
+
100
+ # 本 .feature 文件由 AI 基于后端源码生成,以 HTTP 语义实例化验收标准
101
+ # 测试关注点: 接口契约(@api:contract) / 字段边界(@api:boundary) / 错误码(@api:error)
102
+ # 依赖后端源码:
103
+ # - AuthController.java(POST /api/v1/auth/login、401 异常处理、会话 Cookie 属性)
104
+ # - AuthService.java(凭据校验,InvalidCredentialsException 统一语义)
105
+ # - LoginRequest.java(username/password 的 @NotBlank 约束)
106
+ # 注: AC-1 的前端导航与 Navbar 展示、AC-3 的前端必填拦截,均属 UI 端到端层(@layer:e2e)职责;
107
+ # API 层仅实例化 HTTP 可观测行为,AC-3 在此层实例化为绕开前端后的后端兜底校验
108
+
109
+ Background:
110
+ Given 请求基础地址为 "http://localhost:8080"
111
+
112
+ # source: AuthController#login (POST /api/v1/auth/login);响应字段依据 UserResponse
113
+ @api:contract
114
+ @AC-1
115
+ Scenario: AC-1 接口契约 - 正确凭据登录成功返回用户信息并建立会话 Cookie
116
+ Given 请求认证状态为 "未登录"
117
+ And 存在如下注册用户:
118
+ | id | username | password | display_name |
119
+ | 2 | woolenwhimsy | knit123 | Sarah Chen |
120
+ When 发送 POST 请求 "/api/v1/auth/login",请求体如下
121
+ """
122
+ { "username": "woolenwhimsy", "password": "knit123" }
123
+ """
124
+ Then 响应状态码为 200
125
+ And 返回如下用户信息
126
+ """
127
+ { "id": 2, "username": "woolenwhimsy", "displayName": "Sarah Chen" }
128
+ """
129
+ And 响应头 Set-Cookie 包含 "sid="
130
+ And 响应头 Set-Cookie 包含 "HttpOnly"
131
+
132
+ # source: AuthController#handleInvalidCredentials(密码错误与用户名不存在共用同一异常与文案,防用户名枚举)
133
+ @api:error
134
+ @AC-2
135
+ Scenario: AC-2 错误码 - 密码错误返回 401 统一提示
136
+ Given 请求认证状态为 "未登录"
137
+ And 存在如下注册用户:
138
+ | id | username | password | display_name |
139
+ | 2 | woolenwhimsy | knit123 | Sarah Chen |
140
+ When 发送 POST 请求 "/api/v1/auth/login",请求体如下
141
+ """
142
+ { "username": "woolenwhimsy", "password": "wool456" }
143
+ """
144
+ Then 响应状态码为 401
145
+ And 返回如下错误信息
146
+ """
147
+ { "message": "用户名或密码错误" }
148
+ """
149
+
150
+ # source: LoginRequest#username (@NotBlank)
151
+ # 待人工确认: 400 响应体结构为框架默认校验失败响应,具体字段结构待联调确认
152
+ @api:boundary
153
+ @AC-3
154
+ Scenario: AC-3 字段边界 - 绕过前端直接调用后端时 username 为空字符串
155
+ Given 请求认证状态为 "未登录"
156
+ When 发送 POST 请求 "/api/v1/auth/login",请求体如下
157
+ """
158
+ { "username": "", "password": "knit123" }
159
+ """
160
+ Then 响应状态码为 400
161
+ ```
@@ -0,0 +1,71 @@
1
+ # Base 提示词:User Story → .feature 文件(层无关通用规则)
2
+
3
+ > 本文件与 `automation-test-api-profile.md` / `automation-test-ui-profile.md` 配合使用。生成任一层的 feature 文件时,**先套用本文件的通用规则,再叠加对应 profile 的层专属规则**。两份 profile 不重复定义本文件已覆盖的内容。
4
+
5
+ ## 角色定位
6
+
7
+ 你是一名资深 QA 自动化工程师,精通 BDD、Gherkin 语法与 SBE(实例化规约),负责把 User Story 的验收标准(AC)转化为可执行的 `.feature` 文件,供 API 层或 UI(E2E)层测试执行使用。
8
+
9
+ ## 输入
10
+
11
+ 1. 【必需】User Story 卡片(`.md`),含用户故事、AC(Given-When-Then)、实例化示例、依赖关系、备注(含已确认的"决策"编号)
12
+ 2. 【可选,优先级更高】对应层的已实现代码(后端代码 → api 层;前端代码 → ui 层)
13
+ 3. 【建议提供】同一 Story 已生成的姊妹层 feature 文件(如已有 UI 层,再生成 API 层时应参考其数据与职责边界,避免重复/遗漏)
14
+
15
+ ## 信源优先级
16
+
17
+ - **代码已实现**:以代码为准(source-code-first)。字段名、路径、文案、校验逻辑一律取自代码;卡片描述仅作为业务意图参考,冲突时以代码为准。
18
+ - **代码未实现**:以卡片为准,尤其是已给出的实例化示例数据;技术细节(字段名、具体交互方式)需要合理假设,并在对应位置标注为待确认(见下方"不确定点标注")。
19
+
20
+ ## Feature / Scenario 组织规则
21
+
22
+ ### Tag 体系
23
+ - Feature 级两行标签:第一行 Story 编号 `@FT-XX-US-YY`;第二行 `@layer:api` 或 `@layer:e2e`(由 profile 指定)
24
+ - Scenario 级至少打上对应的 `@AC-N`;层专属的子标签(如 `@api:contract`)由 profile 定义
25
+
26
+ ### 每个实例化示例 = 一个独立命名的 Scenario
27
+ **不使用 `Scenario Outline` + `Examples` 表。** 卡片里同一条 AC 下的每组实例化示例(示例1、示例2……),无论数据结构多相似,都各自展开为一个独立、具名的 Scenario。原因:
28
+ - 每个实例通常有不同的业务语义(如"正确凭据登录"与"用户名大小写变体登录"是两个值得单独命名、单独追溯的场景,合并成 Outline 会丢失这种区分度)
29
+ - per-scenario 的 `# source:` 溯源注释需要精确到每个场景,Outline 会让多组数据共享同一条注释,降低可追溯性
30
+
31
+ Scenario 命名格式:`<AC编号> <体现该场景业务差异的简短描述>`,禁止用"示例1/示例2"这种编号式命名。
32
+
33
+ ### AC 跨层拆分(重要)
34
+ 一条 AC 如果同时涉及"用户可感知的前端行为"与"服务端保障"(如:前端必填校验 vs 后端字段兜底校验;前端展示 vs 后端会话建立),**两层不是同一场景的复制粘贴**,各自体现本层关注点:
35
+ - UI 层:断言用户能看到的拦截行为本身(如"未向后端发出登录请求")
36
+ - API 层:断言"绕过前端直接调用后端"时的行为(如后端参数校验兜底)
37
+
38
+ 生成某一层时,如果发现某条 AC 的部分行为明显不属于本层职责,在 header 的"注:"里显式声明,并指向姊妹 feature 文件,不要在本层勉强模拟。
39
+
40
+ ### 数据一致性
41
+ 若卡片或已有的姊妹层 feature 文件中已给出过被认可的实例化数据(如卡片备注或已有 feature 的 header 中提到的"决策N认可的示例数据"),生成新层时必须复用同一批数据,不要自行更换,保持跨层数据可追溯一致。
42
+
43
+ ## Header 注释块规范(Feature 声明与 Background 之间)
44
+
45
+ 固定包含以下几段(缺项按说明处理,不要跳过声明本身):
46
+
47
+ 1. **生成依据声明**(一行):
48
+ - 代码已实现:`# 本 .feature 文件由 <工具名,如未知则写"AI"> 基于<前端/后端>源码生成`
49
+ - 代码未实现:`# 本 .feature 文件基于 User Story 卡片草拟生成,源码尚未实现,标记为草稿`
50
+ 2. **源码依赖/扫描范围**(代码已实现时必须列出,逐条给出具体文件路径 + 该文件贡献了哪部分依据,例如某个字段约束、某个错误文案、某个决策编号的落地实现);代码未实现时改写为 `# 待补充: 源码实现后需回填依赖源码列表`
51
+ 3. **跨层职责注释**(`# 注: ...`):说明哪些验收内容不属于本层职责,指向姊妹 feature 文件
52
+ 4. **决策引用**:卡片备注中出现的"决策N"编号,在相关的地方原样引用(如 `FT-01 决策 4`、`FT-01 决策 8`),不要省略编号或改写措辞
53
+
54
+ ## Per-Scenario 溯源注释规范
55
+
56
+ 每个 Scenario 正上方一行或多行 `# source: ...` 注释:
57
+ - 代码已实现:引用具体类名#方法名或组件文件+具体代码行为(如状态设置函数名、异常类名),必要时附决策编号
58
+ - 代码未实现:`# source: US卡片 <AC编号> 示例<N>(源码未实现,具体交互/字段名为 AI 推断,待联调确认)`
59
+
60
+ ## 不确定点统一标注
61
+
62
+ 任何生成时无法从代码或卡片直接得到、需要人工核实的内容,统一使用:
63
+ ```
64
+ # 待人工确认: <具体不确定的内容,说明为什么不确定>
65
+ ```
66
+ 不要使用 TODO、⚠️ 或其他自造格式,保持项目内表述一致。
67
+
68
+ ## AC 与实例化示例的取舍
69
+
70
+ - 卡片里已标注"未经业务方确认"的具体数值,仍然要用(保持信息不丢失),但对应 Scenario 需要在 `# source:` 或 `# 待人工确认:` 中说明该数据未经确认。
71
+ - 业务规则本身(如"密码≥6位""用户名唯一性大小写不敏感")视为已确认前置条件,正常写入,不需要额外标注。
@@ -0,0 +1,80 @@
1
+ # 测试代码生成规则:与 automation-test-base.md 系列配合使用
2
+
3
+ > 本文件定义"基于已有 `.feature` 文件生成自动化测试代码"的规则,供 `/test` 命令的模式 C 加载。生成 `.feature` 文件本身的规则见 `automation-test-base.md` / `automation-test-api-profile.md` / `automation-test-ui-profile.md`,此处不重复。
4
+
5
+ ## 适用场景
6
+
7
+ 用户已经有一份 `.feature` 文件(无论是用 `/test` 生成的,还是团队手写的),需要把里面的 Scenario 落地成可执行的自动化测试代码(step definitions / Page Object / API 请求封装等)。
8
+
9
+ ## 前置条件:必须已有 .feature 文件,不能凭空造场景
10
+
11
+ 生成代码的依据是已确认过的 `.feature` 场景。如果定位不到对应的 `.feature` 文件,要告知用户先要有 feature 文件,不能自己脑补一份场景内容再据此生成代码。
12
+
13
+ ## 1. 定位目标 feature 文件
14
+
15
+ - 参数中给出了具体路径 → 直接读取该文件。
16
+ - 只给了 Story 编号/模糊描述、没给路径 → 在仓库测试目录中搜索匹配的 `.feature` 文件,命中多个(比如同一 Story 的 API 层和 UI 层文件都在)时列出来让用户确认要为哪一个生成代码。
17
+ - 找不到对应 `.feature` 文件 → 告知用户需要先有 feature 文件才能生成代码,终止执行。
18
+
19
+ ## 2. 解析 feature 内容与所属层
20
+
21
+ - 读取 `@layer:api` 或 `@layer:e2e` 标签确定层。
22
+ - 逐条列出所有 Scenario 及其 Given/When/Then 原文语句,后面要用它们去匹配仓库已有的 step definition 正则。
23
+
24
+ ## 3. 探测仓库现有自动化技术栈与约定(禁止凭空假设框架)
25
+
26
+ - 搜索仓库里已有的 step definition / glue 代码目录(用 glob 找类似 `*.steps.ts`、`*Steps.java`、`*_steps.py`、`features/step_definitions/**`、`src/test/**/steps/**` 等命名模式),确认实际使用的 BDD 框架与语言(Cucumber-JVM / cucumber-js / behave / pytest-bdd / SpecFlow / playwright-bdd 等)。
27
+ - **API 层**:找出仓库现有的 HTTP 客户端封装(如某个 `apiClient`/request helper 模块)并复用,不要新造一套请求方式;断言方式与仓库现有测试保持一致(用什么断言库)。
28
+ - **UI 层**:找出仓库现有的元素定位/Page Object 约定(data-testid 命名规则、POM 类结构、UI 测试框架如 Playwright/Cypress/Selenium),复用它把 feature 里的业务语言步骤映射到具体操作。定位真实 selector 时按 §4.2 的优先级从前端组件源码提取,不要跳过组件源码直接凭 feature 里的文案猜测选择器。
29
+ - 若仓库里完全没有任何既有自动化代码(全新引入),列出你打算采用的技术栈和目录结构,向用户确认后再动手,不要没经确认就引入新框架或新目录约定。
30
+
31
+ ## 4. 复用已有 step definitions,只补缺失的
32
+
33
+ - 逐条比对 feature 里的 Given/When/Then 语句是否已被仓库现有的 step 正则覆盖。
34
+ - 已有能匹配的步骤 → 直接复用,不要重复定义(重复定义在多数 BDD 框架下会报 "Ambiguous step/Duplicate step" 错误)。
35
+ - 不存在的步骤 → 新增实现;优先追加到该 Story 已有的 step 文件里,没有对应文件时按仓库既有目录/命名约定新建。
36
+
37
+ ## 5. 生成/补充测试代码
38
+
39
+ - 遵循仓库现有代码风格(缩进、命名、import 顺序、断言写法等),不引入新的代码规范。
40
+ - API 层:调用真实接口封装,断言状态码/响应体/响应头;含不可预测值(自增 id、时间戳、token)的字段按 feature 里的处理方式做单字段断言,不做整体 diff。
41
+ - UI 层:用仓库既有元素定位方式实现业务语言到具体操作的映射(定位优先级见 §4.2),技术选择器细节留在代码内部,不反向污染 feature 文件本身。每个 selector 常量旁边用注释标注来源组件文件(如 `# 来源: src/pages/LoginModal.tsx`),便于后续核对维护。
42
+ - 代码中任何不确定/需要联调确认的地方,统一用 `// 待人工确认: <原因>` 注释标注(与 feature 文件里 `# 待人工确认:` 的措辞风格保持一致),不要用 TODO 或其他格式。
43
+
44
+ ### 4.2 UI 层 selector 定位优先级
45
+
46
+ 定位真实 selector 时按以下优先级从前端组件源码里提取,命中即停止:
47
+
48
+ | 优先级 | 来源 | 示例 |
49
+ |---|---|---|
50
+ | 1 | `data-testid` 属性 | `[data-testid="submit-btn"]` |
51
+ | 2 | `id` 属性 | `#username-input` |
52
+ | 3 | 唯一的语义化 class(结合父级限定,避免命中多个元素) | `.login-form .submit-button` |
53
+ | 4 | 表单绑定字段名 + 标签文本,转为语义化定位 | `getByLabel("用户名")`、`getByRole("button", { name: "登录" })` |
54
+ | 5 | 均不满足唯一性时,用最小化结构选择器,并在代码注释里标注风险 | `.form-item:nth-child(2) input`(需注明"⚠️ 非唯一,建议源码补充 data-testid") |
55
+
56
+ 找不到对应组件源码、定位不到元素时,才允许退化为占位符选择器,且必须在代码注释和收尾汇总里显式标注为"⚠️ 未溯源",不能悄悄用占位符糊弄过去。
57
+
58
+ ### 4.3 selector 溯源质量门槛(UI 层必查,生成代码后执行)
59
+
60
+ - 是否每个 selector 常量都带有"来源: <组件文件路径>"的注释?
61
+ - 是否存在没有来源注释、疑似模板默认值的占位符 selector(如 `#element-id`、`.submit` 这种一看就是随手编的)?如果有,说明生成没做完,回到 §3 重新定位源码,不能直接放行。
62
+ - 统计"⚠️ 未溯源"或"⚠️ 非唯一"的 selector 数量,如实写进收尾汇总,不要省略。
63
+
64
+ ### 4.4 Given 步骤造的数据要注册清理(API 层 / UI 层皆适用)
65
+
66
+ 如果某个 `Given` step 的实现是调用真实 API 创建了一条数据(比如"存在如下注册用户"),这条数据在测试跑完之后要能被清理掉,避免测试库越跑越脏、下次撞唯一性约束、多个测试互相污染:
67
+
68
+ - 创建成功后,把返回的资源 ID 记录到一个"待清理"的地方(具体机制看仓库已有约定:可能是某个 `context`/`fixture` 对象上的清理列表,也可能是仓库已有的清理钩子,比如 `afterEach`/`after_scenario`/`@After` 之类)。
69
+ - 先看仓库里其它已有的 step definition 是不是已经有这套清理机制,有就直接复用同样的写法;仓库里完全没有这类机制时,才需要向用户说明"这个仓库目前没有自动清理测试数据的机制,建议引入",并给出一个最小可行的方案(如加一个清理钩子),不要不声不响地不管清理直接交差。
70
+ - 纯读取型的 Given(只是查询/校验已有状态,没有新建数据)不需要注册清理。
71
+
72
+ ## 6. 尝试运行验证(如环境允许)
73
+
74
+ - 若仓库有可直接运行该 feature 对应测试的命令,尝试执行一次,修复编译错误和明显的接线问题。
75
+ - **不要为了让测试"变绿"而弱化断言或篡改业务预期**——运行失败若源于业务逻辑本身的分歧,如实汇报,不要绕过。
76
+ - 无法运行(缺依赖/无网络/CI-only)时,明确告知用户"未实际运行验证,请自行执行",不要假装已验证通过。
77
+
78
+ ## 7. 收尾汇总
79
+
80
+ 汇报:识别到的技术栈/框架、新增或修改的代码文件路径清单、复用了哪些已有 step definitions/新增了哪些、selector 溯源情况(已溯源数量 / ⚠️ 未溯源数量及原因)、是否注册了数据清理及方式、是否实际运行验证及结果、所有 `// 待人工确认:` 条目清单。
@@ -0,0 +1,157 @@
1
+ # UI Profile 提示词:与 automation-test-base.md 配合使用
2
+
3
+ > 本文件只定义 UI(E2E)层专属规则。通用规则见 `automation-test-base.md`,此处不重复。
4
+
5
+ ## 适用场景
6
+
7
+ 基于前端代码(或代码未实现时基于卡片)生成 UI 端到端层 `.feature` 文件,文件名建议 `<StoryID>-<kebab-name>.feature`(不加 `_api` 后缀,与 API 层文件区分;`<kebab-name>` 为业务名称转 kebab-case,如 `user-login`)。
8
+
9
+ ## Feature 声明
10
+
11
+ - 第二行 Tag 固定为 `@layer:e2e`
12
+ - Feature 标题不加后缀,直接用业务名称,如 `Feature: 用户登录`
13
+ - 若项目内 UI 层 feature 统一声明 Gherkin 语言,可在文件第一行加 `# language: zh`(是否所有 feature 文件都要加此行,还是仅 UI 层惯例,建议与团队现有 feature 文件保持一致)
14
+
15
+ ## Header 里的"源码扫描范围"
16
+
17
+ 在 base 规定的 header 基础上,UI profile 把"依赖源码"改写为面向前端的表述:
18
+ ```
19
+ # 源码扫描范围: <组件/页面文件路径列表,逐条列出>
20
+ # 种子数据依据: <前端可见的种子数据来源文件>
21
+ ```
22
+
23
+ ## 元素引用规则(核心,必须遵守)
24
+
25
+ **一律使用用户可感知的业务语言描述界面元素,不出现任何技术定位方式**(css selector、data-testid、xpath、组件内部 prop 名等一律不写进 feature 文件——具体怎么定位到元素是下游 step definition/POM 实现层的事,不在本层职责范围内):
26
+
27
+ | 要断言/操作的内容 | 写法示例 |
28
+ |---|---|
29
+ | 点击按钮 | `点击导航栏的"登录 / 注册"按钮` |
30
+ | 输入框输入 | `在"用户名"输入框输入 "woolenwhimsy"` |
31
+ | 输入框留空 | `"密码"输入框保持留空` |
32
+ | 弹窗出现/消失 | `打开标题为"欢迎回来"的登录弹窗` / `登录弹窗关闭,标题"欢迎回来"不再显示` |
33
+ | 页面状态变化 | `导航栏改为显示用户名"Sarah Chen"与"退出"按钮` |
34
+ | 路由跳转 | `页面导航至个人页 "/users/woolenwhimsy"` |
35
+ | 页面多字段展示 | 用数据表:<br>`| 字段 | 值 |`<br>`| 显示名称 | Sarah Chen |` |
36
+
37
+ ## Background
38
+
39
+ 通常包含种子用户数据表 + 初始导航状态:
40
+ ```gherkin
41
+ Background:
42
+ Given 系统中存在如下注册用户:
43
+ | username | password | displayName |
44
+ | woolenwhimsy | knit123 | Sarah Chen |
45
+ And 用户处于未登录状态,正在浏览首页 "/"
46
+ ```
47
+
48
+ ## 前端专属校验场景的断言要求
49
+
50
+ 当某条 AC 涉及前端必填/格式校验时,Then 断言里必须包含"未向后端发出请求"这类语句,明确该校验是纯前端拦截(区别于 API 层的后端兜底校验,两者不是同一场景):
51
+ ```gherkin
52
+ Then 登录弹窗保持打开,弹窗内显示提示信息"请输入用户名"
53
+ And 未向后端发出登录请求
54
+ And 导航栏仍显示"登录 / 注册"按钮,页面停留在首页 "/"
55
+ ```
56
+
57
+ ## Then 断言可测试性规范(重要)
58
+
59
+ `Then` 步骤是验收断言的核心,必须表达**可被自动化测试逐字段验证的具体期望值**,禁止用笼统的业务描述代替。
60
+
61
+ **禁止的写法**(无法逐字段断言):
62
+ ```gherkin
63
+ # ❌ 引用 Given 中的数据,未显式写出期望值
64
+ Then 页面展示团队空间列表,包含上述 2 条记录
65
+ And 每条记录均展示 id、name、description、role 字段
66
+
67
+ # ❌ 只描述行为,不含具体期望值
68
+ Then 页面展示创建成功提示
69
+ ```
70
+
71
+ **要求的写法**(具体数据,可逐字段断言):
72
+ ```gherkin
73
+ # ✅ 用数据表格显式写出每条记录的每个字段期望值
74
+ Then 页面展示如下团队空间列表:
75
+ | 名称 | 描述 | 角色 |
76
+ | 研发团队空间 | 用于研发协作 | Owner |
77
+ | 产品团队空间 | 用于产品规划 | Member |
78
+
79
+ # ✅ 标量期望值用引号字符串
80
+ Then 页面展示提示信息"创建成功"
81
+ ```
82
+
83
+ **判定规则**:
84
+ - 列表/多条记录 → 必须用带表头的数据表格逐行逐字段写出期望值,不能只写"包含上述 N 条记录"或"展示以下字段:xxx"
85
+ - 提示语/状态等标量值 → 用引号包裹具体文案,不能只写"展示成功提示"这类抽象描述
86
+ - 空列表场景 → 要写出可观察的具体空状态,如 `Then 页面展示空列表,列表行数为 0` + `And 页面展示提示信息"暂无数据"`,不能只写"页面为空"
87
+ - **`Then` 的期望值即使与 `Given` 中的数据相同,也必须重新显式写出**,不能用"上述""如上"等引用词代替;若操作会改变数据状态(新增/编辑后展示),`Then` 表格要写变更后的值,而非引用 Given 原值
88
+
89
+ 生成完成后自检:扫一遍所有 `Then`/`And` 断言步骤,凡是出现"上述"、"如上"、"以下字段"、"包含 N 条"这类指代/笼统表述且未跟着具体数据表格或引号值的,一律改写为显式数据。
90
+
91
+ ## 跨层排除声明
92
+
93
+ 涉及非用户可感知的技术细节(Cookie 具体属性、响应头、状态码数值本身)不出现在 UI feature 里,在 header 的"注:"中显式声明该部分属于 API 层职责并指向姊妹文件:
94
+ ```
95
+ # 注: 会话 Cookie(HttpOnly sid)的响应头断言属 API 层职责(见 <StoryID>_api.feature),
96
+ # 本层以页面可观测行为(弹窗关闭、Navbar 登录态、页面导航)断言登录成功
97
+ ```
98
+
99
+ ## 精简示例
100
+
101
+ ```gherkin
102
+ # language: zh
103
+ # 本 .feature 文件由 AI 基于前端源码生成
104
+ # 源码扫描范围: Navbar.tsx、LoginModal.tsx、AuthContext.tsx、App.tsx(路由 /users/:username)
105
+ # 种子数据依据: users.json(woolenwhimsy / knit123,displayName "Sarah Chen")
106
+ # 注: 会话 Cookie 的响应头断言属 API 层职责(见 FT-01-US-02-user-login_api.feature),
107
+ # 本层以页面可观测行为断言登录成功
108
+
109
+ @FT-01-US-02
110
+ @layer:e2e
111
+ Feature: 用户登录
112
+
113
+ 作为一个已注册的织友,
114
+ 我想要用用户名和密码登录,
115
+ 以便进入我的个人空间并持续使用 KnitHub。
116
+
117
+ Background:
118
+ Given 系统中存在如下注册用户:
119
+ | username | password | displayName |
120
+ | woolenwhimsy | knit123 | Sarah Chen |
121
+ And 用户处于未登录状态,正在浏览首页 "/"
122
+
123
+ # source: LoginModal.tsx(成功后 onClose 关闭弹窗并 navigate 个人页);Navbar.tsx(登录态显示 displayName)
124
+ @AC-1
125
+ Scenario: AC-1 正确凭据登录成功建立会话
126
+ When 用户点击导航栏的"登录 / 注册"按钮,打开标题为"欢迎回来"的登录弹窗
127
+ And 在"用户名"输入框输入 "woolenwhimsy"
128
+ And 在"密码"输入框输入 "knit123"
129
+ And 点击"登录"按钮
130
+ Then 登录弹窗关闭,标题"欢迎回来"不再显示
131
+ And 导航栏不再显示"登录 / 注册"按钮,改为显示用户名"Sarah Chen"与"退出"按钮
132
+ And 页面导航至个人页 "/users/woolenwhimsy",头部展示如下信息:
133
+ | 字段 | 值 |
134
+ | 显示名称 | Sarah Chen |
135
+ | 用户名 | @woolenwhimsy |
136
+
137
+ # source: LoginModal.tsx(登录失败 setError("用户名或密码错误"),失败时弹窗保持打开、无导航)
138
+ @AC-2
139
+ Scenario: AC-2 密码错误时显示统一错误提示
140
+ When 用户点击导航栏的"登录 / 注册"按钮,打开标题为"欢迎回来"的登录弹窗
141
+ And 在"用户名"输入框输入 "woolenwhimsy"
142
+ And 在"密码"输入框输入 "wool456"
143
+ And 点击"登录"按钮
144
+ Then 登录弹窗保持打开,弹窗内显示提示信息"用户名或密码错误"
145
+ And 导航栏仍显示"登录 / 注册"按钮,页面停留在首页 "/"
146
+
147
+ # source: LoginModal.tsx(空输入前端必填校验 !username.trim() → setError("请输入用户名");校验先行、不向后端发出登录请求)
148
+ @AC-3
149
+ Scenario: AC-3 用户名留空时前端必填校验
150
+ When 用户点击导航栏的"登录 / 注册"按钮,打开标题为"欢迎回来"的登录弹窗
151
+ And "用户名"输入框保持留空
152
+ And 在"密码"输入框输入 "knit123"
153
+ And 点击"登录"按钮
154
+ Then 登录弹窗保持打开,弹窗内显示提示信息"请输入用户名"
155
+ And 未向后端发出登录请求
156
+ And 导航栏仍显示"登录 / 注册"按钮,页面停留在首页 "/"
157
+ ```
@@ -0,0 +1,66 @@
1
+ // 每个工具的安装规则。
2
+ //
3
+ // family 决定用哪种"渲染器"处理模板(见 bin/renderers.js):
4
+ // "native" - 目录名不同,文件格式(commands/*.md + frontmatter + $ARGUMENTS)相同,原样复制
5
+ // "args-only" - 格式相同,只是参数占位符语法不同(需要文本替换)
6
+ // "toml" - Gemini CLI 系列:转换成 .toml
7
+ // "copilot" - GitHub Copilot:改名为 *.prompt.md,调整 frontmatter 和参数语法
8
+ // "windsurf" - Windsurf:目录叫 workflows,不支持 frontmatter,需要剥离
9
+ //
10
+ // scope: "project"(默认,装到当前项目目录下) | "user"(装到用户主目录,供以后接入
11
+ // "只认用户主目录、不认项目目录"的工具时使用,目前列表里的工具都不需要它)
12
+ // commandsSubdir / promptsSubdir: 默认分别是 "commands" / "prompts",
13
+ // 仅当工具的实际约定不同时才覆盖(比如 Windsurf 管 commands 叫 workflows)。
14
+ export const TOOLS = {
15
+ claude: {
16
+ label: "Claude Code",
17
+ dir: ".claude",
18
+ family: "native",
19
+ },
20
+ codebuddy: {
21
+ label: "CodeBuddy",
22
+ dir: ".codebuddy",
23
+ family: "native",
24
+ },
25
+ cursor: {
26
+ label: "Cursor",
27
+ dir: ".cursor",
28
+ family: "native",
29
+ },
30
+ trae: {
31
+ label: "Trae",
32
+ dir: ".trae",
33
+ family: "native",
34
+ },
35
+ qwen: {
36
+ label: "Qwen Code",
37
+ dir: ".qwen",
38
+ family: "args-only",
39
+ argPlaceholder: "{{args}}",
40
+ },
41
+ gemini: {
42
+ label: "Gemini CLI",
43
+ dir: ".gemini",
44
+ family: "toml",
45
+ argPlaceholder: "{{args}}",
46
+ },
47
+ copilot: {
48
+ label: "GitHub Copilot",
49
+ dir: ".github",
50
+ family: "copilot",
51
+ argPlaceholder: "${input:arguments}",
52
+ commandsSubdir: "prompts", // Copilot 的"命令"实际上是 prompts/ 目录下的 *.prompt.md
53
+ },
54
+ windsurf: {
55
+ label: "Windsurf",
56
+ dir: ".windsurf",
57
+ family: "windsurf",
58
+ commandsSubdir: "workflows", // Windsurf 管这个叫 workflows,不叫 commands
59
+ },
60
+ };
61
+
62
+ export const DEFAULT_TOOL = "claude";
63
+
64
+ // native / args-only 是"确定可用";toml / copilot / windsurf 涉及格式转换,
65
+ // 属于"尽力而为",建议安装后打开看一眼。
66
+ export const EXPERIMENTAL_FAMILIES = new Set(["toml", "copilot", "windsurf"]);