@raina-npm/opentest 1.0.1

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.
@@ -0,0 +1,32 @@
1
+ ---
2
+ name: opentest-report
3
+ description: OpenTest 测试报告 - 生成或更新 report.md 执行结果摘要。当用户要写测试报告、汇总执行结果、或执行 /ottest:report 时使用。在当前变更目录下产出 report.md(执行概要、通过率、覆盖率、缺陷摘要、结论与建议)。
4
+ ---
5
+
6
+ # opentest-report:测试报告
7
+
8
+ ## 职责
9
+
10
+ 在当前 OpenTest 变更目录下生成或更新 `report.md`,汇总测试执行结果:执行概要、通过率与覆盖率、缺陷摘要、结论与建议。
11
+
12
+ ## 当前变更目录
13
+
14
+ - 若用户指定了变更名,使用 `opentest/changes/<name>/`。
15
+ - 若未指定且项目下仅有一个非归档变更目录,则默认使用该目录。
16
+ - 若有多个未归档变更,询问用户要对哪一个执行。
17
+
18
+ ## 执行步骤
19
+
20
+ 1. **确定当前变更目录**(按上规则)。
21
+ 2. **收集信息**:若用户已提供执行结果(通过/失败数、缺陷列表等),据此填写;否则可先搭好 report 结构,留待用户补充数据。
22
+ 3. **生成或更新 report.md**:在变更目录下写入或更新 `report.md`,包含:
23
+ - 执行概要:执行时间、环境、总用例数、通过/失败/阻塞
24
+ - 通过率与覆盖率:按模块或类型的通过率、需求/场景覆盖情况
25
+ - 缺陷摘要:严重/一般/轻微数量或列表(可简写)
26
+ - 结论与建议:是否达到通过标准、遗留风险或后续建议
27
+ - 模板见:`.claude/skills/opentest/references/artifact-templates.md` 中的「report.md」小节。
28
+ 4. **告知用户**:文件路径及下一步可执行「归档测试」或 `/ottest:archive`。
29
+
30
+ ## 产物
31
+
32
+ - `opentest/changes/<name>/report.md`
@@ -0,0 +1,33 @@
1
+ ---
2
+ name: opentest-scope
3
+ description: OpenTest 定义测试范围 - 生成或更新 scope.md。当用户要写测试范围、定义测什么不测什么、或执行 /ottest:scope 时使用。在当前变更目录下产出 scope.md(在范围/不在范围、风险、依赖)。
4
+ ---
5
+
6
+ # opentest-scope:定义测试范围
7
+
8
+ ## 职责
9
+
10
+ 在当前 OpenTest 变更目录下生成或更新 `scope.md`,明确在范围/不在范围、风险与重点、依赖。
11
+
12
+ ## 当前变更目录
13
+
14
+ - 若用户指定了变更名,使用 `opentest/changes/<name>/`。
15
+ - 若未指定且项目下仅有一个非归档变更目录(即 `opentest/changes/` 下仅有一个非 `archive` 的子目录),则默认使用该目录。
16
+ - 若有多个未归档变更,询问用户要对哪一个执行。
17
+
18
+ ## 执行步骤
19
+
20
+ 1. **确定当前变更目录**(按上规则)。
21
+ 2. **读取上下文**:若存在 `proposal.md`,可读取以保持与提案一致。
22
+ 3. **生成或更新 scope.md**:在变更目录下写入 `scope.md`,内容包含:
23
+ - 在范围内:功能/模块列表、测试类型及优先级
24
+ - 不在范围内:明确排除的功能或场景
25
+ - 风险与重点:高风险区域、必须覆盖的核心场景
26
+ - 依赖:需求文档/接口文档路径、环境、数据、账号等
27
+ - 模板与字段说明见:`.claude/skills/opentest/references/artifact-templates.md` 中的「scope.md」小节。
28
+
29
+ 4. **告知用户**:文件路径及下一步可执行「写测试策略和场景」或 `/ottest:spec`。
30
+
31
+ ## 产物
32
+
33
+ - `opentest/changes/<name>/scope.md`
@@ -0,0 +1,37 @@
1
+ ---
2
+ name: opentest-spec
3
+ description: OpenTest 测试策略与场景 - 生成 strategy.md 和 specs 下的场景文件。当用户要写测试策略和场景、写测试规格、或执行 /ottest:spec、/ottest:ff 时使用。产出测试类型、层级、通过标准及按功能拆分的场景与验收条件。
4
+ ---
5
+
6
+ # opentest-spec:策略与场景
7
+
8
+ ## 职责
9
+
10
+ 在当前 OpenTest 变更目录下生成 `strategy.md` 与 `specs/*.md`,定义测试类型、层级、通过标准及按功能拆分的场景与验收条件(正向/负向/边界)。
11
+
12
+ ## 当前变更目录
13
+
14
+ - 若用户指定了变更名,使用 `opentest/changes/<name>/`。
15
+ - 若未指定且项目下仅有一个非归档变更目录,则默认使用该目录。
16
+ - 若有多个未归档变更,询问用户要对哪一个执行。
17
+
18
+ ## 执行步骤
19
+
20
+ 1. **确定当前变更目录**(按上规则)。
21
+ 2. **读取上下文**:若存在 `proposal.md`、`scope.md`,可读取以保持一致。
22
+ 3. **生成 strategy.md**:在变更目录下写入 `strategy.md`,包含:
23
+ - 测试类型(功能/接口/性能/兼容等)及范围与重点
24
+ - 测试层级(单元/接口/集成/E2E 等)
25
+ - 通过标准(用例通过率、必须通过的场景、准入/准出条件等)
26
+ - 模板见:`.claude/skills/opentest/references/artifact-templates.md` 中的「strategy.md」小节。
27
+ 4. **生成 specs/*.md**:在变更目录的 `specs/` 下按功能或需求拆分场景文件(如 `login.md`、`theme-toggle.md`),每文件包含:
28
+ - 场景概述
29
+ - 场景与验收条件(类型:正向/负向/边界/异常;前置条件、步骤、预期结果、优先级)
30
+ - 数据与边界(如有)
31
+ - 模板见:`.claude/skills/opentest/references/artifact-templates.md` 中的「specs/<feature>.md」小节。
32
+ 5. **告知用户**:产出路径及下一步可执行「写测试设计」或 `/ottest:design`。
33
+
34
+ ## 产物
35
+
36
+ - `opentest/changes/<name>/strategy.md`
37
+ - `opentest/changes/<name>/specs/<feature-a>.md`、`<feature-b>.md` 等
@@ -0,0 +1,18 @@
1
+ ---
2
+ name: opentest-tasks
3
+ description: [已废弃] OpenTest 曾用步骤,已由简化流程替代。请使用 opentest-generate(/ottest:generate Excel 或 XMind)在 spec 后直接生成用例。
4
+ ---
5
+
6
+ # opentest-tasks:已废弃
7
+
8
+ 本技能已废弃。OpenTest 流程已简化为:
9
+
10
+ ```
11
+ new → scope → spec → generate → report → archive
12
+ ```
13
+
14
+ 任务步骤(tasks)已合并到生成步骤。如需生成测试用例,请使用 **opentest-generate**:
15
+
16
+ - `/ottest:generate Excel` — 生成 .xlsx 用例
17
+ - `/ottest:generate XMind` — 生成 .xmind 思维导图
18
+ - `/ottest:generate Excel,XMind` — 同时生成两种格式
package/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # OpenTest
2
+
3
+ 规范驱动测试流程(Spec-driven Testing)——面向 AI 的「先规划再生成」测试工作流。可在**命令行**直接使用(与 OpenSpec 类似),也可在 Cursor 中配合斜杠命令使用。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ npm install -g opentest
9
+ ```
10
+
11
+ 或使用 npx 免安装运行:
12
+
13
+ ```bash
14
+ npx opentest init
15
+ npx opentest new 用户登录
16
+ ```
17
+
18
+ ## 命令行用法
19
+
20
+ 在任意项目根目录执行:
21
+
22
+ | 命令 | 说明 |
23
+ |------|------|
24
+ | `opentest init` | 初始化:复制技能到 `.claude/skills/`,创建 `opentest/changes/`、`opentest/archive/` |
25
+ | `opentest new <name>` | 新建测试变更(创建目录与 proposal.md 模板) |
26
+ | `opentest scope [--name <name>]` | 在当前变更下创建 scope.md 模板 |
27
+ | `opentest spec [--name <name>]` | 创建 strategy.md 与 specs 模板 |
28
+ | `opentest generate Excel\|XMind [--name <name>]` | 根据 testcases.json 生成用例到 artifacts/ |
29
+ | `opentest report [--name <name>]` | 创建 report.md 模板 |
30
+ | `opentest archive [--name <name>]` | 将当前变更归档到 archive/ |
31
+ | `opentest list` | 列出未归档与已归档变更 |
32
+ | `opentest --help` | 显示帮助 |
33
+ | `opentest --version` | 显示版本 |
34
+
35
+ 若只有一个未归档变更,`scope`、`spec`、`generate`、`report`、`archive` 可不写 `--name`。
36
+
37
+ ## 快速开始
38
+
39
+ ```powershell
40
+ # 1. 初始化
41
+ opentest init
42
+
43
+ # 2. 新建一次测试
44
+ opentest new 用户登录
45
+
46
+ # 3. 写范围与策略(或直接编辑生成的文件)
47
+ opentest scope --name 用户登录
48
+ opentest spec --name 用户登录
49
+
50
+ # 4. 若有 testcases.json,生成用例
51
+ opentest generate Excel --name 用户登录
52
+ # 或同时生成 Excel 与 XMind
53
+ opentest generate Excel XMind
54
+
55
+ # 5. 归档
56
+ opentest archive --name 用户登录
57
+ ```
58
+
59
+ ## 在 Cursor 中使用
60
+
61
+ 初始化后,在 Cursor 中可与 AI 使用斜杠命令:
62
+
63
+ - `/ottest:new <被测对象>` — 新建测试变更
64
+ - `/ottest:scope` — 定义测试范围
65
+ - `/ottest:spec` — 写测试策略与场景
66
+ - `/ottest:generate Excel` / `/ottest:generate XMind` — 生成用例(AI 会从 strategy+specs 生成 testcases.json 再调用脚本)
67
+ - `/ottest:report` — 写测试报告
68
+ - `/ottest:archive` — 归档
69
+
70
+ ## Python 依赖(可选)
71
+
72
+ 若需生成 **Excel** 格式用例,请安装:
73
+
74
+ ```bash
75
+ pip install openpyxl
76
+ ```
77
+
78
+ XMind 生成无需额外依赖。
79
+
80
+ ## 目录结构
81
+
82
+ ```
83
+ opentest/
84
+ changes/ # 各次测试变更
85
+ <name>/ # proposal、scope、strategy、specs、artifacts、report
86
+ archive/ # 已归档的变更
87
+ ```
88
+
89
+ ## License
90
+
91
+ MIT
package/bin/cli.js ADDED
@@ -0,0 +1,386 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * OpenTest CLI - 命令行入口,与 OpenSpec 类似。
4
+ * 用法:opentest <子命令> [参数]
5
+ * opentest init
6
+ * opentest new <name>
7
+ * opentest scope [--name <name>]
8
+ * opentest spec [--name <name>]
9
+ * opentest generate Excel|XMind [--name <name>]
10
+ * opentest report [--name <name>]
11
+ * opentest archive [--name <name>]
12
+ * opentest list
13
+ */
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const { spawnSync } = require('child_process');
17
+
18
+ const PKG_ROOT = path.resolve(__dirname, '..');
19
+ const CWD = process.cwd();
20
+ const OPENTEST_ROOT = path.join(CWD, 'opentest');
21
+ const CHANGES_DIR = path.join(OPENTEST_ROOT, 'changes');
22
+ const ARCHIVE_DIR = path.join(OPENTEST_ROOT, 'archive');
23
+ const SKILLS_SRC = path.join(PKG_ROOT, '.claude', 'skills');
24
+ const SKILLS_DEST = path.join(CWD, '.claude', 'skills');
25
+ const GENERATE_SCRIPTS = path.join(PKG_ROOT, '.claude', 'skills', 'opentest-generate', 'scripts');
26
+
27
+ // ---------- 模板 ----------
28
+ const PROPOSAL_TEMPLATE = `# 测试提案:<标题>
29
+
30
+ ## 被测对象
31
+ - 功能/模块/版本:(简要描述)
32
+ - 关联需求或 OpenSpec:(如有)路径或链接
33
+
34
+ ## 测试目标
35
+ - 为什么做这次测试(发布前验证、回归、新功能等)
36
+ - 期望达成的结果(如:核心流程覆盖、接口契约验证)
37
+
38
+ ## 范围概要
39
+ - 在范围内:(一两句话)
40
+ - 不在范围内:(如有)
41
+ `;
42
+
43
+ const SCOPE_TEMPLATE = `# 测试范围:<标题>
44
+
45
+ ## 在范围内
46
+ - 功能/模块列表
47
+ - 测试类型(功能/接口/性能/兼容等)及优先级
48
+
49
+ ## 不在范围内
50
+ - 明确排除的功能或场景
51
+
52
+ ## 风险与重点
53
+ - 高风险区域
54
+ - 必须覆盖的核心场景
55
+
56
+ ## 依赖
57
+ - 需求文档/接口文档路径
58
+ - 环境、数据、账号等依赖
59
+ `;
60
+
61
+ const STRATEGY_TEMPLATE = `# 测试策略:<标题>
62
+
63
+ ## 测试类型
64
+ - 功能测试:范围与重点
65
+ - 接口测试:范围与重点
66
+ - 其他(性能/兼容/安全等):如有
67
+
68
+ ## 测试层级
69
+ - 单元/接口/集成/E2E 等与本次相关的层级
70
+
71
+ ## 通过标准
72
+ - 用例通过率要求
73
+ - 必须通过的场景或用例列表
74
+ - 准入/准出条件(如有)
75
+ `;
76
+
77
+ const SPEC_TEMPLATE = `# 场景:<功能/模块名>
78
+
79
+ ## 场景概述
80
+ - 简要说明该功能要测什么
81
+
82
+ ## 场景与验收条件
83
+
84
+ ### 场景 1:<名称>
85
+ - **类型**:正向 / 负向 / 边界 / 异常
86
+ - **前置条件**:
87
+ - **步骤**:
88
+ - **预期结果**:
89
+ - **优先级**:P0 / P1 / P2
90
+
91
+ ### 场景 2:…
92
+ (同上)
93
+
94
+ ## 数据与边界
95
+ - 关键边界值、异常输入(如有)
96
+ `;
97
+
98
+ const REPORT_TEMPLATE = `# 测试报告:<标题>
99
+
100
+ ## 执行概要
101
+ - 执行时间、环境
102
+ - 总用例数、通过/失败/阻塞
103
+
104
+ ## 通过率与覆盖率
105
+ - 按模块或类型的通过率
106
+ - 需求/场景覆盖情况
107
+
108
+ ## 缺陷摘要
109
+ - 严重/一般/轻微数量或列表(可简写)
110
+
111
+ ## 结论与建议
112
+ - 是否达到通过标准
113
+ - 遗留风险或后续建议
114
+ `;
115
+
116
+ // ---------- 工具 ----------
117
+ function copyDirRecursive(src, dest) {
118
+ if (!fs.existsSync(src)) return false;
119
+ fs.mkdirSync(dest, { recursive: true });
120
+ for (const e of fs.readdirSync(src, { withFileTypes: true })) {
121
+ const s = path.join(src, e.name);
122
+ const d = path.join(dest, e.name);
123
+ if (e.isDirectory()) copyDirRecursive(s, d);
124
+ else fs.copyFileSync(s, d);
125
+ }
126
+ return true;
127
+ }
128
+
129
+ function slug(name) {
130
+ return String(name)
131
+ .trim()
132
+ .replace(/\s+/g, '-')
133
+ .replace(/[^\w\u4e00-\u9fa5-]/g, '')
134
+ .replace(/-+/g, '-')
135
+ .replace(/^-|-$/g, '') || 'untitled';
136
+ }
137
+
138
+ /** 获取未归档的变更目录列表 */
139
+ function getNonArchivedChanges() {
140
+ if (!fs.existsSync(CHANGES_DIR)) return [];
141
+ const archivePath = path.join(CHANGES_DIR, 'archive');
142
+ return fs.readdirSync(CHANGES_DIR, { withFileTypes: true })
143
+ .filter(d => d.isDirectory() && d.name !== 'archive')
144
+ .map(d => d.name);
145
+ }
146
+
147
+ /** 解析「当前变更」:可选 --name,否则取唯一未归档 */
148
+ function resolveChangeDir(args) {
149
+ let name = null;
150
+ for (let i = 0; i < args.length; i++) {
151
+ if ((args[i] === '--name' || args[i] === '-n') && args[i + 1]) {
152
+ name = slug(args[i + 1]);
153
+ break;
154
+ }
155
+ }
156
+ const list = getNonArchivedChanges();
157
+ if (name) {
158
+ if (!list.includes(name)) {
159
+ console.error('错误:未找到变更 "' + name + '"。可用:', list.join(', ') || '(无)');
160
+ process.exit(1);
161
+ }
162
+ return path.join(CHANGES_DIR, name);
163
+ }
164
+ if (list.length === 0) {
165
+ console.error('错误:没有未归档的变更。请先执行 opentest new <name>');
166
+ process.exit(1);
167
+ }
168
+ if (list.length > 1) {
169
+ console.error('错误:存在多个未归档变更,请用 --name 指定:', list.join(', '));
170
+ process.exit(1);
171
+ }
172
+ return path.join(CHANGES_DIR, list[0]);
173
+ }
174
+
175
+ function getChangeName(changeDir) {
176
+ return path.basename(changeDir);
177
+ }
178
+
179
+ // ---------- 子命令 ----------
180
+ function cmdInit() {
181
+ console.log('OpenTest 初始化...\n');
182
+ if (!fs.existsSync(SKILLS_SRC)) {
183
+ console.error('错误:包内未找到 .claude/skills 目录。');
184
+ process.exit(1);
185
+ }
186
+ copyDirRecursive(SKILLS_SRC, SKILLS_DEST);
187
+ console.log('✓ 技能已复制到', path.relative(CWD, SKILLS_DEST));
188
+
189
+ for (const dir of [OPENTEST_ROOT, CHANGES_DIR, ARCHIVE_DIR]) {
190
+ if (!fs.existsSync(dir)) {
191
+ fs.mkdirSync(dir, { recursive: true });
192
+ console.log('✓ 已创建', path.relative(CWD, dir));
193
+ }
194
+ }
195
+ console.log('\n完成。可用:opentest new <name> 新建测试变更。');
196
+ console.log('若需生成 Excel 用例,请安装:pip install openpyxl');
197
+ }
198
+
199
+ function cmdNew(args) {
200
+ const raw = args.filter(a => !a.startsWith('-')).join(' ').trim();
201
+ const name = raw ? slug(raw) : 'untitled';
202
+ const changeDir = path.join(CHANGES_DIR, name);
203
+ if (fs.existsSync(changeDir)) {
204
+ console.error('错误:变更已存在:', name);
205
+ process.exit(1);
206
+ }
207
+
208
+ if (!fs.existsSync(CHANGES_DIR)) {
209
+ fs.mkdirSync(ARCHIVE_DIR, { recursive: true });
210
+ fs.mkdirSync(CHANGES_DIR, { recursive: true });
211
+ }
212
+
213
+ const specsDir = path.join(changeDir, 'specs');
214
+ const artifactsDir = path.join(changeDir, 'artifacts');
215
+ fs.mkdirSync(specsDir, { recursive: true });
216
+ fs.mkdirSync(artifactsDir, { recursive: true });
217
+
218
+ const title = raw || name;
219
+ const proposal = PROPOSAL_TEMPLATE.replace(/<标题>/g, title);
220
+ fs.writeFileSync(path.join(changeDir, 'proposal.md'), proposal, 'utf8');
221
+
222
+ console.log('✓ 已创建变更:', path.relative(CWD, changeDir));
223
+ console.log(' 下一步:opentest scope --name ' + name + ' 或直接编辑 proposal.md / scope.md');
224
+ }
225
+
226
+ function cmdScope(args) {
227
+ const changeDir = resolveChangeDir(args);
228
+ const name = getChangeName(changeDir);
229
+ const scopePath = path.join(changeDir, 'scope.md');
230
+ const content = SCOPE_TEMPLATE.replace(/<标题>/g, name);
231
+ fs.writeFileSync(scopePath, content, 'utf8');
232
+ console.log('✓ 已写入', path.relative(CWD, scopePath));
233
+ }
234
+
235
+ function cmdSpec(args) {
236
+ const changeDir = resolveChangeDir(args);
237
+ const name = getChangeName(changeDir);
238
+ const strategyPath = path.join(changeDir, 'strategy.md');
239
+ const content = STRATEGY_TEMPLATE.replace(/<标题>/g, name);
240
+ fs.writeFileSync(strategyPath, content, 'utf8');
241
+ const specsDir = path.join(changeDir, 'specs');
242
+ if (!fs.existsSync(specsDir)) fs.mkdirSync(specsDir, { recursive: true });
243
+ const placeholderSpec = path.join(specsDir, 'main.md');
244
+ if (!fs.existsSync(placeholderSpec)) {
245
+ fs.writeFileSync(placeholderSpec, SPEC_TEMPLATE.replace(/<功能\/模块名>/, name), 'utf8');
246
+ }
247
+ console.log('✓ 已写入', path.relative(CWD, strategyPath));
248
+ console.log('✓ specs 目录已就绪');
249
+ }
250
+
251
+ function cmdReport(args) {
252
+ const changeDir = resolveChangeDir(args);
253
+ const name = getChangeName(changeDir);
254
+ const reportPath = path.join(changeDir, 'report.md');
255
+ const content = REPORT_TEMPLATE.replace(/<标题>/g, name);
256
+ fs.writeFileSync(reportPath, content, 'utf8');
257
+ console.log('✓ 已写入', path.relative(CWD, reportPath));
258
+ }
259
+
260
+ function cmdArchive(args) {
261
+ const changeDir = resolveChangeDir(args);
262
+ const name = getChangeName(changeDir);
263
+ const date = new Date().toISOString().slice(0, 10);
264
+ const archiveName = date + '-' + name;
265
+ const dest = path.join(CHANGES_DIR, 'archive', archiveName);
266
+ if (!fs.existsSync(path.join(CHANGES_DIR, 'archive'))) {
267
+ fs.mkdirSync(path.join(CHANGES_DIR, 'archive'), { recursive: true });
268
+ }
269
+ fs.renameSync(changeDir, dest);
270
+ console.log('✓ 已归档到', path.relative(CWD, dest));
271
+ }
272
+
273
+ function cmdList() {
274
+ const list = getNonArchivedChanges();
275
+ const archiveDir = path.join(CHANGES_DIR, 'archive');
276
+ let archived = [];
277
+ if (fs.existsSync(archiveDir)) {
278
+ archived = fs.readdirSync(archiveDir, { withFileTypes: true })
279
+ .filter(d => d.isDirectory())
280
+ .map(d => d.name);
281
+ }
282
+ console.log('未归档变更:', list.length ? list.join(', ') : '(无)');
283
+ console.log('已归档:', archived.length ? archived.join(', ') : '(无)');
284
+ }
285
+
286
+ function runPython(scriptName, scriptArgs) {
287
+ const scriptPath = path.join(GENERATE_SCRIPTS, scriptName);
288
+ if (!fs.existsSync(scriptPath)) {
289
+ console.error('错误:未找到脚本', scriptPath);
290
+ process.exit(1);
291
+ }
292
+ const py = process.platform === 'win32' ? 'python' : 'python3';
293
+ const result = spawnSync(py, [scriptPath, ...scriptArgs], {
294
+ stdio: 'inherit',
295
+ });
296
+ if (result.status !== 0) {
297
+ process.exit(result.status == null ? 1 : result.status);
298
+ }
299
+ }
300
+
301
+ function cmdGenerate(args) {
302
+ const formats = [];
303
+ const rest = [];
304
+ for (const a of args) {
305
+ if (a.toLowerCase() === 'excel') formats.push('excel');
306
+ else if (a.toLowerCase() === 'xmind') formats.push('xmind');
307
+ else rest.push(a);
308
+ }
309
+ if (formats.length === 0) formats.push('excel');
310
+
311
+ const changeDir = resolveChangeDir(rest);
312
+ const name = getChangeName(changeDir);
313
+ const jsonPath = path.join(changeDir, 'testcases.json');
314
+ const artifactsDir = path.join(changeDir, 'artifacts');
315
+ if (!fs.existsSync(jsonPath)) {
316
+ console.error('错误:未找到 testcases.json。请先在变更目录下准备 testcases.json,或使用 Cursor /ottest:generate 由 AI 从 strategy+specs 生成。');
317
+ process.exit(1);
318
+ }
319
+ if (!fs.existsSync(artifactsDir)) fs.mkdirSync(artifactsDir, { recursive: true });
320
+
321
+ const baseOut = path.join(artifactsDir, name + '_cases');
322
+ if (formats.includes('excel')) {
323
+ runPython('generate_excel.py', ['--input', jsonPath, '--output', baseOut + '.xlsx']);
324
+ }
325
+ if (formats.includes('xmind')) {
326
+ runPython('generate_xmind.py', ['--input', jsonPath, '--output', baseOut + '.xmind', '--title', '测试用例']);
327
+ }
328
+ }
329
+
330
+ function printHelp() {
331
+ console.log(`
332
+ OpenTest CLI - 规范驱动测试
333
+
334
+ 用法: opentest <子命令> [参数]
335
+
336
+ 子命令:
337
+ init 初始化:复制技能到 .claude/skills,创建 opentest 目录
338
+ new <name> 新建测试变更(创建目录与 proposal.md)
339
+ scope [--name <name>] 在当前变更下创建 scope.md 模板
340
+ spec [--name <name>] 创建 strategy.md 与 specs 模板
341
+ generate Excel|XMind [--name <name>] 根据 testcases.json 生成用例(需先有 testcases.json)
342
+ report [--name <name>] 创建 report.md 模板
343
+ archive [--name <name>] 将当前变更归档到 archive/
344
+ list 列出未归档与已归档变更
345
+
346
+ 若仅有一个未归档变更,scope/spec/generate/report/archive 可不写 --name。
347
+ `);
348
+ }
349
+
350
+ // ---------- 主入口 ----------
351
+ function main() {
352
+ const argv = process.argv.slice(2);
353
+ const cmd = argv[0];
354
+ const args = argv.slice(1);
355
+
356
+ if (!cmd || cmd === '--help' || cmd === '-h') {
357
+ printHelp();
358
+ return;
359
+ }
360
+ if (cmd === '--version' || cmd === '-v') {
361
+ const pkg = require(path.join(PKG_ROOT, 'package.json'));
362
+ console.log(pkg.version);
363
+ return;
364
+ }
365
+
366
+ const commands = {
367
+ init: cmdInit,
368
+ new: cmdNew,
369
+ scope: cmdScope,
370
+ spec: cmdSpec,
371
+ generate: cmdGenerate,
372
+ report: cmdReport,
373
+ archive: cmdArchive,
374
+ list: cmdList,
375
+ };
376
+
377
+ const fn = commands[cmd];
378
+ if (!fn) {
379
+ console.error('未知子命令:', cmd);
380
+ printHelp();
381
+ process.exit(1);
382
+ }
383
+ fn(args);
384
+ }
385
+
386
+ main();
package/bin/init.js ADDED
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * OpenTest 初始化脚本
4
+ * 将技能复制到当前项目的 .claude/skills/,并创建 opentest 目录结构。
5
+ *
6
+ * 用法:
7
+ * npx opentest-init
8
+ * npm exec opentest-init
9
+ */
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+
13
+ const PKG_ROOT = path.resolve(__dirname, '..');
14
+ const SKILLS_SRC = path.join(PKG_ROOT, '.claude', 'skills');
15
+ const CWD = process.cwd();
16
+ const SKILLS_DEST = path.join(CWD, '.claude', 'skills');
17
+ const OPENTEST_ROOT = path.join(CWD, 'opentest');
18
+ const CHANGES_DIR = path.join(OPENTEST_ROOT, 'changes');
19
+ const ARCHIVE_DIR = path.join(OPENTEST_ROOT, 'archive');
20
+
21
+ function copyDirRecursive(src, dest) {
22
+ if (!fs.existsSync(src)) {
23
+ console.error('错误:找不到技能目录', src);
24
+ process.exit(1);
25
+ }
26
+ fs.mkdirSync(dest, { recursive: true });
27
+ const entries = fs.readdirSync(src, { withFileTypes: true });
28
+ for (const e of entries) {
29
+ const s = path.join(src, e.name);
30
+ const d = path.join(dest, e.name);
31
+ if (e.isDirectory()) {
32
+ copyDirRecursive(s, d);
33
+ } else {
34
+ fs.copyFileSync(s, d);
35
+ }
36
+ }
37
+ }
38
+
39
+ function main() {
40
+ console.log('OpenTest 初始化...\n');
41
+
42
+ // 1. 复制技能到 .claude/skills/
43
+ if (!fs.existsSync(SKILLS_SRC)) {
44
+ console.error('错误:包内未找到 .claude/skills 目录,请检查安装。');
45
+ process.exit(1);
46
+ }
47
+
48
+ const skillsExist = fs.existsSync(SKILLS_DEST);
49
+ copyDirRecursive(SKILLS_SRC, SKILLS_DEST);
50
+ console.log('✓ 技能已复制到', path.relative(CWD, SKILLS_DEST));
51
+
52
+ // 2. 创建 opentest 目录结构
53
+ const created = [];
54
+ for (const dir of [OPENTEST_ROOT, CHANGES_DIR, ARCHIVE_DIR]) {
55
+ if (!fs.existsSync(dir)) {
56
+ fs.mkdirSync(dir, { recursive: true });
57
+ created.push(path.relative(CWD, dir));
58
+ }
59
+ }
60
+ if (created.length) {
61
+ console.log('✓ 已创建目录:', created.join(', '));
62
+ } else {
63
+ console.log('✓ opentest 目录已存在');
64
+ }
65
+
66
+ // 3. Python 依赖提示
67
+ console.log('\n---');
68
+ console.log('OpenTest 初始化完成!');
69
+ if (!skillsExist) {
70
+ console.log('\n在 Cursor 中可使用斜杠命令:');
71
+ console.log(' /ottest:new <name> - 新建测试变更');
72
+ console.log(' /ottest:scope - 定义测试范围');
73
+ console.log(' /ottest:spec - 写测试策略与场景');
74
+ console.log(' /ottest:generate Excel - 生成 Excel 用例');
75
+ console.log(' /ottest:generate XMind - 生成 XMind 用例');
76
+ console.log(' /ottest:report - 写测试报告');
77
+ console.log(' /ottest:archive - 归档本次测试');
78
+ }
79
+ console.log('\n提示:若需生成 Excel 用例,请先安装 Python 依赖:');
80
+ console.log(' pip install openpyxl');
81
+ }
82
+
83
+ main();