@epoint-testtech/ep-stage-skill 0.0.3-alpha.1 → 0.0.4-alpha.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/SKILL.md +2 -2
- package/codex-skill/ep-stage/glue-create-project/SKILL.md +186 -0
- package/codex-skill/ep-stage/glue-generate-testcase/SKILL.md +199 -0
- package/codex-skill/ep-stage/glue-generate-testcase/references/testcase-schema.md +112 -0
- package/codex-skill/ep-stage/glue-run-test/SKILL.md +249 -0
- package/codex-skill/ep-stage/glue-run-test/references/crud-pipeline.md +145 -0
- package/codex-skill/ep-stage/{glue-test → glue-run-test}/scripts/generate-crud-spec.mjs +3 -3
- package/codex-skill/ep-stage/recording-to-glue/SKILL.md +1 -0
- package/codex-skill/ep-stage/scripts/validate-skill.mjs +29 -7
- package/dist/src/cli/dev/extract-contract.d.ts +14 -0
- package/dist/src/cli/dev/extract-contract.d.ts.map +1 -0
- package/dist/src/cli/dev/extract-contract.js +114 -0
- package/dist/src/cli/generate-crud-contract.js +7 -77
- package/dist/src/cli/generate-playwright-tests.d.ts +0 -28
- package/dist/src/cli/generate-playwright-tests.d.ts.map +1 -1
- package/dist/src/cli/generate-playwright-tests.js +4 -81
- package/dist/src/cli/generate-testcase.d.ts +83 -0
- package/dist/src/cli/generate-testcase.d.ts.map +1 -0
- package/dist/src/cli/generate-testcase.js +197 -0
- package/dist/src/cli/index.d.ts +18 -0
- package/dist/src/cli/index.d.ts.map +1 -0
- package/dist/src/cli/index.js +55 -0
- package/dist/src/cli/probe.d.ts +44 -0
- package/dist/src/cli/probe.d.ts.map +1 -0
- package/dist/src/cli/probe.js +221 -0
- package/dist/src/cli/run-gap-pipeline.js +4 -0
- package/dist/src/cli/run.d.ts +63 -0
- package/dist/src/cli/run.d.ts.map +1 -0
- package/dist/src/cli/run.js +116 -0
- package/dist/src/cli/spec.d.ts +45 -0
- package/dist/src/cli/spec.d.ts.map +1 -0
- package/dist/src/cli/spec.js +74 -0
- package/dist/src/context/stage-context.d.ts +72 -8
- package/dist/src/context/stage-context.d.ts.map +1 -1
- package/dist/src/context/stage-context.js +61 -15
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/testcase/testcase-generator.d.ts.map +1 -1
- package/dist/src/testcase/testcase-generator.js +4 -0
- package/dist/src/testcase/testcase-v2.d.ts +50 -0
- package/dist/src/testcase/testcase-v2.d.ts.map +1 -0
- package/dist/src/testcase/testcase-v2.js +1 -0
- package/dist/src/util/credentials.d.ts +12 -0
- package/dist/src/util/credentials.d.ts.map +1 -0
- package/dist/src/util/credentials.js +19 -0
- package/dist/src/util/i18n-testcase.d.ts +8 -0
- package/dist/src/util/i18n-testcase.d.ts.map +1 -0
- package/dist/src/util/i18n-testcase.js +55 -0
- package/dist/src/util/softlink.d.ts +33 -0
- package/dist/src/util/softlink.d.ts.map +1 -0
- package/dist/src/util/softlink.js +43 -0
- package/dist/src/validation/credentials.d.ts +19 -0
- package/dist/src/validation/credentials.d.ts.map +1 -0
- package/dist/src/validation/credentials.js +38 -0
- package/dist/src/validation/index.d.ts +5 -0
- package/dist/src/validation/index.d.ts.map +1 -0
- package/dist/src/validation/index.js +3 -0
- package/dist/src/validation/projects-index.d.ts +13 -0
- package/dist/src/validation/projects-index.d.ts.map +1 -0
- package/dist/src/validation/projects-index.js +37 -0
- package/dist/src/validation/testcase.d.ts +13 -0
- package/dist/src/validation/testcase.d.ts.map +1 -0
- package/dist/src/validation/testcase.js +53 -0
- package/dist/test/cli/extract-contract.test.d.ts +2 -0
- package/dist/test/cli/extract-contract.test.d.ts.map +1 -0
- package/dist/test/cli/extract-contract.test.js +32 -0
- package/dist/test/cli/generate-testcase.test.d.ts +2 -0
- package/dist/test/cli/generate-testcase.test.d.ts.map +1 -0
- package/dist/test/cli/generate-testcase.test.js +130 -0
- package/dist/test/cli/index.test.d.ts +2 -0
- package/dist/test/cli/index.test.d.ts.map +1 -0
- package/dist/test/cli/index.test.js +93 -0
- package/dist/test/cli/run.test.d.ts +2 -0
- package/dist/test/cli/run.test.d.ts.map +1 -0
- package/dist/test/cli/run.test.js +149 -0
- package/dist/test/cli/spec.test.d.ts +2 -0
- package/dist/test/cli/spec.test.d.ts.map +1 -0
- package/dist/test/cli/spec.test.js +196 -0
- package/dist/test/stage-context.test.js +145 -13
- package/dist/test/util/credentials.test.d.ts +2 -0
- package/dist/test/util/credentials.test.d.ts.map +1 -0
- package/dist/test/util/credentials.test.js +64 -0
- package/dist/test/util/i18n-testcase.test.d.ts +2 -0
- package/dist/test/util/i18n-testcase.test.d.ts.map +1 -0
- package/dist/test/util/i18n-testcase.test.js +119 -0
- package/dist/test/util/softlink.test.d.ts +2 -0
- package/dist/test/util/softlink.test.d.ts.map +1 -0
- package/dist/test/util/softlink.test.js +82 -0
- package/dist/test/validation/credentials.test.d.ts +2 -0
- package/dist/test/validation/credentials.test.d.ts.map +1 -0
- package/dist/test/validation/credentials.test.js +72 -0
- package/dist/test/validation/projects-index.test.d.ts +2 -0
- package/dist/test/validation/projects-index.test.d.ts.map +1 -0
- package/dist/test/validation/projects-index.test.js +48 -0
- package/dist/test/validation/testcase.test.d.ts +2 -0
- package/dist/test/validation/testcase.test.d.ts.map +1 -0
- package/dist/test/validation/testcase.test.js +129 -0
- package/docs/README.md +6 -6
- package/docs/mvp-usage-guide.md +3 -3
- package/package.json +9 -4
- package/codex-skill/ep-stage/create-project/SKILL.md +0 -59
- package/codex-skill/ep-stage/glue-test/SKILL.md +0 -258
- package/codex-skill/ep-stage/glue-test/references/crud-pipeline.md +0 -139
- package/codex-skill/ep-stage/glue-testcase/SKILL.md +0 -31
- package/codex-skill/ep-stage/glue-testcase/references/testcase-schema.md +0 -67
- /package/codex-skill/ep-stage/{glue-testcase → glue-generate-testcase}/examples/observable-testcase.json +0 -0
- /package/codex-skill/ep-stage/{glue-test → glue-run-test}/references/gap-review-protocol.md +0 -0
- /package/codex-skill/ep-stage/{glue-test → glue-run-test}/references/harness-principles.md +0 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const EVIDENCE_RE = /^(html|java|spec|contract|skeleton|hint)=.+$/;
|
|
2
|
+
/**
|
|
3
|
+
* 校验 testcase.json5 v2 schema(REQ-DATA-02 / REQ-SCHEMA-01)。
|
|
4
|
+
*
|
|
5
|
+
* 必含 schemaVersion:"v2" + scenario + menuPath + requiredSystems[] + requiredRoles[] +
|
|
6
|
+
* coveredActions[] + uncoveredCandidates[] + cases[];evidence 元素符合 <kind>=<ref>。
|
|
7
|
+
*
|
|
8
|
+
* @param obj - 从 JSON5 解析出来的原始对象。
|
|
9
|
+
* @returns 归一化后的 TestcaseV2。
|
|
10
|
+
* @throws 缺字段 / 版本不匹配 / evidence 格式错时抛结构化错误。
|
|
11
|
+
*/
|
|
12
|
+
export function validateTestcase(obj) {
|
|
13
|
+
if (!obj || typeof obj !== 'object')
|
|
14
|
+
throw new Error('testcase.json5 顶层必须是对象');
|
|
15
|
+
const raw = obj;
|
|
16
|
+
if (raw.schemaVersion !== 'v2') {
|
|
17
|
+
throw new Error('testcase.json5 schemaVersion 不是 v2(可能是 v1)。请重跑 `ep-stage-skill testcase` 升级。');
|
|
18
|
+
}
|
|
19
|
+
for (const f of ['scenario', 'menuPath', 'moduleId', 'moduleName']) {
|
|
20
|
+
if (typeof raw[f] !== 'string')
|
|
21
|
+
throw new Error(`testcase.json5 缺 ${f}`);
|
|
22
|
+
}
|
|
23
|
+
for (const f of ['requiredSystems', 'requiredRoles', 'coveredActions', 'uncoveredCandidates', 'cases']) {
|
|
24
|
+
if (!Array.isArray(raw[f]))
|
|
25
|
+
throw new Error(`testcase.json5 缺 ${f}[]`);
|
|
26
|
+
}
|
|
27
|
+
const checkEvidence = (ev) => {
|
|
28
|
+
if (typeof ev !== 'string' || !EVIDENCE_RE.test(ev)) {
|
|
29
|
+
throw new Error(`evidence 必须为 <kind>=<ref> 格式(kind ∈ html/java/spec/contract/skeleton/hint),实际: ${String(ev)}`);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
for (const a of raw.coveredActions) {
|
|
33
|
+
if (!Array.isArray(a.evidence))
|
|
34
|
+
throw new Error('coveredActions[].evidence 必须为数组');
|
|
35
|
+
a.evidence.forEach(checkEvidence);
|
|
36
|
+
}
|
|
37
|
+
for (const item of raw.uncoveredCandidates) {
|
|
38
|
+
if (typeof item.requiredRole !== 'string' || !item.requiredRole.trim()) {
|
|
39
|
+
throw new Error('uncoveredCandidates[].requiredRole 必填');
|
|
40
|
+
}
|
|
41
|
+
if (!Array.isArray(item.evidence))
|
|
42
|
+
throw new Error('uncoveredCandidates[].evidence 必须为数组');
|
|
43
|
+
item.evidence.forEach(checkEvidence);
|
|
44
|
+
}
|
|
45
|
+
for (const c of raw.cases) {
|
|
46
|
+
if (typeof c.requiredRole !== 'string' || !c.requiredRole.trim()) {
|
|
47
|
+
throw new Error('cases[].requiredRole 必填');
|
|
48
|
+
}
|
|
49
|
+
if (Array.isArray(c.evidence))
|
|
50
|
+
c.evidence.forEach(checkEvidence);
|
|
51
|
+
}
|
|
52
|
+
return raw;
|
|
53
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extract-contract.test.d.ts","sourceRoot":"","sources":["../../../test/cli/extract-contract.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dev/extract-contract.ts 入参解析单元测试。
|
|
3
|
+
*
|
|
4
|
+
* 实际 contract 提取已在 crud-contract.test.ts 端到端覆盖;本测试只检查参数解析
|
|
5
|
+
* 边界,避免依赖主仓 knowledge-project 物料(受顶级硬约束保护、本 worktree 可能未填充)。
|
|
6
|
+
*/
|
|
7
|
+
import { describe, expect, it } from 'vitest';
|
|
8
|
+
import { runExtractContract } from '../../src/cli/dev/extract-contract.js';
|
|
9
|
+
describe('runExtractContract 入参解析', () => {
|
|
10
|
+
it('显式使用废弃 --module 时抛错', () => {
|
|
11
|
+
expect(() => runExtractContract([
|
|
12
|
+
'--module', 'zwplace',
|
|
13
|
+
'--docs', '/tmp/docs',
|
|
14
|
+
'--code-list', '/tmp/code_list.md',
|
|
15
|
+
'--webapp', '/tmp/webapp',
|
|
16
|
+
'--java-actions', '/tmp/java',
|
|
17
|
+
'--out', '/tmp/out.json',
|
|
18
|
+
])).toThrow(/已废弃 --module/);
|
|
19
|
+
});
|
|
20
|
+
it('缺失 --module-id 时抛错', () => {
|
|
21
|
+
expect(() => runExtractContract([
|
|
22
|
+
'--docs', '/tmp/docs',
|
|
23
|
+
'--code-list', '/tmp/code_list.md',
|
|
24
|
+
'--webapp', '/tmp/webapp',
|
|
25
|
+
'--java-actions', '/tmp/java',
|
|
26
|
+
'--out', '/tmp/out.json',
|
|
27
|
+
])).toThrow(/Missing required argument --module-id/);
|
|
28
|
+
});
|
|
29
|
+
it('参数对非法时抛错', () => {
|
|
30
|
+
expect(() => runExtractContract(['--module-id'])).toThrow(/Invalid argument pair/);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generate-testcase.test.d.ts","sourceRoot":"","sources":["../../../test/cli/generate-testcase.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* generate-testcase.ts 单元测试。
|
|
3
|
+
*
|
|
4
|
+
* 覆盖:
|
|
5
|
+
* - parseGenerateTestcaseArgs 缺参 / 非法参数对 / -- 分隔符
|
|
6
|
+
* - decideScenario 3 档优先级(CLI 参数 / hints / 默认)
|
|
7
|
+
* - renderTestcaseMarkdown:i18n 字段名 / 状态值;硬编码「未确认项」标题;
|
|
8
|
+
* `unresolved` 字段标题与状态值映射不冲突。
|
|
9
|
+
*
|
|
10
|
+
* 端到端 testcase 子命令(含 probe / 文件落盘)依赖主仓 knowledge-project 物料,
|
|
11
|
+
* 由 Phase 5 真机验收覆盖,此处不重复。
|
|
12
|
+
*/
|
|
13
|
+
import { describe, expect, it } from 'vitest';
|
|
14
|
+
import { decideScenario, parseGenerateTestcaseArgs, renderTestcaseMarkdown, } from '../../src/cli/generate-testcase.js';
|
|
15
|
+
function makeTestcase(overrides = {}) {
|
|
16
|
+
return {
|
|
17
|
+
schemaVersion: 'v2',
|
|
18
|
+
scenario: 'crud.single-page',
|
|
19
|
+
moduleId: 'zwplace',
|
|
20
|
+
moduleName: '场所窗口信息管理',
|
|
21
|
+
menuPath: '业务管理 > 场所窗口信息',
|
|
22
|
+
requiredSystems: [{ url: 'https://example.com', systemName: '示例系统' }],
|
|
23
|
+
requiredRoles: [{ role: 'admin', hint: '后台管理员' }],
|
|
24
|
+
coveredActions: [
|
|
25
|
+
{ actionId: 'create', kind: '新增', evidence: ['contract=crud-business-module/v1'] },
|
|
26
|
+
],
|
|
27
|
+
uncoveredCandidates: [
|
|
28
|
+
{
|
|
29
|
+
actionId: 'export',
|
|
30
|
+
label: '导出 CSV',
|
|
31
|
+
status: 'planned',
|
|
32
|
+
requiredRole: 'admin',
|
|
33
|
+
businessIntent: '导出列表为 CSV 文件',
|
|
34
|
+
assertionExpectation: '下载完成且文件非空',
|
|
35
|
+
evidence: ['contract=crud-business-module/v1'],
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
cases: [
|
|
39
|
+
{
|
|
40
|
+
caseId: 'zwplace.create',
|
|
41
|
+
title: '新增场所窗口',
|
|
42
|
+
scenario: 'crud.single-page',
|
|
43
|
+
reviewStatus: 'needs_review',
|
|
44
|
+
requiredRole: 'admin',
|
|
45
|
+
evidence: ['contract=crud-business-module/v1'],
|
|
46
|
+
steps: ['打开新增弹窗', '提交表单'],
|
|
47
|
+
assertions: ['弹窗关闭', '列表多一条'],
|
|
48
|
+
unresolved: ['菜单路径待确认'],
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
reasoningSummary: {
|
|
52
|
+
conclusion: '基于契约推理生成 1 条用例',
|
|
53
|
+
evidenceChain: ['contract=crud-business-module/v1'],
|
|
54
|
+
alternatives: [],
|
|
55
|
+
confidence: 'medium',
|
|
56
|
+
risks: [],
|
|
57
|
+
needsHumanReview: true,
|
|
58
|
+
},
|
|
59
|
+
...overrides,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
describe('parseGenerateTestcaseArgs', () => {
|
|
63
|
+
it('缺 --code-list 时抛错', () => {
|
|
64
|
+
expect(() => parseGenerateTestcaseArgs(['--tests-dir', '/x'])).toThrow(/缺少 --code-list/);
|
|
65
|
+
});
|
|
66
|
+
it('缺 --tests-dir 时抛错', () => {
|
|
67
|
+
expect(() => parseGenerateTestcaseArgs(['--code-list', '/x'])).toThrow(/缺少 --tests-dir/);
|
|
68
|
+
});
|
|
69
|
+
it('参数对非法时抛错', () => {
|
|
70
|
+
expect(() => parseGenerateTestcaseArgs(['--code-list'])).toThrow(/无效参数对/);
|
|
71
|
+
});
|
|
72
|
+
it('-- 分隔符被忽略', () => {
|
|
73
|
+
const args = parseGenerateTestcaseArgs(['--', '--code-list', '/a', '--tests-dir', '/b']);
|
|
74
|
+
expect(args.codeList).toBe('/a');
|
|
75
|
+
expect(args.testsDir).toBe('/b');
|
|
76
|
+
});
|
|
77
|
+
it('解析 scenario / hints / project-dir / requirement-dir', () => {
|
|
78
|
+
const args = parseGenerateTestcaseArgs([
|
|
79
|
+
'--code-list', '/a',
|
|
80
|
+
'--tests-dir', '/b',
|
|
81
|
+
'--scenario', 'statistic',
|
|
82
|
+
'--hints', '/h.json',
|
|
83
|
+
'--project-dir', '/p',
|
|
84
|
+
'--requirement-dir', '/r',
|
|
85
|
+
]);
|
|
86
|
+
expect(args.scenario).toBe('statistic');
|
|
87
|
+
expect(args.hints).toBe('/h.json');
|
|
88
|
+
expect(args.projectDir).toBe('/p');
|
|
89
|
+
expect(args.requirementDir).toBe('/r');
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
describe('decideScenario 3 档优先级', () => {
|
|
93
|
+
it('①档 --scenario CLI 参数优先', () => {
|
|
94
|
+
expect(decideScenario({ codeList: '', testsDir: '', scenario: 'approval', hints: '/path?scenario=statistic' })).toBe('approval');
|
|
95
|
+
});
|
|
96
|
+
it('②档 hints 路径含 scenario=', () => {
|
|
97
|
+
expect(decideScenario({ codeList: '', testsDir: '', hints: '/path?scenario=statistic.kpi' })).toBe('statistic.kpi');
|
|
98
|
+
});
|
|
99
|
+
it('③档默认 crud.single-page', () => {
|
|
100
|
+
expect(decideScenario({ codeList: '', testsDir: '' })).toBe('crud.single-page');
|
|
101
|
+
});
|
|
102
|
+
it('hints 路径不含 scenario= 时回落默认', () => {
|
|
103
|
+
expect(decideScenario({ codeList: '', testsDir: '', hints: '/h.json' })).toBe('crud.single-page');
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
describe('renderTestcaseMarkdown', () => {
|
|
107
|
+
it('字段名翻译为中文', () => {
|
|
108
|
+
const md = renderTestcaseMarkdown(makeTestcase());
|
|
109
|
+
expect(md).toMatch(/场景类型:单页 CRUD/);
|
|
110
|
+
expect(md).toMatch(/菜单路径:业务管理/);
|
|
111
|
+
expect(md).toMatch(/操作角色:admin/);
|
|
112
|
+
expect(md).toMatch(/骨架已命中动作/);
|
|
113
|
+
expect(md).toMatch(/骨架未命中候选/);
|
|
114
|
+
});
|
|
115
|
+
it('状态值翻译为中文', () => {
|
|
116
|
+
const md = renderTestcaseMarkdown(makeTestcase());
|
|
117
|
+
expect(md).toMatch(/状态:已计划/); // planned
|
|
118
|
+
expect(md).toMatch(/审阅状态:待审阅/); // needs_review
|
|
119
|
+
});
|
|
120
|
+
it('未确认项字段标题硬编码「未确认项」(不被状态值映射污染)', () => {
|
|
121
|
+
const md = renderTestcaseMarkdown(makeTestcase());
|
|
122
|
+
expect(md).toMatch(/未确认项:菜单路径待确认/);
|
|
123
|
+
// i18nTestcase('unresolved') 仍为「未解决」,不允许出现在字段标题位置
|
|
124
|
+
expect(md).not.toMatch(/^\s*-\s+未解决:/m);
|
|
125
|
+
});
|
|
126
|
+
it('无 uncoveredCandidates 时显示「(无)」', () => {
|
|
127
|
+
const md = renderTestcaseMarkdown(makeTestcase({ uncoveredCandidates: [] }));
|
|
128
|
+
expect(md).toMatch(/骨架未命中候选\n(无)/);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["../../../test/cli/index.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/cli/index.ts 路由测试(REQ-CLI-02 修订)。
|
|
3
|
+
*
|
|
4
|
+
* 覆盖:
|
|
5
|
+
* - --help / -h 打印 testcase / spec / run 3 子命令且不含 contract
|
|
6
|
+
* - 未知子命令 → process.exit(1)
|
|
7
|
+
* - 路由到 parseGenerateTestcaseArgs / parseSpecArgs / parseRunArgs
|
|
8
|
+
*
|
|
9
|
+
* 用 vi.mock 替换 generate-testcase / spec / run 模块,避免占位 throw 干扰路由判定。
|
|
10
|
+
*/
|
|
11
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
12
|
+
const generateTestcase = vi.hoisted(() => ({
|
|
13
|
+
parseGenerateTestcaseArgs: vi.fn((argv) => ({ codeList: 'parsed', testsDir: argv.join(' ') })),
|
|
14
|
+
runGenerateTestcase: vi.fn(() => 'TESTCASE_OK'),
|
|
15
|
+
}));
|
|
16
|
+
const spec = vi.hoisted(() => ({
|
|
17
|
+
parseSpecArgs: vi.fn((argv) => ({ testcase: 'parsed', out: argv.join(' ') })),
|
|
18
|
+
runSpec: vi.fn(() => 'SPEC_OK'),
|
|
19
|
+
}));
|
|
20
|
+
const run = vi.hoisted(() => ({
|
|
21
|
+
parseRunArgs: vi.fn((argv) => ({ spec: 'parsed', testcase: argv.join(' '), headless: true })),
|
|
22
|
+
runRun: vi.fn(async () => 'RUN_OK'),
|
|
23
|
+
}));
|
|
24
|
+
vi.mock('../../src/cli/generate-testcase.js', () => generateTestcase);
|
|
25
|
+
vi.mock('../../src/cli/spec.js', () => spec);
|
|
26
|
+
vi.mock('../../src/cli/run.js', () => run);
|
|
27
|
+
const { main, printHelp } = await import('../../src/cli/index.js');
|
|
28
|
+
describe('ep-stage-skill CLI 路由', () => {
|
|
29
|
+
let logSpy;
|
|
30
|
+
let errSpy;
|
|
31
|
+
let exitSpy;
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
|
|
34
|
+
errSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
35
|
+
exitSpy = vi.spyOn(process, 'exit').mockImplementation(((code) => {
|
|
36
|
+
throw new Error(`__exit_${code ?? 0}__`);
|
|
37
|
+
}));
|
|
38
|
+
});
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
logSpy.mockRestore();
|
|
41
|
+
errSpy.mockRestore();
|
|
42
|
+
exitSpy.mockRestore();
|
|
43
|
+
vi.clearAllMocks();
|
|
44
|
+
});
|
|
45
|
+
it('--help 打印 3 子命令 testcase/spec/run 且不注册 contract 子命令', async () => {
|
|
46
|
+
await main(['node', 'cli', '--help']);
|
|
47
|
+
const printed = logSpy.mock.calls.flat().join('\n');
|
|
48
|
+
expect(printed).toMatch(/ep-stage-skill testcase /);
|
|
49
|
+
expect(printed).toMatch(/ep-stage-skill spec /);
|
|
50
|
+
expect(printed).toMatch(/ep-stage-skill run /);
|
|
51
|
+
// 反向断言:不存在 `ep-stage-skill contract` 子命令入口
|
|
52
|
+
expect(printed).not.toMatch(/ep-stage-skill contract\b/);
|
|
53
|
+
});
|
|
54
|
+
it('-h 等价于 --help', async () => {
|
|
55
|
+
await main(['node', 'cli', '-h']);
|
|
56
|
+
expect(logSpy.mock.calls.flat().join('\n')).toMatch(/testcase/);
|
|
57
|
+
});
|
|
58
|
+
it('无参数时打印 help(不 exit)', async () => {
|
|
59
|
+
await main(['node', 'cli']);
|
|
60
|
+
expect(logSpy.mock.calls.flat().join('\n')).toMatch(/testcase/);
|
|
61
|
+
expect(exitSpy).not.toHaveBeenCalled();
|
|
62
|
+
});
|
|
63
|
+
it('printHelp 不注册 contract 子命令', () => {
|
|
64
|
+
printHelp();
|
|
65
|
+
expect(logSpy.mock.calls.flat().join('\n')).not.toMatch(/ep-stage-skill contract\b/);
|
|
66
|
+
});
|
|
67
|
+
it('未知子命令 exit 1 + stderr', async () => {
|
|
68
|
+
await expect(main(['node', 'cli', 'unknown-cmd'])).rejects.toThrow(/__exit_1__/);
|
|
69
|
+
expect(errSpy.mock.calls.flat().join('\n')).toMatch(/未知子命令/);
|
|
70
|
+
});
|
|
71
|
+
it('testcase 子命令路由到 runGenerateTestcase', async () => {
|
|
72
|
+
const result = await main(['node', 'cli', 'testcase', '--code-list', '/x']);
|
|
73
|
+
expect(generateTestcase.parseGenerateTestcaseArgs).toHaveBeenCalledWith(['--code-list', '/x']);
|
|
74
|
+
expect(generateTestcase.runGenerateTestcase).toHaveBeenCalled();
|
|
75
|
+
expect(result).toBe('TESTCASE_OK');
|
|
76
|
+
});
|
|
77
|
+
it('spec 子命令路由到 runSpec', async () => {
|
|
78
|
+
const result = await main(['node', 'cli', 'spec', '--testcase', '/t', '--out', '/o']);
|
|
79
|
+
expect(spec.parseSpecArgs).toHaveBeenCalledWith(['--testcase', '/t', '--out', '/o']);
|
|
80
|
+
expect(spec.runSpec).toHaveBeenCalled();
|
|
81
|
+
expect(result).toBe('SPEC_OK');
|
|
82
|
+
});
|
|
83
|
+
it('run 子命令路由到 runRun', async () => {
|
|
84
|
+
const result = await main(['node', 'cli', 'run', '--spec', '/s', '--testcase', '/t']);
|
|
85
|
+
expect(run.parseRunArgs).toHaveBeenCalledWith(['--spec', '/s', '--testcase', '/t']);
|
|
86
|
+
expect(run.runRun).toHaveBeenCalled();
|
|
87
|
+
expect(result).toBe('RUN_OK');
|
|
88
|
+
});
|
|
89
|
+
it('-- 分隔符被忽略', async () => {
|
|
90
|
+
await main(['node', 'cli', 'testcase', '--', '--code-list', '/x']);
|
|
91
|
+
expect(generateTestcase.parseGenerateTestcaseArgs).toHaveBeenCalledWith(['--code-list', '/x']);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.test.d.ts","sourceRoot":"","sources":["../../../test/cli/run.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* run.ts 单元测试(REQ-CHAIN-02 / REQ-SPEC-01)。
|
|
3
|
+
*
|
|
4
|
+
* 覆盖:
|
|
5
|
+
* - parseRunArgs 缺参 / 非法 / --headless 默认 true / --headless false 关闭无头;
|
|
6
|
+
* - runRun:
|
|
7
|
+
* · v1 spec → 追加 AI 段产 v2(含 AI_APPEND_MARKER);
|
|
8
|
+
* · 已有 AI 追加段时重跑覆盖(幂等);
|
|
9
|
+
* · planned 为空时擦掉历史追加段(幂等收敛);
|
|
10
|
+
* · 不存在 spec 时抛错;
|
|
11
|
+
* · 报告目录基于 projectDir 推导(ISO 时间戳)。
|
|
12
|
+
*/
|
|
13
|
+
import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
|
|
14
|
+
import os from 'node:os';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
17
|
+
import { AI_APPEND_MARKER, parseRunArgs, runRun } from '../../src/cli/run.js';
|
|
18
|
+
let tmpRoot;
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
tmpRoot = mkdtempSync(path.join(os.tmpdir(), 'ep-stage-run-'));
|
|
21
|
+
});
|
|
22
|
+
function makeTestcase(plannedCount = 1) {
|
|
23
|
+
const uncoveredCandidates = Array.from({ length: plannedCount }, (_, i) => ({
|
|
24
|
+
actionId: `gap-${i}`,
|
|
25
|
+
label: `导出 CSV ${i}`,
|
|
26
|
+
status: 'planned',
|
|
27
|
+
requiredRole: 'admin',
|
|
28
|
+
businessIntent: `导出列表 ${i}`,
|
|
29
|
+
assertionExpectation: `下载完成 ${i}`,
|
|
30
|
+
evidence: ['contract=crud-business-module/v1'],
|
|
31
|
+
}));
|
|
32
|
+
return {
|
|
33
|
+
schemaVersion: 'v2',
|
|
34
|
+
scenario: 'crud.single-page',
|
|
35
|
+
moduleId: 'zwplace',
|
|
36
|
+
moduleName: '场所窗口信息管理',
|
|
37
|
+
menuPath: '业务管理 > 场所',
|
|
38
|
+
requiredSystems: [{ url: '' }],
|
|
39
|
+
requiredRoles: [{ role: 'admin' }],
|
|
40
|
+
coveredActions: [],
|
|
41
|
+
uncoveredCandidates,
|
|
42
|
+
cases: [
|
|
43
|
+
{
|
|
44
|
+
caseId: 'zwplace.create',
|
|
45
|
+
title: '新增',
|
|
46
|
+
scenario: 'crud.single-page',
|
|
47
|
+
reviewStatus: 'needs_review',
|
|
48
|
+
requiredRole: 'admin',
|
|
49
|
+
evidence: ['contract=crud-business-module/v1'],
|
|
50
|
+
steps: [],
|
|
51
|
+
assertions: [],
|
|
52
|
+
unresolved: [],
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
reasoningSummary: {
|
|
56
|
+
conclusion: '',
|
|
57
|
+
evidenceChain: [],
|
|
58
|
+
alternatives: [],
|
|
59
|
+
confidence: 'medium',
|
|
60
|
+
risks: [],
|
|
61
|
+
needsHumanReview: true,
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function writeFixtures(opts) {
|
|
66
|
+
const dir = path.join(tmpRoot, 'req');
|
|
67
|
+
mkdirSync(dir, { recursive: true });
|
|
68
|
+
const specPath = path.join(dir, 'crud.spec.ts');
|
|
69
|
+
const testcasePath = path.join(dir, 'testcase.json5');
|
|
70
|
+
writeFileSync(specPath, opts.specBody, 'utf8');
|
|
71
|
+
writeFileSync(testcasePath, JSON.stringify(makeTestcase(opts.plannedCount ?? 1)), 'utf8');
|
|
72
|
+
return { specPath, testcasePath };
|
|
73
|
+
}
|
|
74
|
+
describe('parseRunArgs', () => {
|
|
75
|
+
it('缺 --spec 时抛错', () => {
|
|
76
|
+
expect(() => parseRunArgs(['--testcase', '/x'])).toThrow(/缺少 --spec/);
|
|
77
|
+
});
|
|
78
|
+
it('缺 --testcase 时抛错', () => {
|
|
79
|
+
expect(() => parseRunArgs(['--spec', '/x'])).toThrow(/缺少 --testcase/);
|
|
80
|
+
});
|
|
81
|
+
it('参数对非法时抛错', () => {
|
|
82
|
+
expect(() => parseRunArgs(['--spec'])).toThrow(/无效参数对/);
|
|
83
|
+
});
|
|
84
|
+
it('--headless 默认 true', () => {
|
|
85
|
+
const args = parseRunArgs(['--spec', '/s', '--testcase', '/t']);
|
|
86
|
+
expect(args.headless).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
it('--headless false 关闭无头', () => {
|
|
89
|
+
const args = parseRunArgs(['--spec', '/s', '--testcase', '/t', '--headless', 'false']);
|
|
90
|
+
expect(args.headless).toBe(false);
|
|
91
|
+
});
|
|
92
|
+
it('--project-dir 透传', () => {
|
|
93
|
+
const args = parseRunArgs(['--spec', '/s', '--testcase', '/t', '--project-dir', '/p']);
|
|
94
|
+
expect(args.projectDir).toBe('/p');
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
describe('runRun', () => {
|
|
98
|
+
it('v1 spec 追加 AI 段产 v2', async () => {
|
|
99
|
+
const v1Body = '// === 胶水模式生成 ===\nconst x = 1;\n';
|
|
100
|
+
const { specPath, testcasePath } = writeFixtures({ specBody: v1Body });
|
|
101
|
+
const result = await runRun({
|
|
102
|
+
spec: specPath,
|
|
103
|
+
testcase: testcasePath,
|
|
104
|
+
headless: true,
|
|
105
|
+
projectDir: tmpRoot,
|
|
106
|
+
});
|
|
107
|
+
expect(result.version).toBe('v1-with-planned-stubs');
|
|
108
|
+
const body = readFileSync(specPath, 'utf8');
|
|
109
|
+
expect(body).toContain(AI_APPEND_MARKER);
|
|
110
|
+
expect(body).toMatch(/test\("导出 CSV 0"/);
|
|
111
|
+
expect(body).toMatch(/requiredRole: admin/);
|
|
112
|
+
expect(result.reportDir.startsWith(path.join(tmpRoot, 'e2e/glue-code-mvp/glue-report'))).toBe(true);
|
|
113
|
+
});
|
|
114
|
+
it('已有 AI 追加段时重跑覆盖(幂等)', async () => {
|
|
115
|
+
const v1Body = '// === 胶水模式生成 ===\nconst x = 1;\n';
|
|
116
|
+
const oldAppended = `\n\n${AI_APPEND_MARKER}\ntest("旧追加段", async () => {});\n`;
|
|
117
|
+
const { specPath, testcasePath } = writeFixtures({ specBody: v1Body + oldAppended });
|
|
118
|
+
await runRun({ spec: specPath, testcase: testcasePath, headless: true });
|
|
119
|
+
const body = readFileSync(specPath, 'utf8');
|
|
120
|
+
// 旧追加段被擦掉,只剩当次推理结果
|
|
121
|
+
expect(body).not.toMatch(/旧追加段/);
|
|
122
|
+
// 标记只出现一次(重跑不叠加)
|
|
123
|
+
expect(body.match(/AI 钻探追加/g)?.length).toBe(1);
|
|
124
|
+
expect(body).toMatch(/test\("导出 CSV 0"/);
|
|
125
|
+
});
|
|
126
|
+
it('planned 为空时擦掉历史追加段(幂等收敛)', async () => {
|
|
127
|
+
const v1Body = '// === 胶水模式生成 ===\nconst x = 1;';
|
|
128
|
+
const oldAppended = `\n\n${AI_APPEND_MARKER}\ntest("旧", async () => {});\n`;
|
|
129
|
+
const { specPath, testcasePath } = writeFixtures({
|
|
130
|
+
specBody: v1Body + oldAppended,
|
|
131
|
+
plannedCount: 0,
|
|
132
|
+
});
|
|
133
|
+
await runRun({ spec: specPath, testcase: testcasePath, headless: true });
|
|
134
|
+
const body = readFileSync(specPath, 'utf8');
|
|
135
|
+
expect(body).not.toContain(AI_APPEND_MARKER);
|
|
136
|
+
expect(body).not.toMatch(/旧追加段|旧/);
|
|
137
|
+
});
|
|
138
|
+
it('spec 不存在时抛错', async () => {
|
|
139
|
+
const dir = path.join(tmpRoot, 'r2');
|
|
140
|
+
mkdirSync(dir, { recursive: true });
|
|
141
|
+
const testcasePath = path.join(dir, 'testcase.json5');
|
|
142
|
+
writeFileSync(testcasePath, JSON.stringify(makeTestcase()), 'utf8');
|
|
143
|
+
await expect(runRun({
|
|
144
|
+
spec: path.join(dir, 'missing.spec.ts'),
|
|
145
|
+
testcase: testcasePath,
|
|
146
|
+
headless: true,
|
|
147
|
+
})).rejects.toThrow(/spec 不存在/);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spec.test.d.ts","sourceRoot":"","sources":["../../../test/cli/spec.test.ts"],"names":[],"mappings":""}
|