@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.
- package/codex-skill/ep-stage/.omc/state/sessions/d636ad35-5fc9-4d5e-85b0-2bcad7d86ee5/pre-tool-advisory-throttle.json +10 -0
- package/codex-skill/ep-stage/glue-create-project/SKILL.md +22 -2
- package/codex-skill/ep-stage/glue-generate-testcase/SKILL.md +119 -44
- package/codex-skill/ep-stage/glue-generate-testcase/references/testcase-tpl.md +72 -0
- package/codex-skill/ep-stage/glue-run-test/SKILL.md +45 -5
- package/dist/src/cli/generate-testcase.d.ts +1 -0
- package/dist/src/cli/generate-testcase.d.ts.map +1 -1
- package/dist/src/cli/generate-testcase.js +112 -9
- package/dist/src/cli/index.d.ts +8 -0
- package/dist/src/cli/index.d.ts.map +1 -1
- package/dist/src/cli/index.js +21 -1
- package/dist/src/cli/probe.d.ts +2 -0
- package/dist/src/cli/probe.d.ts.map +1 -1
- package/dist/src/cli/probe.js +239 -13
- package/dist/src/cli/spec.d.ts +6 -3
- package/dist/src/cli/spec.d.ts.map +1 -1
- package/dist/src/cli/spec.js +31 -3
- package/dist/src/contracts/inference-trace.d.ts +223 -0
- package/dist/src/contracts/inference-trace.d.ts.map +1 -0
- package/dist/src/contracts/inference-trace.js +12 -0
- package/dist/src/extractors/code-list.d.ts +3 -3
- package/dist/src/extractors/code-list.d.ts.map +1 -1
- package/dist/src/extractors/code-list.js +46 -11
- package/dist/src/generators/stage-skeleton-script.d.ts.map +1 -1
- package/dist/src/generators/stage-skeleton-script.js +9 -0
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -0
- package/dist/src/inference/module-hints-inference.d.ts +30 -0
- package/dist/src/inference/module-hints-inference.d.ts.map +1 -0
- package/dist/src/inference/module-hints-inference.js +363 -0
- package/dist/src/inference/source-analysis.d.ts +26 -0
- package/dist/src/inference/source-analysis.d.ts.map +1 -0
- package/dist/src/inference/source-analysis.js +258 -0
- package/dist/src/validation/index.d.ts +1 -0
- package/dist/src/validation/index.d.ts.map +1 -1
- package/dist/src/validation/index.js +1 -0
- package/dist/src/validation/inference-trace.d.ts +13 -0
- package/dist/src/validation/inference-trace.d.ts.map +1 -0
- package/dist/src/validation/inference-trace.js +31 -0
- package/dist/test/cli/generate-testcase-inference.test.d.ts +2 -0
- package/dist/test/cli/generate-testcase-inference.test.d.ts.map +1 -0
- package/dist/test/cli/generate-testcase-inference.test.js +63 -0
- package/dist/test/cli/generate-testcase-paths.test.d.ts +2 -0
- package/dist/test/cli/generate-testcase-paths.test.d.ts.map +1 -0
- package/dist/test/cli/generate-testcase-paths.test.js +165 -0
- package/dist/test/cli/generate-testcase-requirements.test.d.ts +2 -0
- package/dist/test/cli/generate-testcase-requirements.test.d.ts.map +1 -0
- package/dist/test/cli/generate-testcase-requirements.test.js +229 -0
- package/dist/test/cli/index.test.js +20 -0
- package/dist/test/cli/probe.test.d.ts +2 -0
- package/dist/test/cli/probe.test.d.ts.map +1 -0
- package/dist/test/cli/probe.test.js +31 -0
- package/dist/test/cli/spec-boundary.test.d.ts +2 -0
- package/dist/test/cli/spec-boundary.test.d.ts.map +1 -0
- package/dist/test/cli/spec-boundary.test.js +198 -0
- package/dist/test/cli/spec.test.js +1 -1
- package/dist/test/code-list.test.d.ts +2 -0
- package/dist/test/code-list.test.d.ts.map +1 -0
- package/dist/test/code-list.test.js +39 -0
- package/dist/test/crud-contract.test.js +12 -32
- package/dist/test/inference/module-hints-inference.test.d.ts +2 -0
- package/dist/test/inference/module-hints-inference.test.d.ts.map +1 -0
- package/dist/test/inference/module-hints-inference.test.js +25 -0
- package/dist/test/inference/source-analysis.test.d.ts +2 -0
- package/dist/test/inference/source-analysis.test.d.ts.map +1 -0
- package/dist/test/inference/source-analysis.test.js +23 -0
- package/dist/test/material-inventory.test.js +13 -0
- package/dist/test/observable-pipeline.test.js +12 -3
- package/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: '
|
|
132
|
+
reviewStatus: 'confirmed',
|
|
133
133
|
requiredRole: 'admin',
|
|
134
134
|
evidence: ['contract=crud-business-module/v1'],
|
|
135
135
|
steps: [],
|
|
@@ -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 @@
|
|
|
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 @@
|
|
|
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
|
|
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,
|
|
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.
|
|
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,
|