@epoint-testtech/ep-stage-skill 0.0.9 → 0.5.0-alpha.2
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/SKILL.md +27 -27
- package/codex-skill/ep-stage/glue-create-project/SKILL.md +223 -223
- package/codex-skill/ep-stage/glue-generate-testcase/SKILL.md +275 -275
- package/codex-skill/ep-stage/glue-generate-testcase/examples/observable-testcase.json +40 -40
- package/codex-skill/ep-stage/glue-generate-testcase/references/testcase-schema.md +95 -95
- package/codex-skill/ep-stage/glue-run-test/SKILL.md +273 -296
- package/codex-skill/ep-stage/glue-run-test/references/ai-runtime-loop.md +30 -30
- package/codex-skill/ep-stage/glue-run-test/references/crud-pipeline.md +132 -132
- package/codex-skill/ep-stage/glue-run-test/references/gap-review-protocol.md +44 -44
- package/codex-skill/ep-stage/glue-run-test/references/harness-principles.md +46 -46
- package/codex-skill/ep-stage/glue-run-test/references/playwright-cli-odav.md +127 -166
- package/codex-skill/ep-stage/glue-run-test/scripts/generate-crud-spec.mjs +149 -149
- package/codex-skill/ep-stage/recording-to-glue/SKILL.md +28 -28
- package/codex-skill/ep-stage/scripts/validate-skill.mjs +95 -98
- package/dist/src/cli/generate-testcase.d.ts +0 -1
- package/dist/src/cli/generate-testcase.d.ts.map +1 -1
- package/dist/src/cli/generate-testcase.js +1 -6
- package/dist/src/cli/index.d.ts.map +1 -1
- package/dist/src/cli/index.js +14 -38
- package/dist/src/cli/run.d.ts +0 -2
- package/dist/src/cli/run.d.ts.map +1 -1
- package/dist/src/cli/run.js +21 -70
- package/dist/src/cli/spec.d.ts.map +1 -1
- package/dist/src/cli/spec.js +0 -9
- package/dist/src/contracts/glue-contract-v1.d.ts +0 -35
- package/dist/src/contracts/glue-contract-v1.d.ts.map +1 -1
- package/dist/src/extractors/code-list-json5.d.ts +1 -34
- package/dist/src/extractors/code-list-json5.d.ts.map +1 -1
- package/dist/src/extractors/code-list-json5.js +0 -1
- package/dist/src/extractors/html-page.d.ts +0 -9
- package/dist/src/extractors/html-page.d.ts.map +1 -1
- package/dist/src/extractors/html-page.js +2 -26
- package/dist/src/generators/agent-inferred-workflow-script.js +66 -66
- package/dist/src/generators/stage-skeleton-script.d.ts +0 -50
- package/dist/src/generators/stage-skeleton-script.d.ts.map +1 -1
- package/dist/src/generators/stage-skeleton-script.js +116 -449
- package/dist/src/runtime/ai-runtime-trace.d.ts.map +1 -1
- package/dist/src/runtime/ai-runtime-trace.js +0 -8
- package/dist/src/testcase/contract-testcase-assembler.d.ts +0 -28
- package/dist/src/testcase/contract-testcase-assembler.d.ts.map +1 -1
- package/dist/src/testcase/contract-testcase-assembler.js +75 -329
- package/dist/src/validation/contract.d.ts.map +1 -1
- package/dist/src/validation/contract.js +0 -29
- package/dist/test/cli/generate-testcase-json5.test.d.ts +2 -0
- package/dist/test/cli/generate-testcase-json5.test.d.ts.map +1 -0
- package/dist/test/cli/generate-testcase-json5.test.js +154 -0
- package/dist/test/cli/index.test.d.ts +2 -0
- package/dist/test/cli/index.test.d.ts.map +1 -0
- package/dist/test/cli/index.test.js +114 -0
- package/dist/test/cli/run.test.d.ts +2 -0
- package/dist/test/cli/run.test.d.ts.map +1 -0
- package/dist/test/cli/run.test.js +429 -0
- package/dist/test/cli/spec.test.d.ts +2 -0
- package/dist/test/cli/spec.test.d.ts.map +1 -0
- package/dist/test/cli/spec.test.js +112 -0
- package/dist/test/code-list-json5.test.d.ts +2 -0
- package/dist/test/code-list-json5.test.d.ts.map +1 -0
- package/dist/test/code-list-json5.test.js +84 -0
- 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/gap-inference.test.d.ts +2 -0
- package/dist/test/gap-inference.test.d.ts.map +1 -0
- package/dist/test/gap-inference.test.js +597 -0
- package/dist/test/generalization.test.d.ts +2 -0
- package/dist/test/generalization.test.d.ts.map +1 -0
- package/dist/test/generalization.test.js +73 -0
- package/dist/test/material-inventory.test.d.ts +2 -0
- package/dist/test/material-inventory.test.d.ts.map +1 -0
- package/dist/test/material-inventory.test.js +155 -0
- package/dist/test/observable-chain.test.d.ts +2 -0
- package/dist/test/observable-chain.test.d.ts.map +1 -0
- package/dist/test/observable-chain.test.js +123 -0
- package/dist/test/page-structure.test.d.ts +2 -0
- package/dist/test/page-structure.test.d.ts.map +1 -0
- package/dist/test/page-structure.test.js +45 -0
- package/dist/test/runtime/ai-runtime-trace.test.d.ts +2 -0
- package/dist/test/runtime/ai-runtime-trace.test.d.ts.map +1 -0
- package/dist/test/runtime/ai-runtime-trace.test.js +78 -0
- package/dist/test/scenario-inference.test.d.ts +2 -0
- package/dist/test/scenario-inference.test.d.ts.map +1 -0
- package/dist/test/scenario-inference.test.js +73 -0
- package/dist/test/stage-context.test.d.ts +2 -0
- package/dist/test/stage-context.test.d.ts.map +1 -0
- package/dist/test/stage-context.test.js +395 -0
- package/dist/test/testcase/testcase-markdown.test.d.ts +2 -0
- package/dist/test/testcase/testcase-markdown.test.d.ts.map +1 -0
- package/dist/test/testcase/testcase-markdown.test.js +48 -0
- package/dist/test/testcase-generator.test.d.ts +2 -0
- package/dist/test/testcase-generator.test.d.ts.map +1 -0
- package/dist/test/testcase-generator.test.js +276 -0
- package/dist/test/testcase-skeleton.test.d.ts +2 -0
- package/dist/test/testcase-skeleton.test.d.ts.map +1 -0
- package/dist/test/testcase-skeleton.test.js +185 -0
- package/dist/test/testcase-spec-assembly.test.d.ts +2 -0
- package/dist/test/testcase-spec-assembly.test.d.ts.map +1 -0
- package/dist/test/testcase-spec-assembly.test.js +105 -0
- package/dist/test/util/credentials.test.d.ts +2 -0
- package/dist/test/util/credentials.test.d.ts.map +1 -0
- package/dist/test/util/credentials.test.js +64 -0
- package/dist/test/util/i18n-testcase.test.d.ts +2 -0
- package/dist/test/util/i18n-testcase.test.d.ts.map +1 -0
- package/dist/test/util/i18n-testcase.test.js +119 -0
- package/dist/test/util/softlink.test.d.ts +2 -0
- package/dist/test/util/softlink.test.d.ts.map +1 -0
- package/dist/test/util/softlink.test.js +82 -0
- package/dist/test/validation/contract.test.d.ts +2 -0
- package/dist/test/validation/contract.test.d.ts.map +1 -0
- package/dist/test/validation/contract.test.js +116 -0
- package/dist/test/validation/credentials.test.d.ts +2 -0
- package/dist/test/validation/credentials.test.d.ts.map +1 -0
- package/dist/test/validation/credentials.test.js +74 -0
- package/dist/test/validation/projects-index.test.d.ts +2 -0
- package/dist/test/validation/projects-index.test.d.ts.map +1 -0
- package/dist/test/validation/projects-index.test.js +48 -0
- package/dist/test/validation/testcase-v1-draft.test.d.ts +2 -0
- package/dist/test/validation/testcase-v1-draft.test.d.ts.map +1 -0
- package/dist/test/validation/testcase-v1-draft.test.js +173 -0
- package/dist/vitest.config.d.ts +3 -0
- package/dist/vitest.config.d.ts.map +1 -0
- package/dist/vitest.config.js +7 -0
- package/docs/README.md +124 -124
- package/examples/schemeresource-observable-docs/schemeresource.context.md +20 -20
- package/examples/schemeresource.module-hints.json +38 -38
- package/examples/schemeresource.observable.code_list.md +37 -37
- package/examples/zwplace-observable-docs/zwplace.context.md +16 -16
- package/examples/zwplace-placecategory-validation.json +29 -29
- package/examples/zwplace.module-hints.json +69 -69
- package/examples/zwplace.observable.code_list.md +37 -37
- package/package.json +19 -13
- package/codex-skill/ep-stage/glue-generate-testcase/references/testcase-tpl.md +0 -72
- package/codex-skill/ep-stage/glue-run-test/scripts/extract-page-elements-eval.mjs +0 -233
- package/codex-skill/ep-stage/page-element-scan/SKILL.md +0 -91
- package/codex-skill/ep-stage/page-element-scan/evals/evals.json +0 -37
- package/codex-skill/ep-stage/page-element-scan/evals/fixtures/sample-list-page.html +0 -85
- package/codex-skill/ep-stage/page-element-scan/references/pagination-controls.md +0 -153
- package/codex-skill/ep-stage/page-element-scan/scripts/extract-page-elements-eval.mjs +0 -233
- package/codex-skill/ep-stage/page-element-scan/scripts/extract-page-elements.mjs +0 -298
- package/codex-skill/ep-stage/page-element-scan/scripts/scan-page.mjs +0 -749
- package/dist/src/cli/probe.d.ts +0 -46
- package/dist/src/cli/probe.d.ts.map +0 -1
- package/dist/src/cli/probe.js +0 -447
- package/dist/src/cli/run-report.d.ts +0 -46
- package/dist/src/cli/run-report.d.ts.map +0 -1
- package/dist/src/cli/run-report.js +0 -316
- package/dist/src/extractors/prd-extractor.d.ts +0 -70
- package/dist/src/extractors/prd-extractor.d.ts.map +0 -1
- package/dist/src/extractors/prd-extractor.js +0 -248
- package/dist/src/testcase/button-interaction-inference.d.ts +0 -37
- package/dist/src/testcase/button-interaction-inference.d.ts.map +0 -1
- package/dist/src/testcase/button-interaction-inference.js +0 -385
- package/dist/src/testcase/prd-button-mapper.d.ts +0 -72
- package/dist/src/testcase/prd-button-mapper.d.ts.map +0 -1
- package/dist/src/testcase/prd-button-mapper.js +0 -148
- package/dist/src/testcase/testcase-v2.d.ts +0 -50
- package/dist/src/testcase/testcase-v2.d.ts.map +0 -1
- package/dist/src/testcase/testcase-v2.js +0 -1
- package/docs/mvp-usage-guide.md +0 -298
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* run.ts 单元测试(新协议 v1-draft,Task 5)。
|
|
3
|
+
*
|
|
4
|
+
* 覆盖:
|
|
5
|
+
* - parseRunArgs 缺参 / 非法 / --headless 默认 true / --headless false 关闭无头;
|
|
6
|
+
* - runRun(端到端:runGenerateTestcase → runRun):
|
|
7
|
+
* · 只传 testcase.json5 也能定位 projectDir/specPath;
|
|
8
|
+
* · spec 缺失时自动先跑 runSpec 组装 glue 骨架;
|
|
9
|
+
* · spec 追加段含 AI_APPEND_MARKER(新注释 generationStrategy=ai);
|
|
10
|
+
* · 追加 test 数量 === ai 候选数量;
|
|
11
|
+
* · blocked 候选不进入 AI Runtime Task,但仍保留在 testcase 审阅数据;
|
|
12
|
+
* · run 会落 `.ep-stage/ai-runtime-trace.json5`;
|
|
13
|
+
* · credentials.json5 缺失时不崩溃,result.credentialsNotice 含提示;
|
|
14
|
+
* · 已有 AI 追加段时重跑覆盖(幂等);
|
|
15
|
+
* · spec 不存在时抛错。
|
|
16
|
+
*/
|
|
17
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
18
|
+
import os from 'node:os';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
import JSON5 from 'json5';
|
|
21
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
22
|
+
import { AI_APPEND_MARKER, buildPlaywrightCommand, loadAiSnippets, parseRunArgs, prepareAiRuntimeTasks, runRun, } from '../../src/cli/run.js';
|
|
23
|
+
import { runSpec } from '../../src/cli/spec.js';
|
|
24
|
+
import { runGenerateTestcase } from '../../src/cli/generate-testcase.js';
|
|
25
|
+
/** code_list.json5 fixture 路径(e2e 014 新协议样例)。 */
|
|
26
|
+
const CODE_LIST_PATH = path.resolve(__dirname, '../../../../e2e/glue-code-mvp/src/tests/014-测试需求10-20260622/_docs/code_list.json5');
|
|
27
|
+
const DIRECT_URL = 'http://example.test/epoint-web/perpage/zwplace/gxhzwplacelist.html';
|
|
28
|
+
let tmpRoot;
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
tmpRoot = mkdtempSync(path.join(os.tmpdir(), 'ep-stage-run-v1-'));
|
|
31
|
+
});
|
|
32
|
+
/**
|
|
33
|
+
* 构造最小 glue 项目夹具。
|
|
34
|
+
*
|
|
35
|
+
* @param name - 临时项目名。
|
|
36
|
+
* @returns projectDir / caseDir。
|
|
37
|
+
*/
|
|
38
|
+
function makeGlueProjectFixture(name) {
|
|
39
|
+
const projectDir = path.join(tmpRoot, name);
|
|
40
|
+
const caseDir = path.join(projectDir, 'src', 'tests', 'req-014');
|
|
41
|
+
mkdirSync(path.join(projectDir, 'src', 'skeletons'), { recursive: true });
|
|
42
|
+
writeFileSync(path.join(projectDir, 'package.json'), '{"name":"fixture-project"}\n', 'utf8');
|
|
43
|
+
writeFileSync(path.join(projectDir, 'playwright.config.ts'), 'export default {};\n', 'utf8');
|
|
44
|
+
return { projectDir, caseDir };
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* 端到端产 testcase.json5 + spec 到临时 glue 项目。
|
|
48
|
+
*
|
|
49
|
+
* @param name - 临时项目名。
|
|
50
|
+
* @returns testcase 路径 + spec 路径 + projectDir。
|
|
51
|
+
*/
|
|
52
|
+
function generateFixtures(name) {
|
|
53
|
+
const { projectDir, caseDir } = makeGlueProjectFixture(name);
|
|
54
|
+
const gen = runGenerateTestcase({
|
|
55
|
+
codeList: CODE_LIST_PATH,
|
|
56
|
+
testsDir: caseDir,
|
|
57
|
+
directUrl: DIRECT_URL,
|
|
58
|
+
});
|
|
59
|
+
const specPath = path.join(caseDir, 'crud.spec.ts');
|
|
60
|
+
const testcase = JSON5.parse(readFileSync(gen.testcasePath, 'utf8'));
|
|
61
|
+
testcase.runtimeContext = {
|
|
62
|
+
testProjectDir: projectDir,
|
|
63
|
+
caseDir,
|
|
64
|
+
specPath,
|
|
65
|
+
};
|
|
66
|
+
writeFileSync(gen.testcasePath, `${JSON5.stringify(testcase, null, 2)}\n`, 'utf8');
|
|
67
|
+
runSpec({ testcase: gen.testcasePath, out: specPath });
|
|
68
|
+
return { testcasePath: gen.testcasePath, specPath, projectDir, caseDir };
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* 单测默认依赖:推理固定 snippet,执行固定成功,避免误触发真实 Playwright。
|
|
72
|
+
*
|
|
73
|
+
* @returns 可注入 runRun 的稳定依赖。
|
|
74
|
+
*/
|
|
75
|
+
function passingDeps() {
|
|
76
|
+
return {
|
|
77
|
+
loadAiSnippets: ({ tasks }) => tasks.map((task) => ({
|
|
78
|
+
caseId: task.caseId,
|
|
79
|
+
status: 'resolved',
|
|
80
|
+
observation: {
|
|
81
|
+
pageState: 'unit-test',
|
|
82
|
+
},
|
|
83
|
+
decision: {
|
|
84
|
+
action: 'append deterministic snippet',
|
|
85
|
+
},
|
|
86
|
+
actionFlow: ['emit snippet'],
|
|
87
|
+
verification: {
|
|
88
|
+
assertionExpectation: task.assertionExpectation,
|
|
89
|
+
},
|
|
90
|
+
specSnippet: [
|
|
91
|
+
`test(${JSON.stringify(task.title)}, async ({ page }) => {`,
|
|
92
|
+
` // caseId: ${task.caseId}`,
|
|
93
|
+
` await test.step(${JSON.stringify(task.businessIntent)}, async () => {`,
|
|
94
|
+
` await expect(page.locator('body')).toBeVisible();`,
|
|
95
|
+
' });',
|
|
96
|
+
'});',
|
|
97
|
+
].join('\n'),
|
|
98
|
+
evidence: [{ source: 'unit-test', note: 'deterministic snippet' }],
|
|
99
|
+
})),
|
|
100
|
+
executeFullSpec: async () => ({ passed: true, output: 'ok' }),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
describe('parseRunArgs', () => {
|
|
104
|
+
it('缺 --testcase 时抛错', () => {
|
|
105
|
+
expect(() => parseRunArgs([])).toThrow(/缺少 --testcase/);
|
|
106
|
+
});
|
|
107
|
+
it('参数对非法时抛错', () => {
|
|
108
|
+
expect(() => parseRunArgs(['--testcase'])).toThrow(/无效参数对/);
|
|
109
|
+
});
|
|
110
|
+
it('--headless 默认 true', () => {
|
|
111
|
+
const args = parseRunArgs(['--testcase', '/t']);
|
|
112
|
+
expect(args.headless).toBe(true);
|
|
113
|
+
});
|
|
114
|
+
it('--headless false 关闭无头', () => {
|
|
115
|
+
const args = parseRunArgs(['--testcase', '/t', '--headless', 'false']);
|
|
116
|
+
expect(args.headless).toBe(false);
|
|
117
|
+
});
|
|
118
|
+
it('--ai-snippets 透传 Agent 结果包路径', () => {
|
|
119
|
+
const args = parseRunArgs(['--testcase', '/t', '--ai-snippets', '/tmp/ai-snippets.json5']);
|
|
120
|
+
expect(args.aiSnippets).toBe('/tmp/ai-snippets.json5');
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
describe('runRun(新协议 v1-draft 端到端)', () => {
|
|
124
|
+
it('prepare-tasks 输出任务包到 trace 并组装 glue spec', () => {
|
|
125
|
+
const { testcasePath, specPath } = generateFixtures('req-prepare');
|
|
126
|
+
rmSync(specPath);
|
|
127
|
+
const result = prepareAiRuntimeTasks(testcasePath);
|
|
128
|
+
expect(result.specPath).toBe(specPath);
|
|
129
|
+
expect(result.taskCount).toBeGreaterThan(0);
|
|
130
|
+
expect(result.blockedCount).toBeGreaterThan(0);
|
|
131
|
+
expect(existsSync(result.tracePath)).toBe(true);
|
|
132
|
+
const trace = JSON5.parse(readFileSync(result.tracePath, 'utf8'));
|
|
133
|
+
expect(trace.tasks.length).toBe(result.taskCount);
|
|
134
|
+
expect(trace.results).toEqual([]);
|
|
135
|
+
expect(existsSync(specPath)).toBe(true);
|
|
136
|
+
});
|
|
137
|
+
it('只传 testcase.json5 也能定位 projectDir/specPath', async () => {
|
|
138
|
+
const { testcasePath, specPath, projectDir } = generateFixtures('req-entry');
|
|
139
|
+
const result = await runRun({
|
|
140
|
+
testcase: testcasePath,
|
|
141
|
+
headless: false,
|
|
142
|
+
}, passingDeps());
|
|
143
|
+
expect(result.specPath).toBe(specPath);
|
|
144
|
+
expect(result.reportDir).toContain(path.join(projectDir, 'e2e/glue-code-mvp/glue-report'));
|
|
145
|
+
});
|
|
146
|
+
it('spec 缺失时自动先跑 runSpec 组装 glue 骨架', async () => {
|
|
147
|
+
const { testcasePath, specPath } = generateFixtures('req-autospec');
|
|
148
|
+
rmSync(specPath);
|
|
149
|
+
const result = await runRun({
|
|
150
|
+
testcase: testcasePath,
|
|
151
|
+
headless: true,
|
|
152
|
+
}, passingDeps());
|
|
153
|
+
expect(result.specPath).toBe(specPath);
|
|
154
|
+
expect(readFileSync(specPath, 'utf8')).toContain(AI_APPEND_MARKER);
|
|
155
|
+
});
|
|
156
|
+
it('存在 AI tasks 但缺少 ai-snippets.json5 时停止等待 Agent,不追加占位 snippet、不执行 Playwright', async () => {
|
|
157
|
+
const { testcasePath, specPath } = generateFixtures('req-needs-review');
|
|
158
|
+
const calls = [];
|
|
159
|
+
const result = await runRun({
|
|
160
|
+
testcase: testcasePath,
|
|
161
|
+
headless: true,
|
|
162
|
+
}, {
|
|
163
|
+
executeFullSpec: async () => {
|
|
164
|
+
calls.push('execute');
|
|
165
|
+
return { passed: true, output: 'should not run' };
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
expect(calls).toEqual([]);
|
|
169
|
+
expect(result.aiRuntimeTracePath).toBe(path.join(path.dirname(testcasePath), '.ep-stage', 'ai-runtime-trace.json5'));
|
|
170
|
+
const body = readFileSync(specPath, 'utf8');
|
|
171
|
+
expect(body).not.toContain(AI_APPEND_MARKER);
|
|
172
|
+
const trace = JSON5.parse(readFileSync(result.aiRuntimeTracePath, 'utf8'));
|
|
173
|
+
expect(trace.results.length).toBe(trace.tasks.length);
|
|
174
|
+
expect(trace.results.every((result) => result.status === 'needs_review')).toBe(true);
|
|
175
|
+
});
|
|
176
|
+
it('runtimeContext 缺失时向上查找测试项目目录', async () => {
|
|
177
|
+
const { testcasePath, specPath } = generateFixtures('req-fallback');
|
|
178
|
+
const testcase = JSON5.parse(readFileSync(testcasePath, 'utf8'));
|
|
179
|
+
delete testcase.runtimeContext;
|
|
180
|
+
writeFileSync(testcasePath, `${JSON5.stringify(testcase, null, 2)}\n`, 'utf8');
|
|
181
|
+
rmSync(specPath);
|
|
182
|
+
const result = await runRun({
|
|
183
|
+
testcase: testcasePath,
|
|
184
|
+
headless: true,
|
|
185
|
+
}, passingDeps());
|
|
186
|
+
expect(result.specPath).toBe(specPath);
|
|
187
|
+
expect(readFileSync(specPath, 'utf8')).toContain(AI_APPEND_MARKER);
|
|
188
|
+
});
|
|
189
|
+
it('追加 Agent resolved snippet,marker 为新注释(generationStrategy=ai)', async () => {
|
|
190
|
+
const { testcasePath, specPath } = generateFixtures('req-014');
|
|
191
|
+
const result = await runRun({
|
|
192
|
+
testcase: testcasePath,
|
|
193
|
+
headless: true,
|
|
194
|
+
}, passingDeps());
|
|
195
|
+
expect(result.version).toBe('v1');
|
|
196
|
+
const body = readFileSync(specPath, 'utf8');
|
|
197
|
+
// marker 出现且为新注释语义。
|
|
198
|
+
expect(body).toContain(AI_APPEND_MARKER);
|
|
199
|
+
expect(body).toMatch(/基于 uncoveredCandidates\[\]\.generationStrategy=ai/);
|
|
200
|
+
// 追加段来自 AI Runtime Result 的 specSnippet,不再生成 TODO 占位。
|
|
201
|
+
expect(body).toMatch(/test\(.*?, async \(\{ page \}\) => \{/);
|
|
202
|
+
expect(body).toMatch(/\/\/ caseId: /);
|
|
203
|
+
expect(body).toMatch(/await test\.step/);
|
|
204
|
+
expect(body).not.toMatch(/TODO: AI runtime 推理后填入实际 Playwright 步骤/);
|
|
205
|
+
});
|
|
206
|
+
it('追加 test 数量 === ai 候选数量', async () => {
|
|
207
|
+
const { testcasePath, specPath } = generateFixtures('req-count');
|
|
208
|
+
const testcase = JSON5.parse(readFileSync(testcasePath, 'utf8'));
|
|
209
|
+
const expectedAiCount = testcase.uncoveredCandidates.filter((c) => c.generationStrategy === 'ai' && c.coverageStatus !== 'blocked').length;
|
|
210
|
+
// fixture 至少有 ai 候选,断言有意义。
|
|
211
|
+
expect(expectedAiCount).toBeGreaterThan(0);
|
|
212
|
+
await runRun({ testcase: testcasePath, headless: true }, passingDeps());
|
|
213
|
+
const body = readFileSync(specPath, 'utf8');
|
|
214
|
+
// 追加段中 test( 出现次数 === ai 候选数(marker 之后的区段计数)。
|
|
215
|
+
const markerIdx = body.indexOf(AI_APPEND_MARKER);
|
|
216
|
+
const appended = body.slice(markerIdx);
|
|
217
|
+
const testCount = (appended.match(/test\(/g) ?? []).length;
|
|
218
|
+
expect(testCount).toBe(expectedAiCount);
|
|
219
|
+
});
|
|
220
|
+
it('写入 ai-runtime-trace.json5,且 blocked 候选不进入 tasks', async () => {
|
|
221
|
+
const { testcasePath, specPath } = generateFixtures('req-blocked');
|
|
222
|
+
const testcase = JSON5.parse(readFileSync(testcasePath, 'utf8'));
|
|
223
|
+
const blockedCandidates = testcase.uncoveredCandidates.filter((c) => c.generationStrategy === 'ai' && c.coverageStatus === 'blocked');
|
|
224
|
+
expect(blockedCandidates.map((c) => c.label)).toEqual(expect.arrayContaining(['统计看板(无入口)', '移动端查询(无入口)']));
|
|
225
|
+
const blockedCaseIds = blockedCandidates.map((c) => c.caseId);
|
|
226
|
+
await runRun({ testcase: testcasePath, headless: true }, passingDeps());
|
|
227
|
+
expect(existsSync(specPath)).toBe(true);
|
|
228
|
+
const tracePath = path.join(path.dirname(testcasePath), '.ep-stage', 'ai-runtime-trace.json5');
|
|
229
|
+
expect(existsSync(tracePath)).toBe(true);
|
|
230
|
+
const trace = JSON5.parse(readFileSync(tracePath, 'utf8'));
|
|
231
|
+
const taskCaseIds = trace.tasks.map((task) => task.caseId);
|
|
232
|
+
expect(taskCaseIds).not.toEqual(expect.arrayContaining(blockedCaseIds));
|
|
233
|
+
expect(taskCaseIds.length).toBe(testcase.uncoveredCandidates.filter((c) => c.generationStrategy === 'ai' && c.coverageStatus !== 'blocked').length);
|
|
234
|
+
const specBody = readFileSync(specPath, 'utf8');
|
|
235
|
+
const appended = specBody.slice(specBody.indexOf(AI_APPEND_MARKER));
|
|
236
|
+
for (const caseId of blockedCaseIds) {
|
|
237
|
+
expect(appended).not.toContain(caseId);
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
it('loadAiSnippets 只接受当前 testcase 的 resolved snippet', () => {
|
|
241
|
+
const { testcasePath, caseDir } = generateFixtures('req-snippets-validation');
|
|
242
|
+
const prepared = prepareAiRuntimeTasks(testcasePath);
|
|
243
|
+
const trace = JSON5.parse(readFileSync(prepared.tracePath, 'utf8'));
|
|
244
|
+
const snippetsPath = path.join(caseDir, '.ep-stage', 'ai-snippets.json5');
|
|
245
|
+
writeFileSync(snippetsPath, `${JSON5.stringify({
|
|
246
|
+
schemaVersion: 'ep-stage-ai-snippets/v1',
|
|
247
|
+
testcasePath,
|
|
248
|
+
generatedAt: '2026-07-02T00:00:00.000Z',
|
|
249
|
+
results: [
|
|
250
|
+
{
|
|
251
|
+
caseId: trace.tasks[0].caseId,
|
|
252
|
+
status: 'resolved',
|
|
253
|
+
specSnippet: 'test("resolved", async () => {});',
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
caseId: trace.tasks[1].caseId,
|
|
257
|
+
status: 'needs_review',
|
|
258
|
+
specSnippet: 'test("not merged", async () => {});',
|
|
259
|
+
},
|
|
260
|
+
],
|
|
261
|
+
}, null, 2)}\n`, 'utf8');
|
|
262
|
+
const results = loadAiSnippets({
|
|
263
|
+
snippetsPath,
|
|
264
|
+
testcasePath,
|
|
265
|
+
tasks: trace.tasks,
|
|
266
|
+
});
|
|
267
|
+
expect(results.map((result) => result.caseId)).toEqual([trace.tasks[0].caseId]);
|
|
268
|
+
writeFileSync(snippetsPath, `${JSON5.stringify({
|
|
269
|
+
schemaVersion: 'ep-stage-ai-snippets/v1',
|
|
270
|
+
testcasePath: '/tmp/other/testcase.json5',
|
|
271
|
+
results: [],
|
|
272
|
+
}, null, 2)}\n`, 'utf8');
|
|
273
|
+
expect(() => loadAiSnippets({
|
|
274
|
+
snippetsPath,
|
|
275
|
+
testcasePath,
|
|
276
|
+
tasks: trace.tasks,
|
|
277
|
+
})).toThrow(/testcasePath 与当前 testcase 不匹配/);
|
|
278
|
+
});
|
|
279
|
+
it('trace task 包含 entryContext / knownContext / codingRules', async () => {
|
|
280
|
+
const { testcasePath } = generateFixtures('req-trace-payload');
|
|
281
|
+
await runRun({ testcase: testcasePath, headless: true }, passingDeps());
|
|
282
|
+
const tracePath = path.join(path.dirname(testcasePath), '.ep-stage', 'ai-runtime-trace.json5');
|
|
283
|
+
const trace = JSON5.parse(readFileSync(tracePath, 'utf8'));
|
|
284
|
+
expect(trace.tasks.length).toBeGreaterThan(0);
|
|
285
|
+
expect(trace.tasks[0].entryContext).toMatchObject({
|
|
286
|
+
role: expect.any(String),
|
|
287
|
+
});
|
|
288
|
+
expect(trace.tasks[0].knownContext).toMatchObject({
|
|
289
|
+
queryKey: expect.anything(),
|
|
290
|
+
precondition: expect.anything(),
|
|
291
|
+
steps: expect.anything(),
|
|
292
|
+
assertion: expect.anything(),
|
|
293
|
+
});
|
|
294
|
+
expect(trace.tasks[0].codingRules).toMatchObject({
|
|
295
|
+
devRulesPath: 'docs/00-memory/dev-rules.md',
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
it('prepare-tasks 按 testcase.testAccess.credentialRef 读取运行访问数据', () => {
|
|
299
|
+
const { testcasePath, caseDir } = generateFixtures('req-credential-ref');
|
|
300
|
+
const testcase = JSON5.parse(readFileSync(testcasePath, 'utf8'));
|
|
301
|
+
testcase.testAccess = {
|
|
302
|
+
credentialRef: 'runtime-access.json5',
|
|
303
|
+
requiredRoles: ['业务管理员'],
|
|
304
|
+
};
|
|
305
|
+
writeFileSync(testcasePath, `${JSON5.stringify(testcase, null, 2)}\n`, 'utf8');
|
|
306
|
+
writeFileSync(path.join(caseDir, 'runtime-access.json5'), `${JSON5.stringify([
|
|
307
|
+
{
|
|
308
|
+
directUrl: DIRECT_URL,
|
|
309
|
+
loginUrl: 'http://example.test/login',
|
|
310
|
+
systemName: '政务系统',
|
|
311
|
+
role: '业务管理员',
|
|
312
|
+
username: 'demo-user',
|
|
313
|
+
password: 'demo-password',
|
|
314
|
+
},
|
|
315
|
+
], null, 2)}\n`, 'utf8');
|
|
316
|
+
const result = prepareAiRuntimeTasks(testcasePath);
|
|
317
|
+
const trace = JSON5.parse(readFileSync(result.tracePath, 'utf8'));
|
|
318
|
+
expect(trace.tasks.length).toBeGreaterThan(0);
|
|
319
|
+
expect(trace.tasks[0].entryContext).toMatchObject({
|
|
320
|
+
directUrl: DIRECT_URL,
|
|
321
|
+
loginUrl: 'http://example.test/login',
|
|
322
|
+
systemName: '政务系统',
|
|
323
|
+
role: '业务管理员',
|
|
324
|
+
username: 'demo-user',
|
|
325
|
+
password: 'demo-password',
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
it('凭据文件缺失时不崩溃,result.credentialsNotice 含提示', async () => {
|
|
329
|
+
const { testcasePath } = generateFixtures('req-cred');
|
|
330
|
+
// fixture 默认不产 credentials.json5(generate-testcase 产 credentials.template.json5)。
|
|
331
|
+
const result = await runRun({
|
|
332
|
+
testcase: testcasePath,
|
|
333
|
+
headless: true,
|
|
334
|
+
}, passingDeps());
|
|
335
|
+
expect(result.credentialsNotice).toMatch(/未找到凭据文件/);
|
|
336
|
+
expect(result.credentialsNotice).toContain(path.join(path.dirname(testcasePath), 'credentials.json5'));
|
|
337
|
+
});
|
|
338
|
+
it('已有 AI 追加段时重跑覆盖(幂等)', async () => {
|
|
339
|
+
const { testcasePath, specPath } = generateFixtures('req-idem');
|
|
340
|
+
// 第一次写入。
|
|
341
|
+
await runRun({ testcase: testcasePath, headless: true }, passingDeps());
|
|
342
|
+
// 注入一条"旧"标记文本,模拟历史追加段残留。
|
|
343
|
+
const afterFirst = readFileSync(specPath, 'utf8');
|
|
344
|
+
const injected = afterFirst.replace(AI_APPEND_MARKER, `${AI_APPEND_MARKER}\ntest("历史残留-不应保留", async () => {});`);
|
|
345
|
+
writeFileSync(specPath, injected, 'utf8');
|
|
346
|
+
// 第二次重跑。
|
|
347
|
+
await runRun({ testcase: testcasePath, headless: true }, passingDeps());
|
|
348
|
+
const body = readFileSync(specPath, 'utf8');
|
|
349
|
+
// 历史残留被擦掉。
|
|
350
|
+
expect(body).not.toMatch(/历史残留-不应保留/);
|
|
351
|
+
// marker 只出现一次(重跑不叠加)。
|
|
352
|
+
const markerCount = (body.match(/AI 钻探追加/g) ?? []).length;
|
|
353
|
+
expect(markerCount).toBe(1);
|
|
354
|
+
});
|
|
355
|
+
it('无法从 testcase.json5 定位测试项目目录时抛错', async () => {
|
|
356
|
+
const caseDir = path.join(tmpRoot, 'orphan-case');
|
|
357
|
+
mkdirSync(path.join(caseDir, '.ep-stage'), { recursive: true });
|
|
358
|
+
writeFileSync(path.join(caseDir, 'testcase.json5'), `${JSON5.stringify({
|
|
359
|
+
schemaVersion: 'ep-stage-testcase/v1-draft',
|
|
360
|
+
testcaseId: 'orphan',
|
|
361
|
+
contractRef: '.ep-stage/contract.json5',
|
|
362
|
+
scenario: 'crud',
|
|
363
|
+
moduleId: 'demo',
|
|
364
|
+
moduleName: '演示模块',
|
|
365
|
+
cases: [],
|
|
366
|
+
uncoveredCandidates: [],
|
|
367
|
+
}, null, 2)}\n`, 'utf8');
|
|
368
|
+
await expect(runRun({
|
|
369
|
+
testcase: path.join(caseDir, 'testcase.json5'),
|
|
370
|
+
headless: true,
|
|
371
|
+
})).rejects.toThrow(/无法从 testcase\.json5 定位测试项目目录/);
|
|
372
|
+
});
|
|
373
|
+
it('成功路径会合并 specSnippet、写 trace results 并执行完整 spec', async () => {
|
|
374
|
+
const { testcasePath, specPath, projectDir } = generateFixtures('req-success-loop');
|
|
375
|
+
const calls = [];
|
|
376
|
+
const result = await runRun({ testcase: testcasePath, headless: false }, {
|
|
377
|
+
...passingDeps(),
|
|
378
|
+
executeFullSpec: async (input) => {
|
|
379
|
+
calls.push(input);
|
|
380
|
+
return { passed: true, output: 'ok' };
|
|
381
|
+
},
|
|
382
|
+
});
|
|
383
|
+
expect(calls).toEqual([{ projectDir, specPath, headless: false }]);
|
|
384
|
+
expect(result.aiRuntimeTracePath).toBe(path.join(path.dirname(testcasePath), '.ep-stage', 'ai-runtime-trace.json5'));
|
|
385
|
+
expect(result.repairReportPath).toBeUndefined();
|
|
386
|
+
const trace = JSON5.parse(readFileSync(result.aiRuntimeTracePath, 'utf8'));
|
|
387
|
+
expect(trace.results.length).toBeGreaterThan(0);
|
|
388
|
+
expect(trace.results[0].specSnippet).toContain('await test.step');
|
|
389
|
+
});
|
|
390
|
+
it('执行失败时写 repair report 并停止等待人审', async () => {
|
|
391
|
+
const { testcasePath, caseDir } = generateFixtures('req-fail-loop');
|
|
392
|
+
const result = await runRun({ testcase: testcasePath, headless: false }, {
|
|
393
|
+
...passingDeps(),
|
|
394
|
+
executeFullSpec: async () => ({
|
|
395
|
+
passed: false,
|
|
396
|
+
output: 'Timeout 30000ms exceeded for locator(".submit")',
|
|
397
|
+
}),
|
|
398
|
+
});
|
|
399
|
+
expect(result.repairReportPath).toBe(path.join(caseDir, '.ep-stage', 'repair-runs', '001', 'repair-report.md'));
|
|
400
|
+
const report = readFileSync(result.repairReportPath, 'utf8');
|
|
401
|
+
expect(report).toContain('失败用例');
|
|
402
|
+
expect(report).toContain('locator_or_wait_error');
|
|
403
|
+
expect(report).toContain('需要你确认');
|
|
404
|
+
const trace = JSON5.parse(readFileSync(result.aiRuntimeTracePath, 'utf8'));
|
|
405
|
+
expect(trace.repairRounds).toEqual([
|
|
406
|
+
expect.objectContaining({
|
|
407
|
+
round: 1,
|
|
408
|
+
status: 'failed',
|
|
409
|
+
failureType: 'locator_or_wait_error',
|
|
410
|
+
}),
|
|
411
|
+
]);
|
|
412
|
+
});
|
|
413
|
+
it('默认 executor 的 Playwright 命令从项目目录执行完整 spec', () => {
|
|
414
|
+
expect(buildPlaywrightCommand({
|
|
415
|
+
projectDir: '/tmp/project',
|
|
416
|
+
specPath: '/tmp/project/src/tests/req/crud.spec.ts',
|
|
417
|
+
headless: true,
|
|
418
|
+
})).toEqual({
|
|
419
|
+
command: 'npx',
|
|
420
|
+
args: ['playwright', 'test', '/tmp/project/src/tests/req/crud.spec.ts'],
|
|
421
|
+
cwd: '/tmp/project',
|
|
422
|
+
});
|
|
423
|
+
expect(buildPlaywrightCommand({
|
|
424
|
+
projectDir: '/tmp/project',
|
|
425
|
+
specPath: '/tmp/project/src/tests/req/crud.spec.ts',
|
|
426
|
+
headless: false,
|
|
427
|
+
}).args).toEqual(['playwright', 'test', '/tmp/project/src/tests/req/crud.spec.ts', '--headed']);
|
|
428
|
+
});
|
|
429
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spec.test.d.ts","sourceRoot":"","sources":["../../../test/cli/spec.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* spec.ts 单元测试(新协议 v1-draft,Task 4)。
|
|
3
|
+
*
|
|
4
|
+
* 覆盖:
|
|
5
|
+
* - parseSpecArgs 缺参 / 非法 / -- 分隔符
|
|
6
|
+
* - runSpec:
|
|
7
|
+
* · 端到端:先 runGenerateTestcase 产 contract.json5 + testcase.json5,再 runSpec 装配 v1 .spec.ts;
|
|
8
|
+
* · 产出含 test.describe、crudSlots(来自 skeletonSlots.crud)、
|
|
9
|
+
* 胶水模式生成 marker;
|
|
10
|
+
* · spec.ts 不读 .ep-stage/contract.json(旧);
|
|
11
|
+
* · 门禁:cases[0].reviewStatus='needs_review' 时 runSpec 抛错。
|
|
12
|
+
*/
|
|
13
|
+
import { mkdirSync, mkdtempSync, readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
14
|
+
import os from 'node:os';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import JSON5 from 'json5';
|
|
17
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
18
|
+
import { parseSpecArgs, runSpec } from '../../src/cli/spec.js';
|
|
19
|
+
import { runGenerateTestcase } from '../../src/cli/generate-testcase.js';
|
|
20
|
+
/** code_list.json5 fixture 路径(e2e 014 新协议样例)。 */
|
|
21
|
+
const CODE_LIST_PATH = path.resolve(__dirname, '../../../../e2e/glue-code-mvp/src/tests/014-测试需求10-20260622/_docs/code_list.json5');
|
|
22
|
+
const DIRECT_URL = 'http://example.test/epoint-web/perpage/zwplace/gxhzwplacelist.html';
|
|
23
|
+
let tmpRoot;
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
tmpRoot = mkdtempSync(path.join(os.tmpdir(), 'ep-stage-spec-v1-'));
|
|
26
|
+
});
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
// 留 tmpdir 给宿主清理。
|
|
29
|
+
});
|
|
30
|
+
/**
|
|
31
|
+
* 端到端产 contract.json5 + testcase.json5 到临时目录。
|
|
32
|
+
*
|
|
33
|
+
* @param testsDir - 临时 tests 目录。
|
|
34
|
+
* @returns testcase 路径 + spec 输出路径。
|
|
35
|
+
*/
|
|
36
|
+
function generateFixtures(testsDir) {
|
|
37
|
+
const result = runGenerateTestcase({ codeList: CODE_LIST_PATH, testsDir, directUrl: DIRECT_URL });
|
|
38
|
+
return { testcasePath: result.testcasePath, specOut: path.join(testsDir, 'crud.spec.ts') };
|
|
39
|
+
}
|
|
40
|
+
describe('parseSpecArgs', () => {
|
|
41
|
+
it('缺 --testcase 时抛错', () => {
|
|
42
|
+
expect(() => parseSpecArgs(['--out', '/x'])).toThrow(/缺少 --testcase/);
|
|
43
|
+
});
|
|
44
|
+
it('缺 --out 时抛错', () => {
|
|
45
|
+
expect(() => parseSpecArgs(['--testcase', '/x'])).toThrow(/缺少 --out/);
|
|
46
|
+
});
|
|
47
|
+
it('参数对非法时抛错', () => {
|
|
48
|
+
expect(() => parseSpecArgs(['--testcase'])).toThrow(/无效参数对/);
|
|
49
|
+
});
|
|
50
|
+
it('-- 分隔符被忽略', () => {
|
|
51
|
+
const args = parseSpecArgs(['--', '--testcase', '/a', '--out', '/b']);
|
|
52
|
+
expect(args).toEqual({ testcase: '/a', out: '/b' });
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
describe('runSpec(新协议 v1-draft)', () => {
|
|
56
|
+
it('端到端装配 v1 .spec.ts(含 marker + test.describe + crudSlots)', () => {
|
|
57
|
+
const testsDir = path.join(tmpRoot, 'requirement-014');
|
|
58
|
+
const { testcasePath, specOut } = generateFixtures(testsDir);
|
|
59
|
+
const result = runSpec({ testcase: testcasePath, out: specOut });
|
|
60
|
+
expect(result).toEqual({ specPath: specOut, version: 'v1' });
|
|
61
|
+
expect(existsSync(specOut)).toBe(true);
|
|
62
|
+
const body = readFileSync(specOut, 'utf8');
|
|
63
|
+
// 头部 marker(基于 contract.skeletonSlots + glue cases)。
|
|
64
|
+
expect(body).toMatch(/^\/\/ === 胶水模式生成(基于 contract\.skeletonSlots \+ glue cases)===/);
|
|
65
|
+
// Playwright test.describe 入口。
|
|
66
|
+
expect(body).toMatch(/test\.describe\(/);
|
|
67
|
+
// crudSlots 来自 contract.scenario.skeletonSlots.crud(moduleId=zwplace)。
|
|
68
|
+
expect(body).toMatch(/crudSlots: CrudFlowSlots/);
|
|
69
|
+
expect(body).toContain("'zwplace'");
|
|
70
|
+
// 模块名(场所窗口信息管理)出现在 suite 标题。
|
|
71
|
+
expect(body).toContain('场所窗口信息管理');
|
|
72
|
+
});
|
|
73
|
+
it('spec.ts 不读旧 .ep-stage/contract.json(仅消费 contract.json5)', () => {
|
|
74
|
+
const testsDir = path.join(tmpRoot, 'requirement-no-old-contract');
|
|
75
|
+
const { testcasePath, specOut } = generateFixtures(testsDir);
|
|
76
|
+
// 显式确保不存在旧 contract.json(只有 contract.json5)。
|
|
77
|
+
const oldContract = path.join(testsDir, '.ep-stage', 'contract.json');
|
|
78
|
+
expect(existsSync(oldContract)).toBe(false);
|
|
79
|
+
// 仍能正常装配(说明只依赖 contract.json5)。
|
|
80
|
+
expect(() => runSpec({ testcase: testcasePath, out: specOut })).not.toThrow();
|
|
81
|
+
});
|
|
82
|
+
it('缺 .ep-stage/contract.json5 时抛错引导(中文)', () => {
|
|
83
|
+
const testsDir = path.join(tmpRoot, 'requirement-missing');
|
|
84
|
+
const { testcasePath } = generateFixtures(testsDir);
|
|
85
|
+
// 复制一份合法 testcase 到不含 .ep-stage 目录的位置,制造 locateContract 返回 null。
|
|
86
|
+
const fakeTestsDir = path.join(tmpRoot, 'requirement-fake');
|
|
87
|
+
mkdirSync(fakeTestsDir, { recursive: true });
|
|
88
|
+
const fakeTestcase = path.join(fakeTestsDir, 'testcase.json5');
|
|
89
|
+
writeFileSync(fakeTestcase, readFileSync(testcasePath, 'utf8'), 'utf8');
|
|
90
|
+
expect(() => runSpec({ testcase: fakeTestcase, out: path.join(fakeTestsDir, 'out.spec.ts') })).toThrow(/未找到.*contract\.json5/);
|
|
91
|
+
});
|
|
92
|
+
it('门禁:cases[0].reviewStatus=needs_review 时 runSpec 抛错', () => {
|
|
93
|
+
const testsDir = path.join(tmpRoot, 'requirement-gate');
|
|
94
|
+
const { testcasePath, specOut } = generateFixtures(testsDir);
|
|
95
|
+
// 读出 testcase,把第一条 case 的 reviewStatus 改成 needs_review 后回写。
|
|
96
|
+
const testcase = JSON5.parse(readFileSync(testcasePath, 'utf8'));
|
|
97
|
+
testcase.cases[0].reviewStatus = 'needs_review';
|
|
98
|
+
writeFileSync(testcasePath, JSON5.stringify(testcase, null, 2), 'utf8');
|
|
99
|
+
expect(() => runSpec({ testcase: testcasePath, out: specOut })).toThrow(/未确认用例/);
|
|
100
|
+
});
|
|
101
|
+
it('门禁不阻断 uncoveredCandidates 的 ai/blocked(014 默认产线应通过)', () => {
|
|
102
|
+
// 014 默认装配产 6 条 uncoveredCandidates(含 blocked),
|
|
103
|
+
// runSpec 仍应正常装配,验证门禁不因 gap 候选阻断。
|
|
104
|
+
const testsDir = path.join(tmpRoot, 'requirement-gap-pass');
|
|
105
|
+
const { testcasePath, specOut } = generateFixtures(testsDir);
|
|
106
|
+
const testcase = JSON5.parse(readFileSync(testcasePath, 'utf8'));
|
|
107
|
+
// 确认 fixture 含 ai/blocked 候选(否则断言无意义)。
|
|
108
|
+
const hasAiBlocked = testcase.uncoveredCandidates.some((c) => c.generationStrategy === 'ai' && c.coverageStatus === 'blocked');
|
|
109
|
+
expect(hasAiBlocked).toBe(true);
|
|
110
|
+
expect(() => runSpec({ testcase: testcasePath, out: specOut })).not.toThrow();
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"code-list-json5.test.d.ts","sourceRoot":"","sources":["../../test/code-list-json5.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { assertCodeListJson5Path, extractCodeListJson5, extractCodeListJson5Object, } from '../src/extractors/code-list-json5.js';
|
|
6
|
+
function writeJson5Fixture(body) {
|
|
7
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ep-stage-code-list-json5-'));
|
|
8
|
+
const file = path.join(dir, 'code_list.json5');
|
|
9
|
+
fs.writeFileSync(file, body, 'utf8');
|
|
10
|
+
return file;
|
|
11
|
+
}
|
|
12
|
+
describe('code_list.json5 extractor', () => {
|
|
13
|
+
it('读取新协议 queryFields,不要求旧 searchConditions / roles.menuPath', () => {
|
|
14
|
+
const fixture = writeJson5Fixture(`{
|
|
15
|
+
module: 'zwplace',
|
|
16
|
+
moduleName: '场所窗口信息管理',
|
|
17
|
+
systems: [{ name: '集成受理平台' }],
|
|
18
|
+
pages: [
|
|
19
|
+
{
|
|
20
|
+
role: 'list',
|
|
21
|
+
path: 'src/main/webapp/perpage/zwplace/gxhzwplacelist.html',
|
|
22
|
+
directUrl: 'http://example/page-list',
|
|
23
|
+
description: '列表页',
|
|
24
|
+
queryFields: [
|
|
25
|
+
{ label: '场所名称', type: 'input', bind: 'dataBean.placename' },
|
|
26
|
+
{ label: '场所分类', type: 'listbox', modelRef: 'placecategoryModel' },
|
|
27
|
+
{ label: '状态', type: 'listbox', modelRef: 'statusModel' },
|
|
28
|
+
],
|
|
29
|
+
buttons: [],
|
|
30
|
+
rowActions: [],
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
}`);
|
|
34
|
+
const result = extractCodeListJson5(fixture);
|
|
35
|
+
expect(result.module).toBe('zwplace');
|
|
36
|
+
expect(result.moduleName).toBe('场所窗口信息管理');
|
|
37
|
+
expect(result.roles).toEqual([]);
|
|
38
|
+
expect(result.pages.find((p) => p.role === 'list')?.directUrl).toBe('http://example/page-list');
|
|
39
|
+
expect(result.pages.find((p) => p.role === 'list')?.queryFields?.map((i) => i.label))
|
|
40
|
+
.toEqual(['场所名称', '场所分类', '状态']);
|
|
41
|
+
});
|
|
42
|
+
it('rejects markdown code_list inputs', () => {
|
|
43
|
+
expect(() => assertCodeListJson5Path('/tmp/code_list.md'))
|
|
44
|
+
.toThrow('glue-generate-testcase 只接受 _docs/code_list.json5');
|
|
45
|
+
});
|
|
46
|
+
it('extracts optional testAccess systems and roles from code_list object', () => {
|
|
47
|
+
const extracted = extractCodeListJson5Object({
|
|
48
|
+
schemaVersion: 'ep-stage-code-list/v1-draft',
|
|
49
|
+
requirement: { id: '014', title: '测试需求' },
|
|
50
|
+
module: 'zwplace',
|
|
51
|
+
moduleName: '场所窗口信息管理',
|
|
52
|
+
systems: [{ name: '政务系统' }],
|
|
53
|
+
pages: [],
|
|
54
|
+
testAccess: {
|
|
55
|
+
systems: [
|
|
56
|
+
{
|
|
57
|
+
systemName: '政务系统',
|
|
58
|
+
loginUrl: 'http://example/login',
|
|
59
|
+
roles: [
|
|
60
|
+
{
|
|
61
|
+
role: '业务管理员',
|
|
62
|
+
username: 'yz',
|
|
63
|
+
password: 'secret',
|
|
64
|
+
permissions: ['新增'],
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
expect(extracted.testAccess?.systems?.[0]?.roles[0]?.password).toBe('secret');
|
|
72
|
+
});
|
|
73
|
+
it('rejects malformed testAccess systems structure', () => {
|
|
74
|
+
expect(() => extractCodeListJson5Object({
|
|
75
|
+
module: 'zwplace',
|
|
76
|
+
moduleName: '场所窗口信息管理',
|
|
77
|
+
systems: [{ name: '政务系统' }],
|
|
78
|
+
pages: [],
|
|
79
|
+
testAccess: {
|
|
80
|
+
systems: 'bad',
|
|
81
|
+
},
|
|
82
|
+
})).toThrow(/testAccess\.systems/);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"code-list.test.d.ts","sourceRoot":"","sources":["../../test/code-list.test.ts"],"names":[],"mappings":""}
|