@epoint-testtech/ep-stage-skill 0.0.16 → 0.0.17
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/glue-generate-testcase/SKILL.md +7 -6
- package/codex-skill/ep-stage/glue-run-test/SKILL.md +97 -34
- package/codex-skill/ep-stage/glue-run-test/references/ai-runtime-loop.md +15 -9
- package/codex-skill/ep-stage/glue-run-test/references/crud-pipeline.md +10 -3
- package/codex-skill/ep-stage/glue-run-test/references/gap-review-protocol.md +11 -0
- package/codex-skill/ep-stage/glue-run-test/references/playwright-cli-odav.md +13 -2
- package/dist/src/cli/calibrate.d.ts +75 -0
- package/dist/src/cli/calibrate.d.ts.map +1 -0
- package/dist/src/cli/calibrate.js +318 -0
- package/dist/src/cli/demote.d.ts +53 -0
- package/dist/src/cli/demote.d.ts.map +1 -0
- package/dist/src/cli/demote.js +111 -0
- package/dist/src/cli/index.d.ts +5 -3
- package/dist/src/cli/index.d.ts.map +1 -1
- package/dist/src/cli/index.js +45 -6
- package/dist/src/cli/run-report.d.ts +6 -0
- package/dist/src/cli/run-report.d.ts.map +1 -1
- package/dist/src/cli/run-report.js +161 -147
- package/dist/src/cli/run.d.ts +51 -0
- package/dist/src/cli/run.d.ts.map +1 -1
- package/dist/src/cli/run.js +229 -27
- package/dist/src/contracts/glue-contract-v1.d.ts +37 -1
- package/dist/src/contracts/glue-contract-v1.d.ts.map +1 -1
- package/dist/src/extractors/code-list-json5.d.ts +29 -0
- package/dist/src/extractors/code-list-json5.d.ts.map +1 -1
- package/dist/src/generators/stage-skeleton-script.d.ts +30 -1
- package/dist/src/generators/stage-skeleton-script.d.ts.map +1 -1
- package/dist/src/generators/stage-skeleton-script.js +74 -11
- package/dist/src/runtime/ai-runtime-trace.d.ts +6 -0
- package/dist/src/runtime/ai-runtime-trace.d.ts.map +1 -1
- package/dist/src/testcase/contract-testcase-assembler.d.ts +23 -4
- package/dist/src/testcase/contract-testcase-assembler.d.ts.map +1 -1
- package/dist/src/testcase/contract-testcase-assembler.js +204 -27
- package/dist/src/testcase/query-component-map.d.ts +51 -0
- package/dist/src/testcase/query-component-map.d.ts.map +1 -0
- package/dist/src/testcase/query-component-map.js +64 -0
- package/dist/src/validation/contract.d.ts.map +1 -1
- package/dist/src/validation/contract.js +45 -3
- package/package.json +1 -1
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* calibrate 子命令:运行时校准回路(A 支柱)。
|
|
3
|
+
*
|
|
4
|
+
* 输入 page-element-scan 产出的 elementmap.json(运行时真实控件形态),按确定性规则
|
|
5
|
+
* 改写 contract.json5 的组件类型与定位符槽位,使静态装配与运行时实据对齐。
|
|
6
|
+
*
|
|
7
|
+
* 契约写入纪律(双写入者):contract 只允许 assembler(静态事实)与本 calibrator
|
|
8
|
+
* (运行时实据)两个写入者,均机器执行 + 账本落盘(calibration-report.json)+
|
|
9
|
+
* 过同一 validateContract 门禁;Agent 不手编 contract。
|
|
10
|
+
*
|
|
11
|
+
* 校准规则表(高置信自动改,低置信落 unresolvedSlots 人审):
|
|
12
|
+
* - R1-R4 查询字段 label 命中且 controlType 映射为白名单类型 → 改 component + 重建 slot;
|
|
13
|
+
* - R5 label 命中但 controlType 未识别 → 不改契约,unresolvedSlots += unsupported_query_component;
|
|
14
|
+
* - R6 searchFields label 在 elementmap 未命中 → unresolvedSlots += query_field_not_found;
|
|
15
|
+
* - R7 导出按钮定位符与 elementmap 实据不一致 → 用实据覆盖 exportButton;
|
|
16
|
+
* - R8 elementmap 无「查询/搜索」按钮 → unresolvedSlots += search_submit_unverified;
|
|
17
|
+
* - R9 elementmap 存在导出面板证据(待选列/已选列)→ 注入 exportContainer 语义锚点。
|
|
18
|
+
*
|
|
19
|
+
* 默认 dry-run(只打印变更计划);--write 才落盘并级联重生成 spec / 重渲染 testcase.md。
|
|
20
|
+
* 组件类别变化导致用例文案变化时,该用例 reviewStatus 拨回 needs_review(机器改语义必人审)。
|
|
21
|
+
*/
|
|
22
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
import JSON5 from 'json5';
|
|
25
|
+
import { validateContract } from '../validation/contract.js';
|
|
26
|
+
import { validateTestcase } from '../validation/testcase.js';
|
|
27
|
+
import { renderUnifiedTestcaseMarkdown } from '../testcase/testcase-markdown.js';
|
|
28
|
+
import { buildGlueExecutionFields } from '../testcase/contract-testcase-assembler.js';
|
|
29
|
+
import { resolveQueryComponent } from '../testcase/query-component-map.js';
|
|
30
|
+
import { resolveRunPaths } from './run.js';
|
|
31
|
+
import { runSpec } from './spec.js';
|
|
32
|
+
/**
|
|
33
|
+
* 解析 calibrate 子命令参数。
|
|
34
|
+
*
|
|
35
|
+
* @param argv - 已剥离子命令名的命令行参数。
|
|
36
|
+
* @returns CalibrateArgs。
|
|
37
|
+
*/
|
|
38
|
+
export function parseCalibrateArgs(argv) {
|
|
39
|
+
const result = {};
|
|
40
|
+
const flags = new Set();
|
|
41
|
+
const normalizedArgv = argv.filter((arg) => arg !== '--');
|
|
42
|
+
for (let index = 0; index < normalizedArgv.length; index++) {
|
|
43
|
+
const key = normalizedArgv[index];
|
|
44
|
+
if (key === '--write') {
|
|
45
|
+
flags.add('write');
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const value = normalizedArgv[index + 1];
|
|
49
|
+
if (!key?.startsWith('--') || !value || value.startsWith('--')) {
|
|
50
|
+
throw new Error(`无效参数对: ${key ?? '<empty>'}`);
|
|
51
|
+
}
|
|
52
|
+
result[key.slice(2).replace(/-([a-z])/g, (_, char) => char.toUpperCase())] = value;
|
|
53
|
+
index += 1;
|
|
54
|
+
}
|
|
55
|
+
if (!result.testcase)
|
|
56
|
+
throw new Error('缺少 --testcase');
|
|
57
|
+
return { testcase: result.testcase, elementmap: result.elementmap, write: flags.has('write') };
|
|
58
|
+
}
|
|
59
|
+
/** 24 小时(elementmap 新鲜度阈值)。 */
|
|
60
|
+
const ELEMENTMAP_STALE_MS = 24 * 60 * 60 * 1000;
|
|
61
|
+
/**
|
|
62
|
+
* 按校准后组件类型重建 searchConditions 槽位(与装配器槽位形状镜像)。
|
|
63
|
+
*
|
|
64
|
+
* @param kind - 校准后组件类型。
|
|
65
|
+
* @param field - 字段名。
|
|
66
|
+
* @param label - 中文标签。
|
|
67
|
+
* @param runtimeLocator - elementmap 实据定位器(可选)。
|
|
68
|
+
* @param existingSlot - 既有槽位(listbox 的 createSelections 等需保留)。
|
|
69
|
+
* @returns 新槽位对象。
|
|
70
|
+
*/
|
|
71
|
+
function buildCalibratedSlot(kind, field, label, runtimeLocator, existingSlot) {
|
|
72
|
+
const inputLocator = runtimeLocator ?? `xpath=//div[@label='${label}']//input`;
|
|
73
|
+
if (kind === 'input') {
|
|
74
|
+
return {
|
|
75
|
+
component: 'input',
|
|
76
|
+
field,
|
|
77
|
+
label,
|
|
78
|
+
seedValue: existingSlot?.seedValue ?? '测试数据',
|
|
79
|
+
strategy: 'timestamp_prefix',
|
|
80
|
+
addLocator: inputLocator,
|
|
81
|
+
editLocator: inputLocator,
|
|
82
|
+
listLocator: inputLocator,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (kind === 'datepicker') {
|
|
86
|
+
return {
|
|
87
|
+
component: 'datepicker',
|
|
88
|
+
field,
|
|
89
|
+
label,
|
|
90
|
+
listLocator: inputLocator,
|
|
91
|
+
searchAssertion: 'none',
|
|
92
|
+
triggerSearch: true,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if (kind === 'tree') {
|
|
96
|
+
return {
|
|
97
|
+
component: 'tree',
|
|
98
|
+
field,
|
|
99
|
+
label,
|
|
100
|
+
...(runtimeLocator ? { treeLocator: runtimeLocator } : {}),
|
|
101
|
+
nodeStrategy: 'first_leaf',
|
|
102
|
+
searchOnSelect: true,
|
|
103
|
+
searchAssertion: 'none',
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
component: 'listbox',
|
|
108
|
+
field,
|
|
109
|
+
label,
|
|
110
|
+
valueStrategy: 'first_non_placeholder',
|
|
111
|
+
skipValues: ['请选择'],
|
|
112
|
+
searchAssertion: 'none',
|
|
113
|
+
// 保留装配期按 add 表单推导的 createSelections;校准不改写 create 侧语义
|
|
114
|
+
createSelections: existingSlot?.createSelections ?? [],
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* 执行运行时校准。
|
|
119
|
+
*
|
|
120
|
+
* @param args - calibrate 参数。
|
|
121
|
+
* @returns CalibrateResult。
|
|
122
|
+
* @throws elementmap 缺失 / 校验门禁失败(不落盘)时抛错。
|
|
123
|
+
*/
|
|
124
|
+
export function runCalibrate(args) {
|
|
125
|
+
const testcasePath = path.resolve(args.testcase);
|
|
126
|
+
const paths = resolveRunPaths(testcasePath);
|
|
127
|
+
const elementmapPath = args.elementmap
|
|
128
|
+
? path.resolve(args.elementmap)
|
|
129
|
+
: path.join(paths.caseDir, '.ep-stage', 'elementmap.json');
|
|
130
|
+
if (!existsSync(elementmapPath)) {
|
|
131
|
+
throw new Error(`elementmap.json 不存在: ${elementmapPath}。请先经 /glue-run-test Step 4a(登录 + 全 frame 扫描)产出新鲜 elementmap。`);
|
|
132
|
+
}
|
|
133
|
+
const scannedAt = JSON5.parse(readFileSync(elementmapPath, 'utf8'));
|
|
134
|
+
if (scannedAt.scannedAt && Date.now() - Date.parse(scannedAt.scannedAt) > ELEMENTMAP_STALE_MS) {
|
|
135
|
+
console.warn(`警告:elementmap 扫描时间 ${scannedAt.scannedAt} 距今超过 24h,建议重新扫描后再校准。`);
|
|
136
|
+
}
|
|
137
|
+
const elementmap = scannedAt;
|
|
138
|
+
const elements = (elementmap.elements ?? []).filter((e) => e.visible !== false);
|
|
139
|
+
const contractPath = path.join(paths.caseDir, '.ep-stage', 'contract.json5');
|
|
140
|
+
if (!existsSync(contractPath)) {
|
|
141
|
+
throw new Error(`contract.json5 不存在: ${contractPath}(应先由 testcase 子命令产出)`);
|
|
142
|
+
}
|
|
143
|
+
const contract = JSON5.parse(readFileSync(contractPath, 'utf8'));
|
|
144
|
+
const testcase = validateTestcase(JSON5.parse(readFileSync(testcasePath, 'utf8')));
|
|
145
|
+
const changes = [];
|
|
146
|
+
const unresolvedAdded = [];
|
|
147
|
+
const casesPatched = [];
|
|
148
|
+
contract.unresolvedSlots = contract.unresolvedSlots ?? [];
|
|
149
|
+
const pushUnresolved = (record) => {
|
|
150
|
+
const duplicated = contract.unresolvedSlots.some((slot) => slot.kind === record.kind && slot.slotPath === record.slotPath);
|
|
151
|
+
if (!duplicated) {
|
|
152
|
+
contract.unresolvedSlots.push(record);
|
|
153
|
+
unresolvedAdded.push(record);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
const crudSlots = contract.scenario.skeletonSlots.crud;
|
|
157
|
+
const searchConditions = Array.isArray(crudSlots.searchConditions)
|
|
158
|
+
? crudSlots.searchConditions
|
|
159
|
+
: [];
|
|
160
|
+
crudSlots.searchConditions = searchConditions;
|
|
161
|
+
// R1-R6:查询字段组件形态校准
|
|
162
|
+
for (const searchField of contract.scenario.searchFields) {
|
|
163
|
+
const label = searchField.label;
|
|
164
|
+
const hit = elements.find((e) => e.type === 'field' && e.label === label);
|
|
165
|
+
if (!hit) {
|
|
166
|
+
pushUnresolved({
|
|
167
|
+
slotPath: `scenario.searchFields[${label}]`,
|
|
168
|
+
kind: 'query_field_not_found',
|
|
169
|
+
reason: `查询字段「${label}」在运行时 elementmap 中未命中(隐藏/未渲染/label 漂移)。`,
|
|
170
|
+
suggestedAction: 'human_review',
|
|
171
|
+
detectedAt: 'calibrate',
|
|
172
|
+
evidence: { source: 'elementmap', ref: path.basename(elementmapPath) },
|
|
173
|
+
});
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const controlType = hit.controlType ?? '';
|
|
177
|
+
const resolution = resolveQueryComponent({ component: controlType });
|
|
178
|
+
if (resolution.source === 'unrecognized') {
|
|
179
|
+
pushUnresolved({
|
|
180
|
+
slotPath: `scenario.searchFields[${label}]`,
|
|
181
|
+
kind: 'unsupported_query_component',
|
|
182
|
+
rawComponent: resolution.raw,
|
|
183
|
+
reason: `查询字段「${label}」运行时组件 ${resolution.raw} 不在 glue 白名单映射表。`,
|
|
184
|
+
suggestedAction: 'extend_component_library',
|
|
185
|
+
detectedAt: 'calibrate',
|
|
186
|
+
evidence: { source: 'elementmap', ref: hit.locator ?? label },
|
|
187
|
+
});
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
const nextKind = resolution.kind;
|
|
191
|
+
if (nextKind && nextKind !== searchField.component) {
|
|
192
|
+
const ruleByKind = {
|
|
193
|
+
input: 'R1', listbox: 'R2', datepicker: 'R3', tree: 'R4',
|
|
194
|
+
};
|
|
195
|
+
changes.push({
|
|
196
|
+
slotPath: `scenario.searchFields[${label}].component`,
|
|
197
|
+
from: searchField.component,
|
|
198
|
+
to: nextKind,
|
|
199
|
+
rule: ruleByKind[nextKind],
|
|
200
|
+
evidence: { label, locator: hit.locator, controlType },
|
|
201
|
+
});
|
|
202
|
+
searchField.component = nextKind;
|
|
203
|
+
// 重建 searchConditions 槽位(按 label 找既有槽位保留需保留字段)
|
|
204
|
+
const existingIndex = searchConditions.findIndex((c) => c.label === label);
|
|
205
|
+
const existingSlot = existingIndex >= 0 ? searchConditions[existingIndex] : undefined;
|
|
206
|
+
const nextSlot = buildCalibratedSlot(nextKind, searchField.field, label, hit.locator, existingSlot);
|
|
207
|
+
if (existingIndex >= 0) {
|
|
208
|
+
searchConditions[existingIndex] = nextSlot;
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
searchConditions.push(nextSlot);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// R7:导出按钮定位符用 elementmap 实据覆盖
|
|
216
|
+
const exportButtonHit = elements.find((e) => e.type === 'button' && e.label === '导出');
|
|
217
|
+
const exportLocators = (contract.scenario.skeletonSlots.export.locators ??= {});
|
|
218
|
+
if (exportButtonHit?.locator && exportLocators.exportButton !== exportButtonHit.locator) {
|
|
219
|
+
changes.push({
|
|
220
|
+
slotPath: 'scenario.skeletonSlots.export.locators.exportButton',
|
|
221
|
+
from: exportLocators.exportButton ?? '<未配置>',
|
|
222
|
+
to: exportButtonHit.locator,
|
|
223
|
+
rule: 'R7',
|
|
224
|
+
evidence: { label: '导出', locator: exportButtonHit.locator },
|
|
225
|
+
});
|
|
226
|
+
exportLocators.exportButton = exportButtonHit.locator;
|
|
227
|
+
}
|
|
228
|
+
// R8:查询/搜索按钮在 elementmap 中缺席 → 人审(树节点选择后仍要求显式提交)
|
|
229
|
+
const hasSearchSubmit = elements.some((e) => e.type === 'button' && (e.label === '查询' || e.label === '搜索'));
|
|
230
|
+
if (!hasSearchSubmit) {
|
|
231
|
+
pushUnresolved({
|
|
232
|
+
slotPath: 'scenario.skeletonSlots.crud.locators.listSearchSubmit',
|
|
233
|
+
kind: 'search_submit_unverified',
|
|
234
|
+
reason: 'elementmap 中未发现「查询/搜索」按钮;datepicker/input 条件的 triggerSearch 与 tree 的 searchOnSelect 语义需人工确认。',
|
|
235
|
+
suggestedAction: 'human_review',
|
|
236
|
+
detectedAt: 'calibrate',
|
|
237
|
+
evidence: { source: 'elementmap', ref: path.basename(elementmapPath) },
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
// R9:导出面板证据(待选列/已选列)→ 注入 exportContainer 语义锚点
|
|
241
|
+
const hasPanelEvidence = elements.some((e) => e.label === '待选列' || e.label === '已选列');
|
|
242
|
+
if (hasPanelEvidence && !exportLocators.exportContainer) {
|
|
243
|
+
const containerAnchor = "xpath=//div[.//*[normalize-space()='待选列'] and .//*[normalize-space()='已选列']]";
|
|
244
|
+
changes.push({
|
|
245
|
+
slotPath: 'scenario.skeletonSlots.export.locators.exportContainer',
|
|
246
|
+
from: '<未配置>',
|
|
247
|
+
to: containerAnchor,
|
|
248
|
+
rule: 'R9',
|
|
249
|
+
evidence: { label: '待选列/已选列' },
|
|
250
|
+
});
|
|
251
|
+
exportLocators.exportContainer = containerAnchor;
|
|
252
|
+
}
|
|
253
|
+
// 用例级联:组件类别变化影响 glue 用例语义时的诚实拨回(机器改语义必人审)。
|
|
254
|
+
// - read 用例:按新组件类型重算 steps/assertion,文案有变化才拨回 needs_review;
|
|
255
|
+
// - create/update/delete 用例:组件变为 datepicker/tree 后用例计划规则已不再产出它们
|
|
256
|
+
// (datepicker/tree 只产 read),无条件拨回 needs_review,由人审后重跑 testcase 子命令重整。
|
|
257
|
+
const changedFields = new Map(changes
|
|
258
|
+
.filter((change) => change.slotPath.startsWith('scenario.searchFields['))
|
|
259
|
+
.map((change) => [
|
|
260
|
+
change.slotPath.slice('scenario.searchFields['.length, -1),
|
|
261
|
+
change.to,
|
|
262
|
+
]));
|
|
263
|
+
const searchFieldLabels = contract.scenario.searchFields.map((field) => field.label);
|
|
264
|
+
for (const testcaseCase of testcase.cases) {
|
|
265
|
+
if (testcaseCase.generationStrategy !== 'glue')
|
|
266
|
+
continue;
|
|
267
|
+
if (!testcaseCase.queryKey)
|
|
268
|
+
continue;
|
|
269
|
+
const nextKind = changedFields.get(testcaseCase.queryKey);
|
|
270
|
+
if (!nextKind)
|
|
271
|
+
continue;
|
|
272
|
+
const isReadCase = testcaseCase.title.includes(' read');
|
|
273
|
+
if (!isReadCase) {
|
|
274
|
+
testcaseCase.reviewStatus = 'needs_review';
|
|
275
|
+
casesPatched.push(testcaseCase.caseId);
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
const rebuilt = buildGlueExecutionFields('read', testcaseCase.queryKey, nextKind, false, searchFieldLabels);
|
|
279
|
+
if (rebuilt.steps !== testcaseCase.steps || rebuilt.assertion !== testcaseCase.assertion) {
|
|
280
|
+
testcaseCase.steps = rebuilt.steps;
|
|
281
|
+
testcaseCase.assertion = rebuilt.assertion;
|
|
282
|
+
testcaseCase.reviewStatus = 'needs_review';
|
|
283
|
+
casesPatched.push(testcaseCase.caseId);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
// 门禁:改写后的 contract 必须过同一 validateContract(含白名单交叉规则),失败则不落盘。
|
|
287
|
+
validateContract(contract);
|
|
288
|
+
validateTestcase(testcase);
|
|
289
|
+
const report = {
|
|
290
|
+
schemaVersion: 'ep-stage-calibration-report/v1',
|
|
291
|
+
calibratedAt: new Date().toISOString(),
|
|
292
|
+
elementmapRef: elementmapPath,
|
|
293
|
+
changes,
|
|
294
|
+
unresolvedAdded,
|
|
295
|
+
testcaseCasesPatched: casesPatched.map((caseId) => ({ caseId })),
|
|
296
|
+
written: args.write,
|
|
297
|
+
};
|
|
298
|
+
const reportPath = path.join(paths.caseDir, '.ep-stage', 'calibration-report.json');
|
|
299
|
+
if (!args.write) {
|
|
300
|
+
console.log(JSON5.stringify(report, null, 2));
|
|
301
|
+
return { reportPath, changes, unresolvedAdded, casesPatched, written: false, specRegenerated: false };
|
|
302
|
+
}
|
|
303
|
+
writeFileSync(contractPath, `${JSON5.stringify(contract, null, 2)}\n`, 'utf8');
|
|
304
|
+
let specRegenerated = false;
|
|
305
|
+
if (casesPatched.length > 0) {
|
|
306
|
+
// 用例语义被机器改写 → reviewStatus 已拨回 needs_review,spec 门禁会阻断重生成;
|
|
307
|
+
// 本轮只落 contract/testcase/账本,spec 待人工确认用例后由 prepare-tasks/run 重生成。
|
|
308
|
+
writeFileSync(testcasePath, `${JSON5.stringify(testcase, null, 2)}\n`, 'utf8');
|
|
309
|
+
writeFileSync(path.join(paths.caseDir, 'testcase.md'), renderUnifiedTestcaseMarkdown(testcase, contract), 'utf8');
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
// 级联重生成 spec(无条件覆写;AI 追加段由 run 时从 ai-snippets.json5 幂等重建)
|
|
313
|
+
runSpec({ testcase: testcasePath, out: paths.specPath });
|
|
314
|
+
specRegenerated = true;
|
|
315
|
+
}
|
|
316
|
+
writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
|
|
317
|
+
return { reportPath, changes, unresolvedAdded, casesPatched, written: true, specRegenerated };
|
|
318
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* demote 子命令:把执行失败的 glue 用例降级为 AI 候选用例(运行期降级通道)。
|
|
3
|
+
*
|
|
4
|
+
* 触发场景:run 失败且 repair-report 分类为 locator_or_wait_error / iframe_context_error,
|
|
5
|
+
* 人工确认根因是「组件形态错配」(骨架组件库未覆盖该控件形态)后,由本命令做确定性的
|
|
6
|
+
* 桶位迁移,避免 Agent 手编 testcase.json5 出错。
|
|
7
|
+
*
|
|
8
|
+
* 行为(全链路确定性,逐步过门禁):
|
|
9
|
+
* 1. 从 cases[] 摘除目标 case,构造 uncoveredCandidates 条目(保留原 caseId 与执行字段,
|
|
10
|
+
* businessIntent 注明降级来源 repair-round 与根因);
|
|
11
|
+
* 2. validateTestcase 门禁后写回 testcase.json5,并重渲染 testcase.md;
|
|
12
|
+
* 3. 显式调 runSpec 重生成 spec(无条件覆写;AI 追加段由 run 时从 ai-snippets.json5 幂等重建)。
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* demote 子命令参数。
|
|
16
|
+
*/
|
|
17
|
+
export type DemoteArgs = {
|
|
18
|
+
/** testcase.json5 路径 */
|
|
19
|
+
testcase: string;
|
|
20
|
+
/** 待降级用例 ID(如 TC01003) */
|
|
21
|
+
caseId: string;
|
|
22
|
+
/** 降级原因(写入 .ep-stage/demote-runs 供人审追溯) */
|
|
23
|
+
reason: string;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* demote 子命令产物。
|
|
27
|
+
*/
|
|
28
|
+
export type DemoteResult = {
|
|
29
|
+
testcasePath: string;
|
|
30
|
+
/** 降级后的用例 ID(与原 caseId 相同,桶位从 cases[] 迁到 uncoveredCandidates[]) */
|
|
31
|
+
caseId: string;
|
|
32
|
+
/** 重生成的 spec 路径 */
|
|
33
|
+
specPath: string;
|
|
34
|
+
/** 重渲染的 testcase.md 路径 */
|
|
35
|
+
testcaseMdPath: string;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* 解析 demote 子命令参数。
|
|
39
|
+
*
|
|
40
|
+
* @param argv - 已剥离子命令名的命令行参数。
|
|
41
|
+
* @returns DemoteArgs。
|
|
42
|
+
* @throws 缺少必填参数时抛错。
|
|
43
|
+
*/
|
|
44
|
+
export declare function parseDemoteArgs(argv: string[]): DemoteArgs;
|
|
45
|
+
/**
|
|
46
|
+
* 执行用例降级:cases[] → uncoveredCandidates[],并重生成 testcase.md 与 spec。
|
|
47
|
+
*
|
|
48
|
+
* @param args - demote 参数。
|
|
49
|
+
* @returns DemoteResult。
|
|
50
|
+
* @throws 用例不存在 / 用例不在 glue 桶 / 门禁失败时抛错(不写盘)。
|
|
51
|
+
*/
|
|
52
|
+
export declare function runDemote(args: DemoteArgs): DemoteResult;
|
|
53
|
+
//# sourceMappingURL=demote.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"demote.d.ts","sourceRoot":"","sources":["../../../src/cli/demote.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAYH;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG;IACvB,wBAAwB;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,0BAA0B;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,2CAA2C;IAC3C,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,mEAAmE;IACnE,MAAM,EAAE,MAAM,CAAC;IACf,mBAAmB;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,0BAA0B;IAC1B,cAAc,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,CAmB1D;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,UAAU,GAAG,YAAY,CAqExD"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* demote 子命令:把执行失败的 glue 用例降级为 AI 候选用例(运行期降级通道)。
|
|
3
|
+
*
|
|
4
|
+
* 触发场景:run 失败且 repair-report 分类为 locator_or_wait_error / iframe_context_error,
|
|
5
|
+
* 人工确认根因是「组件形态错配」(骨架组件库未覆盖该控件形态)后,由本命令做确定性的
|
|
6
|
+
* 桶位迁移,避免 Agent 手编 testcase.json5 出错。
|
|
7
|
+
*
|
|
8
|
+
* 行为(全链路确定性,逐步过门禁):
|
|
9
|
+
* 1. 从 cases[] 摘除目标 case,构造 uncoveredCandidates 条目(保留原 caseId 与执行字段,
|
|
10
|
+
* businessIntent 注明降级来源 repair-round 与根因);
|
|
11
|
+
* 2. validateTestcase 门禁后写回 testcase.json5,并重渲染 testcase.md;
|
|
12
|
+
* 3. 显式调 runSpec 重生成 spec(无条件覆写;AI 追加段由 run 时从 ai-snippets.json5 幂等重建)。
|
|
13
|
+
*/
|
|
14
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import JSON5 from 'json5';
|
|
17
|
+
import { validateTestcase } from '../validation/testcase.js';
|
|
18
|
+
import { validateContract } from '../validation/contract.js';
|
|
19
|
+
import { renderUnifiedTestcaseMarkdown } from '../testcase/testcase-markdown.js';
|
|
20
|
+
import { resolveRunPaths } from './run.js';
|
|
21
|
+
import { runSpec } from './spec.js';
|
|
22
|
+
/**
|
|
23
|
+
* 解析 demote 子命令参数。
|
|
24
|
+
*
|
|
25
|
+
* @param argv - 已剥离子命令名的命令行参数。
|
|
26
|
+
* @returns DemoteArgs。
|
|
27
|
+
* @throws 缺少必填参数时抛错。
|
|
28
|
+
*/
|
|
29
|
+
export function parseDemoteArgs(argv) {
|
|
30
|
+
const result = {};
|
|
31
|
+
const normalizedArgv = argv.filter((arg) => arg !== '--');
|
|
32
|
+
for (let index = 0; index < normalizedArgv.length; index += 2) {
|
|
33
|
+
const key = normalizedArgv[index];
|
|
34
|
+
const value = normalizedArgv[index + 1];
|
|
35
|
+
if (!key?.startsWith('--') || !value) {
|
|
36
|
+
throw new Error(`无效参数对: ${key ?? '<empty>'}`);
|
|
37
|
+
}
|
|
38
|
+
result[key.slice(2).replace(/-([a-z])/g, (_, char) => char.toUpperCase())] = value;
|
|
39
|
+
}
|
|
40
|
+
if (!result.testcase)
|
|
41
|
+
throw new Error('缺少 --testcase');
|
|
42
|
+
if (!result.caseId)
|
|
43
|
+
throw new Error('缺少 --case-id');
|
|
44
|
+
if (!result.reason)
|
|
45
|
+
throw new Error('缺少 --reason(建议带 repair-round 与根因,供人审追溯)');
|
|
46
|
+
return {
|
|
47
|
+
testcase: result.testcase,
|
|
48
|
+
caseId: result.caseId,
|
|
49
|
+
reason: result.reason,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* 执行用例降级:cases[] → uncoveredCandidates[],并重生成 testcase.md 与 spec。
|
|
54
|
+
*
|
|
55
|
+
* @param args - demote 参数。
|
|
56
|
+
* @returns DemoteResult。
|
|
57
|
+
* @throws 用例不存在 / 用例不在 glue 桶 / 门禁失败时抛错(不写盘)。
|
|
58
|
+
*/
|
|
59
|
+
export function runDemote(args) {
|
|
60
|
+
const testcasePath = path.resolve(args.testcase);
|
|
61
|
+
const paths = resolveRunPaths(testcasePath);
|
|
62
|
+
const testcaseRaw = JSON5.parse(readFileSync(testcasePath, 'utf8'));
|
|
63
|
+
const testcase = validateTestcase(testcaseRaw);
|
|
64
|
+
const caseIndex = testcase.cases.findIndex((c) => c.caseId === args.caseId);
|
|
65
|
+
if (caseIndex < 0) {
|
|
66
|
+
const inUncovered = testcase.uncoveredCandidates.find((c) => c.caseId === args.caseId);
|
|
67
|
+
throw new Error(inUncovered
|
|
68
|
+
? `用例 ${args.caseId} 已是 AI 候选(uncoveredCandidates),无需降级`
|
|
69
|
+
: `用例 ${args.caseId} 不存在于 cases[](可降级对象仅限 glue 桶用例)`);
|
|
70
|
+
}
|
|
71
|
+
const [target] = testcase.cases.splice(caseIndex, 1);
|
|
72
|
+
const firstRole = testcase.testAccess?.requiredRoles?.[0] ?? '业务角色待确认';
|
|
73
|
+
const demoted = {
|
|
74
|
+
caseId: target.caseId,
|
|
75
|
+
label: target.title,
|
|
76
|
+
generationStrategy: 'ai',
|
|
77
|
+
coverageStatus: 'gap',
|
|
78
|
+
requiredRole: firstRole,
|
|
79
|
+
// Runtime evidence belongs in demote-runs, not in the testcase fact source.
|
|
80
|
+
businessIntent: target.title,
|
|
81
|
+
assertionExpectation: target.assertion ?? '(原用例无断言文本,见 repair 记录)',
|
|
82
|
+
queryKey: target.queryKey ?? '不适用',
|
|
83
|
+
precondition: target.precondition,
|
|
84
|
+
steps: target.steps,
|
|
85
|
+
assertion: target.assertion,
|
|
86
|
+
};
|
|
87
|
+
testcase.uncoveredCandidates.push(demoted);
|
|
88
|
+
// 门禁:迁移后的 testcase 必须仍然合法(桶位约束等),失败则不写盘。
|
|
89
|
+
validateTestcase(testcase);
|
|
90
|
+
// contract 只读(testcase.md 重渲染需要按钮映射表等契约事实),缺失时降级为不带契约渲染。
|
|
91
|
+
const contractPath = path.join(paths.caseDir, '.ep-stage', 'contract.json5');
|
|
92
|
+
const contract = existsSync(contractPath)
|
|
93
|
+
? validateContract(JSON5.parse(readFileSync(contractPath, 'utf8')))
|
|
94
|
+
: undefined;
|
|
95
|
+
writeFileSync(testcasePath, `${JSON5.stringify(testcase, null, 2)}\n`, 'utf8');
|
|
96
|
+
const testcaseMdPath = path.join(paths.caseDir, 'testcase.md');
|
|
97
|
+
writeFileSync(testcaseMdPath, renderUnifiedTestcaseMarkdown(testcase, contract), 'utf8');
|
|
98
|
+
const demoteDir = path.join(paths.caseDir, '.ep-stage', 'demote-runs');
|
|
99
|
+
mkdirSync(demoteDir, { recursive: true });
|
|
100
|
+
const recordedAt = new Date().toISOString();
|
|
101
|
+
const demoteRecordPath = path.join(demoteDir, `${recordedAt.replace(/[:.]/g, '-')}-${args.caseId}.json`);
|
|
102
|
+
writeFileSync(demoteRecordPath, `${JSON.stringify({ caseId: args.caseId, reason: args.reason, recordedAt }, null, 2)}\n`, 'utf8');
|
|
103
|
+
// 显式重生成 spec(无条件覆写;AI 追加段由 run 时从 ai-snippets.json5 幂等重建)。
|
|
104
|
+
runSpec({ testcase: testcasePath, out: paths.specPath });
|
|
105
|
+
return {
|
|
106
|
+
testcasePath,
|
|
107
|
+
caseId: args.caseId,
|
|
108
|
+
specPath: paths.specPath,
|
|
109
|
+
testcaseMdPath,
|
|
110
|
+
};
|
|
111
|
+
}
|
package/dist/src/cli/index.d.ts
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* ep-stage-skill CLI
|
|
3
|
+
* ep-stage-skill CLI 统一入口:1 bin + 6 子命令。
|
|
4
4
|
*
|
|
5
5
|
* 用法:
|
|
6
6
|
* ep-stage-skill testcase --code-list <p> --tests-dir <p> --direct-url <url> [--project-dir <p>] [--requirement-dir <p>]
|
|
7
7
|
* ep-stage-skill spec --testcase <p> --out <p>
|
|
8
8
|
* ep-stage-skill prepare-tasks --testcase <p>
|
|
9
|
-
* ep-stage-skill run --testcase <p> [--ai-snippets <p>] [--headless false]
|
|
9
|
+
* ep-stage-skill run --testcase <p> [--ai-snippets <p>] [--headless false] [--strict-ai true]
|
|
10
|
+
* ep-stage-skill calibrate --testcase <p> [--elementmap <p>] [--write]
|
|
11
|
+
* ep-stage-skill demote --testcase <p> --case-id <id> --reason <text>
|
|
10
12
|
*
|
|
11
13
|
* 无 contract 子命令(contract 提取作为 testcase 子命令内部步骤;
|
|
12
14
|
* 开发期独立调试入口为 dev/extract-contract.ts,被 pnpm generate:crud-contract 调用)。
|
|
13
15
|
*/
|
|
14
16
|
export declare function main(argv?: string[]): Promise<unknown>;
|
|
15
17
|
/**
|
|
16
|
-
* 打印 CLI help
|
|
18
|
+
* 打印 CLI help。
|
|
17
19
|
*/
|
|
18
20
|
export declare function printHelp(): void;
|
|
19
21
|
/**
|
|
@@ -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":";AASA;;;;;;;;;;;;;GAaG;AACH,wBAAsB,IAAI,CAAC,IAAI,GAAE,MAAM,EAAiB,GAAG,OAAO,CAAC,OAAO,CAAC,CA0F1E;AAED;;GAEG;AACH,wBAAgB,SAAS,IAAI,IAAI,CAkBhC;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
|
@@ -4,14 +4,18 @@ import { fileURLToPath } from 'node:url';
|
|
|
4
4
|
import { parseGenerateTestcaseArgs, runGenerateTestcase } from './generate-testcase.js';
|
|
5
5
|
import { parseSpecArgs, runSpec } from './spec.js';
|
|
6
6
|
import { parseRunArgs, prepareAiRuntimeTasks, runRun } from './run.js';
|
|
7
|
+
import { parseDemoteArgs, runDemote } from './demote.js';
|
|
8
|
+
import { parseCalibrateArgs, runCalibrate } from './calibrate.js';
|
|
7
9
|
/**
|
|
8
|
-
* ep-stage-skill CLI
|
|
10
|
+
* ep-stage-skill CLI 统一入口:1 bin + 6 子命令。
|
|
9
11
|
*
|
|
10
12
|
* 用法:
|
|
11
13
|
* ep-stage-skill testcase --code-list <p> --tests-dir <p> --direct-url <url> [--project-dir <p>] [--requirement-dir <p>]
|
|
12
14
|
* ep-stage-skill spec --testcase <p> --out <p>
|
|
13
15
|
* ep-stage-skill prepare-tasks --testcase <p>
|
|
14
|
-
* ep-stage-skill run --testcase <p> [--ai-snippets <p>] [--headless false]
|
|
16
|
+
* ep-stage-skill run --testcase <p> [--ai-snippets <p>] [--headless false] [--strict-ai true]
|
|
17
|
+
* ep-stage-skill calibrate --testcase <p> [--elementmap <p>] [--write]
|
|
18
|
+
* ep-stage-skill demote --testcase <p> --case-id <id> --reason <text>
|
|
15
19
|
*
|
|
16
20
|
* 无 contract 子命令(contract 提取作为 testcase 子命令内部步骤;
|
|
17
21
|
* 开发期独立调试入口为 dev/extract-contract.ts,被 pnpm generate:crud-contract 调用)。
|
|
@@ -34,6 +38,34 @@ export async function main(argv = process.argv) {
|
|
|
34
38
|
].join('\n'));
|
|
35
39
|
return result;
|
|
36
40
|
}
|
|
41
|
+
case 'calibrate': {
|
|
42
|
+
const result = runCalibrate(parseCalibrateArgs(args));
|
|
43
|
+
console.log([
|
|
44
|
+
`calibration-report: ${result.reportPath}`,
|
|
45
|
+
`changes: ${result.changes.length}`,
|
|
46
|
+
`unresolved added: ${result.unresolvedAdded.length}`,
|
|
47
|
+
...(result.casesPatched.length > 0
|
|
48
|
+
? [`用例拨回 needs_review(待人工确认后重跑): ${result.casesPatched.join('、')}`]
|
|
49
|
+
: []),
|
|
50
|
+
result.written
|
|
51
|
+
? result.specRegenerated
|
|
52
|
+
? '已写盘(contract/spec 已级联重生成)'
|
|
53
|
+
: '已写盘(spec 待用例人审后重生成)'
|
|
54
|
+
: 'dry-run(未写盘,加 --write 生效)',
|
|
55
|
+
].join('\n'));
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
case 'demote': {
|
|
59
|
+
const result = runDemote(parseDemoteArgs(args));
|
|
60
|
+
console.log([
|
|
61
|
+
`已降级: ${result.caseId} → uncoveredCandidates(AI 桶)`,
|
|
62
|
+
`testcase: ${result.testcasePath}`,
|
|
63
|
+
`spec 已重生成: ${result.specPath}`,
|
|
64
|
+
`testcase.md 已重渲染: ${result.testcaseMdPath}`,
|
|
65
|
+
'下一步:重跑 prepare-tasks 产出该用例的 AI 任务,经 O-D-A-V 后 run。',
|
|
66
|
+
].join('\n'));
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
37
69
|
case 'run': {
|
|
38
70
|
const result = await runRun(parseRunArgs(args));
|
|
39
71
|
// 结果摘要回显(此前 run 静默退出,自动化无法感知成败)。
|
|
@@ -42,6 +74,9 @@ export async function main(argv = process.argv) {
|
|
|
42
74
|
`report: ${result.reportDir}`,
|
|
43
75
|
`trace: ${result.aiRuntimeTracePath}`,
|
|
44
76
|
];
|
|
77
|
+
if (result.needsReviewCount) {
|
|
78
|
+
lines.push(`AI 任务待审(本轮未执行): ${result.needsReviewCount} 条`);
|
|
79
|
+
}
|
|
45
80
|
if (result.comprehensiveReportPath) {
|
|
46
81
|
lines.push(`综合报告: ${result.comprehensiveReportPath}`);
|
|
47
82
|
}
|
|
@@ -66,12 +101,12 @@ export async function main(argv = process.argv) {
|
|
|
66
101
|
printHelp();
|
|
67
102
|
return;
|
|
68
103
|
default:
|
|
69
|
-
console.error(`未知子命令: ${subcommand}。可用: testcase / spec / prepare-tasks / run`);
|
|
104
|
+
console.error(`未知子命令: ${subcommand}。可用: testcase / spec / prepare-tasks / run / calibrate / demote`);
|
|
70
105
|
process.exit(1);
|
|
71
106
|
}
|
|
72
107
|
}
|
|
73
108
|
/**
|
|
74
|
-
* 打印 CLI help
|
|
109
|
+
* 打印 CLI help。
|
|
75
110
|
*/
|
|
76
111
|
export function printHelp() {
|
|
77
112
|
console.log(`ep-stage-skill — 胶水测试链路 CLI
|
|
@@ -80,13 +115,17 @@ export function printHelp() {
|
|
|
80
115
|
ep-stage-skill testcase --code-list <path> --tests-dir <path> --direct-url <url> [--project-dir <path>] [--requirement-dir <path>]
|
|
81
116
|
ep-stage-skill spec --testcase <path> --out <path>
|
|
82
117
|
ep-stage-skill prepare-tasks --testcase <path>
|
|
83
|
-
ep-stage-skill run --testcase <path> [--ai-snippets <path>] [--headless false]
|
|
118
|
+
ep-stage-skill run --testcase <path> [--ai-snippets <path>] [--headless false] [--strict-ai true]
|
|
119
|
+
ep-stage-skill calibrate --testcase <path> [--elementmap <path>] [--write]
|
|
120
|
+
ep-stage-skill demote --testcase <path> --case-id <id> --reason <text>
|
|
84
121
|
|
|
85
122
|
子命令:
|
|
86
123
|
testcase 读取 _docs/code_list.json5 + 生成 contract.json5 / testcase.json5 / inference-trace.json
|
|
87
124
|
spec 基于 testcase.json5 + contract.json5 的 glue cases 生成初始版 .spec.ts(只生成不执行)
|
|
88
125
|
prepare-tasks 组装 glue 骨架并输出 AI Runtime 任务包
|
|
89
|
-
run 合并 Agent 产出的 ai-snippets.json5 并执行完整 spec
|
|
126
|
+
run 合并 Agent 产出的 ai-snippets.json5 并执行完整 spec(默认部分执行:needs_review 的 AI 任务不阻断;--strict-ai true 恢复旧短路语义)
|
|
127
|
+
calibrate 用 elementmap.json 运行时实据校准 contract(组件形态/定位符),默认 dry-run,--write 落盘并重生成 spec
|
|
128
|
+
demote 把形态错配失败的 glue 用例降级为 AI 候选(迁移桶位并重渲染 testcase.md、重生成 spec)`);
|
|
90
129
|
}
|
|
91
130
|
/**
|
|
92
131
|
* 判断当前 CLI 文件是否以“直接执行”方式启动。
|
|
@@ -14,6 +14,12 @@ export type RunReportTestItem = {
|
|
|
14
14
|
durationMs: number;
|
|
15
15
|
location?: string;
|
|
16
16
|
errorMessage?: string;
|
|
17
|
+
/**
|
|
18
|
+
* 骨架运行时复核结论(如搜索前后数据量无变化/查询字段不在列表列)。
|
|
19
|
+
* 由 run 子命令从 stage-glue 执行记录(recorder JSON)按用例标题合并进来;
|
|
20
|
+
* 存在时报告按「待审阅」呈现,Playwright 层的 passed 不代表业务语义已验证。
|
|
21
|
+
*/
|
|
22
|
+
reviewNote?: string;
|
|
17
23
|
};
|
|
18
24
|
export type RunReportData = {
|
|
19
25
|
suiteTitle: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run-report.d.ts","sourceRoot":"","sources":["../../../src/cli/run-report.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,GAAG,SAAS,CAAC;IAClD,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"run-report.d.ts","sourceRoot":"","sources":["../../../src/cli/run-report.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,GAAG,SAAS,CAAC;IAClD,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,iBAAiB,EAAE,CAAC;IAC3B,mDAAmD;IACnD,OAAO,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAClD,wBAAwB;IACxB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,CAAC;AAkDF;;GAEG;AACH,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,MAAM,GAAG;IAC5D,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,iBAAiB,EAAE,CAAC;CAC5B,CA0CA;AAsFD;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM,CAgM/D"}
|