csspilot 0.4.0 → 0.6.0
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/README.md +76 -23
- package/dist/bin.js +1415 -308
- package/dist/index.js +1415 -308
- package/dist/skills/testpilot/SKILL.md +5 -1
- package/dist/skills/testpilot/manifest.yaml +1 -1
- package/dist/skills/testpilot/rules/assertion.md +8 -0
- package/dist/skills/testpilot/rules/case-design.md +13 -1
- package/dist/skills/testpilot/rules/dsl.md +62 -0
- package/dist/skills/testpilot/workflows/analyze-result.md +8 -5
- package/dist/skills/testpilot/workflows/create-case.md +4 -0
- package/dist/skills/testpilot/workflows/run-case.md +3 -0
- package/dist/skills/testpilot/workflows/test-requirements-review.md +46 -4
- package/dist/skills/testpilot/workflows/validate.md +4 -0
- package/package.json +11 -11
- package/LICENSE +0 -21
package/dist/bin.js
CHANGED
|
@@ -102,7 +102,7 @@ var require_visit = __commonJS({
|
|
|
102
102
|
var BREAK = Symbol("break visit");
|
|
103
103
|
var SKIP = Symbol("skip children");
|
|
104
104
|
var REMOVE = Symbol("remove node");
|
|
105
|
-
function
|
|
105
|
+
function visit3(node2, visitor) {
|
|
106
106
|
const visitor_ = initVisitor(visitor);
|
|
107
107
|
if (identity.isDocument(node2)) {
|
|
108
108
|
const cd = visit_(null, node2.contents, visitor_, Object.freeze([node2]));
|
|
@@ -111,9 +111,9 @@ var require_visit = __commonJS({
|
|
|
111
111
|
} else
|
|
112
112
|
visit_(null, node2, visitor_, Object.freeze([]));
|
|
113
113
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
114
|
+
visit3.BREAK = BREAK;
|
|
115
|
+
visit3.SKIP = SKIP;
|
|
116
|
+
visit3.REMOVE = REMOVE;
|
|
117
117
|
function visit_(key, node2, visitor, path) {
|
|
118
118
|
const ctrl = callVisitor(key, node2, visitor, path);
|
|
119
119
|
if (identity.isNode(ctrl) || identity.isPair(ctrl)) {
|
|
@@ -247,7 +247,7 @@ var require_visit = __commonJS({
|
|
|
247
247
|
throw new Error(`Cannot replace node with ${pt} parent`);
|
|
248
248
|
}
|
|
249
249
|
}
|
|
250
|
-
exports.visit =
|
|
250
|
+
exports.visit = visit3;
|
|
251
251
|
exports.visitAsync = visitAsync;
|
|
252
252
|
}
|
|
253
253
|
});
|
|
@@ -257,7 +257,7 @@ var require_directives = __commonJS({
|
|
|
257
257
|
"../../node_modules/.pnpm/yaml@2.9.0/node_modules/yaml/dist/doc/directives.js"(exports) {
|
|
258
258
|
"use strict";
|
|
259
259
|
var identity = require_identity();
|
|
260
|
-
var
|
|
260
|
+
var visit3 = require_visit();
|
|
261
261
|
var escapeChars = {
|
|
262
262
|
"!": "%21",
|
|
263
263
|
",": "%2C",
|
|
@@ -401,7 +401,7 @@ var require_directives = __commonJS({
|
|
|
401
401
|
let tagNames;
|
|
402
402
|
if (doc && tagEntries.length > 0 && identity.isNode(doc.contents)) {
|
|
403
403
|
const tags = {};
|
|
404
|
-
|
|
404
|
+
visit3.visit(doc.contents, (_key, node2) => {
|
|
405
405
|
if (identity.isNode(node2) && node2.tag)
|
|
406
406
|
tags[node2.tag] = true;
|
|
407
407
|
});
|
|
@@ -428,7 +428,7 @@ var require_anchors = __commonJS({
|
|
|
428
428
|
"../../node_modules/.pnpm/yaml@2.9.0/node_modules/yaml/dist/doc/anchors.js"(exports) {
|
|
429
429
|
"use strict";
|
|
430
430
|
var identity = require_identity();
|
|
431
|
-
var
|
|
431
|
+
var visit3 = require_visit();
|
|
432
432
|
function anchorIsValid(anchor2) {
|
|
433
433
|
if (/[\x00-\x19\s,[\]{}]/.test(anchor2)) {
|
|
434
434
|
const sa = JSON.stringify(anchor2);
|
|
@@ -439,7 +439,7 @@ var require_anchors = __commonJS({
|
|
|
439
439
|
}
|
|
440
440
|
function anchorNames(root) {
|
|
441
441
|
const anchors = /* @__PURE__ */ new Set();
|
|
442
|
-
|
|
442
|
+
visit3.visit(root, {
|
|
443
443
|
Value(_key, node2) {
|
|
444
444
|
if (node2.anchor)
|
|
445
445
|
anchors.add(node2.anchor);
|
|
@@ -619,7 +619,7 @@ var require_Alias = __commonJS({
|
|
|
619
619
|
"../../node_modules/.pnpm/yaml@2.9.0/node_modules/yaml/dist/nodes/Alias.js"(exports) {
|
|
620
620
|
"use strict";
|
|
621
621
|
var anchors = require_anchors();
|
|
622
|
-
var
|
|
622
|
+
var visit3 = require_visit();
|
|
623
623
|
var identity = require_identity();
|
|
624
624
|
var Node = require_Node();
|
|
625
625
|
var toJS = require_toJS();
|
|
@@ -645,7 +645,7 @@ var require_Alias = __commonJS({
|
|
|
645
645
|
nodes = ctx.aliasResolveCache;
|
|
646
646
|
} else {
|
|
647
647
|
nodes = [];
|
|
648
|
-
|
|
648
|
+
visit3.visit(doc, {
|
|
649
649
|
Node: (_key, node2) => {
|
|
650
650
|
if (identity.isAlias(node2) || identity.hasAnchor(node2))
|
|
651
651
|
nodes.push(node2);
|
|
@@ -5564,15 +5564,15 @@ var require_cst_visit = __commonJS({
|
|
|
5564
5564
|
var BREAK = Symbol("break visit");
|
|
5565
5565
|
var SKIP = Symbol("skip children");
|
|
5566
5566
|
var REMOVE = Symbol("remove item");
|
|
5567
|
-
function
|
|
5567
|
+
function visit3(cst, visitor) {
|
|
5568
5568
|
if ("type" in cst && cst.type === "document")
|
|
5569
5569
|
cst = { start: cst.start, value: cst.value };
|
|
5570
5570
|
_visit(Object.freeze([]), cst, visitor);
|
|
5571
5571
|
}
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5572
|
+
visit3.BREAK = BREAK;
|
|
5573
|
+
visit3.SKIP = SKIP;
|
|
5574
|
+
visit3.REMOVE = REMOVE;
|
|
5575
|
+
visit3.itemAtPath = (cst, path) => {
|
|
5576
5576
|
let item = cst;
|
|
5577
5577
|
for (const [field, index] of path) {
|
|
5578
5578
|
const tok = item?.[field];
|
|
@@ -5583,8 +5583,8 @@ var require_cst_visit = __commonJS({
|
|
|
5583
5583
|
}
|
|
5584
5584
|
return item;
|
|
5585
5585
|
};
|
|
5586
|
-
|
|
5587
|
-
const parent =
|
|
5586
|
+
visit3.parentCollection = (cst, path) => {
|
|
5587
|
+
const parent = visit3.itemAtPath(cst, path.slice(0, -1));
|
|
5588
5588
|
const field = path[path.length - 1][0];
|
|
5589
5589
|
const coll = parent?.[field];
|
|
5590
5590
|
if (coll && "items" in coll)
|
|
@@ -5615,7 +5615,7 @@ var require_cst_visit = __commonJS({
|
|
|
5615
5615
|
}
|
|
5616
5616
|
return typeof ctrl === "function" ? ctrl(item, path) : ctrl;
|
|
5617
5617
|
}
|
|
5618
|
-
exports.visit =
|
|
5618
|
+
exports.visit = visit3;
|
|
5619
5619
|
}
|
|
5620
5620
|
});
|
|
5621
5621
|
|
|
@@ -7331,7 +7331,7 @@ var require_dist = __commonJS({
|
|
|
7331
7331
|
var lineCounter = require_line_counter();
|
|
7332
7332
|
var parser = require_parser();
|
|
7333
7333
|
var publicApi = require_public_api();
|
|
7334
|
-
var
|
|
7334
|
+
var visit3 = require_visit();
|
|
7335
7335
|
exports.Composer = composer.Composer;
|
|
7336
7336
|
exports.Document = Document.Document;
|
|
7337
7337
|
exports.Schema = Schema.Schema;
|
|
@@ -7359,8 +7359,8 @@ var require_dist = __commonJS({
|
|
|
7359
7359
|
exports.parseAllDocuments = publicApi.parseAllDocuments;
|
|
7360
7360
|
exports.parseDocument = publicApi.parseDocument;
|
|
7361
7361
|
exports.stringify = publicApi.stringify;
|
|
7362
|
-
exports.visit =
|
|
7363
|
-
exports.visitAsync =
|
|
7362
|
+
exports.visit = visit3.visit;
|
|
7363
|
+
exports.visitAsync = visit3.visitAsync;
|
|
7364
7364
|
}
|
|
7365
7365
|
});
|
|
7366
7366
|
|
|
@@ -15418,7 +15418,7 @@ var recursive = /* @__PURE__ */ new WeakMap();
|
|
|
15418
15418
|
var NONE = 0;
|
|
15419
15419
|
var ASSUMED = 1;
|
|
15420
15420
|
var PROVEN = 2;
|
|
15421
|
-
function isRecursive(inst, stack,
|
|
15421
|
+
function isRecursive(inst, stack, resolve7) {
|
|
15422
15422
|
const cached2 = recursive.get(inst);
|
|
15423
15423
|
if (cached2 !== void 0)
|
|
15424
15424
|
return cached2 ? PROVEN : NONE;
|
|
@@ -15428,7 +15428,7 @@ function isRecursive(inst, stack, resolve5) {
|
|
|
15428
15428
|
let result = NONE;
|
|
15429
15429
|
const check2 = (child) => {
|
|
15430
15430
|
if (result !== PROVEN && child?._zod) {
|
|
15431
|
-
const answer = isRecursive(child, stack,
|
|
15431
|
+
const answer = isRecursive(child, stack, resolve7);
|
|
15432
15432
|
if (answer > result)
|
|
15433
15433
|
result = answer;
|
|
15434
15434
|
}
|
|
@@ -15439,7 +15439,7 @@ function isRecursive(inst, stack, resolve5) {
|
|
|
15439
15439
|
const desc = Object.getOwnPropertyDescriptor(sh, key);
|
|
15440
15440
|
if (spread && !desc.enumerable)
|
|
15441
15441
|
continue;
|
|
15442
|
-
const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack,
|
|
15442
|
+
const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve7) : NONE;
|
|
15443
15443
|
if (child > answer)
|
|
15444
15444
|
answer = child;
|
|
15445
15445
|
}
|
|
@@ -15506,7 +15506,7 @@ function isRecursive(inst, stack, resolve5) {
|
|
|
15506
15506
|
break;
|
|
15507
15507
|
// `$ZodLazy` caches its inner on the def, so a resolved edge is followed exactly
|
|
15508
15508
|
case "lazy": {
|
|
15509
|
-
const inner = def._cachedInner ?? (
|
|
15509
|
+
const inner = def._cachedInner ?? (resolve7 ? inst._zod.innerType : void 0);
|
|
15510
15510
|
merge2(inner ? isRecursive(inner, stack, false) : ASSUMED);
|
|
15511
15511
|
break;
|
|
15512
15512
|
}
|
|
@@ -30072,6 +30072,8 @@ var configSchema = external_exports.strictObject({
|
|
|
30072
30072
|
}).optional(),
|
|
30073
30073
|
/** 环境配置:default 指定默认环境名,其余键为环境名 -> { baseUrl } */
|
|
30074
30074
|
environment: external_exports.record(external_exports.string(), external_exports.union([external_exports.string().min(1), environmentEntrySchema])).optional(),
|
|
30075
|
+
/** Case 可见的项目变量白名单;值可通过 ${ENV_VAR} 从运行环境注入 */
|
|
30076
|
+
variables: external_exports.record(external_exports.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/), external_exports.string()).optional(),
|
|
30075
30077
|
/** 项目声明的执行端(playwright / wechatide);仅作声明,实际可用性以 doctor 检测为准 */
|
|
30076
30078
|
adapters: external_exports.array(external_exports.string().min(1)).optional(),
|
|
30077
30079
|
workspace: external_exports.strictObject({ default: external_exports.string().min(1).optional() }).optional(),
|
|
@@ -30136,6 +30138,9 @@ async function loadTestpilotConfig(cwd = process.cwd()) {
|
|
|
30136
30138
|
testDirectory: parsed.test?.directory,
|
|
30137
30139
|
projectName: parsed.project?.name,
|
|
30138
30140
|
environment,
|
|
30141
|
+
variables: parsed.variables ? Object.fromEntries(
|
|
30142
|
+
Object.entries(parsed.variables).map(([name, value]) => [name, interpolateEnvRefs(value)])
|
|
30143
|
+
) : void 0,
|
|
30139
30144
|
adapters: parsed.adapters,
|
|
30140
30145
|
workspace: parsed.workspace,
|
|
30141
30146
|
server: parsed.server ? {
|
|
@@ -30338,15 +30343,15 @@ function makeCiCommand() {
|
|
|
30338
30343
|
}
|
|
30339
30344
|
|
|
30340
30345
|
// src/commands/init/doctor.ts
|
|
30341
|
-
import { existsSync as
|
|
30342
|
-
import { join as
|
|
30346
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
30347
|
+
import { join as join24 } from "node:path";
|
|
30343
30348
|
|
|
30344
30349
|
// ../../packages/sdk/src/client.ts
|
|
30345
|
-
import { isAbsolute as
|
|
30350
|
+
import { isAbsolute as isAbsolute4, join as join19 } from "node:path";
|
|
30346
30351
|
|
|
30347
30352
|
// ../../packages/sdk/src/cases.ts
|
|
30348
|
-
import { mkdir as mkdir3, readFile as
|
|
30349
|
-
import { dirname, extname, isAbsolute, join as join6, resolve as
|
|
30353
|
+
import { mkdir as mkdir3, readFile as readFile4, readdir, stat as stat2, writeFile as writeFile3 } from "node:fs/promises";
|
|
30354
|
+
import { dirname as dirname2, extname as extname2, isAbsolute as isAbsolute2, join as join6, resolve as resolve3 } from "node:path";
|
|
30350
30355
|
|
|
30351
30356
|
// ../../packages/dsl/src/schema/index.ts
|
|
30352
30357
|
var STEP_TARGETS = ["web", "miniapp", "api"];
|
|
@@ -30367,6 +30372,12 @@ var locatorSchema = external_exports.strictObject({
|
|
|
30367
30372
|
text: external_exports.string().min(1).optional(),
|
|
30368
30373
|
css: external_exports.string().min(1).optional()
|
|
30369
30374
|
}).describe("text \u4E0E css \u53EA\u80FD\u63D0\u4F9B\u5176\u4E00");
|
|
30375
|
+
var requestExpectationSchema = external_exports.strictObject({
|
|
30376
|
+
method: external_exports.enum(HTTP_METHODS).optional(),
|
|
30377
|
+
urlContains: external_exports.string().min(1),
|
|
30378
|
+
count: external_exports.number().int().nonnegative(),
|
|
30379
|
+
windowMs: external_exports.number().int().nonnegative().default(1e3)
|
|
30380
|
+
});
|
|
30370
30381
|
var stepSchema = external_exports.strictObject({
|
|
30371
30382
|
target: external_exports.enum(STEP_TARGETS),
|
|
30372
30383
|
action: external_exports.enum(ACTIONS),
|
|
@@ -30381,8 +30392,18 @@ var stepSchema = external_exports.strictObject({
|
|
|
30381
30392
|
/** api target:请求体;对象按 JSON 序列化,字符串原样发送(支持 ${var} 模板) */
|
|
30382
30393
|
body: external_exports.union([external_exports.string(), external_exports.record(external_exports.string(), external_exports.unknown())]).optional(),
|
|
30383
30394
|
variable: external_exports.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/, "\u53D8\u91CF\u540D\u9700\u4EE5\u5B57\u6BCD\u6216\u4E0B\u5212\u7EBF\u5F00\u5934,\u4EC5\u542B\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u4E0B\u5212\u7EBF").optional(),
|
|
30384
|
-
timeout: external_exports.number().int().positive().optional()
|
|
30395
|
+
timeout: external_exports.number().int().positive().optional(),
|
|
30396
|
+
/** UI 动作触发的网络请求精确次数断言 */
|
|
30397
|
+
expectRequests: external_exports.array(requestExpectationSchema).min(1).optional()
|
|
30398
|
+
});
|
|
30399
|
+
var datasetReferenceSchema = external_exports.strictObject({
|
|
30400
|
+
file: external_exports.string().min(1),
|
|
30401
|
+
idField: external_exports.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/, "idField \u9700\u4E3A\u5408\u6CD5\u5B57\u6BB5\u540D").default("id")
|
|
30385
30402
|
});
|
|
30403
|
+
var dependencyPathSchema = external_exports.string().regex(
|
|
30404
|
+
/^(fixture|dataset|account|variable)(?:\.[A-Za-z_][A-Za-z0-9_]*)+$/,
|
|
30405
|
+
"requires \u8DEF\u5F84\u5FC5\u987B\u4EE5 fixture/dataset/account/variable \u5F00\u5934\u5E76\u5305\u542B\u5B57\u6BB5"
|
|
30406
|
+
);
|
|
30386
30407
|
var caseSchema = external_exports.strictObject({
|
|
30387
30408
|
id: external_exports.string().regex(/^[a-z0-9][a-z0-9-]*$/, "id \u9700\u4E3A kebab-case(\u5C0F\u5199\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u8FDE\u5B57\u7B26)"),
|
|
30388
30409
|
name: external_exports.string().min(1, "name \u4E0D\u80FD\u4E3A\u7A7A"),
|
|
@@ -30394,7 +30415,12 @@ var caseSchema = external_exports.strictObject({
|
|
|
30394
30415
|
/** 引用测试账号标识;凭据由 Environment + Server/CI Secret 管理,禁止写进 Case */
|
|
30395
30416
|
accountRef: external_exports.string().min(1).optional(),
|
|
30396
30417
|
tags: external_exports.array(external_exports.string().min(1)).optional(),
|
|
30397
|
-
|
|
30418
|
+
fixtures: external_exports.array(external_exports.string().min(1)).min(1).optional(),
|
|
30419
|
+
datasets: datasetReferenceSchema.optional(),
|
|
30420
|
+
requires: external_exports.array(dependencyPathSchema).min(1).optional(),
|
|
30421
|
+
setup: external_exports.array(stepSchema).min(1).optional(),
|
|
30422
|
+
steps: external_exports.array(stepSchema).min(1, "Case \u81F3\u5C11\u5305\u542B\u4E00\u4E2A step"),
|
|
30423
|
+
teardown: external_exports.array(stepSchema).min(1).optional()
|
|
30398
30424
|
});
|
|
30399
30425
|
|
|
30400
30426
|
// ../../packages/dsl/src/parser/index.ts
|
|
@@ -30445,6 +30471,7 @@ var ADAPTER_SUPPORT = {
|
|
|
30445
30471
|
};
|
|
30446
30472
|
var LOCATOR_REQUIRED = /* @__PURE__ */ new Set(["click", "input", "select", "assert", "extract"]);
|
|
30447
30473
|
var VALUE_REQUIRED = /* @__PURE__ */ new Set(["input", "select"]);
|
|
30474
|
+
var REQUEST_EXPECTATION_ACTIONS = /* @__PURE__ */ new Set(["navigate", "click", "input", "select"]);
|
|
30448
30475
|
var FIELD_ALLOWED = {
|
|
30449
30476
|
url: ["navigate", "request"],
|
|
30450
30477
|
value: ["input", "select", "extract"],
|
|
@@ -30459,12 +30486,21 @@ var VARIABLE_REF = /\$\{([A-Za-z_][A-Za-z0-9_.]*)\}/g;
|
|
|
30459
30486
|
function validateCase(data) {
|
|
30460
30487
|
const issues = [];
|
|
30461
30488
|
const declared = /* @__PURE__ */ new Set();
|
|
30489
|
+
if (data.fixtures) declared.add("fixture");
|
|
30490
|
+
if (data.datasets) declared.add("dataset");
|
|
30491
|
+
declared.add("variable");
|
|
30462
30492
|
if (data.accountRef) {
|
|
30463
30493
|
declared.add("account");
|
|
30464
30494
|
}
|
|
30495
|
+
data.setup?.forEach((step, index) => {
|
|
30496
|
+
checkStep(step, `setup[${index}]`, declared, issues);
|
|
30497
|
+
});
|
|
30465
30498
|
data.steps.forEach((step, index) => {
|
|
30466
30499
|
checkStep(step, `steps[${index}]`, declared, issues);
|
|
30467
30500
|
});
|
|
30501
|
+
data.teardown?.forEach((step, index) => {
|
|
30502
|
+
checkStep(step, `teardown[${index}]`, declared, issues);
|
|
30503
|
+
});
|
|
30468
30504
|
return issues;
|
|
30469
30505
|
}
|
|
30470
30506
|
function validateCaseSource(source) {
|
|
@@ -30539,6 +30575,15 @@ function checkStep(step, at, declared, issues) {
|
|
|
30539
30575
|
if (step.action === "wait" && !step.locator && step.timeout === void 0) {
|
|
30540
30576
|
issues.push({ path: at, code: "semantic", message: "wait \u9700\u8981\u63D0\u4F9B locator(\u7B49\u5F85\u5143\u7D20)\u6216 timeout(\u6BEB\u79D2)" });
|
|
30541
30577
|
}
|
|
30578
|
+
if (step.expectRequests) {
|
|
30579
|
+
if (step.target === "api" || !REQUEST_EXPECTATION_ACTIONS.has(step.action)) {
|
|
30580
|
+
issues.push({
|
|
30581
|
+
path: `${at}.expectRequests`,
|
|
30582
|
+
code: "semantic",
|
|
30583
|
+
message: "expectRequests \u4EC5\u7528\u4E8E web/miniapp \u7684 navigate/click/input/select"
|
|
30584
|
+
});
|
|
30585
|
+
}
|
|
30586
|
+
}
|
|
30542
30587
|
checkVariableRefs(step, at, declared, issues);
|
|
30543
30588
|
}
|
|
30544
30589
|
function checkLocator(step, at, issues) {
|
|
@@ -30571,9 +30616,14 @@ function checkVariableRefs(step, at, declared, issues) {
|
|
|
30571
30616
|
collect("value", step.value);
|
|
30572
30617
|
collect("expected", step.expected);
|
|
30573
30618
|
collect("url", step.url);
|
|
30619
|
+
collect("locator.text", step.locator?.text);
|
|
30620
|
+
collect("locator.css", step.locator?.css);
|
|
30574
30621
|
if (typeof step.body === "string") collect("body", step.body);
|
|
30575
30622
|
else if (step.body) collect("body", JSON.stringify(step.body));
|
|
30576
30623
|
for (const headerValue of Object.values(step.headers ?? {})) collect("headers", headerValue);
|
|
30624
|
+
for (const [index, expectation] of (step.expectRequests ?? []).entries()) {
|
|
30625
|
+
collect(`expectRequests[${index}].urlContains`, expectation.urlContains);
|
|
30626
|
+
}
|
|
30577
30627
|
for (const [field, name] of refs) {
|
|
30578
30628
|
const root = name.split(".")[0] ?? name;
|
|
30579
30629
|
if (!declared.has(root)) {
|
|
@@ -30588,9 +30638,20 @@ function checkVariableRefs(step, at, declared, issues) {
|
|
|
30588
30638
|
|
|
30589
30639
|
// ../../packages/sdk/src/project.ts
|
|
30590
30640
|
import { mkdir as mkdir2, readFile as readFile2, stat, writeFile as writeFile2 } from "node:fs/promises";
|
|
30591
|
-
import {
|
|
30641
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
30642
|
+
import { dirname, join as join5, resolve } from "node:path";
|
|
30592
30643
|
function resolveRoot(root) {
|
|
30593
|
-
|
|
30644
|
+
const start = resolve(root ?? process.cwd());
|
|
30645
|
+
if (root) return start;
|
|
30646
|
+
let current = start;
|
|
30647
|
+
for (; ; ) {
|
|
30648
|
+
if (existsSync2(join5(current, "testpilot.yaml")) || existsSync2(join5(current, TESTPILOT_DIR))) {
|
|
30649
|
+
return current;
|
|
30650
|
+
}
|
|
30651
|
+
const parent = dirname(current);
|
|
30652
|
+
if (parent === current) return start;
|
|
30653
|
+
current = parent;
|
|
30654
|
+
}
|
|
30594
30655
|
}
|
|
30595
30656
|
async function loadProjectConfig(root) {
|
|
30596
30657
|
return loadTestpilotConfig(resolveRoot(root));
|
|
@@ -30639,6 +30700,164 @@ async function writeProjectLink(link, root) {
|
|
|
30639
30700
|
return file2;
|
|
30640
30701
|
}
|
|
30641
30702
|
|
|
30703
|
+
// ../../packages/sdk/src/case-data.ts
|
|
30704
|
+
var import_yaml3 = __toESM(require_dist(), 1);
|
|
30705
|
+
import { readFile as readFile3, realpath } from "node:fs/promises";
|
|
30706
|
+
import { extname, isAbsolute, relative, resolve as resolve2 } from "node:path";
|
|
30707
|
+
async function loadCaseDataFile(reference, options) {
|
|
30708
|
+
if (isAbsolute(reference)) {
|
|
30709
|
+
throw new Error(`\u6570\u636E\u6587\u4EF6\u5FC5\u987B\u4F7F\u7528\u9879\u76EE\u76F8\u5BF9\u8DEF\u5F84:${reference}`);
|
|
30710
|
+
}
|
|
30711
|
+
const root = resolve2(options.root);
|
|
30712
|
+
const dataDir = resolve2(options.dataDir);
|
|
30713
|
+
const target = resolve2(root, reference);
|
|
30714
|
+
if (!isInside(dataDir, target)) {
|
|
30715
|
+
throw new Error(`\u6570\u636E\u6587\u4EF6\u5FC5\u987B\u4F4D\u4E8E tests/e2e/data \u76EE\u5F55:${reference}`);
|
|
30716
|
+
}
|
|
30717
|
+
const [realDataDir, realTarget] = await Promise.all([
|
|
30718
|
+
realpath(dataDir).catch(() => dataDir),
|
|
30719
|
+
realpath(target).catch((error62) => {
|
|
30720
|
+
throw new Error(
|
|
30721
|
+
`\u8BFB\u53D6\u6570\u636E\u6587\u4EF6\u5931\u8D25:${reference}:${error62 instanceof Error ? error62.message : String(error62)}`
|
|
30722
|
+
);
|
|
30723
|
+
})
|
|
30724
|
+
]);
|
|
30725
|
+
if (!isInside(realDataDir, realTarget)) {
|
|
30726
|
+
throw new Error(`\u6570\u636E\u6587\u4EF6\u89E3\u6790\u540E\u8D8A\u51FA tests/e2e/data \u76EE\u5F55:${reference}`);
|
|
30727
|
+
}
|
|
30728
|
+
const source = await readFile3(realTarget, "utf8");
|
|
30729
|
+
const extension = extname(realTarget).toLowerCase();
|
|
30730
|
+
try {
|
|
30731
|
+
if (extension === ".yaml" || extension === ".yml") return (0, import_yaml3.parse)(source);
|
|
30732
|
+
if (extension === ".json") return JSON.parse(source);
|
|
30733
|
+
} catch (error62) {
|
|
30734
|
+
throw new Error(
|
|
30735
|
+
`\u89E3\u6790\u6570\u636E\u6587\u4EF6\u5931\u8D25:${reference}:${error62 instanceof Error ? error62.message : String(error62)}`
|
|
30736
|
+
);
|
|
30737
|
+
}
|
|
30738
|
+
throw new Error(`\u6570\u636E\u6587\u4EF6\u4EC5\u652F\u6301 .yaml/.yml/.json:${reference}`);
|
|
30739
|
+
}
|
|
30740
|
+
function isDataObject(value) {
|
|
30741
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
30742
|
+
}
|
|
30743
|
+
function mergeFixture(target, source, at = "fixture") {
|
|
30744
|
+
const output2 = { ...target };
|
|
30745
|
+
for (const [key, value] of Object.entries(source)) {
|
|
30746
|
+
const path = `${at}.${key}`;
|
|
30747
|
+
if (!(key in output2)) {
|
|
30748
|
+
output2[key] = value;
|
|
30749
|
+
continue;
|
|
30750
|
+
}
|
|
30751
|
+
const current = output2[key];
|
|
30752
|
+
if (isDataObject(current) && isDataObject(value)) {
|
|
30753
|
+
output2[key] = mergeFixture(current, value, path);
|
|
30754
|
+
continue;
|
|
30755
|
+
}
|
|
30756
|
+
throw new Error(`fixture \u5B57\u6BB5\u51B2\u7A81:${path}`);
|
|
30757
|
+
}
|
|
30758
|
+
return output2;
|
|
30759
|
+
}
|
|
30760
|
+
function flattenVariables(prefix, value) {
|
|
30761
|
+
const output2 = {};
|
|
30762
|
+
visit2(prefix, value, output2);
|
|
30763
|
+
return output2;
|
|
30764
|
+
}
|
|
30765
|
+
function visit2(path, value, output2) {
|
|
30766
|
+
if (value === null || value === void 0) return;
|
|
30767
|
+
if (typeof value === "object") {
|
|
30768
|
+
output2[path] = JSON.stringify(value);
|
|
30769
|
+
if (Array.isArray(value)) {
|
|
30770
|
+
value.forEach((item, index) => visit2(`${path}.${index}`, item, output2));
|
|
30771
|
+
} else {
|
|
30772
|
+
for (const [key, item] of Object.entries(value)) {
|
|
30773
|
+
visit2(`${path}.${key}`, item, output2);
|
|
30774
|
+
}
|
|
30775
|
+
}
|
|
30776
|
+
return;
|
|
30777
|
+
}
|
|
30778
|
+
output2[path] = String(value);
|
|
30779
|
+
}
|
|
30780
|
+
function isInside(base, candidate) {
|
|
30781
|
+
const path = relative(base, candidate);
|
|
30782
|
+
return path === "" || !path.startsWith("..") && !isAbsolute(path);
|
|
30783
|
+
}
|
|
30784
|
+
|
|
30785
|
+
// ../../packages/sdk/src/case-preparation.ts
|
|
30786
|
+
async function prepareCaseExecutions(item, options) {
|
|
30787
|
+
const fixture = await loadFixtures(item.data.fixtures ?? [], options);
|
|
30788
|
+
const rows = await loadDataset(item.data, options);
|
|
30789
|
+
const projectVariables = flattenVariables("variable", options.variables ?? {});
|
|
30790
|
+
const accountVariables = accountValues(item.data, options.accounts);
|
|
30791
|
+
return rows.map(({ row, rowId }, rowIndex) => {
|
|
30792
|
+
const initialVariables = {
|
|
30793
|
+
...flattenVariables("fixture", fixture),
|
|
30794
|
+
...flattenVariables("dataset", row),
|
|
30795
|
+
...projectVariables,
|
|
30796
|
+
...accountVariables
|
|
30797
|
+
};
|
|
30798
|
+
const missingDependencies = (item.data.requires ?? []).filter(
|
|
30799
|
+
(path) => isUnavailable(initialVariables[path])
|
|
30800
|
+
);
|
|
30801
|
+
return {
|
|
30802
|
+
data: item.data,
|
|
30803
|
+
file: item.file,
|
|
30804
|
+
templateId: item.data.id,
|
|
30805
|
+
rowId,
|
|
30806
|
+
rowIndex,
|
|
30807
|
+
initialVariables,
|
|
30808
|
+
missingDependencies
|
|
30809
|
+
};
|
|
30810
|
+
});
|
|
30811
|
+
}
|
|
30812
|
+
async function loadFixtures(references, options) {
|
|
30813
|
+
let merged = {};
|
|
30814
|
+
for (const reference of references) {
|
|
30815
|
+
const value = await loadCaseDataFile(reference, options);
|
|
30816
|
+
if (!isDataObject(value)) {
|
|
30817
|
+
throw new Error(`fixture \u6587\u4EF6\u6839\u5FC5\u987B\u662F\u5BF9\u8C61:${reference}`);
|
|
30818
|
+
}
|
|
30819
|
+
merged = mergeFixture(merged, value);
|
|
30820
|
+
}
|
|
30821
|
+
return merged;
|
|
30822
|
+
}
|
|
30823
|
+
async function loadDataset(data, options) {
|
|
30824
|
+
if (!data.datasets) return [{ row: {}, rowId: "default" }];
|
|
30825
|
+
const value = await loadCaseDataFile(data.datasets.file, options);
|
|
30826
|
+
if (!Array.isArray(value)) {
|
|
30827
|
+
throw new Error(`dataset \u6587\u4EF6\u6839\u5FC5\u987B\u662F\u5BF9\u8C61\u6570\u7EC4:${data.datasets.file}`);
|
|
30828
|
+
}
|
|
30829
|
+
const seen = /* @__PURE__ */ new Set();
|
|
30830
|
+
return value.map((row, index) => {
|
|
30831
|
+
if (!isDataObject(row)) {
|
|
30832
|
+
throw new Error(`dataset \u7B2C ${index + 1} \u884C\u5FC5\u987B\u662F\u5BF9\u8C61:${data.datasets.file}`);
|
|
30833
|
+
}
|
|
30834
|
+
const rawId = row[data.datasets.idField];
|
|
30835
|
+
if (typeof rawId !== "string" && typeof rawId !== "number" || String(rawId).trim() === "") {
|
|
30836
|
+
throw new Error(
|
|
30837
|
+
`dataset \u7B2C ${index + 1} \u884C\u7F3A\u5C11\u975E\u7A7A row id \u5B57\u6BB5 "${data.datasets.idField}"`
|
|
30838
|
+
);
|
|
30839
|
+
}
|
|
30840
|
+
const rowId = String(rawId);
|
|
30841
|
+
if (seen.has(rowId)) throw new Error(`dataset row id \u91CD\u590D:${rowId}`);
|
|
30842
|
+
seen.add(rowId);
|
|
30843
|
+
return { row, rowId };
|
|
30844
|
+
});
|
|
30845
|
+
}
|
|
30846
|
+
function accountValues(data, accounts) {
|
|
30847
|
+
if (!data.accountRef) return {};
|
|
30848
|
+
const raw = accounts?.[data.accountRef];
|
|
30849
|
+
if (raw === void 0) return {};
|
|
30850
|
+
try {
|
|
30851
|
+
const parsed = JSON.parse(raw);
|
|
30852
|
+
if (isDataObject(parsed)) return flattenVariables("account", parsed);
|
|
30853
|
+
} catch {
|
|
30854
|
+
}
|
|
30855
|
+
return { account: raw };
|
|
30856
|
+
}
|
|
30857
|
+
function isUnavailable(value) {
|
|
30858
|
+
return value === void 0 || value.trim() === "" || /\$\{[A-Za-z_][A-Za-z0-9_]*\}/.test(value);
|
|
30859
|
+
}
|
|
30860
|
+
|
|
30642
30861
|
// ../../packages/sdk/src/cases.ts
|
|
30643
30862
|
async function collectCaseFiles(paths) {
|
|
30644
30863
|
const files = [];
|
|
@@ -30667,19 +30886,20 @@ async function collectFromDir(dir) {
|
|
|
30667
30886
|
return found;
|
|
30668
30887
|
}
|
|
30669
30888
|
function isCaseFile(file2) {
|
|
30670
|
-
const ext =
|
|
30889
|
+
const ext = extname2(file2).toLowerCase();
|
|
30671
30890
|
return ext === ".yaml" || ext === ".yml";
|
|
30672
30891
|
}
|
|
30673
30892
|
function resolveAgainstRoot(root, file2) {
|
|
30674
|
-
return
|
|
30893
|
+
return isAbsolute2(file2) ? file2 : resolve3(resolveRoot(root), file2);
|
|
30675
30894
|
}
|
|
30676
30895
|
async function collectCases(paths, options = {}) {
|
|
30677
30896
|
const files = await collectCaseFiles(
|
|
30678
30897
|
paths.map((input2) => resolveAgainstRoot(options.root, input2))
|
|
30679
30898
|
);
|
|
30680
30899
|
const infos = [];
|
|
30900
|
+
const root = resolveRoot(options.root);
|
|
30681
30901
|
for (const file2 of files) {
|
|
30682
|
-
const validation = await validateCaseFile(file2);
|
|
30902
|
+
const validation = await validateCaseFile(file2, root);
|
|
30683
30903
|
infos.push({
|
|
30684
30904
|
file: file2,
|
|
30685
30905
|
valid: validation.ok,
|
|
@@ -30691,16 +30911,16 @@ async function collectCases(paths, options = {}) {
|
|
|
30691
30911
|
}
|
|
30692
30912
|
async function readCaseFile(file2, options = {}) {
|
|
30693
30913
|
const target = resolveAgainstRoot(options.root, file2);
|
|
30694
|
-
const source = await
|
|
30695
|
-
return { file: target, source, validation:
|
|
30914
|
+
const source = await readFile4(target, "utf8");
|
|
30915
|
+
return { file: target, source, validation: await validateCaseDataSource(source, target, resolveRoot(options.root)) };
|
|
30696
30916
|
}
|
|
30697
|
-
async function validateCaseFile(file2) {
|
|
30698
|
-
const source = await
|
|
30699
|
-
return
|
|
30917
|
+
async function validateCaseFile(file2, root) {
|
|
30918
|
+
const source = await readFile4(file2, "utf8");
|
|
30919
|
+
return validateCaseDataSource(source, file2, root);
|
|
30700
30920
|
}
|
|
30701
30921
|
async function writeCaseFile(file2, source, options = {}) {
|
|
30702
30922
|
const target = resolveAgainstRoot(options.root, file2);
|
|
30703
|
-
const validation =
|
|
30923
|
+
const validation = await validateCaseDataSource(source, target, resolveRoot(options.root));
|
|
30704
30924
|
if (!validation.ok) {
|
|
30705
30925
|
return { file: target, written: false, validation };
|
|
30706
30926
|
}
|
|
@@ -30710,18 +30930,91 @@ async function writeCaseFile(file2, source, options = {}) {
|
|
|
30710
30930
|
throw new Error(`\u7528\u4F8B\u6587\u4EF6\u5DF2\u5B58\u5728:${target}(\u8986\u76D6\u9700\u663E\u5F0F overwrite: true)`);
|
|
30711
30931
|
}
|
|
30712
30932
|
}
|
|
30713
|
-
await mkdir3(
|
|
30933
|
+
await mkdir3(dirname2(target), { recursive: true });
|
|
30714
30934
|
await writeFile3(target, source, "utf8");
|
|
30715
30935
|
return { file: target, written: true, validation };
|
|
30716
30936
|
}
|
|
30937
|
+
async function validateCaseDataSource(source, file2, root) {
|
|
30938
|
+
const validation = validateCaseSource(source);
|
|
30939
|
+
if (!validation.ok || !validation.data) return validation;
|
|
30940
|
+
try {
|
|
30941
|
+
await prepareCaseExecutions(
|
|
30942
|
+
{ data: validation.data, file: file2 },
|
|
30943
|
+
{ root, dataDir: join6(root, "tests", "e2e", "data") }
|
|
30944
|
+
);
|
|
30945
|
+
return validation;
|
|
30946
|
+
} catch (error62) {
|
|
30947
|
+
return {
|
|
30948
|
+
ok: false,
|
|
30949
|
+
data: validation.data,
|
|
30950
|
+
issues: [
|
|
30951
|
+
{
|
|
30952
|
+
path: "(root)",
|
|
30953
|
+
code: "semantic",
|
|
30954
|
+
message: error62 instanceof Error ? error62.message : String(error62)
|
|
30955
|
+
}
|
|
30956
|
+
]
|
|
30957
|
+
};
|
|
30958
|
+
}
|
|
30959
|
+
}
|
|
30717
30960
|
|
|
30718
30961
|
// ../../packages/sdk/src/reports.ts
|
|
30719
|
-
import { readFile as
|
|
30720
|
-
import { join as
|
|
30962
|
+
import { readFile as readFile8 } from "node:fs/promises";
|
|
30963
|
+
import { join as join18 } from "node:path";
|
|
30721
30964
|
|
|
30722
30965
|
// ../../packages/reporter/src/report.ts
|
|
30723
30966
|
import { writeFile as writeFile4 } from "node:fs/promises";
|
|
30724
|
-
import { join as
|
|
30967
|
+
import { dirname as dirname3, join as join8, resolve as resolve4 } from "node:path";
|
|
30968
|
+
|
|
30969
|
+
// ../../packages/reporter/src/classify.ts
|
|
30970
|
+
var FAILURE_CATEGORIES = [
|
|
30971
|
+
{ id: "assert", label: "\u65AD\u8A00\u5931\u8D25" },
|
|
30972
|
+
{ id: "element", label: "\u5143\u7D20\u672A\u627E\u5230" },
|
|
30973
|
+
{ id: "adapter", label: "Adapter \u95EE\u9898" },
|
|
30974
|
+
{ id: "env", label: "\u73AF\u5883 / \u8FDE\u63A5" },
|
|
30975
|
+
{ id: "timeout", label: "\u8D85\u65F6" },
|
|
30976
|
+
{ id: "other", label: "\u5176\u4ED6" }
|
|
30977
|
+
];
|
|
30978
|
+
function categoryLabel(id) {
|
|
30979
|
+
return FAILURE_CATEGORIES.find((item) => item.id === id)?.label ?? id;
|
|
30980
|
+
}
|
|
30981
|
+
var RULES = [
|
|
30982
|
+
{ id: "assert", pattern: /断言失败|AssertionError|expect\(.+\)\.to/i },
|
|
30983
|
+
{ id: "element", pattern: /未找到元素|等待元素超时|waiting for locator|waiting for selector|element not found/i },
|
|
30984
|
+
{ id: "adapter", pattern: /未注册 target|ADAPTER_NOT_FOUND|无法定位 TestPilot Skill/i },
|
|
30985
|
+
{ id: "env", pattern: /net::ERR|ECONNREFUSED|Connection closed|Failed to launch|服务端口|不可达|ENOTFOUND|EACCES/i },
|
|
30986
|
+
{ id: "timeout", pattern: /timeout|超时/i }
|
|
30987
|
+
];
|
|
30988
|
+
function classifyFailure(message) {
|
|
30989
|
+
if (!message) return "other";
|
|
30990
|
+
for (const rule of RULES) {
|
|
30991
|
+
if (rule.pattern.test(message)) return rule.id;
|
|
30992
|
+
}
|
|
30993
|
+
return "other";
|
|
30994
|
+
}
|
|
30995
|
+
|
|
30996
|
+
// ../../packages/reporter/src/history.ts
|
|
30997
|
+
import { readFile as readFile5, readdir as readdir2 } from "node:fs/promises";
|
|
30998
|
+
import { join as join7 } from "node:path";
|
|
30999
|
+
async function collectHistory(runsRoot2, skipRunId, limit = 12) {
|
|
31000
|
+
const entries = (await readdir2(runsRoot2).catch(() => [])).filter((name) => /^\d{8}-\d{3}$/.test(name) && name !== skipRunId).sort().slice(-limit);
|
|
31001
|
+
const points = [];
|
|
31002
|
+
for (const runId of entries) {
|
|
31003
|
+
try {
|
|
31004
|
+
const summary = JSON.parse(await readFile5(join7(runsRoot2, runId, "result.json"), "utf8"));
|
|
31005
|
+
points.push({
|
|
31006
|
+
runId,
|
|
31007
|
+
startedAt: summary.startedAt,
|
|
31008
|
+
status: summary.status ?? "unknown",
|
|
31009
|
+
passed: summary.totals?.passed ?? 0,
|
|
31010
|
+
failed: summary.totals?.failed ?? 0,
|
|
31011
|
+
durationMs: summary.durationMs
|
|
31012
|
+
});
|
|
31013
|
+
} catch {
|
|
31014
|
+
}
|
|
31015
|
+
}
|
|
31016
|
+
return points;
|
|
31017
|
+
}
|
|
30725
31018
|
|
|
30726
31019
|
// ../../packages/reporter/src/summary.ts
|
|
30727
31020
|
function toSummaryView(summary) {
|
|
@@ -30729,81 +31022,286 @@ function toSummaryView(summary) {
|
|
|
30729
31022
|
runId: summary.runId,
|
|
30730
31023
|
status: summary.status,
|
|
30731
31024
|
durationMs: summary.durationMs,
|
|
30732
|
-
cases: {
|
|
31025
|
+
cases: {
|
|
31026
|
+
templates: summary.totals.templates ?? summary.totals.cases,
|
|
31027
|
+
passed: summary.totals.passed,
|
|
31028
|
+
failed: summary.totals.failed,
|
|
31029
|
+
skipped: summary.totals.skipped ?? 0,
|
|
31030
|
+
warnings: summary.totals.warnings ?? 0
|
|
31031
|
+
},
|
|
30733
31032
|
steps: {
|
|
30734
31033
|
total: summary.totals.steps,
|
|
30735
31034
|
passed: summary.totals.stepsPassed,
|
|
30736
31035
|
failed: summary.totals.stepsFailed,
|
|
30737
|
-
skipped: summary.totals.stepsSkipped
|
|
31036
|
+
skipped: summary.totals.stepsSkipped,
|
|
31037
|
+
warning: summary.totals.stepsWarning ?? 0
|
|
30738
31038
|
}
|
|
30739
31039
|
};
|
|
30740
31040
|
}
|
|
30741
31041
|
|
|
30742
31042
|
// ../../packages/reporter/src/report.ts
|
|
30743
|
-
|
|
30744
|
-
|
|
30745
|
-
const
|
|
30746
|
-
const
|
|
30747
|
-
const
|
|
30748
|
-
|
|
30749
|
-
|
|
31043
|
+
var HISTORY_LIMIT = 12;
|
|
31044
|
+
async function writeReports(runDir, summary, options = {}) {
|
|
31045
|
+
const dir = resolve4(runDir);
|
|
31046
|
+
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
31047
|
+
const history = options.history ?? await collectHistory(dirname3(dir), summary.runId, options.historyLimit ?? HISTORY_LIMIT);
|
|
31048
|
+
const failures = [];
|
|
31049
|
+
const cases = summary.cases.map((item) => {
|
|
31050
|
+
const categories = /* @__PURE__ */ new Set();
|
|
31051
|
+
const steps = item.steps.map((step) => {
|
|
31052
|
+
const category = step.status === "failed" ? classifyFailure(step.error) : void 0;
|
|
31053
|
+
if (category) {
|
|
31054
|
+
categories.add(category);
|
|
31055
|
+
failures.push({
|
|
31056
|
+
category,
|
|
31057
|
+
label: categoryLabel(category),
|
|
31058
|
+
caseId: item.caseId,
|
|
31059
|
+
index: step.index,
|
|
31060
|
+
message: step.error ?? ""
|
|
31061
|
+
});
|
|
31062
|
+
}
|
|
31063
|
+
return { ...step, category };
|
|
31064
|
+
});
|
|
31065
|
+
return { ...item, steps, categories: [...categories] };
|
|
31066
|
+
});
|
|
31067
|
+
const categoryLabels = {};
|
|
31068
|
+
for (const item of FAILURE_CATEGORIES) categoryLabels[item.id] = item.label;
|
|
31069
|
+
const data = {
|
|
31070
|
+
generatedAt,
|
|
31071
|
+
summary: { ...summary, cases },
|
|
31072
|
+
history,
|
|
31073
|
+
failures,
|
|
31074
|
+
view: toSummaryView(summary),
|
|
31075
|
+
categoryLabels
|
|
31076
|
+
};
|
|
31077
|
+
const jsonPath = join8(dir, "report.json");
|
|
31078
|
+
const htmlPath = join8(dir, "report.html");
|
|
31079
|
+
await writeFile4(jsonPath, JSON.stringify(data, null, 2), "utf8");
|
|
31080
|
+
await writeFile4(htmlPath, renderHtml(data), "utf8");
|
|
30750
31081
|
return { json: jsonPath, html: htmlPath };
|
|
30751
31082
|
}
|
|
30752
|
-
function
|
|
30753
|
-
const
|
|
30754
|
-
const
|
|
31083
|
+
function categoryChips(data) {
|
|
31084
|
+
const counts = /* @__PURE__ */ new Map();
|
|
31085
|
+
for (const failure2 of data.failures) {
|
|
31086
|
+
counts.set(failure2.category, (counts.get(failure2.category) ?? 0) + 1);
|
|
31087
|
+
}
|
|
31088
|
+
const chips = [
|
|
31089
|
+
'<span class="chip active" data-group="status" data-value="all">\u5168\u90E8\u72B6\u6001</span>',
|
|
31090
|
+
'<span class="chip" data-group="status" data-value="failed">\u4EC5\u5931\u8D25</span>'
|
|
31091
|
+
];
|
|
31092
|
+
for (const item of FAILURE_CATEGORIES) {
|
|
31093
|
+
const count = counts.get(item.id) ?? 0;
|
|
31094
|
+
if (count > 0) {
|
|
31095
|
+
chips.push(
|
|
31096
|
+
`<span class="chip" data-group="cat" data-value="${item.id}">${escapeHtml(item.label)}(${count})</span>`
|
|
31097
|
+
);
|
|
31098
|
+
}
|
|
31099
|
+
}
|
|
31100
|
+
return chips.join("\n");
|
|
31101
|
+
}
|
|
31102
|
+
function renderHtml(data) {
|
|
31103
|
+
const view = toSummaryView(data.summary);
|
|
31104
|
+
const summary = data.summary;
|
|
31105
|
+
const trendSvg = renderTrend(data);
|
|
31106
|
+
const casesHtml = groupedCasesHtml(summary.cases);
|
|
31107
|
+
const chips = categoryChips(data);
|
|
30755
31108
|
return `<!doctype html>
|
|
30756
31109
|
<html lang="zh-CN">
|
|
30757
31110
|
<head>
|
|
30758
31111
|
<meta charset="utf-8" />
|
|
30759
31112
|
<title>TestPilot Report ${escapeHtml(summary.runId)}</title>
|
|
30760
31113
|
<style>
|
|
30761
|
-
|
|
30762
|
-
|
|
30763
|
-
|
|
30764
|
-
.
|
|
30765
|
-
|
|
30766
|
-
|
|
30767
|
-
|
|
30768
|
-
|
|
30769
|
-
|
|
30770
|
-
|
|
31114
|
+
:root { --ok:#1a7f37; --fail:#cf222e; --warn:#9a6700; --skip:#6e7781; --line:#d0d7de; --muted:#59636e; }
|
|
31115
|
+
* { box-sizing: border-box; }
|
|
31116
|
+
body { font-family: system-ui, "Segoe UI", sans-serif; margin: 0; color: #1f2328; background: #f6f8fa; }
|
|
31117
|
+
.wrap { max-width: 1080px; margin: 0 auto; padding: 20px 16px 48px; }
|
|
31118
|
+
h1 { font-size: 20px; margin: 8px 0; } h2 { font-size: 15px; margin: 24px 0 8px; }
|
|
31119
|
+
.badge { display: inline-block; padding: 2px 10px; border-radius: 10px; color: #fff; font-size: 12px; }
|
|
31120
|
+
.passed { background: var(--ok); } .failed { background: var(--fail); } .warning { background: var(--warn); } .skipped { background: var(--skip); }
|
|
31121
|
+
code { background: #eef1f4; padding: 1px 5px; border-radius: 4px; font-size: 12px; }
|
|
31122
|
+
.cards { display: flex; gap: 12px; flex-wrap: wrap; margin: 14px 0; }
|
|
31123
|
+
.card { background: #fff; border: 1px solid var(--line); border-radius: 8px; padding: 10px 16px; min-width: 130px; }
|
|
31124
|
+
.card b { font-size: 20px; display: block; }
|
|
31125
|
+
.card span { color: var(--muted); font-size: 12px; }
|
|
31126
|
+
.toolbar { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin: 10px 0; }
|
|
31127
|
+
.chip { border: 1px solid var(--line); background: #fff; border-radius: 14px; padding: 3px 12px; font-size: 12px; cursor: pointer; }
|
|
31128
|
+
.chip.active { background: #0969da; color: #fff; border-color: #0969da; }
|
|
31129
|
+
input#q { flex: 1; min-width: 180px; padding: 5px 10px; border: 1px solid var(--line); border-radius: 6px; font-size: 13px; }
|
|
31130
|
+
.btn { border: 1px solid var(--line); background: #fff; border-radius: 6px; padding: 4px 10px; font-size: 12px; cursor: pointer; }
|
|
31131
|
+
#count { color: var(--muted); font-size: 12px; margin-left: auto; }
|
|
31132
|
+
details.case { background: #fff; border: 1px solid var(--line); border-radius: 8px; margin: 10px 0; }
|
|
31133
|
+
section.template { border-left: 3px solid var(--line); padding-left: 12px; margin: 14px 0; }
|
|
31134
|
+
section.template > h3 { font-size: 14px; margin: 0 0 6px; }
|
|
31135
|
+
details.case > summary { cursor: pointer; padding: 10px 14px; font-size: 14px; list-style: none; }
|
|
31136
|
+
details.case > summary::-webkit-details-marker { display: none; }
|
|
31137
|
+
details.case > summary:hover { background: #f6f8fa; }
|
|
31138
|
+
table { border-collapse: collapse; width: 100%; font-size: 13px; }
|
|
31139
|
+
th, td { border-top: 1px solid var(--line); padding: 6px 10px; text-align: left; vertical-align: top; }
|
|
31140
|
+
td.num { text-align: right; white-space: nowrap; }
|
|
31141
|
+
.error { color: var(--fail); white-space: pre-wrap; }
|
|
31142
|
+
a { color: #0969da; text-decoration: none; }
|
|
31143
|
+
.trend { background: #fff; border: 1px solid var(--line); border-radius: 8px; padding: 12px; overflow-x: auto; }
|
|
31144
|
+
.muted { color: var(--muted); }
|
|
31145
|
+
.hide { display: none !important; }
|
|
31146
|
+
.meta { color: var(--muted); font-size: 12px; margin: 6px 0; }
|
|
30771
31147
|
</style>
|
|
30772
31148
|
</head>
|
|
30773
31149
|
<body>
|
|
30774
|
-
<
|
|
30775
|
-
<
|
|
30776
|
-
${
|
|
31150
|
+
<div class="wrap">
|
|
31151
|
+
<h1>TestPilot \u6D4B\u8BD5\u62A5\u544A <span class="badge ${summary.status}">${summary.status}</span></h1>
|
|
31152
|
+
<p class="meta">run <code>${escapeHtml(summary.runId)}</code> \xB7 \u751F\u6210\u4E8E ${escapeHtml(data.generatedAt)} \xB7 \u8017\u65F6 ${(view.durationMs / 1e3).toFixed(1)}s</p>
|
|
31153
|
+
|
|
31154
|
+
<div class="cards">
|
|
31155
|
+
<div class="card"><b>${view.cases.passed}<span style="color:var(--ok)"> \u2713</span></b><span>\u901A\u8FC7\u7528\u4F8B</span></div>
|
|
31156
|
+
<div class="card"><b>${view.cases.failed}<span style="color:var(--fail)"> \u2717</span></b><span>\u5931\u8D25\u7528\u4F8B</span></div>
|
|
31157
|
+
<div class="card"><b>${view.cases.skipped} \u21B7</b><span>\u8DF3\u8FC7\u6570\u636E\u884C</span></div>
|
|
31158
|
+
<div class="card"><b>${view.cases.warnings} \u26A0</b><span>\u6E05\u7406\u8B66\u544A</span></div>
|
|
31159
|
+
<div class="card"><b>${view.steps.passed}<span style="color:var(--ok)"> \u2713</span></b><span>\u901A\u8FC7\u6B65\u9AA4</span></div>
|
|
31160
|
+
<div class="card"><b>${view.steps.failed}<span style="color:var(--fail)"> \u2717</span></b><span>\u5931\u8D25\u6B65\u9AA4</span></div>
|
|
31161
|
+
<div class="card"><b>${view.steps.skipped} \u21B7</b><span>\u8DF3\u8FC7\u6B65\u9AA4</span></div>
|
|
31162
|
+
</div>
|
|
31163
|
+
|
|
31164
|
+
<h2>\u5386\u53F2\u8D8B\u52BF(\u6700\u8FD1 ${data.history.length + 1} \u6B21\u8FD0\u884C)</h2>
|
|
31165
|
+
<div class="trend">${trendSvg}</div>
|
|
31166
|
+
|
|
31167
|
+
<h2>\u5931\u8D25\u5206\u7C7B</h2>
|
|
31168
|
+
<div class="toolbar" id="cat-chips">${chips}</div>
|
|
31169
|
+
|
|
31170
|
+
<h2>\u7528\u4F8B</h2>
|
|
31171
|
+
<div class="toolbar">
|
|
31172
|
+
<input id="q" placeholder="\u641C\u7D22\u7528\u4F8B id / \u540D\u79F0 / \u6587\u4EF6\u2026" />
|
|
31173
|
+
<button class="btn" id="expand">\u5C55\u5F00\u5168\u90E8</button>
|
|
31174
|
+
<button class="btn" id="collapse">\u6536\u8D77\u5168\u90E8</button>
|
|
31175
|
+
<span id="count"></span>
|
|
31176
|
+
</div>
|
|
31177
|
+
<div id="cases">${casesHtml}</div>
|
|
31178
|
+
</div>
|
|
31179
|
+
|
|
31180
|
+
<script id="report-data" type="application/json">${embedJson(data)}</script>
|
|
31181
|
+
<script>
|
|
31182
|
+
(function () {
|
|
31183
|
+
var data = JSON.parse(document.getElementById('report-data').textContent);
|
|
31184
|
+
var state = { status: 'all', cat: 'all', q: '' };
|
|
31185
|
+
var labels = data.categoryLabels || {};
|
|
31186
|
+
|
|
31187
|
+
function setChip(group, value) {
|
|
31188
|
+
document.querySelectorAll('.chip[data-group="' + group + '"]').forEach(function (chip) {
|
|
31189
|
+
chip.classList.toggle('active', chip.getAttribute('data-value') === value);
|
|
31190
|
+
});
|
|
31191
|
+
}
|
|
31192
|
+
|
|
31193
|
+
function matches(card) {
|
|
31194
|
+
if (state.status !== 'all' && card.getAttribute('data-status') !== state.status) return false;
|
|
31195
|
+
if (state.cat !== 'all' && (',' + (card.getAttribute('data-cats') || '') + ',').indexOf(',' + state.cat + ',') === -1) return false;
|
|
31196
|
+
if (state.q) {
|
|
31197
|
+
var text = (card.getAttribute('data-search') || '').toLowerCase();
|
|
31198
|
+
if (text.indexOf(state.q.toLowerCase()) === -1) return false;
|
|
31199
|
+
}
|
|
31200
|
+
return true;
|
|
31201
|
+
}
|
|
31202
|
+
|
|
31203
|
+
function apply() {
|
|
31204
|
+
var cards = document.querySelectorAll('details.case');
|
|
31205
|
+
var shown = 0;
|
|
31206
|
+
cards.forEach(function (card) {
|
|
31207
|
+
var show = matches(card);
|
|
31208
|
+
card.classList.toggle('hide', !show);
|
|
31209
|
+
if (show) shown++;
|
|
31210
|
+
});
|
|
31211
|
+
var count = document.getElementById('count');
|
|
31212
|
+
if (count) count.textContent = shown + ' / ' + cards.length + ' \u7528\u4F8B';
|
|
31213
|
+
}
|
|
31214
|
+
|
|
31215
|
+
document.querySelectorAll('.chip').forEach(function (chip) {
|
|
31216
|
+
chip.addEventListener('click', function () {
|
|
31217
|
+
var group = chip.getAttribute('data-group');
|
|
31218
|
+
var value = chip.getAttribute('data-value');
|
|
31219
|
+
if (group === 'status') state.status = value;
|
|
31220
|
+
if (group === 'cat') state.cat = value;
|
|
31221
|
+
setChip(group, value);
|
|
31222
|
+
apply();
|
|
31223
|
+
});
|
|
31224
|
+
});
|
|
31225
|
+
var q = document.getElementById('q');
|
|
31226
|
+
if (q) q.addEventListener('input', function () { state.q = q.value.trim(); apply(); });
|
|
31227
|
+
var expand = document.getElementById('expand');
|
|
31228
|
+
if (expand) expand.addEventListener('click', function () {
|
|
31229
|
+
document.querySelectorAll('details.case').forEach(function (c) { c.open = true; });
|
|
31230
|
+
});
|
|
31231
|
+
var collapse = document.getElementById('collapse');
|
|
31232
|
+
if (collapse) collapse.addEventListener('click', function () {
|
|
31233
|
+
document.querySelectorAll('details.case').forEach(function (c) { c.open = false; });
|
|
31234
|
+
});
|
|
31235
|
+
|
|
31236
|
+
function failedSteps(card) {
|
|
31237
|
+
return data.summary.cases.find(function (c) { return c.caseId === card; });
|
|
31238
|
+
}
|
|
31239
|
+
apply();
|
|
31240
|
+
})();
|
|
31241
|
+
</script>
|
|
30777
31242
|
</body>
|
|
30778
31243
|
</html>`;
|
|
30779
31244
|
}
|
|
31245
|
+
function groupedCasesHtml(cases) {
|
|
31246
|
+
const groups = /* @__PURE__ */ new Map();
|
|
31247
|
+
for (const item of cases) {
|
|
31248
|
+
const current = groups.get(item.caseId) ?? [];
|
|
31249
|
+
current.push(item);
|
|
31250
|
+
groups.set(item.caseId, current);
|
|
31251
|
+
}
|
|
31252
|
+
return [...groups.entries()].map(
|
|
31253
|
+
([caseId, rows]) => `<section class="template">
|
|
31254
|
+
<h3>${escapeHtml(caseId)} <span class="muted">\xB7 ${rows.length} \u4E2A\u6570\u636E\u884C</span></h3>
|
|
31255
|
+
${rows.map((item) => caseHtml(item)).join("\n")}
|
|
31256
|
+
</section>`
|
|
31257
|
+
).join("\n");
|
|
31258
|
+
}
|
|
30780
31259
|
function caseHtml(item) {
|
|
30781
31260
|
const rows = item.steps.map((step) => stepHtml(step)).join("\n");
|
|
30782
31261
|
const errorLine = item.error ? `<p class="error">${escapeHtml(item.error)}</p>` : "";
|
|
31262
|
+
const skipLine = item.skipReason ? `<p class="muted">${escapeHtml(item.skipReason)}${item.missingDependencies?.length ? `: ${escapeHtml(item.missingDependencies.join(", "))}` : ""}</p>` : "";
|
|
31263
|
+
const warningLine = item.warnings?.length ? `<p class="error">\u26A0 ${item.warnings.map(escapeHtml).join("<br />\u26A0 ")}</p>` : "";
|
|
30783
31264
|
const links = [];
|
|
30784
31265
|
if (item.video) links.push(`<a href="${escapeHtml(item.video)}">\u89C6\u9891</a>`);
|
|
30785
31266
|
if (item.trace) links.push(`<a href="${escapeHtml(item.trace)}">Trace</a>`);
|
|
30786
31267
|
const linksLine = links.length > 0 ? `<p>${links.join(" \xB7 ")}</p>` : "";
|
|
31268
|
+
const search = escapeHtml(`${item.caseId} ${item.rowId ?? "default"} ${item.caseName} ${item.file}`).replaceAll('"', """);
|
|
31269
|
+
const cats = escapeHtml(item.categories.join(","));
|
|
30787
31270
|
return `
|
|
30788
|
-
<
|
|
30789
|
-
|
|
30790
|
-
${
|
|
30791
|
-
<
|
|
30792
|
-
|
|
30793
|
-
|
|
30794
|
-
|
|
31271
|
+
<details class="case" data-status="${item.status}" data-cats="${cats}" data-search="${search}">
|
|
31272
|
+
<summary>
|
|
31273
|
+
<span class="badge ${item.status}">${item.status}</span>
|
|
31274
|
+
<b>\u6570\u636E\u884C ${escapeHtml(item.rowId ?? "default")}</b> \xB7 ${escapeHtml(item.caseName)}
|
|
31275
|
+
<span class="muted">\xB7 ${item.steps.length} \u6B65 \xB7 ${(item.durationMs / 1e3).toFixed(1)}s</span>
|
|
31276
|
+
</summary>
|
|
31277
|
+
${errorLine}
|
|
31278
|
+
${skipLine}
|
|
31279
|
+
${warningLine}
|
|
31280
|
+
${linksLine}
|
|
31281
|
+
<table>
|
|
31282
|
+
<tr><th>#</th><th>\u9636\u6BB5</th><th>target</th><th>action</th><th>\u72B6\u6001</th><th>\u8017\u65F6</th><th>\u8BE6\u60C5</th></tr>
|
|
31283
|
+
${rows}
|
|
31284
|
+
</table>
|
|
31285
|
+
</details>`;
|
|
30795
31286
|
}
|
|
30796
31287
|
function stepHtml(step) {
|
|
30797
31288
|
const details = [];
|
|
30798
31289
|
if (step.error) details.push(`<span class="error">${escapeHtml(step.error)}</span>`);
|
|
31290
|
+
if (step.category) details.push(`<span class="muted">\u5206\u7C7B: ${escapeHtml(categoryLabel(step.category))}</span>`);
|
|
30799
31291
|
if (step.extracted) {
|
|
30800
31292
|
for (const [name, value] of Object.entries(step.extracted)) {
|
|
30801
31293
|
details.push(`\u53D8\u91CF ${escapeHtml(name)} = <code>${escapeHtml(value)}</code>`);
|
|
30802
31294
|
}
|
|
30803
31295
|
}
|
|
30804
31296
|
if (step.screenshot) details.push(`<a href="${escapeHtml(step.screenshot)}">\u622A\u56FE</a>`);
|
|
31297
|
+
for (const assertion of step.requestAssertions ?? []) {
|
|
31298
|
+
details.push(
|
|
31299
|
+
`\u8BF7\u6C42 ${escapeHtml(assertion.method ?? "*")} <code>${escapeHtml(assertion.urlContains)}</code>: ${assertion.actualCount}/${assertion.count}`
|
|
31300
|
+
);
|
|
31301
|
+
}
|
|
30805
31302
|
return `<tr>
|
|
30806
31303
|
<td class="num">${step.index}</td>
|
|
31304
|
+
<td>${escapeHtml(step.phase ?? "steps")}</td>
|
|
30807
31305
|
<td>${escapeHtml(step.target)}</td>
|
|
30808
31306
|
<td>${escapeHtml(step.action)}</td>
|
|
30809
31307
|
<td><span class="badge ${step.status}">${step.status}</span></td>
|
|
@@ -30811,21 +31309,348 @@ function stepHtml(step) {
|
|
|
30811
31309
|
<td>${details.join("<br />") || "-"}</td>
|
|
30812
31310
|
</tr>`;
|
|
30813
31311
|
}
|
|
31312
|
+
function renderTrend(data) {
|
|
31313
|
+
const points = [
|
|
31314
|
+
...data.history.map((h) => ({ runId: h.runId, passed: h.passed, failed: h.failed, status: h.status })),
|
|
31315
|
+
{
|
|
31316
|
+
runId: data.summary.runId,
|
|
31317
|
+
passed: data.summary.totals.passed,
|
|
31318
|
+
failed: data.summary.totals.failed,
|
|
31319
|
+
status: data.summary.status
|
|
31320
|
+
}
|
|
31321
|
+
];
|
|
31322
|
+
if (points.length === 0) return '<p class="muted">\u6682\u65E0\u5386\u53F2\u6570\u636E</p>';
|
|
31323
|
+
const max = Math.max(1, ...points.map((p) => p.passed + p.failed));
|
|
31324
|
+
const barW = 34;
|
|
31325
|
+
const gap = 14;
|
|
31326
|
+
const chartH = 90;
|
|
31327
|
+
const labelH = 34;
|
|
31328
|
+
const width = points.length * (barW + gap) + gap;
|
|
31329
|
+
const height = chartH + labelH;
|
|
31330
|
+
const bars = points.map((point, index) => {
|
|
31331
|
+
const total = point.passed + point.failed;
|
|
31332
|
+
const scale = (count) => total === 0 ? 0 : count / max * chartH;
|
|
31333
|
+
const passedH = scale(point.passed);
|
|
31334
|
+
const failedH = scale(point.failed);
|
|
31335
|
+
const x = gap + index * (barW + gap);
|
|
31336
|
+
const passedY = chartH - passedH;
|
|
31337
|
+
const failedY = passedY - failedH;
|
|
31338
|
+
const tip = `${point.runId}: \u2713${point.passed} \u2717${point.failed}`;
|
|
31339
|
+
return `<g>
|
|
31340
|
+
<title>${escapeHtml(tip)}</title>
|
|
31341
|
+
<rect x="${x}" y="${chartH + 4}" width="${barW}" height="1" fill="var(--line)"></rect>
|
|
31342
|
+
${passedH > 0 ? `<rect x="${x}" y="${passedY.toFixed(1)}" width="${barW}" height="${passedH.toFixed(1)}" fill="var(--ok)" rx="2"></rect>` : ""}
|
|
31343
|
+
${failedH > 0 ? `<rect x="${x}" y="${failedY.toFixed(1)}" width="${barW}" height="${failedH.toFixed(1)}" fill="var(--fail)" rx="2"></rect>` : ""}
|
|
31344
|
+
<text x="${x + barW / 2}" y="${chartH + 20}" font-size="9" fill="var(--muted)" text-anchor="middle">${escapeHtml(shortId(point.runId))}</text>
|
|
31345
|
+
</g>`;
|
|
31346
|
+
}).join("\n");
|
|
31347
|
+
return `<svg width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" xmlns="http://www.w3.org/2000/svg">${bars}</svg>`;
|
|
31348
|
+
}
|
|
31349
|
+
function shortId(runId) {
|
|
31350
|
+
return runId.replace(/^\d{8}-/, "");
|
|
31351
|
+
}
|
|
30814
31352
|
function escapeHtml(input2) {
|
|
30815
31353
|
return input2.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
30816
31354
|
}
|
|
31355
|
+
function embedJson(data) {
|
|
31356
|
+
return JSON.stringify(data).replaceAll("<", "\\u003c");
|
|
31357
|
+
}
|
|
31358
|
+
|
|
31359
|
+
// ../../packages/reporter/src/summary-report.ts
|
|
31360
|
+
import { mkdir as mkdir4, writeFile as writeFile5 } from "node:fs/promises";
|
|
31361
|
+
import { join as join9, resolve as resolve5 } from "node:path";
|
|
31362
|
+
function escapeHtml2(value) {
|
|
31363
|
+
return String(value).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
31364
|
+
}
|
|
31365
|
+
function pct(value) {
|
|
31366
|
+
return (value * 100).toFixed(1) + "%";
|
|
31367
|
+
}
|
|
31368
|
+
function buildSummaryData(runs) {
|
|
31369
|
+
const executions = [];
|
|
31370
|
+
const runPoints = [];
|
|
31371
|
+
for (const { runId, summary } of runs) {
|
|
31372
|
+
for (const item of summary.cases) {
|
|
31373
|
+
const platforms = [...new Set(item.steps.map((step) => step.target))];
|
|
31374
|
+
const failedStep = item.steps.find((step) => step.status === "failed");
|
|
31375
|
+
const category = item.status === "failed" ? classifyFailure(failedStep?.error) : void 0;
|
|
31376
|
+
executions.push({
|
|
31377
|
+
runId,
|
|
31378
|
+
caseId: item.caseId,
|
|
31379
|
+
rowId: item.rowId,
|
|
31380
|
+
identity: item.rowId && item.rowId !== "default" ? `${item.caseId}#${item.rowId}` : item.caseId,
|
|
31381
|
+
caseName: item.caseName,
|
|
31382
|
+
status: item.status,
|
|
31383
|
+
category,
|
|
31384
|
+
platforms
|
|
31385
|
+
});
|
|
31386
|
+
}
|
|
31387
|
+
runPoints.push({
|
|
31388
|
+
runId,
|
|
31389
|
+
startedAt: summary.startedAt,
|
|
31390
|
+
status: summary.status,
|
|
31391
|
+
passed: summary.totals.passed,
|
|
31392
|
+
failed: summary.totals.failed,
|
|
31393
|
+
passRate: summary.totals.cases > 0 ? summary.totals.passed / summary.totals.cases : 0
|
|
31394
|
+
});
|
|
31395
|
+
}
|
|
31396
|
+
const byCase = /* @__PURE__ */ new Map();
|
|
31397
|
+
for (const exec2 of executions) {
|
|
31398
|
+
let agg = byCase.get(exec2.identity);
|
|
31399
|
+
if (!agg) {
|
|
31400
|
+
agg = {
|
|
31401
|
+
caseId: exec2.caseId,
|
|
31402
|
+
rowId: exec2.rowId,
|
|
31403
|
+
caseName: exec2.caseName,
|
|
31404
|
+
platforms: [],
|
|
31405
|
+
runs: 0,
|
|
31406
|
+
passed: 0,
|
|
31407
|
+
failed: 0,
|
|
31408
|
+
passRate: 0,
|
|
31409
|
+
lastStatus: "unknown",
|
|
31410
|
+
flaky: false
|
|
31411
|
+
};
|
|
31412
|
+
byCase.set(exec2.identity, agg);
|
|
31413
|
+
}
|
|
31414
|
+
agg.runs++;
|
|
31415
|
+
if (exec2.status === "passed") agg.passed++;
|
|
31416
|
+
else if (exec2.status === "failed") agg.failed++;
|
|
31417
|
+
for (const platform of exec2.platforms) {
|
|
31418
|
+
if (!agg.platforms.includes(platform)) agg.platforms.push(platform);
|
|
31419
|
+
}
|
|
31420
|
+
}
|
|
31421
|
+
for (const agg of byCase.values()) {
|
|
31422
|
+
const identity = agg.rowId && agg.rowId !== "default" ? `${agg.caseId}#${agg.rowId}` : agg.caseId;
|
|
31423
|
+
const last = [...executions].reverse().find((exec2) => exec2.identity === identity);
|
|
31424
|
+
agg.lastStatus = last?.status ?? "unknown";
|
|
31425
|
+
agg.lastCategory = last?.category;
|
|
31426
|
+
agg.passRate = agg.runs > 0 ? agg.passed / agg.runs : 0;
|
|
31427
|
+
agg.flaky = agg.failed > 0 && agg.passed > 0;
|
|
31428
|
+
}
|
|
31429
|
+
const cases = [...byCase.values()];
|
|
31430
|
+
const flaky = cases.filter((item) => item.flaky);
|
|
31431
|
+
const categories = FAILURE_CATEGORIES.map((item) => ({
|
|
31432
|
+
id: item.id,
|
|
31433
|
+
label: item.label,
|
|
31434
|
+
count: executions.filter((exec2) => exec2.status === "failed" && exec2.category === item.id).length
|
|
31435
|
+
})).filter((item) => item.count > 0);
|
|
31436
|
+
const durations = runs.map((item) => item.summary.durationMs).filter((value) => typeof value === "number");
|
|
31437
|
+
return {
|
|
31438
|
+
window: {
|
|
31439
|
+
runs: runs.length,
|
|
31440
|
+
from: runPoints[0]?.startedAt,
|
|
31441
|
+
to: runPoints.at(-1)?.startedAt
|
|
31442
|
+
},
|
|
31443
|
+
kpis: {
|
|
31444
|
+
runs: runs.length,
|
|
31445
|
+
executions: executions.length,
|
|
31446
|
+
overallRate: executions.length > 0 ? executions.filter((exec2) => exec2.status === "passed").length / executions.length : 0,
|
|
31447
|
+
avgDurationMs: durations.length > 0 ? durations.reduce((sum, value) => sum + value, 0) / durations.length : 0,
|
|
31448
|
+
flakyCount: flaky.length
|
|
31449
|
+
},
|
|
31450
|
+
runs: runPoints,
|
|
31451
|
+
cases,
|
|
31452
|
+
platforms: [...new Set(cases.flatMap((item) => item.platforms))].sort(),
|
|
31453
|
+
categories,
|
|
31454
|
+
categoryLabels: Object.fromEntries(FAILURE_CATEGORIES.map((item) => [item.id, item.label]))
|
|
31455
|
+
};
|
|
31456
|
+
}
|
|
31457
|
+
function renderSummaryTrend(runs) {
|
|
31458
|
+
if (runs.length === 0) return '<p class="muted">\u7EDF\u8BA1\u7A97\u53E3\u5185\u6CA1\u6709\u8FD0\u884C\u6570\u636E</p>';
|
|
31459
|
+
const barW = 30;
|
|
31460
|
+
const gap = 8;
|
|
31461
|
+
const chartH = 96;
|
|
31462
|
+
const width = runs.length * (barW + gap) + gap;
|
|
31463
|
+
const height = chartH + 30;
|
|
31464
|
+
const max = Math.max(1, ...runs.map((run) => run.passed + run.failed));
|
|
31465
|
+
const bars = runs.map((run, index) => {
|
|
31466
|
+
const x = gap + index * (barW + gap);
|
|
31467
|
+
const passedH = run.passed / max * chartH;
|
|
31468
|
+
const failedH = run.failed / max * chartH;
|
|
31469
|
+
const passedY = chartH - passedH;
|
|
31470
|
+
const failedY = passedY - failedH;
|
|
31471
|
+
const total = run.passed + run.failed;
|
|
31472
|
+
const rate = total > 0 ? Math.round(run.passed / total * 100) : 0;
|
|
31473
|
+
const label = run.runId.replace(/^\d{8}-/, "");
|
|
31474
|
+
return "<g><title>" + escapeHtml2(run.runId) + ": \u2713" + run.passed + " \u2717" + run.failed + '</title><rect x="' + x + '" y="' + passedY.toFixed(1) + '" width="' + barW + '" height="' + passedH.toFixed(1) + '" fill="#1a7f37" rx="2"></rect><rect x="' + x + '" y="' + failedY.toFixed(1) + '" width="' + barW + '" height="' + failedH.toFixed(1) + '" fill="#cf222e" rx="2"></rect><text x="' + (x + barW / 2) + '" y="' + (chartH + 12) + '" font-size="8" fill="#59636e" text-anchor="middle">' + escapeHtml2(label) + '</text><text x="' + (x + barW / 2) + '" y="' + (chartH + 24) + '" font-size="8" fill="#59636e" text-anchor="middle">' + rate + "%</text></g>";
|
|
31475
|
+
}).join("\n");
|
|
31476
|
+
return '<svg width="' + width + '" height="' + height + '" viewBox="0 0 ' + width + " " + height + '" xmlns="http://www.w3.org/2000/svg">' + bars + "</svg>";
|
|
31477
|
+
}
|
|
31478
|
+
function renderSummaryHtml(data) {
|
|
31479
|
+
const kpis = data.kpis;
|
|
31480
|
+
const rateText = pct(kpis.overallRate);
|
|
31481
|
+
const trendSvg = renderSummaryTrend(data.runs);
|
|
31482
|
+
const catBars = data.categories.map(
|
|
31483
|
+
(item) => '<div class="hbar-row" data-cat="' + item.id + '" title="\u70B9\u51FB\u7B5B\u9009\u7528\u4F8B\u660E\u7EC6"><span class="hbar-label">' + escapeHtml2(item.label) + '</span><span class="hbar-track"><span class="hbar" style="width:' + (kpis.executions > 0 ? Math.round(item.count / kpis.executions * 100) : 0) + '%"></span></span><span class="hbar-num">' + item.count + "</span></div>"
|
|
31484
|
+
).join("\n");
|
|
31485
|
+
const catHtml = catBars || '<p class="muted">\u7EDF\u8BA1\u7A97\u53E3\u5185\u65E0\u5931\u8D25</p>';
|
|
31486
|
+
const flaky = data.cases.filter((item) => item.flaky);
|
|
31487
|
+
const flakyRows = flaky.map(
|
|
31488
|
+
(item) => '<tr data-platform="' + escapeHtml2(item.platforms.join(" ")) + '" data-q="' + escapeHtml2(item.caseId + " " + item.caseName) + '"><td><code class="flaky-link" data-q="' + escapeHtml2(item.caseId) + '">' + escapeHtml2(item.caseId) + "</code></td><td>" + escapeHtml2(item.caseName) + "</td><td>" + escapeHtml2(item.platforms.join(" + ")) + '</td><td class="num">' + item.failed + " / " + item.runs + '</td><td class="num">' + pct(item.passRate) + '</td><td><span class="badge ' + item.lastStatus + '">' + item.lastStatus + "</span></td></tr>"
|
|
31489
|
+
).join("\n");
|
|
31490
|
+
const platformChips = ["all", ...data.platforms].map(
|
|
31491
|
+
(platform) => '<span class="chip' + (platform === "all" ? " active" : "") + '" data-platform="' + platform + '">' + (platform === "all" ? "\u5168\u90E8\u5E73\u53F0" : platform) + "</span>"
|
|
31492
|
+
).join("\n");
|
|
31493
|
+
const detailRows = data.cases.map(
|
|
31494
|
+
(item) => '<tr data-platform="' + escapeHtml2(item.platforms.join(" ")) + '" data-cat="' + escapeHtml2(item.lastCategory ?? "") + '" data-q="' + escapeHtml2(item.caseId + " " + item.caseName) + '" data-pass="' + item.passRate + '" data-runs="' + item.runs + '" data-failed="' + item.failed + '"><td><code>' + escapeHtml2(item.caseId) + "</code></td><td>" + escapeHtml2(item.caseName) + "</td><td>" + escapeHtml2(item.platforms.join(" + ")) + '</td><td class="num">' + item.runs + '</td><td class="num">' + item.passed + '</td><td class="num">' + item.failed + '</td><td><span class="mini-track"><span class="mini ' + (item.failed > 0 ? "warn" : "ok") + '" style="width:' + Math.round(item.passRate * 60) + 'px"></span></span> ' + pct(item.passRate) + "</td><td>" + (item.lastCategory ? escapeHtml2(categoryLabel(item.lastCategory)) : "-") + '</td><td><span class="badge ' + item.lastStatus + '">' + item.lastStatus + "</span></td></tr>"
|
|
31495
|
+
).join("\n");
|
|
31496
|
+
const range = (data.window.from ?? "").slice(0, 10) + " \u2192 " + (data.window.to ?? "").slice(0, 10);
|
|
31497
|
+
return `<!doctype html>
|
|
31498
|
+
<html lang="zh-CN">
|
|
31499
|
+
<head>
|
|
31500
|
+
<meta charset="utf-8" />
|
|
31501
|
+
<title>TestPilot \u6C47\u603B\u62A5\u544A</title>
|
|
31502
|
+
<style>
|
|
31503
|
+
:root { --ok:#1a7f37; --fail:#cf222e; --line:#d0d7de; --muted:#59636e; }
|
|
31504
|
+
* { box-sizing: border-box; }
|
|
31505
|
+
body { font-family: system-ui, "Segoe UI", sans-serif; margin: 0; color: #1f2328; background: #f6f8fa; }
|
|
31506
|
+
.wrap { max-width: 1120px; margin: 0 auto; padding: 20px 16px 56px; }
|
|
31507
|
+
h1 { font-size: 21px; margin: 8px 0; } h2 { font-size: 15px; margin: 26px 0 8px; }
|
|
31508
|
+
.meta { color: var(--muted); font-size: 12px; margin: 4px 0 12px; }
|
|
31509
|
+
.cards { display: flex; gap: 12px; flex-wrap: wrap; margin: 14px 0; }
|
|
31510
|
+
.card { background: #fff; border: 1px solid var(--line); border-radius: 8px; padding: 12px 18px; min-width: 140px; }
|
|
31511
|
+
.card b { font-size: 22px; display: block; } .card span { color: var(--muted); font-size: 12px; }
|
|
31512
|
+
.row2 { display: flex; gap: 14px; flex-wrap: wrap; align-items: stretch; }
|
|
31513
|
+
.panel { background: #fff; border: 1px solid var(--line); border-radius: 8px; padding: 14px; }
|
|
31514
|
+
.panel h3 { margin: 0 0 8px; font-size: 13px; color: var(--muted); font-weight: 500; }
|
|
31515
|
+
.trend { overflow-x: auto; flex: 1; min-width: 320px; }
|
|
31516
|
+
table { border-collapse: collapse; width: 100%; font-size: 13px; background: #fff; }
|
|
31517
|
+
th, td { border-bottom: 1px solid var(--line); padding: 7px 10px; text-align: left; }
|
|
31518
|
+
th { color: var(--muted); font-weight: 500; cursor: pointer; white-space: nowrap; }
|
|
31519
|
+
th.num, td.num { text-align: right; }
|
|
31520
|
+
code { background: #eef1f4; padding: 1px 5px; border-radius: 4px; font-size: 12px; cursor: pointer; }
|
|
31521
|
+
.badge { display: inline-block; padding: 1px 9px; border-radius: 10px; color: #fff; font-size: 11px; }
|
|
31522
|
+
.passed { background: #1a7f37; } .failed { background: #cf222e; } .unknown { background: #6e7781; }
|
|
31523
|
+
.toolbar { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin: 10px 0; }
|
|
31524
|
+
.chip { border: 1px solid var(--line); background: #fff; border-radius: 14px; padding: 3px 12px; font-size: 12px; cursor: pointer; }
|
|
31525
|
+
.chip.active { background: #0969da; color: #fff; border-color: #0969da; }
|
|
31526
|
+
input#q { flex: 1; min-width: 160px; padding: 5px 10px; border: 1px solid var(--line); border-radius: 6px; font-size: 13px; }
|
|
31527
|
+
#count { color: var(--muted); font-size: 12px; margin-left: auto; }
|
|
31528
|
+
.hbar-row { display: flex; align-items: center; gap: 10px; margin: 7px 0; cursor: pointer; }
|
|
31529
|
+
.hbar-label { width: 90px; font-size: 13px; } .hbar-num { width: 30px; text-align: right; font-size: 13px; }
|
|
31530
|
+
.hbar-track { flex: 1; background: #eef1f4; border-radius: 5px; height: 12px; overflow: hidden; }
|
|
31531
|
+
.hbar { display: block; height: 100%; background: var(--fail); }
|
|
31532
|
+
.mini-track { display: inline-block; width: 60px; height: 7px; background: #eef1f4; border-radius: 4px; overflow: hidden; vertical-align: middle; }
|
|
31533
|
+
.mini { display: block; height: 100%; } .mini.ok { background: var(--ok); } .mini.warn { background: #bf8700; }
|
|
31534
|
+
.muted { color: var(--muted); } .hide { display: none !important; }
|
|
31535
|
+
</style>
|
|
31536
|
+
</head>
|
|
31537
|
+
<body>
|
|
31538
|
+
<div class="wrap">
|
|
31539
|
+
<h1>TestPilot \u6C47\u603B\u62A5\u544A</h1>
|
|
31540
|
+
<p class="meta">\u7EDF\u8BA1\u8303\u56F4:\u6700\u8FD1 ${data.window.runs} \u6B21\u8FD0\u884C(${escapeHtml2(range)})\xB7 \u751F\u6210\u4E8E ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}</p>
|
|
31541
|
+
|
|
31542
|
+
<div class="cards">
|
|
31543
|
+
<div class="card"><b>${kpis.runs}</b><span>\u8FD0\u884C\u6B21\u6570</span></div>
|
|
31544
|
+
<div class="card"><b>${kpis.executions}</b><span>\u7528\u4F8B\u6267\u884C\u6B21\u6570</span></div>
|
|
31545
|
+
<div class="card"><b>${rateText}</b><span>\u6574\u4F53\u901A\u8FC7\u7387</span></div>
|
|
31546
|
+
<div class="card"><b>${(kpis.avgDurationMs / 1e3).toFixed(1)}s</b><span>\u5E73\u5747\u8FD0\u884C\u8017\u65F6</span></div>
|
|
31547
|
+
<div class="card"><b>${kpis.flakyCount}</b><span>Flaky \u7528\u4F8B</span></div>
|
|
31548
|
+
</div>
|
|
31549
|
+
|
|
31550
|
+
<h2>\u8D8B\u52BF(\u6BCF\u6B21\u8FD0\u884C\u7ED3\u679C\u4E0E\u901A\u8FC7\u7387,\u60AC\u6D6E\u770B\u8BE6\u60C5)</h2>
|
|
31551
|
+
<div class="panel trend">${trendSvg}</div>
|
|
31552
|
+
|
|
31553
|
+
<h2>\u5931\u8D25\u5206\u7C7B\u5206\u5E03(\u70B9\u51FB\u7B5B\u9009\u660E\u7EC6)</h2>
|
|
31554
|
+
<div class="panel">${catHtml}</div>
|
|
31555
|
+
|
|
31556
|
+
<h2>Flaky \u699C(\u53C8\u8FC7\u53C8\u6302;\u70B9\u7528\u4F8B\u540D\u5B9A\u4F4D\u660E\u7EC6)</h2>
|
|
31557
|
+
<div class="panel"><table>
|
|
31558
|
+
<tr><th>\u7528\u4F8B</th><th>\u540D\u79F0</th><th>\u5E73\u53F0</th><th class="num">\u5931\u8D25 / \u53C2\u4E0E</th><th class="num">\u901A\u8FC7\u7387</th><th>\u6700\u8FD1\u72B6\u6001</th></tr>
|
|
31559
|
+
${flakyRows || '<tr><td colspan="6" class="muted">\u7EDF\u8BA1\u7A97\u53E3\u5185\u65E0 Flaky \u7528\u4F8B</td></tr>'}
|
|
31560
|
+
</table></div>
|
|
31561
|
+
|
|
31562
|
+
<h2>\u7528\u4F8B\u660E\u7EC6</h2>
|
|
31563
|
+
<div class="toolbar">
|
|
31564
|
+
${platformChips}
|
|
31565
|
+
<input id="q" placeholder="\u641C\u7D22\u7528\u4F8B id / \u540D\u79F0\u2026" />
|
|
31566
|
+
<span id="count"></span>
|
|
31567
|
+
</div>
|
|
31568
|
+
<table id="detail">
|
|
31569
|
+
<thead><tr>
|
|
31570
|
+
<th>\u7528\u4F8B</th><th>\u540D\u79F0</th><th>\u5E73\u53F0</th>
|
|
31571
|
+
<th class="num" data-sort="runs">\u8FD0\u884C\u6B21\u6570</th><th class="num" data-sort="passed">\u901A\u8FC7</th><th class="num" data-sort="failed">\u5931\u8D25</th>
|
|
31572
|
+
<th data-sort="passRate">\u901A\u8FC7\u7387 \u25BE</th><th>\u6700\u8FD1\u5206\u7C7B</th><th>\u6700\u8FD1\u72B6\u6001</th>
|
|
31573
|
+
</tr></thead>
|
|
31574
|
+
<tbody>${detailRows}</tbody>
|
|
31575
|
+
</table>
|
|
31576
|
+
</div>
|
|
31577
|
+
|
|
31578
|
+
<script id="summary-data" type="application/json">${JSON.stringify(data).replaceAll("<", "\\u003c")}</script>
|
|
31579
|
+
<script>
|
|
31580
|
+
(function () {
|
|
31581
|
+
var rows = Array.prototype.slice.call(document.querySelectorAll('#detail tbody tr'));
|
|
31582
|
+
var state = { platform: 'all', cat: 'all', q: '', sortKey: 'passRate', sortDir: -1 };
|
|
31583
|
+
|
|
31584
|
+
function apply() {
|
|
31585
|
+
var shown = 0;
|
|
31586
|
+
rows.forEach(function (row) {
|
|
31587
|
+
var show = true;
|
|
31588
|
+
if (state.platform !== 'all' && row.getAttribute('data-platform').split(' ').indexOf(state.platform) === -1) show = false;
|
|
31589
|
+
if (state.cat !== 'all' && (row.getAttribute('data-cat') || '') !== state.cat) show = false;
|
|
31590
|
+
if (state.q && row.getAttribute('data-q').toLowerCase().indexOf(state.q.toLowerCase()) === -1) show = false;
|
|
31591
|
+
row.classList.toggle('hide', !show);
|
|
31592
|
+
if (show) shown++;
|
|
31593
|
+
});
|
|
31594
|
+
document.getElementById('count').textContent = shown + ' / ' + rows.length + ' \u7528\u4F8B';
|
|
31595
|
+
}
|
|
31596
|
+
|
|
31597
|
+
document.querySelectorAll('.chip[data-platform]').forEach(function (chip) {
|
|
31598
|
+
chip.addEventListener('click', function () {
|
|
31599
|
+
document.querySelectorAll('.chip[data-platform]').forEach(function (c) { c.classList.remove('active'); });
|
|
31600
|
+
chip.classList.add('active');
|
|
31601
|
+
state.platform = chip.getAttribute('data-platform');
|
|
31602
|
+
apply();
|
|
31603
|
+
});
|
|
31604
|
+
});
|
|
31605
|
+
document.querySelectorAll('.hbar-row').forEach(function (bar) {
|
|
31606
|
+
bar.addEventListener('click', function () {
|
|
31607
|
+
state.cat = bar.getAttribute('data-cat');
|
|
31608
|
+
apply();
|
|
31609
|
+
});
|
|
31610
|
+
});
|
|
31611
|
+
var q = document.getElementById('q');
|
|
31612
|
+
q.addEventListener('input', function () { state.q = q.value.trim(); apply(); });
|
|
31613
|
+
|
|
31614
|
+
document.querySelectorAll('#detail th[data-sort]').forEach(function (th) {
|
|
31615
|
+
th.addEventListener('click', function () {
|
|
31616
|
+
var key = th.getAttribute('data-sort');
|
|
31617
|
+
state.sortDir = state.sortKey === key ? -state.sortDir : -1;
|
|
31618
|
+
state.sortKey = key;
|
|
31619
|
+
var sorted = rows.slice().sort(function (a, b) {
|
|
31620
|
+
return (Number(a.getAttribute('data-' + key)) - Number(b.getAttribute('data-' + key))) * state.sortDir;
|
|
31621
|
+
});
|
|
31622
|
+
var tbody = document.querySelector('#detail tbody');
|
|
31623
|
+
sorted.forEach(function (row) { tbody.appendChild(row); });
|
|
31624
|
+
});
|
|
31625
|
+
});
|
|
31626
|
+
|
|
31627
|
+
apply();
|
|
31628
|
+
})();
|
|
31629
|
+
</script>
|
|
31630
|
+
</body>
|
|
31631
|
+
</html>`;
|
|
31632
|
+
}
|
|
31633
|
+
async function writeSummaryReport(artifactsDir, data) {
|
|
31634
|
+
const dir = resolve5(artifactsDir);
|
|
31635
|
+
await mkdir4(dir, { recursive: true });
|
|
31636
|
+
const htmlPath = join9(dir, "summary.html");
|
|
31637
|
+
const jsonPath = join9(dir, "summary.json");
|
|
31638
|
+
await writeFile5(jsonPath, JSON.stringify(data, null, 2), "utf8");
|
|
31639
|
+
await writeFile5(htmlPath, renderSummaryHtml(data), "utf8");
|
|
31640
|
+
return { html: htmlPath, json: jsonPath };
|
|
31641
|
+
}
|
|
30817
31642
|
|
|
30818
31643
|
// ../../packages/sdk/src/runs.ts
|
|
30819
|
-
import { readFile as
|
|
30820
|
-
import { isAbsolute as
|
|
31644
|
+
import { readFile as readFile7, readdir as readdir4 } from "node:fs/promises";
|
|
31645
|
+
import { isAbsolute as isAbsolute3, join as join17 } from "node:path";
|
|
30821
31646
|
|
|
30822
31647
|
// ../../packages/execution-engine/src/artifacts/index.ts
|
|
30823
|
-
import { mkdir as
|
|
30824
|
-
import { join as
|
|
31648
|
+
import { mkdir as mkdir9 } from "node:fs/promises";
|
|
31649
|
+
import { join as join13 } from "node:path";
|
|
30825
31650
|
|
|
30826
31651
|
// ../../packages/evidence/src/log.ts
|
|
30827
|
-
import { appendFile, mkdir as
|
|
30828
|
-
import { dirname as
|
|
31652
|
+
import { appendFile, mkdir as mkdir5 } from "node:fs/promises";
|
|
31653
|
+
import { dirname as dirname4 } from "node:path";
|
|
30829
31654
|
var RunLogger = class {
|
|
30830
31655
|
constructor(filePath) {
|
|
30831
31656
|
this.filePath = filePath;
|
|
@@ -30840,7 +31665,7 @@ var RunLogger = class {
|
|
|
30840
31665
|
return this.write("ERROR", message);
|
|
30841
31666
|
}
|
|
30842
31667
|
async write(level, message) {
|
|
30843
|
-
await
|
|
31668
|
+
await mkdir5(dirname4(this.filePath), { recursive: true });
|
|
30844
31669
|
const line = `${(/* @__PURE__ */ new Date()).toISOString()} ${level} ${message}
|
|
30845
31670
|
`;
|
|
30846
31671
|
await appendFile(this.filePath, line, "utf8");
|
|
@@ -30848,32 +31673,32 @@ var RunLogger = class {
|
|
|
30848
31673
|
};
|
|
30849
31674
|
|
|
30850
31675
|
// ../../packages/evidence/src/screenshot.ts
|
|
30851
|
-
import { mkdir as
|
|
30852
|
-
import { dirname as
|
|
31676
|
+
import { mkdir as mkdir6, writeFile as writeFile6 } from "node:fs/promises";
|
|
31677
|
+
import { dirname as dirname5, join as join10 } from "node:path";
|
|
30853
31678
|
async function saveScreenshot(dir, fileName, data) {
|
|
30854
|
-
const file2 =
|
|
30855
|
-
await
|
|
30856
|
-
await
|
|
31679
|
+
const file2 = join10(dir, fileName);
|
|
31680
|
+
await mkdir6(dirname5(file2), { recursive: true });
|
|
31681
|
+
await writeFile6(file2, data);
|
|
30857
31682
|
return file2;
|
|
30858
31683
|
}
|
|
30859
31684
|
|
|
30860
31685
|
// ../../packages/evidence/src/trace.ts
|
|
30861
|
-
import { mkdir as
|
|
30862
|
-
import { dirname as
|
|
31686
|
+
import { mkdir as mkdir7, writeFile as writeFile7 } from "node:fs/promises";
|
|
31687
|
+
import { dirname as dirname6, join as join11 } from "node:path";
|
|
30863
31688
|
async function saveTrace(dir, fileName, data) {
|
|
30864
|
-
const file2 =
|
|
30865
|
-
await
|
|
30866
|
-
await
|
|
31689
|
+
const file2 = join11(dir, fileName);
|
|
31690
|
+
await mkdir7(dirname6(file2), { recursive: true });
|
|
31691
|
+
await writeFile7(file2, data);
|
|
30867
31692
|
return file2;
|
|
30868
31693
|
}
|
|
30869
31694
|
|
|
30870
31695
|
// ../../packages/evidence/src/video.ts
|
|
30871
|
-
import { mkdir as
|
|
30872
|
-
import { dirname as
|
|
31696
|
+
import { mkdir as mkdir8, writeFile as writeFile8 } from "node:fs/promises";
|
|
31697
|
+
import { dirname as dirname7, join as join12 } from "node:path";
|
|
30873
31698
|
async function saveVideo(dir, fileName, data) {
|
|
30874
|
-
const file2 =
|
|
30875
|
-
await
|
|
30876
|
-
await
|
|
31699
|
+
const file2 = join12(dir, fileName);
|
|
31700
|
+
await mkdir8(dirname7(file2), { recursive: true });
|
|
31701
|
+
await writeFile8(file2, data);
|
|
30877
31702
|
return file2;
|
|
30878
31703
|
}
|
|
30879
31704
|
|
|
@@ -30881,11 +31706,11 @@ async function saveVideo(dir, fileName, data) {
|
|
|
30881
31706
|
var ArtifactManager = class {
|
|
30882
31707
|
constructor(runDir) {
|
|
30883
31708
|
this.runDir = runDir;
|
|
30884
|
-
this.screenshotsDir =
|
|
30885
|
-
this.videosDir =
|
|
30886
|
-
this.tracesDir =
|
|
30887
|
-
this.logsDir =
|
|
30888
|
-
this.logger = new RunLogger(
|
|
31709
|
+
this.screenshotsDir = join13(runDir, "screenshots");
|
|
31710
|
+
this.videosDir = join13(runDir, "videos");
|
|
31711
|
+
this.tracesDir = join13(runDir, "traces");
|
|
31712
|
+
this.logsDir = join13(runDir, "logs");
|
|
31713
|
+
this.logger = new RunLogger(join13(this.logsDir, "run.log"));
|
|
30889
31714
|
}
|
|
30890
31715
|
screenshotsDir;
|
|
30891
31716
|
videosDir;
|
|
@@ -30893,8 +31718,8 @@ var ArtifactManager = class {
|
|
|
30893
31718
|
logsDir;
|
|
30894
31719
|
logger;
|
|
30895
31720
|
async prepare() {
|
|
30896
|
-
await
|
|
30897
|
-
await
|
|
31721
|
+
await mkdir9(this.screenshotsDir, { recursive: true });
|
|
31722
|
+
await mkdir9(this.logsDir, { recursive: true });
|
|
30898
31723
|
}
|
|
30899
31724
|
/** 保存步骤截图,返回相对 run 目录的路径(screenshots/xxx.png,统一正斜杠供报告链接使用) */
|
|
30900
31725
|
async saveStepScreenshot(fileName, data) {
|
|
@@ -30922,6 +31747,11 @@ var ExecutionContext = class {
|
|
|
30922
31747
|
get(name) {
|
|
30923
31748
|
return this.variables.get(name);
|
|
30924
31749
|
}
|
|
31750
|
+
seed(values) {
|
|
31751
|
+
for (const [name, value] of Object.entries(values)) {
|
|
31752
|
+
this.variables.set(name, value);
|
|
31753
|
+
}
|
|
31754
|
+
}
|
|
30925
31755
|
/**
|
|
30926
31756
|
* 将字符串中的 ${name} 替换为已提取的变量值;未定义的引用保持原样(校验阶段已拦截)。
|
|
30927
31757
|
* 名字允许点号,用于引擎注入的 account.username 一类凭据变量(用户 extract 变量名不含点号)。
|
|
@@ -30958,8 +31788,8 @@ var ExecutionContext = class {
|
|
|
30958
31788
|
};
|
|
30959
31789
|
|
|
30960
31790
|
// ../../packages/execution-engine/src/lifecycle/index.ts
|
|
30961
|
-
import { mkdir as
|
|
30962
|
-
import { join as
|
|
31791
|
+
import { mkdir as mkdir10, readdir as readdir3 } from "node:fs/promises";
|
|
31792
|
+
import { join as join14 } from "node:path";
|
|
30963
31793
|
function dateStamp(date5 = /* @__PURE__ */ new Date()) {
|
|
30964
31794
|
const year = date5.getFullYear();
|
|
30965
31795
|
const month = String(date5.getMonth() + 1).padStart(2, "0");
|
|
@@ -30968,7 +31798,7 @@ function dateStamp(date5 = /* @__PURE__ */ new Date()) {
|
|
|
30968
31798
|
}
|
|
30969
31799
|
async function allocateRunId(runsRoot2) {
|
|
30970
31800
|
const stamp = dateStamp();
|
|
30971
|
-
const existing = await
|
|
31801
|
+
const existing = await readdir3(runsRoot2).catch(() => []);
|
|
30972
31802
|
let max = 0;
|
|
30973
31803
|
for (const name of existing) {
|
|
30974
31804
|
const match = /^(\d{8})-(\d{3})$/.exec(name);
|
|
@@ -30979,14 +31809,14 @@ async function allocateRunId(runsRoot2) {
|
|
|
30979
31809
|
return `${stamp}-${String(max + 1).padStart(3, "0")}`;
|
|
30980
31810
|
}
|
|
30981
31811
|
async function ensureRunDir(runsRoot2, runId) {
|
|
30982
|
-
const runDir =
|
|
30983
|
-
await
|
|
31812
|
+
const runDir = join14(runsRoot2, runId);
|
|
31813
|
+
await mkdir10(runDir, { recursive: true });
|
|
30984
31814
|
return runDir;
|
|
30985
31815
|
}
|
|
30986
31816
|
|
|
30987
31817
|
// ../../packages/execution-engine/src/events/index.ts
|
|
30988
|
-
import { appendFile as appendFile2, mkdir as
|
|
30989
|
-
import { dirname as
|
|
31818
|
+
import { appendFile as appendFile2, mkdir as mkdir11 } from "node:fs/promises";
|
|
31819
|
+
import { dirname as dirname8 } from "node:path";
|
|
30990
31820
|
var EventBus = class {
|
|
30991
31821
|
subscribers = [];
|
|
30992
31822
|
subscribe(subscriber) {
|
|
@@ -31000,7 +31830,7 @@ var EventBus = class {
|
|
|
31000
31830
|
};
|
|
31001
31831
|
function ndjsonSubscriber(filePath) {
|
|
31002
31832
|
return async (event) => {
|
|
31003
|
-
await
|
|
31833
|
+
await mkdir11(dirname8(filePath), { recursive: true });
|
|
31004
31834
|
await appendFile2(filePath, `${JSON.stringify(event)}
|
|
31005
31835
|
`, "utf8");
|
|
31006
31836
|
};
|
|
@@ -31081,11 +31911,19 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
|
|
|
31081
31911
|
status: "passed",
|
|
31082
31912
|
durationMs: 0
|
|
31083
31913
|
};
|
|
31914
|
+
let captureStarted = false;
|
|
31084
31915
|
try {
|
|
31085
31916
|
const locator = step.locator ? {
|
|
31086
31917
|
text: step.locator.text !== void 0 ? context.resolve(step.locator.text) : void 0,
|
|
31087
31918
|
css: step.locator.css !== void 0 ? context.resolve(step.locator.css) : void 0
|
|
31088
31919
|
} : void 0;
|
|
31920
|
+
if (step.expectRequests) {
|
|
31921
|
+
if (!adapter.startRequestCapture || !adapter.stopRequestCapture) {
|
|
31922
|
+
throw new Error(`\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301\u8BF7\u6C42\u6B21\u6570\u65AD\u8A00`);
|
|
31923
|
+
}
|
|
31924
|
+
await adapter.startRequestCapture();
|
|
31925
|
+
captureStarted = true;
|
|
31926
|
+
}
|
|
31089
31927
|
switch (step.action) {
|
|
31090
31928
|
case "launch":
|
|
31091
31929
|
await adapter.launch();
|
|
@@ -31156,7 +31994,29 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
|
|
|
31156
31994
|
throw new Error(`\u4E0D\u652F\u6301\u7684 action:${String(exhaustive)}`);
|
|
31157
31995
|
}
|
|
31158
31996
|
}
|
|
31997
|
+
if (step.expectRequests) {
|
|
31998
|
+
const windowMs = Math.max(...step.expectRequests.map((item) => item.windowMs));
|
|
31999
|
+
if (windowMs > 0) await delay(windowMs);
|
|
32000
|
+
const requests = await adapter.stopRequestCapture();
|
|
32001
|
+
captureStarted = false;
|
|
32002
|
+
result.requestAssertions = buildRequestAssertions(step, requests, context);
|
|
32003
|
+
const mismatches = result.requestAssertions.filter((item) => item.actualCount !== item.count);
|
|
32004
|
+
if (mismatches.length > 0) {
|
|
32005
|
+
throw new Error(
|
|
32006
|
+
mismatches.map(
|
|
32007
|
+
(item) => `\u8BF7\u6C42\u6B21\u6570\u4E0D\u5339\u914D:${item.method ?? "*"} ${item.urlContains},\u5B9E\u9645 ${item.actualCount},\u671F\u671B ${item.count}`
|
|
32008
|
+
).join("; ")
|
|
32009
|
+
);
|
|
32010
|
+
}
|
|
32011
|
+
}
|
|
31159
32012
|
} catch (err) {
|
|
32013
|
+
if (captureStarted) {
|
|
32014
|
+
try {
|
|
32015
|
+
await adapter.stopRequestCapture?.();
|
|
32016
|
+
} catch {
|
|
32017
|
+
}
|
|
32018
|
+
captureStarted = false;
|
|
32019
|
+
}
|
|
31160
32020
|
result.status = "failed";
|
|
31161
32021
|
result.error = err instanceof Error ? err.message : String(err);
|
|
31162
32022
|
try {
|
|
@@ -31201,92 +32061,195 @@ function resolveDeep(value, context) {
|
|
|
31201
32061
|
function snippet(text, limit = SNIPPET_LIMIT) {
|
|
31202
32062
|
return text.length > limit ? `${text.slice(0, limit)}\u2026` : text;
|
|
31203
32063
|
}
|
|
32064
|
+
function buildRequestAssertions(step, requests, context) {
|
|
32065
|
+
return (step.expectRequests ?? []).map((expectation) => {
|
|
32066
|
+
const method = expectation.method?.toUpperCase();
|
|
32067
|
+
const urlContains = context.resolve(expectation.urlContains);
|
|
32068
|
+
const matches = requests.filter(
|
|
32069
|
+
(request) => (!method || request.method.toUpperCase() === method) && request.url.includes(urlContains)
|
|
32070
|
+
);
|
|
32071
|
+
return {
|
|
32072
|
+
method,
|
|
32073
|
+
urlContains: expectation.urlContains,
|
|
32074
|
+
count: expectation.count,
|
|
32075
|
+
actualCount: matches.length,
|
|
32076
|
+
windowMs: expectation.windowMs,
|
|
32077
|
+
requests: matches.slice(0, 20).map((request) => ({
|
|
32078
|
+
method: request.method.toUpperCase(),
|
|
32079
|
+
url: sanitizeRequestUrl(request.url)
|
|
32080
|
+
}))
|
|
32081
|
+
};
|
|
32082
|
+
});
|
|
32083
|
+
}
|
|
32084
|
+
function sanitizeRequestUrl(url2) {
|
|
32085
|
+
const queryIndex = url2.indexOf("?");
|
|
32086
|
+
const safe = queryIndex >= 0 ? `${url2.slice(0, queryIndex)}?<redacted>` : url2;
|
|
32087
|
+
return snippet(safe, 300);
|
|
32088
|
+
}
|
|
32089
|
+
function delay(milliseconds) {
|
|
32090
|
+
return new Promise((resolve7) => setTimeout(resolve7, milliseconds));
|
|
32091
|
+
}
|
|
31204
32092
|
|
|
31205
32093
|
// ../../packages/execution-engine/src/engine/execution-engine.ts
|
|
31206
32094
|
var ExecutionEngine = class {
|
|
31207
32095
|
async runCase(data, options) {
|
|
31208
|
-
const { runId, file: file2, artifacts, bus, resolver } = options;
|
|
32096
|
+
const { runId, file: file2, artifacts, bus, resolver, rowId, rowIndex } = options;
|
|
31209
32097
|
const startedAt = /* @__PURE__ */ new Date();
|
|
31210
32098
|
const context = new ExecutionContext();
|
|
32099
|
+
context.seed(options.initialVariables);
|
|
31211
32100
|
if (data.accountRef && options.accounts?.[data.accountRef] !== void 0) {
|
|
31212
32101
|
context.seedAccount(options.accounts[data.accountRef]);
|
|
31213
32102
|
}
|
|
31214
32103
|
const steps = [];
|
|
32104
|
+
const warnings = [];
|
|
31215
32105
|
const evidenceAdapters = /* @__PURE__ */ new Map();
|
|
31216
32106
|
let caseError;
|
|
31217
|
-
|
|
31218
|
-
await
|
|
31219
|
-
|
|
32107
|
+
const executionId = safeArtifactName(`${data.id}-${rowId}`);
|
|
32108
|
+
await bus.emit({
|
|
32109
|
+
type: "case-started",
|
|
32110
|
+
runId,
|
|
32111
|
+
caseId: data.id,
|
|
32112
|
+
rowId,
|
|
32113
|
+
file: file2,
|
|
32114
|
+
ts: startedAt.toISOString()
|
|
32115
|
+
});
|
|
32116
|
+
await artifacts.logger.info(`case ${data.id}#${rowId} started (${file2})`);
|
|
32117
|
+
if (options.missingDependencies.length > 0) {
|
|
32118
|
+
const finishedAt2 = /* @__PURE__ */ new Date();
|
|
32119
|
+
const skipped = {
|
|
32120
|
+
caseId: data.id,
|
|
32121
|
+
caseName: data.name,
|
|
32122
|
+
file: file2,
|
|
32123
|
+
rowId,
|
|
32124
|
+
rowIndex,
|
|
32125
|
+
status: "skipped",
|
|
32126
|
+
skipReason: "dependency-not-ready",
|
|
32127
|
+
missingDependencies: options.missingDependencies,
|
|
32128
|
+
steps,
|
|
32129
|
+
startedAt: startedAt.toISOString(),
|
|
32130
|
+
finishedAt: finishedAt2.toISOString(),
|
|
32131
|
+
durationMs: finishedAt2.getTime() - startedAt.getTime()
|
|
32132
|
+
};
|
|
31220
32133
|
await bus.emit({
|
|
31221
|
-
type: "
|
|
32134
|
+
type: "case-finished",
|
|
31222
32135
|
runId,
|
|
31223
32136
|
caseId: data.id,
|
|
31224
|
-
|
|
31225
|
-
|
|
31226
|
-
|
|
31227
|
-
ts:
|
|
32137
|
+
rowId,
|
|
32138
|
+
status: "skipped",
|
|
32139
|
+
durationMs: skipped.durationMs,
|
|
32140
|
+
ts: finishedAt2.toISOString()
|
|
31228
32141
|
});
|
|
31229
|
-
|
|
31230
|
-
|
|
31231
|
-
|
|
31232
|
-
|
|
31233
|
-
|
|
31234
|
-
|
|
31235
|
-
|
|
31236
|
-
|
|
31237
|
-
|
|
31238
|
-
|
|
31239
|
-
|
|
32142
|
+
await artifacts.logger.info(
|
|
32143
|
+
`case ${data.id}#${rowId} skipped: missing ${options.missingDependencies.join(", ")}`
|
|
32144
|
+
);
|
|
32145
|
+
return skipped;
|
|
32146
|
+
}
|
|
32147
|
+
const runPhase = async (phase, phaseSteps) => {
|
|
32148
|
+
let passed = true;
|
|
32149
|
+
for (const [index, step] of phaseSteps.entries()) {
|
|
32150
|
+
await bus.emit({
|
|
32151
|
+
type: "step-started",
|
|
32152
|
+
runId,
|
|
32153
|
+
caseId: data.id,
|
|
32154
|
+
rowId,
|
|
32155
|
+
phase,
|
|
32156
|
+
index,
|
|
32157
|
+
target: step.target,
|
|
32158
|
+
action: step.action,
|
|
32159
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
32160
|
+
});
|
|
32161
|
+
let result;
|
|
32162
|
+
try {
|
|
32163
|
+
const adapter = await (await resolver.session(step.target)).get();
|
|
32164
|
+
if (adapter.startEvidence && !evidenceAdapters.has(step.target)) {
|
|
32165
|
+
try {
|
|
32166
|
+
await adapter.startEvidence(executionId);
|
|
32167
|
+
evidenceAdapters.set(step.target, adapter);
|
|
32168
|
+
} catch (err) {
|
|
32169
|
+
await artifacts.logger.info(
|
|
32170
|
+
`evidence start failed for ${step.target}: ${err instanceof Error ? err.message : String(err)}`
|
|
32171
|
+
);
|
|
32172
|
+
}
|
|
31240
32173
|
}
|
|
32174
|
+
result = await executeStep(step, index, adapter, context, artifacts, {
|
|
32175
|
+
screenshotName: `${executionId}-${phase}-${String(index).padStart(2, "0")}`
|
|
32176
|
+
});
|
|
32177
|
+
} catch (err) {
|
|
32178
|
+
result = {
|
|
32179
|
+
index,
|
|
32180
|
+
target: step.target,
|
|
32181
|
+
action: step.action,
|
|
32182
|
+
status: "failed",
|
|
32183
|
+
durationMs: 0,
|
|
32184
|
+
error: err instanceof Error ? err.message : String(err)
|
|
32185
|
+
};
|
|
31241
32186
|
}
|
|
31242
|
-
result =
|
|
31243
|
-
|
|
31244
|
-
|
|
31245
|
-
|
|
31246
|
-
|
|
32187
|
+
result.phase = phase;
|
|
32188
|
+
if (phase === "teardown" && result.status === "failed") {
|
|
32189
|
+
result.status = "warning";
|
|
32190
|
+
warnings.push(result.error ?? `teardown step ${index} failed`);
|
|
32191
|
+
}
|
|
32192
|
+
steps.push(result);
|
|
32193
|
+
await artifacts.logger.info(
|
|
32194
|
+
`${phase} step ${index} ${step.action} -> ${result.status}${result.error ? ` - ${result.error}` : ""}`
|
|
32195
|
+
);
|
|
32196
|
+
await bus.emit({
|
|
32197
|
+
type: "step-finished",
|
|
32198
|
+
runId,
|
|
32199
|
+
caseId: data.id,
|
|
32200
|
+
rowId,
|
|
32201
|
+
phase,
|
|
31247
32202
|
index,
|
|
31248
32203
|
target: step.target,
|
|
31249
32204
|
action: step.action,
|
|
31250
|
-
status:
|
|
31251
|
-
durationMs:
|
|
31252
|
-
error:
|
|
31253
|
-
|
|
31254
|
-
|
|
31255
|
-
|
|
31256
|
-
|
|
31257
|
-
|
|
31258
|
-
|
|
31259
|
-
|
|
31260
|
-
|
|
31261
|
-
|
|
31262
|
-
|
|
31263
|
-
|
|
31264
|
-
|
|
31265
|
-
|
|
31266
|
-
|
|
31267
|
-
|
|
31268
|
-
|
|
31269
|
-
|
|
31270
|
-
caseError = result.error;
|
|
31271
|
-
for (const [skipIndex, skippedStep] of data.steps.entries()) {
|
|
31272
|
-
if (skipIndex <= index) continue;
|
|
31273
|
-
steps.push({
|
|
31274
|
-
index: skipIndex,
|
|
31275
|
-
target: skippedStep.target,
|
|
31276
|
-
action: skippedStep.action,
|
|
31277
|
-
status: "skipped",
|
|
31278
|
-
durationMs: 0
|
|
31279
|
-
});
|
|
32205
|
+
status: result.status,
|
|
32206
|
+
durationMs: result.durationMs,
|
|
32207
|
+
error: result.error,
|
|
32208
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
32209
|
+
});
|
|
32210
|
+
if (result.status === "failed") {
|
|
32211
|
+
passed = false;
|
|
32212
|
+
caseError = result.error;
|
|
32213
|
+
for (const [skipIndex, skippedStep] of phaseSteps.entries()) {
|
|
32214
|
+
if (skipIndex <= index) continue;
|
|
32215
|
+
steps.push({
|
|
32216
|
+
index: skipIndex,
|
|
32217
|
+
phase,
|
|
32218
|
+
target: skippedStep.target,
|
|
32219
|
+
action: skippedStep.action,
|
|
32220
|
+
status: "skipped",
|
|
32221
|
+
durationMs: 0
|
|
32222
|
+
});
|
|
32223
|
+
}
|
|
32224
|
+
break;
|
|
31280
32225
|
}
|
|
31281
|
-
|
|
32226
|
+
}
|
|
32227
|
+
return passed;
|
|
32228
|
+
};
|
|
32229
|
+
const setupPassed = await runPhase("setup", data.setup ?? []);
|
|
32230
|
+
if (setupPassed) {
|
|
32231
|
+
await runPhase("steps", data.steps);
|
|
32232
|
+
} else {
|
|
32233
|
+
for (const [index, step] of data.steps.entries()) {
|
|
32234
|
+
steps.push({
|
|
32235
|
+
index,
|
|
32236
|
+
phase: "steps",
|
|
32237
|
+
target: step.target,
|
|
32238
|
+
action: step.action,
|
|
32239
|
+
status: "skipped",
|
|
32240
|
+
durationMs: 0
|
|
32241
|
+
});
|
|
31282
32242
|
}
|
|
31283
32243
|
}
|
|
32244
|
+
await runPhase("teardown", data.teardown ?? []);
|
|
31284
32245
|
const finishedAt = /* @__PURE__ */ new Date();
|
|
31285
32246
|
const status = caseError ? "failed" : "passed";
|
|
31286
32247
|
const caseResult = {
|
|
31287
32248
|
caseId: data.id,
|
|
31288
32249
|
caseName: data.name,
|
|
31289
32250
|
file: file2,
|
|
32251
|
+
rowId,
|
|
32252
|
+
rowIndex,
|
|
31290
32253
|
status,
|
|
31291
32254
|
steps,
|
|
31292
32255
|
startedAt: startedAt.toISOString(),
|
|
@@ -31294,15 +32257,16 @@ var ExecutionEngine = class {
|
|
|
31294
32257
|
durationMs: finishedAt.getTime() - startedAt.getTime()
|
|
31295
32258
|
};
|
|
31296
32259
|
if (caseError) caseResult.error = caseError;
|
|
32260
|
+
if (warnings.length > 0) caseResult.warnings = warnings;
|
|
31297
32261
|
for (const [target, adapter] of evidenceAdapters) {
|
|
31298
32262
|
if (!adapter.stopEvidence) continue;
|
|
31299
32263
|
try {
|
|
31300
|
-
const evidence = await adapter.stopEvidence(
|
|
32264
|
+
const evidence = await adapter.stopEvidence(executionId);
|
|
31301
32265
|
if (evidence.video) {
|
|
31302
|
-
caseResult.video = await artifacts.saveVideo(`${
|
|
32266
|
+
caseResult.video = await artifacts.saveVideo(`${executionId}-${target}.webm`, evidence.video);
|
|
31303
32267
|
}
|
|
31304
32268
|
if (evidence.trace) {
|
|
31305
|
-
caseResult.trace = await artifacts.saveTrace(`${
|
|
32269
|
+
caseResult.trace = await artifacts.saveTrace(`${executionId}-${target}.zip`, evidence.trace);
|
|
31306
32270
|
}
|
|
31307
32271
|
} catch (err) {
|
|
31308
32272
|
await artifacts.logger.info(
|
|
@@ -31314,33 +32278,45 @@ var ExecutionEngine = class {
|
|
|
31314
32278
|
type: "case-finished",
|
|
31315
32279
|
runId,
|
|
31316
32280
|
caseId: data.id,
|
|
32281
|
+
rowId,
|
|
31317
32282
|
status,
|
|
31318
32283
|
durationMs: finishedAt.getTime() - startedAt.getTime(),
|
|
31319
32284
|
ts: finishedAt.toISOString()
|
|
31320
32285
|
});
|
|
31321
|
-
await artifacts.logger.info(`case ${data.id} ${status}`);
|
|
32286
|
+
await artifacts.logger.info(`case ${data.id}#${rowId} ${status}`);
|
|
31322
32287
|
return caseResult;
|
|
31323
32288
|
}
|
|
31324
32289
|
};
|
|
32290
|
+
function safeArtifactName(value) {
|
|
32291
|
+
return value.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "case";
|
|
32292
|
+
}
|
|
31325
32293
|
|
|
31326
32294
|
// ../../packages/execution-engine/src/engine/result-collector.ts
|
|
31327
32295
|
function buildRunSummary(runId, startedAt, cases, options = {}) {
|
|
31328
32296
|
const finishedAt = /* @__PURE__ */ new Date();
|
|
31329
32297
|
const totals = {
|
|
32298
|
+
templates: options.templates ?? new Set(cases.map((item) => item.caseId)).size,
|
|
31330
32299
|
cases: cases.length,
|
|
31331
32300
|
passed: cases.filter((item) => item.status === "passed").length,
|
|
31332
32301
|
failed: cases.filter((item) => item.status === "failed").length,
|
|
32302
|
+
skipped: cases.filter((item) => item.status === "skipped").length,
|
|
32303
|
+
warnings: 0,
|
|
31333
32304
|
steps: 0,
|
|
31334
32305
|
stepsPassed: 0,
|
|
31335
32306
|
stepsFailed: 0,
|
|
31336
|
-
stepsSkipped: 0
|
|
32307
|
+
stepsSkipped: 0,
|
|
32308
|
+
stepsWarning: 0
|
|
31337
32309
|
};
|
|
31338
32310
|
for (const item of cases) {
|
|
31339
32311
|
for (const step of item.steps) {
|
|
31340
32312
|
totals.steps++;
|
|
31341
32313
|
if (step.status === "passed") totals.stepsPassed++;
|
|
31342
32314
|
else if (step.status === "failed") totals.stepsFailed++;
|
|
31343
|
-
else totals.stepsSkipped++;
|
|
32315
|
+
else if (step.status === "skipped") totals.stepsSkipped++;
|
|
32316
|
+
else {
|
|
32317
|
+
totals.stepsWarning++;
|
|
32318
|
+
totals.warnings++;
|
|
32319
|
+
}
|
|
31344
32320
|
}
|
|
31345
32321
|
}
|
|
31346
32322
|
return {
|
|
@@ -31348,7 +32324,7 @@ function buildRunSummary(runId, startedAt, cases, options = {}) {
|
|
|
31348
32324
|
startedAt: startedAt.toISOString(),
|
|
31349
32325
|
finishedAt: finishedAt.toISOString(),
|
|
31350
32326
|
durationMs: finishedAt.getTime() - startedAt.getTime(),
|
|
31351
|
-
status: totals.failed > 0 ? "failed" : "passed",
|
|
32327
|
+
status: totals.failed > 0 ? "failed" : totals.passed > 0 || totals.cases === 0 ? "passed" : "skipped",
|
|
31352
32328
|
...options.cancelled ? { cancelled: true } : {},
|
|
31353
32329
|
cases,
|
|
31354
32330
|
totals
|
|
@@ -31356,8 +32332,8 @@ function buildRunSummary(runId, startedAt, cases, options = {}) {
|
|
|
31356
32332
|
}
|
|
31357
32333
|
|
|
31358
32334
|
// ../../packages/execution-engine/src/engine/test-runner.ts
|
|
31359
|
-
import { writeFile as
|
|
31360
|
-
import { join as
|
|
32335
|
+
import { writeFile as writeFile9 } from "node:fs/promises";
|
|
32336
|
+
import { join as join15, resolve as resolve6 } from "node:path";
|
|
31361
32337
|
var TestRunner = class {
|
|
31362
32338
|
resolver;
|
|
31363
32339
|
runsRoot;
|
|
@@ -31367,7 +32343,7 @@ var TestRunner = class {
|
|
|
31367
32343
|
accounts;
|
|
31368
32344
|
constructor(options) {
|
|
31369
32345
|
this.resolver = options.resolver;
|
|
31370
|
-
this.runsRoot =
|
|
32346
|
+
this.runsRoot = resolve6(options.runsRoot ?? join15(process.cwd(), ".testpilot", "artifacts", "runs"));
|
|
31371
32347
|
this.customRunId = options.runId;
|
|
31372
32348
|
this.extraSubscriber = options.onEvent;
|
|
31373
32349
|
this.signal = options.signal;
|
|
@@ -31380,7 +32356,7 @@ var TestRunner = class {
|
|
|
31380
32356
|
const artifacts = new ArtifactManager(runDir);
|
|
31381
32357
|
await artifacts.prepare();
|
|
31382
32358
|
const bus = new EventBus();
|
|
31383
|
-
bus.subscribe(ndjsonSubscriber(
|
|
32359
|
+
bus.subscribe(ndjsonSubscriber(join15(runDir, "events.ndjson")));
|
|
31384
32360
|
if (this.extraSubscriber) bus.subscribe(this.extraSubscriber);
|
|
31385
32361
|
await bus.emit({ type: "run-started", runId, totalCases: cases.length, ts: startedAt.toISOString() });
|
|
31386
32362
|
await artifacts.logger.info(`run ${runId} started with ${cases.length} case(s)`);
|
|
@@ -31395,6 +32371,11 @@ var TestRunner = class {
|
|
|
31395
32371
|
await engine.runCase(item.data, {
|
|
31396
32372
|
runId,
|
|
31397
32373
|
file: item.file,
|
|
32374
|
+
templateId: item.templateId ?? item.data.id,
|
|
32375
|
+
rowId: item.rowId ?? "default",
|
|
32376
|
+
rowIndex: item.rowIndex ?? 0,
|
|
32377
|
+
initialVariables: item.initialVariables ?? {},
|
|
32378
|
+
missingDependencies: item.missingDependencies ?? [],
|
|
31398
32379
|
artifacts,
|
|
31399
32380
|
bus,
|
|
31400
32381
|
resolver: this.resolver,
|
|
@@ -31404,7 +32385,8 @@ var TestRunner = class {
|
|
|
31404
32385
|
}
|
|
31405
32386
|
const cancelled = this.signal?.aborted === true;
|
|
31406
32387
|
await this.resolver.closeAll();
|
|
31407
|
-
const
|
|
32388
|
+
const templates = new Set(cases.map((item) => item.templateId ?? item.data.id)).size;
|
|
32389
|
+
const summary = buildRunSummary(runId, startedAt, results, { cancelled, templates });
|
|
31408
32390
|
await bus.emit({
|
|
31409
32391
|
type: "run-finished",
|
|
31410
32392
|
runId,
|
|
@@ -31413,7 +32395,7 @@ var TestRunner = class {
|
|
|
31413
32395
|
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
31414
32396
|
});
|
|
31415
32397
|
await artifacts.logger.info(`run ${runId} finished: ${summary.status}`);
|
|
31416
|
-
await
|
|
32398
|
+
await writeFile9(join15(runDir, RESULT_FILE), JSON.stringify(summary, null, 2), "utf8");
|
|
31417
32399
|
return summary;
|
|
31418
32400
|
}
|
|
31419
32401
|
};
|
|
@@ -31446,10 +32428,10 @@ async function openCaseContext(bundle, options, evidenceTmpDir) {
|
|
|
31446
32428
|
}
|
|
31447
32429
|
|
|
31448
32430
|
// ../../packages/adapter-playwright/src/page.ts
|
|
31449
|
-
import { existsSync as
|
|
31450
|
-
import { mkdtemp, readFile as
|
|
32431
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
32432
|
+
import { mkdtemp, readFile as readFile6, rm } from "node:fs/promises";
|
|
31451
32433
|
import { tmpdir } from "node:os";
|
|
31452
|
-
import { join as
|
|
32434
|
+
import { join as join16 } from "node:path";
|
|
31453
32435
|
var PlaywrightAdapter = class {
|
|
31454
32436
|
constructor(bundle, options = {}) {
|
|
31455
32437
|
this.bundle = bundle;
|
|
@@ -31457,13 +32439,14 @@ var PlaywrightAdapter = class {
|
|
|
31457
32439
|
}
|
|
31458
32440
|
target = "web";
|
|
31459
32441
|
current;
|
|
32442
|
+
requestCapture;
|
|
31460
32443
|
/** 浏览器在 create() 时已启动;context/page 按用例惰性创建,launch 保持幂等 */
|
|
31461
32444
|
async launch() {
|
|
31462
32445
|
}
|
|
31463
32446
|
/** 开始用例级取证:为本用例创建独立 context(录屏 + trace) */
|
|
31464
32447
|
async startEvidence(_caseId) {
|
|
31465
32448
|
await this.closeContextQuietly();
|
|
31466
|
-
const evidenceTmpDir = await mkdtemp(
|
|
32449
|
+
const evidenceTmpDir = await mkdtemp(join16(tmpdir(), "testpilot-pw-"));
|
|
31467
32450
|
this.current = {
|
|
31468
32451
|
...await openCaseContext(this.bundle, this.options, evidenceTmpDir),
|
|
31469
32452
|
evidenceTmpDir
|
|
@@ -31471,18 +32454,36 @@ var PlaywrightAdapter = class {
|
|
|
31471
32454
|
}
|
|
31472
32455
|
/** 结束取证:trace 落 zip、录屏在 context 关闭后完成,返回二者字节 */
|
|
31473
32456
|
async stopEvidence(_caseId) {
|
|
32457
|
+
await this.stopRequestCapture().catch(() => []);
|
|
31474
32458
|
if (!this.current) return {};
|
|
31475
32459
|
const { context, page, evidenceTmpDir } = this.current;
|
|
31476
32460
|
this.current = void 0;
|
|
31477
|
-
const traceZipPath =
|
|
32461
|
+
const traceZipPath = join16(evidenceTmpDir ?? tmpdir(), "trace.zip");
|
|
31478
32462
|
await context.tracing.stop({ path: traceZipPath }).catch(() => void 0);
|
|
31479
32463
|
const recordedVideoPath = await page.video()?.path().catch(() => void 0);
|
|
31480
32464
|
await context.close().catch(() => void 0);
|
|
31481
|
-
const video = recordedVideoPath &&
|
|
31482
|
-
const trace = evidenceTmpDir &&
|
|
32465
|
+
const video = recordedVideoPath && existsSync3(recordedVideoPath) ? await readFile6(recordedVideoPath) : void 0;
|
|
32466
|
+
const trace = evidenceTmpDir && existsSync3(traceZipPath) ? await readFile6(traceZipPath) : void 0;
|
|
31483
32467
|
if (evidenceTmpDir) await rm(evidenceTmpDir, { recursive: true, force: true }).catch(() => void 0);
|
|
31484
32468
|
return { video, trace };
|
|
31485
32469
|
}
|
|
32470
|
+
async startRequestCapture() {
|
|
32471
|
+
if (this.requestCapture) await this.stopRequestCapture();
|
|
32472
|
+
const page = await this.ensurePage();
|
|
32473
|
+
const requests = [];
|
|
32474
|
+
const listener = (request) => {
|
|
32475
|
+
requests.push({ method: request.method(), url: request.url() });
|
|
32476
|
+
};
|
|
32477
|
+
page.on("request", listener);
|
|
32478
|
+
this.requestCapture = { page, listener, requests };
|
|
32479
|
+
}
|
|
32480
|
+
async stopRequestCapture() {
|
|
32481
|
+
if (!this.requestCapture) return [];
|
|
32482
|
+
const capture = this.requestCapture;
|
|
32483
|
+
this.requestCapture = void 0;
|
|
32484
|
+
capture.page.off("request", capture.listener);
|
|
32485
|
+
return capture.requests;
|
|
32486
|
+
}
|
|
31486
32487
|
async navigate(url2) {
|
|
31487
32488
|
const options = this.options;
|
|
31488
32489
|
const page = await this.ensurePage();
|
|
@@ -31544,6 +32545,7 @@ var PlaywrightAdapter = class {
|
|
|
31544
32545
|
throw new Error(`\u65E0\u6548\u7684 locator:${JSON.stringify(locator)}`);
|
|
31545
32546
|
}
|
|
31546
32547
|
async closeContextQuietly() {
|
|
32548
|
+
await this.stopRequestCapture().catch(() => []);
|
|
31547
32549
|
if (!this.current) return;
|
|
31548
32550
|
const { context } = this.current;
|
|
31549
32551
|
this.current = void 0;
|
|
@@ -31582,8 +32584,8 @@ var automatorLauncher = {
|
|
|
31582
32584
|
}
|
|
31583
32585
|
}
|
|
31584
32586
|
};
|
|
31585
|
-
function
|
|
31586
|
-
return new Promise((
|
|
32587
|
+
function delay2(ms) {
|
|
32588
|
+
return new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
31587
32589
|
}
|
|
31588
32590
|
async function connectWithRetry(wsEndpoint, timeoutMs) {
|
|
31589
32591
|
const startedAt = Date.now();
|
|
@@ -31593,7 +32595,7 @@ async function connectWithRetry(wsEndpoint, timeoutMs) {
|
|
|
31593
32595
|
return await automator.connect({ wsEndpoint });
|
|
31594
32596
|
} catch (err) {
|
|
31595
32597
|
lastError = err;
|
|
31596
|
-
await
|
|
32598
|
+
await delay2(1e3);
|
|
31597
32599
|
}
|
|
31598
32600
|
}
|
|
31599
32601
|
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
@@ -31644,6 +32646,7 @@ var MiniAppAdapter = class {
|
|
|
31644
32646
|
}
|
|
31645
32647
|
target = "miniapp";
|
|
31646
32648
|
program;
|
|
32649
|
+
requestCapture;
|
|
31647
32650
|
/** 拉起开发者工具并建立自动化连接;需幂等 */
|
|
31648
32651
|
async launch() {
|
|
31649
32652
|
if (this.program) return;
|
|
@@ -31664,7 +32667,7 @@ var MiniAppAdapter = class {
|
|
|
31664
32667
|
return;
|
|
31665
32668
|
} catch (err) {
|
|
31666
32669
|
lastError = err;
|
|
31667
|
-
await
|
|
32670
|
+
await delay2(3e3);
|
|
31668
32671
|
}
|
|
31669
32672
|
}
|
|
31670
32673
|
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
@@ -31710,7 +32713,43 @@ var MiniAppAdapter = class {
|
|
|
31710
32713
|
return Buffer.from(base643, "base64");
|
|
31711
32714
|
});
|
|
31712
32715
|
}
|
|
32716
|
+
async startRequestCapture() {
|
|
32717
|
+
if (this.requestCapture) await this.stopRequestCapture();
|
|
32718
|
+
const program2 = await this.getProgram();
|
|
32719
|
+
const requests = [];
|
|
32720
|
+
const exposedName = `__testpilotRequest_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
32721
|
+
await program2.exposeFunction(exposedName, (value) => {
|
|
32722
|
+
if (value === null || typeof value !== "object") return;
|
|
32723
|
+
const request = value;
|
|
32724
|
+
if (typeof request.url !== "string") return;
|
|
32725
|
+
requests.push({
|
|
32726
|
+
method: typeof request.method === "string" ? request.method : "GET",
|
|
32727
|
+
url: request.url
|
|
32728
|
+
});
|
|
32729
|
+
});
|
|
32730
|
+
await program2.mockWxMethod(
|
|
32731
|
+
"request",
|
|
32732
|
+
function(options, callbackName) {
|
|
32733
|
+
const callbacks = globalThis;
|
|
32734
|
+
callbacks[callbackName]?.({
|
|
32735
|
+
method: typeof options.method === "string" ? options.method : "GET",
|
|
32736
|
+
url: typeof options.url === "string" ? options.url : ""
|
|
32737
|
+
});
|
|
32738
|
+
return this.origin(options);
|
|
32739
|
+
},
|
|
32740
|
+
exposedName
|
|
32741
|
+
);
|
|
32742
|
+
this.requestCapture = { program: program2, exposedName, requests };
|
|
32743
|
+
}
|
|
32744
|
+
async stopRequestCapture() {
|
|
32745
|
+
if (!this.requestCapture) return [];
|
|
32746
|
+
const capture = this.requestCapture;
|
|
32747
|
+
this.requestCapture = void 0;
|
|
32748
|
+
await capture.program.restoreWxMethod("request");
|
|
32749
|
+
return capture.requests;
|
|
32750
|
+
}
|
|
31713
32751
|
async close() {
|
|
32752
|
+
await this.stopRequestCapture().catch(() => []);
|
|
31714
32753
|
if (!this.program) return;
|
|
31715
32754
|
const program2 = this.program;
|
|
31716
32755
|
this.program = void 0;
|
|
@@ -31758,17 +32797,17 @@ var MiniAppAdapter = class {
|
|
|
31758
32797
|
if (Date.now() - startedAt > ELEMENT_WAIT_MS) {
|
|
31759
32798
|
throw new Error(`\u7B49\u5F85\u5143\u7D20\u8D85\u65F6(${ELEMENT_WAIT_MS}ms):${selector}`);
|
|
31760
32799
|
}
|
|
31761
|
-
await
|
|
32800
|
+
await delay2(500);
|
|
31762
32801
|
}
|
|
31763
32802
|
}
|
|
31764
32803
|
};
|
|
31765
32804
|
|
|
31766
32805
|
// ../../packages/adapter-wechatide/src/project.ts
|
|
31767
|
-
import { existsSync as
|
|
32806
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
31768
32807
|
async function resolveMiniappConfig(input2) {
|
|
31769
32808
|
const projectPath = input2.projectPath ?? process.env.TESTPILOT_MINIPROGRAM_PROJECT_PATH;
|
|
31770
32809
|
if (!projectPath) return void 0;
|
|
31771
|
-
if (!
|
|
32810
|
+
if (!existsSync4(projectPath)) {
|
|
31772
32811
|
throw new Error(`\u5C0F\u7A0B\u5E8F\u9879\u76EE\u8DEF\u5F84\u4E0D\u5B58\u5728:${projectPath}`);
|
|
31773
32812
|
}
|
|
31774
32813
|
const cliPath = input2.cliPath ?? process.env.WECHAT_DEVTOOLS_CLI ?? await findWechatDevToolsCli();
|
|
@@ -31943,7 +32982,7 @@ function createDefaultAdapters(config2) {
|
|
|
31943
32982
|
async function runCases(options = {}) {
|
|
31944
32983
|
const root = resolveRoot(options.root);
|
|
31945
32984
|
const config2 = await loadProjectConfig(root);
|
|
31946
|
-
const inputs = options.paths && options.paths.length > 0 ? options.paths.map((input2) =>
|
|
32985
|
+
const inputs = options.paths && options.paths.length > 0 ? options.paths.map((input2) => isAbsolute3(input2) ? input2 : join17(root, input2)) : [join17(root, config2.casesDir)];
|
|
31947
32986
|
const cases = await collectCases(inputs, { root });
|
|
31948
32987
|
if (cases.length === 0) {
|
|
31949
32988
|
throw new RunError("ENO_CASES", `\u672A\u627E\u5230\u7528\u4F8B\u6587\u4EF6(\u67E5\u627E:${inputs.join(", ")})`);
|
|
@@ -31969,26 +33008,41 @@ async function runCases(options = {}) {
|
|
|
31969
33008
|
];
|
|
31970
33009
|
const runner = new TestRunner({
|
|
31971
33010
|
resolver,
|
|
31972
|
-
runsRoot:
|
|
33011
|
+
runsRoot: join17(root, RUNS_DIR),
|
|
31973
33012
|
runId: options.runId,
|
|
31974
33013
|
onEvent: options.onEvent,
|
|
31975
33014
|
signal: options.signal,
|
|
31976
33015
|
accounts: options.accounts
|
|
31977
33016
|
});
|
|
33017
|
+
const prepared = (await Promise.all(
|
|
33018
|
+
runnable.map(
|
|
33019
|
+
(item) => prepareCaseExecutions(
|
|
33020
|
+
{ data: item.case, file: item.file },
|
|
33021
|
+
{
|
|
33022
|
+
root,
|
|
33023
|
+
dataDir: join17(root, "tests", "e2e", "data"),
|
|
33024
|
+
variables: config2.variables,
|
|
33025
|
+
accounts: options.accounts
|
|
33026
|
+
}
|
|
33027
|
+
)
|
|
33028
|
+
)
|
|
33029
|
+
)).flat();
|
|
31978
33030
|
const summary = await runner.run(
|
|
31979
|
-
|
|
33031
|
+
prepared.map(
|
|
33032
|
+
(item) => item
|
|
33033
|
+
)
|
|
31980
33034
|
);
|
|
31981
33035
|
return { summary, invalid, tagFiltered, missingAccounts };
|
|
31982
33036
|
}
|
|
31983
33037
|
function runsRoot(root) {
|
|
31984
|
-
return
|
|
33038
|
+
return join17(resolveRoot(root), RUNS_DIR);
|
|
31985
33039
|
}
|
|
31986
33040
|
async function listRuns(options = {}) {
|
|
31987
33041
|
const base = runsRoot(options.root);
|
|
31988
|
-
const entries = await
|
|
33042
|
+
const entries = await readdir4(base).catch(() => []);
|
|
31989
33043
|
const metas = [];
|
|
31990
33044
|
for (const entry of entries.filter((name) => /^\d{8}-\d{3}$/.test(name)).sort().reverse()) {
|
|
31991
|
-
metas.push({ ...await readRunMeta(base, entry), runId: entry, dir:
|
|
33045
|
+
metas.push({ ...await readRunMeta(base, entry), runId: entry, dir: join17(base, entry) });
|
|
31992
33046
|
}
|
|
31993
33047
|
return metas;
|
|
31994
33048
|
}
|
|
@@ -31998,22 +33052,22 @@ async function latestRunId(options = {}) {
|
|
|
31998
33052
|
}
|
|
31999
33053
|
async function getRun(runId, options = {}) {
|
|
32000
33054
|
const base = runsRoot(options.root);
|
|
32001
|
-
const file2 =
|
|
33055
|
+
const file2 = join17(base, runId, RESULT_FILE);
|
|
32002
33056
|
try {
|
|
32003
|
-
return JSON.parse(await
|
|
33057
|
+
return JSON.parse(await readFile7(file2, "utf8"));
|
|
32004
33058
|
} catch {
|
|
32005
33059
|
throw new RunError("ENO_CASES", `\u8BFB\u53D6\u8FD0\u884C\u7ED3\u679C\u5931\u8D25:${file2}(\u8FD0\u884C\u672A\u5B8C\u6210\u6216 runId \u4E0D\u5B58\u5728)`);
|
|
32006
33060
|
}
|
|
32007
33061
|
}
|
|
32008
33062
|
async function getRunEvents(runId, options = {}) {
|
|
32009
|
-
const file2 =
|
|
32010
|
-
const source = await
|
|
33063
|
+
const file2 = join17(runsRoot(options.root), runId, "events.ndjson");
|
|
33064
|
+
const source = await readFile7(file2, "utf8").catch(() => "");
|
|
32011
33065
|
return source.split("\n").filter((line) => line.trim().length > 0).map((line) => JSON.parse(line));
|
|
32012
33066
|
}
|
|
32013
33067
|
async function readRunMeta(base, runId) {
|
|
32014
33068
|
try {
|
|
32015
33069
|
const summary = JSON.parse(
|
|
32016
|
-
await
|
|
33070
|
+
await readFile7(join17(base, runId, RESULT_FILE), "utf8")
|
|
32017
33071
|
);
|
|
32018
33072
|
return {
|
|
32019
33073
|
status: summary.status,
|
|
@@ -32034,25 +33088,47 @@ async function generateReport(runId, options = {}) {
|
|
|
32034
33088
|
if (!id) {
|
|
32035
33089
|
throw new Error("\u672A\u627E\u5230\u8FD0\u884C\u8BB0\u5F55(\u5148\u8FD0\u884C\u4E00\u6B21\u7528\u4F8B)");
|
|
32036
33090
|
}
|
|
32037
|
-
const runDir =
|
|
33091
|
+
const runDir = join18(resolveRoot(options.root), RUNS_DIR, id);
|
|
32038
33092
|
let summary;
|
|
32039
33093
|
try {
|
|
32040
|
-
summary = JSON.parse(await
|
|
33094
|
+
summary = JSON.parse(await readFile8(join18(runDir, "result.json"), "utf8"));
|
|
32041
33095
|
} catch {
|
|
32042
|
-
throw new Error(`\u8BFB\u53D6\u8FD0\u884C\u7ED3\u679C\u5931\u8D25:${
|
|
33096
|
+
throw new Error(`\u8BFB\u53D6\u8FD0\u884C\u7ED3\u679C\u5931\u8D25:${join18(runDir, "result.json")}(\u8FD0\u884C\u672A\u5B8C\u6210\u6216 runId \u4E0D\u5B58\u5728)`);
|
|
32043
33097
|
}
|
|
32044
33098
|
const written = await writeReports(runDir, summary);
|
|
32045
33099
|
return { runId: id, json: written.json, html: written.html };
|
|
32046
33100
|
}
|
|
33101
|
+
async function generateSummaryReport(options = {}) {
|
|
33102
|
+
const root = resolveRoot(options.root);
|
|
33103
|
+
const limit = options.last && options.last > 0 ? options.last : 30;
|
|
33104
|
+
const metas = await listRuns({ root });
|
|
33105
|
+
if (metas.length === 0) {
|
|
33106
|
+
throw new Error("\u7EDF\u8BA1\u7A97\u53E3\u5185\u6CA1\u6709\u4EFB\u4F55\u8FD0\u884C\u8BB0\u5F55(\u5148\u8FD0\u884C npx csspilot run)");
|
|
33107
|
+
}
|
|
33108
|
+
const runs = [];
|
|
33109
|
+
for (const meta3 of metas.slice(0, limit)) {
|
|
33110
|
+
try {
|
|
33111
|
+
runs.push({ runId: meta3.runId, summary: await getRun(meta3.runId, { root }) });
|
|
33112
|
+
} catch {
|
|
33113
|
+
}
|
|
33114
|
+
}
|
|
33115
|
+
if (runs.length === 0) {
|
|
33116
|
+
throw new Error("\u7EDF\u8BA1\u7A97\u53E3\u5185\u7684\u8FD0\u884C\u7ED3\u679C\u5747\u4E0D\u53EF\u8BFB");
|
|
33117
|
+
}
|
|
33118
|
+
const data = buildSummaryData(runs);
|
|
33119
|
+
const artifactsDir = join18(root, ".testpilot", "artifacts");
|
|
33120
|
+
const written = await writeSummaryReport(artifactsDir, data);
|
|
33121
|
+
return { html: written.html, json: written.json, data, window: { runs: runs.length } };
|
|
33122
|
+
}
|
|
32047
33123
|
async function readReport(runId, options = {}) {
|
|
32048
|
-
const file2 =
|
|
32049
|
-
const source = await
|
|
33124
|
+
const file2 = join18(resolveRoot(options.root), RUNS_DIR, runId, "report.json");
|
|
33125
|
+
const source = await readFile8(file2, "utf8").catch(() => void 0);
|
|
32050
33126
|
return source ? JSON.parse(source) : void 0;
|
|
32051
33127
|
}
|
|
32052
33128
|
|
|
32053
33129
|
// ../../packages/sdk/src/client.ts
|
|
32054
33130
|
function resolveInput(root, input2) {
|
|
32055
|
-
return
|
|
33131
|
+
return isAbsolute4(input2) ? input2 : join19(root, input2);
|
|
32056
33132
|
}
|
|
32057
33133
|
var TestPilotClient = class {
|
|
32058
33134
|
/** 业务项目根目录(绝对路径) */
|
|
@@ -32117,6 +33193,10 @@ var TestPilotClient = class {
|
|
|
32117
33193
|
generateReport(runId) {
|
|
32118
33194
|
return generateReport(runId, { root: this.root });
|
|
32119
33195
|
}
|
|
33196
|
+
/** 生成跨 run 汇总报告(summary.html + summary.json) */
|
|
33197
|
+
generateSummaryReport(options = {}) {
|
|
33198
|
+
return generateSummaryReport({ ...options, root: this.root });
|
|
33199
|
+
}
|
|
32120
33200
|
/** 读取已生成的报告 */
|
|
32121
33201
|
readReport(runId) {
|
|
32122
33202
|
return readReport(runId, { root: this.root });
|
|
@@ -32124,13 +33204,13 @@ var TestPilotClient = class {
|
|
|
32124
33204
|
};
|
|
32125
33205
|
|
|
32126
33206
|
// src/commands/init/adapter-detector.ts
|
|
32127
|
-
import { existsSync as
|
|
32128
|
-
import { join as
|
|
33207
|
+
import { existsSync as existsSync5, readFileSync } from "node:fs";
|
|
33208
|
+
import { join as join20 } from "node:path";
|
|
32129
33209
|
async function detectAdapters(root, projectType) {
|
|
32130
33210
|
const required2 = projectType === "hybrid" ? ["playwright", "wechatide"] : projectType === "wechat-miniapp" ? ["wechatide"] : projectType === "web" ? ["playwright"] : [];
|
|
32131
33211
|
const pkg = readPackageJsonDeps(root);
|
|
32132
33212
|
const hasPlaywrightDep = pkg !== void 0 && ("playwright" in pkg || "@playwright/test" in pkg);
|
|
32133
|
-
const playwrightInstalled = hasPlaywrightDep ||
|
|
33213
|
+
const playwrightInstalled = hasPlaywrightDep || existsSync5(join20(root, "node_modules", "playwright"));
|
|
32134
33214
|
const devtoolsCli = await findWechatDevToolsCli().catch(() => void 0);
|
|
32135
33215
|
return [
|
|
32136
33216
|
{
|
|
@@ -32148,8 +33228,8 @@ async function detectAdapters(root, projectType) {
|
|
|
32148
33228
|
];
|
|
32149
33229
|
}
|
|
32150
33230
|
function readPackageJsonDeps(root) {
|
|
32151
|
-
const file2 =
|
|
32152
|
-
if (!
|
|
33231
|
+
const file2 = join20(root, "package.json");
|
|
33232
|
+
if (!existsSync5(file2)) return void 0;
|
|
32153
33233
|
try {
|
|
32154
33234
|
const pkg = JSON.parse(readFileSync(file2, "utf8"));
|
|
32155
33235
|
return { ...pkg.dependencies, ...pkg.devDependencies };
|
|
@@ -32159,13 +33239,13 @@ function readPackageJsonDeps(root) {
|
|
|
32159
33239
|
}
|
|
32160
33240
|
|
|
32161
33241
|
// src/commands/init/environment-initializer.ts
|
|
32162
|
-
import { readFile as
|
|
32163
|
-
import { join as
|
|
33242
|
+
import { readFile as readFile9 } from "node:fs/promises";
|
|
33243
|
+
import { join as join21 } from "node:path";
|
|
32164
33244
|
var ENV_REF_PATTERN = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
32165
33245
|
async function collectEnvVarRefs(root) {
|
|
32166
33246
|
let text;
|
|
32167
33247
|
try {
|
|
32168
|
-
text = await
|
|
33248
|
+
text = await readFile9(join21(root, "testpilot.yaml"), "utf8");
|
|
32169
33249
|
} catch {
|
|
32170
33250
|
return [];
|
|
32171
33251
|
}
|
|
@@ -32181,8 +33261,8 @@ async function collectEnvVarRefs(root) {
|
|
|
32181
33261
|
}
|
|
32182
33262
|
|
|
32183
33263
|
// src/commands/init/project-detector.ts
|
|
32184
|
-
import { existsSync as
|
|
32185
|
-
import { basename, join as
|
|
33264
|
+
import { existsSync as existsSync6, readFileSync as readFileSync2 } from "node:fs";
|
|
33265
|
+
import { basename, join as join22 } from "node:path";
|
|
32186
33266
|
|
|
32187
33267
|
// src/lib/git.ts
|
|
32188
33268
|
import { execFile as execFile2 } from "node:child_process";
|
|
@@ -32219,8 +33299,8 @@ var WEB_DEP_MARKERS = [
|
|
|
32219
33299
|
];
|
|
32220
33300
|
var E2E_DIR_CANDIDATES = ["tests/e2e", "test/e2e", "e2e", "cypress"];
|
|
32221
33301
|
function readPackageJson(root) {
|
|
32222
|
-
const file2 =
|
|
32223
|
-
if (!
|
|
33302
|
+
const file2 = join22(root, "package.json");
|
|
33303
|
+
if (!existsSync6(file2)) return void 0;
|
|
32224
33304
|
try {
|
|
32225
33305
|
return JSON.parse(readFileSync2(file2, "utf8"));
|
|
32226
33306
|
} catch {
|
|
@@ -32230,8 +33310,8 @@ function readPackageJson(root) {
|
|
|
32230
33310
|
function detectProjectType(root) {
|
|
32231
33311
|
const pkg = readPackageJson(root);
|
|
32232
33312
|
const deps = { ...pkg?.dependencies, ...pkg?.devDependencies };
|
|
32233
|
-
const isWechat =
|
|
32234
|
-
const isWeb =
|
|
33313
|
+
const isWechat = existsSync6(join22(root, "project.config.json")) || existsSync6(join22(root, "app.json")) || existsSync6(join22(root, "miniprogram", "app.json"));
|
|
33314
|
+
const isWeb = existsSync6(join22(root, "index.html")) || WEB_DEP_MARKERS.some((marker) => deps?.[marker] !== void 0) || [".ts", ".tsx", ".js", ".jsx"].some((ext) => existsSync6(join22(root, "src", `index${ext}`)));
|
|
32235
33315
|
if (isWechat && isWeb) return "hybrid";
|
|
32236
33316
|
if (isWechat) return "wechat-miniapp";
|
|
32237
33317
|
if (isWeb) return "web";
|
|
@@ -32240,9 +33320,9 @@ function detectProjectType(root) {
|
|
|
32240
33320
|
async function detectProject(root) {
|
|
32241
33321
|
const pkg = readPackageJson(root);
|
|
32242
33322
|
const type = detectProjectType(root);
|
|
32243
|
-
const language =
|
|
32244
|
-
const packageManager =
|
|
32245
|
-
const existingE2eDirs = E2E_DIR_CANDIDATES.filter((dir) =>
|
|
33323
|
+
const language = existsSync6(join22(root, "tsconfig.json")) ? "TypeScript" : pkg || existsSync6(join22(root, "src")) ? "JavaScript" : "Unknown";
|
|
33324
|
+
const packageManager = existsSync6(join22(root, "pnpm-lock.yaml")) ? "pnpm" : existsSync6(join22(root, "yarn.lock")) ? "yarn" : existsSync6(join22(root, "package-lock.json")) ? "npm" : "npx";
|
|
33325
|
+
const existingE2eDirs = E2E_DIR_CANDIDATES.filter((dir) => existsSync6(join22(root, dir)));
|
|
32246
33326
|
return {
|
|
32247
33327
|
root,
|
|
32248
33328
|
name: pkg?.name ?? basename(root),
|
|
@@ -32252,8 +33332,8 @@ async function detectProject(root) {
|
|
|
32252
33332
|
git: await readGitInfo(root),
|
|
32253
33333
|
hasTestsE2e: existingE2eDirs.includes("tests/e2e"),
|
|
32254
33334
|
existingE2eDirs,
|
|
32255
|
-
hasTestpilotConfig:
|
|
32256
|
-
hasProjectLink:
|
|
33335
|
+
hasTestpilotConfig: existsSync6(join22(root, "testpilot.yaml")),
|
|
33336
|
+
hasProjectLink: existsSync6(join22(root, ".testpilot", "project.json"))
|
|
32257
33337
|
};
|
|
32258
33338
|
}
|
|
32259
33339
|
function defaultAdapterFor(type) {
|
|
@@ -32268,9 +33348,9 @@ function adaptersFor(type) {
|
|
|
32268
33348
|
}
|
|
32269
33349
|
|
|
32270
33350
|
// src/commands/init/skill-installer.ts
|
|
32271
|
-
import { existsSync as
|
|
32272
|
-
import { access as access2, cp, mkdir as
|
|
32273
|
-
import { dirname as
|
|
33351
|
+
import { existsSync as existsSync7, readFileSync as readFileSync3 } from "node:fs";
|
|
33352
|
+
import { access as access2, cp, mkdir as mkdir12, writeFile as writeFile10 } from "node:fs/promises";
|
|
33353
|
+
import { dirname as dirname9, join as join23 } from "node:path";
|
|
32274
33354
|
import { fileURLToPath } from "node:url";
|
|
32275
33355
|
var CORE_FILES = ["SKILL.md", "manifest.yaml"];
|
|
32276
33356
|
var CORE_DIRS = ["rules", "workflows"];
|
|
@@ -32283,39 +33363,39 @@ async function exists3(path) {
|
|
|
32283
33363
|
}
|
|
32284
33364
|
}
|
|
32285
33365
|
function readManifestVersion(dir) {
|
|
32286
|
-
const manifestPath =
|
|
32287
|
-
if (!
|
|
33366
|
+
const manifestPath = join23(dir, "manifest.yaml");
|
|
33367
|
+
if (!existsSync7(manifestPath)) return void 0;
|
|
32288
33368
|
return /^version:\s*([^\s#]+)/m.exec(readFileSync3(manifestPath, "utf8"))?.[1];
|
|
32289
33369
|
}
|
|
32290
33370
|
function resolveSkillSourceDir() {
|
|
32291
33371
|
const fromEnv = process.env.TESTPILOT_SKILL_DIR;
|
|
32292
33372
|
if (fromEnv) return fromEnv;
|
|
32293
33373
|
const bundled = fileURLToPath(new URL("./skills/testpilot", import.meta.url));
|
|
32294
|
-
if (
|
|
33374
|
+
if (existsSync7(bundled)) return bundled;
|
|
32295
33375
|
const inRepo = fileURLToPath(new URL("../../../../../skills/testpilot", import.meta.url));
|
|
32296
|
-
return
|
|
33376
|
+
return existsSync7(inRepo) ? inRepo : void 0;
|
|
32297
33377
|
}
|
|
32298
33378
|
async function copyCoreFiles(src, dest) {
|
|
32299
33379
|
for (const name of CORE_FILES) {
|
|
32300
|
-
await cp(
|
|
33380
|
+
await cp(join23(src, name), join23(dest, name), { recursive: true, force: true });
|
|
32301
33381
|
}
|
|
32302
33382
|
for (const dir of CORE_DIRS) {
|
|
32303
|
-
if (
|
|
32304
|
-
await cp(
|
|
33383
|
+
if (existsSync7(join23(src, dir))) {
|
|
33384
|
+
await cp(join23(src, dir), join23(dest, dir), { recursive: true, force: true });
|
|
32305
33385
|
}
|
|
32306
33386
|
}
|
|
32307
33387
|
}
|
|
32308
33388
|
async function syncSkillFiles(root, options = {}) {
|
|
32309
|
-
const dest =
|
|
33389
|
+
const dest = join23(root, ".agents", "skills", "testpilot");
|
|
32310
33390
|
const bundledVersion = readManifestVersion(resolveSkillSourceDir() ?? "");
|
|
32311
|
-
if (!await exists3(
|
|
33391
|
+
if (!await exists3(join23(dest, "manifest.yaml"))) {
|
|
32312
33392
|
const src = resolveSkillSourceDir();
|
|
32313
33393
|
if (!src) {
|
|
32314
33394
|
throw new Error(
|
|
32315
33395
|
"\u65E0\u6CD5\u5B9A\u4F4D TestPilot Skill \u6E90\u76EE\u5F55;\u53EF\u901A\u8FC7\u73AF\u5883\u53D8\u91CF TESTPILOT_SKILL_DIR \u6307\u5B9A skills/testpilot \u8DEF\u5F84"
|
|
32316
33396
|
);
|
|
32317
33397
|
}
|
|
32318
|
-
await
|
|
33398
|
+
await mkdir12(dirname9(dest), { recursive: true });
|
|
32319
33399
|
await cp(src, dest, { recursive: true });
|
|
32320
33400
|
return { core: "created", bundledVersion, dest };
|
|
32321
33401
|
}
|
|
@@ -32336,14 +33416,14 @@ async function syncSkillFiles(root, options = {}) {
|
|
|
32336
33416
|
};
|
|
32337
33417
|
}
|
|
32338
33418
|
async function installSkill(root, info, adapters) {
|
|
32339
|
-
const dest =
|
|
33419
|
+
const dest = join23(root, ".agents", "skills", "testpilot");
|
|
32340
33420
|
const sync = await syncSkillFiles(root);
|
|
32341
33421
|
const core = sync.core === "created" ? "created" : sync.core === "updated" ? "updated" : "reused";
|
|
32342
33422
|
const coreVersion = sync.core === "updated" || sync.core === "created" ? { from: sync.installedVersion, to: sync.bundledVersion } : void 0;
|
|
32343
33423
|
const config2 = await loadTestpilotConfig(root).catch(() => void 0);
|
|
32344
33424
|
const defaultEnvironment = typeof config2?.environment?.default === "string" ? config2.environment.default : "\u672A\u8BBE\u7F6E";
|
|
32345
33425
|
const defaultWorkspace = config2?.workspace?.default ?? "\u672A\u8BBE\u7F6E(\u53EF\u5728 yaml workspace.default \u914D\u7F6E)";
|
|
32346
|
-
const refsDir =
|
|
33426
|
+
const refsDir = join23(dest, "references");
|
|
32347
33427
|
const files = [
|
|
32348
33428
|
["project.md", () => renderProjectMd(info, adapters, defaultEnvironment, defaultWorkspace)],
|
|
32349
33429
|
["test-conventions.md", renderTestConventionsMd],
|
|
@@ -32351,10 +33431,10 @@ async function installSkill(root, info, adapters) {
|
|
|
32351
33431
|
];
|
|
32352
33432
|
let references = "reused";
|
|
32353
33433
|
for (const [name, render] of files) {
|
|
32354
|
-
const file2 =
|
|
33434
|
+
const file2 = join23(refsDir, name);
|
|
32355
33435
|
if (await exists3(file2)) continue;
|
|
32356
|
-
await
|
|
32357
|
-
await
|
|
33436
|
+
await mkdir12(refsDir, { recursive: true });
|
|
33437
|
+
await writeFile10(file2, render(), "utf8");
|
|
32358
33438
|
references = "created";
|
|
32359
33439
|
}
|
|
32360
33440
|
return coreVersion ? { core, coreVersion, references, dest } : { core, references, dest };
|
|
@@ -32389,6 +33469,12 @@ function renderTestConventionsMd() {
|
|
|
32389
33469
|
6. \u4F18\u5148\u590D\u7528\u5DF2\u6709 Case
|
|
32390
33470
|
7. \u65B0\u589E Case \u5FC5\u987B\u901A\u8FC7 csspilot validate
|
|
32391
33471
|
8. Case ID \u5FC5\u987B\u552F\u4E00
|
|
33472
|
+
9. fixture/dataset \u4F7F\u7528 YAML \u6216 JSON,\u7EDF\u4E00\u4F4D\u4E8E tests/e2e/data
|
|
33473
|
+
10. \u9700\u6C42\u89C4\u5219\u77E9\u9635\u4E0E QA \u8986\u76D6\u6620\u5C04\u5199\u5165 tests/e2e/reviews/<\u9700\u6C42>-coverage.md
|
|
33474
|
+
11. \u53EA\u5728\u6B65\u9AA4\u6A21\u677F\u76F8\u540C\u65F6\u4F7F\u7528 data-driven Case,\u6BCF\u884C\u4FDD\u6301\u4E00\u4E2A\u53EF\u5224\u5B9A\u89C4\u5219/\u6570\u636E\u70B9
|
|
33475
|
+
12. requires \u53EA\u58F0\u660E\u9879\u76EE\u8BC1\u636E\u652F\u6301\u7684\u4F9D\u8D56,\u4E0D\u5F97\u5957\u7528\u8D26\u53F7/\u4F18\u60E0\u5238/PPM \u7B49\u9ED8\u8BA4\u5B57\u6BB5
|
|
33476
|
+
13. Case \u4E0D\u5F97\u8BFB\u53D6\u4EFB\u610F\u73AF\u5883\u53D8\u91CF;\u5148\u5728 testpilot.yaml variables \u767D\u540D\u5355\u6620\u5C04\u540E\u4F7F\u7528 \${variable.*}
|
|
33477
|
+
14. setup/teardown \u4F18\u5148 API;teardown \u5931\u8D25\u4F5C\u4E3A warning,\u4E0D\u6539\u53D8\u4E3B\u6D41\u7A0B\u7ED3\u679C
|
|
32392
33478
|
`;
|
|
32393
33479
|
}
|
|
32394
33480
|
function renderAdaptersMd(adapters) {
|
|
@@ -32429,27 +33515,27 @@ async function runDoctor(root) {
|
|
|
32429
33515
|
const projectItems = [
|
|
32430
33516
|
ok(`Project ${info.name}(${info.type})`),
|
|
32431
33517
|
info.git.commit ? ok(`Git repository(${info.git.branch ?? "detached"} \xB7 ${info.git.commit.slice(0, 7)})`) : warn("Git repository \u672A\u68C0\u6D4B\u5230", "Case Metadata \u65E0\u6CD5\u540C\u6B65\u5230 Server"),
|
|
32432
|
-
|
|
32433
|
-
|
|
33518
|
+
existsSync8(join24(root, ".testpilot", "project.json")) ? ok(".testpilot/project.json") : error62(".testpilot/project.json", "\u8FD0\u884C npx csspilot init \u751F\u6210"),
|
|
33519
|
+
existsSync8(join24(root, "testpilot.yaml")) ? ok("testpilot.yaml") : error62("testpilot.yaml", "\u8FD0\u884C npx csspilot init \u751F\u6210")
|
|
32434
33520
|
];
|
|
32435
33521
|
push("Project", projectItems);
|
|
32436
33522
|
const config2 = await loadTestpilotConfig(root).catch(() => void 0);
|
|
32437
33523
|
const casesDir = config2?.casesDir ?? "tests/e2e/cases";
|
|
32438
|
-
const cases = await collectCases([
|
|
33524
|
+
const cases = await collectCases([join24(root, casesDir)], { root }).catch(() => []);
|
|
32439
33525
|
const invalidCount = cases.filter((item) => !item.valid).length;
|
|
32440
|
-
const testDir =
|
|
33526
|
+
const testDir = join24(root, "tests", "e2e");
|
|
32441
33527
|
push("Test", [
|
|
32442
|
-
|
|
32443
|
-
|
|
33528
|
+
existsSync8(testDir) ? ok("tests/e2e") : error62("tests/e2e", "\u8FD0\u884C npx csspilot init \u521B\u5EFA"),
|
|
33529
|
+
existsSync8(join24(root, casesDir)) ? ok(`cases directory(${casesDir})`) : error62(`cases directory(${casesDir})`, "\u8FD0\u884C npx csspilot init \u521B\u5EFA"),
|
|
32444
33530
|
cases.length === 0 ? warn("Case validation \u8DF3\u8FC7(\u672A\u627E\u5230\u7528\u4F8B)", "\u5728 tests/e2e/cases \u521B\u5EFA Case") : invalidCount === 0 ? ok(`Case validation(${cases.length} \u4E2A\u7528\u4F8B\u5168\u90E8\u6709\u6548)`) : error62(`Case validation(${invalidCount}/${cases.length} \u4E2A\u7528\u4F8B\u6821\u9A8C\u5931\u8D25)`, "\u8FD0\u884C npx csspilot validate \u67E5\u770B\u8BE6\u60C5")
|
|
32445
33531
|
]);
|
|
32446
|
-
const skillDir =
|
|
33532
|
+
const skillDir = join24(root, ".agents", "skills", "testpilot");
|
|
32447
33533
|
const installedSkillVersion = readManifestVersion(skillDir);
|
|
32448
33534
|
const skillSource = resolveSkillSourceDir();
|
|
32449
33535
|
const bundledSkillVersion = skillSource ? readManifestVersion(skillSource) : void 0;
|
|
32450
33536
|
const skillItems = [
|
|
32451
33537
|
installedSkillVersion ? ok(`TestPilot Skill(.agents/skills/testpilot,v${installedSkillVersion})`) : error62("TestPilot Skill", "\u8FD0\u884C npx csspilot init \u5B89\u88C5"),
|
|
32452
|
-
|
|
33538
|
+
existsSync8(join24(skillDir, "references", "project.md")) ? ok("Project testing guide(references/)") : warn("Project testing guide \u7F3A\u5931", "references/ \u7531 init \u751F\u6210;\u53EF\u91CD\u65B0\u8FD0\u884C npx csspilot init")
|
|
32453
33539
|
];
|
|
32454
33540
|
if (installedSkillVersion && bundledSkillVersion && installedSkillVersion !== bundledSkillVersion) {
|
|
32455
33541
|
skillItems.push(
|
|
@@ -32541,11 +33627,11 @@ function makeDoctorCommand() {
|
|
|
32541
33627
|
}
|
|
32542
33628
|
|
|
32543
33629
|
// src/commands/init/index.ts
|
|
32544
|
-
import { join as
|
|
33630
|
+
import { join as join27 } from "node:path";
|
|
32545
33631
|
|
|
32546
33632
|
// src/commands/init/config-initializer.ts
|
|
32547
|
-
import { access as access3, mkdir as
|
|
32548
|
-
import { basename as basename2, join as
|
|
33633
|
+
import { access as access3, mkdir as mkdir13, readFile as readFile10, writeFile as writeFile11 } from "node:fs/promises";
|
|
33634
|
+
import { basename as basename2, join as join25 } from "node:path";
|
|
32549
33635
|
async function exists4(path) {
|
|
32550
33636
|
try {
|
|
32551
33637
|
await access3(path);
|
|
@@ -32576,6 +33662,10 @@ environment:
|
|
|
32576
33662
|
staging:
|
|
32577
33663
|
baseUrl: \${STAGING_BASE_URL}
|
|
32578
33664
|
|
|
33665
|
+
# Case \u53EF\u89C1\u7684\u9879\u76EE\u53D8\u91CF\u767D\u540D\u5355(\u503C\u6765\u81EA .env / CI Secret;Case \u7528 \${variable.<name>} \u5F15\u7528)
|
|
33666
|
+
# variables:
|
|
33667
|
+
# ppmProjectId: \${PPM_PROJECT_ID}
|
|
33668
|
+
|
|
32579
33669
|
# \u9879\u76EE\u58F0\u660E\u7684\u6267\u884C\u7AEF(\u5B9E\u9645\u53EF\u7528\u6027\u4EE5 csspilot doctor \u68C0\u6D4B\u4E3A\u51C6)
|
|
32580
33670
|
adapters:
|
|
32581
33671
|
${adapters}
|
|
@@ -32619,42 +33709,42 @@ STAGING_BASE_URL=https://staging.example.com
|
|
|
32619
33709
|
`;
|
|
32620
33710
|
}
|
|
32621
33711
|
async function ensureGitignoreEntry(root, entry, comment = "TestPilot \u672C\u5730\u73AF\u5883\u53D8\u91CF") {
|
|
32622
|
-
const gitignorePath =
|
|
33712
|
+
const gitignorePath = join25(root, ".gitignore");
|
|
32623
33713
|
if (!await exists4(gitignorePath)) {
|
|
32624
|
-
await
|
|
33714
|
+
await writeFile11(gitignorePath, `# ${comment}
|
|
32625
33715
|
${entry}
|
|
32626
33716
|
`, "utf8");
|
|
32627
33717
|
return "created";
|
|
32628
33718
|
}
|
|
32629
|
-
const content = await
|
|
33719
|
+
const content = await readFile10(gitignorePath, "utf8");
|
|
32630
33720
|
if (content.split(/\r?\n/).some((item) => item.trim() === entry)) {
|
|
32631
33721
|
return "exists";
|
|
32632
33722
|
}
|
|
32633
33723
|
const separator = content.endsWith("\n") || content === "" ? "" : "\n";
|
|
32634
|
-
await
|
|
33724
|
+
await writeFile11(gitignorePath, `${content}${separator}# ${comment}
|
|
32635
33725
|
${entry}
|
|
32636
33726
|
`, "utf8");
|
|
32637
33727
|
return "updated";
|
|
32638
33728
|
}
|
|
32639
33729
|
async function initTestPilotConfig(root, info, adapters) {
|
|
32640
|
-
const yamlPath =
|
|
33730
|
+
const yamlPath = join25(root, "testpilot.yaml");
|
|
32641
33731
|
let yaml = "reused";
|
|
32642
33732
|
if (!await exists4(yamlPath)) {
|
|
32643
|
-
await
|
|
33733
|
+
await writeFile11(yamlPath, renderTestpilotYaml(info), "utf8");
|
|
32644
33734
|
yaml = "created";
|
|
32645
33735
|
}
|
|
32646
|
-
const envExamplePath =
|
|
33736
|
+
const envExamplePath = join25(root, ".env.example");
|
|
32647
33737
|
let envExample = "reused";
|
|
32648
33738
|
if (!await exists4(envExamplePath)) {
|
|
32649
|
-
await
|
|
33739
|
+
await writeFile11(envExamplePath, renderDotEnvExample(), "utf8");
|
|
32650
33740
|
envExample = "created";
|
|
32651
33741
|
}
|
|
32652
33742
|
const gitignore = await ensureGitignoreEntry(root, ".env");
|
|
32653
|
-
const runtimeDir =
|
|
32654
|
-
await
|
|
32655
|
-
const keep =
|
|
33743
|
+
const runtimeDir = join25(root, ".testpilot", "artifacts");
|
|
33744
|
+
await mkdir13(runtimeDir, { recursive: true });
|
|
33745
|
+
const keep = join25(runtimeDir, ".gitkeep");
|
|
32656
33746
|
if (!await exists4(keep)) {
|
|
32657
|
-
await
|
|
33747
|
+
await writeFile11(keep, "", "utf8");
|
|
32658
33748
|
}
|
|
32659
33749
|
const existing = await readProjectLink(root);
|
|
32660
33750
|
const defaultAdapter = defaultAdapterFor(info.type);
|
|
@@ -32675,7 +33765,7 @@ async function initTestPilotConfig(root, info, adapters) {
|
|
|
32675
33765
|
const projectLinkFile = await writeProjectLink(link, root);
|
|
32676
33766
|
return {
|
|
32677
33767
|
yaml,
|
|
32678
|
-
runtimeDir:
|
|
33768
|
+
runtimeDir: join25(root, ".testpilot"),
|
|
32679
33769
|
projectLink: link,
|
|
32680
33770
|
projectLinkFile,
|
|
32681
33771
|
envExample,
|
|
@@ -32710,32 +33800,35 @@ function randomId() {
|
|
|
32710
33800
|
}
|
|
32711
33801
|
|
|
32712
33802
|
// src/commands/init/test-structure.ts
|
|
32713
|
-
import { existsSync as
|
|
32714
|
-
import { mkdir as
|
|
32715
|
-
import { join as
|
|
33803
|
+
import { existsSync as existsSync9, readdirSync as readdirSync2 } from "node:fs";
|
|
33804
|
+
import { mkdir as mkdir14, writeFile as writeFile12 } from "node:fs/promises";
|
|
33805
|
+
import { join as join26 } from "node:path";
|
|
32716
33806
|
async function initTestStructure(root, _info) {
|
|
32717
33807
|
const dirs = {
|
|
32718
|
-
cases:
|
|
32719
|
-
fixtures:
|
|
32720
|
-
data:
|
|
33808
|
+
cases: join26(root, "tests", "e2e", "cases"),
|
|
33809
|
+
fixtures: join26(root, "tests", "e2e", "fixtures"),
|
|
33810
|
+
data: join26(root, "tests", "e2e", "data"),
|
|
33811
|
+
reviews: join26(root, "tests", "e2e", "reviews")
|
|
32721
33812
|
};
|
|
32722
33813
|
const existed = {
|
|
32723
|
-
cases:
|
|
32724
|
-
fixtures:
|
|
32725
|
-
data:
|
|
33814
|
+
cases: existsSync9(dirs.cases),
|
|
33815
|
+
fixtures: existsSync9(dirs.fixtures),
|
|
33816
|
+
data: existsSync9(dirs.data),
|
|
33817
|
+
reviews: existsSync9(dirs.reviews)
|
|
32726
33818
|
};
|
|
32727
33819
|
for (const dir of Object.values(dirs)) {
|
|
32728
|
-
await
|
|
33820
|
+
await mkdir14(dir, { recursive: true });
|
|
32729
33821
|
}
|
|
32730
33822
|
for (const [name, dir] of Object.entries(dirs)) {
|
|
32731
33823
|
if (!existed[name] && readdirSync2(dir).length === 0) {
|
|
32732
|
-
await
|
|
33824
|
+
await writeFile12(join26(dir, ".gitkeep"), "", "utf8").catch(() => void 0);
|
|
32733
33825
|
}
|
|
32734
33826
|
}
|
|
32735
33827
|
return {
|
|
32736
33828
|
cases: existed.cases ? "reused" : "created",
|
|
32737
33829
|
fixtures: existed.fixtures ? "reused" : "created",
|
|
32738
|
-
data: existed.data ? "reused" : "created"
|
|
33830
|
+
data: existed.data ? "reused" : "created",
|
|
33831
|
+
reviews: existed.reviews ? "reused" : "created"
|
|
32739
33832
|
};
|
|
32740
33833
|
}
|
|
32741
33834
|
|
|
@@ -32763,7 +33856,7 @@ async function runInit(root, options = {}) {
|
|
|
32763
33856
|
console.log(
|
|
32764
33857
|
config2.yaml === "created" ? "\u2713 \u9879\u76EE\u914D\u7F6E testpilot.yaml" : "\u2713 \u9879\u76EE\u914D\u7F6E testpilot.yaml(\u5DF2\u5B58\u5728,\u4FDD\u7559)"
|
|
32765
33858
|
);
|
|
32766
|
-
console.log(`\u2713 Runtime \u76EE\u5F55 ${
|
|
33859
|
+
console.log(`\u2713 Runtime \u76EE\u5F55 ${join27(".testpilot", "artifacts")}`);
|
|
32767
33860
|
const serverMode = config2.projectLink.serverUrl ? `Server:${config2.projectLink.serverUrl}` : "\u672C\u5730\u6A21\u5F0F";
|
|
32768
33861
|
console.log(`\u2713 \u9879\u76EE\u5173\u8054 ${config2.projectLink.projectId}(${serverMode})`);
|
|
32769
33862
|
console.log(
|
|
@@ -32775,7 +33868,7 @@ async function runInit(root, options = {}) {
|
|
|
32775
33868
|
const testStructure = await initTestStructure(root, project);
|
|
32776
33869
|
const reuseNote = project.existingE2eDirs.filter((dir) => dir !== "tests/e2e");
|
|
32777
33870
|
console.log(
|
|
32778
|
-
testStructure.cases === "reused" ? "\u2713 \u6D4B\u8BD5\u76EE\u5F55 tests/e2e(\u5DF2\u5B58\u5728,\u590D\u7528)" : "\u2713 \u6D4B\u8BD5\u76EE\u5F55 tests/e2e/{cases,fixtures,data}"
|
|
33871
|
+
testStructure.cases === "reused" ? "\u2713 \u6D4B\u8BD5\u76EE\u5F55 tests/e2e(\u5DF2\u5B58\u5728,\u590D\u7528)" : "\u2713 \u6D4B\u8BD5\u76EE\u5F55 tests/e2e/{cases,fixtures,data,reviews}"
|
|
32779
33872
|
);
|
|
32780
33873
|
if (reuseNote.length > 0) {
|
|
32781
33874
|
console.log(` \u26A0 \u68C0\u6D4B\u5230\u5176\u4ED6 E2E \u76EE\u5F55:${reuseNote.join(", ")}(\u4FDD\u7559\u4E0D\u52A8,\u672A\u5F3A\u5236\u8FC1\u79FB)`);
|
|
@@ -32787,7 +33880,7 @@ async function runInit(root, options = {}) {
|
|
|
32787
33880
|
);
|
|
32788
33881
|
} else {
|
|
32789
33882
|
console.log(
|
|
32790
|
-
skill.core === "created" ? `\u2713 Skill \u5B89\u88C5 ${
|
|
33883
|
+
skill.core === "created" ? `\u2713 Skill \u5B89\u88C5 ${join27(".agents", "skills", "testpilot")}` : "\u2713 Skill \u5B89\u88C5 .agents/skills/testpilot/(\u5DF2\u5B58\u5728,\u8DF3\u8FC7)"
|
|
32791
33884
|
);
|
|
32792
33885
|
}
|
|
32793
33886
|
console.log(
|
|
@@ -32864,9 +33957,20 @@ function makeListCommand() {
|
|
|
32864
33957
|
// src/commands/report.ts
|
|
32865
33958
|
import { exec } from "node:child_process";
|
|
32866
33959
|
function makeReportCommand() {
|
|
32867
|
-
return new Command("report").description("\u751F\u6210\u6D4B\u8BD5\u62A5\u544A(JSON + HTML)").option("--run <id>", "\u8FD0\u884C\u8BB0\u5F55 ID,\u9ED8\u8BA4\u6700\u8FD1\u4E00\u6B21").option("--open", "\u751F\u6210\u540E\u5728\u6D4F\u89C8\u5668\u6253\u5F00", false).action(async (options) => {
|
|
33960
|
+
return new Command("report").description("\u751F\u6210\u6D4B\u8BD5\u62A5\u544A(JSON + HTML)").option("--run <id>", "\u8FD0\u884C\u8BB0\u5F55 ID,\u9ED8\u8BA4\u6700\u8FD1\u4E00\u6B21").option("--open", "\u751F\u6210\u540E\u5728\u6D4F\u89C8\u5668\u6253\u5F00", false).option("--summary", "\u751F\u6210\u8DE8 run \u6C47\u603B\u62A5\u544A(summary.html + summary.json)", false).option("--last <n>", "\u6C47\u603B\u7EDF\u8BA1\u7A97\u53E3:\u6700\u8FD1 N \u6B21\u8FD0\u884C", "30").action(async (options) => {
|
|
32868
33961
|
const client = new TestPilotClient();
|
|
32869
33962
|
try {
|
|
33963
|
+
if (options.summary) {
|
|
33964
|
+
const result = await client.generateSummaryReport({
|
|
33965
|
+
last: Number(options.last) > 0 ? Number(options.last) : void 0
|
|
33966
|
+
});
|
|
33967
|
+
console.log(
|
|
33968
|
+
`\u2713 summary.html ${result.html}(\u7EDF\u8BA1\u6700\u8FD1 ${result.window.runs} \u6B21\u8FD0\u884C,\u6574\u4F53\u901A\u8FC7\u7387 ${(result.data.kpis.overallRate * 100).toFixed(1)}%)`
|
|
33969
|
+
);
|
|
33970
|
+
console.log(`\u2713 summary.json ${result.json}`);
|
|
33971
|
+
if (options.open) await openInBrowser(result.html);
|
|
33972
|
+
return;
|
|
33973
|
+
}
|
|
32870
33974
|
const report = await client.generateReport(options.run);
|
|
32871
33975
|
console.log(`\u2713 report.json ${report.json}`);
|
|
32872
33976
|
console.log(`\u2713 report.html ${report.html}`);
|
|
@@ -32885,7 +33989,7 @@ function openInBrowser(file2) {
|
|
|
32885
33989
|
}
|
|
32886
33990
|
|
|
32887
33991
|
// src/commands/run.ts
|
|
32888
|
-
import { join as
|
|
33992
|
+
import { join as join28 } from "node:path";
|
|
32889
33993
|
function makeRunCommand() {
|
|
32890
33994
|
return new Command("run").description("\u6267\u884C\u6D4B\u8BD5\u7528\u4F8B(\u9ED8\u8BA4\u8FD0\u884C testpilot.yaml \u914D\u7F6E\u7684 Case)").argument("[paths...]", "\u7528\u4F8B\u6587\u4EF6\u6216\u76EE\u5F55,\u9ED8\u8BA4\u6309\u9879\u76EE\u914D\u7F6E").option("--tag <tag>", "\u6309\u6807\u7B7E\u8FC7\u6EE4\u7528\u4F8B").action(async (paths, options) => {
|
|
32891
33995
|
const client = new TestPilotClient();
|
|
@@ -32907,9 +34011,9 @@ function makeRunCommand() {
|
|
|
32907
34011
|
].filter(Boolean).join(",");
|
|
32908
34012
|
console.log("");
|
|
32909
34013
|
console.log(
|
|
32910
|
-
`Run ${summary.runId}${summary.cancelled ? "(\u5DF2\u53D6\u6D88)" : ""}: ${summary.status.toUpperCase()} \u2014 \
|
|
34014
|
+
`Run ${summary.runId}${summary.cancelled ? "(\u5DF2\u53D6\u6D88)" : ""}: ${summary.status.toUpperCase()} \u2014 \u6A21\u677F ${summary.totals.templates ?? summary.totals.cases},\u6570\u636E\u884C \u2713${summary.totals.passed} \u2717${summary.totals.failed} \u21B7${summary.totals.skipped ?? 0} \u26A0${summary.totals.warnings ?? 0},\u6B65\u9AA4 \u2713${summary.totals.stepsPassed} \u2717${summary.totals.stepsFailed} \u21B7${summary.totals.stepsSkipped} \u26A0${summary.totals.stepsWarning ?? 0}${notes ? `(${notes})` : ""}`
|
|
32911
34015
|
);
|
|
32912
|
-
console.log(`\u4EA7\u7269\u76EE\u5F55:${
|
|
34016
|
+
console.log(`\u4EA7\u7269\u76EE\u5F55:${join28(".testpilot", "artifacts", "runs", summary.runId)}`);
|
|
32913
34017
|
console.log("\u67E5\u770B\u62A5\u544A:npx csspilot report");
|
|
32914
34018
|
if (summary.status === "failed") process.exitCode = 1;
|
|
32915
34019
|
} catch (err) {
|
|
@@ -32934,12 +34038,12 @@ function consoleEventHandler(event) {
|
|
|
32934
34038
|
`);
|
|
32935
34039
|
break;
|
|
32936
34040
|
case "case-started":
|
|
32937
|
-
console.log(`\u25B6 ${event.caseId} (${event.file})`);
|
|
34041
|
+
console.log(`\u25B6 ${event.caseId}#${event.rowId ?? "default"} (${event.file})`);
|
|
32938
34042
|
break;
|
|
32939
34043
|
case "step-finished": {
|
|
32940
|
-
const mark = event.status === "passed" ? " \u2713" : event.status === "failed" ? " \u2717" : " \u21B7";
|
|
34044
|
+
const mark = event.status === "passed" ? " \u2713" : event.status === "failed" ? " \u2717" : event.status === "warning" ? " \u26A0" : " \u21B7";
|
|
32941
34045
|
console.log(
|
|
32942
|
-
`${mark} ${event.target}/${event.action} ${event.durationMs}ms${event.error ? ` \u2014 ${event.error}` : ""}`
|
|
34046
|
+
`${mark} ${event.phase ?? "steps"} ${event.target}/${event.action} ${event.durationMs}ms${event.error ? ` \u2014 ${event.error}` : ""}`
|
|
32943
34047
|
);
|
|
32944
34048
|
break;
|
|
32945
34049
|
}
|
|
@@ -32949,7 +34053,7 @@ function consoleEventHandler(event) {
|
|
|
32949
34053
|
}
|
|
32950
34054
|
|
|
32951
34055
|
// src/commands/sync-metadata.ts
|
|
32952
|
-
import { relative } from "node:path";
|
|
34056
|
+
import { relative as relative2 } from "node:path";
|
|
32953
34057
|
function makeSyncMetadataCommand() {
|
|
32954
34058
|
return new Command("sync-metadata").description("\u540C\u6B65 Case Metadata \u5FEB\u7167\u5230 TestPilot Server(CI / git push \u540E\u4F7F\u7528)").option("--server <url>", "TestPilot Server \u5730\u5740(\u9ED8\u8BA4\u53D6 project.json \u6216 testpilot.yaml)").action(async (options) => {
|
|
32955
34059
|
const client = new TestPilotClient();
|
|
@@ -32976,7 +34080,7 @@ function makeSyncMetadataCommand() {
|
|
|
32976
34080
|
const cases = infos.filter((item) => item.valid && item.case).map((item) => ({
|
|
32977
34081
|
id: item.case.id,
|
|
32978
34082
|
title: item.case.name,
|
|
32979
|
-
file:
|
|
34083
|
+
file: relative2(process.cwd(), item.file).split("\\").join("/"),
|
|
32980
34084
|
tags: item.case.tags ?? []
|
|
32981
34085
|
}));
|
|
32982
34086
|
const skipped = infos.length - cases.length;
|
|
@@ -33020,12 +34124,12 @@ function makeSyncMetadataCommand() {
|
|
|
33020
34124
|
}
|
|
33021
34125
|
|
|
33022
34126
|
// src/commands/update.ts
|
|
33023
|
-
import { writeFile as
|
|
33024
|
-
import { join as
|
|
34127
|
+
import { writeFile as writeFile13 } from "node:fs/promises";
|
|
34128
|
+
import { join as join29 } from "node:path";
|
|
33025
34129
|
function makeUpdateCommand() {
|
|
33026
34130
|
const update = new Command("update").description("\u540C\u6B65 TestPilot \u63A5\u5165\u7269\u5230\u5F53\u524D CLI \u7248\u672C(Skill / .env.example;\u4E0D\u89E6\u78B0 .env \u4E0E\u7528\u4F8B)").option("--force", "Skill \u7248\u672C\u4E00\u81F4\u65F6\u4E5F\u5F3A\u5236\u8986\u76D6\u672C\u5730\u4FEE\u6539", false).action(async (options) => {
|
|
33027
34131
|
const root = process.cwd();
|
|
33028
|
-
const dest =
|
|
34132
|
+
const dest = join29(".agents", "skills", "testpilot");
|
|
33029
34133
|
const skill = await syncSkillFiles(root, { force: options.force });
|
|
33030
34134
|
switch (skill.core) {
|
|
33031
34135
|
case "created":
|
|
@@ -33039,7 +34143,7 @@ function makeUpdateCommand() {
|
|
|
33039
34143
|
default:
|
|
33040
34144
|
console.log(`\u2713 Skill \u5DF2\u662F\u6700\u65B0 v${skill.bundledVersion ?? "?"}`);
|
|
33041
34145
|
}
|
|
33042
|
-
await
|
|
34146
|
+
await writeFile13(join29(root, ".env.example"), renderDotEnvExample(), "utf8");
|
|
33043
34147
|
console.log("\u2713 .env.example \u5DF2\u540C\u6B65\u4E3A\u5F53\u524D\u7248\u672C\u7684\u793A\u4F8B(.env \u4E0D\u53D7\u5F71\u54CD)");
|
|
33044
34148
|
const gitignore = await ensureGitignoreEntry(root, ".env");
|
|
33045
34149
|
console.log(
|
|
@@ -33086,10 +34190,13 @@ function makeValidateCommand() {
|
|
|
33086
34190
|
}
|
|
33087
34191
|
|
|
33088
34192
|
// src/cli.ts
|
|
33089
|
-
var CLI_VERSION = "0.
|
|
34193
|
+
var CLI_VERSION = "0.6.0";
|
|
33090
34194
|
async function createCli() {
|
|
33091
34195
|
const program2 = new Command();
|
|
33092
|
-
program2.name("csspilot").description("TestPilot \u2014 AI Native \u8DE8\u7AEF\u4E1A\u52A1\u6D4B\u8BD5\u57FA\u7840\u8BBE\u65BD CLI").version(CLI_VERSION).
|
|
34196
|
+
program2.name("csspilot").description("TestPilot \u2014 AI Native \u8DE8\u7AEF\u4E1A\u52A1\u6D4B\u8BD5\u57FA\u7840\u8BBE\u65BD CLI").version(CLI_VERSION, "-v, --version", "output the current version").option("-V", "output the current version", () => {
|
|
34197
|
+
console.log(CLI_VERSION);
|
|
34198
|
+
process.exit(0);
|
|
34199
|
+
}).addCommand(makeInitCommand()).addCommand(makeValidateCommand()).addCommand(makeListCommand()).addCommand(makeRunCommand()).addCommand(makeReportCommand()).addCommand(makeSyncMetadataCommand()).addCommand(makeDoctorCommand()).addCommand(makeUpdateCommand()).addCommand(makeCiCommand());
|
|
33093
34200
|
return program2;
|
|
33094
34201
|
}
|
|
33095
34202
|
|