@epoint-testtech/ep-stage-skill 0.0.4-alpha.0 → 0.0.5
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 -186
- package/codex-skill/ep-stage/glue-generate-testcase/SKILL.md +275 -199
- 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 -112
- package/codex-skill/ep-stage/glue-generate-testcase/references/testcase-tpl.md +72 -0
- package/codex-skill/ep-stage/glue-run-test/SKILL.md +292 -249
- package/codex-skill/ep-stage/glue-run-test/references/ai-runtime-loop.md +30 -0
- package/codex-skill/ep-stage/glue-run-test/references/crud-pipeline.md +132 -145
- package/codex-skill/ep-stage/glue-run-test/references/gap-review-protocol.md +44 -43
- 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 +166 -0
- package/codex-skill/ep-stage/glue-run-test/scripts/extract-page-elements-eval.mjs +233 -0
- package/codex-skill/ep-stage/glue-run-test/scripts/generate-crud-spec.mjs +149 -149
- package/codex-skill/ep-stage/page-element-scan/SKILL.md +91 -0
- package/codex-skill/ep-stage/page-element-scan/evals/evals.json +37 -0
- package/codex-skill/ep-stage/page-element-scan/evals/fixtures/sample-list-page.html +85 -0
- package/codex-skill/ep-stage/page-element-scan/references/pagination-controls.md +153 -0
- package/codex-skill/ep-stage/page-element-scan/scripts/extract-page-elements-eval.mjs +233 -0
- package/codex-skill/ep-stage/page-element-scan/scripts/extract-page-elements.mjs +298 -0
- package/codex-skill/ep-stage/page-element-scan/scripts/scan-page.mjs +749 -0
- package/codex-skill/ep-stage/recording-to-glue/SKILL.md +28 -28
- package/codex-skill/ep-stage/scripts/validate-skill.mjs +98 -95
- package/dist/src/cli/generate-testcase.d.ts +18 -51
- package/dist/src/cli/generate-testcase.d.ts.map +1 -1
- package/dist/src/cli/generate-testcase.js +127 -131
- package/dist/src/cli/index.d.ts +11 -2
- package/dist/src/cli/index.d.ts.map +1 -1
- package/dist/src/cli/index.js +49 -16
- 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/run.d.ts +136 -29
- package/dist/src/cli/run.d.ts.map +1 -1
- package/dist/src/cli/run.js +494 -65
- package/dist/src/cli/spec.d.ts +18 -11
- package/dist/src/cli/spec.d.ts.map +1 -1
- package/dist/src/cli/spec.js +74 -20
- package/dist/src/contracts/crud-business-module.d.ts +6 -0
- package/dist/src/contracts/crud-business-module.d.ts.map +1 -1
- package/dist/src/contracts/glue-contract-v1.d.ts +98 -0
- package/dist/src/contracts/glue-contract-v1.d.ts.map +1 -0
- package/dist/src/contracts/glue-contract-v1.js +9 -0
- 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/contracts/observable-chain.d.ts +4 -0
- package/dist/src/contracts/observable-chain.d.ts.map +1 -1
- package/dist/src/extractors/code-list-json5.d.ts +192 -0
- package/dist/src/extractors/code-list-json5.d.ts.map +1 -0
- package/dist/src/extractors/code-list-json5.js +91 -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/extractors/html-page.d.ts +9 -0
- package/dist/src/extractors/html-page.d.ts.map +1 -1
- package/dist/src/extractors/html-page.js +26 -2
- package/dist/src/extractors/prd-extractor.d.ts +70 -0
- package/dist/src/extractors/prd-extractor.d.ts.map +1 -0
- package/dist/src/extractors/prd-extractor.js +248 -0
- package/dist/src/generators/agent-inferred-workflow-script.js +66 -66
- package/dist/src/generators/stage-skeleton-script.d.ts +87 -20
- package/dist/src/generators/stage-skeleton-script.d.ts.map +1 -1
- package/dist/src/generators/stage-skeleton-script.js +656 -238
- package/dist/src/index.d.ts +7 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -1
- 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/normalizers/crud-contract.d.ts +3 -0
- package/dist/src/normalizers/crud-contract.d.ts.map +1 -1
- package/dist/src/normalizers/crud-contract.js +3 -0
- package/dist/src/runtime/ai-runtime-trace.d.ts +88 -0
- package/dist/src/runtime/ai-runtime-trace.d.ts.map +1 -0
- package/dist/src/runtime/ai-runtime-trace.js +119 -0
- package/dist/src/testcase/button-interaction-inference.d.ts +37 -0
- package/dist/src/testcase/button-interaction-inference.d.ts.map +1 -0
- package/dist/src/testcase/button-interaction-inference.js +385 -0
- package/dist/src/testcase/contract-testcase-assembler.d.ts +44 -0
- package/dist/src/testcase/contract-testcase-assembler.d.ts.map +1 -0
- package/dist/src/testcase/contract-testcase-assembler.js +806 -0
- package/dist/src/testcase/prd-button-mapper.d.ts +72 -0
- package/dist/src/testcase/prd-button-mapper.d.ts.map +1 -0
- package/dist/src/testcase/prd-button-mapper.js +148 -0
- package/dist/src/testcase/runtime-context.d.ts +9 -0
- package/dist/src/testcase/runtime-context.d.ts.map +1 -0
- package/dist/src/testcase/runtime-context.js +1 -0
- package/dist/src/testcase/test-access.d.ts +38 -0
- package/dist/src/testcase/test-access.d.ts.map +1 -0
- package/dist/src/testcase/test-access.js +106 -0
- package/dist/src/testcase/testcase-execution-fields.d.ts +10 -0
- package/dist/src/testcase/testcase-execution-fields.d.ts.map +1 -0
- package/dist/src/testcase/testcase-execution-fields.js +1 -0
- package/dist/src/testcase/testcase-generator.d.ts.map +1 -1
- package/dist/src/testcase/testcase-generator.js +6 -0
- package/dist/src/testcase/testcase-markdown.d.ts +11 -0
- package/dist/src/testcase/testcase-markdown.d.ts.map +1 -0
- package/dist/src/testcase/testcase-markdown.js +181 -0
- package/dist/src/testcase/testcase-skeleton.d.ts +4 -0
- package/dist/src/testcase/testcase-skeleton.d.ts.map +1 -1
- package/dist/src/testcase/testcase-spec-assembly.d.ts +4 -0
- package/dist/src/testcase/testcase-spec-assembly.d.ts.map +1 -1
- package/dist/src/testcase/testcase-v1-draft.d.ts +58 -0
- package/dist/src/testcase/testcase-v1-draft.d.ts.map +1 -0
- package/dist/src/testcase/testcase-v1-draft.js +13 -0
- package/dist/src/validation/contract.d.ts +14 -0
- package/dist/src/validation/contract.d.ts.map +1 -0
- package/dist/src/validation/contract.js +132 -0
- package/dist/src/validation/credentials.d.ts +7 -2
- package/dist/src/validation/credentials.d.ts.map +1 -1
- package/dist/src/validation/credentials.js +15 -4
- package/dist/src/validation/index.d.ts +2 -0
- package/dist/src/validation/index.d.ts.map +1 -1
- package/dist/src/validation/index.js +2 -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/src/validation/testcase.d.ts +9 -7
- package/dist/src/validation/testcase.d.ts.map +1 -1
- package/dist/src/validation/testcase.js +181 -34
- package/docs/README.md +124 -134
- package/docs/mvp-usage-guide.md +298 -298
- 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 +2 -2
- package/dist/test/cli/extract-contract.test.d.ts +0 -2
- package/dist/test/cli/extract-contract.test.d.ts.map +0 -1
- package/dist/test/cli/extract-contract.test.js +0 -32
- package/dist/test/cli/generate-testcase.test.d.ts +0 -2
- package/dist/test/cli/generate-testcase.test.d.ts.map +0 -1
- package/dist/test/cli/generate-testcase.test.js +0 -130
- package/dist/test/cli/index.test.d.ts +0 -2
- package/dist/test/cli/index.test.d.ts.map +0 -1
- package/dist/test/cli/index.test.js +0 -93
- package/dist/test/cli/run.test.d.ts +0 -2
- package/dist/test/cli/run.test.d.ts.map +0 -1
- package/dist/test/cli/run.test.js +0 -149
- package/dist/test/cli/spec.test.d.ts +0 -2
- package/dist/test/cli/spec.test.d.ts.map +0 -1
- package/dist/test/cli/spec.test.js +0 -196
- package/dist/test/crud-contract.test.d.ts +0 -2
- package/dist/test/crud-contract.test.d.ts.map +0 -1
- package/dist/test/crud-contract.test.js +0 -819
- package/dist/test/gap-inference.test.d.ts +0 -2
- package/dist/test/gap-inference.test.d.ts.map +0 -1
- package/dist/test/gap-inference.test.js +0 -597
- package/dist/test/generalization.test.d.ts +0 -2
- package/dist/test/generalization.test.d.ts.map +0 -1
- package/dist/test/generalization.test.js +0 -73
- package/dist/test/material-inventory.test.d.ts +0 -2
- package/dist/test/material-inventory.test.d.ts.map +0 -1
- package/dist/test/material-inventory.test.js +0 -141
- package/dist/test/observable-chain.test.d.ts +0 -2
- package/dist/test/observable-chain.test.d.ts.map +0 -1
- package/dist/test/observable-chain.test.js +0 -123
- package/dist/test/observable-pipeline.test.d.ts +0 -2
- package/dist/test/observable-pipeline.test.d.ts.map +0 -1
- package/dist/test/observable-pipeline.test.js +0 -461
- package/dist/test/page-structure.test.d.ts +0 -2
- package/dist/test/page-structure.test.d.ts.map +0 -1
- package/dist/test/page-structure.test.js +0 -45
- package/dist/test/scenario-inference.test.d.ts +0 -2
- package/dist/test/scenario-inference.test.d.ts.map +0 -1
- package/dist/test/scenario-inference.test.js +0 -73
- package/dist/test/stage-context.test.d.ts +0 -2
- package/dist/test/stage-context.test.d.ts.map +0 -1
- package/dist/test/stage-context.test.js +0 -395
- package/dist/test/testcase-generator.test.d.ts +0 -2
- package/dist/test/testcase-generator.test.d.ts.map +0 -1
- package/dist/test/testcase-generator.test.js +0 -276
- package/dist/test/testcase-skeleton.test.d.ts +0 -2
- package/dist/test/testcase-skeleton.test.d.ts.map +0 -1
- package/dist/test/testcase-skeleton.test.js +0 -185
- package/dist/test/testcase-spec-assembly.test.d.ts +0 -2
- package/dist/test/testcase-spec-assembly.test.d.ts.map +0 -1
- package/dist/test/testcase-spec-assembly.test.js +0 -105
- package/dist/test/util/credentials.test.d.ts +0 -2
- package/dist/test/util/credentials.test.d.ts.map +0 -1
- package/dist/test/util/credentials.test.js +0 -64
- package/dist/test/util/i18n-testcase.test.d.ts +0 -2
- package/dist/test/util/i18n-testcase.test.d.ts.map +0 -1
- package/dist/test/util/i18n-testcase.test.js +0 -119
- package/dist/test/util/softlink.test.d.ts +0 -2
- package/dist/test/util/softlink.test.d.ts.map +0 -1
- package/dist/test/util/softlink.test.js +0 -82
- package/dist/test/validation/credentials.test.d.ts +0 -2
- package/dist/test/validation/credentials.test.d.ts.map +0 -1
- package/dist/test/validation/credentials.test.js +0 -72
- package/dist/test/validation/projects-index.test.d.ts +0 -2
- package/dist/test/validation/projects-index.test.d.ts.map +0 -1
- package/dist/test/validation/projects-index.test.js +0 -48
- package/dist/test/validation/testcase.test.d.ts +0 -2
- package/dist/test/validation/testcase.test.d.ts.map +0 -1
- package/dist/test/validation/testcase.test.js +0 -129
- package/dist/vitest.config.d.ts +0 -3
- package/dist/vitest.config.d.ts.map +0 -1
- package/dist/vitest.config.js +0 -7
package/dist/src/cli/run.js
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import JSON5 from 'json5';
|
|
4
5
|
import { validateTestcase } from '../validation/testcase.js';
|
|
6
|
+
import { classifyRunFailure, createInitialAiRuntimeTrace, nextRepairRound, writeAiRuntimeTrace, writeRepairReport, } from '../runtime/ai-runtime-trace.js';
|
|
7
|
+
import { runSpec } from './spec.js';
|
|
5
8
|
/**
|
|
6
9
|
* AI 追加段分隔标记。重跑时 detect 已有标记则覆盖(保证幂等)。
|
|
10
|
+
*
|
|
11
|
+
* 注释语义:本段由 uncoveredCandidates[].generationStrategy='ai' 的候选渲染而来。
|
|
7
12
|
*/
|
|
8
|
-
export const AI_APPEND_MARKER = '// === AI 钻探追加(基于 uncoveredCandidates[].
|
|
13
|
+
export const AI_APPEND_MARKER = '// === AI 钻探追加(基于 uncoveredCandidates[].generationStrategy=ai)===';
|
|
9
14
|
/**
|
|
10
15
|
* 解析 run 子命令参数。
|
|
11
16
|
*
|
|
12
17
|
* @param argv - 已剥离子命令名的命令行参数。
|
|
13
18
|
* @returns RunArgs。
|
|
14
|
-
* @throws 缺少 --
|
|
19
|
+
* @throws 缺少 --testcase 或参数对非法时抛错。
|
|
15
20
|
*/
|
|
16
21
|
export function parseRunArgs(argv) {
|
|
17
22
|
const result = {};
|
|
@@ -22,49 +27,174 @@ export function parseRunArgs(argv) {
|
|
|
22
27
|
if (!key?.startsWith('--') || !value) {
|
|
23
28
|
throw new Error(`无效参数对: ${key ?? '<empty>'}`);
|
|
24
29
|
}
|
|
25
|
-
result[key.slice(2)] = value;
|
|
30
|
+
result[key.slice(2).replace(/-([a-z])/g, (_, char) => char.toUpperCase())] = value;
|
|
26
31
|
}
|
|
27
|
-
if (!result.spec)
|
|
28
|
-
throw new Error('缺少 --spec');
|
|
29
32
|
if (!result.testcase)
|
|
30
33
|
throw new Error('缺少 --testcase');
|
|
31
34
|
return {
|
|
32
|
-
spec: result.spec,
|
|
33
35
|
testcase: result.testcase,
|
|
34
36
|
// headless 默认 true;--headless false 时关闭无头。
|
|
35
37
|
headless: result.headless !== 'false',
|
|
36
|
-
|
|
38
|
+
aiSnippets: result.aiSnippets,
|
|
37
39
|
};
|
|
38
40
|
}
|
|
39
41
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
42
|
+
* 判断 testcase 是否位于项目目录内部。
|
|
43
|
+
*
|
|
44
|
+
* @param projectDir - 测试项目目录。
|
|
45
|
+
* @param testcasePath - testcase.json5 路径。
|
|
46
|
+
* @returns true 表示 testcase 位于 projectDir 内。
|
|
47
|
+
*/
|
|
48
|
+
function isPathInsideProject(projectDir, testcasePath) {
|
|
49
|
+
const relative = path.relative(projectDir, testcasePath);
|
|
50
|
+
return !!relative && !relative.startsWith('..') && !path.isAbsolute(relative);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* 校验目录是否具备 glue 测试项目最小结构。
|
|
54
|
+
*
|
|
55
|
+
* @param candidateDir - 待校验目录。
|
|
56
|
+
* @returns true 表示目录结构满足 run 阶段回退条件。
|
|
57
|
+
*/
|
|
58
|
+
function isGlueProjectDir(candidateDir) {
|
|
59
|
+
return (existsSync(path.join(candidateDir, 'package.json')) &&
|
|
60
|
+
existsSync(path.join(candidateDir, 'playwright.config.ts')) &&
|
|
61
|
+
existsSync(path.join(candidateDir, 'src', 'skeletons')));
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* 从 testcase.json5 定位测试项目和 spec 路径。
|
|
65
|
+
*
|
|
66
|
+
* 优先消费 runtimeContext;若缺失或目录校验失败,则从 testcase 所在目录向上查找。
|
|
67
|
+
*
|
|
68
|
+
* @param testcasePath - testcase.json5 路径。
|
|
69
|
+
* @returns run 阶段消费的路径集合。
|
|
70
|
+
*/
|
|
71
|
+
export function resolveRunPaths(testcasePath) {
|
|
72
|
+
const resolvedTestcasePath = path.resolve(testcasePath);
|
|
73
|
+
const caseDir = path.dirname(resolvedTestcasePath);
|
|
74
|
+
const testcaseRaw = JSON5.parse(readFileSync(resolvedTestcasePath, 'utf8'));
|
|
75
|
+
const testcase = validateTestcase(testcaseRaw);
|
|
76
|
+
if (testcase.runtimeContext) {
|
|
77
|
+
const projectDir = path.resolve(testcase.runtimeContext.testProjectDir);
|
|
78
|
+
const specPath = path.resolve(testcase.runtimeContext.specPath);
|
|
79
|
+
if (isGlueProjectDir(projectDir) && isPathInsideProject(projectDir, resolvedTestcasePath)) {
|
|
80
|
+
return {
|
|
81
|
+
testcasePath: resolvedTestcasePath,
|
|
82
|
+
caseDir,
|
|
83
|
+
projectDir,
|
|
84
|
+
specPath,
|
|
85
|
+
contractPath: path.join(caseDir, '.ep-stage', 'contract.json5'),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
let cursor = caseDir;
|
|
90
|
+
while (true) {
|
|
91
|
+
if (isGlueProjectDir(cursor)) {
|
|
92
|
+
return {
|
|
93
|
+
testcasePath: resolvedTestcasePath,
|
|
94
|
+
caseDir,
|
|
95
|
+
projectDir: cursor,
|
|
96
|
+
specPath: path.join(caseDir, 'crud.spec.ts'),
|
|
97
|
+
contractPath: path.join(caseDir, '.ep-stage', 'contract.json5'),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
const parent = path.dirname(cursor);
|
|
101
|
+
if (parent === cursor) {
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
cursor = parent;
|
|
105
|
+
}
|
|
106
|
+
throw new Error(`无法从 testcase.json5 定位测试项目目录: ${resolvedTestcasePath}`);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* 构造从项目目录执行完整 spec 的 Playwright 命令。
|
|
110
|
+
*
|
|
111
|
+
* headless=false 时追加 --headed;specPath 原样透传(绝对路径,跨目录稳定)。
|
|
112
|
+
*
|
|
113
|
+
* @param input - projectDir / specPath / headless。
|
|
114
|
+
* @returns 命令、参数与 cwd,供 execFileSync 直接消费。
|
|
115
|
+
*/
|
|
116
|
+
export function buildPlaywrightCommand(input) {
|
|
117
|
+
// Playwright 命令行参数按正则表达式匹配测试文件路径;原样传中文/含点号
|
|
118
|
+
// 的绝对路径会被误解析为无效正则。取 basename 并转义正则特殊字符,
|
|
119
|
+
// 构造 `.*<escaped-basename>` 模式,在 testDir 内稳定命中对应 spec。
|
|
120
|
+
const basename = path.basename(input.specPath);
|
|
121
|
+
const escapedBasename = basename.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
122
|
+
const specArg = `.*${escapedBasename}`;
|
|
123
|
+
// 追加 list reporter 保证控制台有可被 classifyRunFailure 消费的原生输出;
|
|
124
|
+
// stage-core 等 reporter 会吞并原生输出,导致失败信息为空。
|
|
125
|
+
const args = ['playwright', 'test', specArg, '--reporter=list'];
|
|
126
|
+
if (!input.headless) {
|
|
127
|
+
args.push('--headed');
|
|
128
|
+
}
|
|
129
|
+
return { command: 'npx', args, cwd: input.projectDir };
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* 默认执行器:从 projectDir 通过 npx playwright test 执行完整 spec。
|
|
133
|
+
*
|
|
134
|
+
* 捕获 stdout 与 stderr;非零退出码即视为失败。同步执行(Playwright run 本身阻塞)。
|
|
135
|
+
*
|
|
136
|
+
* @param input - projectDir / specPath / headless。
|
|
137
|
+
* @returns passed 与合并输出。
|
|
138
|
+
*/
|
|
139
|
+
async function defaultExecuteFullSpec(input) {
|
|
140
|
+
const cmd = buildPlaywrightCommand(input);
|
|
141
|
+
// Windows 上 execFileSync 调用 npx 经常捕获不到子进程输出(status=null,
|
|
142
|
+
// stdout/stderr 为空)。改用 shell 执行 spawnSync,并手动拼接命令字符串
|
|
143
|
+
// 避免 Node 的 DEP0190 警告(shell=true 时仍传 args 会被弃用)。
|
|
144
|
+
const command = `${cmd.command} ${cmd.args.map((arg) => `"${arg.replace(/"/g, '\\"')}"`).join(' ')}`;
|
|
145
|
+
const spawned = spawnSync(command, {
|
|
146
|
+
cwd: cmd.cwd,
|
|
147
|
+
encoding: 'utf8',
|
|
148
|
+
shell: true,
|
|
149
|
+
});
|
|
150
|
+
const stdout = spawned.stdout ?? '';
|
|
151
|
+
const stderr = spawned.stderr ?? '';
|
|
152
|
+
const status = spawned.status ?? 1;
|
|
153
|
+
return { passed: status === 0, output: `${stdout}\n${stderr}` };
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* 获取默认 AI snippets 路径。
|
|
157
|
+
*
|
|
158
|
+
* @param caseDir - 用例目录。
|
|
159
|
+
* @returns 默认 .ep-stage/ai-snippets.json5 路径。
|
|
160
|
+
*/
|
|
161
|
+
function defaultAiSnippetsPath(caseDir) {
|
|
162
|
+
return path.join(caseDir, '.ep-stage', 'ai-snippets.json5');
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* 读取并校验 Agent O-D-A-V 产出的 snippets。
|
|
43
166
|
*
|
|
44
|
-
*
|
|
45
|
-
* (参考 run-gap-pipeline.ts 的 planAgentInferredWorkflows 调用链)。本 Sub-step
|
|
46
|
-
* 聚焦于「追加段幂等覆盖」契约,确保重跑不会无限叠加注释体。
|
|
167
|
+
* 只有 testcasePath 匹配、caseId 属于当前任务、status=resolved 且 specSnippet 非空时才可合并。
|
|
47
168
|
*
|
|
48
|
-
* @param
|
|
49
|
-
* @returns
|
|
169
|
+
* @param input - snippets 路径、testcase 路径和当前 AI tasks。
|
|
170
|
+
* @returns 可合并的 AiRuntimeResult 列表。
|
|
50
171
|
*/
|
|
51
|
-
function
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
172
|
+
export function loadAiSnippets(input) {
|
|
173
|
+
if (!existsSync(input.snippetsPath)) {
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
176
|
+
const parsed = JSON5.parse(readFileSync(input.snippetsPath, 'utf8'));
|
|
177
|
+
if (parsed.schemaVersion !== 'ep-stage-ai-snippets/v1') {
|
|
178
|
+
throw new Error(`ai-snippets.json5 schemaVersion 非法: ${input.snippetsPath}`);
|
|
179
|
+
}
|
|
180
|
+
if (path.resolve(String(parsed.testcasePath)) !== path.resolve(input.testcasePath)) {
|
|
181
|
+
throw new Error(`ai-snippets.json5 testcasePath 与当前 testcase 不匹配: ${input.snippetsPath}`);
|
|
182
|
+
}
|
|
183
|
+
if (!Array.isArray(parsed.results)) {
|
|
184
|
+
throw new Error(`ai-snippets.json5 缺少 results 数组: ${input.snippetsPath}`);
|
|
185
|
+
}
|
|
186
|
+
const taskIds = new Set(input.tasks.map((task) => task.caseId));
|
|
187
|
+
return parsed.results.filter((result) => {
|
|
188
|
+
if (!taskIds.has(result.caseId)) {
|
|
189
|
+
throw new Error(`ai-snippets.json5 包含未知 caseId: ${result.caseId}`);
|
|
190
|
+
}
|
|
191
|
+
return (result.status === 'resolved' &&
|
|
192
|
+
typeof result.specSnippet === 'string' &&
|
|
193
|
+
result.specSnippet.trim().length > 0);
|
|
194
|
+
});
|
|
65
195
|
}
|
|
66
196
|
/**
|
|
67
|
-
* 截断现有 spec 的 AI
|
|
197
|
+
* 截断现有 spec 的 AI 追加段(用于重跑覆盖,幂等)。
|
|
68
198
|
*
|
|
69
199
|
* @param specBody - 现有 spec 文件全文。
|
|
70
200
|
* @returns 截断到 AI_APPEND_MARKER 之前的基线 spec。
|
|
@@ -74,43 +204,342 @@ function truncateAtMarker(specBody) {
|
|
|
74
204
|
return markerIndex >= 0 ? specBody.slice(0, markerIndex).trimEnd() : specBody.trimEnd();
|
|
75
205
|
}
|
|
76
206
|
/**
|
|
77
|
-
*
|
|
207
|
+
* 按角色从 testcase.testAccess 中查找最佳匹配。
|
|
208
|
+
*
|
|
209
|
+
* @param testcase - 已校验 testcase。
|
|
210
|
+
* @param requiredRole - 候选用例要求角色。
|
|
211
|
+
* @returns 匹配到的系统与角色;未命中则返回 undefined。
|
|
212
|
+
*/
|
|
213
|
+
function findRoleAccess(testcase, requiredRole) {
|
|
214
|
+
for (const system of testcase.testAccess?.systems ?? []) {
|
|
215
|
+
for (const role of system.roles) {
|
|
216
|
+
if (role.role === requiredRole) {
|
|
217
|
+
return { system, role };
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return undefined;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* 宽松读取 credentials.json5,用于构建 AI Runtime Task 上下文。
|
|
225
|
+
*
|
|
226
|
+
* 这里不做全量强校验,避免未使用角色的空占位阻断任务包生成;真正执行前由运行门禁报告缺失字段。
|
|
227
|
+
*
|
|
228
|
+
* @param testcasePath - testcase.json5 绝对路径。
|
|
229
|
+
* @returns credentials 条目数组;缺文件或结构异常时返回空数组。
|
|
230
|
+
*/
|
|
231
|
+
function resolveCredentialPath(testcase, testcasePath) {
|
|
232
|
+
const ref = testcase.testAccess?.credentialRef ?? 'credentials.json5';
|
|
233
|
+
return path.isAbsolute(ref) ? ref : path.join(path.dirname(testcasePath), ref);
|
|
234
|
+
}
|
|
235
|
+
function loadCredentialEntriesLoose(testcase, testcasePath) {
|
|
236
|
+
const credPath = resolveCredentialPath(testcase, testcasePath);
|
|
237
|
+
if (!existsSync(credPath))
|
|
238
|
+
return [];
|
|
239
|
+
try {
|
|
240
|
+
const raw = JSON5.parse(readFileSync(credPath, 'utf8'));
|
|
241
|
+
return Array.isArray(raw) ? raw : [];
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
return [];
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* 从 contract.json5 读取待测页面 directUrl。
|
|
249
|
+
*
|
|
250
|
+
* @param contractPath - contract.json5 绝对路径。
|
|
251
|
+
* @returns directUrl 或 undefined。
|
|
252
|
+
*/
|
|
253
|
+
function loadContractDirectUrl(contractPath) {
|
|
254
|
+
if (!existsSync(contractPath))
|
|
255
|
+
return undefined;
|
|
256
|
+
try {
|
|
257
|
+
const raw = JSON5.parse(readFileSync(contractPath, 'utf8'));
|
|
258
|
+
return typeof raw.base?.navigation?.directUrl === 'string'
|
|
259
|
+
? raw.base.navigation.directUrl
|
|
260
|
+
: undefined;
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* 按角色从 credentials.json5 中查找运行访问数据。
|
|
268
|
+
*
|
|
269
|
+
* @param paths - run 阶段解析出的路径。
|
|
270
|
+
* @param requiredRole - 候选用例要求角色。
|
|
271
|
+
* @returns 匹配到的 credentials 条目。
|
|
272
|
+
*/
|
|
273
|
+
function findCredentialAccess(testcase, paths, requiredRole) {
|
|
274
|
+
return loadCredentialEntriesLoose(testcase, paths.testcasePath).find((entry) => entry.role === requiredRole);
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* 构建 AI Runtime Task 的入口上下文。
|
|
278
|
+
*
|
|
279
|
+
* @param testcase - 已校验 testcase。
|
|
280
|
+
* @param requiredRole - 候选用例要求角色。
|
|
281
|
+
* @param paths - run 阶段解析出的路径。
|
|
282
|
+
* @returns entryContext。
|
|
283
|
+
*/
|
|
284
|
+
function buildEntryContext(testcase, requiredRole, paths) {
|
|
285
|
+
const credentialAccess = findCredentialAccess(testcase, paths, requiredRole);
|
|
286
|
+
const matchedAccess = findRoleAccess(testcase, requiredRole);
|
|
287
|
+
return {
|
|
288
|
+
systemName: credentialAccess?.systemName ?? matchedAccess?.system.systemName,
|
|
289
|
+
directUrl: credentialAccess?.directUrl ?? loadContractDirectUrl(paths.contractPath),
|
|
290
|
+
loginUrl: credentialAccess?.loginUrl ?? credentialAccess?.url ?? matchedAccess?.system.loginUrl,
|
|
291
|
+
role: requiredRole,
|
|
292
|
+
username: credentialAccess?.username ?? matchedAccess?.role.username,
|
|
293
|
+
password: credentialAccess?.password ?? matchedAccess?.role.password,
|
|
294
|
+
permissions: matchedAccess?.role.permissions ?? [],
|
|
295
|
+
testProjectDir: paths.projectDir,
|
|
296
|
+
caseDir: paths.caseDir,
|
|
297
|
+
specPath: paths.specPath,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* 从 testcase 构建 AI Runtime Task 列表。
|
|
302
|
+
*
|
|
303
|
+
* `coverageStatus='blocked'` 的候选仅保留在 testcase/审阅侧,不进入 runtime task。
|
|
304
|
+
*
|
|
305
|
+
* @param testcase - 已校验 testcase。
|
|
306
|
+
* @param paths - run 阶段解析出的路径。
|
|
307
|
+
* @returns AI Runtime Task 列表。
|
|
308
|
+
*/
|
|
309
|
+
export function buildAiRuntimeTasks(testcase, paths) {
|
|
310
|
+
return testcase.uncoveredCandidates
|
|
311
|
+
.filter((item) => item.generationStrategy === 'ai' && item.coverageStatus !== 'blocked')
|
|
312
|
+
.map((item) => ({
|
|
313
|
+
caseId: item.caseId,
|
|
314
|
+
title: item.label,
|
|
315
|
+
role: item.requiredRole,
|
|
316
|
+
businessIntent: item.businessIntent,
|
|
317
|
+
assertionExpectation: item.assertionExpectation,
|
|
318
|
+
entryContext: buildEntryContext(testcase, item.requiredRole, paths),
|
|
319
|
+
knownContext: {
|
|
320
|
+
queryKey: item.queryKey,
|
|
321
|
+
precondition: item.precondition,
|
|
322
|
+
steps: item.steps,
|
|
323
|
+
assertion: item.assertion,
|
|
324
|
+
},
|
|
325
|
+
codingRules: {
|
|
326
|
+
devRulesPath: 'docs/00-memory/dev-rules.md',
|
|
327
|
+
},
|
|
328
|
+
status: 'pending',
|
|
329
|
+
}));
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* 生成 AI Runtime 初始任务包。
|
|
333
|
+
*
|
|
334
|
+
* 该函数只做确定性工作:定位路径、补齐 glue 骨架 spec、构建 tasks 并写 trace。
|
|
335
|
+
*
|
|
336
|
+
* @param testcasePath - testcase.json5 路径。
|
|
337
|
+
* @returns trace 路径、spec 路径和任务摘要。
|
|
338
|
+
*/
|
|
339
|
+
export function prepareAiRuntimeTasks(testcasePath) {
|
|
340
|
+
const paths = resolveRunPaths(testcasePath);
|
|
341
|
+
if (!existsSync(paths.specPath)) {
|
|
342
|
+
runSpec({ testcase: paths.testcasePath, out: paths.specPath });
|
|
343
|
+
}
|
|
344
|
+
const testcaseRaw = JSON5.parse(readFileSync(paths.testcasePath, 'utf8'));
|
|
345
|
+
const testcase = validateTestcase(testcaseRaw);
|
|
346
|
+
const tasks = buildAiRuntimeTasks(testcase, paths);
|
|
347
|
+
const blockedCount = testcase.uncoveredCandidates.filter((item) => item.generationStrategy === 'ai' && item.coverageStatus === 'blocked').length;
|
|
348
|
+
const tracePath = path.join(paths.caseDir, '.ep-stage', 'ai-runtime-trace.json5');
|
|
349
|
+
writeAiRuntimeTrace(tracePath, createInitialAiRuntimeTrace({
|
|
350
|
+
testcasePath: paths.testcasePath,
|
|
351
|
+
tasks,
|
|
352
|
+
}));
|
|
353
|
+
return {
|
|
354
|
+
testcasePath: paths.testcasePath,
|
|
355
|
+
tracePath,
|
|
356
|
+
specPath: paths.specPath,
|
|
357
|
+
taskCount: tasks.length,
|
|
358
|
+
blockedCount,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* 为缺少 resolved snippet 的 AI tasks 生成 needs_review 结果。
|
|
78
363
|
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
364
|
+
* @param tasks - 当前待推理任务。
|
|
365
|
+
* @param resolvedResults - 已由 Agent 解决的结果。
|
|
366
|
+
* @returns 未解决任务的 needs_review 结果。
|
|
367
|
+
*/
|
|
368
|
+
function buildNeedsReviewResults(tasks, resolvedResults) {
|
|
369
|
+
const resolvedIds = new Set(resolvedResults.map((result) => result.caseId));
|
|
370
|
+
return tasks
|
|
371
|
+
.filter((task) => !resolvedIds.has(task.caseId))
|
|
372
|
+
.map((task) => ({
|
|
373
|
+
caseId: task.caseId,
|
|
374
|
+
status: 'needs_review',
|
|
375
|
+
observation: {
|
|
376
|
+
summary: '未找到 Agent O-D-A-V 产出的 resolved snippet,等待人工或 Agent 继续推理。',
|
|
377
|
+
},
|
|
378
|
+
decision: {
|
|
379
|
+
action: 'skip merge and execution',
|
|
380
|
+
},
|
|
381
|
+
verification: {
|
|
382
|
+
assertionExpectation: task.assertionExpectation,
|
|
383
|
+
},
|
|
384
|
+
evidence: [{ source: 'ep-stage-cli', note: 'CLI 不生成占位 snippet,避免假通过' }],
|
|
385
|
+
}));
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* 生成器在 test.describe 闭包内输出的 in-place 定位 marker(` // @stage-gap-append`)。
|
|
389
|
+
*
|
|
390
|
+
* snippet 必须插到此 marker 之前,marker 与随后的 `});` 保留原位,才能让 AI test 落在
|
|
391
|
+
* describe 闭包内部并访问 recorder / workflowMeta / openModule / crudSlots 等常量。
|
|
392
|
+
*/
|
|
393
|
+
const AI_INPLACE_MARKER = ' // @stage-gap-append';
|
|
394
|
+
/**
|
|
395
|
+
* 将 resolved snippets 按任务顺序合并到 spec 的 AI_INPLACE_MARKER 之前(describe 闭包内)。
|
|
396
|
+
*
|
|
397
|
+
* @param specPath - crud.spec.ts 路径。
|
|
398
|
+
* @param tasks - 当前 AI tasks。
|
|
399
|
+
* @param results - Agent 产出的 resolved results。
|
|
400
|
+
*/
|
|
401
|
+
function appendAiSnippets(specPath, tasks, results) {
|
|
402
|
+
const byCaseId = new Map(results.map((result) => [result.caseId, result]));
|
|
403
|
+
const snippets = tasks
|
|
404
|
+
.map((task) => byCaseId.get(task.caseId)?.specSnippet)
|
|
405
|
+
.filter((snippet) => typeof snippet === 'string' && snippet.trim().length > 0);
|
|
406
|
+
const currentSpec = readFileSync(specPath, 'utf8');
|
|
407
|
+
const markerIndex = currentSpec.indexOf(AI_INPLACE_MARKER);
|
|
408
|
+
// Fallback:老 spec 无 in-place marker → 沿用旧的末尾追加逻辑(兼容)。
|
|
409
|
+
if (markerIndex < 0) {
|
|
410
|
+
const baseSpec = truncateAtMarker(currentSpec);
|
|
411
|
+
if (snippets.length > 0) {
|
|
412
|
+
writeFileSync(specPath, `${baseSpec}\n\n${AI_APPEND_MARKER}\n${snippets.join('\n\n')}\n`, 'utf8');
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
writeFileSync(specPath, `${baseSpec}\n`, 'utf8');
|
|
416
|
+
}
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
// in-place 合并:snippet 插入 marker 之前,marker 与随后 `});` 保留原位。
|
|
420
|
+
let before = currentSpec.slice(0, markerIndex);
|
|
421
|
+
const oldHeaderIndex = before.lastIndexOf(AI_APPEND_MARKER);
|
|
422
|
+
if (oldHeaderIndex >= 0) {
|
|
423
|
+
// 幂等清除上一轮追加段(旧 header 到当前 marker 之间的所有内容)。
|
|
424
|
+
before = `${before.slice(0, oldHeaderIndex).replace(/[\s\n]+$/, '')}\n\n`;
|
|
425
|
+
}
|
|
426
|
+
const after = currentSpec.slice(markerIndex);
|
|
427
|
+
if (snippets.length === 0) {
|
|
428
|
+
writeFileSync(specPath, `${before}${after}`, 'utf8');
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
const section = ` ${AI_APPEND_MARKER}\n${snippets.join('\n\n')}\n\n`;
|
|
432
|
+
writeFileSync(specPath, `${before}${section}${after}`, 'utf8');
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* 读取并声明消费 credentials.json5(消费边界声明)。
|
|
436
|
+
*
|
|
437
|
+
* 当前版本:缺失时不崩溃,返回提示字符串供上游告知用户填充;
|
|
438
|
+
* 存在则 JSON5.parse 读入(本阶段不实现真登录,仅声明消费边界)。
|
|
439
|
+
*
|
|
440
|
+
* @param testcasePath - testcase.json5 路径,credentials 约定与之间目录。
|
|
441
|
+
* @returns credentialsNotice:缺失时为提示文本;存在时为 undefined。
|
|
442
|
+
*/
|
|
443
|
+
function loadCredentialsNotice(testcase, testcasePath) {
|
|
444
|
+
const credPath = resolveCredentialPath(testcase, testcasePath);
|
|
445
|
+
if (!existsSync(credPath)) {
|
|
446
|
+
return `未找到凭据文件(预期路径:${credPath}),当前未消费凭据执行真实登录,请填充凭据后再启用真机模式。`;
|
|
447
|
+
}
|
|
448
|
+
// 读入以校验可解析;本 task 不实现真登录,仅声明消费边界。
|
|
449
|
+
JSON5.parse(readFileSync(credPath, 'utf8'));
|
|
450
|
+
return undefined;
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* run 子命令主体(REQ-CHAIN-02 / REQ-SPEC-01,新协议 v1-draft)。
|
|
86
454
|
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
455
|
+
* 行为:
|
|
456
|
+
* 1. resolveRunPaths 定位 projectDir/specPath;spec 缺失时先跑 runSpec 组装 glue 骨架。
|
|
457
|
+
* 2. 解析 testcase,构建 aiRuntimeTasks(已过滤 coverageStatus='blocked')。
|
|
458
|
+
* 3. 读取 Agent O-D-A-V 产出的 ai-snippets.json5。
|
|
459
|
+
* 4. 若 AI tasks 未全部 resolved,则写 trace results=needs_review 并停止,避免假通过。
|
|
460
|
+
* 5. 幂等合并 specSnippet 到 spec:truncateAtMarker 后追加 marker + snippets。
|
|
461
|
+
* 6. 执行完整 spec(deps 注入或默认真实 Playwright);失败则分类、回写 trace repairRounds 并写 repair report。
|
|
462
|
+
*
|
|
463
|
+
* 失败时不静默改变测试意图:写 repair-report.md 等待人审确认。
|
|
92
464
|
*
|
|
93
465
|
* @param args - 已解析参数。
|
|
94
|
-
* @
|
|
95
|
-
*
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (!existsSync(
|
|
100
|
-
|
|
101
|
-
|
|
466
|
+
* @param deps - 可选注入依赖(测试用于替换推理与执行)。
|
|
467
|
+
* @returns RunResult。
|
|
468
|
+
*/
|
|
469
|
+
export async function runRun(args, deps) {
|
|
470
|
+
const paths = resolveRunPaths(args.testcase);
|
|
471
|
+
if (!existsSync(paths.specPath)) {
|
|
472
|
+
runSpec({ testcase: paths.testcasePath, out: paths.specPath });
|
|
473
|
+
}
|
|
474
|
+
const testcaseRaw = JSON5.parse(readFileSync(paths.testcasePath, 'utf8'));
|
|
102
475
|
const testcase = validateTestcase(testcaseRaw);
|
|
103
|
-
const
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
476
|
+
const tracePath = path.join(paths.caseDir, '.ep-stage', 'ai-runtime-trace.json5');
|
|
477
|
+
const aiRuntimeTasks = buildAiRuntimeTasks(testcase, paths);
|
|
478
|
+
const trace = createInitialAiRuntimeTrace({
|
|
479
|
+
testcasePath: paths.testcasePath,
|
|
480
|
+
tasks: aiRuntimeTasks,
|
|
481
|
+
});
|
|
482
|
+
const snippetsPath = args.aiSnippets ? path.resolve(args.aiSnippets) : defaultAiSnippetsPath(paths.caseDir);
|
|
483
|
+
const loadSnippets = deps?.loadAiSnippets ?? loadAiSnippets;
|
|
484
|
+
const resolvedResults = loadSnippets({
|
|
485
|
+
snippetsPath,
|
|
486
|
+
testcasePath: paths.testcasePath,
|
|
487
|
+
tasks: aiRuntimeTasks,
|
|
488
|
+
});
|
|
489
|
+
const needsReviewResults = buildNeedsReviewResults(aiRuntimeTasks, resolvedResults);
|
|
490
|
+
trace.results = [...resolvedResults, ...needsReviewResults];
|
|
491
|
+
if (needsReviewResults.length > 0) {
|
|
492
|
+
writeAiRuntimeTrace(tracePath, trace);
|
|
493
|
+
const reportDir = path.join(paths.projectDir, 'e2e/glue-code-mvp/glue-report', new Date().toISOString().replace(/[:.]/g, '-'));
|
|
494
|
+
mkdirSync(reportDir, { recursive: true });
|
|
495
|
+
return {
|
|
496
|
+
specPath: paths.specPath,
|
|
497
|
+
reportDir,
|
|
498
|
+
credentialsNotice: loadCredentialsNotice(testcase, paths.testcasePath),
|
|
499
|
+
version: 'v1',
|
|
500
|
+
aiRuntimeTracePath: tracePath,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
appendAiSnippets(paths.specPath, aiRuntimeTasks, resolvedResults);
|
|
504
|
+
writeAiRuntimeTrace(tracePath, trace);
|
|
505
|
+
// 执行完整 spec。
|
|
506
|
+
const executor = deps?.executeFullSpec ?? defaultExecuteFullSpec;
|
|
507
|
+
const execution = await executor({
|
|
508
|
+
projectDir: paths.projectDir,
|
|
509
|
+
specPath: paths.specPath,
|
|
510
|
+
headless: args.headless,
|
|
511
|
+
});
|
|
512
|
+
// 失败时分类并写 repair report,停止等待人审;成功时 repairReportPath 留空。
|
|
513
|
+
let repairReportPath;
|
|
514
|
+
if (!execution.passed) {
|
|
515
|
+
const classification = classifyRunFailure(execution.output);
|
|
516
|
+
const round = nextRepairRound(paths.caseDir);
|
|
517
|
+
repairReportPath = writeRepairReport({
|
|
518
|
+
caseDir: paths.caseDir,
|
|
519
|
+
round,
|
|
520
|
+
failureType: classification.failureType,
|
|
521
|
+
summary: classification.summary,
|
|
522
|
+
output: execution.output,
|
|
523
|
+
});
|
|
524
|
+
trace.repairRounds.push({
|
|
525
|
+
round,
|
|
526
|
+
status: 'failed',
|
|
527
|
+
failureType: classification.failureType,
|
|
528
|
+
summary: classification.summary,
|
|
529
|
+
});
|
|
530
|
+
writeAiRuntimeTrace(tracePath, trace);
|
|
531
|
+
}
|
|
532
|
+
// 报告目录。
|
|
533
|
+
const reportDir = path.join(paths.projectDir, 'e2e/glue-code-mvp/glue-report', new Date().toISOString().replace(/[:.]/g, '-'));
|
|
534
|
+
mkdirSync(reportDir, { recursive: true });
|
|
535
|
+
// credentials.json5 消费边界声明。
|
|
536
|
+
const credentialsNotice = loadCredentialsNotice(testcase, paths.testcasePath);
|
|
537
|
+
return {
|
|
538
|
+
specPath: paths.specPath,
|
|
539
|
+
reportDir,
|
|
540
|
+
credentialsNotice,
|
|
541
|
+
version: 'v1',
|
|
542
|
+
aiRuntimeTracePath: tracePath,
|
|
543
|
+
repairReportPath,
|
|
544
|
+
};
|
|
116
545
|
}
|
package/dist/src/cli/spec.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* spec
|
|
2
|
+
* spec 子命令参数(新协议 v1-draft,REQ-SPEC-01)。
|
|
3
3
|
*
|
|
4
|
-
* - testcase:testcase.json5 路径(必填,由 testcase
|
|
4
|
+
* - testcase:testcase.json5 路径(必填,由 testcase 子命令产出,v1-draft);
|
|
5
5
|
* - out:.spec.ts 输出路径(必填,通常为 <tests-dir>/crud.spec.ts)。
|
|
6
6
|
*/
|
|
7
7
|
export type SpecArgs = {
|
|
@@ -12,7 +12,7 @@ export type SpecArgs = {
|
|
|
12
12
|
* spec 子命令产物。
|
|
13
13
|
*
|
|
14
14
|
* @property specPath - 写盘后的 .spec.ts 路径。
|
|
15
|
-
* @property version - 'v1' 表示只含
|
|
15
|
+
* @property version - 'v1' 表示只含 glue 骨架部分,待 run 子命令追加 AI 推理段。
|
|
16
16
|
*/
|
|
17
17
|
export type SpecResult = {
|
|
18
18
|
specPath: string;
|
|
@@ -27,16 +27,23 @@ export type SpecResult = {
|
|
|
27
27
|
*/
|
|
28
28
|
export declare function parseSpecArgs(argv: string[]): SpecArgs;
|
|
29
29
|
/**
|
|
30
|
-
* spec
|
|
30
|
+
* spec 子命令主体(新协议 v1-draft,REQ-SPEC-01):只生成 v1 初始版 .spec.ts,不启动浏览器。
|
|
31
31
|
*
|
|
32
|
-
* v1 = 仅
|
|
33
|
-
* run
|
|
32
|
+
* v1 = 仅 glue 骨架部分(顶部注释 `// === 胶水模式生成 ===`);
|
|
33
|
+
* run 子命令后续在底部 `// @stage-gap-append` 处追加 AI 推理段。
|
|
34
34
|
*
|
|
35
|
-
*
|
|
36
|
-
* 1.
|
|
37
|
-
* 2.
|
|
38
|
-
* 3.
|
|
39
|
-
* 4.
|
|
35
|
+
* 流程:
|
|
36
|
+
* 1. JSON5.parse 读 testcase.json5 → validateTestcase(v1-draft schema);
|
|
37
|
+
* 2. 门禁 assertTestcaseReadyForSpec(cases 全 confirmed;不阻断 uncoveredCandidates);
|
|
38
|
+
* 3. 定位 `.ep-stage/contract.json5`(不再是 contract.json;不存在则中文报错引导先跑 testcase 子命令);
|
|
39
|
+
* 4. JSON5.parse + validateContract;
|
|
40
|
+
* 5. 从 `contract.scenario.skeletonSlots.{crud,export}` 取槽位、
|
|
41
|
+
* 从 `testcase.{moduleId,moduleName}` 取模块 → 调
|
|
42
|
+
* {@link generateStageSkeletonCrudSpecFromSlots} 生成基线骨架脚本;
|
|
43
|
+
* 6. 头部插 SKELETON_MARKER_LINE 标记,写盘。
|
|
44
|
+
*
|
|
45
|
+
* 消费边界(REQ-INFER-008):只读 testcase.json5 + contract.json5,不读 inference-trace.json。
|
|
46
|
+
* 新链路零依赖旧 CrudBusinessModuleContract 类型。
|
|
40
47
|
*
|
|
41
48
|
* @param args - 解析后的参数。
|
|
42
49
|
* @returns SpecResult(含 version='v1' 显式标识阶段)。
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"spec.d.ts","sourceRoot":"","sources":["../../../src/cli/spec.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"spec.d.ts","sourceRoot":"","sources":["../../../src/cli/spec.ts"],"names":[],"mappings":"AASA;;;;;GAKG;AACH,MAAM,MAAM,QAAQ,GAAG;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,UAAU,GAAG;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,IAAI,CAAC;CACf,CAAC;AAiCF;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,QAAQ,CActD;AAcD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,QAAQ,GAAG,UAAU,CA+ClD"}
|