@epoint-testtech/ep-stage-skill 0.0.3-alpha.2 → 0.0.4-alpha.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.
- package/SKILL.md +2 -2
- package/codex-skill/ep-stage/.omc/state/sessions/d636ad35-5fc9-4d5e-85b0-2bcad7d86ee5/pre-tool-advisory-throttle.json +10 -0
- package/codex-skill/ep-stage/glue-create-project/SKILL.md +206 -0
- package/codex-skill/ep-stage/glue-generate-testcase/SKILL.md +274 -0
- package/codex-skill/ep-stage/glue-generate-testcase/references/testcase-schema.md +112 -0
- package/codex-skill/ep-stage/glue-generate-testcase/references/testcase-tpl.md +72 -0
- package/codex-skill/ep-stage/glue-run-test/SKILL.md +289 -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 +84 -0
- package/dist/src/cli/generate-testcase.d.ts.map +1 -0
- package/dist/src/cli/generate-testcase.js +300 -0
- package/dist/src/cli/index.d.ts +26 -0
- package/dist/src/cli/index.d.ts.map +1 -0
- package/dist/src/cli/index.js +75 -0
- package/dist/src/cli/probe.d.ts +46 -0
- package/dist/src/cli/probe.d.ts.map +1 -0
- package/dist/src/cli/probe.js +447 -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 +48 -0
- package/dist/src/cli/spec.d.ts.map +1 -0
- package/dist/src/cli/spec.js +102 -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/contracts/inference-trace.d.ts +223 -0
- package/dist/src/contracts/inference-trace.d.ts.map +1 -0
- package/dist/src/contracts/inference-trace.js +12 -0
- package/dist/src/extractors/code-list.d.ts +3 -3
- package/dist/src/extractors/code-list.d.ts.map +1 -1
- package/dist/src/extractors/code-list.js +46 -11
- package/dist/src/generators/stage-skeleton-script.d.ts.map +1 -1
- package/dist/src/generators/stage-skeleton-script.js +9 -0
- package/dist/src/index.d.ts +7 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -1
- package/dist/src/inference/module-hints-inference.d.ts +30 -0
- package/dist/src/inference/module-hints-inference.d.ts.map +1 -0
- package/dist/src/inference/module-hints-inference.js +363 -0
- package/dist/src/inference/source-analysis.d.ts +26 -0
- package/dist/src/inference/source-analysis.d.ts.map +1 -0
- package/dist/src/inference/source-analysis.js +258 -0
- 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 +6 -0
- package/dist/src/validation/index.d.ts.map +1 -0
- package/dist/src/validation/index.js +4 -0
- package/dist/src/validation/inference-trace.d.ts +13 -0
- package/dist/src/validation/inference-trace.d.ts.map +1 -0
- package/dist/src/validation/inference-trace.js +31 -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-inference.test.d.ts +2 -0
- package/dist/test/cli/generate-testcase-inference.test.d.ts.map +1 -0
- package/dist/test/cli/generate-testcase-inference.test.js +63 -0
- package/dist/test/cli/generate-testcase-paths.test.d.ts +2 -0
- package/dist/test/cli/generate-testcase-paths.test.d.ts.map +1 -0
- package/dist/test/cli/generate-testcase-paths.test.js +165 -0
- package/dist/test/cli/generate-testcase-requirements.test.d.ts +2 -0
- package/dist/test/cli/generate-testcase-requirements.test.d.ts.map +1 -0
- package/dist/test/cli/generate-testcase-requirements.test.js +229 -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 +113 -0
- package/dist/test/cli/probe.test.d.ts +2 -0
- package/dist/test/cli/probe.test.d.ts.map +1 -0
- package/dist/test/cli/probe.test.js +31 -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-boundary.test.d.ts +2 -0
- package/dist/test/cli/spec-boundary.test.d.ts.map +1 -0
- package/dist/test/cli/spec-boundary.test.js +198 -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/code-list.test.d.ts +2 -0
- package/dist/test/code-list.test.d.ts.map +1 -0
- package/dist/test/code-list.test.js +39 -0
- package/dist/test/crud-contract.test.js +12 -32
- package/dist/test/inference/module-hints-inference.test.d.ts +2 -0
- package/dist/test/inference/module-hints-inference.test.d.ts.map +1 -0
- package/dist/test/inference/module-hints-inference.test.js +25 -0
- package/dist/test/inference/source-analysis.test.d.ts +2 -0
- package/dist/test/inference/source-analysis.test.d.ts.map +1 -0
- package/dist/test/inference/source-analysis.test.js +23 -0
- package/dist/test/material-inventory.test.js +13 -0
- package/dist/test/observable-pipeline.test.js +12 -3
- 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,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,113 @@
|
|
|
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
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync, symlinkSync } from 'node:fs';
|
|
13
|
+
import os from 'node:os';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { pathToFileURL } from 'node:url';
|
|
16
|
+
const generateTestcase = vi.hoisted(() => ({
|
|
17
|
+
parseGenerateTestcaseArgs: vi.fn((argv) => ({ codeList: 'parsed', testsDir: argv.join(' ') })),
|
|
18
|
+
runGenerateTestcase: vi.fn(() => 'TESTCASE_OK'),
|
|
19
|
+
}));
|
|
20
|
+
const spec = vi.hoisted(() => ({
|
|
21
|
+
parseSpecArgs: vi.fn((argv) => ({ testcase: 'parsed', out: argv.join(' ') })),
|
|
22
|
+
runSpec: vi.fn(() => 'SPEC_OK'),
|
|
23
|
+
}));
|
|
24
|
+
const run = vi.hoisted(() => ({
|
|
25
|
+
parseRunArgs: vi.fn((argv) => ({ spec: 'parsed', testcase: argv.join(' '), headless: true })),
|
|
26
|
+
runRun: vi.fn(async () => 'RUN_OK'),
|
|
27
|
+
}));
|
|
28
|
+
vi.mock('../../src/cli/generate-testcase.js', () => generateTestcase);
|
|
29
|
+
vi.mock('../../src/cli/spec.js', () => spec);
|
|
30
|
+
vi.mock('../../src/cli/run.js', () => run);
|
|
31
|
+
const { main, printHelp } = await import('../../src/cli/index.js');
|
|
32
|
+
describe('ep-stage-skill CLI 路由', () => {
|
|
33
|
+
let logSpy;
|
|
34
|
+
let errSpy;
|
|
35
|
+
let exitSpy;
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
|
|
38
|
+
errSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
39
|
+
exitSpy = vi.spyOn(process, 'exit').mockImplementation(((code) => {
|
|
40
|
+
throw new Error(`__exit_${code ?? 0}__`);
|
|
41
|
+
}));
|
|
42
|
+
});
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
logSpy.mockRestore();
|
|
45
|
+
errSpy.mockRestore();
|
|
46
|
+
exitSpy.mockRestore();
|
|
47
|
+
vi.clearAllMocks();
|
|
48
|
+
});
|
|
49
|
+
it('--help 打印 3 子命令 testcase/spec/run 且不注册 contract 子命令', async () => {
|
|
50
|
+
await main(['node', 'cli', '--help']);
|
|
51
|
+
const printed = logSpy.mock.calls.flat().join('\n');
|
|
52
|
+
expect(printed).toMatch(/ep-stage-skill testcase /);
|
|
53
|
+
expect(printed).toMatch(/ep-stage-skill spec /);
|
|
54
|
+
expect(printed).toMatch(/ep-stage-skill run /);
|
|
55
|
+
// 反向断言:不存在 `ep-stage-skill contract` 子命令入口
|
|
56
|
+
expect(printed).not.toMatch(/ep-stage-skill contract\b/);
|
|
57
|
+
});
|
|
58
|
+
it('-h 等价于 --help', async () => {
|
|
59
|
+
await main(['node', 'cli', '-h']);
|
|
60
|
+
expect(logSpy.mock.calls.flat().join('\n')).toMatch(/testcase/);
|
|
61
|
+
});
|
|
62
|
+
it('无参数时打印 help(不 exit)', async () => {
|
|
63
|
+
await main(['node', 'cli']);
|
|
64
|
+
expect(logSpy.mock.calls.flat().join('\n')).toMatch(/testcase/);
|
|
65
|
+
expect(exitSpy).not.toHaveBeenCalled();
|
|
66
|
+
});
|
|
67
|
+
it('printHelp 不注册 contract 子命令', () => {
|
|
68
|
+
printHelp();
|
|
69
|
+
expect(logSpy.mock.calls.flat().join('\n')).not.toMatch(/ep-stage-skill contract\b/);
|
|
70
|
+
});
|
|
71
|
+
it('未知子命令 exit 1 + stderr', async () => {
|
|
72
|
+
await expect(main(['node', 'cli', 'unknown-cmd'])).rejects.toThrow(/__exit_1__/);
|
|
73
|
+
expect(errSpy.mock.calls.flat().join('\n')).toMatch(/未知子命令/);
|
|
74
|
+
});
|
|
75
|
+
it('testcase 子命令路由到 runGenerateTestcase', async () => {
|
|
76
|
+
const result = await main(['node', 'cli', 'testcase', '--code-list', '/x']);
|
|
77
|
+
expect(generateTestcase.parseGenerateTestcaseArgs).toHaveBeenCalledWith(['--code-list', '/x']);
|
|
78
|
+
expect(generateTestcase.runGenerateTestcase).toHaveBeenCalled();
|
|
79
|
+
expect(result).toBe('TESTCASE_OK');
|
|
80
|
+
});
|
|
81
|
+
it('spec 子命令路由到 runSpec', async () => {
|
|
82
|
+
const result = await main(['node', 'cli', 'spec', '--testcase', '/t', '--out', '/o']);
|
|
83
|
+
expect(spec.parseSpecArgs).toHaveBeenCalledWith(['--testcase', '/t', '--out', '/o']);
|
|
84
|
+
expect(spec.runSpec).toHaveBeenCalled();
|
|
85
|
+
expect(result).toBe('SPEC_OK');
|
|
86
|
+
});
|
|
87
|
+
it('run 子命令路由到 runRun', async () => {
|
|
88
|
+
const result = await main(['node', 'cli', 'run', '--spec', '/s', '--testcase', '/t']);
|
|
89
|
+
expect(run.parseRunArgs).toHaveBeenCalledWith(['--spec', '/s', '--testcase', '/t']);
|
|
90
|
+
expect(run.runRun).toHaveBeenCalled();
|
|
91
|
+
expect(result).toBe('RUN_OK');
|
|
92
|
+
});
|
|
93
|
+
it('-- 分隔符被忽略', async () => {
|
|
94
|
+
await main(['node', 'cli', 'testcase', '--', '--code-list', '/x']);
|
|
95
|
+
expect(generateTestcase.parseGenerateTestcaseArgs).toHaveBeenCalledWith(['--code-list', '/x']);
|
|
96
|
+
});
|
|
97
|
+
it('软链 CLI 入口也应识别为直接执行', async () => {
|
|
98
|
+
const mod = await import('../../src/cli/index.js');
|
|
99
|
+
const tmpDir = mkdtempSync(path.join(os.tmpdir(), 'ep-stage-skill-cli-'));
|
|
100
|
+
const realEntry = path.join(tmpDir, 'dist', 'src', 'cli', 'index.js');
|
|
101
|
+
const binEntry = path.join(tmpDir, '.bin', 'ep-stage-skill');
|
|
102
|
+
try {
|
|
103
|
+
mkdirSync(path.dirname(realEntry), { recursive: true });
|
|
104
|
+
mkdirSync(path.dirname(binEntry), { recursive: true });
|
|
105
|
+
writeFileSync(realEntry, '// mock cli entry\n', 'utf8');
|
|
106
|
+
symlinkSync(realEntry, binEntry);
|
|
107
|
+
expect(mod.isDirectExecution(pathToFileURL(realEntry).href, binEntry)).toBe(true);
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"probe.test.d.ts","sourceRoot":"","sources":["../../../test/cli/probe.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { dirname, join, resolve } from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import { runProbeAndCoverage } from '../../src/cli/probe.js';
|
|
5
|
+
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../../../..');
|
|
6
|
+
const knowledgeRoot = join(repoRoot, 'knowledge-project/epoint-web-v9.5.2');
|
|
7
|
+
const requirementDir = join(knowledgeRoot, '_docs/014-测试需求10-20260622');
|
|
8
|
+
const codeListPath = join(requirementDir, 'code_list.md');
|
|
9
|
+
const webappDir = join(knowledgeRoot, 'src/main/webapp/perpage/zwplace');
|
|
10
|
+
const javaActionsDir = join(knowledgeRoot, 'src/main/java/com/epoint/zwplace/action');
|
|
11
|
+
describe('runProbeAndCoverage spec fallback', () => {
|
|
12
|
+
it('在 code_list 缺少 spec.yaml 与 module-hints 时,仍能基于 HTML/Java 生成可审阅 testcase', () => {
|
|
13
|
+
const result = runProbeAndCoverage({
|
|
14
|
+
moduleId: 'zwplace',
|
|
15
|
+
codeListPath,
|
|
16
|
+
docsDir: requirementDir,
|
|
17
|
+
webappDir,
|
|
18
|
+
javaActionsDir,
|
|
19
|
+
scenario: 'crud.single-page',
|
|
20
|
+
});
|
|
21
|
+
expect(result.contract.dataKey.field).toBe('placename');
|
|
22
|
+
expect(result.contract.searchConditions?.[0]?.field).toBe('placename');
|
|
23
|
+
// 推理 hints 流入后,dataKey 已解析为 placename;unresolvedSlots 只剩非 dataKey 项。
|
|
24
|
+
expect(result.contract.unresolvedSlots.map((slot) => slot.slotId)).not.toContain('dataKey.selectedField');
|
|
25
|
+
expect(result.testcaseV2.moduleId).toBe('zwplace');
|
|
26
|
+
expect(result.testcaseV2.cases).toHaveLength(4);
|
|
27
|
+
// Task 3: inference trace 随 probe 返回。
|
|
28
|
+
expect(result.inferenceTrace.schemaVersion).toBe('inference-trace/v1');
|
|
29
|
+
expect(result.inferenceTrace.moduleHintsInference.dataKey.selectedField).toBe('placename');
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -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-boundary.test.d.ts","sourceRoot":"","sources":["../../../test/cli/spec-boundary.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import JSON5 from 'json5';
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
+
// 边界测试目的:验证 runSpec 只消费 testcase.json5 + contract.json,不消费 inference-trace.json。
|
|
7
|
+
// 因此 mock 掉 generateStageSkeletonCrudSpec,隔离骨架生成器的实现细节(避免 fixture 不完整导致崩溃)。
|
|
8
|
+
vi.mock('../../src/generators/stage-skeleton-script.js', () => ({
|
|
9
|
+
generateStageSkeletonCrudSpec: vi.fn(() => '// skeleton stub\n'),
|
|
10
|
+
}));
|
|
11
|
+
import { generateStageSkeletonCrudSpec } from '../../src/generators/stage-skeleton-script.js';
|
|
12
|
+
import { runSpec } from '../../src/cli/spec.js';
|
|
13
|
+
/** 构造一份最小可用的 testcase v2 对象(用例与未覆盖项均为空 → 空数组满足门禁)。 */
|
|
14
|
+
function makeBaseTestcase(overrides = {}) {
|
|
15
|
+
return {
|
|
16
|
+
schemaVersion: 'v2',
|
|
17
|
+
scenario: 'crud.single-page',
|
|
18
|
+
moduleId: 'm1',
|
|
19
|
+
moduleName: '模块',
|
|
20
|
+
menuPath: '一级>二级',
|
|
21
|
+
requiredSystems: [{ url: '', systemName: undefined }],
|
|
22
|
+
requiredRoles: [{ role: 'admin', hint: undefined }],
|
|
23
|
+
coveredActions: [],
|
|
24
|
+
uncoveredCandidates: [],
|
|
25
|
+
cases: [],
|
|
26
|
+
reasoningSummary: {
|
|
27
|
+
conclusion: 'ok',
|
|
28
|
+
evidenceChain: [],
|
|
29
|
+
alternatives: [],
|
|
30
|
+
confidence: 'high',
|
|
31
|
+
risks: [],
|
|
32
|
+
needsHumanReview: false,
|
|
33
|
+
},
|
|
34
|
+
...overrides,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/** 构造一份最小 contract(mock 了生成器,结构不需 skeleton 完备)。 */
|
|
38
|
+
function makeBaseContract(overrides = {}) {
|
|
39
|
+
return {
|
|
40
|
+
contractVersion: 'crud-business-module/v1',
|
|
41
|
+
module: { id: 'm1', label: '模块' },
|
|
42
|
+
pages: { list: { pageId: 'list', role: 'list', title: '列表', fields: [], buttons: [], grids: [] } },
|
|
43
|
+
dataKey: { field: 'name', label: '名称', availability: 'human_required', generationStrategy: 'human_required', sources: [] },
|
|
44
|
+
searchConditions: [],
|
|
45
|
+
flows: {},
|
|
46
|
+
assertions: [],
|
|
47
|
+
businessRules: [],
|
|
48
|
+
agentInferredWorkflows: [],
|
|
49
|
+
unresolvedSlots: [],
|
|
50
|
+
...overrides,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
describe('runSpec input boundary', () => {
|
|
54
|
+
let tmpDir = '';
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
vi.mocked(generateStageSkeletonCrudSpec).mockClear();
|
|
57
|
+
});
|
|
58
|
+
afterEach(() => {
|
|
59
|
+
if (tmpDir)
|
|
60
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
61
|
+
tmpDir = '';
|
|
62
|
+
});
|
|
63
|
+
/** 在 tmpDir/case 下铺好 testcase + contract 文件,返回路径句柄。 */
|
|
64
|
+
function setupFixture(testcase, contract, opts) {
|
|
65
|
+
tmpDir = mkdtempSync(join(tmpdir(), 'ep-stage-spec-boundary-'));
|
|
66
|
+
const testsDir = join(tmpDir, 'case');
|
|
67
|
+
mkdirSync(join(testsDir, '.ep-stage'), { recursive: true });
|
|
68
|
+
const testcasePath = join(testsDir, 'testcase.json5');
|
|
69
|
+
const specOut = join(testsDir, 'crud.spec.ts');
|
|
70
|
+
writeFileSync(testcasePath, JSON5.stringify(testcase, null, 2), 'utf8');
|
|
71
|
+
writeFileSync(join(testsDir, '.ep-stage', 'contract.json'), JSON.stringify(contract, null, 2), 'utf8');
|
|
72
|
+
if (opts?.writeTrace) {
|
|
73
|
+
writeFileSync(join(testsDir, '.ep-stage', 'inference-trace.json'), '{"broken": true}\n', 'utf8');
|
|
74
|
+
}
|
|
75
|
+
return { testcasePath, specOut, testsDir };
|
|
76
|
+
}
|
|
77
|
+
it('不依赖 inference-trace.json,只消费 testcase.json5 与 contract.json(REQ-INFER-008)', () => {
|
|
78
|
+
// 写一份 trace 后立即删除,断言 runSpec 不依赖它
|
|
79
|
+
const { testcasePath, specOut, testsDir } = setupFixture(makeBaseTestcase(), makeBaseContract(), { writeTrace: true });
|
|
80
|
+
rmSync(join(testsDir, '.ep-stage', 'inference-trace.json'), { force: true });
|
|
81
|
+
const result = runSpec({ testcase: testcasePath, out: specOut });
|
|
82
|
+
expect(result.specPath).toBe(specOut);
|
|
83
|
+
expect(generateStageSkeletonCrudSpec).toHaveBeenCalledTimes(1);
|
|
84
|
+
});
|
|
85
|
+
it('缺失 contract.json 时抛中文错误,引导先跑 testcase(REQ-INFER-006)', () => {
|
|
86
|
+
const { testcasePath, specOut } = setupFixture(makeBaseTestcase(), makeBaseContract());
|
|
87
|
+
// 删掉 contract.json
|
|
88
|
+
rmSync(join(tmpDir, 'case', '.ep-stage', 'contract.json'));
|
|
89
|
+
expect(() => runSpec({ testcase: testcasePath, out: specOut })).toThrow(/contract\.json/);
|
|
90
|
+
expect(generateStageSkeletonCrudSpec).not.toHaveBeenCalled();
|
|
91
|
+
});
|
|
92
|
+
it('testcase 存在 needs_review 用例时阻断 spec 组装(REQ-INFER-007)', () => {
|
|
93
|
+
const testcase = makeBaseTestcase({
|
|
94
|
+
cases: [
|
|
95
|
+
{
|
|
96
|
+
caseId: 'c1',
|
|
97
|
+
title: 't',
|
|
98
|
+
scenario: 's',
|
|
99
|
+
reviewStatus: 'needs_review',
|
|
100
|
+
requiredRole: 'admin',
|
|
101
|
+
evidence: [],
|
|
102
|
+
steps: [],
|
|
103
|
+
assertions: [],
|
|
104
|
+
unresolved: [],
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
});
|
|
108
|
+
const { testcasePath, specOut } = setupFixture(testcase, makeBaseContract());
|
|
109
|
+
expect(() => runSpec({ testcase: testcasePath, out: specOut })).toThrow(/未确认用例.*needs_review\/blocked/);
|
|
110
|
+
expect(generateStageSkeletonCrudSpec).not.toHaveBeenCalled();
|
|
111
|
+
});
|
|
112
|
+
it('testcase 存在 blocked 用例时阻断 spec 组装(REQ-INFER-007)', () => {
|
|
113
|
+
const testcase = makeBaseTestcase({
|
|
114
|
+
cases: [
|
|
115
|
+
{
|
|
116
|
+
caseId: 'c1',
|
|
117
|
+
title: 't',
|
|
118
|
+
scenario: 's',
|
|
119
|
+
reviewStatus: 'blocked',
|
|
120
|
+
requiredRole: 'admin',
|
|
121
|
+
evidence: [],
|
|
122
|
+
steps: [],
|
|
123
|
+
assertions: [],
|
|
124
|
+
unresolved: [],
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
});
|
|
128
|
+
const { testcasePath, specOut } = setupFixture(testcase, makeBaseContract());
|
|
129
|
+
expect(() => runSpec({ testcase: testcasePath, out: specOut })).toThrow(/未确认用例.*needs_review\/blocked/);
|
|
130
|
+
});
|
|
131
|
+
it('testcase 存在 unresolved 未覆盖项时阻断 spec 组装(REQ-INFER-007)', () => {
|
|
132
|
+
const testcase = makeBaseTestcase({
|
|
133
|
+
uncoveredCandidates: [
|
|
134
|
+
{
|
|
135
|
+
actionId: 'a1',
|
|
136
|
+
label: 'l',
|
|
137
|
+
status: 'unresolved',
|
|
138
|
+
requiredRole: 'admin',
|
|
139
|
+
businessIntent: 'b',
|
|
140
|
+
assertionExpectation: 'e',
|
|
141
|
+
evidence: [],
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
});
|
|
145
|
+
const { testcasePath, specOut } = setupFixture(testcase, makeBaseContract());
|
|
146
|
+
expect(() => runSpec({ testcase: testcasePath, out: specOut })).toThrow(/candidate\/unresolved 未覆盖项/);
|
|
147
|
+
expect(generateStageSkeletonCrudSpec).not.toHaveBeenCalled();
|
|
148
|
+
});
|
|
149
|
+
it('testcase 存在 candidate 未覆盖项时阻断 spec 组装(REQ-INFER-007)', () => {
|
|
150
|
+
const testcase = makeBaseTestcase({
|
|
151
|
+
uncoveredCandidates: [
|
|
152
|
+
{
|
|
153
|
+
actionId: 'a1',
|
|
154
|
+
label: 'l',
|
|
155
|
+
status: 'candidate',
|
|
156
|
+
requiredRole: 'admin',
|
|
157
|
+
businessIntent: 'b',
|
|
158
|
+
assertionExpectation: 'e',
|
|
159
|
+
evidence: [],
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
});
|
|
163
|
+
const { testcasePath, specOut } = setupFixture(testcase, makeBaseContract());
|
|
164
|
+
expect(() => runSpec({ testcase: testcasePath, out: specOut })).toThrow(/candidate\/unresolved 未覆盖项/);
|
|
165
|
+
});
|
|
166
|
+
it('全部 confirmed + planned/resolved 时正常组装(门禁 happy path)', () => {
|
|
167
|
+
const testcase = makeBaseTestcase({
|
|
168
|
+
cases: [
|
|
169
|
+
{
|
|
170
|
+
caseId: 'c1',
|
|
171
|
+
title: 't',
|
|
172
|
+
scenario: 's',
|
|
173
|
+
reviewStatus: 'confirmed',
|
|
174
|
+
requiredRole: 'admin',
|
|
175
|
+
evidence: [],
|
|
176
|
+
steps: [],
|
|
177
|
+
assertions: [],
|
|
178
|
+
unresolved: [],
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
uncoveredCandidates: [
|
|
182
|
+
{
|
|
183
|
+
actionId: 'a1',
|
|
184
|
+
label: 'l',
|
|
185
|
+
status: 'planned',
|
|
186
|
+
requiredRole: 'admin',
|
|
187
|
+
businessIntent: 'b',
|
|
188
|
+
assertionExpectation: 'e',
|
|
189
|
+
evidence: [],
|
|
190
|
+
},
|
|
191
|
+
],
|
|
192
|
+
});
|
|
193
|
+
const { testcasePath, specOut } = setupFixture(testcase, makeBaseContract());
|
|
194
|
+
const result = runSpec({ testcase: testcasePath, out: specOut });
|
|
195
|
+
expect(result.specPath).toBe(specOut);
|
|
196
|
+
expect(generateStageSkeletonCrudSpec).toHaveBeenCalledTimes(1);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spec.test.d.ts","sourceRoot":"","sources":["../../../test/cli/spec.test.ts"],"names":[],"mappings":""}
|