@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.
Files changed (155) hide show
  1. package/SKILL.md +2 -2
  2. package/codex-skill/ep-stage/.omc/state/sessions/d636ad35-5fc9-4d5e-85b0-2bcad7d86ee5/pre-tool-advisory-throttle.json +10 -0
  3. package/codex-skill/ep-stage/glue-create-project/SKILL.md +206 -0
  4. package/codex-skill/ep-stage/glue-generate-testcase/SKILL.md +274 -0
  5. package/codex-skill/ep-stage/glue-generate-testcase/references/testcase-schema.md +112 -0
  6. package/codex-skill/ep-stage/glue-generate-testcase/references/testcase-tpl.md +72 -0
  7. package/codex-skill/ep-stage/glue-run-test/SKILL.md +289 -0
  8. package/codex-skill/ep-stage/glue-run-test/references/crud-pipeline.md +145 -0
  9. package/codex-skill/ep-stage/{glue-test → glue-run-test}/scripts/generate-crud-spec.mjs +3 -3
  10. package/codex-skill/ep-stage/recording-to-glue/SKILL.md +1 -0
  11. package/codex-skill/ep-stage/scripts/validate-skill.mjs +29 -7
  12. package/dist/src/cli/dev/extract-contract.d.ts +14 -0
  13. package/dist/src/cli/dev/extract-contract.d.ts.map +1 -0
  14. package/dist/src/cli/dev/extract-contract.js +114 -0
  15. package/dist/src/cli/generate-crud-contract.js +7 -77
  16. package/dist/src/cli/generate-playwright-tests.d.ts +0 -28
  17. package/dist/src/cli/generate-playwright-tests.d.ts.map +1 -1
  18. package/dist/src/cli/generate-playwright-tests.js +4 -81
  19. package/dist/src/cli/generate-testcase.d.ts +84 -0
  20. package/dist/src/cli/generate-testcase.d.ts.map +1 -0
  21. package/dist/src/cli/generate-testcase.js +300 -0
  22. package/dist/src/cli/index.d.ts +26 -0
  23. package/dist/src/cli/index.d.ts.map +1 -0
  24. package/dist/src/cli/index.js +75 -0
  25. package/dist/src/cli/probe.d.ts +46 -0
  26. package/dist/src/cli/probe.d.ts.map +1 -0
  27. package/dist/src/cli/probe.js +447 -0
  28. package/dist/src/cli/run-gap-pipeline.js +4 -0
  29. package/dist/src/cli/run.d.ts +63 -0
  30. package/dist/src/cli/run.d.ts.map +1 -0
  31. package/dist/src/cli/run.js +116 -0
  32. package/dist/src/cli/spec.d.ts +48 -0
  33. package/dist/src/cli/spec.d.ts.map +1 -0
  34. package/dist/src/cli/spec.js +102 -0
  35. package/dist/src/context/stage-context.d.ts +72 -8
  36. package/dist/src/context/stage-context.d.ts.map +1 -1
  37. package/dist/src/context/stage-context.js +61 -15
  38. package/dist/src/contracts/inference-trace.d.ts +223 -0
  39. package/dist/src/contracts/inference-trace.d.ts.map +1 -0
  40. package/dist/src/contracts/inference-trace.js +12 -0
  41. package/dist/src/extractors/code-list.d.ts +3 -3
  42. package/dist/src/extractors/code-list.d.ts.map +1 -1
  43. package/dist/src/extractors/code-list.js +46 -11
  44. package/dist/src/generators/stage-skeleton-script.d.ts.map +1 -1
  45. package/dist/src/generators/stage-skeleton-script.js +9 -0
  46. package/dist/src/index.d.ts +7 -2
  47. package/dist/src/index.d.ts.map +1 -1
  48. package/dist/src/index.js +3 -1
  49. package/dist/src/inference/module-hints-inference.d.ts +30 -0
  50. package/dist/src/inference/module-hints-inference.d.ts.map +1 -0
  51. package/dist/src/inference/module-hints-inference.js +363 -0
  52. package/dist/src/inference/source-analysis.d.ts +26 -0
  53. package/dist/src/inference/source-analysis.d.ts.map +1 -0
  54. package/dist/src/inference/source-analysis.js +258 -0
  55. package/dist/src/testcase/testcase-generator.d.ts.map +1 -1
  56. package/dist/src/testcase/testcase-generator.js +4 -0
  57. package/dist/src/testcase/testcase-v2.d.ts +50 -0
  58. package/dist/src/testcase/testcase-v2.d.ts.map +1 -0
  59. package/dist/src/testcase/testcase-v2.js +1 -0
  60. package/dist/src/util/credentials.d.ts +12 -0
  61. package/dist/src/util/credentials.d.ts.map +1 -0
  62. package/dist/src/util/credentials.js +19 -0
  63. package/dist/src/util/i18n-testcase.d.ts +8 -0
  64. package/dist/src/util/i18n-testcase.d.ts.map +1 -0
  65. package/dist/src/util/i18n-testcase.js +55 -0
  66. package/dist/src/util/softlink.d.ts +33 -0
  67. package/dist/src/util/softlink.d.ts.map +1 -0
  68. package/dist/src/util/softlink.js +43 -0
  69. package/dist/src/validation/credentials.d.ts +19 -0
  70. package/dist/src/validation/credentials.d.ts.map +1 -0
  71. package/dist/src/validation/credentials.js +38 -0
  72. package/dist/src/validation/index.d.ts +6 -0
  73. package/dist/src/validation/index.d.ts.map +1 -0
  74. package/dist/src/validation/index.js +4 -0
  75. package/dist/src/validation/inference-trace.d.ts +13 -0
  76. package/dist/src/validation/inference-trace.d.ts.map +1 -0
  77. package/dist/src/validation/inference-trace.js +31 -0
  78. package/dist/src/validation/projects-index.d.ts +13 -0
  79. package/dist/src/validation/projects-index.d.ts.map +1 -0
  80. package/dist/src/validation/projects-index.js +37 -0
  81. package/dist/src/validation/testcase.d.ts +13 -0
  82. package/dist/src/validation/testcase.d.ts.map +1 -0
  83. package/dist/src/validation/testcase.js +53 -0
  84. package/dist/test/cli/extract-contract.test.d.ts +2 -0
  85. package/dist/test/cli/extract-contract.test.d.ts.map +1 -0
  86. package/dist/test/cli/extract-contract.test.js +32 -0
  87. package/dist/test/cli/generate-testcase-inference.test.d.ts +2 -0
  88. package/dist/test/cli/generate-testcase-inference.test.d.ts.map +1 -0
  89. package/dist/test/cli/generate-testcase-inference.test.js +63 -0
  90. package/dist/test/cli/generate-testcase-paths.test.d.ts +2 -0
  91. package/dist/test/cli/generate-testcase-paths.test.d.ts.map +1 -0
  92. package/dist/test/cli/generate-testcase-paths.test.js +165 -0
  93. package/dist/test/cli/generate-testcase-requirements.test.d.ts +2 -0
  94. package/dist/test/cli/generate-testcase-requirements.test.d.ts.map +1 -0
  95. package/dist/test/cli/generate-testcase-requirements.test.js +229 -0
  96. package/dist/test/cli/generate-testcase.test.d.ts +2 -0
  97. package/dist/test/cli/generate-testcase.test.d.ts.map +1 -0
  98. package/dist/test/cli/generate-testcase.test.js +130 -0
  99. package/dist/test/cli/index.test.d.ts +2 -0
  100. package/dist/test/cli/index.test.d.ts.map +1 -0
  101. package/dist/test/cli/index.test.js +113 -0
  102. package/dist/test/cli/probe.test.d.ts +2 -0
  103. package/dist/test/cli/probe.test.d.ts.map +1 -0
  104. package/dist/test/cli/probe.test.js +31 -0
  105. package/dist/test/cli/run.test.d.ts +2 -0
  106. package/dist/test/cli/run.test.d.ts.map +1 -0
  107. package/dist/test/cli/run.test.js +149 -0
  108. package/dist/test/cli/spec-boundary.test.d.ts +2 -0
  109. package/dist/test/cli/spec-boundary.test.d.ts.map +1 -0
  110. package/dist/test/cli/spec-boundary.test.js +198 -0
  111. package/dist/test/cli/spec.test.d.ts +2 -0
  112. package/dist/test/cli/spec.test.d.ts.map +1 -0
  113. package/dist/test/cli/spec.test.js +196 -0
  114. package/dist/test/code-list.test.d.ts +2 -0
  115. package/dist/test/code-list.test.d.ts.map +1 -0
  116. package/dist/test/code-list.test.js +39 -0
  117. package/dist/test/crud-contract.test.js +12 -32
  118. package/dist/test/inference/module-hints-inference.test.d.ts +2 -0
  119. package/dist/test/inference/module-hints-inference.test.d.ts.map +1 -0
  120. package/dist/test/inference/module-hints-inference.test.js +25 -0
  121. package/dist/test/inference/source-analysis.test.d.ts +2 -0
  122. package/dist/test/inference/source-analysis.test.d.ts.map +1 -0
  123. package/dist/test/inference/source-analysis.test.js +23 -0
  124. package/dist/test/material-inventory.test.js +13 -0
  125. package/dist/test/observable-pipeline.test.js +12 -3
  126. package/dist/test/stage-context.test.js +145 -13
  127. package/dist/test/util/credentials.test.d.ts +2 -0
  128. package/dist/test/util/credentials.test.d.ts.map +1 -0
  129. package/dist/test/util/credentials.test.js +64 -0
  130. package/dist/test/util/i18n-testcase.test.d.ts +2 -0
  131. package/dist/test/util/i18n-testcase.test.d.ts.map +1 -0
  132. package/dist/test/util/i18n-testcase.test.js +119 -0
  133. package/dist/test/util/softlink.test.d.ts +2 -0
  134. package/dist/test/util/softlink.test.d.ts.map +1 -0
  135. package/dist/test/util/softlink.test.js +82 -0
  136. package/dist/test/validation/credentials.test.d.ts +2 -0
  137. package/dist/test/validation/credentials.test.d.ts.map +1 -0
  138. package/dist/test/validation/credentials.test.js +72 -0
  139. package/dist/test/validation/projects-index.test.d.ts +2 -0
  140. package/dist/test/validation/projects-index.test.d.ts.map +1 -0
  141. package/dist/test/validation/projects-index.test.js +48 -0
  142. package/dist/test/validation/testcase.test.d.ts +2 -0
  143. package/dist/test/validation/testcase.test.d.ts.map +1 -0
  144. package/dist/test/validation/testcase.test.js +129 -0
  145. package/docs/README.md +6 -6
  146. package/docs/mvp-usage-guide.md +3 -3
  147. package/package.json +9 -4
  148. package/codex-skill/ep-stage/create-project/SKILL.md +0 -59
  149. package/codex-skill/ep-stage/glue-test/SKILL.md +0 -258
  150. package/codex-skill/ep-stage/glue-test/references/crud-pipeline.md +0 -139
  151. package/codex-skill/ep-stage/glue-testcase/SKILL.md +0 -31
  152. package/codex-skill/ep-stage/glue-testcase/references/testcase-schema.md +0 -67
  153. /package/codex-skill/ep-stage/{glue-testcase → glue-generate-testcase}/examples/observable-testcase.json +0 -0
  154. /package/codex-skill/ep-stage/{glue-test → glue-run-test}/references/gap-review-protocol.md +0 -0
  155. /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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.test.d.ts.map
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=probe.test.d.ts.map
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=run.test.d.ts.map
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=spec-boundary.test.d.ts.map
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=spec.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spec.test.d.ts","sourceRoot":"","sources":["../../../test/cli/spec.test.ts"],"names":[],"mappings":""}