@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
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import JSON5 from 'json5';
|
|
4
5
|
import { runProbeAndCoverage } from './probe.js';
|
|
5
6
|
import { createSoftlinkWithFallback } from '../util/softlink.js';
|
|
6
7
|
import { i18nTestcase } from '../util/i18n-testcase.js';
|
|
8
|
+
import { extractCodeListSummary } from '../extractors/code-list.js';
|
|
9
|
+
import { resolveMaterialInventory } from '../material/material-inventory.js';
|
|
10
|
+
import { validateInferenceTrace } from '../validation/inference-trace.js';
|
|
11
|
+
import { writeProjectIndex } from '../context/stage-context.js';
|
|
7
12
|
/**
|
|
8
13
|
* 解析 testcase 子命令参数。
|
|
9
14
|
*
|
|
@@ -134,6 +139,89 @@ function renderCredentialsTemplate(testcase) {
|
|
|
134
139
|
}));
|
|
135
140
|
return `${JSON5.stringify(entries, null, 2)}\n`;
|
|
136
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* 从 code_list.md 解析 testcase 子命令所需的 HTML / Action 物料目录。
|
|
144
|
+
*
|
|
145
|
+
* @param codeListPath - code_list.md 绝对路径。
|
|
146
|
+
* @returns probe 阶段所需的 webapp / java 目录。
|
|
147
|
+
* @throws 无法唯一解析 HTML 或 Action 目录时抛错。
|
|
148
|
+
*/
|
|
149
|
+
function resolveProbeInputsFromCodeList(codeListPath) {
|
|
150
|
+
const codeList = extractCodeListSummary(codeListPath);
|
|
151
|
+
const materialResult = resolveMaterialInventory({
|
|
152
|
+
codeListPath,
|
|
153
|
+
codeList,
|
|
154
|
+
debugOverrides: {},
|
|
155
|
+
});
|
|
156
|
+
if (materialResult.resolvedInputs.webappDir.status !== 'resolved') {
|
|
157
|
+
throw new Error(`code_list.md 无法唯一解析 HTML 目录:${codeListPath}`);
|
|
158
|
+
}
|
|
159
|
+
if (materialResult.resolvedInputs.javaActionsDir.status !== 'resolved') {
|
|
160
|
+
throw new Error(`code_list.md 无法唯一解析 Action 目录:${codeListPath}`);
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
docsDir: materialResult.resolvedInputs.docsDir.status === 'resolved'
|
|
164
|
+
? materialResult.resolvedInputs.docsDir.path
|
|
165
|
+
: undefined,
|
|
166
|
+
webappDir: materialResult.resolvedInputs.webappDir.path,
|
|
167
|
+
javaActionsDir: materialResult.resolvedInputs.javaActionsDir.path,
|
|
168
|
+
hintsPath: materialResult.resolvedInputs.hintsPath.status === 'resolved'
|
|
169
|
+
? materialResult.resolvedInputs.hintsPath.path
|
|
170
|
+
: undefined,
|
|
171
|
+
moduleId: materialResult.resolvedInputs.moduleId.status === 'resolved'
|
|
172
|
+
? materialResult.resolvedInputs.moduleId.value
|
|
173
|
+
: undefined,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* 往 projects.index.json5 对应 project 的 requirements[] 追加(去重)一条需求条目。
|
|
178
|
+
*
|
|
179
|
+
* 设计:
|
|
180
|
+
* - 读 index 时同时保留 systems[](writeProjectIndex 整体覆写,丢失 systems 会丢凭据缓存);
|
|
181
|
+
* - 用 JSON5.parse(readFileSync) 直接读,不复用 readProjectIndexRaw(后者未导出且丢 systems);
|
|
182
|
+
* - 按 projectDir 精确匹配 project,找不到抛中文错误引导先跑 /glue-create-project;
|
|
183
|
+
* - 同 name 的 requirement 去重:存在则更新 requirementDir/testsDir/lastRunStatus,不存在则追加。
|
|
184
|
+
*
|
|
185
|
+
* @param input - projectDir / requirementName / requirementDir / testsDir / homeDir。
|
|
186
|
+
* @returns 写入后的索引文件路径。
|
|
187
|
+
* @throws projectDir 不在 index 时抛中文错误。
|
|
188
|
+
*/
|
|
189
|
+
function appendRequirementToProjectIndex(input) {
|
|
190
|
+
const homeDir = input.homeDir ?? os.homedir();
|
|
191
|
+
const indexPath = path.join(homeDir, '.ep-stage', 'projects.index.json5');
|
|
192
|
+
let parsed;
|
|
193
|
+
if (existsSync(indexPath)) {
|
|
194
|
+
parsed = JSON5.parse(readFileSync(indexPath, 'utf8'));
|
|
195
|
+
parsed.projects = parsed.projects ?? [];
|
|
196
|
+
parsed.systems = parsed.systems ?? [];
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
parsed = { projects: [], systems: [] };
|
|
200
|
+
}
|
|
201
|
+
const targetProjectDir = path.resolve(input.projectDir);
|
|
202
|
+
const projectIdx = parsed.projects.findIndex((p) => path.resolve(p.projectDir) === targetProjectDir);
|
|
203
|
+
if (projectIdx === -1) {
|
|
204
|
+
throw new Error(`未在 ~/.ep-stage/projects.index.json5 找到 projectDir=${input.projectDir} 的 glue 项目,请先跑 /glue-create-project 创建脚手架`);
|
|
205
|
+
}
|
|
206
|
+
const projects = parsed.projects;
|
|
207
|
+
const project = projects[projectIdx];
|
|
208
|
+
project.requirements = project.requirements ?? [];
|
|
209
|
+
const entry = {
|
|
210
|
+
name: input.requirementName,
|
|
211
|
+
requirementDir: input.requirementDir,
|
|
212
|
+
testsDir: input.testsDir,
|
|
213
|
+
lastRunStatus: 'never',
|
|
214
|
+
};
|
|
215
|
+
// 去重:同 name 存在则更新,不存在则追加。
|
|
216
|
+
const existIdx = project.requirements.findIndex((r) => r.name === entry.name);
|
|
217
|
+
if (existIdx >= 0) {
|
|
218
|
+
project.requirements[existIdx] = entry;
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
project.requirements.push(entry);
|
|
222
|
+
}
|
|
223
|
+
return writeProjectIndex({ homeDir, projects, systems: parsed.systems });
|
|
224
|
+
}
|
|
137
225
|
/**
|
|
138
226
|
* testcase 子命令主体(REQ-CHAIN-01 / REQ-CHAIN-02 / REQ-SCENE-01 / REQ-DATA-02)。
|
|
139
227
|
*
|
|
@@ -155,36 +243,51 @@ export function runGenerateTestcase(args) {
|
|
|
155
243
|
const scenario = decideScenario(args);
|
|
156
244
|
mkdirSync(args.testsDir, { recursive: true });
|
|
157
245
|
const requirementDir = args.requirementDir ?? path.dirname(args.codeList);
|
|
158
|
-
const
|
|
159
|
-
const
|
|
160
|
-
const
|
|
161
|
-
const
|
|
162
|
-
const { contract, testcaseV2 } = runProbeAndCoverage({
|
|
246
|
+
const resolvedInputs = resolveProbeInputsFromCodeList(args.codeList);
|
|
247
|
+
const docsDir = resolvedInputs.docsDir ?? requirementDir;
|
|
248
|
+
const moduleId = resolvedInputs.moduleId ?? path.basename(args.testsDir);
|
|
249
|
+
const { contract, testcaseV2, inferenceTrace } = runProbeAndCoverage({
|
|
163
250
|
moduleId,
|
|
164
251
|
codeListPath: args.codeList,
|
|
165
252
|
docsDir,
|
|
166
|
-
webappDir,
|
|
167
|
-
javaActionsDir,
|
|
168
|
-
hintsPath: args.hints,
|
|
253
|
+
webappDir: resolvedInputs.webappDir,
|
|
254
|
+
javaActionsDir: resolvedInputs.javaActionsDir,
|
|
255
|
+
hintsPath: args.hints ?? resolvedInputs.hintsPath,
|
|
169
256
|
scenario,
|
|
170
257
|
});
|
|
171
258
|
const epStageDir = path.join(args.testsDir, '.ep-stage');
|
|
172
259
|
mkdirSync(epStageDir, { recursive: true });
|
|
173
260
|
const contractPath = path.join(epStageDir, 'contract.json');
|
|
261
|
+
const inferenceTracePath = path.join(epStageDir, 'inference-trace.json');
|
|
174
262
|
const testcasePath = path.join(args.testsDir, 'testcase.json5');
|
|
175
263
|
const testcaseMdPath = path.join(args.testsDir, 'testcase.md');
|
|
176
264
|
const credentialsTemplatePath = path.join(args.testsDir, 'credentials.template.json5');
|
|
177
265
|
const credentialsActualPath = path.join(args.testsDir, 'credentials.json5');
|
|
178
266
|
// 落盘 contract.json 供 spec / run 子命令复用(避免重复钻探)。
|
|
179
267
|
writeFileSync(contractPath, `${JSON.stringify(contract, null, 2)}\n`, 'utf8');
|
|
268
|
+
// 落盘 inference-trace.json(解释链产物,REQ-INFER-004 / REQ-INFER-008;trace 只做解释,不驱动 spec)。
|
|
269
|
+
writeFileSync(inferenceTracePath, `${JSON.stringify(validateInferenceTrace(inferenceTrace), null, 2)}\n`, 'utf8');
|
|
180
270
|
writeFileSync(testcasePath, `${JSON5.stringify(testcaseV2, null, 2)}\n`, 'utf8');
|
|
181
271
|
writeFileSync(credentialsTemplatePath, renderCredentialsTemplate(testcaseV2), 'utf8');
|
|
182
272
|
writeFileSync(testcaseMdPath, renderTestcaseMarkdown(testcaseV2), 'utf8');
|
|
183
273
|
createSoftlinkWithFallback(args.codeList, path.join(args.testsDir, 'code_list.md'));
|
|
274
|
+
// 往 projects.index.json5 对应 project 追加(去重)一条 requirement 条目(skill Step 7)。
|
|
275
|
+
// 仅当显式传入 projectDir 时追加(skill Step 4 命令总带 --project-dir);
|
|
276
|
+
// 未传则跳过 —— 不反推 projectDir,避免非标准 testsDir 位置静默写入错误 project。
|
|
277
|
+
if (args.projectDir) {
|
|
278
|
+
appendRequirementToProjectIndex({
|
|
279
|
+
projectDir: args.projectDir,
|
|
280
|
+
// 需求名 = testsDir 的 basename(与 moduleId 来源一致:resolvedInputs.moduleId ?? path.basename(args.testsDir))。
|
|
281
|
+
requirementName: path.basename(args.testsDir),
|
|
282
|
+
requirementDir,
|
|
283
|
+
testsDir: args.testsDir,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
184
286
|
return {
|
|
185
287
|
testcasePath,
|
|
186
288
|
testcaseMdPath,
|
|
187
289
|
credentialsTemplatePath,
|
|
290
|
+
inferenceTracePath,
|
|
188
291
|
inference: {
|
|
189
292
|
scenario,
|
|
190
293
|
requiredSystems: testcaseV2.requiredSystems,
|
package/dist/src/cli/index.d.ts
CHANGED
|
@@ -15,4 +15,12 @@ export declare function main(argv?: string[]): Promise<unknown>;
|
|
|
15
15
|
* 打印 CLI help(仅 testcase / spec / run 3 子命令,无 contract)。
|
|
16
16
|
*/
|
|
17
17
|
export declare function printHelp(): void;
|
|
18
|
+
/**
|
|
19
|
+
* 判断当前 CLI 文件是否以“直接执行”方式启动。
|
|
20
|
+
*
|
|
21
|
+
* @param importMetaUrl - 当前模块的 import.meta.url。
|
|
22
|
+
* @param argv1 - 进程入口参数,可能是 npm bin 软链路径。
|
|
23
|
+
* @returns 若当前模块就是实际执行入口则返回 true。
|
|
24
|
+
*/
|
|
25
|
+
export declare function isDirectExecution(importMetaUrl: string, argv1: string | undefined): boolean;
|
|
18
26
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/cli/index.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/cli/index.ts"],"names":[],"mappings":";AAOA;;;;;;;;;;GAUG;AACH,wBAAsB,IAAI,CAAC,IAAI,GAAE,MAAM,EAAiB,GAAG,OAAO,CAAC,OAAO,CAAC,CAmB1E;AAED;;GAEG;AACH,wBAAgB,SAAS,IAAI,IAAI,CAYhC;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,aAAa,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAS3F"}
|
package/dist/src/cli/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { realpathSync } from 'node:fs';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
2
4
|
import { parseGenerateTestcaseArgs, runGenerateTestcase } from './generate-testcase.js';
|
|
3
5
|
import { parseSpecArgs, runSpec } from './spec.js';
|
|
4
6
|
import { parseRunArgs, runRun } from './run.js';
|
|
@@ -49,7 +51,25 @@ export function printHelp() {
|
|
|
49
51
|
spec 基于 testcase + contract 生成 v1 初始版 .spec.ts(只生成不执行)
|
|
50
52
|
run 启动 Playwright 跑胶水部分 + AI 推理追加 uncoveredCandidates → 产 v2 + 报告`);
|
|
51
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* 判断当前 CLI 文件是否以“直接执行”方式启动。
|
|
56
|
+
*
|
|
57
|
+
* @param importMetaUrl - 当前模块的 import.meta.url。
|
|
58
|
+
* @param argv1 - 进程入口参数,可能是 npm bin 软链路径。
|
|
59
|
+
* @returns 若当前模块就是实际执行入口则返回 true。
|
|
60
|
+
*/
|
|
61
|
+
export function isDirectExecution(importMetaUrl, argv1) {
|
|
62
|
+
try {
|
|
63
|
+
if (!argv1) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
return realpathSync(fileURLToPath(importMetaUrl)) === realpathSync(argv1);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
52
72
|
// 直接执行时调用 main;作为模块 import 时由调用方决定是否执行。
|
|
53
|
-
if (import.meta.url
|
|
73
|
+
if (isDirectExecution(import.meta.url, process.argv[1])) {
|
|
54
74
|
await main();
|
|
55
75
|
}
|
package/dist/src/cli/probe.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CrudBusinessModuleContract } from '../contracts/crud-business-module.js';
|
|
2
2
|
import type { CoverageDiffTrace } from '../contracts/observable-chain.js';
|
|
3
3
|
import type { TestcaseV2 } from '../testcase/testcase-v2.js';
|
|
4
|
+
import type { InferenceTraceV1 } from '../contracts/inference-trace.js';
|
|
4
5
|
/**
|
|
5
6
|
* probe 子模块入参(REQ-CHAIN-01)。
|
|
6
7
|
*
|
|
@@ -28,6 +29,7 @@ export type ProbeAndCoverageResult = {
|
|
|
28
29
|
contract: CrudBusinessModuleContract;
|
|
29
30
|
coverageDiff: CoverageDiffTrace;
|
|
30
31
|
testcaseV2: TestcaseV2;
|
|
32
|
+
inferenceTrace: InferenceTraceV1;
|
|
31
33
|
};
|
|
32
34
|
/**
|
|
33
35
|
* probe + coverage + testcase v2 装配的统一入口(REQ-CHAIN-01)。
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"probe.d.ts","sourceRoot":"","sources":["../../../src/cli/probe.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,0BAA0B,EAAe,MAAM,sCAAsC,CAAC;AACpG,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAC;AAC1E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"probe.d.ts","sourceRoot":"","sources":["../../../src/cli/probe.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,0BAA0B,EAAe,MAAM,sCAAsC,CAAC;AACpG,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAC;AAC1E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AAI7D,OAAO,KAAK,EAAE,gBAAgB,EAAoE,MAAM,iCAAiC,CAAC;AAI1I;;;;;GAKG;AACH,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACnC,QAAQ,EAAE,0BAA0B,CAAC;IACrC,YAAY,EAAE,iBAAiB,CAAC;IAChC,UAAU,EAAE,UAAU,CAAC;IACvB,cAAc,EAAE,gBAAgB,CAAC;CAClC,CAAC;AAoYF;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,oBAAoB,GAAG,sBAAsB,CAuFtF"}
|
package/dist/src/cli/probe.js
CHANGED
|
@@ -10,6 +10,8 @@ import { createSkeletonCoverage } from '../gap-planner/skeleton-coverage.js';
|
|
|
10
10
|
import { planAgentInferredWorkflows } from '../gap-planner/plan-agent-workflows.js';
|
|
11
11
|
import { summarizePageStructure } from '../capability/page-structure.js';
|
|
12
12
|
import { createCoverageDiff } from '../capability/coverage-diff.js';
|
|
13
|
+
import { buildSourceAnalysis } from '../inference/source-analysis.js';
|
|
14
|
+
import { inferModuleHintsFromMaterials } from '../inference/module-hints-inference.js';
|
|
13
15
|
/**
|
|
14
16
|
* 收集指定目录下的 HTML 物料文件。
|
|
15
17
|
*
|
|
@@ -35,23 +37,130 @@ function javaFiles(dir) {
|
|
|
35
37
|
.map((fileName) => path.join(dir, fileName));
|
|
36
38
|
}
|
|
37
39
|
/**
|
|
38
|
-
*
|
|
40
|
+
* 根据页面 ID 推断 CRUD 页面角色。
|
|
41
|
+
*
|
|
42
|
+
* @param pageId - 页面标识。
|
|
43
|
+
* @returns CRUD 页面角色。
|
|
44
|
+
*/
|
|
45
|
+
function inferCrudPageRole(pageId) {
|
|
46
|
+
if (pageId.endsWith('add'))
|
|
47
|
+
return 'add';
|
|
48
|
+
if (pageId.endsWith('edit'))
|
|
49
|
+
return 'edit';
|
|
50
|
+
if (pageId.endsWith('detail'))
|
|
51
|
+
return 'detail';
|
|
52
|
+
return 'list';
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 将 HTML 控件类型映射为 spec.yaml 兼容的组件类型。
|
|
56
|
+
*
|
|
57
|
+
* @param controlType - HTML extractor 提取到的控件类型。
|
|
58
|
+
* @returns spec 字段组件类型。
|
|
59
|
+
*/
|
|
60
|
+
function mapHtmlControlTypeToSpecComponent(controlType) {
|
|
61
|
+
if (controlType === 'mini-combobox') {
|
|
62
|
+
return 'listbox';
|
|
63
|
+
}
|
|
64
|
+
if (controlType === 'mini-textarea') {
|
|
65
|
+
return 'textarea';
|
|
66
|
+
}
|
|
67
|
+
return 'input';
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* 在缺少 spec.yaml 时,根据 HTML 页面结构合成最小可用的 spec。
|
|
71
|
+
*
|
|
72
|
+
* 设计取舍:
|
|
73
|
+
* - searchable: 仅由列表页搜索区字段决定;
|
|
74
|
+
* - formVisible: 由 add/edit 页面字段决定;
|
|
75
|
+
* - listVisible: 优先取列表页 datagrid 列定义;
|
|
76
|
+
* - businessRules: 当前无法可靠结构化抽取,保持空数组,交给 testcase 审阅阶段。
|
|
77
|
+
*
|
|
78
|
+
* @param docsDir - 需求文档目录。
|
|
79
|
+
* @param moduleId - 模块 ID。
|
|
80
|
+
* @param codeListSummary - code_list 摘要。
|
|
81
|
+
* @param pages - 已提取的 HTML 页面集合。
|
|
82
|
+
* @returns 供 contract 正常生成使用的推理 spec。
|
|
83
|
+
*/
|
|
84
|
+
function inferSpecFromPages(docsDir, moduleId, codeListSummary, pages) {
|
|
85
|
+
const fieldMap = new Map();
|
|
86
|
+
const listPage = pages.find((page) => inferCrudPageRole(page.pageId) === 'list');
|
|
87
|
+
/**
|
|
88
|
+
* 合并同名字段的页面证据,避免列表页 / 新增页重复字段互相覆盖。
|
|
89
|
+
*
|
|
90
|
+
* @param field - 当前页面字段。
|
|
91
|
+
* @param role - 当前页面角色。
|
|
92
|
+
* @returns 无返回值。
|
|
93
|
+
*/
|
|
94
|
+
function mergeField(field, role) {
|
|
95
|
+
const existing = fieldMap.get(field.name) ?? {
|
|
96
|
+
name: field.name,
|
|
97
|
+
label: field.label,
|
|
98
|
+
component: mapHtmlControlTypeToSpecComponent(field.controlType),
|
|
99
|
+
required: field.required,
|
|
100
|
+
searchable: false,
|
|
101
|
+
listVisible: false,
|
|
102
|
+
formVisible: false,
|
|
103
|
+
length: field.maxLength,
|
|
104
|
+
};
|
|
105
|
+
existing.label ||= field.label;
|
|
106
|
+
existing.component ||= mapHtmlControlTypeToSpecComponent(field.controlType);
|
|
107
|
+
existing.required ||= field.required;
|
|
108
|
+
existing.length ??= field.maxLength;
|
|
109
|
+
if (role === 'list') {
|
|
110
|
+
existing.searchable = true;
|
|
111
|
+
}
|
|
112
|
+
if (role === 'add' || role === 'edit') {
|
|
113
|
+
existing.formVisible = true;
|
|
114
|
+
}
|
|
115
|
+
fieldMap.set(field.name, existing);
|
|
116
|
+
}
|
|
117
|
+
for (const page of pages) {
|
|
118
|
+
const role = inferCrudPageRole(page.pageId);
|
|
119
|
+
for (const field of page.fields) {
|
|
120
|
+
mergeField(field, role);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
for (const column of listPage?.grids.flatMap((grid) => grid.dataColumns) ?? []) {
|
|
124
|
+
if (!column.field) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const existing = fieldMap.get(column.field) ?? {
|
|
128
|
+
name: column.field,
|
|
129
|
+
label: column.headerText || column.field,
|
|
130
|
+
component: 'input',
|
|
131
|
+
required: false,
|
|
132
|
+
searchable: false,
|
|
133
|
+
listVisible: false,
|
|
134
|
+
formVisible: false,
|
|
135
|
+
};
|
|
136
|
+
existing.label ||= column.headerText || column.field;
|
|
137
|
+
existing.listVisible = true;
|
|
138
|
+
fieldMap.set(column.field, existing);
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
path: path.join(docsDir, 'spec.inferred-from-html.yaml'),
|
|
142
|
+
module: {
|
|
143
|
+
id: moduleId,
|
|
144
|
+
label: codeListSummary.moduleLabel || moduleId,
|
|
145
|
+
},
|
|
146
|
+
fields: Array.from(fieldMap.values()),
|
|
147
|
+
businessRules: [],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* 解析 docs 目录下的 spec.yaml;缺失时根据 HTML 页面结构合成 fallback spec。
|
|
39
152
|
*
|
|
40
153
|
* @param docsDir - 需求文档目录。
|
|
41
154
|
* @param moduleId - 模块 ID。
|
|
42
155
|
* @param codeListSummary - 已解析的 code_list 摘要。
|
|
156
|
+
* @param pages - 已提取的 HTML 页面集合。
|
|
43
157
|
* @returns ExtractedSpecYaml。
|
|
44
158
|
*/
|
|
45
|
-
function resolveSpec(docsDir, moduleId, codeListSummary) {
|
|
159
|
+
function resolveSpec(docsDir, moduleId, codeListSummary, pages) {
|
|
46
160
|
const specPath = path.join(docsDir, 'spec.yaml');
|
|
47
161
|
if (existsSync(specPath))
|
|
48
162
|
return extractSpecYaml(specPath);
|
|
49
|
-
return
|
|
50
|
-
path: specPath,
|
|
51
|
-
module: { id: moduleId, label: codeListSummary.moduleLabel },
|
|
52
|
-
fields: [],
|
|
53
|
-
businessRules: [],
|
|
54
|
-
};
|
|
163
|
+
return inferSpecFromPages(docsDir, moduleId, codeListSummary, pages);
|
|
55
164
|
}
|
|
56
165
|
/**
|
|
57
166
|
* 推断 requiredRole 默认值。
|
|
@@ -173,6 +282,75 @@ function assembleTestcaseV2(input) {
|
|
|
173
282
|
},
|
|
174
283
|
};
|
|
175
284
|
}
|
|
285
|
+
/**
|
|
286
|
+
* 组装 materialInventory 章节(spec §7)。
|
|
287
|
+
*
|
|
288
|
+
* 把 probe 已解析到的 codeList / pages / actions / args 登记成可审阅的物料清单,
|
|
289
|
+
* 缺失项进 `missing` 数组。保持私有,不做跨文件抽象(Task 3 范围)。
|
|
290
|
+
*
|
|
291
|
+
* @param args - probe 入参。
|
|
292
|
+
* @param codeList - 已解析的 code_list 摘要。
|
|
293
|
+
* @param pages - 已抽取的 HTML 页面。
|
|
294
|
+
* @param actions - 已抽取的 Java Action。
|
|
295
|
+
* @returns MaterialInventoryTrace。
|
|
296
|
+
*/
|
|
297
|
+
function buildMaterialInventory(args, codeList, pages, actions) {
|
|
298
|
+
const missing = [];
|
|
299
|
+
const moduleIdSlot = { status: 'resolved', value: args.moduleId };
|
|
300
|
+
const menuSlot = codeList.menu
|
|
301
|
+
? { status: 'resolved', value: codeList.menu }
|
|
302
|
+
: (() => {
|
|
303
|
+
missing.push('menu');
|
|
304
|
+
return { status: 'missing', reason: 'code_list.md 未提供 menu' };
|
|
305
|
+
})();
|
|
306
|
+
const htmlFiles = pages.map((page) => ({
|
|
307
|
+
status: 'resolved',
|
|
308
|
+
value: page.path,
|
|
309
|
+
}));
|
|
310
|
+
const javaActionFiles = actions.map((action) => ({
|
|
311
|
+
status: 'resolved',
|
|
312
|
+
value: action.path,
|
|
313
|
+
}));
|
|
314
|
+
const docsFiles = [];
|
|
315
|
+
if (args.docsDir && existsSync(args.docsDir)) {
|
|
316
|
+
docsFiles.push({ status: 'resolved', value: args.docsDir });
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
missing.push('docsDir');
|
|
320
|
+
docsFiles.push({ status: 'missing', reason: '未提供 docsDir' });
|
|
321
|
+
}
|
|
322
|
+
return {
|
|
323
|
+
knowledgeRoot: codeList.knowledgeRoot ?? '',
|
|
324
|
+
moduleId: moduleIdSlot,
|
|
325
|
+
menu: menuSlot,
|
|
326
|
+
htmlFiles,
|
|
327
|
+
javaActionFiles,
|
|
328
|
+
docsFiles,
|
|
329
|
+
missing,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* 把 coverageDiff(CoverageDiffTrace)映射为 InferenceTraceV1.coverageDiff 的内联形状。
|
|
334
|
+
*
|
|
335
|
+
* 映射规则(controller 裁定):
|
|
336
|
+
* - coveredActions ← coverageDiff.covered
|
|
337
|
+
* - uncoveredCandidates ← [...gapCandidates, ...partiallyCovered, ...unresolved]
|
|
338
|
+
* - needsReview ← unresolved.map(i => i.actionId)
|
|
339
|
+
*
|
|
340
|
+
* @param coverageDiff - probe 产出的 CoverageDiffTrace。
|
|
341
|
+
* @returns InferenceTraceV1.coverageDiff。
|
|
342
|
+
*/
|
|
343
|
+
function mapCoverageDiffForTrace(coverageDiff) {
|
|
344
|
+
return {
|
|
345
|
+
coveredActions: coverageDiff.covered,
|
|
346
|
+
uncoveredCandidates: [
|
|
347
|
+
...coverageDiff.gapCandidates,
|
|
348
|
+
...coverageDiff.partiallyCovered,
|
|
349
|
+
...coverageDiff.unresolved,
|
|
350
|
+
],
|
|
351
|
+
needsReview: coverageDiff.unresolved.map((item) => item.actionId),
|
|
352
|
+
};
|
|
353
|
+
}
|
|
176
354
|
/**
|
|
177
355
|
* probe + coverage + testcase v2 装配的统一入口(REQ-CHAIN-01)。
|
|
178
356
|
*
|
|
@@ -185,16 +363,43 @@ function assembleTestcaseV2(input) {
|
|
|
185
363
|
* @returns contract + coverageDiff + 装配好的 TestcaseV2。
|
|
186
364
|
*/
|
|
187
365
|
export function runProbeAndCoverage(args) {
|
|
188
|
-
const hints = args.hintsPath
|
|
189
|
-
? JSON.parse(readFileSync(args.hintsPath, 'utf8'))
|
|
190
|
-
: undefined;
|
|
191
366
|
const codeList = extractCodeListSummary(args.codeListPath);
|
|
192
367
|
const pages = htmlFiles(args.webappDir).map(extractHtmlPage);
|
|
193
368
|
const actions = javaFiles(args.javaActionsDir).map(extractJavaAction);
|
|
369
|
+
// 源分析快照(spec §7 sourceAnalysis)。
|
|
370
|
+
const sourceAnalysis = buildSourceAnalysis({
|
|
371
|
+
codeListPath: args.codeListPath,
|
|
372
|
+
docsDir: args.docsDir,
|
|
373
|
+
codeList,
|
|
374
|
+
pages,
|
|
375
|
+
actions,
|
|
376
|
+
});
|
|
377
|
+
// 显式 hints 文件 vs 推理 hints:
|
|
378
|
+
// - 有 hintsPath 时读取为 explicitHints,mode 标记 explicit_override,explicitHints 流入 builders。
|
|
379
|
+
// - 无 hintsPath 时调 inferModuleHintsFromMaterials,推理出的 hints 流入 builders(替换原 undefined)。
|
|
380
|
+
let hints;
|
|
381
|
+
let moduleHintsInference;
|
|
382
|
+
if (args.hintsPath) {
|
|
383
|
+
const explicitHints = JSON.parse(readFileSync(args.hintsPath, 'utf8'));
|
|
384
|
+
// 仍跑一次推理以记录推理过程,但 mode 标记 explicit_override,builders 用 explicitHints。
|
|
385
|
+
const inferred = inferModuleHintsFromMaterials({
|
|
386
|
+
sourceAnalysis,
|
|
387
|
+
pages,
|
|
388
|
+
actions,
|
|
389
|
+
explicitHints,
|
|
390
|
+
});
|
|
391
|
+
moduleHintsInference = { ...inferred.inference, mode: 'explicit_override' };
|
|
392
|
+
hints = explicitHints;
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
const inferred = inferModuleHintsFromMaterials({ sourceAnalysis, pages, actions });
|
|
396
|
+
moduleHintsInference = inferred.inference;
|
|
397
|
+
hints = inferred.hints;
|
|
398
|
+
}
|
|
194
399
|
const pageStructures = pages.map(summarizePageStructure);
|
|
195
400
|
const contract = buildCrudBusinessModuleContract({
|
|
196
401
|
moduleId: args.moduleId,
|
|
197
|
-
spec: resolveSpec(args.docsDir, args.moduleId, codeList),
|
|
402
|
+
spec: resolveSpec(args.docsDir, args.moduleId, codeList, pages),
|
|
198
403
|
codeList,
|
|
199
404
|
pages,
|
|
200
405
|
actions,
|
|
@@ -217,5 +422,26 @@ export function runProbeAndCoverage(args) {
|
|
|
217
422
|
menuPath: args.menuPath ?? codeList.menu ?? '',
|
|
218
423
|
codeListPath: args.codeListPath,
|
|
219
424
|
});
|
|
220
|
-
|
|
425
|
+
// 组装 inference trace(REQ-INFER-004 / REQ-INFER-006)。
|
|
426
|
+
const materialInventory = buildMaterialInventory(args, codeList, pages, actions);
|
|
427
|
+
const reasoningSummary = {
|
|
428
|
+
conclusion: `基于 ${sourceAnalysis.mode} 源分析(置信度 ${sourceAnalysis.confidence})与 ${moduleHintsInference.mode} 槽位推理生成契约;coverageDiff unresolved ${coverageDiff.unresolved.length} 项。`,
|
|
429
|
+
confidence: sourceAnalysis.confidence,
|
|
430
|
+
risks: [
|
|
431
|
+
...sourceAnalysis.unresolved,
|
|
432
|
+
...moduleHintsInference.unresolved.map((slot) => `未解析槽位:${slot}`),
|
|
433
|
+
],
|
|
434
|
+
needsHumanReview: moduleHintsInference.unresolved.length > 0 || sourceAnalysis.confidence === 'low',
|
|
435
|
+
};
|
|
436
|
+
const inferenceTrace = {
|
|
437
|
+
schemaVersion: 'inference-trace/v1',
|
|
438
|
+
codeListPath: args.codeListPath,
|
|
439
|
+
generatedAt: new Date().toISOString(),
|
|
440
|
+
materialInventory,
|
|
441
|
+
sourceAnalysis,
|
|
442
|
+
moduleHintsInference,
|
|
443
|
+
coverageDiff: mapCoverageDiffForTrace(coverageDiff),
|
|
444
|
+
reasoningSummary,
|
|
445
|
+
};
|
|
446
|
+
return { contract, coverageDiff, testcaseV2, inferenceTrace };
|
|
221
447
|
}
|
package/dist/src/cli/spec.d.ts
CHANGED
|
@@ -34,9 +34,12 @@ export declare function parseSpecArgs(argv: string[]): SpecArgs;
|
|
|
34
34
|
*
|
|
35
35
|
* 实现:
|
|
36
36
|
* 1. 校验 testcase.json5 通过 validateTestcase(v2 schema);
|
|
37
|
-
* 2.
|
|
38
|
-
* 3.
|
|
39
|
-
* 4.
|
|
37
|
+
* 2. 门禁检查:用例必须全部 confirmed、未覆盖项不得为 candidate/unresolved(REQ-INFER-007);
|
|
38
|
+
* 3. 加载 contract.json(位于 <tests-dir>/.ep-stage/contract.json;不存在时报错引导用户先跑 testcase 子命令);
|
|
39
|
+
* 4. 调 generateStageSkeletonCrudSpec 生成基线骨架脚本;
|
|
40
|
+
* 5. 头部插入 SKELETON_MARKER_LINE 标记,写盘。
|
|
41
|
+
*
|
|
42
|
+
* 消费边界(REQ-INFER-008):只读 testcase.json5 + contract.json,不读 inference-trace.json。
|
|
40
43
|
*
|
|
41
44
|
* @param args - 解析后的参数。
|
|
42
45
|
* @returns SpecResult(含 version='v1' 显式标识阶段)。
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"spec.d.ts","sourceRoot":"","sources":["../../../src/cli/spec.ts"],"names":[],"mappings":"AAOA;;;;;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;
|
|
1
|
+
{"version":3,"file":"spec.d.ts","sourceRoot":"","sources":["../../../src/cli/spec.ts"],"names":[],"mappings":"AAOA;;;;;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;AAmCF;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,QAAQ,CActD;AAeD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,QAAQ,GAAG,UAAU,CAmBlD"}
|
package/dist/src/cli/spec.js
CHANGED
|
@@ -4,6 +4,30 @@ import JSON5 from 'json5';
|
|
|
4
4
|
import { generateStageSkeletonCrudSpec } from '../generators/stage-skeleton-script.js';
|
|
5
5
|
import { validateTestcase } from '../validation/testcase.js';
|
|
6
6
|
const SKELETON_MARKER_LINE = '// === 胶水模式生成(基于 contract + coveredActions)===';
|
|
7
|
+
/**
|
|
8
|
+
* spec 组装前的 testcase 就绪门禁(REQ-INFER-007)。
|
|
9
|
+
*
|
|
10
|
+
* 独立于 validateTestcase(纯 schema 校验,被 spec 和 run 共享),这里只判定
|
|
11
|
+
* "testcase 是否已通过人工审阅、可进入 spec 组装阶段"的 readiness 策略,
|
|
12
|
+
* 仅在 runSpec 内调用,不影响 run 子命令。
|
|
13
|
+
*
|
|
14
|
+
* 阻断条件:
|
|
15
|
+
* - 任一 cases[].reviewStatus !== 'confirmed'(needs_review / blocked 均阻断);
|
|
16
|
+
* - 任一 uncoveredCandidates[].status ∈ {'candidate','unresolved'}。
|
|
17
|
+
*
|
|
18
|
+
* @param testcase - 已通过 validateTestcase 的 TestcaseV2。
|
|
19
|
+
* @throws 存在未确认用例或未处理未覆盖项时抛中文错误。
|
|
20
|
+
*/
|
|
21
|
+
function assertTestcaseReadyForSpec(testcase) {
|
|
22
|
+
const unconfirmed = testcase.cases.find((c) => c.reviewStatus !== 'confirmed');
|
|
23
|
+
if (unconfirmed) {
|
|
24
|
+
throw new Error(`testcase.json5 存在未确认用例(needs_review/blocked),请逐条审阅置为 confirmed 后再生成 spec(caseId=${unconfirmed.caseId},reviewStatus=${unconfirmed.reviewStatus})`);
|
|
25
|
+
}
|
|
26
|
+
const unresolvedCandidate = testcase.uncoveredCandidates.find((item) => item.status === 'candidate' || item.status === 'unresolved');
|
|
27
|
+
if (unresolvedCandidate) {
|
|
28
|
+
throw new Error(`testcase.json5 存在 candidate/unresolved 未覆盖项,请处理后再生成 spec(actionId=${unresolvedCandidate.actionId},status=${unresolvedCandidate.status})`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
7
31
|
/**
|
|
8
32
|
* 解析 spec 子命令参数。
|
|
9
33
|
*
|
|
@@ -48,9 +72,12 @@ function locateContract(testcasePath) {
|
|
|
48
72
|
*
|
|
49
73
|
* 实现:
|
|
50
74
|
* 1. 校验 testcase.json5 通过 validateTestcase(v2 schema);
|
|
51
|
-
* 2.
|
|
52
|
-
* 3.
|
|
53
|
-
* 4.
|
|
75
|
+
* 2. 门禁检查:用例必须全部 confirmed、未覆盖项不得为 candidate/unresolved(REQ-INFER-007);
|
|
76
|
+
* 3. 加载 contract.json(位于 <tests-dir>/.ep-stage/contract.json;不存在时报错引导用户先跑 testcase 子命令);
|
|
77
|
+
* 4. 调 generateStageSkeletonCrudSpec 生成基线骨架脚本;
|
|
78
|
+
* 5. 头部插入 SKELETON_MARKER_LINE 标记,写盘。
|
|
79
|
+
*
|
|
80
|
+
* 消费边界(REQ-INFER-008):只读 testcase.json5 + contract.json,不读 inference-trace.json。
|
|
54
81
|
*
|
|
55
82
|
* @param args - 解析后的参数。
|
|
56
83
|
* @returns SpecResult(含 version='v1' 显式标识阶段)。
|
|
@@ -58,6 +85,7 @@ function locateContract(testcasePath) {
|
|
|
58
85
|
export function runSpec(args) {
|
|
59
86
|
const testcaseRaw = JSON5.parse(readFileSync(args.testcase, 'utf8'));
|
|
60
87
|
const testcase = validateTestcase(testcaseRaw);
|
|
88
|
+
assertTestcaseReadyForSpec(testcase);
|
|
61
89
|
const contractPath = locateContract(args.testcase);
|
|
62
90
|
if (!contractPath) {
|
|
63
91
|
throw new Error(`未找到 ${path.dirname(args.testcase)}/.ep-stage/contract.json。请先运行 \`ep-stage-skill testcase --code-list <p> --tests-dir <p>\` 生成 contract.json。`);
|