@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 @@
1
+ {"version":3,"file":"projects-index.d.ts","sourceRoot":"","sources":["../../../src/validation/projects-index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAkC,MAAM,6BAA6B,CAAC;AAEjG;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,OAAO,GAAG,aAAa,CAqBjE"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * 校验 ~/.ep-stage/projects.index.json5 的顶层结构(REQ-DATA-01 修订)。
3
+ *
4
+ * 必含 projects[] 与 systems[];projects[] 条目必含 stageCreateVersion;
5
+ * 拒绝 stage-context.md 砍掉后遗留的 stageContextPath/envPath/knowledgeRoot/codeListPaths 字段。
6
+ *
7
+ * @param obj - 从 JSON5 解析出来的原始对象。
8
+ * @returns 归一化后的 ProjectsIndex。
9
+ * @throws 缺字段或含已废弃字段时抛结构化错误。
10
+ */
11
+ export function validateProjectsIndex(obj) {
12
+ if (!obj || typeof obj !== 'object')
13
+ throw new Error('projects.index.json5 顶层必须是对象');
14
+ const raw = obj;
15
+ if (!Array.isArray(raw.projects))
16
+ throw new Error('projects.index.json5 缺 projects[] 字段');
17
+ if (!Array.isArray(raw.systems))
18
+ throw new Error('projects.index.json5 缺 systems[] 字段');
19
+ const removedFields = ['stageContextPath', 'envPath', 'knowledgeRoot', 'codeListPaths'];
20
+ for (const entry of raw.projects) {
21
+ const e = entry;
22
+ if (typeof e.projectName !== 'string')
23
+ throw new Error('projects[] 条目缺 projectName');
24
+ if (typeof e.projectDir !== 'string')
25
+ throw new Error('projects[] 条目缺 projectDir');
26
+ if (e.mode !== 'glue')
27
+ throw new Error('projects[] 条目 mode 必须为 glue');
28
+ if (typeof e.stageCreateVersion !== 'string' || !e.stageCreateVersion) {
29
+ throw new Error('projects[] 条目缺 stageCreateVersion(承接砍掉的 stage-context.md 元信息)');
30
+ }
31
+ for (const f of removedFields) {
32
+ if (f in e)
33
+ throw new Error(`projects[] 条目含已废弃字段 ${f}(stage-context.md 已砍,应 removed)`);
34
+ }
35
+ }
36
+ return { projects: raw.projects, systems: raw.systems };
37
+ }
@@ -0,0 +1,13 @@
1
+ import type { TestcaseV2 } from '../testcase/testcase-v2.js';
2
+ /**
3
+ * 校验 testcase.json5 v2 schema(REQ-DATA-02 / REQ-SCHEMA-01)。
4
+ *
5
+ * 必含 schemaVersion:"v2" + scenario + menuPath + requiredSystems[] + requiredRoles[] +
6
+ * coveredActions[] + uncoveredCandidates[] + cases[];evidence 元素符合 <kind>=<ref>。
7
+ *
8
+ * @param obj - 从 JSON5 解析出来的原始对象。
9
+ * @returns 归一化后的 TestcaseV2。
10
+ * @throws 缺字段 / 版本不匹配 / evidence 格式错时抛结构化错误。
11
+ */
12
+ export declare function validateTestcase(obj: unknown): TestcaseV2;
13
+ //# sourceMappingURL=testcase.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testcase.d.ts","sourceRoot":"","sources":["../../../src/validation/testcase.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AAI7D;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,OAAO,GAAG,UAAU,CAuCzD"}
@@ -0,0 +1,53 @@
1
+ const EVIDENCE_RE = /^(html|java|spec|contract|skeleton|hint)=.+$/;
2
+ /**
3
+ * 校验 testcase.json5 v2 schema(REQ-DATA-02 / REQ-SCHEMA-01)。
4
+ *
5
+ * 必含 schemaVersion:"v2" + scenario + menuPath + requiredSystems[] + requiredRoles[] +
6
+ * coveredActions[] + uncoveredCandidates[] + cases[];evidence 元素符合 <kind>=<ref>。
7
+ *
8
+ * @param obj - 从 JSON5 解析出来的原始对象。
9
+ * @returns 归一化后的 TestcaseV2。
10
+ * @throws 缺字段 / 版本不匹配 / evidence 格式错时抛结构化错误。
11
+ */
12
+ export function validateTestcase(obj) {
13
+ if (!obj || typeof obj !== 'object')
14
+ throw new Error('testcase.json5 顶层必须是对象');
15
+ const raw = obj;
16
+ if (raw.schemaVersion !== 'v2') {
17
+ throw new Error('testcase.json5 schemaVersion 不是 v2(可能是 v1)。请重跑 `ep-stage-skill testcase` 升级。');
18
+ }
19
+ for (const f of ['scenario', 'menuPath', 'moduleId', 'moduleName']) {
20
+ if (typeof raw[f] !== 'string')
21
+ throw new Error(`testcase.json5 缺 ${f}`);
22
+ }
23
+ for (const f of ['requiredSystems', 'requiredRoles', 'coveredActions', 'uncoveredCandidates', 'cases']) {
24
+ if (!Array.isArray(raw[f]))
25
+ throw new Error(`testcase.json5 缺 ${f}[]`);
26
+ }
27
+ const checkEvidence = (ev) => {
28
+ if (typeof ev !== 'string' || !EVIDENCE_RE.test(ev)) {
29
+ throw new Error(`evidence 必须为 <kind>=<ref> 格式(kind ∈ html/java/spec/contract/skeleton/hint),实际: ${String(ev)}`);
30
+ }
31
+ };
32
+ for (const a of raw.coveredActions) {
33
+ if (!Array.isArray(a.evidence))
34
+ throw new Error('coveredActions[].evidence 必须为数组');
35
+ a.evidence.forEach(checkEvidence);
36
+ }
37
+ for (const item of raw.uncoveredCandidates) {
38
+ if (typeof item.requiredRole !== 'string' || !item.requiredRole.trim()) {
39
+ throw new Error('uncoveredCandidates[].requiredRole 必填');
40
+ }
41
+ if (!Array.isArray(item.evidence))
42
+ throw new Error('uncoveredCandidates[].evidence 必须为数组');
43
+ item.evidence.forEach(checkEvidence);
44
+ }
45
+ for (const c of raw.cases) {
46
+ if (typeof c.requiredRole !== 'string' || !c.requiredRole.trim()) {
47
+ throw new Error('cases[].requiredRole 必填');
48
+ }
49
+ if (Array.isArray(c.evidence))
50
+ c.evidence.forEach(checkEvidence);
51
+ }
52
+ return raw;
53
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=extract-contract.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extract-contract.test.d.ts","sourceRoot":"","sources":["../../../test/cli/extract-contract.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * dev/extract-contract.ts 入参解析单元测试。
3
+ *
4
+ * 实际 contract 提取已在 crud-contract.test.ts 端到端覆盖;本测试只检查参数解析
5
+ * 边界,避免依赖主仓 knowledge-project 物料(受顶级硬约束保护、本 worktree 可能未填充)。
6
+ */
7
+ import { describe, expect, it } from 'vitest';
8
+ import { runExtractContract } from '../../src/cli/dev/extract-contract.js';
9
+ describe('runExtractContract 入参解析', () => {
10
+ it('显式使用废弃 --module 时抛错', () => {
11
+ expect(() => runExtractContract([
12
+ '--module', 'zwplace',
13
+ '--docs', '/tmp/docs',
14
+ '--code-list', '/tmp/code_list.md',
15
+ '--webapp', '/tmp/webapp',
16
+ '--java-actions', '/tmp/java',
17
+ '--out', '/tmp/out.json',
18
+ ])).toThrow(/已废弃 --module/);
19
+ });
20
+ it('缺失 --module-id 时抛错', () => {
21
+ expect(() => runExtractContract([
22
+ '--docs', '/tmp/docs',
23
+ '--code-list', '/tmp/code_list.md',
24
+ '--webapp', '/tmp/webapp',
25
+ '--java-actions', '/tmp/java',
26
+ '--out', '/tmp/out.json',
27
+ ])).toThrow(/Missing required argument --module-id/);
28
+ });
29
+ it('参数对非法时抛错', () => {
30
+ expect(() => runExtractContract(['--module-id'])).toThrow(/Invalid argument pair/);
31
+ });
32
+ });
@@ -0,0 +1,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
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=generate-testcase.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generate-testcase.test.d.ts","sourceRoot":"","sources":["../../../test/cli/generate-testcase.test.ts"],"names":[],"mappings":""}