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