@epoint-testtech/ep-stage-skill 0.0.4-alpha.0 → 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 (70) hide show
  1. package/codex-skill/ep-stage/.omc/state/sessions/d636ad35-5fc9-4d5e-85b0-2bcad7d86ee5/pre-tool-advisory-throttle.json +10 -0
  2. package/codex-skill/ep-stage/glue-create-project/SKILL.md +22 -2
  3. package/codex-skill/ep-stage/glue-generate-testcase/SKILL.md +119 -44
  4. package/codex-skill/ep-stage/glue-generate-testcase/references/testcase-tpl.md +72 -0
  5. package/codex-skill/ep-stage/glue-run-test/SKILL.md +45 -5
  6. package/dist/src/cli/generate-testcase.d.ts +1 -0
  7. package/dist/src/cli/generate-testcase.d.ts.map +1 -1
  8. package/dist/src/cli/generate-testcase.js +112 -9
  9. package/dist/src/cli/index.d.ts +8 -0
  10. package/dist/src/cli/index.d.ts.map +1 -1
  11. package/dist/src/cli/index.js +21 -1
  12. package/dist/src/cli/probe.d.ts +2 -0
  13. package/dist/src/cli/probe.d.ts.map +1 -1
  14. package/dist/src/cli/probe.js +239 -13
  15. package/dist/src/cli/spec.d.ts +6 -3
  16. package/dist/src/cli/spec.d.ts.map +1 -1
  17. package/dist/src/cli/spec.js +31 -3
  18. package/dist/src/contracts/inference-trace.d.ts +223 -0
  19. package/dist/src/contracts/inference-trace.d.ts.map +1 -0
  20. package/dist/src/contracts/inference-trace.js +12 -0
  21. package/dist/src/extractors/code-list.d.ts +3 -3
  22. package/dist/src/extractors/code-list.d.ts.map +1 -1
  23. package/dist/src/extractors/code-list.js +46 -11
  24. package/dist/src/generators/stage-skeleton-script.d.ts.map +1 -1
  25. package/dist/src/generators/stage-skeleton-script.js +9 -0
  26. package/dist/src/index.d.ts +5 -0
  27. package/dist/src/index.d.ts.map +1 -1
  28. package/dist/src/index.js +2 -0
  29. package/dist/src/inference/module-hints-inference.d.ts +30 -0
  30. package/dist/src/inference/module-hints-inference.d.ts.map +1 -0
  31. package/dist/src/inference/module-hints-inference.js +363 -0
  32. package/dist/src/inference/source-analysis.d.ts +26 -0
  33. package/dist/src/inference/source-analysis.d.ts.map +1 -0
  34. package/dist/src/inference/source-analysis.js +258 -0
  35. package/dist/src/validation/index.d.ts +1 -0
  36. package/dist/src/validation/index.d.ts.map +1 -1
  37. package/dist/src/validation/index.js +1 -0
  38. package/dist/src/validation/inference-trace.d.ts +13 -0
  39. package/dist/src/validation/inference-trace.d.ts.map +1 -0
  40. package/dist/src/validation/inference-trace.js +31 -0
  41. package/dist/test/cli/generate-testcase-inference.test.d.ts +2 -0
  42. package/dist/test/cli/generate-testcase-inference.test.d.ts.map +1 -0
  43. package/dist/test/cli/generate-testcase-inference.test.js +63 -0
  44. package/dist/test/cli/generate-testcase-paths.test.d.ts +2 -0
  45. package/dist/test/cli/generate-testcase-paths.test.d.ts.map +1 -0
  46. package/dist/test/cli/generate-testcase-paths.test.js +165 -0
  47. package/dist/test/cli/generate-testcase-requirements.test.d.ts +2 -0
  48. package/dist/test/cli/generate-testcase-requirements.test.d.ts.map +1 -0
  49. package/dist/test/cli/generate-testcase-requirements.test.js +229 -0
  50. package/dist/test/cli/index.test.js +20 -0
  51. package/dist/test/cli/probe.test.d.ts +2 -0
  52. package/dist/test/cli/probe.test.d.ts.map +1 -0
  53. package/dist/test/cli/probe.test.js +31 -0
  54. package/dist/test/cli/spec-boundary.test.d.ts +2 -0
  55. package/dist/test/cli/spec-boundary.test.d.ts.map +1 -0
  56. package/dist/test/cli/spec-boundary.test.js +198 -0
  57. package/dist/test/cli/spec.test.js +1 -1
  58. package/dist/test/code-list.test.d.ts +2 -0
  59. package/dist/test/code-list.test.d.ts.map +1 -0
  60. package/dist/test/code-list.test.js +39 -0
  61. package/dist/test/crud-contract.test.js +12 -32
  62. package/dist/test/inference/module-hints-inference.test.d.ts +2 -0
  63. package/dist/test/inference/module-hints-inference.test.d.ts.map +1 -0
  64. package/dist/test/inference/module-hints-inference.test.js +25 -0
  65. package/dist/test/inference/source-analysis.test.d.ts +2 -0
  66. package/dist/test/inference/source-analysis.test.d.ts.map +1 -0
  67. package/dist/test/inference/source-analysis.test.js +23 -0
  68. package/dist/test/material-inventory.test.js +13 -0
  69. package/dist/test/observable-pipeline.test.js +12 -3
  70. package/package.json +1 -1
@@ -0,0 +1,31 @@
1
+ /**
2
+ * 校验 inference-trace.json 的顶层结构(REQ-INFER-006)。
3
+ *
4
+ * 最小校验:只保证 schemaVersion 正确且 5 个必需顶层章节存在。
5
+ * 章节内部证据数组 / reason 允许扩展(spec §8),不做深校验。
6
+ *
7
+ * @param obj - 从 JSON.parse 得到的任意值。
8
+ * @returns 类型收窄后的 InferenceTraceV1。
9
+ * @throws 顶层非对象、schemaVersion 不符或缺章节时抛错。
10
+ */
11
+ export function validateInferenceTrace(obj) {
12
+ const raw = obj;
13
+ if (!raw || typeof raw !== 'object') {
14
+ throw new Error('inference-trace.json 顶层必须是对象');
15
+ }
16
+ if (raw.schemaVersion !== 'inference-trace/v1') {
17
+ throw new Error('inference-trace.json schemaVersion 必须是 inference-trace/v1');
18
+ }
19
+ for (const key of [
20
+ 'materialInventory',
21
+ 'sourceAnalysis',
22
+ 'moduleHintsInference',
23
+ 'coverageDiff',
24
+ 'reasoningSummary',
25
+ ]) {
26
+ if (!raw[key]) {
27
+ throw new Error(`inference-trace.json 缺 ${key}`);
28
+ }
29
+ }
30
+ return raw;
31
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=generate-testcase-inference.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generate-testcase-inference.test.d.ts","sourceRoot":"","sources":["../../../test/cli/generate-testcase-inference.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,63 @@
1
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { dirname, join, resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import JSON5 from 'json5';
6
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
7
+ import { runGenerateTestcase } from '../../src/cli/generate-testcase.js';
8
+ const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../../../..');
9
+ const codeList = join(repoRoot, 'knowledge-project/epoint-web-v9.5.2/_docs/014-测试需求10-20260622/code_list.md');
10
+ /**
11
+ * HOME 隔离 + 预填 projects.index.json5,让 runGenerateTestcase 的 requirement 追加不报错、不污染真实环境。
12
+ */
13
+ function seedIsolatedHomeWithProject(homeDir, projectDir) {
14
+ const indexPath = join(homeDir, '.ep-stage', 'projects.index.json5');
15
+ mkdirSync(dirname(indexPath), { recursive: true });
16
+ writeFileSync(indexPath, `${JSON5.stringify({
17
+ projects: [
18
+ {
19
+ projectName: 'test-inference',
20
+ projectDir,
21
+ mode: 'glue',
22
+ stageCreateVersion: '0.0.0-test',
23
+ requirements: [],
24
+ },
25
+ ],
26
+ systems: [],
27
+ }, null, 2)}\n`, 'utf8');
28
+ }
29
+ describe('runGenerateTestcase inference trace output', () => {
30
+ let tmpDir = '';
31
+ let tmpHome = '';
32
+ const originalHome = process.env.HOME;
33
+ beforeEach(() => {
34
+ tmpHome = mkdtempSync(join(tmpdir(), 'ep-stage-inference-home-'));
35
+ process.env.HOME = tmpHome;
36
+ });
37
+ afterEach(() => {
38
+ process.env.HOME = originalHome;
39
+ if (tmpHome)
40
+ rmSync(tmpHome, { recursive: true, force: true });
41
+ if (tmpDir)
42
+ rmSync(tmpDir, { recursive: true, force: true });
43
+ });
44
+ it('只传 code_list 时落 contract.json 与 inference-trace.json,不落独立 module-hints 文件', () => {
45
+ tmpDir = mkdtempSync(join(tmpdir(), 'ep-stage-testcase-'));
46
+ const projectDir = join(tmpDir, 'project');
47
+ const testsDir = join(projectDir, 'src', 'tests', '014-测试需求10-20260622');
48
+ seedIsolatedHomeWithProject(tmpHome, projectDir);
49
+ runGenerateTestcase({ codeList, testsDir, requirementDir: dirname(codeList), projectDir });
50
+ const tracePath = join(testsDir, '.ep-stage/inference-trace.json');
51
+ const contractPath = join(testsDir, '.ep-stage/contract.json');
52
+ expect(existsSync(tracePath)).toBe(true);
53
+ expect(existsSync(contractPath)).toBe(true);
54
+ expect(existsSync(join(testsDir, '.ep-stage/source-spec.json'))).toBe(false);
55
+ expect(existsSync(join(testsDir, '.ep-stage/module-hints.inferred.json5'))).toBe(false);
56
+ const trace = JSON.parse(readFileSync(tracePath, 'utf8'));
57
+ expect(trace.schemaVersion).toBe('inference-trace/v1');
58
+ expect(trace.sourceAnalysis.mode).toMatch(/prd|mixed|inferred/);
59
+ expect(trace.moduleHintsInference.dataKey.selectedField).toBe('placename');
60
+ const testcase = JSON5.parse(readFileSync(join(testsDir, 'testcase.json5'), 'utf8'));
61
+ expect(testcase.moduleId).toBe('zwplace');
62
+ });
63
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=generate-testcase-paths.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generate-testcase-paths.test.d.ts","sourceRoot":"","sources":["../../../test/cli/generate-testcase-paths.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,165 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import JSON5 from 'json5';
6
+ /**
7
+ * HOME 隔离 + 预填 projects.index.json5,让 runGenerateTestcase 的 requirement 追加不报错、不污染真实环境。
8
+ * 仅服务于本文件的路径解析断言;requirement 追加的正确性由 generate-testcase-requirements.test.ts 覆盖。
9
+ */
10
+ function seedIsolatedHomeWithProject(homeDir, projectDir) {
11
+ const indexPath = path.join(homeDir, '.ep-stage', 'projects.index.json5');
12
+ mkdirSync(path.dirname(indexPath), { recursive: true });
13
+ writeFileSync(indexPath, `${JSON5.stringify({
14
+ projects: [
15
+ {
16
+ projectName: 'test-paths',
17
+ projectDir,
18
+ mode: 'glue',
19
+ stageCreateVersion: '0.0.0-test',
20
+ requirements: [],
21
+ },
22
+ ],
23
+ systems: [],
24
+ }, null, 2)}\n`, 'utf8');
25
+ }
26
+ const probe = vi.hoisted(() => ({
27
+ runProbeAndCoverage: vi.fn(() => ({
28
+ contract: { contractVersion: 'crud-business-module/v1', module: { id: 'm1', label: '模块' } },
29
+ testcaseV2: {
30
+ schemaVersion: 'v2',
31
+ scenario: 'crud.single-page',
32
+ moduleId: 'm1',
33
+ moduleName: '模块',
34
+ menuPath: '一级>二级',
35
+ requiredSystems: [{ url: '', systemName: undefined }],
36
+ requiredRoles: [{ role: 'admin', hint: undefined }],
37
+ coveredActions: [],
38
+ uncoveredCandidates: [],
39
+ cases: [],
40
+ reasoningSummary: {
41
+ conclusion: 'ok',
42
+ evidenceChain: [],
43
+ alternatives: [],
44
+ confidence: 'high',
45
+ risks: [],
46
+ needsHumanReview: false,
47
+ },
48
+ },
49
+ inferenceTrace: {
50
+ schemaVersion: 'inference-trace/v1',
51
+ codeListPath: 'mock',
52
+ generatedAt: '2026-06-25T00:00:00.000Z',
53
+ materialInventory: {
54
+ knowledgeRoot: '',
55
+ moduleId: { status: 'resolved', value: 'm1' },
56
+ menu: { status: 'resolved', value: '一级>二级' },
57
+ htmlFiles: [],
58
+ javaActionFiles: [],
59
+ docsFiles: [],
60
+ missing: [],
61
+ },
62
+ sourceAnalysis: {
63
+ mode: 'inferred',
64
+ confidence: 'medium',
65
+ sources: [],
66
+ fields: [],
67
+ businessRules: [],
68
+ uiSignals: [],
69
+ backendSignals: [],
70
+ unresolved: [],
71
+ },
72
+ moduleHintsInference: {
73
+ mode: 'inferred',
74
+ confidence: 'medium',
75
+ dataKey: { evidence: [] },
76
+ searchConditions: [],
77
+ buttonAliases: {},
78
+ deletePolicy: { evidence: [] },
79
+ assertionPolicy: {},
80
+ customWorkflows: [],
81
+ unresolved: [],
82
+ },
83
+ coverageDiff: {
84
+ coveredActions: [],
85
+ uncoveredCandidates: [],
86
+ needsReview: [],
87
+ },
88
+ reasoningSummary: {
89
+ conclusion: 'ok',
90
+ confidence: 'high',
91
+ risks: [],
92
+ needsHumanReview: false,
93
+ },
94
+ },
95
+ })),
96
+ }));
97
+ const softlink = vi.hoisted(() => ({
98
+ createSoftlinkWithFallback: vi.fn(() => ({ strategy: 'symlink' })),
99
+ }));
100
+ vi.mock('../../src/cli/probe.js', () => probe);
101
+ vi.mock('../../src/util/softlink.js', () => softlink);
102
+ const { runGenerateTestcase } = await import('../../src/cli/generate-testcase.js');
103
+ describe('runGenerateTestcase 路径解析', () => {
104
+ let tmpDir;
105
+ let tmpHome;
106
+ const originalHome = process.env.HOME;
107
+ beforeEach(() => {
108
+ tmpDir = mkdtempSync(path.join(os.tmpdir(), 'generate-testcase-paths-'));
109
+ tmpHome = mkdtempSync(path.join(os.tmpdir(), 'ep-stage-paths-home-'));
110
+ process.env.HOME = tmpHome;
111
+ vi.clearAllMocks();
112
+ });
113
+ afterEach(() => {
114
+ process.env.HOME = originalHome;
115
+ rmSync(tmpHome, { recursive: true, force: true });
116
+ rmSync(tmpDir, { recursive: true, force: true });
117
+ });
118
+ it('根据 code_list.md 的整理目录与物料路径推导 probe 输入,而不是误用 requirementDir', () => {
119
+ const knowledgeRoot = path.join(tmpDir, 'knowledge-project', 'sample-app');
120
+ const requirementDir = path.join(knowledgeRoot, '_docs', '014-测试需求10-20260622');
121
+ const testsDir = path.join(tmpDir, 'project', 'src', 'tests', '014-测试需求10-20260622');
122
+ const codeListPath = path.join(requirementDir, 'code_list.md');
123
+ const htmlDir = path.join(knowledgeRoot, 'src', 'main', 'webapp', 'perpage', 'zwplace');
124
+ const javaDir = path.join(knowledgeRoot, 'src', 'main', 'java', 'com', 'epoint', 'zwplace', 'action');
125
+ const htmlPath = path.join(htmlDir, 'gxhzwplacelist.html');
126
+ const javaPath = path.join(javaDir, 'GxhZwPlaceListAction.java');
127
+ const docsDir = path.join(knowledgeRoot, '_docs', 'zwplace-observable-docs');
128
+ const docsPath = path.join(docsDir, 'spec.yaml');
129
+ const hintsPath = path.join(knowledgeRoot, '_docs', 'zwplace.module-hints.json');
130
+ mkdirSync(requirementDir, { recursive: true });
131
+ mkdirSync(htmlDir, { recursive: true });
132
+ mkdirSync(javaDir, { recursive: true });
133
+ mkdirSync(docsDir, { recursive: true });
134
+ writeFileSync(htmlPath, '<html></html>\n', 'utf8');
135
+ writeFileSync(javaPath, 'public class GxhZwPlaceListAction {}\n', 'utf8');
136
+ writeFileSync(docsPath, 'module:\n id: zwplace\n', 'utf8');
137
+ writeFileSync(hintsPath, '{}\n', 'utf8');
138
+ writeFileSync(codeListPath, `# 测试模块代码清单
139
+
140
+ > 整理目录:\`${knowledgeRoot}\`
141
+
142
+ menu: 一级>二级
143
+
144
+ | 文件路径 |
145
+ |---------|
146
+ | \`_docs/zwplace-observable-docs/spec.yaml\` |
147
+ | \`src/main/webapp/perpage/zwplace/gxhzwplacelist.html\` |
148
+ | \`src/main/java/com/epoint/zwplace/action/GxhZwPlaceListAction.java\` |
149
+ | \`_docs/zwplace.module-hints.json\` |
150
+ `, 'utf8');
151
+ // HOME 隔离 + 预填 index(projectDir = <tmpDir>/project,与 testsDir 反推一致),避开末尾 requirement 追加校验。
152
+ seedIsolatedHomeWithProject(tmpHome, path.join(tmpDir, 'project'));
153
+ runGenerateTestcase({
154
+ codeList: codeListPath,
155
+ testsDir,
156
+ requirementDir,
157
+ });
158
+ expect(probe.runProbeAndCoverage).toHaveBeenCalledWith(expect.objectContaining({
159
+ docsDir,
160
+ webappDir: htmlDir,
161
+ javaActionsDir: javaDir,
162
+ hintsPath,
163
+ }));
164
+ });
165
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=generate-testcase-requirements.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generate-testcase-requirements.test.d.ts","sourceRoot":"","sources":["../../../test/cli/generate-testcase-requirements.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,229 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync, readFileSync } from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import JSON5 from 'json5';
6
+ // mock probe 避开真实钻探;requirements 追加逻辑不 mock,真实跑。
7
+ const probe = vi.hoisted(() => ({
8
+ runProbeAndCoverage: vi.fn(() => ({
9
+ contract: { contractVersion: 'crud-business-module/v1', module: { id: 'm1', label: '模块' } },
10
+ testcaseV2: {
11
+ schemaVersion: 'v2',
12
+ scenario: 'crud.single-page',
13
+ moduleId: 'm1',
14
+ moduleName: '模块',
15
+ menuPath: '一级>二级',
16
+ requiredSystems: [{ url: '', systemName: undefined }],
17
+ requiredRoles: [{ role: 'admin', hint: undefined }],
18
+ coveredActions: [],
19
+ uncoveredCandidates: [],
20
+ cases: [],
21
+ reasoningSummary: {
22
+ conclusion: 'ok',
23
+ evidenceChain: [],
24
+ alternatives: [],
25
+ confidence: 'high',
26
+ risks: [],
27
+ needsHumanReview: false,
28
+ },
29
+ },
30
+ inferenceTrace: {
31
+ schemaVersion: 'inference-trace/v1',
32
+ codeListPath: 'mock',
33
+ generatedAt: '2026-06-26T00:00:00.000Z',
34
+ materialInventory: {
35
+ knowledgeRoot: '',
36
+ moduleId: { status: 'resolved', value: 'm1' },
37
+ menu: { status: 'resolved', value: '一级>二级' },
38
+ htmlFiles: [],
39
+ javaActionFiles: [],
40
+ docsFiles: [],
41
+ missing: [],
42
+ },
43
+ sourceAnalysis: {
44
+ mode: 'inferred',
45
+ confidence: 'medium',
46
+ sources: [],
47
+ fields: [],
48
+ businessRules: [],
49
+ uiSignals: [],
50
+ backendSignals: [],
51
+ unresolved: [],
52
+ },
53
+ moduleHintsInference: {
54
+ mode: 'inferred',
55
+ confidence: 'medium',
56
+ dataKey: { evidence: [] },
57
+ searchConditions: [],
58
+ buttonAliases: {},
59
+ deletePolicy: { evidence: [] },
60
+ assertionPolicy: {},
61
+ customWorkflows: [],
62
+ unresolved: [],
63
+ },
64
+ coverageDiff: {
65
+ coveredActions: [],
66
+ uncoveredCandidates: [],
67
+ needsReview: [],
68
+ },
69
+ reasoningSummary: {
70
+ conclusion: 'ok',
71
+ confidence: 'high',
72
+ risks: [],
73
+ needsHumanReview: false,
74
+ },
75
+ },
76
+ })),
77
+ }));
78
+ const softlink = vi.hoisted(() => ({
79
+ createSoftlinkWithFallback: vi.fn(() => ({ strategy: 'symlink' })),
80
+ }));
81
+ vi.mock('../../src/cli/probe.js', () => probe);
82
+ vi.mock('../../src/util/softlink.js', () => softlink);
83
+ const { runGenerateTestcase } = await import('../../src/cli/generate-testcase.js');
84
+ const REQUIREMENT_NAME = '014-测试需求10-20260622';
85
+ /**
86
+ * 构造一组能让 resolveProbeInputsFromCodeList 成功的最小物料 fixture
87
+ * (复用 generate-testcase-paths.test.ts 验证过的目录结构)。
88
+ */
89
+ function buildMaterialFixture(workDir) {
90
+ const knowledgeRoot = path.join(workDir, 'knowledge-project', 'sample-app');
91
+ const requirementDir = path.join(knowledgeRoot, '_docs', REQUIREMENT_NAME);
92
+ const codeListPath = path.join(requirementDir, 'code_list.md');
93
+ const htmlDir = path.join(knowledgeRoot, 'src', 'main', 'webapp', 'perpage', 'zwplace');
94
+ const javaDir = path.join(knowledgeRoot, 'src', 'main', 'java', 'com', 'epoint', 'zwplace', 'action');
95
+ const htmlPath = path.join(htmlDir, 'gxhzwplacelist.html');
96
+ const javaPath = path.join(javaDir, 'GxhZwPlaceListAction.java');
97
+ const docsDir = path.join(knowledgeRoot, '_docs', 'zwplace-observable-docs');
98
+ const docsPath = path.join(docsDir, 'spec.yaml');
99
+ const hintsPath = path.join(knowledgeRoot, '_docs', 'zwplace.module-hints.json');
100
+ mkdirSync(requirementDir, { recursive: true });
101
+ mkdirSync(htmlDir, { recursive: true });
102
+ mkdirSync(javaDir, { recursive: true });
103
+ mkdirSync(docsDir, { recursive: true });
104
+ writeFileSync(htmlPath, '<html></html>\n', 'utf8');
105
+ writeFileSync(javaPath, 'public class GxhZwPlaceListAction {}\n', 'utf8');
106
+ writeFileSync(docsPath, 'module:\n id: zwplace\n', 'utf8');
107
+ writeFileSync(hintsPath, '{}\n', 'utf8');
108
+ writeFileSync(codeListPath, `# 测试模块代码清单
109
+
110
+ > 整理目录:\`${knowledgeRoot}\`
111
+
112
+ menu: 一级>二级
113
+
114
+ | 文件路径 |
115
+ |---------|
116
+ | \`_docs/zwplace-observable-docs/spec.yaml\` |
117
+ | \`src/main/webapp/perpage/zwplace/gxhzwplacelist.html\` |
118
+ | \`src/main/java/com/epoint/zwplace/action/GxhZwPlaceListAction.java\` |
119
+ | \`_docs/zwplace.module-hints.json\` |
120
+ `, 'utf8');
121
+ return { codeListPath, requirementDir };
122
+ }
123
+ /**
124
+ * 在临时 HOME 下造一个最小可用的 projects.index.json5(含一条 project + 一条 system),
125
+ * 用于验证 requirements 追加不丢 systems。
126
+ */
127
+ function seedIndex(homeDir, projectDir) {
128
+ const indexPath = path.join(homeDir, '.ep-stage', 'projects.index.json5');
129
+ mkdirSync(path.dirname(indexPath), { recursive: true });
130
+ writeFileSync(indexPath, `${JSON5.stringify({
131
+ projects: [
132
+ {
133
+ projectName: 'demo-glue',
134
+ projectDir,
135
+ mode: 'glue',
136
+ stageCreateVersion: '0.0.0-test',
137
+ requirements: [],
138
+ },
139
+ ],
140
+ systems: [
141
+ {
142
+ url: 'http://demo.example.com',
143
+ knownRoles: ['admin'],
144
+ credentialsCache: {
145
+ admin: {
146
+ username: 'demo',
147
+ password: 'secret',
148
+ lastUsed: '2026-06-26T00:00:00.000Z',
149
+ },
150
+ },
151
+ },
152
+ ],
153
+ }, null, 2)}\n`, 'utf8');
154
+ }
155
+ function readIndex(homeDir) {
156
+ const indexPath = path.join(homeDir, '.ep-stage', 'projects.index.json5');
157
+ return JSON5.parse(readFileSync(indexPath, 'utf8'));
158
+ }
159
+ describe('runGenerateTestcase 追加 requirement 到 projects.index', () => {
160
+ let tmpHome;
161
+ let tmpWork;
162
+ const originalHome = process.env.HOME;
163
+ beforeEach(() => {
164
+ tmpHome = mkdtempSync(path.join(os.tmpdir(), 'ep-stage-req-home-'));
165
+ tmpWork = mkdtempSync(path.join(os.tmpdir(), 'ep-stage-req-work-'));
166
+ // 关键:HOME 隔离,绝对不污染真实 ~/.ep-stage
167
+ process.env.HOME = tmpHome;
168
+ vi.clearAllMocks();
169
+ });
170
+ afterEach(() => {
171
+ process.env.HOME = originalHome;
172
+ rmSync(tmpHome, { recursive: true, force: true });
173
+ rmSync(tmpWork, { recursive: true, force: true });
174
+ });
175
+ it('跑完后向对应 project.requirements 追加一条 name=<basename(testsDir)>, lastRunStatus=never', () => {
176
+ const { codeListPath, requirementDir } = buildMaterialFixture(tmpWork);
177
+ const projectDir = path.join(tmpWork, 'glue-project');
178
+ const testsDir = path.join(projectDir, 'src', 'tests', REQUIREMENT_NAME);
179
+ seedIndex(tmpHome, projectDir);
180
+ runGenerateTestcase({ codeList: codeListPath, testsDir, requirementDir, projectDir });
181
+ const index = readIndex(tmpHome);
182
+ const project = index.projects[0];
183
+ expect(project.projectDir).toBe(projectDir);
184
+ expect(project.requirements).toHaveLength(1);
185
+ const req = project.requirements[0];
186
+ expect(req.name).toBe(REQUIREMENT_NAME);
187
+ expect(req.testsDir).toBe(testsDir);
188
+ expect(req.requirementDir).toBe(requirementDir);
189
+ expect(req.lastRunStatus).toBe('never');
190
+ });
191
+ it('重跑同名需求不重复追加(去重更新)', () => {
192
+ const { codeListPath, requirementDir } = buildMaterialFixture(tmpWork);
193
+ const projectDir = path.join(tmpWork, 'glue-project');
194
+ const testsDir = path.join(projectDir, 'src', 'tests', REQUIREMENT_NAME);
195
+ seedIndex(tmpHome, projectDir);
196
+ runGenerateTestcase({ codeList: codeListPath, testsDir, requirementDir, projectDir });
197
+ runGenerateTestcase({ codeList: codeListPath, testsDir, requirementDir, projectDir });
198
+ const index = readIndex(tmpHome);
199
+ const project = index.projects[0];
200
+ expect(project.requirements).toHaveLength(1);
201
+ });
202
+ it('不丢 systems(整体覆写后 systems 仍在)', () => {
203
+ const { codeListPath, requirementDir } = buildMaterialFixture(tmpWork);
204
+ const projectDir = path.join(tmpWork, 'glue-project');
205
+ const testsDir = path.join(projectDir, 'src', 'tests', REQUIREMENT_NAME);
206
+ seedIndex(tmpHome, projectDir);
207
+ runGenerateTestcase({ codeList: codeListPath, testsDir, requirementDir, projectDir });
208
+ const index = readIndex(tmpHome);
209
+ expect(index.systems).toHaveLength(1);
210
+ const sys = index.systems[0];
211
+ expect(sys.url).toBe('http://demo.example.com');
212
+ expect(sys.credentialsCache.admin.username).toBe('demo');
213
+ });
214
+ it('projectDir 不在 index 时抛中文错误并引导先跑 /glue-create-project', () => {
215
+ const { codeListPath, requirementDir } = buildMaterialFixture(tmpWork);
216
+ const projectDir = path.join(tmpWork, 'unknown-project');
217
+ const testsDir = path.join(projectDir, 'src', 'tests', REQUIREMENT_NAME);
218
+ // seed 的是另一个 projectDir
219
+ seedIndex(tmpHome, path.join(tmpWork, 'another-project'));
220
+ expect(() => runGenerateTestcase({ codeList: codeListPath, testsDir, requirementDir, projectDir })).toThrowError(/未在.*projects\.index\.json5.*glue-create-project/u);
221
+ });
222
+ it('index 文件不存在时也按 projectDir 不在 index 处理并抛中文错误', () => {
223
+ const { codeListPath, requirementDir } = buildMaterialFixture(tmpWork);
224
+ const projectDir = path.join(tmpWork, 'glue-project');
225
+ const testsDir = path.join(projectDir, 'src', 'tests', REQUIREMENT_NAME);
226
+ // 不 seedIndex,连 index 文件都不存在
227
+ expect(() => runGenerateTestcase({ codeList: codeListPath, testsDir, requirementDir, projectDir })).toThrowError(/未在.*projects\.index\.json5.*glue-create-project/u);
228
+ });
229
+ });
@@ -9,6 +9,10 @@
9
9
  * 用 vi.mock 替换 generate-testcase / spec / run 模块,避免占位 throw 干扰路由判定。
10
10
  */
11
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';
12
16
  const generateTestcase = vi.hoisted(() => ({
13
17
  parseGenerateTestcaseArgs: vi.fn((argv) => ({ codeList: 'parsed', testsDir: argv.join(' ') })),
14
18
  runGenerateTestcase: vi.fn(() => 'TESTCASE_OK'),
@@ -90,4 +94,20 @@ describe('ep-stage-skill CLI 路由', () => {
90
94
  await main(['node', 'cli', 'testcase', '--', '--code-list', '/x']);
91
95
  expect(generateTestcase.parseGenerateTestcaseArgs).toHaveBeenCalledWith(['--code-list', '/x']);
92
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
+ });
93
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=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":""}