@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,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
+ });
@@ -129,7 +129,7 @@ function createTestcase() {
129
129
  caseId: 'zwplace.create',
130
130
  title: '新增',
131
131
  scenario: 'crud.single-page',
132
- reviewStatus: 'needs_review',
132
+ reviewStatus: 'confirmed',
133
133
  requiredRole: 'admin',
134
134
  evidence: ['contract=crud-business-module/v1'],
135
135
  steps: [],
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=code-list.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"code-list.test.d.ts","sourceRoot":"","sources":["../../test/code-list.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,39 @@
1
+ import { mkdtempSync, writeFileSync } from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { describe, expect, it } from 'vitest';
5
+ import { extractCodeListSummary } from '../src/index.js';
6
+ /**
7
+ * code_list.md 解析器单元测试。
8
+ *
9
+ * 重点覆盖菜单字段的人工读写约定:
10
+ * - 中文键「菜单」为 code_list.md 人工首选(与「整理目录」同风格),支持 blockquote 与裸行;
11
+ * - 英文键 menu / menuPath 向后兼容(编码内部字段名仍为 menu)。
12
+ */
13
+ describe('code_list.md 菜单字段解析', () => {
14
+ /** 在临时目录写一份最小 code_list.md 并返回其解析摘要。 */
15
+ function summarizeMenu(lines) {
16
+ const root = mkdtempSync(path.join(os.tmpdir(), 'ep-stage-codelist-'));
17
+ const codeListPath = path.join(root, 'code_list.md');
18
+ writeFileSync(codeListPath, lines.join('\n'), 'utf8');
19
+ return extractCodeListSummary(codeListPath).menu;
20
+ }
21
+ it('中文键「菜单」blockquote 形式(无反引号)', () => {
22
+ expect(summarizeMenu(['# 场所窗口信息管理代码清单', '> 菜单:场所窗口信息管理>场所窗口信息列表'])).toBe('场所窗口信息管理>场所窗口信息列表');
23
+ });
24
+ it('中文键「菜单」blockquote 形式(反引号包裹)', () => {
25
+ expect(summarizeMenu(['# 场所窗口信息管理代码清单', '> 菜单:`场所窗口信息管理>场所窗口信息列表`'])).toBe('场所窗口信息管理>场所窗口信息列表');
26
+ });
27
+ it('中文键「菜单」裸行形式', () => {
28
+ expect(summarizeMenu(['# 场所窗口信息管理代码清单', '菜单: 场所窗口信息管理>场所窗口信息列表'])).toBe('场所窗口信息管理>场所窗口信息列表');
29
+ });
30
+ it('英文键 menu 向后兼容', () => {
31
+ expect(summarizeMenu(['# 场所窗口信息管理代码清单', 'menu: 场所窗口信息管理>场所窗口信息列表'])).toBe('场所窗口信息管理>场所窗口信息列表');
32
+ });
33
+ it('英文键 menuPath 向后兼容', () => {
34
+ expect(summarizeMenu(['# 场所窗口信息管理代码清单', 'menuPath: 场所窗口信息管理>场所窗口信息列表'])).toBe('场所窗口信息管理>场所窗口信息列表');
35
+ });
36
+ it('未声明菜单时返回 undefined', () => {
37
+ expect(summarizeMenu(['# 场所窗口信息管理代码清单'])).toBeUndefined();
38
+ });
39
+ });
@@ -11,7 +11,7 @@ const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../../..');
11
11
  const zwplaceDocs = join(repoRoot, 'knowledge-project/epoint-web-v9.5.2/_docs');
12
12
  const zwplaceWebapp = join(repoRoot, 'knowledge-project/epoint-web-v9.5.2/src/main/webapp/perpage/zwplace');
13
13
  const zwplaceActions = join(repoRoot, 'knowledge-project/epoint-web-v9.5.2/src/main/java/com/epoint/zwplace/action');
14
- const codeListPath = join(repoRoot, 'knowledge-project/epoint-web-v9.5.2/_docs/code_list.md');
14
+ const codeListPath = join(repoRoot, 'knowledge-project/epoint-web-v9.5.2/_docs/014-测试需求10-20260622/code_list.md');
15
15
  function readZwplaceSpecFixture() {
16
16
  return {
17
17
  path: join(zwplaceDocs, 'spec.yaml'),
@@ -743,37 +743,6 @@ describe('stage-skeleton Playwright generator', () => {
743
743
  expect(compileResult.status).toBe(0);
744
744
  expect(compileResult.stderr).toBe('');
745
745
  });
746
- it('generates a stage-skeleton spec from CLI', () => {
747
- const outDir = mkdtempSync(join(tmpdir(), 'test-glue-playwright-'));
748
- const contractFile = join(outDir, 'zwplace.crud.contract.json');
749
- const specFile = join(outDir, 'zwplace.spec.ts');
750
- try {
751
- writeFileSync(contractFile, JSON.stringify(buildZwplaceContractWithHints(), null, 2), 'utf8');
752
- execFileSync('pnpm', [
753
- 'generate:playwright-tests',
754
- '--',
755
- '--contract',
756
- contractFile,
757
- '--out',
758
- specFile,
759
- '--menu',
760
- '场所窗口信息管理>场所窗口信息列表'
761
- ], { cwd: packageRoot, stdio: 'pipe' });
762
- const script = readFileSync(specFile, 'utf8');
763
- expect(script).toContain("import { CrudPage, ExportPage, LoginPage, MenuPage, type CrudFlowSlots, type ExportFlowSlots, type CrudWorkflowData } from '../skeletons';");
764
- expect(script).toContain("import { GlueWorkflowRecorder, runRecordedWorkflow, type GlueWorkflowRecordInput } from '../report/glue-report';");
765
- expect(script).toContain('const crudSlots: CrudFlowSlots =');
766
- expect(script).toContain("workflowId: 'zwplace.create'");
767
- expect(script).toContain('workflowData = crudPage.prepareWorkflowData();');
768
- expect(script).not.toContain('runCrudFlow');
769
- const compileResult = compileGlueTemplateSpec(script);
770
- expect(compileResult.status).toBe(0);
771
- expect(compileResult.stderr).toBe('');
772
- }
773
- finally {
774
- rmSync(outDir, { recursive: true, force: true });
775
- }
776
- });
777
746
  it('prefers contract locators and result grid ids over hard-coded zwplace fallbacks', () => {
778
747
  const contract = buildZwplaceContractWithHints();
779
748
  contract.flows.create.entryButton.locator = 'css=[data-create]';
@@ -816,4 +785,15 @@ describe('stage-skeleton Playwright generator', () => {
816
785
  menuNavigation: '场所窗口信息管理>场所窗口信息列表'
817
786
  })).toThrow('当前数据生成策略无法自动映射到 CrudFlowSlots: external_data_pool');
818
787
  });
788
+ it('defaults human_required generation strategy to timestamp_prefix (CRUD 冒烟标配,避免 tester 未喂策略时硬死锁)', () => {
789
+ const contract = buildZwplaceContractWithHints();
790
+ contract.dataKey.generationStrategy = 'human_required';
791
+ contract.searchConditions[0].generationStrategy = 'human_required';
792
+ const script = generateStageSkeletonCrudSpec({
793
+ contract,
794
+ menuNavigation: '场所窗口信息管理>场所窗口信息列表'
795
+ });
796
+ expect(script).toContain("strategy: 'timestamp_prefix'");
797
+ expect(script).not.toContain('human_required');
798
+ });
819
799
  });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=module-hints-inference.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"module-hints-inference.test.d.ts","sourceRoot":"","sources":["../../../test/inference/module-hints-inference.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,25 @@
1
+ import { dirname, join, resolve } from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { describe, expect, it } from 'vitest';
4
+ import { extractCodeListSummary, extractHtmlPage, extractJavaAction } from '../../src/index.js';
5
+ import { buildSourceAnalysis } from '../../src/inference/source-analysis.js';
6
+ import { inferModuleHintsFromMaterials } from '../../src/inference/module-hints-inference.js';
7
+ const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../../../..');
8
+ const knowledgeRoot = join(repoRoot, 'knowledge-project/epoint-web-v9.5.2');
9
+ const requirementDir = join(knowledgeRoot, '_docs/014-测试需求10-20260622');
10
+ const webappDir = join(knowledgeRoot, 'src/main/webapp/perpage/zwplace');
11
+ const javaActionsDir = join(knowledgeRoot, 'src/main/java/com/epoint/zwplace/action');
12
+ describe('inferModuleHintsFromMaterials', () => {
13
+ it('不依赖 module-hints 文件也能推理 zwplace 的基础骨架槽位', () => {
14
+ const codeListPath = join(requirementDir, 'code_list.md');
15
+ const codeList = extractCodeListSummary(codeListPath);
16
+ const pages = ['gxhzwplacelist.html', 'gxhzwplaceadd.html', 'gxhzwplaceedit.html'].map((file) => extractHtmlPage(join(webappDir, file)));
17
+ const actions = ['GxhZwPlaceListAction.java', 'GxhZwPlaceAddAction.java', 'GxhZwPlaceEditAction.java'].map((file) => extractJavaAction(join(javaActionsDir, file)));
18
+ const sourceAnalysis = buildSourceAnalysis({ codeListPath, docsDir: requirementDir, codeList, pages, actions });
19
+ const result = inferModuleHintsFromMaterials({ sourceAnalysis, pages, actions });
20
+ expect(result.hints.dataKey?.selectedField).toBe('placename');
21
+ expect(result.hints.searchConditions?.[0]?.field).toBe('placename');
22
+ expect(result.inference.mode).toBe('inferred');
23
+ expect(result.inference.customWorkflows.map((workflow) => workflow.label)).toContain('同步窗口管理系统');
24
+ });
25
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=source-analysis.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"source-analysis.test.d.ts","sourceRoot":"","sources":["../../../test/inference/source-analysis.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,23 @@
1
+ import { dirname, join, resolve } from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { describe, expect, it } from 'vitest';
4
+ import { extractCodeListSummary, extractHtmlPage, extractJavaAction } from '../../src/index.js';
5
+ import { buildSourceAnalysis } from '../../src/inference/source-analysis.js';
6
+ const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../../../..');
7
+ const knowledgeRoot = join(repoRoot, 'knowledge-project/epoint-web-v9.5.2');
8
+ const requirementDir = join(knowledgeRoot, '_docs/014-测试需求10-20260622');
9
+ const webappDir = join(knowledgeRoot, 'src/main/webapp/perpage/zwplace');
10
+ const javaActionsDir = join(knowledgeRoot, 'src/main/java/com/epoint/zwplace/action');
11
+ describe('buildSourceAnalysis', () => {
12
+ it('标记 014 真实物料为 prd+html+java 源分析', () => {
13
+ const codeListPath = join(requirementDir, 'code_list.md');
14
+ const codeList = extractCodeListSummary(codeListPath);
15
+ const pages = ['gxhzwplacelist.html', 'gxhzwplaceadd.html'].map((file) => extractHtmlPage(join(webappDir, file)));
16
+ const actions = ['GxhZwPlaceListAction.java', 'GxhZwPlaceAddAction.java'].map((file) => extractJavaAction(join(javaActionsDir, file)));
17
+ const analysis = buildSourceAnalysis({ codeListPath, docsDir: requirementDir, codeList, pages, actions });
18
+ expect(analysis.mode).toBe('prd+html+java');
19
+ expect(analysis.sources.map((source) => source.kind)).toContain('prd_markdown');
20
+ expect(analysis.uiSignals.length).toBeGreaterThan(0);
21
+ expect(analysis.backendSignals.length).toBeGreaterThan(0);
22
+ });
23
+ });
@@ -21,6 +21,7 @@ describe('material inventory resolver', () => {
21
21
  writeFileSync(hintsPath, '{}');
22
22
  writeFileSync(codeListPath, [
23
23
  '# 场所窗口信息管理代码清单',
24
+ `> 整理目录:\`${root}\``,
24
25
  'menu: 场所窗口信息管理>场所窗口信息列表',
25
26
  '`_docs/003-场所窗口信息管理-20260518/spec.yaml`',
26
27
  '`src/main/webapp/perpage/zwplace/gxhzwplacelist.html`',
@@ -53,6 +54,18 @@ describe('material inventory resolver', () => {
53
54
  });
54
55
  expect(result.gate.status).toBe('confirmed');
55
56
  });
57
+ it('does not infer knowledgeRoot from _docs hierarchy when code_list lacks structured root info', () => {
58
+ const root = mkdtempSync(path.join(os.tmpdir(), 'ep-stage-material-no-root-'));
59
+ const codeListPath = path.join(root, '_docs', '014-测试需求10-20260622', 'code_list.md');
60
+ mkdirSync(path.dirname(codeListPath), { recursive: true });
61
+ writeFileSync(codeListPath, [
62
+ '# 场所窗口信息管理代码清单',
63
+ '`src/main/webapp/perpage/zwplace/gxhzwplacelist.html`',
64
+ ].join('\n'), 'utf8');
65
+ const codeList = extractCodeListSummary(codeListPath);
66
+ expect(codeList.knowledgeRoot).toBeUndefined();
67
+ expect(codeList.knowledgeRootSource).toBe('none');
68
+ });
56
69
  it('stops at material gate when webapp or java actions cannot be resolved', () => {
57
70
  const root = mkdtempSync(path.join(os.tmpdir(), 'ep-stage-material-missing-'));
58
71
  const codeListPath = path.join(root, 'code_list.md');
@@ -75,7 +75,7 @@ describe('observable pipeline material inventory', () => {
75
75
  },
76
76
  ]);
77
77
  });
78
- it('resolves paths relative to knowledge root when code_list is under _docs', () => {
78
+ it('resolves paths relative to the 整理目录 declared inside code_list', () => {
79
79
  const root = mkdtempSync(path.join(os.tmpdir(), 'ep-stage-knowledge-root-'));
80
80
  const docsDir = path.join(root, '_docs');
81
81
  const htmlPath = path.join(root, 'src', 'main', 'webapp', 'perpage', 'zwplace', 'gxhzwplacelist.html');
@@ -83,9 +83,18 @@ describe('observable pipeline material inventory', () => {
83
83
  mkdirSync(path.dirname(htmlPath), { recursive: true });
84
84
  mkdirSync(docsDir, { recursive: true });
85
85
  writeFileSync(htmlPath, '<html></html>');
86
- writeFileSync(codeListPath, '| HTML | `src/main/webapp/perpage/zwplace/gxhzwplacelist.html` |', 'utf8');
86
+ writeFileSync(codeListPath, [
87
+ `> 整理目录:\`${root}\``,
88
+ '',
89
+ '| HTML | `src/main/webapp/perpage/zwplace/gxhzwplacelist.html` |',
90
+ ].join('\n'), 'utf8');
87
91
  const summary = extractCodeListSummary(codeListPath);
88
- expect(summary.materialRefs[0]).toMatchObject({
92
+ expect(summary.knowledgeRoot).toBe(root);
93
+ expect(summary.knowledgeRootSource).toBe('code_list');
94
+ // `> 整理目录:` 声明行因含反引号路径会被物料提取器多读出一条 other 引用,
95
+ // 故用 find 定位 HTML 引用,而非依赖数组首位。
96
+ const htmlRef = summary.materialRefs.find((ref) => ref.kind === 'html');
97
+ expect(htmlRef).toMatchObject({
89
98
  kind: 'html',
90
99
  path: htmlPath,
91
100
  exists: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@epoint-testtech/ep-stage-skill",
3
- "version": "0.0.4-alpha.0",
3
+ "version": "0.0.4-alpha.1",
4
4
  "type": "module",
5
5
  "main": "dist/src/index.js",
6
6
  "types": "dist/src/index.d.ts",