csspilot 0.5.0 → 0.7.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 +934 -249
- package/dist/index.js +934 -249
- package/dist/skills/testpilot/SKILL.md +84 -78
- 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 +47 -29
- package/dist/skills/testpilot/rules/dsl.md +62 -0
- package/dist/skills/testpilot/workflows/analyze-result.md +4 -2
- package/dist/skills/testpilot/workflows/create-case.md +33 -16
- package/dist/skills/testpilot/workflows/run-case.md +3 -0
- package/dist/skills/testpilot/workflows/test-requirements-review.md +143 -61
- package/dist/skills/testpilot/workflows/validate.md +4 -0
- package/package.json +11 -11
- package/LICENSE +0 -21
package/dist/bin.js
CHANGED
|
@@ -102,7 +102,7 @@ var require_visit = __commonJS({
|
|
|
102
102
|
var BREAK = Symbol("break visit");
|
|
103
103
|
var SKIP = Symbol("skip children");
|
|
104
104
|
var REMOVE = Symbol("remove node");
|
|
105
|
-
function
|
|
105
|
+
function visit3(node2, visitor) {
|
|
106
106
|
const visitor_ = initVisitor(visitor);
|
|
107
107
|
if (identity.isDocument(node2)) {
|
|
108
108
|
const cd = visit_(null, node2.contents, visitor_, Object.freeze([node2]));
|
|
@@ -111,9 +111,9 @@ var require_visit = __commonJS({
|
|
|
111
111
|
} else
|
|
112
112
|
visit_(null, node2, visitor_, Object.freeze([]));
|
|
113
113
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
114
|
+
visit3.BREAK = BREAK;
|
|
115
|
+
visit3.SKIP = SKIP;
|
|
116
|
+
visit3.REMOVE = REMOVE;
|
|
117
117
|
function visit_(key, node2, visitor, path) {
|
|
118
118
|
const ctrl = callVisitor(key, node2, visitor, path);
|
|
119
119
|
if (identity.isNode(ctrl) || identity.isPair(ctrl)) {
|
|
@@ -247,7 +247,7 @@ var require_visit = __commonJS({
|
|
|
247
247
|
throw new Error(`Cannot replace node with ${pt} parent`);
|
|
248
248
|
}
|
|
249
249
|
}
|
|
250
|
-
exports.visit =
|
|
250
|
+
exports.visit = visit3;
|
|
251
251
|
exports.visitAsync = visitAsync;
|
|
252
252
|
}
|
|
253
253
|
});
|
|
@@ -257,7 +257,7 @@ var require_directives = __commonJS({
|
|
|
257
257
|
"../../node_modules/.pnpm/yaml@2.9.0/node_modules/yaml/dist/doc/directives.js"(exports) {
|
|
258
258
|
"use strict";
|
|
259
259
|
var identity = require_identity();
|
|
260
|
-
var
|
|
260
|
+
var visit3 = require_visit();
|
|
261
261
|
var escapeChars = {
|
|
262
262
|
"!": "%21",
|
|
263
263
|
",": "%2C",
|
|
@@ -401,7 +401,7 @@ var require_directives = __commonJS({
|
|
|
401
401
|
let tagNames;
|
|
402
402
|
if (doc && tagEntries.length > 0 && identity.isNode(doc.contents)) {
|
|
403
403
|
const tags = {};
|
|
404
|
-
|
|
404
|
+
visit3.visit(doc.contents, (_key, node2) => {
|
|
405
405
|
if (identity.isNode(node2) && node2.tag)
|
|
406
406
|
tags[node2.tag] = true;
|
|
407
407
|
});
|
|
@@ -428,7 +428,7 @@ var require_anchors = __commonJS({
|
|
|
428
428
|
"../../node_modules/.pnpm/yaml@2.9.0/node_modules/yaml/dist/doc/anchors.js"(exports) {
|
|
429
429
|
"use strict";
|
|
430
430
|
var identity = require_identity();
|
|
431
|
-
var
|
|
431
|
+
var visit3 = require_visit();
|
|
432
432
|
function anchorIsValid(anchor2) {
|
|
433
433
|
if (/[\x00-\x19\s,[\]{}]/.test(anchor2)) {
|
|
434
434
|
const sa = JSON.stringify(anchor2);
|
|
@@ -439,7 +439,7 @@ var require_anchors = __commonJS({
|
|
|
439
439
|
}
|
|
440
440
|
function anchorNames(root) {
|
|
441
441
|
const anchors = /* @__PURE__ */ new Set();
|
|
442
|
-
|
|
442
|
+
visit3.visit(root, {
|
|
443
443
|
Value(_key, node2) {
|
|
444
444
|
if (node2.anchor)
|
|
445
445
|
anchors.add(node2.anchor);
|
|
@@ -619,7 +619,7 @@ var require_Alias = __commonJS({
|
|
|
619
619
|
"../../node_modules/.pnpm/yaml@2.9.0/node_modules/yaml/dist/nodes/Alias.js"(exports) {
|
|
620
620
|
"use strict";
|
|
621
621
|
var anchors = require_anchors();
|
|
622
|
-
var
|
|
622
|
+
var visit3 = require_visit();
|
|
623
623
|
var identity = require_identity();
|
|
624
624
|
var Node = require_Node();
|
|
625
625
|
var toJS = require_toJS();
|
|
@@ -645,7 +645,7 @@ var require_Alias = __commonJS({
|
|
|
645
645
|
nodes = ctx.aliasResolveCache;
|
|
646
646
|
} else {
|
|
647
647
|
nodes = [];
|
|
648
|
-
|
|
648
|
+
visit3.visit(doc, {
|
|
649
649
|
Node: (_key, node2) => {
|
|
650
650
|
if (identity.isAlias(node2) || identity.hasAnchor(node2))
|
|
651
651
|
nodes.push(node2);
|
|
@@ -5110,7 +5110,7 @@ var require_composer = __commonJS({
|
|
|
5110
5110
|
var node_process = __require("process");
|
|
5111
5111
|
var directives = require_directives();
|
|
5112
5112
|
var Document = require_Document();
|
|
5113
|
-
var
|
|
5113
|
+
var errors2 = require_errors();
|
|
5114
5114
|
var identity = require_identity();
|
|
5115
5115
|
var composeDoc = require_compose_doc();
|
|
5116
5116
|
var resolveEnd = require_resolve_end();
|
|
@@ -5157,9 +5157,9 @@ var require_composer = __commonJS({
|
|
|
5157
5157
|
this.onError = (source, code, message, warning) => {
|
|
5158
5158
|
const pos = getErrorPos(source);
|
|
5159
5159
|
if (warning)
|
|
5160
|
-
this.warnings.push(new
|
|
5160
|
+
this.warnings.push(new errors2.YAMLWarning(pos, code, message));
|
|
5161
5161
|
else
|
|
5162
|
-
this.errors.push(new
|
|
5162
|
+
this.errors.push(new errors2.YAMLParseError(pos, code, message));
|
|
5163
5163
|
};
|
|
5164
5164
|
this.directives = new directives.Directives({ version: options.version || "1.2" });
|
|
5165
5165
|
this.options = options;
|
|
@@ -5257,7 +5257,7 @@ ${cb}` : comment;
|
|
|
5257
5257
|
break;
|
|
5258
5258
|
case "error": {
|
|
5259
5259
|
const msg = token.source ? `${token.message}: ${JSON.stringify(token.source)}` : token.message;
|
|
5260
|
-
const error62 = new
|
|
5260
|
+
const error62 = new errors2.YAMLParseError(getErrorPos(token), "UNEXPECTED_TOKEN", msg);
|
|
5261
5261
|
if (this.atDirectives || !this.doc)
|
|
5262
5262
|
this.errors.push(error62);
|
|
5263
5263
|
else
|
|
@@ -5267,7 +5267,7 @@ ${cb}` : comment;
|
|
|
5267
5267
|
case "doc-end": {
|
|
5268
5268
|
if (!this.doc) {
|
|
5269
5269
|
const msg = "Unexpected doc-end without preceding document";
|
|
5270
|
-
this.errors.push(new
|
|
5270
|
+
this.errors.push(new errors2.YAMLParseError(getErrorPos(token), "UNEXPECTED_TOKEN", msg));
|
|
5271
5271
|
break;
|
|
5272
5272
|
}
|
|
5273
5273
|
this.doc.directives.docEnd = true;
|
|
@@ -5282,7 +5282,7 @@ ${end.comment}` : end.comment;
|
|
|
5282
5282
|
break;
|
|
5283
5283
|
}
|
|
5284
5284
|
default:
|
|
5285
|
-
this.errors.push(new
|
|
5285
|
+
this.errors.push(new errors2.YAMLParseError(getErrorPos(token), "UNEXPECTED_TOKEN", `Unsupported token ${token.type}`));
|
|
5286
5286
|
}
|
|
5287
5287
|
}
|
|
5288
5288
|
/**
|
|
@@ -5317,7 +5317,7 @@ var require_cst_scalar = __commonJS({
|
|
|
5317
5317
|
"use strict";
|
|
5318
5318
|
var resolveBlockScalar = require_resolve_block_scalar();
|
|
5319
5319
|
var resolveFlowScalar = require_resolve_flow_scalar();
|
|
5320
|
-
var
|
|
5320
|
+
var errors2 = require_errors();
|
|
5321
5321
|
var stringifyString = require_stringifyString();
|
|
5322
5322
|
function resolveAsScalar(token, strict = true, onError) {
|
|
5323
5323
|
if (token) {
|
|
@@ -5326,7 +5326,7 @@ var require_cst_scalar = __commonJS({
|
|
|
5326
5326
|
if (onError)
|
|
5327
5327
|
onError(offset, code, message);
|
|
5328
5328
|
else
|
|
5329
|
-
throw new
|
|
5329
|
+
throw new errors2.YAMLParseError([offset, offset + 1], code, message);
|
|
5330
5330
|
};
|
|
5331
5331
|
switch (token.type) {
|
|
5332
5332
|
case "scalar":
|
|
@@ -5564,15 +5564,15 @@ var require_cst_visit = __commonJS({
|
|
|
5564
5564
|
var BREAK = Symbol("break visit");
|
|
5565
5565
|
var SKIP = Symbol("skip children");
|
|
5566
5566
|
var REMOVE = Symbol("remove item");
|
|
5567
|
-
function
|
|
5567
|
+
function visit3(cst, visitor) {
|
|
5568
5568
|
if ("type" in cst && cst.type === "document")
|
|
5569
5569
|
cst = { start: cst.start, value: cst.value };
|
|
5570
5570
|
_visit(Object.freeze([]), cst, visitor);
|
|
5571
5571
|
}
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5572
|
+
visit3.BREAK = BREAK;
|
|
5573
|
+
visit3.SKIP = SKIP;
|
|
5574
|
+
visit3.REMOVE = REMOVE;
|
|
5575
|
+
visit3.itemAtPath = (cst, path) => {
|
|
5576
5576
|
let item = cst;
|
|
5577
5577
|
for (const [field, index] of path) {
|
|
5578
5578
|
const tok = item?.[field];
|
|
@@ -5583,8 +5583,8 @@ var require_cst_visit = __commonJS({
|
|
|
5583
5583
|
}
|
|
5584
5584
|
return item;
|
|
5585
5585
|
};
|
|
5586
|
-
|
|
5587
|
-
const parent =
|
|
5586
|
+
visit3.parentCollection = (cst, path) => {
|
|
5587
|
+
const parent = visit3.itemAtPath(cst, path.slice(0, -1));
|
|
5588
5588
|
const field = path[path.length - 1][0];
|
|
5589
5589
|
const coll = parent?.[field];
|
|
5590
5590
|
if (coll && "items" in coll)
|
|
@@ -5615,7 +5615,7 @@ var require_cst_visit = __commonJS({
|
|
|
5615
5615
|
}
|
|
5616
5616
|
return typeof ctrl === "function" ? ctrl(item, path) : ctrl;
|
|
5617
5617
|
}
|
|
5618
|
-
exports.visit =
|
|
5618
|
+
exports.visit = visit3;
|
|
5619
5619
|
}
|
|
5620
5620
|
});
|
|
5621
5621
|
|
|
@@ -7221,7 +7221,7 @@ var require_public_api = __commonJS({
|
|
|
7221
7221
|
"use strict";
|
|
7222
7222
|
var composer = require_composer();
|
|
7223
7223
|
var Document = require_Document();
|
|
7224
|
-
var
|
|
7224
|
+
var errors2 = require_errors();
|
|
7225
7225
|
var log = require_log();
|
|
7226
7226
|
var identity = require_identity();
|
|
7227
7227
|
var lineCounter = require_line_counter();
|
|
@@ -7238,8 +7238,8 @@ var require_public_api = __commonJS({
|
|
|
7238
7238
|
const docs = Array.from(composer$1.compose(parser$1.parse(source)));
|
|
7239
7239
|
if (prettyErrors && lineCounter2)
|
|
7240
7240
|
for (const doc of docs) {
|
|
7241
|
-
doc.errors.forEach(
|
|
7242
|
-
doc.warnings.forEach(
|
|
7241
|
+
doc.errors.forEach(errors2.prettifyError(source, lineCounter2));
|
|
7242
|
+
doc.warnings.forEach(errors2.prettifyError(source, lineCounter2));
|
|
7243
7243
|
}
|
|
7244
7244
|
if (docs.length > 0)
|
|
7245
7245
|
return docs;
|
|
@@ -7254,13 +7254,13 @@ var require_public_api = __commonJS({
|
|
|
7254
7254
|
if (!doc)
|
|
7255
7255
|
doc = _doc;
|
|
7256
7256
|
else if (doc.options.logLevel !== "silent") {
|
|
7257
|
-
doc.errors.push(new
|
|
7257
|
+
doc.errors.push(new errors2.YAMLParseError(_doc.range.slice(0, 2), "MULTIPLE_DOCS", "Source contains multiple documents; please use YAML.parseAllDocuments()"));
|
|
7258
7258
|
break;
|
|
7259
7259
|
}
|
|
7260
7260
|
}
|
|
7261
7261
|
if (prettyErrors && lineCounter2) {
|
|
7262
|
-
doc.errors.forEach(
|
|
7263
|
-
doc.warnings.forEach(
|
|
7262
|
+
doc.errors.forEach(errors2.prettifyError(source, lineCounter2));
|
|
7263
|
+
doc.warnings.forEach(errors2.prettifyError(source, lineCounter2));
|
|
7264
7264
|
}
|
|
7265
7265
|
return doc;
|
|
7266
7266
|
}
|
|
@@ -7319,7 +7319,7 @@ var require_dist = __commonJS({
|
|
|
7319
7319
|
var composer = require_composer();
|
|
7320
7320
|
var Document = require_Document();
|
|
7321
7321
|
var Schema = require_Schema();
|
|
7322
|
-
var
|
|
7322
|
+
var errors2 = require_errors();
|
|
7323
7323
|
var Alias = require_Alias();
|
|
7324
7324
|
var identity = require_identity();
|
|
7325
7325
|
var Pair = require_Pair();
|
|
@@ -7331,13 +7331,13 @@ var require_dist = __commonJS({
|
|
|
7331
7331
|
var lineCounter = require_line_counter();
|
|
7332
7332
|
var parser = require_parser();
|
|
7333
7333
|
var publicApi = require_public_api();
|
|
7334
|
-
var
|
|
7334
|
+
var visit3 = require_visit();
|
|
7335
7335
|
exports.Composer = composer.Composer;
|
|
7336
7336
|
exports.Document = Document.Document;
|
|
7337
7337
|
exports.Schema = Schema.Schema;
|
|
7338
|
-
exports.YAMLError =
|
|
7339
|
-
exports.YAMLParseError =
|
|
7340
|
-
exports.YAMLWarning =
|
|
7338
|
+
exports.YAMLError = errors2.YAMLError;
|
|
7339
|
+
exports.YAMLParseError = errors2.YAMLParseError;
|
|
7340
|
+
exports.YAMLWarning = errors2.YAMLWarning;
|
|
7341
7341
|
exports.Alias = Alias.Alias;
|
|
7342
7342
|
exports.isAlias = identity.isAlias;
|
|
7343
7343
|
exports.isCollection = identity.isCollection;
|
|
@@ -7359,8 +7359,8 @@ var require_dist = __commonJS({
|
|
|
7359
7359
|
exports.parseAllDocuments = publicApi.parseAllDocuments;
|
|
7360
7360
|
exports.parseDocument = publicApi.parseDocument;
|
|
7361
7361
|
exports.stringify = publicApi.stringify;
|
|
7362
|
-
exports.visit =
|
|
7363
|
-
exports.visitAsync =
|
|
7362
|
+
exports.visit = visit3.visit;
|
|
7363
|
+
exports.visitAsync = visit3.visitAsync;
|
|
7364
7364
|
}
|
|
7365
7365
|
});
|
|
7366
7366
|
|
|
@@ -10367,6 +10367,28 @@ var TestPilotError = class extends Error {
|
|
|
10367
10367
|
}
|
|
10368
10368
|
};
|
|
10369
10369
|
|
|
10370
|
+
// ../../packages/core/src/verdict.ts
|
|
10371
|
+
var INFRA_CODES = /* @__PURE__ */ new Set([
|
|
10372
|
+
"ENVIRONMENT_UNAVAILABLE",
|
|
10373
|
+
"ADAPTER_NOT_FOUND",
|
|
10374
|
+
"UNSUPPORTED_ACTION",
|
|
10375
|
+
"FIXTURE_MISSING",
|
|
10376
|
+
"ACCOUNT_UNAVAILABLE",
|
|
10377
|
+
"CONFIG_MISSING"
|
|
10378
|
+
]);
|
|
10379
|
+
var UNVERIFIED_CODES = /* @__PURE__ */ new Set(["PREREQ_VARIABLE_MISSING"]);
|
|
10380
|
+
function verdictFromErrorCode(code) {
|
|
10381
|
+
if (code !== void 0 && INFRA_CODES.has(code)) return "infra";
|
|
10382
|
+
if (code !== void 0 && UNVERIFIED_CODES.has(code)) return "unverified";
|
|
10383
|
+
return "product";
|
|
10384
|
+
}
|
|
10385
|
+
function caseStatusFromErrorCode(code) {
|
|
10386
|
+
const verdict = verdictFromErrorCode(code);
|
|
10387
|
+
if (verdict === "infra") return "blocked";
|
|
10388
|
+
if (verdict === "unverified") return "inconclusive";
|
|
10389
|
+
return "failed";
|
|
10390
|
+
}
|
|
10391
|
+
|
|
10370
10392
|
// ../../packages/core/src/constants/index.ts
|
|
10371
10393
|
import { join } from "node:path";
|
|
10372
10394
|
var TESTPILOT_DIR = ".testpilot";
|
|
@@ -15418,7 +15440,7 @@ var recursive = /* @__PURE__ */ new WeakMap();
|
|
|
15418
15440
|
var NONE = 0;
|
|
15419
15441
|
var ASSUMED = 1;
|
|
15420
15442
|
var PROVEN = 2;
|
|
15421
|
-
function isRecursive(inst, stack,
|
|
15443
|
+
function isRecursive(inst, stack, resolve7) {
|
|
15422
15444
|
const cached2 = recursive.get(inst);
|
|
15423
15445
|
if (cached2 !== void 0)
|
|
15424
15446
|
return cached2 ? PROVEN : NONE;
|
|
@@ -15428,7 +15450,7 @@ function isRecursive(inst, stack, resolve6) {
|
|
|
15428
15450
|
let result = NONE;
|
|
15429
15451
|
const check2 = (child) => {
|
|
15430
15452
|
if (result !== PROVEN && child?._zod) {
|
|
15431
|
-
const answer = isRecursive(child, stack,
|
|
15453
|
+
const answer = isRecursive(child, stack, resolve7);
|
|
15432
15454
|
if (answer > result)
|
|
15433
15455
|
result = answer;
|
|
15434
15456
|
}
|
|
@@ -15439,7 +15461,7 @@ function isRecursive(inst, stack, resolve6) {
|
|
|
15439
15461
|
const desc = Object.getOwnPropertyDescriptor(sh, key);
|
|
15440
15462
|
if (spread && !desc.enumerable)
|
|
15441
15463
|
continue;
|
|
15442
|
-
const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack,
|
|
15464
|
+
const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve7) : NONE;
|
|
15443
15465
|
if (child > answer)
|
|
15444
15466
|
answer = child;
|
|
15445
15467
|
}
|
|
@@ -15506,7 +15528,7 @@ function isRecursive(inst, stack, resolve6) {
|
|
|
15506
15528
|
break;
|
|
15507
15529
|
// `$ZodLazy` caches its inner on the def, so a resolved edge is followed exactly
|
|
15508
15530
|
case "lazy": {
|
|
15509
|
-
const inner = def._cachedInner ?? (
|
|
15531
|
+
const inner = def._cachedInner ?? (resolve7 ? inst._zod.innerType : void 0);
|
|
15510
15532
|
merge2(inner ? isRecursive(inner, stack, false) : ASSUMED);
|
|
15511
15533
|
break;
|
|
15512
15534
|
}
|
|
@@ -30072,6 +30094,8 @@ var configSchema = external_exports.strictObject({
|
|
|
30072
30094
|
}).optional(),
|
|
30073
30095
|
/** 环境配置:default 指定默认环境名,其余键为环境名 -> { baseUrl } */
|
|
30074
30096
|
environment: external_exports.record(external_exports.string(), external_exports.union([external_exports.string().min(1), environmentEntrySchema])).optional(),
|
|
30097
|
+
/** Case 可见的项目变量白名单;值可通过 ${ENV_VAR} 从运行环境注入 */
|
|
30098
|
+
variables: external_exports.record(external_exports.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/), external_exports.string()).optional(),
|
|
30075
30099
|
/** 项目声明的执行端(playwright / wechatide);仅作声明,实际可用性以 doctor 检测为准 */
|
|
30076
30100
|
adapters: external_exports.array(external_exports.string().min(1)).optional(),
|
|
30077
30101
|
workspace: external_exports.strictObject({ default: external_exports.string().min(1).optional() }).optional(),
|
|
@@ -30136,6 +30160,9 @@ async function loadTestpilotConfig(cwd = process.cwd()) {
|
|
|
30136
30160
|
testDirectory: parsed.test?.directory,
|
|
30137
30161
|
projectName: parsed.project?.name,
|
|
30138
30162
|
environment,
|
|
30163
|
+
variables: parsed.variables ? Object.fromEntries(
|
|
30164
|
+
Object.entries(parsed.variables).map(([name, value]) => [name, interpolateEnvRefs(value)])
|
|
30165
|
+
) : void 0,
|
|
30139
30166
|
adapters: parsed.adapters,
|
|
30140
30167
|
workspace: parsed.workspace,
|
|
30141
30168
|
server: parsed.server ? {
|
|
@@ -30338,15 +30365,15 @@ function makeCiCommand() {
|
|
|
30338
30365
|
}
|
|
30339
30366
|
|
|
30340
30367
|
// src/commands/init/doctor.ts
|
|
30341
|
-
import { existsSync as
|
|
30368
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
30342
30369
|
import { join as join24 } from "node:path";
|
|
30343
30370
|
|
|
30344
30371
|
// ../../packages/sdk/src/client.ts
|
|
30345
|
-
import { isAbsolute as
|
|
30372
|
+
import { isAbsolute as isAbsolute4, join as join19 } from "node:path";
|
|
30346
30373
|
|
|
30347
30374
|
// ../../packages/sdk/src/cases.ts
|
|
30348
|
-
import { mkdir as mkdir3, readFile as
|
|
30349
|
-
import { dirname, extname, isAbsolute, join as join6, resolve as
|
|
30375
|
+
import { mkdir as mkdir3, readFile as readFile4, readdir, stat as stat2, writeFile as writeFile3 } from "node:fs/promises";
|
|
30376
|
+
import { dirname as dirname2, extname as extname2, isAbsolute as isAbsolute2, join as join6, resolve as resolve3 } from "node:path";
|
|
30350
30377
|
|
|
30351
30378
|
// ../../packages/dsl/src/schema/index.ts
|
|
30352
30379
|
var STEP_TARGETS = ["web", "miniapp", "api"];
|
|
@@ -30367,6 +30394,12 @@ var locatorSchema = external_exports.strictObject({
|
|
|
30367
30394
|
text: external_exports.string().min(1).optional(),
|
|
30368
30395
|
css: external_exports.string().min(1).optional()
|
|
30369
30396
|
}).describe("text \u4E0E css \u53EA\u80FD\u63D0\u4F9B\u5176\u4E00");
|
|
30397
|
+
var requestExpectationSchema = external_exports.strictObject({
|
|
30398
|
+
method: external_exports.enum(HTTP_METHODS).optional(),
|
|
30399
|
+
urlContains: external_exports.string().min(1),
|
|
30400
|
+
count: external_exports.number().int().nonnegative(),
|
|
30401
|
+
windowMs: external_exports.number().int().nonnegative().default(1e3)
|
|
30402
|
+
});
|
|
30370
30403
|
var stepSchema = external_exports.strictObject({
|
|
30371
30404
|
target: external_exports.enum(STEP_TARGETS),
|
|
30372
30405
|
action: external_exports.enum(ACTIONS),
|
|
@@ -30381,8 +30414,18 @@ var stepSchema = external_exports.strictObject({
|
|
|
30381
30414
|
/** api target:请求体;对象按 JSON 序列化,字符串原样发送(支持 ${var} 模板) */
|
|
30382
30415
|
body: external_exports.union([external_exports.string(), external_exports.record(external_exports.string(), external_exports.unknown())]).optional(),
|
|
30383
30416
|
variable: external_exports.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/, "\u53D8\u91CF\u540D\u9700\u4EE5\u5B57\u6BCD\u6216\u4E0B\u5212\u7EBF\u5F00\u5934,\u4EC5\u542B\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u4E0B\u5212\u7EBF").optional(),
|
|
30384
|
-
timeout: external_exports.number().int().positive().optional()
|
|
30417
|
+
timeout: external_exports.number().int().positive().optional(),
|
|
30418
|
+
/** UI 动作触发的网络请求精确次数断言 */
|
|
30419
|
+
expectRequests: external_exports.array(requestExpectationSchema).min(1).optional()
|
|
30420
|
+
});
|
|
30421
|
+
var datasetReferenceSchema = external_exports.strictObject({
|
|
30422
|
+
file: external_exports.string().min(1),
|
|
30423
|
+
idField: external_exports.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/, "idField \u9700\u4E3A\u5408\u6CD5\u5B57\u6BB5\u540D").default("id")
|
|
30385
30424
|
});
|
|
30425
|
+
var dependencyPathSchema = external_exports.string().regex(
|
|
30426
|
+
/^(fixture|dataset|account|variable)(?:\.[A-Za-z_][A-Za-z0-9_]*)+$/,
|
|
30427
|
+
"requires \u8DEF\u5F84\u5FC5\u987B\u4EE5 fixture/dataset/account/variable \u5F00\u5934\u5E76\u5305\u542B\u5B57\u6BB5"
|
|
30428
|
+
);
|
|
30386
30429
|
var caseSchema = external_exports.strictObject({
|
|
30387
30430
|
id: external_exports.string().regex(/^[a-z0-9][a-z0-9-]*$/, "id \u9700\u4E3A kebab-case(\u5C0F\u5199\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u8FDE\u5B57\u7B26)"),
|
|
30388
30431
|
name: external_exports.string().min(1, "name \u4E0D\u80FD\u4E3A\u7A7A"),
|
|
@@ -30394,7 +30437,12 @@ var caseSchema = external_exports.strictObject({
|
|
|
30394
30437
|
/** 引用测试账号标识;凭据由 Environment + Server/CI Secret 管理,禁止写进 Case */
|
|
30395
30438
|
accountRef: external_exports.string().min(1).optional(),
|
|
30396
30439
|
tags: external_exports.array(external_exports.string().min(1)).optional(),
|
|
30397
|
-
|
|
30440
|
+
fixtures: external_exports.array(external_exports.string().min(1)).min(1).optional(),
|
|
30441
|
+
datasets: datasetReferenceSchema.optional(),
|
|
30442
|
+
requires: external_exports.array(dependencyPathSchema).min(1).optional(),
|
|
30443
|
+
setup: external_exports.array(stepSchema).min(1).optional(),
|
|
30444
|
+
steps: external_exports.array(stepSchema).min(1, "Case \u81F3\u5C11\u5305\u542B\u4E00\u4E2A step"),
|
|
30445
|
+
teardown: external_exports.array(stepSchema).min(1).optional()
|
|
30398
30446
|
});
|
|
30399
30447
|
|
|
30400
30448
|
// ../../packages/dsl/src/parser/index.ts
|
|
@@ -30445,6 +30493,7 @@ var ADAPTER_SUPPORT = {
|
|
|
30445
30493
|
};
|
|
30446
30494
|
var LOCATOR_REQUIRED = /* @__PURE__ */ new Set(["click", "input", "select", "assert", "extract"]);
|
|
30447
30495
|
var VALUE_REQUIRED = /* @__PURE__ */ new Set(["input", "select"]);
|
|
30496
|
+
var REQUEST_EXPECTATION_ACTIONS = /* @__PURE__ */ new Set(["navigate", "click", "input", "select"]);
|
|
30448
30497
|
var FIELD_ALLOWED = {
|
|
30449
30498
|
url: ["navigate", "request"],
|
|
30450
30499
|
value: ["input", "select", "extract"],
|
|
@@ -30459,12 +30508,21 @@ var VARIABLE_REF = /\$\{([A-Za-z_][A-Za-z0-9_.]*)\}/g;
|
|
|
30459
30508
|
function validateCase(data) {
|
|
30460
30509
|
const issues = [];
|
|
30461
30510
|
const declared = /* @__PURE__ */ new Set();
|
|
30511
|
+
if (data.fixtures) declared.add("fixture");
|
|
30512
|
+
if (data.datasets) declared.add("dataset");
|
|
30513
|
+
declared.add("variable");
|
|
30462
30514
|
if (data.accountRef) {
|
|
30463
30515
|
declared.add("account");
|
|
30464
30516
|
}
|
|
30517
|
+
data.setup?.forEach((step, index) => {
|
|
30518
|
+
checkStep(step, `setup[${index}]`, declared, issues);
|
|
30519
|
+
});
|
|
30465
30520
|
data.steps.forEach((step, index) => {
|
|
30466
30521
|
checkStep(step, `steps[${index}]`, declared, issues);
|
|
30467
30522
|
});
|
|
30523
|
+
data.teardown?.forEach((step, index) => {
|
|
30524
|
+
checkStep(step, `teardown[${index}]`, declared, issues);
|
|
30525
|
+
});
|
|
30468
30526
|
return issues;
|
|
30469
30527
|
}
|
|
30470
30528
|
function validateCaseSource(source) {
|
|
@@ -30539,6 +30597,15 @@ function checkStep(step, at, declared, issues) {
|
|
|
30539
30597
|
if (step.action === "wait" && !step.locator && step.timeout === void 0) {
|
|
30540
30598
|
issues.push({ path: at, code: "semantic", message: "wait \u9700\u8981\u63D0\u4F9B locator(\u7B49\u5F85\u5143\u7D20)\u6216 timeout(\u6BEB\u79D2)" });
|
|
30541
30599
|
}
|
|
30600
|
+
if (step.expectRequests) {
|
|
30601
|
+
if (step.target === "api" || !REQUEST_EXPECTATION_ACTIONS.has(step.action)) {
|
|
30602
|
+
issues.push({
|
|
30603
|
+
path: `${at}.expectRequests`,
|
|
30604
|
+
code: "semantic",
|
|
30605
|
+
message: "expectRequests \u4EC5\u7528\u4E8E web/miniapp \u7684 navigate/click/input/select"
|
|
30606
|
+
});
|
|
30607
|
+
}
|
|
30608
|
+
}
|
|
30542
30609
|
checkVariableRefs(step, at, declared, issues);
|
|
30543
30610
|
}
|
|
30544
30611
|
function checkLocator(step, at, issues) {
|
|
@@ -30571,9 +30638,14 @@ function checkVariableRefs(step, at, declared, issues) {
|
|
|
30571
30638
|
collect("value", step.value);
|
|
30572
30639
|
collect("expected", step.expected);
|
|
30573
30640
|
collect("url", step.url);
|
|
30641
|
+
collect("locator.text", step.locator?.text);
|
|
30642
|
+
collect("locator.css", step.locator?.css);
|
|
30574
30643
|
if (typeof step.body === "string") collect("body", step.body);
|
|
30575
30644
|
else if (step.body) collect("body", JSON.stringify(step.body));
|
|
30576
30645
|
for (const headerValue of Object.values(step.headers ?? {})) collect("headers", headerValue);
|
|
30646
|
+
for (const [index, expectation] of (step.expectRequests ?? []).entries()) {
|
|
30647
|
+
collect(`expectRequests[${index}].urlContains`, expectation.urlContains);
|
|
30648
|
+
}
|
|
30577
30649
|
for (const [field, name] of refs) {
|
|
30578
30650
|
const root = name.split(".")[0] ?? name;
|
|
30579
30651
|
if (!declared.has(root)) {
|
|
@@ -30588,9 +30660,20 @@ function checkVariableRefs(step, at, declared, issues) {
|
|
|
30588
30660
|
|
|
30589
30661
|
// ../../packages/sdk/src/project.ts
|
|
30590
30662
|
import { mkdir as mkdir2, readFile as readFile2, stat, writeFile as writeFile2 } from "node:fs/promises";
|
|
30591
|
-
import {
|
|
30663
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
30664
|
+
import { dirname, join as join5, resolve } from "node:path";
|
|
30592
30665
|
function resolveRoot(root) {
|
|
30593
|
-
|
|
30666
|
+
const start = resolve(root ?? process.cwd());
|
|
30667
|
+
if (root) return start;
|
|
30668
|
+
let current = start;
|
|
30669
|
+
for (; ; ) {
|
|
30670
|
+
if (existsSync2(join5(current, "testpilot.yaml")) || existsSync2(join5(current, TESTPILOT_DIR))) {
|
|
30671
|
+
return current;
|
|
30672
|
+
}
|
|
30673
|
+
const parent = dirname(current);
|
|
30674
|
+
if (parent === current) return start;
|
|
30675
|
+
current = parent;
|
|
30676
|
+
}
|
|
30594
30677
|
}
|
|
30595
30678
|
async function loadProjectConfig(root) {
|
|
30596
30679
|
return loadTestpilotConfig(resolveRoot(root));
|
|
@@ -30639,6 +30722,167 @@ async function writeProjectLink(link, root) {
|
|
|
30639
30722
|
return file2;
|
|
30640
30723
|
}
|
|
30641
30724
|
|
|
30725
|
+
// ../../packages/sdk/src/case-data.ts
|
|
30726
|
+
var import_yaml3 = __toESM(require_dist(), 1);
|
|
30727
|
+
import { readFile as readFile3, realpath } from "node:fs/promises";
|
|
30728
|
+
import { extname, isAbsolute, relative, resolve as resolve2 } from "node:path";
|
|
30729
|
+
async function loadCaseDataFile(reference, options) {
|
|
30730
|
+
if (isAbsolute(reference)) {
|
|
30731
|
+
throw new Error(`\u6570\u636E\u6587\u4EF6\u5FC5\u987B\u4F7F\u7528\u9879\u76EE\u76F8\u5BF9\u8DEF\u5F84:${reference}`);
|
|
30732
|
+
}
|
|
30733
|
+
const root = resolve2(options.root);
|
|
30734
|
+
const dataDir = resolve2(options.dataDir);
|
|
30735
|
+
const target = resolve2(root, reference);
|
|
30736
|
+
if (!isInside(dataDir, target)) {
|
|
30737
|
+
throw new Error(`\u6570\u636E\u6587\u4EF6\u5FC5\u987B\u4F4D\u4E8E tests/e2e/data \u76EE\u5F55:${reference}`);
|
|
30738
|
+
}
|
|
30739
|
+
const [realDataDir, realTarget] = await Promise.all([
|
|
30740
|
+
realpath(dataDir).catch(() => dataDir),
|
|
30741
|
+
realpath(target).catch((error62) => {
|
|
30742
|
+
if (error62 instanceof Error && error62.code === "ENOENT") {
|
|
30743
|
+
throw new TestPilotError("FIXTURE_MISSING", `\u6570\u636E\u6587\u4EF6\u4E0D\u5B58\u5728:${reference}`);
|
|
30744
|
+
}
|
|
30745
|
+
throw new Error(
|
|
30746
|
+
`\u8BFB\u53D6\u6570\u636E\u6587\u4EF6\u5931\u8D25:${reference}:${error62 instanceof Error ? error62.message : String(error62)}`
|
|
30747
|
+
);
|
|
30748
|
+
})
|
|
30749
|
+
]);
|
|
30750
|
+
if (!isInside(realDataDir, realTarget)) {
|
|
30751
|
+
throw new Error(`\u6570\u636E\u6587\u4EF6\u89E3\u6790\u540E\u8D8A\u51FA tests/e2e/data \u76EE\u5F55:${reference}`);
|
|
30752
|
+
}
|
|
30753
|
+
const source = await readFile3(realTarget, "utf8");
|
|
30754
|
+
const extension = extname(realTarget).toLowerCase();
|
|
30755
|
+
try {
|
|
30756
|
+
if (extension === ".yaml" || extension === ".yml") return (0, import_yaml3.parse)(source);
|
|
30757
|
+
if (extension === ".json") return JSON.parse(source);
|
|
30758
|
+
} catch (error62) {
|
|
30759
|
+
throw new Error(
|
|
30760
|
+
`\u89E3\u6790\u6570\u636E\u6587\u4EF6\u5931\u8D25:${reference}:${error62 instanceof Error ? error62.message : String(error62)}`
|
|
30761
|
+
);
|
|
30762
|
+
}
|
|
30763
|
+
throw new Error(`\u6570\u636E\u6587\u4EF6\u4EC5\u652F\u6301 .yaml/.yml/.json:${reference}`);
|
|
30764
|
+
}
|
|
30765
|
+
function isDataObject(value) {
|
|
30766
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
30767
|
+
}
|
|
30768
|
+
function mergeFixture(target, source, at = "fixture") {
|
|
30769
|
+
const output2 = { ...target };
|
|
30770
|
+
for (const [key, value] of Object.entries(source)) {
|
|
30771
|
+
const path = `${at}.${key}`;
|
|
30772
|
+
if (!(key in output2)) {
|
|
30773
|
+
output2[key] = value;
|
|
30774
|
+
continue;
|
|
30775
|
+
}
|
|
30776
|
+
const current = output2[key];
|
|
30777
|
+
if (isDataObject(current) && isDataObject(value)) {
|
|
30778
|
+
output2[key] = mergeFixture(current, value, path);
|
|
30779
|
+
continue;
|
|
30780
|
+
}
|
|
30781
|
+
throw new Error(`fixture \u5B57\u6BB5\u51B2\u7A81:${path}`);
|
|
30782
|
+
}
|
|
30783
|
+
return output2;
|
|
30784
|
+
}
|
|
30785
|
+
function flattenVariables(prefix, value) {
|
|
30786
|
+
const output2 = {};
|
|
30787
|
+
visit2(prefix, value, output2);
|
|
30788
|
+
return output2;
|
|
30789
|
+
}
|
|
30790
|
+
function visit2(path, value, output2) {
|
|
30791
|
+
if (value === null || value === void 0) return;
|
|
30792
|
+
if (typeof value === "object") {
|
|
30793
|
+
output2[path] = JSON.stringify(value);
|
|
30794
|
+
if (Array.isArray(value)) {
|
|
30795
|
+
value.forEach((item, index) => visit2(`${path}.${index}`, item, output2));
|
|
30796
|
+
} else {
|
|
30797
|
+
for (const [key, item] of Object.entries(value)) {
|
|
30798
|
+
visit2(`${path}.${key}`, item, output2);
|
|
30799
|
+
}
|
|
30800
|
+
}
|
|
30801
|
+
return;
|
|
30802
|
+
}
|
|
30803
|
+
output2[path] = String(value);
|
|
30804
|
+
}
|
|
30805
|
+
function isInside(base, candidate) {
|
|
30806
|
+
const path = relative(base, candidate);
|
|
30807
|
+
return path === "" || !path.startsWith("..") && !isAbsolute(path);
|
|
30808
|
+
}
|
|
30809
|
+
|
|
30810
|
+
// ../../packages/sdk/src/case-preparation.ts
|
|
30811
|
+
async function prepareCaseExecutions(item, options) {
|
|
30812
|
+
const fixture = await loadFixtures(item.data.fixtures ?? [], options);
|
|
30813
|
+
const rows = await loadDataset(item.data, options);
|
|
30814
|
+
const projectVariables = flattenVariables("variable", options.variables ?? {});
|
|
30815
|
+
const accountVariables = accountValues(item.data, options.accounts);
|
|
30816
|
+
return rows.map(({ row, rowId }, rowIndex) => {
|
|
30817
|
+
const initialVariables = {
|
|
30818
|
+
...flattenVariables("fixture", fixture),
|
|
30819
|
+
...flattenVariables("dataset", row),
|
|
30820
|
+
...projectVariables,
|
|
30821
|
+
...accountVariables
|
|
30822
|
+
};
|
|
30823
|
+
const missingDependencies = (item.data.requires ?? []).filter(
|
|
30824
|
+
(path) => isUnavailable(initialVariables[path])
|
|
30825
|
+
);
|
|
30826
|
+
return {
|
|
30827
|
+
data: item.data,
|
|
30828
|
+
file: item.file,
|
|
30829
|
+
templateId: item.data.id,
|
|
30830
|
+
rowId,
|
|
30831
|
+
rowIndex,
|
|
30832
|
+
initialVariables,
|
|
30833
|
+
missingDependencies
|
|
30834
|
+
};
|
|
30835
|
+
});
|
|
30836
|
+
}
|
|
30837
|
+
async function loadFixtures(references, options) {
|
|
30838
|
+
let merged = {};
|
|
30839
|
+
for (const reference of references) {
|
|
30840
|
+
const value = await loadCaseDataFile(reference, options);
|
|
30841
|
+
if (!isDataObject(value)) {
|
|
30842
|
+
throw new Error(`fixture \u6587\u4EF6\u6839\u5FC5\u987B\u662F\u5BF9\u8C61:${reference}`);
|
|
30843
|
+
}
|
|
30844
|
+
merged = mergeFixture(merged, value);
|
|
30845
|
+
}
|
|
30846
|
+
return merged;
|
|
30847
|
+
}
|
|
30848
|
+
async function loadDataset(data, options) {
|
|
30849
|
+
if (!data.datasets) return [{ row: {}, rowId: "default" }];
|
|
30850
|
+
const value = await loadCaseDataFile(data.datasets.file, options);
|
|
30851
|
+
if (!Array.isArray(value)) {
|
|
30852
|
+
throw new Error(`dataset \u6587\u4EF6\u6839\u5FC5\u987B\u662F\u5BF9\u8C61\u6570\u7EC4:${data.datasets.file}`);
|
|
30853
|
+
}
|
|
30854
|
+
const seen = /* @__PURE__ */ new Set();
|
|
30855
|
+
return value.map((row, index) => {
|
|
30856
|
+
if (!isDataObject(row)) {
|
|
30857
|
+
throw new Error(`dataset \u7B2C ${index + 1} \u884C\u5FC5\u987B\u662F\u5BF9\u8C61:${data.datasets.file}`);
|
|
30858
|
+
}
|
|
30859
|
+
const rawId = row[data.datasets.idField];
|
|
30860
|
+
if (typeof rawId !== "string" && typeof rawId !== "number" || String(rawId).trim() === "") {
|
|
30861
|
+
throw new Error(
|
|
30862
|
+
`dataset \u7B2C ${index + 1} \u884C\u7F3A\u5C11\u975E\u7A7A row id \u5B57\u6BB5 "${data.datasets.idField}"`
|
|
30863
|
+
);
|
|
30864
|
+
}
|
|
30865
|
+
const rowId = String(rawId);
|
|
30866
|
+
if (seen.has(rowId)) throw new Error(`dataset row id \u91CD\u590D:${rowId}`);
|
|
30867
|
+
seen.add(rowId);
|
|
30868
|
+
return { row, rowId };
|
|
30869
|
+
});
|
|
30870
|
+
}
|
|
30871
|
+
function accountValues(data, accounts) {
|
|
30872
|
+
if (!data.accountRef) return {};
|
|
30873
|
+
const raw = accounts?.[data.accountRef];
|
|
30874
|
+
if (raw === void 0) return {};
|
|
30875
|
+
try {
|
|
30876
|
+
const parsed = JSON.parse(raw);
|
|
30877
|
+
if (isDataObject(parsed)) return flattenVariables("account", parsed);
|
|
30878
|
+
} catch {
|
|
30879
|
+
}
|
|
30880
|
+
return { account: raw };
|
|
30881
|
+
}
|
|
30882
|
+
function isUnavailable(value) {
|
|
30883
|
+
return value === void 0 || value.trim() === "" || /\$\{[A-Za-z_][A-Za-z0-9_]*\}/.test(value);
|
|
30884
|
+
}
|
|
30885
|
+
|
|
30642
30886
|
// ../../packages/sdk/src/cases.ts
|
|
30643
30887
|
async function collectCaseFiles(paths) {
|
|
30644
30888
|
const files = [];
|
|
@@ -30667,19 +30911,20 @@ async function collectFromDir(dir) {
|
|
|
30667
30911
|
return found;
|
|
30668
30912
|
}
|
|
30669
30913
|
function isCaseFile(file2) {
|
|
30670
|
-
const ext =
|
|
30914
|
+
const ext = extname2(file2).toLowerCase();
|
|
30671
30915
|
return ext === ".yaml" || ext === ".yml";
|
|
30672
30916
|
}
|
|
30673
30917
|
function resolveAgainstRoot(root, file2) {
|
|
30674
|
-
return
|
|
30918
|
+
return isAbsolute2(file2) ? file2 : resolve3(resolveRoot(root), file2);
|
|
30675
30919
|
}
|
|
30676
30920
|
async function collectCases(paths, options = {}) {
|
|
30677
30921
|
const files = await collectCaseFiles(
|
|
30678
30922
|
paths.map((input2) => resolveAgainstRoot(options.root, input2))
|
|
30679
30923
|
);
|
|
30680
30924
|
const infos = [];
|
|
30925
|
+
const root = resolveRoot(options.root);
|
|
30681
30926
|
for (const file2 of files) {
|
|
30682
|
-
const validation = await validateCaseFile(file2);
|
|
30927
|
+
const validation = await validateCaseFile(file2, root);
|
|
30683
30928
|
infos.push({
|
|
30684
30929
|
file: file2,
|
|
30685
30930
|
valid: validation.ok,
|
|
@@ -30691,16 +30936,16 @@ async function collectCases(paths, options = {}) {
|
|
|
30691
30936
|
}
|
|
30692
30937
|
async function readCaseFile(file2, options = {}) {
|
|
30693
30938
|
const target = resolveAgainstRoot(options.root, file2);
|
|
30694
|
-
const source = await
|
|
30695
|
-
return { file: target, source, validation:
|
|
30939
|
+
const source = await readFile4(target, "utf8");
|
|
30940
|
+
return { file: target, source, validation: await validateCaseDataSource(source, target, resolveRoot(options.root)) };
|
|
30696
30941
|
}
|
|
30697
|
-
async function validateCaseFile(file2) {
|
|
30698
|
-
const source = await
|
|
30699
|
-
return
|
|
30942
|
+
async function validateCaseFile(file2, root) {
|
|
30943
|
+
const source = await readFile4(file2, "utf8");
|
|
30944
|
+
return validateCaseDataSource(source, file2, root);
|
|
30700
30945
|
}
|
|
30701
30946
|
async function writeCaseFile(file2, source, options = {}) {
|
|
30702
30947
|
const target = resolveAgainstRoot(options.root, file2);
|
|
30703
|
-
const validation =
|
|
30948
|
+
const validation = await validateCaseDataSource(source, target, resolveRoot(options.root));
|
|
30704
30949
|
if (!validation.ok) {
|
|
30705
30950
|
return { file: target, written: false, validation };
|
|
30706
30951
|
}
|
|
@@ -30710,18 +30955,44 @@ async function writeCaseFile(file2, source, options = {}) {
|
|
|
30710
30955
|
throw new Error(`\u7528\u4F8B\u6587\u4EF6\u5DF2\u5B58\u5728:${target}(\u8986\u76D6\u9700\u663E\u5F0F overwrite: true)`);
|
|
30711
30956
|
}
|
|
30712
30957
|
}
|
|
30713
|
-
await mkdir3(
|
|
30958
|
+
await mkdir3(dirname2(target), { recursive: true });
|
|
30714
30959
|
await writeFile3(target, source, "utf8");
|
|
30715
30960
|
return { file: target, written: true, validation };
|
|
30716
30961
|
}
|
|
30962
|
+
async function validateCaseDataSource(source, file2, root) {
|
|
30963
|
+
const validation = validateCaseSource(source);
|
|
30964
|
+
if (!validation.ok || !validation.data) return validation;
|
|
30965
|
+
try {
|
|
30966
|
+
await prepareCaseExecutions(
|
|
30967
|
+
{ data: validation.data, file: file2 },
|
|
30968
|
+
{ root, dataDir: join6(root, "tests", "e2e", "data") }
|
|
30969
|
+
);
|
|
30970
|
+
return validation;
|
|
30971
|
+
} catch (error62) {
|
|
30972
|
+
if (error62 instanceof TestPilotError && error62.code === "FIXTURE_MISSING") {
|
|
30973
|
+
return validation;
|
|
30974
|
+
}
|
|
30975
|
+
return {
|
|
30976
|
+
ok: false,
|
|
30977
|
+
data: validation.data,
|
|
30978
|
+
issues: [
|
|
30979
|
+
{
|
|
30980
|
+
path: "(root)",
|
|
30981
|
+
code: "semantic",
|
|
30982
|
+
message: error62 instanceof Error ? error62.message : String(error62)
|
|
30983
|
+
}
|
|
30984
|
+
]
|
|
30985
|
+
};
|
|
30986
|
+
}
|
|
30987
|
+
}
|
|
30717
30988
|
|
|
30718
30989
|
// ../../packages/sdk/src/reports.ts
|
|
30719
|
-
import { readFile as
|
|
30990
|
+
import { readFile as readFile8 } from "node:fs/promises";
|
|
30720
30991
|
import { join as join18 } from "node:path";
|
|
30721
30992
|
|
|
30722
30993
|
// ../../packages/reporter/src/report.ts
|
|
30723
30994
|
import { writeFile as writeFile4 } from "node:fs/promises";
|
|
30724
|
-
import { dirname as
|
|
30995
|
+
import { dirname as dirname3, join as join8, resolve as resolve4 } from "node:path";
|
|
30725
30996
|
|
|
30726
30997
|
// ../../packages/reporter/src/classify.ts
|
|
30727
30998
|
var FAILURE_CATEGORIES = [
|
|
@@ -30749,16 +31020,34 @@ function classifyFailure(message) {
|
|
|
30749
31020
|
}
|
|
30750
31021
|
return "other";
|
|
30751
31022
|
}
|
|
31023
|
+
var CODE_CATEGORIES = {
|
|
31024
|
+
ASSERTION_FAILED: "assert",
|
|
31025
|
+
REQUEST_COUNT_MISMATCH: "assert",
|
|
31026
|
+
STATUS_MISMATCH: "assert",
|
|
31027
|
+
STEP_TIMEOUT: "assert",
|
|
31028
|
+
ELEMENT_NOT_FOUND: "element",
|
|
31029
|
+
ADAPTER_NOT_FOUND: "adapter",
|
|
31030
|
+
UNSUPPORTED_ACTION: "adapter",
|
|
31031
|
+
CONFIG_MISSING: "adapter",
|
|
31032
|
+
ENVIRONMENT_UNAVAILABLE: "env",
|
|
31033
|
+
FIXTURE_MISSING: "env",
|
|
31034
|
+
ACCOUNT_UNAVAILABLE: "env",
|
|
31035
|
+
PREREQ_VARIABLE_MISSING: "other"
|
|
31036
|
+
};
|
|
31037
|
+
function classifyFromCode(code) {
|
|
31038
|
+
if (code === void 0) return void 0;
|
|
31039
|
+
return CODE_CATEGORIES[code];
|
|
31040
|
+
}
|
|
30752
31041
|
|
|
30753
31042
|
// ../../packages/reporter/src/history.ts
|
|
30754
|
-
import { readFile as
|
|
31043
|
+
import { readFile as readFile5, readdir as readdir2 } from "node:fs/promises";
|
|
30755
31044
|
import { join as join7 } from "node:path";
|
|
30756
31045
|
async function collectHistory(runsRoot2, skipRunId, limit = 12) {
|
|
30757
31046
|
const entries = (await readdir2(runsRoot2).catch(() => [])).filter((name) => /^\d{8}-\d{3}$/.test(name) && name !== skipRunId).sort().slice(-limit);
|
|
30758
31047
|
const points = [];
|
|
30759
31048
|
for (const runId of entries) {
|
|
30760
31049
|
try {
|
|
30761
|
-
const summary = JSON.parse(await
|
|
31050
|
+
const summary = JSON.parse(await readFile5(join7(runsRoot2, runId, "result.json"), "utf8"));
|
|
30762
31051
|
points.push({
|
|
30763
31052
|
runId,
|
|
30764
31053
|
startedAt: summary.startedAt,
|
|
@@ -30779,12 +31068,19 @@ function toSummaryView(summary) {
|
|
|
30779
31068
|
runId: summary.runId,
|
|
30780
31069
|
status: summary.status,
|
|
30781
31070
|
durationMs: summary.durationMs,
|
|
30782
|
-
cases: {
|
|
31071
|
+
cases: {
|
|
31072
|
+
templates: summary.totals.templates ?? summary.totals.cases,
|
|
31073
|
+
passed: summary.totals.passed,
|
|
31074
|
+
failed: summary.totals.failed,
|
|
31075
|
+
skipped: summary.totals.skipped ?? 0,
|
|
31076
|
+
warnings: summary.totals.warnings ?? 0
|
|
31077
|
+
},
|
|
30783
31078
|
steps: {
|
|
30784
31079
|
total: summary.totals.steps,
|
|
30785
31080
|
passed: summary.totals.stepsPassed,
|
|
30786
31081
|
failed: summary.totals.stepsFailed,
|
|
30787
|
-
skipped: summary.totals.stepsSkipped
|
|
31082
|
+
skipped: summary.totals.stepsSkipped,
|
|
31083
|
+
warning: summary.totals.stepsWarning ?? 0
|
|
30788
31084
|
}
|
|
30789
31085
|
};
|
|
30790
31086
|
}
|
|
@@ -30792,14 +31088,14 @@ function toSummaryView(summary) {
|
|
|
30792
31088
|
// ../../packages/reporter/src/report.ts
|
|
30793
31089
|
var HISTORY_LIMIT = 12;
|
|
30794
31090
|
async function writeReports(runDir, summary, options = {}) {
|
|
30795
|
-
const dir =
|
|
31091
|
+
const dir = resolve4(runDir);
|
|
30796
31092
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
30797
|
-
const history = options.history ?? await collectHistory(
|
|
31093
|
+
const history = options.history ?? await collectHistory(dirname3(dir), summary.runId, options.historyLimit ?? HISTORY_LIMIT);
|
|
30798
31094
|
const failures = [];
|
|
30799
31095
|
const cases = summary.cases.map((item) => {
|
|
30800
31096
|
const categories = /* @__PURE__ */ new Set();
|
|
30801
31097
|
const steps = item.steps.map((step) => {
|
|
30802
|
-
const category = step.status === "failed" ? classifyFailure(step.error) : void 0;
|
|
31098
|
+
const category = classifyFromCode(step.errorCode) ?? (step.status === "failed" ? classifyFailure(step.error) : void 0);
|
|
30803
31099
|
if (category) {
|
|
30804
31100
|
categories.add(category);
|
|
30805
31101
|
failures.push({
|
|
@@ -30807,7 +31103,8 @@ async function writeReports(runDir, summary, options = {}) {
|
|
|
30807
31103
|
label: categoryLabel(category),
|
|
30808
31104
|
caseId: item.caseId,
|
|
30809
31105
|
index: step.index,
|
|
30810
|
-
message: step.error ?? ""
|
|
31106
|
+
message: step.error ?? "",
|
|
31107
|
+
verdict: verdictFromErrorCode(step.errorCode ?? item.errorCode)
|
|
30811
31108
|
});
|
|
30812
31109
|
}
|
|
30813
31110
|
return { ...step, category };
|
|
@@ -30830,6 +31127,20 @@ async function writeReports(runDir, summary, options = {}) {
|
|
|
30830
31127
|
await writeFile4(htmlPath, renderHtml(data), "utf8");
|
|
30831
31128
|
return { json: jsonPath, html: htmlPath };
|
|
30832
31129
|
}
|
|
31130
|
+
var VERDICT_GROUPS = [
|
|
31131
|
+
{ verdict: "product", title: "\u4EA7\u54C1\u5931\u8D25" },
|
|
31132
|
+
{ verdict: "infra", title: "\u73AF\u5883\u4E0E\u524D\u7F6E(\u975E\u4EA7\u54C1\u56DE\u5F52)" },
|
|
31133
|
+
{ verdict: "unverified", title: "\u672A\u9A8C\u8BC1" }
|
|
31134
|
+
];
|
|
31135
|
+
function verdictGroupsHtml(data) {
|
|
31136
|
+
return VERDICT_GROUPS.map((group) => {
|
|
31137
|
+
const items = data.failures.filter((failure2) => failure2.verdict === group.verdict);
|
|
31138
|
+
const list = items.length > 0 ? items.map(
|
|
31139
|
+
(failure2) => `<li><code>${escapeHtml(failure2.caseId)}</code> #${failure2.index} ${escapeHtml(failure2.message.slice(0, 160))}</li>`
|
|
31140
|
+
).join("\n") : '<li class="muted">\u65E0</li>';
|
|
31141
|
+
return `<div class="verdict-group"><h3>${escapeHtml(group.title)}(${items.length})</h3><ul>${list}</ul></div>`;
|
|
31142
|
+
}).join("\n");
|
|
31143
|
+
}
|
|
30833
31144
|
function categoryChips(data) {
|
|
30834
31145
|
const counts = /* @__PURE__ */ new Map();
|
|
30835
31146
|
for (const failure2 of data.failures) {
|
|
@@ -30853,21 +31164,22 @@ function renderHtml(data) {
|
|
|
30853
31164
|
const view = toSummaryView(data.summary);
|
|
30854
31165
|
const summary = data.summary;
|
|
30855
31166
|
const trendSvg = renderTrend(data);
|
|
30856
|
-
const casesHtml = summary.cases
|
|
31167
|
+
const casesHtml = groupedCasesHtml(summary.cases);
|
|
30857
31168
|
const chips = categoryChips(data);
|
|
31169
|
+
const verdictHtml = verdictGroupsHtml(data);
|
|
30858
31170
|
return `<!doctype html>
|
|
30859
31171
|
<html lang="zh-CN">
|
|
30860
31172
|
<head>
|
|
30861
31173
|
<meta charset="utf-8" />
|
|
30862
31174
|
<title>TestPilot Report ${escapeHtml(summary.runId)}</title>
|
|
30863
31175
|
<style>
|
|
30864
|
-
:root { --ok:#1a7f37; --fail:#cf222e; --skip:#6e7781; --line:#d0d7de; --muted:#59636e; }
|
|
31176
|
+
:root { --ok:#1a7f37; --fail:#cf222e; --warn:#9a6700; --skip:#6e7781; --blocked:#8250df; --inconclusive:#9a6700; --line:#d0d7de; --muted:#59636e; }
|
|
30865
31177
|
* { box-sizing: border-box; }
|
|
30866
31178
|
body { font-family: system-ui, "Segoe UI", sans-serif; margin: 0; color: #1f2328; background: #f6f8fa; }
|
|
30867
31179
|
.wrap { max-width: 1080px; margin: 0 auto; padding: 20px 16px 48px; }
|
|
30868
31180
|
h1 { font-size: 20px; margin: 8px 0; } h2 { font-size: 15px; margin: 24px 0 8px; }
|
|
30869
31181
|
.badge { display: inline-block; padding: 2px 10px; border-radius: 10px; color: #fff; font-size: 12px; }
|
|
30870
|
-
.passed { background: var(--ok); } .failed { background: var(--fail); } .skipped { background: var(--skip); }
|
|
31182
|
+
.passed { background: var(--ok); } .failed { background: var(--fail); } .warning { background: var(--warn); } .skipped { background: var(--skip); } .blocked { background: var(--blocked); } .inconclusive { background: var(--inconclusive); }
|
|
30871
31183
|
code { background: #eef1f4; padding: 1px 5px; border-radius: 4px; font-size: 12px; }
|
|
30872
31184
|
.cards { display: flex; gap: 12px; flex-wrap: wrap; margin: 14px 0; }
|
|
30873
31185
|
.card { background: #fff; border: 1px solid var(--line); border-radius: 8px; padding: 10px 16px; min-width: 130px; }
|
|
@@ -30880,6 +31192,8 @@ function renderHtml(data) {
|
|
|
30880
31192
|
.btn { border: 1px solid var(--line); background: #fff; border-radius: 6px; padding: 4px 10px; font-size: 12px; cursor: pointer; }
|
|
30881
31193
|
#count { color: var(--muted); font-size: 12px; margin-left: auto; }
|
|
30882
31194
|
details.case { background: #fff; border: 1px solid var(--line); border-radius: 8px; margin: 10px 0; }
|
|
31195
|
+
section.template { border-left: 3px solid var(--line); padding-left: 12px; margin: 14px 0; }
|
|
31196
|
+
section.template > h3 { font-size: 14px; margin: 0 0 6px; }
|
|
30883
31197
|
details.case > summary { cursor: pointer; padding: 10px 14px; font-size: 14px; list-style: none; }
|
|
30884
31198
|
details.case > summary::-webkit-details-marker { display: none; }
|
|
30885
31199
|
details.case > summary:hover { background: #f6f8fa; }
|
|
@@ -30892,6 +31206,11 @@ function renderHtml(data) {
|
|
|
30892
31206
|
.muted { color: var(--muted); }
|
|
30893
31207
|
.hide { display: none !important; }
|
|
30894
31208
|
.meta { color: var(--muted); font-size: 12px; margin: 6px 0; }
|
|
31209
|
+
.verdicts { display: flex; gap: 14px; flex-wrap: wrap; }
|
|
31210
|
+
.verdict-group { background: #fff; border: 1px solid var(--line); border-radius: 8px; padding: 10px 14px; flex: 1; min-width: 220px; }
|
|
31211
|
+
.verdict-group h3 { font-size: 13px; margin: 0 0 6px; }
|
|
31212
|
+
.verdict-group ul { margin: 0; padding-left: 18px; font-size: 12px; }
|
|
31213
|
+
.verdict-group li { margin: 3px 0; }
|
|
30895
31214
|
</style>
|
|
30896
31215
|
</head>
|
|
30897
31216
|
<body>
|
|
@@ -30902,6 +31221,8 @@ function renderHtml(data) {
|
|
|
30902
31221
|
<div class="cards">
|
|
30903
31222
|
<div class="card"><b>${view.cases.passed}<span style="color:var(--ok)"> \u2713</span></b><span>\u901A\u8FC7\u7528\u4F8B</span></div>
|
|
30904
31223
|
<div class="card"><b>${view.cases.failed}<span style="color:var(--fail)"> \u2717</span></b><span>\u5931\u8D25\u7528\u4F8B</span></div>
|
|
31224
|
+
<div class="card"><b>${view.cases.skipped} \u21B7</b><span>\u8DF3\u8FC7\u6570\u636E\u884C</span></div>
|
|
31225
|
+
<div class="card"><b>${view.cases.warnings} \u26A0</b><span>\u6E05\u7406\u8B66\u544A</span></div>
|
|
30905
31226
|
<div class="card"><b>${view.steps.passed}<span style="color:var(--ok)"> \u2713</span></b><span>\u901A\u8FC7\u6B65\u9AA4</span></div>
|
|
30906
31227
|
<div class="card"><b>${view.steps.failed}<span style="color:var(--fail)"> \u2717</span></b><span>\u5931\u8D25\u6B65\u9AA4</span></div>
|
|
30907
31228
|
<div class="card"><b>${view.steps.skipped} \u21B7</b><span>\u8DF3\u8FC7\u6B65\u9AA4</span></div>
|
|
@@ -30910,6 +31231,9 @@ function renderHtml(data) {
|
|
|
30910
31231
|
<h2>\u5386\u53F2\u8D8B\u52BF(\u6700\u8FD1 ${data.history.length + 1} \u6B21\u8FD0\u884C)</h2>
|
|
30911
31232
|
<div class="trend">${trendSvg}</div>
|
|
30912
31233
|
|
|
31234
|
+
<h2>\u5931\u8D25\u4E09\u6876</h2>
|
|
31235
|
+
<div class="verdicts">${verdictHtml}</div>
|
|
31236
|
+
|
|
30913
31237
|
<h2>\u5931\u8D25\u5206\u7C7B</h2>
|
|
30914
31238
|
<div class="toolbar" id="cat-chips">${chips}</div>
|
|
30915
31239
|
|
|
@@ -30988,26 +31312,44 @@ function renderHtml(data) {
|
|
|
30988
31312
|
</body>
|
|
30989
31313
|
</html>`;
|
|
30990
31314
|
}
|
|
31315
|
+
function groupedCasesHtml(cases) {
|
|
31316
|
+
const groups = /* @__PURE__ */ new Map();
|
|
31317
|
+
for (const item of cases) {
|
|
31318
|
+
const current = groups.get(item.caseId) ?? [];
|
|
31319
|
+
current.push(item);
|
|
31320
|
+
groups.set(item.caseId, current);
|
|
31321
|
+
}
|
|
31322
|
+
return [...groups.entries()].map(
|
|
31323
|
+
([caseId, rows]) => `<section class="template">
|
|
31324
|
+
<h3>${escapeHtml(caseId)} <span class="muted">\xB7 ${rows.length} \u4E2A\u6570\u636E\u884C</span></h3>
|
|
31325
|
+
${rows.map((item) => caseHtml(item)).join("\n")}
|
|
31326
|
+
</section>`
|
|
31327
|
+
).join("\n");
|
|
31328
|
+
}
|
|
30991
31329
|
function caseHtml(item) {
|
|
30992
31330
|
const rows = item.steps.map((step) => stepHtml(step)).join("\n");
|
|
30993
31331
|
const errorLine = item.error ? `<p class="error">${escapeHtml(item.error)}</p>` : "";
|
|
31332
|
+
const skipLine = item.skipReason ? `<p class="muted">${escapeHtml(item.skipReason)}${item.missingDependencies?.length ? `: ${escapeHtml(item.missingDependencies.join(", "))}` : ""}</p>` : "";
|
|
31333
|
+
const warningLine = item.warnings?.length ? `<p class="error">\u26A0 ${item.warnings.map(escapeHtml).join("<br />\u26A0 ")}</p>` : "";
|
|
30994
31334
|
const links = [];
|
|
30995
31335
|
if (item.video) links.push(`<a href="${escapeHtml(item.video)}">\u89C6\u9891</a>`);
|
|
30996
31336
|
if (item.trace) links.push(`<a href="${escapeHtml(item.trace)}">Trace</a>`);
|
|
30997
31337
|
const linksLine = links.length > 0 ? `<p>${links.join(" \xB7 ")}</p>` : "";
|
|
30998
|
-
const search = escapeHtml(`${item.caseId} ${item.caseName} ${item.file}`).replaceAll('"', """);
|
|
31338
|
+
const search = escapeHtml(`${item.caseId} ${item.rowId ?? "default"} ${item.caseName} ${item.file}`).replaceAll('"', """);
|
|
30999
31339
|
const cats = escapeHtml(item.categories.join(","));
|
|
31000
31340
|
return `
|
|
31001
31341
|
<details class="case" data-status="${item.status}" data-cats="${cats}" data-search="${search}">
|
|
31002
31342
|
<summary>
|
|
31003
31343
|
<span class="badge ${item.status}">${item.status}</span>
|
|
31004
|
-
<b
|
|
31344
|
+
<b>\u6570\u636E\u884C ${escapeHtml(item.rowId ?? "default")}</b> \xB7 ${escapeHtml(item.caseName)}
|
|
31005
31345
|
<span class="muted">\xB7 ${item.steps.length} \u6B65 \xB7 ${(item.durationMs / 1e3).toFixed(1)}s</span>
|
|
31006
31346
|
</summary>
|
|
31007
31347
|
${errorLine}
|
|
31348
|
+
${skipLine}
|
|
31349
|
+
${warningLine}
|
|
31008
31350
|
${linksLine}
|
|
31009
31351
|
<table>
|
|
31010
|
-
<tr><th>#</th><th>target</th><th>action</th><th>\u72B6\u6001</th><th>\u8017\u65F6</th><th>\u8BE6\u60C5</th></tr>
|
|
31352
|
+
<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>
|
|
31011
31353
|
${rows}
|
|
31012
31354
|
</table>
|
|
31013
31355
|
</details>`;
|
|
@@ -31022,8 +31364,14 @@ function stepHtml(step) {
|
|
|
31022
31364
|
}
|
|
31023
31365
|
}
|
|
31024
31366
|
if (step.screenshot) details.push(`<a href="${escapeHtml(step.screenshot)}">\u622A\u56FE</a>`);
|
|
31367
|
+
for (const assertion of step.requestAssertions ?? []) {
|
|
31368
|
+
details.push(
|
|
31369
|
+
`\u8BF7\u6C42 ${escapeHtml(assertion.method ?? "*")} <code>${escapeHtml(assertion.urlContains)}</code>: ${assertion.actualCount}/${assertion.count}`
|
|
31370
|
+
);
|
|
31371
|
+
}
|
|
31025
31372
|
return `<tr>
|
|
31026
31373
|
<td class="num">${step.index}</td>
|
|
31374
|
+
<td>${escapeHtml(step.phase ?? "steps")}</td>
|
|
31027
31375
|
<td>${escapeHtml(step.target)}</td>
|
|
31028
31376
|
<td>${escapeHtml(step.action)}</td>
|
|
31029
31377
|
<td><span class="badge ${step.status}">${step.status}</span></td>
|
|
@@ -31080,7 +31428,7 @@ function embedJson(data) {
|
|
|
31080
31428
|
|
|
31081
31429
|
// ../../packages/reporter/src/summary-report.ts
|
|
31082
31430
|
import { mkdir as mkdir4, writeFile as writeFile5 } from "node:fs/promises";
|
|
31083
|
-
import { join as join9, resolve as
|
|
31431
|
+
import { join as join9, resolve as resolve5 } from "node:path";
|
|
31084
31432
|
function escapeHtml2(value) {
|
|
31085
31433
|
return String(value).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
31086
31434
|
}
|
|
@@ -31094,10 +31442,12 @@ function buildSummaryData(runs) {
|
|
|
31094
31442
|
for (const item of summary.cases) {
|
|
31095
31443
|
const platforms = [...new Set(item.steps.map((step) => step.target))];
|
|
31096
31444
|
const failedStep = item.steps.find((step) => step.status === "failed");
|
|
31097
|
-
const category = item.status === "failed" ? classifyFailure(failedStep?.error) : void 0;
|
|
31445
|
+
const category = classifyFromCode(item.errorCode ?? failedStep?.errorCode) ?? (item.status === "failed" ? classifyFailure(failedStep?.error) : void 0);
|
|
31098
31446
|
executions.push({
|
|
31099
31447
|
runId,
|
|
31100
31448
|
caseId: item.caseId,
|
|
31449
|
+
rowId: item.rowId,
|
|
31450
|
+
identity: item.rowId && item.rowId !== "default" ? `${item.caseId}#${item.rowId}` : item.caseId,
|
|
31101
31451
|
caseName: item.caseName,
|
|
31102
31452
|
status: item.status,
|
|
31103
31453
|
category,
|
|
@@ -31115,10 +31465,11 @@ function buildSummaryData(runs) {
|
|
|
31115
31465
|
}
|
|
31116
31466
|
const byCase = /* @__PURE__ */ new Map();
|
|
31117
31467
|
for (const exec2 of executions) {
|
|
31118
|
-
let agg = byCase.get(exec2.
|
|
31468
|
+
let agg = byCase.get(exec2.identity);
|
|
31119
31469
|
if (!agg) {
|
|
31120
31470
|
agg = {
|
|
31121
31471
|
caseId: exec2.caseId,
|
|
31472
|
+
rowId: exec2.rowId,
|
|
31122
31473
|
caseName: exec2.caseName,
|
|
31123
31474
|
platforms: [],
|
|
31124
31475
|
runs: 0,
|
|
@@ -31128,17 +31479,18 @@ function buildSummaryData(runs) {
|
|
|
31128
31479
|
lastStatus: "unknown",
|
|
31129
31480
|
flaky: false
|
|
31130
31481
|
};
|
|
31131
|
-
byCase.set(exec2.
|
|
31482
|
+
byCase.set(exec2.identity, agg);
|
|
31132
31483
|
}
|
|
31133
31484
|
agg.runs++;
|
|
31134
31485
|
if (exec2.status === "passed") agg.passed++;
|
|
31135
|
-
else agg.failed++;
|
|
31486
|
+
else if (exec2.status === "failed") agg.failed++;
|
|
31136
31487
|
for (const platform of exec2.platforms) {
|
|
31137
31488
|
if (!agg.platforms.includes(platform)) agg.platforms.push(platform);
|
|
31138
31489
|
}
|
|
31139
31490
|
}
|
|
31140
31491
|
for (const agg of byCase.values()) {
|
|
31141
|
-
const
|
|
31492
|
+
const identity = agg.rowId && agg.rowId !== "default" ? `${agg.caseId}#${agg.rowId}` : agg.caseId;
|
|
31493
|
+
const last = [...executions].reverse().find((exec2) => exec2.identity === identity);
|
|
31142
31494
|
agg.lastStatus = last?.status ?? "unknown";
|
|
31143
31495
|
agg.lastCategory = last?.category;
|
|
31144
31496
|
agg.passRate = agg.runs > 0 ? agg.passed / agg.runs : 0;
|
|
@@ -31153,6 +31505,10 @@ function buildSummaryData(runs) {
|
|
|
31153
31505
|
})).filter((item) => item.count > 0);
|
|
31154
31506
|
const durations = runs.map((item) => item.summary.durationMs).filter((value) => typeof value === "number");
|
|
31155
31507
|
return {
|
|
31508
|
+
buckets: {
|
|
31509
|
+
blocked: executions.filter((exec2) => exec2.status === "blocked").length,
|
|
31510
|
+
inconclusive: executions.filter((exec2) => exec2.status === "inconclusive").length
|
|
31511
|
+
},
|
|
31156
31512
|
window: {
|
|
31157
31513
|
runs: runs.length,
|
|
31158
31514
|
from: runPoints[0]?.startedAt,
|
|
@@ -31237,7 +31593,7 @@ function renderSummaryHtml(data) {
|
|
|
31237
31593
|
th.num, td.num { text-align: right; }
|
|
31238
31594
|
code { background: #eef1f4; padding: 1px 5px; border-radius: 4px; font-size: 12px; cursor: pointer; }
|
|
31239
31595
|
.badge { display: inline-block; padding: 1px 9px; border-radius: 10px; color: #fff; font-size: 11px; }
|
|
31240
|
-
.passed { background: #1a7f37; } .failed { background: #cf222e; } .unknown { background: #6e7781; }
|
|
31596
|
+
.passed { background: #1a7f37; } .failed { background: #cf222e; } .unknown { background: #6e7781; } .blocked { background: #8250df; } .inconclusive { background: #9a6700; }
|
|
31241
31597
|
.toolbar { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin: 10px 0; }
|
|
31242
31598
|
.chip { border: 1px solid var(--line); background: #fff; border-radius: 14px; padding: 3px 12px; font-size: 12px; cursor: pointer; }
|
|
31243
31599
|
.chip.active { background: #0969da; color: #fff; border-color: #0969da; }
|
|
@@ -31263,6 +31619,8 @@ function renderSummaryHtml(data) {
|
|
|
31263
31619
|
<div class="card"><b>${rateText}</b><span>\u6574\u4F53\u901A\u8FC7\u7387</span></div>
|
|
31264
31620
|
<div class="card"><b>${(kpis.avgDurationMs / 1e3).toFixed(1)}s</b><span>\u5E73\u5747\u8FD0\u884C\u8017\u65F6</span></div>
|
|
31265
31621
|
<div class="card"><b>${kpis.flakyCount}</b><span>Flaky \u7528\u4F8B</span></div>
|
|
31622
|
+
${data.buckets.blocked > 0 ? `<div class="card"><b>${data.buckets.blocked} \u2298</b><span>\u73AF\u5883/\u524D\u7F6E\u963B\u585E(\u975E\u4EA7\u54C1)</span></div>` : ""}
|
|
31623
|
+
${data.buckets.inconclusive > 0 ? `<div class="card"><b>${data.buckets.inconclusive} ?</b><span>\u672A\u9A8C\u8BC1(\u8BC1\u636E\u4E0D\u8DB3)</span></div>` : ""}
|
|
31266
31624
|
</div>
|
|
31267
31625
|
|
|
31268
31626
|
<h2>\u8D8B\u52BF(\u6BCF\u6B21\u8FD0\u884C\u7ED3\u679C\u4E0E\u901A\u8FC7\u7387,\u60AC\u6D6E\u770B\u8BE6\u60C5)</h2>
|
|
@@ -31349,7 +31707,7 @@ function renderSummaryHtml(data) {
|
|
|
31349
31707
|
</html>`;
|
|
31350
31708
|
}
|
|
31351
31709
|
async function writeSummaryReport(artifactsDir, data) {
|
|
31352
|
-
const dir =
|
|
31710
|
+
const dir = resolve5(artifactsDir);
|
|
31353
31711
|
await mkdir4(dir, { recursive: true });
|
|
31354
31712
|
const htmlPath = join9(dir, "summary.html");
|
|
31355
31713
|
const jsonPath = join9(dir, "summary.json");
|
|
@@ -31359,8 +31717,8 @@ async function writeSummaryReport(artifactsDir, data) {
|
|
|
31359
31717
|
}
|
|
31360
31718
|
|
|
31361
31719
|
// ../../packages/sdk/src/runs.ts
|
|
31362
|
-
import { readFile as
|
|
31363
|
-
import { isAbsolute as
|
|
31720
|
+
import { readFile as readFile7, readdir as readdir4 } from "node:fs/promises";
|
|
31721
|
+
import { isAbsolute as isAbsolute3, join as join17 } from "node:path";
|
|
31364
31722
|
|
|
31365
31723
|
// ../../packages/execution-engine/src/artifacts/index.ts
|
|
31366
31724
|
import { mkdir as mkdir9 } from "node:fs/promises";
|
|
@@ -31368,7 +31726,7 @@ import { join as join13 } from "node:path";
|
|
|
31368
31726
|
|
|
31369
31727
|
// ../../packages/evidence/src/log.ts
|
|
31370
31728
|
import { appendFile, mkdir as mkdir5 } from "node:fs/promises";
|
|
31371
|
-
import { dirname as
|
|
31729
|
+
import { dirname as dirname4 } from "node:path";
|
|
31372
31730
|
var RunLogger = class {
|
|
31373
31731
|
constructor(filePath) {
|
|
31374
31732
|
this.filePath = filePath;
|
|
@@ -31383,7 +31741,7 @@ var RunLogger = class {
|
|
|
31383
31741
|
return this.write("ERROR", message);
|
|
31384
31742
|
}
|
|
31385
31743
|
async write(level, message) {
|
|
31386
|
-
await mkdir5(
|
|
31744
|
+
await mkdir5(dirname4(this.filePath), { recursive: true });
|
|
31387
31745
|
const line = `${(/* @__PURE__ */ new Date()).toISOString()} ${level} ${message}
|
|
31388
31746
|
`;
|
|
31389
31747
|
await appendFile(this.filePath, line, "utf8");
|
|
@@ -31392,30 +31750,30 @@ var RunLogger = class {
|
|
|
31392
31750
|
|
|
31393
31751
|
// ../../packages/evidence/src/screenshot.ts
|
|
31394
31752
|
import { mkdir as mkdir6, writeFile as writeFile6 } from "node:fs/promises";
|
|
31395
|
-
import { dirname as
|
|
31753
|
+
import { dirname as dirname5, join as join10 } from "node:path";
|
|
31396
31754
|
async function saveScreenshot(dir, fileName, data) {
|
|
31397
31755
|
const file2 = join10(dir, fileName);
|
|
31398
|
-
await mkdir6(
|
|
31756
|
+
await mkdir6(dirname5(file2), { recursive: true });
|
|
31399
31757
|
await writeFile6(file2, data);
|
|
31400
31758
|
return file2;
|
|
31401
31759
|
}
|
|
31402
31760
|
|
|
31403
31761
|
// ../../packages/evidence/src/trace.ts
|
|
31404
31762
|
import { mkdir as mkdir7, writeFile as writeFile7 } from "node:fs/promises";
|
|
31405
|
-
import { dirname as
|
|
31763
|
+
import { dirname as dirname6, join as join11 } from "node:path";
|
|
31406
31764
|
async function saveTrace(dir, fileName, data) {
|
|
31407
31765
|
const file2 = join11(dir, fileName);
|
|
31408
|
-
await mkdir7(
|
|
31766
|
+
await mkdir7(dirname6(file2), { recursive: true });
|
|
31409
31767
|
await writeFile7(file2, data);
|
|
31410
31768
|
return file2;
|
|
31411
31769
|
}
|
|
31412
31770
|
|
|
31413
31771
|
// ../../packages/evidence/src/video.ts
|
|
31414
31772
|
import { mkdir as mkdir8, writeFile as writeFile8 } from "node:fs/promises";
|
|
31415
|
-
import { dirname as
|
|
31773
|
+
import { dirname as dirname7, join as join12 } from "node:path";
|
|
31416
31774
|
async function saveVideo(dir, fileName, data) {
|
|
31417
31775
|
const file2 = join12(dir, fileName);
|
|
31418
|
-
await mkdir8(
|
|
31776
|
+
await mkdir8(dirname7(file2), { recursive: true });
|
|
31419
31777
|
await writeFile8(file2, data);
|
|
31420
31778
|
return file2;
|
|
31421
31779
|
}
|
|
@@ -31465,15 +31823,29 @@ var ExecutionContext = class {
|
|
|
31465
31823
|
get(name) {
|
|
31466
31824
|
return this.variables.get(name);
|
|
31467
31825
|
}
|
|
31826
|
+
seed(values) {
|
|
31827
|
+
for (const [name, value] of Object.entries(values)) {
|
|
31828
|
+
this.variables.set(name, value);
|
|
31829
|
+
}
|
|
31830
|
+
}
|
|
31468
31831
|
/**
|
|
31469
|
-
* 将字符串中的 ${name}
|
|
31470
|
-
*
|
|
31832
|
+
* 将字符串中的 ${name} 替换为已提取的变量值;未定义的引用抛 TestPilotError——
|
|
31833
|
+
* 校验阶段已静态拦截"从未声明"的引用,运行时未命中即数据未兑现
|
|
31834
|
+
* (前置 extract 未执行或 fixture/dataset/凭据缺失),按三态归 unverified/account 桶。
|
|
31835
|
+
* 凭据桶仅限 `account` 整值或 `account.*` 字段;用户变量撞 account 前缀(如 accountId)
|
|
31836
|
+
* 不误归凭据桶。名字允许点号,用于引擎注入的 account.username 一类凭据变量
|
|
31837
|
+
* (用户 extract 变量名不含点号)。
|
|
31471
31838
|
*/
|
|
31472
31839
|
resolve(template) {
|
|
31473
|
-
return template.replace(
|
|
31474
|
-
|
|
31475
|
-
(
|
|
31476
|
-
|
|
31840
|
+
return template.replace(/\$\{([A-Za-z_][A-Za-z0-9_.]*)\}/g, (_match, name) => {
|
|
31841
|
+
const value = this.variables.get(name);
|
|
31842
|
+
if (value !== void 0) return value;
|
|
31843
|
+
const isAccount = name === "account" || name.startsWith("account.");
|
|
31844
|
+
throw new TestPilotError(
|
|
31845
|
+
isAccount ? "ACCOUNT_UNAVAILABLE" : "PREREQ_VARIABLE_MISSING",
|
|
31846
|
+
`\u53D8\u91CF "${name}" \u672A\u5B9A\u4E49(\u524D\u7F6E extract \u672A\u6267\u884C\u6216 fixture/dataset/\u51ED\u636E\u7F3A\u5931)`
|
|
31847
|
+
);
|
|
31848
|
+
});
|
|
31477
31849
|
}
|
|
31478
31850
|
/**
|
|
31479
31851
|
* 注入 accountRef 凭据:值为 JSON 对象时按 `${account.<字段>}` 展开为字符串,
|
|
@@ -31529,7 +31901,7 @@ async function ensureRunDir(runsRoot2, runId) {
|
|
|
31529
31901
|
|
|
31530
31902
|
// ../../packages/execution-engine/src/events/index.ts
|
|
31531
31903
|
import { appendFile as appendFile2, mkdir as mkdir11 } from "node:fs/promises";
|
|
31532
|
-
import { dirname as
|
|
31904
|
+
import { dirname as dirname8 } from "node:path";
|
|
31533
31905
|
var EventBus = class {
|
|
31534
31906
|
subscribers = [];
|
|
31535
31907
|
subscribe(subscriber) {
|
|
@@ -31543,7 +31915,7 @@ var EventBus = class {
|
|
|
31543
31915
|
};
|
|
31544
31916
|
function ndjsonSubscriber(filePath) {
|
|
31545
31917
|
return async (event) => {
|
|
31546
|
-
await mkdir11(
|
|
31918
|
+
await mkdir11(dirname8(filePath), { recursive: true });
|
|
31547
31919
|
await appendFile2(filePath, `${JSON.stringify(event)}
|
|
31548
31920
|
`, "utf8");
|
|
31549
31921
|
};
|
|
@@ -31624,11 +31996,19 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
|
|
|
31624
31996
|
status: "passed",
|
|
31625
31997
|
durationMs: 0
|
|
31626
31998
|
};
|
|
31999
|
+
let captureStarted = false;
|
|
31627
32000
|
try {
|
|
31628
32001
|
const locator = step.locator ? {
|
|
31629
32002
|
text: step.locator.text !== void 0 ? context.resolve(step.locator.text) : void 0,
|
|
31630
32003
|
css: step.locator.css !== void 0 ? context.resolve(step.locator.css) : void 0
|
|
31631
32004
|
} : void 0;
|
|
32005
|
+
if (step.expectRequests) {
|
|
32006
|
+
if (!adapter.startRequestCapture || !adapter.stopRequestCapture) {
|
|
32007
|
+
throw new TestPilotError("UNSUPPORTED_ACTION", `\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301\u8BF7\u6C42\u6B21\u6570\u65AD\u8A00`);
|
|
32008
|
+
}
|
|
32009
|
+
await adapter.startRequestCapture();
|
|
32010
|
+
captureStarted = true;
|
|
32011
|
+
}
|
|
31632
32012
|
switch (step.action) {
|
|
31633
32013
|
case "launch":
|
|
31634
32014
|
await adapter.launch();
|
|
@@ -31650,7 +32030,7 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
|
|
|
31650
32030
|
break;
|
|
31651
32031
|
case "assert":
|
|
31652
32032
|
if (step.target === "api") {
|
|
31653
|
-
if (!adapter.assertResponse) throw new
|
|
32033
|
+
if (!adapter.assertResponse) throw new TestPilotError("UNSUPPORTED_ACTION", "\u5F53\u524D adapter \u4E0D\u652F\u6301 api \u65AD\u8A00");
|
|
31654
32034
|
await adapter.assertResponse(context.resolve(step.expected ?? ""));
|
|
31655
32035
|
} else {
|
|
31656
32036
|
await adapter.assert(locator, context.resolve(step.expected ?? ""));
|
|
@@ -31670,7 +32050,7 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
|
|
|
31670
32050
|
break;
|
|
31671
32051
|
}
|
|
31672
32052
|
case "request": {
|
|
31673
|
-
if (!adapter.request) throw new
|
|
32053
|
+
if (!adapter.request) throw new TestPilotError("UNSUPPORTED_ACTION", "\u5F53\u524D adapter \u4E0D\u652F\u6301 api request");
|
|
31674
32054
|
const method = step.method ?? "GET";
|
|
31675
32055
|
const url2 = context.resolve(step.url ?? "");
|
|
31676
32056
|
const headers = resolveHeaders(step.headers, context);
|
|
@@ -31687,7 +32067,8 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
|
|
|
31687
32067
|
if (step.expected !== void 0) {
|
|
31688
32068
|
const expectedStatus = context.resolve(step.expected);
|
|
31689
32069
|
if (String(response.status) !== expectedStatus) {
|
|
31690
|
-
throw new
|
|
32070
|
+
throw new TestPilotError(
|
|
32071
|
+
"STATUS_MISMATCH",
|
|
31691
32072
|
`HTTP \u72B6\u6001\u7801 ${response.status} ${response.statusText},\u671F\u671B ${expectedStatus}:${snippet(response.bodyText, 200)}`
|
|
31692
32073
|
);
|
|
31693
32074
|
}
|
|
@@ -31696,12 +32077,36 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
|
|
|
31696
32077
|
}
|
|
31697
32078
|
default: {
|
|
31698
32079
|
const exhaustive = step.action;
|
|
31699
|
-
throw new
|
|
32080
|
+
throw new TestPilotError("UNSUPPORTED_ACTION", `\u4E0D\u652F\u6301\u7684 action:${String(exhaustive)}`);
|
|
32081
|
+
}
|
|
32082
|
+
}
|
|
32083
|
+
if (step.expectRequests) {
|
|
32084
|
+
const windowMs = Math.max(...step.expectRequests.map((item) => item.windowMs));
|
|
32085
|
+
if (windowMs > 0) await delay(windowMs);
|
|
32086
|
+
const requests = await adapter.stopRequestCapture();
|
|
32087
|
+
captureStarted = false;
|
|
32088
|
+
result.requestAssertions = buildRequestAssertions(step, requests, context);
|
|
32089
|
+
const mismatches = result.requestAssertions.filter((item) => item.actualCount !== item.count);
|
|
32090
|
+
if (mismatches.length > 0) {
|
|
32091
|
+
throw new TestPilotError(
|
|
32092
|
+
"REQUEST_COUNT_MISMATCH",
|
|
32093
|
+
mismatches.map(
|
|
32094
|
+
(item) => `\u8BF7\u6C42\u6B21\u6570\u4E0D\u5339\u914D:${item.method ?? "*"} ${item.urlContains},\u5B9E\u9645 ${item.actualCount},\u671F\u671B ${item.count}`
|
|
32095
|
+
).join("; ")
|
|
32096
|
+
);
|
|
31700
32097
|
}
|
|
31701
32098
|
}
|
|
31702
32099
|
} catch (err) {
|
|
32100
|
+
if (captureStarted) {
|
|
32101
|
+
try {
|
|
32102
|
+
await adapter.stopRequestCapture?.();
|
|
32103
|
+
} catch {
|
|
32104
|
+
}
|
|
32105
|
+
captureStarted = false;
|
|
32106
|
+
}
|
|
31703
32107
|
result.status = "failed";
|
|
31704
32108
|
result.error = err instanceof Error ? err.message : String(err);
|
|
32109
|
+
result.errorCode = err instanceof TestPilotError ? err.code : void 0;
|
|
31705
32110
|
try {
|
|
31706
32111
|
const data = await adapter.screenshot();
|
|
31707
32112
|
result.screenshot = await artifacts.saveStepScreenshot(`${options.screenshotName}-failed.png`, data);
|
|
@@ -31712,8 +32117,8 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
|
|
|
31712
32117
|
return result;
|
|
31713
32118
|
}
|
|
31714
32119
|
async function extractApiResponse(adapter, path) {
|
|
31715
|
-
if (!adapter.extractResponse) throw new
|
|
31716
|
-
if (!path) throw new
|
|
32120
|
+
if (!adapter.extractResponse) throw new TestPilotError("UNSUPPORTED_ACTION", "\u5F53\u524D adapter \u4E0D\u652F\u6301 api \u63D0\u53D6");
|
|
32121
|
+
if (!path) throw new TestPilotError("UNSUPPORTED_ACTION", "api \u7AEF extract \u9700\u8981\u63D0\u4F9B value(\u70B9\u53F7 JSON path)");
|
|
31717
32122
|
return adapter.extractResponse(path);
|
|
31718
32123
|
}
|
|
31719
32124
|
function resolveHeaders(headers, context) {
|
|
@@ -31744,92 +32149,216 @@ function resolveDeep(value, context) {
|
|
|
31744
32149
|
function snippet(text, limit = SNIPPET_LIMIT) {
|
|
31745
32150
|
return text.length > limit ? `${text.slice(0, limit)}\u2026` : text;
|
|
31746
32151
|
}
|
|
32152
|
+
function buildRequestAssertions(step, requests, context) {
|
|
32153
|
+
return (step.expectRequests ?? []).map((expectation) => {
|
|
32154
|
+
const method = expectation.method?.toUpperCase();
|
|
32155
|
+
const urlContains = context.resolve(expectation.urlContains);
|
|
32156
|
+
const matches = requests.filter(
|
|
32157
|
+
(request) => (!method || request.method.toUpperCase() === method) && request.url.includes(urlContains)
|
|
32158
|
+
);
|
|
32159
|
+
return {
|
|
32160
|
+
method,
|
|
32161
|
+
urlContains: expectation.urlContains,
|
|
32162
|
+
count: expectation.count,
|
|
32163
|
+
actualCount: matches.length,
|
|
32164
|
+
windowMs: expectation.windowMs,
|
|
32165
|
+
requests: matches.slice(0, 20).map((request) => ({
|
|
32166
|
+
method: request.method.toUpperCase(),
|
|
32167
|
+
url: sanitizeRequestUrl(request.url)
|
|
32168
|
+
}))
|
|
32169
|
+
};
|
|
32170
|
+
});
|
|
32171
|
+
}
|
|
32172
|
+
function sanitizeRequestUrl(url2) {
|
|
32173
|
+
const queryIndex = url2.indexOf("?");
|
|
32174
|
+
const safe = queryIndex >= 0 ? `${url2.slice(0, queryIndex)}?<redacted>` : url2;
|
|
32175
|
+
return snippet(safe, 300);
|
|
32176
|
+
}
|
|
32177
|
+
function delay(milliseconds) {
|
|
32178
|
+
return new Promise((resolve7) => setTimeout(resolve7, milliseconds));
|
|
32179
|
+
}
|
|
31747
32180
|
|
|
31748
32181
|
// ../../packages/execution-engine/src/engine/execution-engine.ts
|
|
31749
32182
|
var ExecutionEngine = class {
|
|
31750
32183
|
async runCase(data, options) {
|
|
31751
|
-
const { runId, file: file2, artifacts, bus, resolver } = options;
|
|
32184
|
+
const { runId, file: file2, artifacts, bus, resolver, rowId, rowIndex } = options;
|
|
31752
32185
|
const startedAt = /* @__PURE__ */ new Date();
|
|
31753
32186
|
const context = new ExecutionContext();
|
|
32187
|
+
context.seed(options.initialVariables);
|
|
31754
32188
|
if (data.accountRef && options.accounts?.[data.accountRef] !== void 0) {
|
|
31755
32189
|
context.seedAccount(options.accounts[data.accountRef]);
|
|
31756
32190
|
}
|
|
31757
32191
|
const steps = [];
|
|
32192
|
+
const warnings = [];
|
|
31758
32193
|
const evidenceAdapters = /* @__PURE__ */ new Map();
|
|
31759
32194
|
let caseError;
|
|
31760
|
-
|
|
31761
|
-
|
|
31762
|
-
|
|
32195
|
+
let caseErrorCode;
|
|
32196
|
+
const executionId = safeArtifactName(`${data.id}-${rowId}`);
|
|
32197
|
+
await bus.emit({
|
|
32198
|
+
type: "case-started",
|
|
32199
|
+
runId,
|
|
32200
|
+
caseId: data.id,
|
|
32201
|
+
rowId,
|
|
32202
|
+
file: file2,
|
|
32203
|
+
ts: startedAt.toISOString()
|
|
32204
|
+
});
|
|
32205
|
+
await artifacts.logger.info(`case ${data.id}#${rowId} started (${file2})`);
|
|
32206
|
+
if (options.missingDependencies.length > 0) {
|
|
32207
|
+
const finishedAt2 = /* @__PURE__ */ new Date();
|
|
32208
|
+
const skipped = {
|
|
32209
|
+
caseId: data.id,
|
|
32210
|
+
caseName: data.name,
|
|
32211
|
+
file: file2,
|
|
32212
|
+
rowId,
|
|
32213
|
+
rowIndex,
|
|
32214
|
+
status: "skipped",
|
|
32215
|
+
skipReason: "dependency-not-ready",
|
|
32216
|
+
missingDependencies: options.missingDependencies,
|
|
32217
|
+
steps,
|
|
32218
|
+
startedAt: startedAt.toISOString(),
|
|
32219
|
+
finishedAt: finishedAt2.toISOString(),
|
|
32220
|
+
durationMs: finishedAt2.getTime() - startedAt.getTime()
|
|
32221
|
+
};
|
|
31763
32222
|
await bus.emit({
|
|
31764
|
-
type: "
|
|
32223
|
+
type: "case-finished",
|
|
31765
32224
|
runId,
|
|
31766
32225
|
caseId: data.id,
|
|
31767
|
-
|
|
31768
|
-
|
|
31769
|
-
|
|
31770
|
-
ts:
|
|
32226
|
+
rowId,
|
|
32227
|
+
status: "skipped",
|
|
32228
|
+
durationMs: skipped.durationMs,
|
|
32229
|
+
ts: finishedAt2.toISOString()
|
|
31771
32230
|
});
|
|
31772
|
-
|
|
31773
|
-
|
|
31774
|
-
|
|
31775
|
-
|
|
31776
|
-
|
|
31777
|
-
|
|
31778
|
-
|
|
31779
|
-
|
|
31780
|
-
|
|
31781
|
-
|
|
31782
|
-
|
|
32231
|
+
await artifacts.logger.info(
|
|
32232
|
+
`case ${data.id}#${rowId} skipped: missing ${options.missingDependencies.join(", ")}`
|
|
32233
|
+
);
|
|
32234
|
+
return skipped;
|
|
32235
|
+
}
|
|
32236
|
+
const runPhase = async (phase, phaseSteps) => {
|
|
32237
|
+
let passed = true;
|
|
32238
|
+
for (const [index, step] of phaseSteps.entries()) {
|
|
32239
|
+
await bus.emit({
|
|
32240
|
+
type: "step-started",
|
|
32241
|
+
runId,
|
|
32242
|
+
caseId: data.id,
|
|
32243
|
+
rowId,
|
|
32244
|
+
phase,
|
|
32245
|
+
index,
|
|
32246
|
+
target: step.target,
|
|
32247
|
+
action: step.action,
|
|
32248
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
32249
|
+
});
|
|
32250
|
+
let result;
|
|
32251
|
+
try {
|
|
32252
|
+
const adapter = await (await resolver.session(step.target)).get();
|
|
32253
|
+
if (adapter.startEvidence && !evidenceAdapters.has(step.target)) {
|
|
32254
|
+
try {
|
|
32255
|
+
await adapter.startEvidence(executionId);
|
|
32256
|
+
evidenceAdapters.set(step.target, adapter);
|
|
32257
|
+
} catch (err) {
|
|
32258
|
+
await artifacts.logger.info(
|
|
32259
|
+
`evidence start failed for ${step.target}: ${err instanceof Error ? err.message : String(err)}`
|
|
32260
|
+
);
|
|
32261
|
+
}
|
|
31783
32262
|
}
|
|
32263
|
+
result = await executeStep(step, index, adapter, context, artifacts, {
|
|
32264
|
+
screenshotName: `${executionId}-${phase}-${String(index).padStart(2, "0")}`
|
|
32265
|
+
});
|
|
32266
|
+
} catch (err) {
|
|
32267
|
+
result = {
|
|
32268
|
+
index,
|
|
32269
|
+
target: step.target,
|
|
32270
|
+
action: step.action,
|
|
32271
|
+
status: "failed",
|
|
32272
|
+
durationMs: 0,
|
|
32273
|
+
error: err instanceof Error ? err.message : String(err),
|
|
32274
|
+
errorCode: err instanceof TestPilotError ? err.code : void 0
|
|
32275
|
+
};
|
|
31784
32276
|
}
|
|
31785
|
-
result =
|
|
31786
|
-
|
|
31787
|
-
|
|
31788
|
-
|
|
31789
|
-
|
|
32277
|
+
result.phase = phase;
|
|
32278
|
+
if (phase === "teardown" && result.status === "failed") {
|
|
32279
|
+
result.status = "warning";
|
|
32280
|
+
warnings.push(result.error ?? `teardown step ${index} failed`);
|
|
32281
|
+
}
|
|
32282
|
+
steps.push(result);
|
|
32283
|
+
await artifacts.logger.info(
|
|
32284
|
+
`${phase} step ${index} ${step.action} -> ${result.status}${result.error ? ` - ${result.error}` : ""}`
|
|
32285
|
+
);
|
|
32286
|
+
await bus.emit({
|
|
32287
|
+
type: "step-finished",
|
|
32288
|
+
runId,
|
|
32289
|
+
caseId: data.id,
|
|
32290
|
+
rowId,
|
|
32291
|
+
phase,
|
|
31790
32292
|
index,
|
|
31791
32293
|
target: step.target,
|
|
31792
32294
|
action: step.action,
|
|
32295
|
+
status: result.status,
|
|
32296
|
+
durationMs: result.durationMs,
|
|
32297
|
+
error: result.error,
|
|
32298
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
32299
|
+
});
|
|
32300
|
+
if (result.status === "failed") {
|
|
32301
|
+
passed = false;
|
|
32302
|
+
caseError = result.error;
|
|
32303
|
+
caseErrorCode = result.errorCode;
|
|
32304
|
+
for (const [skipIndex, skippedStep] of phaseSteps.entries()) {
|
|
32305
|
+
if (skipIndex <= index) continue;
|
|
32306
|
+
steps.push({
|
|
32307
|
+
index: skipIndex,
|
|
32308
|
+
phase,
|
|
32309
|
+
target: skippedStep.target,
|
|
32310
|
+
action: skippedStep.action,
|
|
32311
|
+
status: "skipped",
|
|
32312
|
+
durationMs: 0
|
|
32313
|
+
});
|
|
32314
|
+
}
|
|
32315
|
+
break;
|
|
32316
|
+
}
|
|
32317
|
+
}
|
|
32318
|
+
return passed;
|
|
32319
|
+
};
|
|
32320
|
+
if (options.blockedError) {
|
|
32321
|
+
const first = data.setup?.[0] ?? data.steps[0];
|
|
32322
|
+
if (first) {
|
|
32323
|
+
steps.push({
|
|
32324
|
+
index: 0,
|
|
32325
|
+
phase: "setup",
|
|
32326
|
+
target: first.target,
|
|
32327
|
+
action: first.action,
|
|
31793
32328
|
status: "failed",
|
|
31794
32329
|
durationMs: 0,
|
|
31795
|
-
error:
|
|
31796
|
-
|
|
32330
|
+
error: options.blockedError.message,
|
|
32331
|
+
errorCode: options.blockedError.code
|
|
32332
|
+
});
|
|
31797
32333
|
}
|
|
31798
|
-
|
|
31799
|
-
|
|
31800
|
-
|
|
31801
|
-
|
|
31802
|
-
|
|
31803
|
-
|
|
31804
|
-
|
|
31805
|
-
|
|
31806
|
-
action: step.action,
|
|
31807
|
-
status: result.status,
|
|
31808
|
-
durationMs: result.durationMs,
|
|
31809
|
-
error: result.error,
|
|
31810
|
-
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
31811
|
-
});
|
|
31812
|
-
if (result.status === "failed") {
|
|
31813
|
-
caseError = result.error;
|
|
31814
|
-
for (const [skipIndex, skippedStep] of data.steps.entries()) {
|
|
31815
|
-
if (skipIndex <= index) continue;
|
|
32334
|
+
caseError = options.blockedError.message;
|
|
32335
|
+
caseErrorCode = options.blockedError.code;
|
|
32336
|
+
} else {
|
|
32337
|
+
const setupPassed = await runPhase("setup", data.setup ?? []);
|
|
32338
|
+
if (setupPassed) {
|
|
32339
|
+
await runPhase("steps", data.steps);
|
|
32340
|
+
} else {
|
|
32341
|
+
for (const [index, step] of data.steps.entries()) {
|
|
31816
32342
|
steps.push({
|
|
31817
|
-
index
|
|
31818
|
-
|
|
31819
|
-
|
|
32343
|
+
index,
|
|
32344
|
+
phase: "steps",
|
|
32345
|
+
target: step.target,
|
|
32346
|
+
action: step.action,
|
|
31820
32347
|
status: "skipped",
|
|
31821
32348
|
durationMs: 0
|
|
31822
32349
|
});
|
|
31823
32350
|
}
|
|
31824
|
-
break;
|
|
31825
32351
|
}
|
|
32352
|
+
await runPhase("teardown", data.teardown ?? []);
|
|
31826
32353
|
}
|
|
31827
32354
|
const finishedAt = /* @__PURE__ */ new Date();
|
|
31828
|
-
const status = caseError ?
|
|
32355
|
+
const status = caseError ? caseStatusFromErrorCode(caseErrorCode) : "passed";
|
|
31829
32356
|
const caseResult = {
|
|
31830
32357
|
caseId: data.id,
|
|
31831
32358
|
caseName: data.name,
|
|
31832
32359
|
file: file2,
|
|
32360
|
+
rowId,
|
|
32361
|
+
rowIndex,
|
|
31833
32362
|
status,
|
|
31834
32363
|
steps,
|
|
31835
32364
|
startedAt: startedAt.toISOString(),
|
|
@@ -31837,15 +32366,17 @@ var ExecutionEngine = class {
|
|
|
31837
32366
|
durationMs: finishedAt.getTime() - startedAt.getTime()
|
|
31838
32367
|
};
|
|
31839
32368
|
if (caseError) caseResult.error = caseError;
|
|
32369
|
+
if (caseErrorCode) caseResult.errorCode = caseErrorCode;
|
|
32370
|
+
if (warnings.length > 0) caseResult.warnings = warnings;
|
|
31840
32371
|
for (const [target, adapter] of evidenceAdapters) {
|
|
31841
32372
|
if (!adapter.stopEvidence) continue;
|
|
31842
32373
|
try {
|
|
31843
|
-
const evidence = await adapter.stopEvidence(
|
|
32374
|
+
const evidence = await adapter.stopEvidence(executionId);
|
|
31844
32375
|
if (evidence.video) {
|
|
31845
|
-
caseResult.video = await artifacts.saveVideo(`${
|
|
32376
|
+
caseResult.video = await artifacts.saveVideo(`${executionId}-${target}.webm`, evidence.video);
|
|
31846
32377
|
}
|
|
31847
32378
|
if (evidence.trace) {
|
|
31848
|
-
caseResult.trace = await artifacts.saveTrace(`${
|
|
32379
|
+
caseResult.trace = await artifacts.saveTrace(`${executionId}-${target}.zip`, evidence.trace);
|
|
31849
32380
|
}
|
|
31850
32381
|
} catch (err) {
|
|
31851
32382
|
await artifacts.logger.info(
|
|
@@ -31857,33 +32388,47 @@ var ExecutionEngine = class {
|
|
|
31857
32388
|
type: "case-finished",
|
|
31858
32389
|
runId,
|
|
31859
32390
|
caseId: data.id,
|
|
32391
|
+
rowId,
|
|
31860
32392
|
status,
|
|
31861
32393
|
durationMs: finishedAt.getTime() - startedAt.getTime(),
|
|
31862
32394
|
ts: finishedAt.toISOString()
|
|
31863
32395
|
});
|
|
31864
|
-
await artifacts.logger.info(`case ${data.id} ${status}`);
|
|
32396
|
+
await artifacts.logger.info(`case ${data.id}#${rowId} ${status}`);
|
|
31865
32397
|
return caseResult;
|
|
31866
32398
|
}
|
|
31867
32399
|
};
|
|
32400
|
+
function safeArtifactName(value) {
|
|
32401
|
+
return value.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "case";
|
|
32402
|
+
}
|
|
31868
32403
|
|
|
31869
32404
|
// ../../packages/execution-engine/src/engine/result-collector.ts
|
|
31870
32405
|
function buildRunSummary(runId, startedAt, cases, options = {}) {
|
|
31871
32406
|
const finishedAt = /* @__PURE__ */ new Date();
|
|
31872
32407
|
const totals = {
|
|
32408
|
+
templates: options.templates ?? new Set(cases.map((item) => item.caseId)).size,
|
|
31873
32409
|
cases: cases.length,
|
|
31874
32410
|
passed: cases.filter((item) => item.status === "passed").length,
|
|
31875
32411
|
failed: cases.filter((item) => item.status === "failed").length,
|
|
32412
|
+
skipped: cases.filter((item) => item.status === "skipped").length,
|
|
32413
|
+
...cases.some((item) => item.status === "blocked") ? { blocked: cases.filter((item) => item.status === "blocked").length } : {},
|
|
32414
|
+
...cases.some((item) => item.status === "inconclusive") ? { inconclusive: cases.filter((item) => item.status === "inconclusive").length } : {},
|
|
32415
|
+
warnings: 0,
|
|
31876
32416
|
steps: 0,
|
|
31877
32417
|
stepsPassed: 0,
|
|
31878
32418
|
stepsFailed: 0,
|
|
31879
|
-
stepsSkipped: 0
|
|
32419
|
+
stepsSkipped: 0,
|
|
32420
|
+
stepsWarning: 0
|
|
31880
32421
|
};
|
|
31881
32422
|
for (const item of cases) {
|
|
31882
32423
|
for (const step of item.steps) {
|
|
31883
32424
|
totals.steps++;
|
|
31884
32425
|
if (step.status === "passed") totals.stepsPassed++;
|
|
31885
32426
|
else if (step.status === "failed") totals.stepsFailed++;
|
|
31886
|
-
else totals.stepsSkipped++;
|
|
32427
|
+
else if (step.status === "skipped") totals.stepsSkipped++;
|
|
32428
|
+
else {
|
|
32429
|
+
totals.stepsWarning++;
|
|
32430
|
+
totals.warnings++;
|
|
32431
|
+
}
|
|
31887
32432
|
}
|
|
31888
32433
|
}
|
|
31889
32434
|
return {
|
|
@@ -31891,7 +32436,8 @@ function buildRunSummary(runId, startedAt, cases, options = {}) {
|
|
|
31891
32436
|
startedAt: startedAt.toISOString(),
|
|
31892
32437
|
finishedAt: finishedAt.toISOString(),
|
|
31893
32438
|
durationMs: finishedAt.getTime() - startedAt.getTime(),
|
|
31894
|
-
|
|
32439
|
+
// run 状态按三桶优先级推导:产品失败 > 证据不足 > 环境阻塞 > 通过 > 跳过
|
|
32440
|
+
status: totals.failed > 0 ? "failed" : (totals.inconclusive ?? 0) > 0 ? "inconclusive" : (totals.blocked ?? 0) > 0 ? "blocked" : totals.passed > 0 || totals.cases === 0 ? "passed" : "skipped",
|
|
31895
32441
|
...options.cancelled ? { cancelled: true } : {},
|
|
31896
32442
|
cases,
|
|
31897
32443
|
totals
|
|
@@ -31900,7 +32446,7 @@ function buildRunSummary(runId, startedAt, cases, options = {}) {
|
|
|
31900
32446
|
|
|
31901
32447
|
// ../../packages/execution-engine/src/engine/test-runner.ts
|
|
31902
32448
|
import { writeFile as writeFile9 } from "node:fs/promises";
|
|
31903
|
-
import { join as join15, resolve as
|
|
32449
|
+
import { join as join15, resolve as resolve6 } from "node:path";
|
|
31904
32450
|
var TestRunner = class {
|
|
31905
32451
|
resolver;
|
|
31906
32452
|
runsRoot;
|
|
@@ -31910,7 +32456,7 @@ var TestRunner = class {
|
|
|
31910
32456
|
accounts;
|
|
31911
32457
|
constructor(options) {
|
|
31912
32458
|
this.resolver = options.resolver;
|
|
31913
|
-
this.runsRoot =
|
|
32459
|
+
this.runsRoot = resolve6(options.runsRoot ?? join15(process.cwd(), ".testpilot", "artifacts", "runs"));
|
|
31914
32460
|
this.customRunId = options.runId;
|
|
31915
32461
|
this.extraSubscriber = options.onEvent;
|
|
31916
32462
|
this.signal = options.signal;
|
|
@@ -31938,6 +32484,12 @@ var TestRunner = class {
|
|
|
31938
32484
|
await engine.runCase(item.data, {
|
|
31939
32485
|
runId,
|
|
31940
32486
|
file: item.file,
|
|
32487
|
+
templateId: item.templateId ?? item.data.id,
|
|
32488
|
+
rowId: item.rowId ?? "default",
|
|
32489
|
+
rowIndex: item.rowIndex ?? 0,
|
|
32490
|
+
initialVariables: item.initialVariables ?? {},
|
|
32491
|
+
missingDependencies: item.missingDependencies ?? [],
|
|
32492
|
+
blockedError: item.blockedError,
|
|
31941
32493
|
artifacts,
|
|
31942
32494
|
bus,
|
|
31943
32495
|
resolver: this.resolver,
|
|
@@ -31947,7 +32499,8 @@ var TestRunner = class {
|
|
|
31947
32499
|
}
|
|
31948
32500
|
const cancelled = this.signal?.aborted === true;
|
|
31949
32501
|
await this.resolver.closeAll();
|
|
31950
|
-
const
|
|
32502
|
+
const templates = new Set(cases.map((item) => item.templateId ?? item.data.id)).size;
|
|
32503
|
+
const summary = buildRunSummary(runId, startedAt, results, { cancelled, templates });
|
|
31951
32504
|
await bus.emit({
|
|
31952
32505
|
type: "run-finished",
|
|
31953
32506
|
runId,
|
|
@@ -31989,10 +32542,11 @@ async function openCaseContext(bundle, options, evidenceTmpDir) {
|
|
|
31989
32542
|
}
|
|
31990
32543
|
|
|
31991
32544
|
// ../../packages/adapter-playwright/src/page.ts
|
|
31992
|
-
import { existsSync as
|
|
31993
|
-
import { mkdtemp, readFile as
|
|
32545
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
32546
|
+
import { mkdtemp, readFile as readFile6, rm } from "node:fs/promises";
|
|
31994
32547
|
import { tmpdir } from "node:os";
|
|
31995
32548
|
import { join as join16 } from "node:path";
|
|
32549
|
+
import { errors } from "playwright";
|
|
31996
32550
|
var PlaywrightAdapter = class {
|
|
31997
32551
|
constructor(bundle, options = {}) {
|
|
31998
32552
|
this.bundle = bundle;
|
|
@@ -32000,6 +32554,7 @@ var PlaywrightAdapter = class {
|
|
|
32000
32554
|
}
|
|
32001
32555
|
target = "web";
|
|
32002
32556
|
current;
|
|
32557
|
+
requestCapture;
|
|
32003
32558
|
/** 浏览器在 create() 时已启动;context/page 按用例惰性创建,launch 保持幂等 */
|
|
32004
32559
|
async launch() {
|
|
32005
32560
|
}
|
|
@@ -32014,6 +32569,7 @@ var PlaywrightAdapter = class {
|
|
|
32014
32569
|
}
|
|
32015
32570
|
/** 结束取证:trace 落 zip、录屏在 context 关闭后完成,返回二者字节 */
|
|
32016
32571
|
async stopEvidence(_caseId) {
|
|
32572
|
+
await this.stopRequestCapture().catch(() => []);
|
|
32017
32573
|
if (!this.current) return {};
|
|
32018
32574
|
const { context, page, evidenceTmpDir } = this.current;
|
|
32019
32575
|
this.current = void 0;
|
|
@@ -32021,45 +32577,76 @@ var PlaywrightAdapter = class {
|
|
|
32021
32577
|
await context.tracing.stop({ path: traceZipPath }).catch(() => void 0);
|
|
32022
32578
|
const recordedVideoPath = await page.video()?.path().catch(() => void 0);
|
|
32023
32579
|
await context.close().catch(() => void 0);
|
|
32024
|
-
const video = recordedVideoPath &&
|
|
32025
|
-
const trace = evidenceTmpDir &&
|
|
32580
|
+
const video = recordedVideoPath && existsSync3(recordedVideoPath) ? await readFile6(recordedVideoPath) : void 0;
|
|
32581
|
+
const trace = evidenceTmpDir && existsSync3(traceZipPath) ? await readFile6(traceZipPath) : void 0;
|
|
32026
32582
|
if (evidenceTmpDir) await rm(evidenceTmpDir, { recursive: true, force: true }).catch(() => void 0);
|
|
32027
32583
|
return { video, trace };
|
|
32028
32584
|
}
|
|
32585
|
+
async startRequestCapture() {
|
|
32586
|
+
if (this.requestCapture) await this.stopRequestCapture();
|
|
32587
|
+
const page = await this.ensurePage();
|
|
32588
|
+
const requests = [];
|
|
32589
|
+
const listener = (request) => {
|
|
32590
|
+
requests.push({ method: request.method(), url: request.url() });
|
|
32591
|
+
};
|
|
32592
|
+
page.on("request", listener);
|
|
32593
|
+
this.requestCapture = { page, listener, requests };
|
|
32594
|
+
}
|
|
32595
|
+
async stopRequestCapture() {
|
|
32596
|
+
if (!this.requestCapture) return [];
|
|
32597
|
+
const capture = this.requestCapture;
|
|
32598
|
+
this.requestCapture = void 0;
|
|
32599
|
+
capture.page.off("request", capture.listener);
|
|
32600
|
+
return capture.requests;
|
|
32601
|
+
}
|
|
32029
32602
|
async navigate(url2) {
|
|
32030
32603
|
const options = this.options;
|
|
32031
32604
|
const page = await this.ensurePage();
|
|
32032
32605
|
const target = options.baseUrl && !/^https?:\/\//i.test(url2) ? new URL(url2, options.baseUrl).toString() : url2;
|
|
32033
|
-
|
|
32606
|
+
try {
|
|
32607
|
+
await page.goto(target);
|
|
32608
|
+
} catch (err) {
|
|
32609
|
+
if (err instanceof errors.TimeoutError) {
|
|
32610
|
+
throw new TestPilotError("STEP_TIMEOUT", err.message, { cause: err });
|
|
32611
|
+
}
|
|
32612
|
+
if (err instanceof Error && err.message.includes("net::ERR")) {
|
|
32613
|
+
throw new TestPilotError("ENVIRONMENT_UNAVAILABLE", err.message, { cause: err });
|
|
32614
|
+
}
|
|
32615
|
+
throw err;
|
|
32616
|
+
}
|
|
32034
32617
|
}
|
|
32035
32618
|
async click(locator) {
|
|
32036
|
-
|
|
32619
|
+
const target = await this.toLocator(locator);
|
|
32620
|
+
await this.coded(() => target.click());
|
|
32037
32621
|
}
|
|
32038
32622
|
async input(locator, value) {
|
|
32039
|
-
|
|
32623
|
+
const target = await this.toLocator(locator);
|
|
32624
|
+
await this.coded(() => target.fill(value));
|
|
32040
32625
|
}
|
|
32041
32626
|
async select(locator, value) {
|
|
32042
|
-
|
|
32627
|
+
const target = await this.toLocator(locator);
|
|
32628
|
+
await this.coded(() => target.selectOption(value));
|
|
32043
32629
|
}
|
|
32044
32630
|
async wait(locator, timeoutMs) {
|
|
32045
32631
|
const page = await this.ensurePage();
|
|
32046
32632
|
if (locator) {
|
|
32047
|
-
await this.toLocatorOn(page, locator).waitFor({ state: "visible", timeout: timeoutMs });
|
|
32633
|
+
await this.coded(() => this.toLocatorOn(page, locator).waitFor({ state: "visible", timeout: timeoutMs }));
|
|
32048
32634
|
return;
|
|
32049
32635
|
}
|
|
32050
32636
|
await page.waitForTimeout(timeoutMs ?? 1e3);
|
|
32051
32637
|
}
|
|
32052
32638
|
async assert(locator, expected) {
|
|
32053
32639
|
const target = await this.toLocator(locator);
|
|
32054
|
-
await target.waitFor({ state: "visible" });
|
|
32055
|
-
const actual = (await target.innerText()).trim();
|
|
32640
|
+
await this.coded(() => target.waitFor({ state: "visible" }));
|
|
32641
|
+
const actual = (await this.coded(() => target.innerText())).trim();
|
|
32056
32642
|
const wanted = expected.trim();
|
|
32057
32643
|
if (!actual.includes(wanted)) {
|
|
32058
|
-
throw new
|
|
32644
|
+
throw new TestPilotError("ASSERTION_FAILED", `\u65AD\u8A00\u5931\u8D25:\u671F\u671B\u5305\u542B "${wanted}",\u5B9E\u9645\u4E3A "${actual.slice(0, 200)}"`);
|
|
32059
32645
|
}
|
|
32060
32646
|
}
|
|
32061
32647
|
async extract(locator) {
|
|
32062
|
-
|
|
32648
|
+
const target = await this.toLocator(locator);
|
|
32649
|
+
return this.coded(() => target.innerText()).then((text) => text.trim());
|
|
32063
32650
|
}
|
|
32064
32651
|
async screenshot() {
|
|
32065
32652
|
return (await this.ensurePage()).screenshot();
|
|
@@ -32077,6 +32664,17 @@ var PlaywrightAdapter = class {
|
|
|
32077
32664
|
async toLocator(locator) {
|
|
32078
32665
|
return this.toLocatorOn(await this.ensurePage(), locator);
|
|
32079
32666
|
}
|
|
32667
|
+
/** 定位类操作的统一兜底:等待超时按「元素没等到」归类(可能是回归也可能是 selector 腐烂,不允许静默) */
|
|
32668
|
+
async coded(fn) {
|
|
32669
|
+
try {
|
|
32670
|
+
return await fn();
|
|
32671
|
+
} catch (err) {
|
|
32672
|
+
if (err instanceof errors.TimeoutError) {
|
|
32673
|
+
throw new TestPilotError("ELEMENT_NOT_FOUND", err.message, { cause: err });
|
|
32674
|
+
}
|
|
32675
|
+
throw err;
|
|
32676
|
+
}
|
|
32677
|
+
}
|
|
32080
32678
|
toLocatorOn(page, locator) {
|
|
32081
32679
|
if (locator.text !== void 0) {
|
|
32082
32680
|
return page.getByText(locator.text, { exact: true });
|
|
@@ -32087,6 +32685,7 @@ var PlaywrightAdapter = class {
|
|
|
32087
32685
|
throw new Error(`\u65E0\u6548\u7684 locator:${JSON.stringify(locator)}`);
|
|
32088
32686
|
}
|
|
32089
32687
|
async closeContextQuietly() {
|
|
32688
|
+
await this.stopRequestCapture().catch(() => []);
|
|
32090
32689
|
if (!this.current) return;
|
|
32091
32690
|
const { context } = this.current;
|
|
32092
32691
|
this.current = void 0;
|
|
@@ -32125,8 +32724,8 @@ var automatorLauncher = {
|
|
|
32125
32724
|
}
|
|
32126
32725
|
}
|
|
32127
32726
|
};
|
|
32128
|
-
function
|
|
32129
|
-
return new Promise((
|
|
32727
|
+
function delay2(ms) {
|
|
32728
|
+
return new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
32130
32729
|
}
|
|
32131
32730
|
async function connectWithRetry(wsEndpoint, timeoutMs) {
|
|
32132
32731
|
const startedAt = Date.now();
|
|
@@ -32136,7 +32735,7 @@ async function connectWithRetry(wsEndpoint, timeoutMs) {
|
|
|
32136
32735
|
return await automator.connect({ wsEndpoint });
|
|
32137
32736
|
} catch (err) {
|
|
32138
32737
|
lastError = err;
|
|
32139
|
-
await
|
|
32738
|
+
await delay2(1e3);
|
|
32140
32739
|
}
|
|
32141
32740
|
}
|
|
32142
32741
|
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
@@ -32153,12 +32752,12 @@ function startCliAuto(cliPath, projectPath, port) {
|
|
|
32153
32752
|
child.stderr?.on("data", (chunk) => {
|
|
32154
32753
|
output2 += chunk.toString("utf8");
|
|
32155
32754
|
});
|
|
32156
|
-
child.once("error", (err) => rejectPromise(new
|
|
32755
|
+
child.once("error", (err) => rejectPromise(new TestPilotError("ENVIRONMENT_UNAVAILABLE", `\u62C9\u8D77\u5F00\u53D1\u8005\u5DE5\u5177 cli \u5931\u8D25:${err.message}`)));
|
|
32157
32756
|
child.once("exit", (code) => {
|
|
32158
32757
|
if (code === 0) {
|
|
32159
32758
|
resolvePromise();
|
|
32160
32759
|
} else {
|
|
32161
|
-
rejectPromise(new
|
|
32760
|
+
rejectPromise(new TestPilotError("ENVIRONMENT_UNAVAILABLE", `cli auto \u9000\u51FA\u7801 ${code}:${output2.slice(-400)}`));
|
|
32162
32761
|
}
|
|
32163
32762
|
});
|
|
32164
32763
|
const startedAt = Date.now();
|
|
@@ -32171,7 +32770,7 @@ function startCliAuto(cliPath, projectPath, port) {
|
|
|
32171
32770
|
socket.once("error", () => {
|
|
32172
32771
|
socket.destroy();
|
|
32173
32772
|
if (Date.now() - startedAt > AUTO_PORT_FALLBACK_WAIT_MS) {
|
|
32174
|
-
rejectPromise(new
|
|
32773
|
+
rejectPromise(new TestPilotError("ENVIRONMENT_UNAVAILABLE", `\u7B49\u5F85\u5F00\u53D1\u8005\u5DE5\u5177\u81EA\u52A8\u5316\u7AEF\u53E3 ${port} \u8D85\u65F6;\u8BF7\u786E\u8BA4:1) \u5DE5\u5177\u5DF2\u767B\u5F55 2) \u8BBE\u7F6E->\u5B89\u5168\u8BBE\u7F6E->\u670D\u52A1\u7AEF\u53E3\u5DF2\u5F00\u542F`));
|
|
32175
32774
|
} else {
|
|
32176
32775
|
setTimeout(poll, 500);
|
|
32177
32776
|
}
|
|
@@ -32187,6 +32786,7 @@ var MiniAppAdapter = class {
|
|
|
32187
32786
|
}
|
|
32188
32787
|
target = "miniapp";
|
|
32189
32788
|
program;
|
|
32789
|
+
requestCapture;
|
|
32190
32790
|
/** 拉起开发者工具并建立自动化连接;需幂等 */
|
|
32191
32791
|
async launch() {
|
|
32192
32792
|
if (this.program) return;
|
|
@@ -32207,7 +32807,7 @@ var MiniAppAdapter = class {
|
|
|
32207
32807
|
return;
|
|
32208
32808
|
} catch (err) {
|
|
32209
32809
|
lastError = err;
|
|
32210
|
-
await
|
|
32810
|
+
await delay2(3e3);
|
|
32211
32811
|
}
|
|
32212
32812
|
}
|
|
32213
32813
|
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
@@ -32219,7 +32819,7 @@ var MiniAppAdapter = class {
|
|
|
32219
32819
|
await this.runWithReconnect(async () => (await this.element(locator)).input(value));
|
|
32220
32820
|
}
|
|
32221
32821
|
async select() {
|
|
32222
|
-
throw new
|
|
32822
|
+
throw new TestPilotError("UNSUPPORTED_ACTION", "\u5C0F\u7A0B\u5E8F\u7AEF\u6682\u4E0D\u652F\u6301 select(picker \u4EA4\u4E92),\u8BF7\u6539\u7528 click \u5B8C\u6210\u9009\u62E9");
|
|
32223
32823
|
}
|
|
32224
32824
|
async wait(locator, timeoutMs) {
|
|
32225
32825
|
await this.runWithReconnect(async () => {
|
|
@@ -32237,7 +32837,7 @@ var MiniAppAdapter = class {
|
|
|
32237
32837
|
const actual = (await element.text()).trim();
|
|
32238
32838
|
const wanted = expected.trim();
|
|
32239
32839
|
if (!actual.includes(wanted)) {
|
|
32240
|
-
throw new
|
|
32840
|
+
throw new TestPilotError("ASSERTION_FAILED", `\u65AD\u8A00\u5931\u8D25:\u671F\u671B\u5305\u542B "${wanted}",\u5B9E\u9645\u4E3A "${actual.slice(0, 200)}"`);
|
|
32241
32841
|
}
|
|
32242
32842
|
});
|
|
32243
32843
|
}
|
|
@@ -32253,7 +32853,43 @@ var MiniAppAdapter = class {
|
|
|
32253
32853
|
return Buffer.from(base643, "base64");
|
|
32254
32854
|
});
|
|
32255
32855
|
}
|
|
32856
|
+
async startRequestCapture() {
|
|
32857
|
+
if (this.requestCapture) await this.stopRequestCapture();
|
|
32858
|
+
const program2 = await this.getProgram();
|
|
32859
|
+
const requests = [];
|
|
32860
|
+
const exposedName = `__testpilotRequest_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
32861
|
+
await program2.exposeFunction(exposedName, (value) => {
|
|
32862
|
+
if (value === null || typeof value !== "object") return;
|
|
32863
|
+
const request = value;
|
|
32864
|
+
if (typeof request.url !== "string") return;
|
|
32865
|
+
requests.push({
|
|
32866
|
+
method: typeof request.method === "string" ? request.method : "GET",
|
|
32867
|
+
url: request.url
|
|
32868
|
+
});
|
|
32869
|
+
});
|
|
32870
|
+
await program2.mockWxMethod(
|
|
32871
|
+
"request",
|
|
32872
|
+
function(options, callbackName) {
|
|
32873
|
+
const callbacks = globalThis;
|
|
32874
|
+
callbacks[callbackName]?.({
|
|
32875
|
+
method: typeof options.method === "string" ? options.method : "GET",
|
|
32876
|
+
url: typeof options.url === "string" ? options.url : ""
|
|
32877
|
+
});
|
|
32878
|
+
return this.origin(options);
|
|
32879
|
+
},
|
|
32880
|
+
exposedName
|
|
32881
|
+
);
|
|
32882
|
+
this.requestCapture = { program: program2, exposedName, requests };
|
|
32883
|
+
}
|
|
32884
|
+
async stopRequestCapture() {
|
|
32885
|
+
if (!this.requestCapture) return [];
|
|
32886
|
+
const capture = this.requestCapture;
|
|
32887
|
+
this.requestCapture = void 0;
|
|
32888
|
+
await capture.program.restoreWxMethod("request");
|
|
32889
|
+
return capture.requests;
|
|
32890
|
+
}
|
|
32256
32891
|
async close() {
|
|
32892
|
+
await this.stopRequestCapture().catch(() => []);
|
|
32257
32893
|
if (!this.program) return;
|
|
32258
32894
|
const program2 = this.program;
|
|
32259
32895
|
this.program = void 0;
|
|
@@ -32299,19 +32935,19 @@ var MiniAppAdapter = class {
|
|
|
32299
32935
|
const element = await page.$(selector);
|
|
32300
32936
|
if (element) return element;
|
|
32301
32937
|
if (Date.now() - startedAt > ELEMENT_WAIT_MS) {
|
|
32302
|
-
throw new
|
|
32938
|
+
throw new TestPilotError("ELEMENT_NOT_FOUND", `\u7B49\u5F85\u5143\u7D20\u8D85\u65F6(${ELEMENT_WAIT_MS}ms):${selector}`);
|
|
32303
32939
|
}
|
|
32304
|
-
await
|
|
32940
|
+
await delay2(500);
|
|
32305
32941
|
}
|
|
32306
32942
|
}
|
|
32307
32943
|
};
|
|
32308
32944
|
|
|
32309
32945
|
// ../../packages/adapter-wechatide/src/project.ts
|
|
32310
|
-
import { existsSync as
|
|
32946
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
32311
32947
|
async function resolveMiniappConfig(input2) {
|
|
32312
32948
|
const projectPath = input2.projectPath ?? process.env.TESTPILOT_MINIPROGRAM_PROJECT_PATH;
|
|
32313
32949
|
if (!projectPath) return void 0;
|
|
32314
|
-
if (!
|
|
32950
|
+
if (!existsSync4(projectPath)) {
|
|
32315
32951
|
throw new Error(`\u5C0F\u7A0B\u5E8F\u9879\u76EE\u8DEF\u5F84\u4E0D\u5B58\u5728:${projectPath}`);
|
|
32316
32952
|
}
|
|
32317
32953
|
const cliPath = input2.cliPath ?? process.env.WECHAT_DEVTOOLS_CLI ?? await findWechatDevToolsCli();
|
|
@@ -32377,7 +33013,7 @@ var ApiAdapter = class {
|
|
|
32377
33013
|
signal: AbortSignal.timeout(this.options.timeoutMs ?? 15e3)
|
|
32378
33014
|
});
|
|
32379
33015
|
} catch (err) {
|
|
32380
|
-
throw new
|
|
33016
|
+
throw new TestPilotError("ENVIRONMENT_UNAVAILABLE", `API \u8BF7\u6C42\u5931\u8D25 ${request.method.toUpperCase()} ${url2}:${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
32381
33017
|
}
|
|
32382
33018
|
const bodyText = await res.text();
|
|
32383
33019
|
let json2;
|
|
@@ -32399,7 +33035,7 @@ var ApiAdapter = class {
|
|
|
32399
33035
|
if (!this.last) throw new Error("\u5C1A\u672A\u53D1\u8D77\u4EFB\u4F55 API \u8BF7\u6C42,\u65E0\u6CD5\u65AD\u8A00\u54CD\u5E94");
|
|
32400
33036
|
if (!this.last.bodyText.includes(expected)) {
|
|
32401
33037
|
const snippet2 = this.last.bodyText.slice(0, 200);
|
|
32402
|
-
throw new
|
|
33038
|
+
throw new TestPilotError("ASSERTION_FAILED", `\u54CD\u5E94\u4F53\u4E0D\u5305\u542B "${expected}":HTTP ${this.last.status} ${snippet2}`);
|
|
32403
33039
|
}
|
|
32404
33040
|
}
|
|
32405
33041
|
async extractResponse(path) {
|
|
@@ -32486,7 +33122,7 @@ function createDefaultAdapters(config2) {
|
|
|
32486
33122
|
async function runCases(options = {}) {
|
|
32487
33123
|
const root = resolveRoot(options.root);
|
|
32488
33124
|
const config2 = await loadProjectConfig(root);
|
|
32489
|
-
const inputs = options.paths && options.paths.length > 0 ? options.paths.map((input2) =>
|
|
33125
|
+
const inputs = options.paths && options.paths.length > 0 ? options.paths.map((input2) => isAbsolute3(input2) ? input2 : join17(root, input2)) : [join17(root, config2.casesDir)];
|
|
32490
33126
|
const cases = await collectCases(inputs, { root });
|
|
32491
33127
|
if (cases.length === 0) {
|
|
32492
33128
|
throw new RunError("ENO_CASES", `\u672A\u627E\u5230\u7528\u4F8B\u6587\u4EF6(\u67E5\u627E:${inputs.join(", ")})`);
|
|
@@ -32518,8 +33154,39 @@ async function runCases(options = {}) {
|
|
|
32518
33154
|
signal: options.signal,
|
|
32519
33155
|
accounts: options.accounts
|
|
32520
33156
|
});
|
|
33157
|
+
const prepared = (await Promise.all(
|
|
33158
|
+
runnable.map(
|
|
33159
|
+
(item) => prepareCaseExecutions(
|
|
33160
|
+
{ data: item.case, file: item.file },
|
|
33161
|
+
{
|
|
33162
|
+
root,
|
|
33163
|
+
dataDir: join17(root, "tests", "e2e", "data"),
|
|
33164
|
+
variables: config2.variables,
|
|
33165
|
+
accounts: options.accounts
|
|
33166
|
+
}
|
|
33167
|
+
).catch((err) => {
|
|
33168
|
+
if (err instanceof TestPilotError && err.code === "FIXTURE_MISSING") {
|
|
33169
|
+
return [
|
|
33170
|
+
{
|
|
33171
|
+
data: item.case,
|
|
33172
|
+
file: item.file,
|
|
33173
|
+
templateId: item.case.id,
|
|
33174
|
+
rowId: "default",
|
|
33175
|
+
rowIndex: 0,
|
|
33176
|
+
initialVariables: {},
|
|
33177
|
+
missingDependencies: [],
|
|
33178
|
+
blockedError: { code: err.code, message: err.message }
|
|
33179
|
+
}
|
|
33180
|
+
];
|
|
33181
|
+
}
|
|
33182
|
+
throw err;
|
|
33183
|
+
})
|
|
33184
|
+
)
|
|
33185
|
+
)).flat();
|
|
32521
33186
|
const summary = await runner.run(
|
|
32522
|
-
|
|
33187
|
+
prepared.map(
|
|
33188
|
+
(item) => item
|
|
33189
|
+
)
|
|
32523
33190
|
);
|
|
32524
33191
|
return { summary, invalid, tagFiltered, missingAccounts };
|
|
32525
33192
|
}
|
|
@@ -32543,20 +33210,20 @@ async function getRun(runId, options = {}) {
|
|
|
32543
33210
|
const base = runsRoot(options.root);
|
|
32544
33211
|
const file2 = join17(base, runId, RESULT_FILE);
|
|
32545
33212
|
try {
|
|
32546
|
-
return JSON.parse(await
|
|
33213
|
+
return JSON.parse(await readFile7(file2, "utf8"));
|
|
32547
33214
|
} catch {
|
|
32548
33215
|
throw new RunError("ENO_CASES", `\u8BFB\u53D6\u8FD0\u884C\u7ED3\u679C\u5931\u8D25:${file2}(\u8FD0\u884C\u672A\u5B8C\u6210\u6216 runId \u4E0D\u5B58\u5728)`);
|
|
32549
33216
|
}
|
|
32550
33217
|
}
|
|
32551
33218
|
async function getRunEvents(runId, options = {}) {
|
|
32552
33219
|
const file2 = join17(runsRoot(options.root), runId, "events.ndjson");
|
|
32553
|
-
const source = await
|
|
33220
|
+
const source = await readFile7(file2, "utf8").catch(() => "");
|
|
32554
33221
|
return source.split("\n").filter((line) => line.trim().length > 0).map((line) => JSON.parse(line));
|
|
32555
33222
|
}
|
|
32556
33223
|
async function readRunMeta(base, runId) {
|
|
32557
33224
|
try {
|
|
32558
33225
|
const summary = JSON.parse(
|
|
32559
|
-
await
|
|
33226
|
+
await readFile7(join17(base, runId, RESULT_FILE), "utf8")
|
|
32560
33227
|
);
|
|
32561
33228
|
return {
|
|
32562
33229
|
status: summary.status,
|
|
@@ -32580,7 +33247,7 @@ async function generateReport(runId, options = {}) {
|
|
|
32580
33247
|
const runDir = join18(resolveRoot(options.root), RUNS_DIR, id);
|
|
32581
33248
|
let summary;
|
|
32582
33249
|
try {
|
|
32583
|
-
summary = JSON.parse(await
|
|
33250
|
+
summary = JSON.parse(await readFile8(join18(runDir, "result.json"), "utf8"));
|
|
32584
33251
|
} catch {
|
|
32585
33252
|
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)`);
|
|
32586
33253
|
}
|
|
@@ -32611,13 +33278,13 @@ async function generateSummaryReport(options = {}) {
|
|
|
32611
33278
|
}
|
|
32612
33279
|
async function readReport(runId, options = {}) {
|
|
32613
33280
|
const file2 = join18(resolveRoot(options.root), RUNS_DIR, runId, "report.json");
|
|
32614
|
-
const source = await
|
|
33281
|
+
const source = await readFile8(file2, "utf8").catch(() => void 0);
|
|
32615
33282
|
return source ? JSON.parse(source) : void 0;
|
|
32616
33283
|
}
|
|
32617
33284
|
|
|
32618
33285
|
// ../../packages/sdk/src/client.ts
|
|
32619
33286
|
function resolveInput(root, input2) {
|
|
32620
|
-
return
|
|
33287
|
+
return isAbsolute4(input2) ? input2 : join19(root, input2);
|
|
32621
33288
|
}
|
|
32622
33289
|
var TestPilotClient = class {
|
|
32623
33290
|
/** 业务项目根目录(绝对路径) */
|
|
@@ -32693,13 +33360,13 @@ var TestPilotClient = class {
|
|
|
32693
33360
|
};
|
|
32694
33361
|
|
|
32695
33362
|
// src/commands/init/adapter-detector.ts
|
|
32696
|
-
import { existsSync as
|
|
33363
|
+
import { existsSync as existsSync5, readFileSync } from "node:fs";
|
|
32697
33364
|
import { join as join20 } from "node:path";
|
|
32698
33365
|
async function detectAdapters(root, projectType) {
|
|
32699
33366
|
const required2 = projectType === "hybrid" ? ["playwright", "wechatide"] : projectType === "wechat-miniapp" ? ["wechatide"] : projectType === "web" ? ["playwright"] : [];
|
|
32700
33367
|
const pkg = readPackageJsonDeps(root);
|
|
32701
33368
|
const hasPlaywrightDep = pkg !== void 0 && ("playwright" in pkg || "@playwright/test" in pkg);
|
|
32702
|
-
const playwrightInstalled = hasPlaywrightDep ||
|
|
33369
|
+
const playwrightInstalled = hasPlaywrightDep || existsSync5(join20(root, "node_modules", "playwright"));
|
|
32703
33370
|
const devtoolsCli = await findWechatDevToolsCli().catch(() => void 0);
|
|
32704
33371
|
return [
|
|
32705
33372
|
{
|
|
@@ -32718,7 +33385,7 @@ async function detectAdapters(root, projectType) {
|
|
|
32718
33385
|
}
|
|
32719
33386
|
function readPackageJsonDeps(root) {
|
|
32720
33387
|
const file2 = join20(root, "package.json");
|
|
32721
|
-
if (!
|
|
33388
|
+
if (!existsSync5(file2)) return void 0;
|
|
32722
33389
|
try {
|
|
32723
33390
|
const pkg = JSON.parse(readFileSync(file2, "utf8"));
|
|
32724
33391
|
return { ...pkg.dependencies, ...pkg.devDependencies };
|
|
@@ -32728,13 +33395,13 @@ function readPackageJsonDeps(root) {
|
|
|
32728
33395
|
}
|
|
32729
33396
|
|
|
32730
33397
|
// src/commands/init/environment-initializer.ts
|
|
32731
|
-
import { readFile as
|
|
33398
|
+
import { readFile as readFile9 } from "node:fs/promises";
|
|
32732
33399
|
import { join as join21 } from "node:path";
|
|
32733
33400
|
var ENV_REF_PATTERN = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
32734
33401
|
async function collectEnvVarRefs(root) {
|
|
32735
33402
|
let text;
|
|
32736
33403
|
try {
|
|
32737
|
-
text = await
|
|
33404
|
+
text = await readFile9(join21(root, "testpilot.yaml"), "utf8");
|
|
32738
33405
|
} catch {
|
|
32739
33406
|
return [];
|
|
32740
33407
|
}
|
|
@@ -32750,7 +33417,7 @@ async function collectEnvVarRefs(root) {
|
|
|
32750
33417
|
}
|
|
32751
33418
|
|
|
32752
33419
|
// src/commands/init/project-detector.ts
|
|
32753
|
-
import { existsSync as
|
|
33420
|
+
import { existsSync as existsSync6, readFileSync as readFileSync2 } from "node:fs";
|
|
32754
33421
|
import { basename, join as join22 } from "node:path";
|
|
32755
33422
|
|
|
32756
33423
|
// src/lib/git.ts
|
|
@@ -32789,7 +33456,7 @@ var WEB_DEP_MARKERS = [
|
|
|
32789
33456
|
var E2E_DIR_CANDIDATES = ["tests/e2e", "test/e2e", "e2e", "cypress"];
|
|
32790
33457
|
function readPackageJson(root) {
|
|
32791
33458
|
const file2 = join22(root, "package.json");
|
|
32792
|
-
if (!
|
|
33459
|
+
if (!existsSync6(file2)) return void 0;
|
|
32793
33460
|
try {
|
|
32794
33461
|
return JSON.parse(readFileSync2(file2, "utf8"));
|
|
32795
33462
|
} catch {
|
|
@@ -32799,8 +33466,8 @@ function readPackageJson(root) {
|
|
|
32799
33466
|
function detectProjectType(root) {
|
|
32800
33467
|
const pkg = readPackageJson(root);
|
|
32801
33468
|
const deps = { ...pkg?.dependencies, ...pkg?.devDependencies };
|
|
32802
|
-
const isWechat =
|
|
32803
|
-
const isWeb =
|
|
33469
|
+
const isWechat = existsSync6(join22(root, "project.config.json")) || existsSync6(join22(root, "app.json")) || existsSync6(join22(root, "miniprogram", "app.json"));
|
|
33470
|
+
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}`)));
|
|
32804
33471
|
if (isWechat && isWeb) return "hybrid";
|
|
32805
33472
|
if (isWechat) return "wechat-miniapp";
|
|
32806
33473
|
if (isWeb) return "web";
|
|
@@ -32809,9 +33476,9 @@ function detectProjectType(root) {
|
|
|
32809
33476
|
async function detectProject(root) {
|
|
32810
33477
|
const pkg = readPackageJson(root);
|
|
32811
33478
|
const type = detectProjectType(root);
|
|
32812
|
-
const language =
|
|
32813
|
-
const packageManager =
|
|
32814
|
-
const existingE2eDirs = E2E_DIR_CANDIDATES.filter((dir) =>
|
|
33479
|
+
const language = existsSync6(join22(root, "tsconfig.json")) ? "TypeScript" : pkg || existsSync6(join22(root, "src")) ? "JavaScript" : "Unknown";
|
|
33480
|
+
const packageManager = existsSync6(join22(root, "pnpm-lock.yaml")) ? "pnpm" : existsSync6(join22(root, "yarn.lock")) ? "yarn" : existsSync6(join22(root, "package-lock.json")) ? "npm" : "npx";
|
|
33481
|
+
const existingE2eDirs = E2E_DIR_CANDIDATES.filter((dir) => existsSync6(join22(root, dir)));
|
|
32815
33482
|
return {
|
|
32816
33483
|
root,
|
|
32817
33484
|
name: pkg?.name ?? basename(root),
|
|
@@ -32821,8 +33488,8 @@ async function detectProject(root) {
|
|
|
32821
33488
|
git: await readGitInfo(root),
|
|
32822
33489
|
hasTestsE2e: existingE2eDirs.includes("tests/e2e"),
|
|
32823
33490
|
existingE2eDirs,
|
|
32824
|
-
hasTestpilotConfig:
|
|
32825
|
-
hasProjectLink:
|
|
33491
|
+
hasTestpilotConfig: existsSync6(join22(root, "testpilot.yaml")),
|
|
33492
|
+
hasProjectLink: existsSync6(join22(root, ".testpilot", "project.json"))
|
|
32826
33493
|
};
|
|
32827
33494
|
}
|
|
32828
33495
|
function defaultAdapterFor(type) {
|
|
@@ -32837,9 +33504,9 @@ function adaptersFor(type) {
|
|
|
32837
33504
|
}
|
|
32838
33505
|
|
|
32839
33506
|
// src/commands/init/skill-installer.ts
|
|
32840
|
-
import { existsSync as
|
|
33507
|
+
import { existsSync as existsSync7, readFileSync as readFileSync3 } from "node:fs";
|
|
32841
33508
|
import { access as access2, cp, mkdir as mkdir12, writeFile as writeFile10 } from "node:fs/promises";
|
|
32842
|
-
import { dirname as
|
|
33509
|
+
import { dirname as dirname9, join as join23 } from "node:path";
|
|
32843
33510
|
import { fileURLToPath } from "node:url";
|
|
32844
33511
|
var CORE_FILES = ["SKILL.md", "manifest.yaml"];
|
|
32845
33512
|
var CORE_DIRS = ["rules", "workflows"];
|
|
@@ -32853,23 +33520,23 @@ async function exists3(path) {
|
|
|
32853
33520
|
}
|
|
32854
33521
|
function readManifestVersion(dir) {
|
|
32855
33522
|
const manifestPath = join23(dir, "manifest.yaml");
|
|
32856
|
-
if (!
|
|
33523
|
+
if (!existsSync7(manifestPath)) return void 0;
|
|
32857
33524
|
return /^version:\s*([^\s#]+)/m.exec(readFileSync3(manifestPath, "utf8"))?.[1];
|
|
32858
33525
|
}
|
|
32859
33526
|
function resolveSkillSourceDir() {
|
|
32860
33527
|
const fromEnv = process.env.TESTPILOT_SKILL_DIR;
|
|
32861
33528
|
if (fromEnv) return fromEnv;
|
|
32862
33529
|
const bundled = fileURLToPath(new URL("./skills/testpilot", import.meta.url));
|
|
32863
|
-
if (
|
|
33530
|
+
if (existsSync7(bundled)) return bundled;
|
|
32864
33531
|
const inRepo = fileURLToPath(new URL("../../../../../skills/testpilot", import.meta.url));
|
|
32865
|
-
return
|
|
33532
|
+
return existsSync7(inRepo) ? inRepo : void 0;
|
|
32866
33533
|
}
|
|
32867
33534
|
async function copyCoreFiles(src, dest) {
|
|
32868
33535
|
for (const name of CORE_FILES) {
|
|
32869
33536
|
await cp(join23(src, name), join23(dest, name), { recursive: true, force: true });
|
|
32870
33537
|
}
|
|
32871
33538
|
for (const dir of CORE_DIRS) {
|
|
32872
|
-
if (
|
|
33539
|
+
if (existsSync7(join23(src, dir))) {
|
|
32873
33540
|
await cp(join23(src, dir), join23(dest, dir), { recursive: true, force: true });
|
|
32874
33541
|
}
|
|
32875
33542
|
}
|
|
@@ -32884,7 +33551,7 @@ async function syncSkillFiles(root, options = {}) {
|
|
|
32884
33551
|
"\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"
|
|
32885
33552
|
);
|
|
32886
33553
|
}
|
|
32887
|
-
await mkdir12(
|
|
33554
|
+
await mkdir12(dirname9(dest), { recursive: true });
|
|
32888
33555
|
await cp(src, dest, { recursive: true });
|
|
32889
33556
|
return { core: "created", bundledVersion, dest };
|
|
32890
33557
|
}
|
|
@@ -32958,6 +33625,12 @@ function renderTestConventionsMd() {
|
|
|
32958
33625
|
6. \u4F18\u5148\u590D\u7528\u5DF2\u6709 Case
|
|
32959
33626
|
7. \u65B0\u589E Case \u5FC5\u987B\u901A\u8FC7 csspilot validate
|
|
32960
33627
|
8. Case ID \u5FC5\u987B\u552F\u4E00
|
|
33628
|
+
9. fixture/dataset \u4F7F\u7528 YAML \u6216 JSON,\u7EDF\u4E00\u4F4D\u4E8E tests/e2e/data
|
|
33629
|
+
10. \u9700\u6C42\u89C4\u5219\u77E9\u9635\u4E0E QA \u8986\u76D6\u6620\u5C04\u5199\u5165 tests/e2e/reviews/<\u9700\u6C42>-coverage.md
|
|
33630
|
+
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
|
|
33631
|
+
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
|
|
33632
|
+
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.*}
|
|
33633
|
+
14. setup/teardown \u4F18\u5148 API;teardown \u5931\u8D25\u4F5C\u4E3A warning,\u4E0D\u6539\u53D8\u4E3B\u6D41\u7A0B\u7ED3\u679C
|
|
32961
33634
|
`;
|
|
32962
33635
|
}
|
|
32963
33636
|
function renderAdaptersMd(adapters) {
|
|
@@ -32998,8 +33671,8 @@ async function runDoctor(root) {
|
|
|
32998
33671
|
const projectItems = [
|
|
32999
33672
|
ok(`Project ${info.name}(${info.type})`),
|
|
33000
33673
|
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"),
|
|
33001
|
-
|
|
33002
|
-
|
|
33674
|
+
existsSync8(join24(root, ".testpilot", "project.json")) ? ok(".testpilot/project.json") : error62(".testpilot/project.json", "\u8FD0\u884C npx csspilot init \u751F\u6210"),
|
|
33675
|
+
existsSync8(join24(root, "testpilot.yaml")) ? ok("testpilot.yaml") : error62("testpilot.yaml", "\u8FD0\u884C npx csspilot init \u751F\u6210")
|
|
33003
33676
|
];
|
|
33004
33677
|
push("Project", projectItems);
|
|
33005
33678
|
const config2 = await loadTestpilotConfig(root).catch(() => void 0);
|
|
@@ -33008,8 +33681,8 @@ async function runDoctor(root) {
|
|
|
33008
33681
|
const invalidCount = cases.filter((item) => !item.valid).length;
|
|
33009
33682
|
const testDir = join24(root, "tests", "e2e");
|
|
33010
33683
|
push("Test", [
|
|
33011
|
-
|
|
33012
|
-
|
|
33684
|
+
existsSync8(testDir) ? ok("tests/e2e") : error62("tests/e2e", "\u8FD0\u884C npx csspilot init \u521B\u5EFA"),
|
|
33685
|
+
existsSync8(join24(root, casesDir)) ? ok(`cases directory(${casesDir})`) : error62(`cases directory(${casesDir})`, "\u8FD0\u884C npx csspilot init \u521B\u5EFA"),
|
|
33013
33686
|
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")
|
|
33014
33687
|
]);
|
|
33015
33688
|
const skillDir = join24(root, ".agents", "skills", "testpilot");
|
|
@@ -33018,7 +33691,7 @@ async function runDoctor(root) {
|
|
|
33018
33691
|
const bundledSkillVersion = skillSource ? readManifestVersion(skillSource) : void 0;
|
|
33019
33692
|
const skillItems = [
|
|
33020
33693
|
installedSkillVersion ? ok(`TestPilot Skill(.agents/skills/testpilot,v${installedSkillVersion})`) : error62("TestPilot Skill", "\u8FD0\u884C npx csspilot init \u5B89\u88C5"),
|
|
33021
|
-
|
|
33694
|
+
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")
|
|
33022
33695
|
];
|
|
33023
33696
|
if (installedSkillVersion && bundledSkillVersion && installedSkillVersion !== bundledSkillVersion) {
|
|
33024
33697
|
skillItems.push(
|
|
@@ -33113,7 +33786,7 @@ function makeDoctorCommand() {
|
|
|
33113
33786
|
import { join as join27 } from "node:path";
|
|
33114
33787
|
|
|
33115
33788
|
// src/commands/init/config-initializer.ts
|
|
33116
|
-
import { access as access3, mkdir as mkdir13, readFile as
|
|
33789
|
+
import { access as access3, mkdir as mkdir13, readFile as readFile10, writeFile as writeFile11 } from "node:fs/promises";
|
|
33117
33790
|
import { basename as basename2, join as join25 } from "node:path";
|
|
33118
33791
|
async function exists4(path) {
|
|
33119
33792
|
try {
|
|
@@ -33145,6 +33818,10 @@ environment:
|
|
|
33145
33818
|
staging:
|
|
33146
33819
|
baseUrl: \${STAGING_BASE_URL}
|
|
33147
33820
|
|
|
33821
|
+
# Case \u53EF\u89C1\u7684\u9879\u76EE\u53D8\u91CF\u767D\u540D\u5355(\u503C\u6765\u81EA .env / CI Secret;Case \u7528 \${variable.<name>} \u5F15\u7528)
|
|
33822
|
+
# variables:
|
|
33823
|
+
# ppmProjectId: \${PPM_PROJECT_ID}
|
|
33824
|
+
|
|
33148
33825
|
# \u9879\u76EE\u58F0\u660E\u7684\u6267\u884C\u7AEF(\u5B9E\u9645\u53EF\u7528\u6027\u4EE5 csspilot doctor \u68C0\u6D4B\u4E3A\u51C6)
|
|
33149
33826
|
adapters:
|
|
33150
33827
|
${adapters}
|
|
@@ -33195,7 +33872,7 @@ ${entry}
|
|
|
33195
33872
|
`, "utf8");
|
|
33196
33873
|
return "created";
|
|
33197
33874
|
}
|
|
33198
|
-
const content = await
|
|
33875
|
+
const content = await readFile10(gitignorePath, "utf8");
|
|
33199
33876
|
if (content.split(/\r?\n/).some((item) => item.trim() === entry)) {
|
|
33200
33877
|
return "exists";
|
|
33201
33878
|
}
|
|
@@ -33279,19 +33956,21 @@ function randomId() {
|
|
|
33279
33956
|
}
|
|
33280
33957
|
|
|
33281
33958
|
// src/commands/init/test-structure.ts
|
|
33282
|
-
import { existsSync as
|
|
33959
|
+
import { existsSync as existsSync9, readdirSync as readdirSync2 } from "node:fs";
|
|
33283
33960
|
import { mkdir as mkdir14, writeFile as writeFile12 } from "node:fs/promises";
|
|
33284
33961
|
import { join as join26 } from "node:path";
|
|
33285
33962
|
async function initTestStructure(root, _info) {
|
|
33286
33963
|
const dirs = {
|
|
33287
33964
|
cases: join26(root, "tests", "e2e", "cases"),
|
|
33288
33965
|
fixtures: join26(root, "tests", "e2e", "fixtures"),
|
|
33289
|
-
data: join26(root, "tests", "e2e", "data")
|
|
33966
|
+
data: join26(root, "tests", "e2e", "data"),
|
|
33967
|
+
reviews: join26(root, "tests", "e2e", "reviews")
|
|
33290
33968
|
};
|
|
33291
33969
|
const existed = {
|
|
33292
|
-
cases:
|
|
33293
|
-
fixtures:
|
|
33294
|
-
data:
|
|
33970
|
+
cases: existsSync9(dirs.cases),
|
|
33971
|
+
fixtures: existsSync9(dirs.fixtures),
|
|
33972
|
+
data: existsSync9(dirs.data),
|
|
33973
|
+
reviews: existsSync9(dirs.reviews)
|
|
33295
33974
|
};
|
|
33296
33975
|
for (const dir of Object.values(dirs)) {
|
|
33297
33976
|
await mkdir14(dir, { recursive: true });
|
|
@@ -33304,7 +33983,8 @@ async function initTestStructure(root, _info) {
|
|
|
33304
33983
|
return {
|
|
33305
33984
|
cases: existed.cases ? "reused" : "created",
|
|
33306
33985
|
fixtures: existed.fixtures ? "reused" : "created",
|
|
33307
|
-
data: existed.data ? "reused" : "created"
|
|
33986
|
+
data: existed.data ? "reused" : "created",
|
|
33987
|
+
reviews: existed.reviews ? "reused" : "created"
|
|
33308
33988
|
};
|
|
33309
33989
|
}
|
|
33310
33990
|
|
|
@@ -33344,7 +34024,7 @@ async function runInit(root, options = {}) {
|
|
|
33344
34024
|
const testStructure = await initTestStructure(root, project);
|
|
33345
34025
|
const reuseNote = project.existingE2eDirs.filter((dir) => dir !== "tests/e2e");
|
|
33346
34026
|
console.log(
|
|
33347
|
-
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}"
|
|
34027
|
+
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}"
|
|
33348
34028
|
);
|
|
33349
34029
|
if (reuseNote.length > 0) {
|
|
33350
34030
|
console.log(` \u26A0 \u68C0\u6D4B\u5230\u5176\u4ED6 E2E \u76EE\u5F55:${reuseNote.join(", ")}(\u4FDD\u7559\u4E0D\u52A8,\u672A\u5F3A\u5236\u8FC1\u79FB)`);
|
|
@@ -33466,6 +34146,11 @@ function openInBrowser(file2) {
|
|
|
33466
34146
|
|
|
33467
34147
|
// src/commands/run.ts
|
|
33468
34148
|
import { join as join28 } from "node:path";
|
|
34149
|
+
function exitCodeForSummary(status) {
|
|
34150
|
+
if (status === "failed" || status === "inconclusive") return 1;
|
|
34151
|
+
if (status === "blocked") return 2;
|
|
34152
|
+
return 0;
|
|
34153
|
+
}
|
|
33469
34154
|
function makeRunCommand() {
|
|
33470
34155
|
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) => {
|
|
33471
34156
|
const client = new TestPilotClient();
|
|
@@ -33487,11 +34172,11 @@ function makeRunCommand() {
|
|
|
33487
34172
|
].filter(Boolean).join(",");
|
|
33488
34173
|
console.log("");
|
|
33489
34174
|
console.log(
|
|
33490
|
-
`Run ${summary.runId}${summary.cancelled ? "(\u5DF2\u53D6\u6D88)" : ""}: ${summary.status.toUpperCase()} \u2014 \
|
|
34175
|
+
`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} \u2298${summary.totals.blocked ?? 0} ?${summary.totals.inconclusive ?? 0} \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})` : ""}`
|
|
33491
34176
|
);
|
|
33492
34177
|
console.log(`\u4EA7\u7269\u76EE\u5F55:${join28(".testpilot", "artifacts", "runs", summary.runId)}`);
|
|
33493
34178
|
console.log("\u67E5\u770B\u62A5\u544A:npx csspilot report");
|
|
33494
|
-
|
|
34179
|
+
process.exitCode = exitCodeForSummary(summary.status);
|
|
33495
34180
|
} catch (err) {
|
|
33496
34181
|
console.error(err instanceof Error ? err.message : err);
|
|
33497
34182
|
process.exitCode = 1;
|
|
@@ -33514,12 +34199,12 @@ function consoleEventHandler(event) {
|
|
|
33514
34199
|
`);
|
|
33515
34200
|
break;
|
|
33516
34201
|
case "case-started":
|
|
33517
|
-
console.log(`\u25B6 ${event.caseId} (${event.file})`);
|
|
34202
|
+
console.log(`\u25B6 ${event.caseId}#${event.rowId ?? "default"} (${event.file})`);
|
|
33518
34203
|
break;
|
|
33519
34204
|
case "step-finished": {
|
|
33520
|
-
const mark = event.status === "passed" ? " \u2713" : event.status === "failed" ? " \u2717" : " \u21B7";
|
|
34205
|
+
const mark = event.status === "passed" ? " \u2713" : event.status === "failed" ? " \u2717" : event.status === "warning" ? " \u26A0" : " \u21B7";
|
|
33521
34206
|
console.log(
|
|
33522
|
-
`${mark} ${event.target}/${event.action} ${event.durationMs}ms${event.error ? ` \u2014 ${event.error}` : ""}`
|
|
34207
|
+
`${mark} ${event.phase ?? "steps"} ${event.target}/${event.action} ${event.durationMs}ms${event.error ? ` \u2014 ${event.error}` : ""}`
|
|
33523
34208
|
);
|
|
33524
34209
|
break;
|
|
33525
34210
|
}
|
|
@@ -33529,7 +34214,7 @@ function consoleEventHandler(event) {
|
|
|
33529
34214
|
}
|
|
33530
34215
|
|
|
33531
34216
|
// src/commands/sync-metadata.ts
|
|
33532
|
-
import { relative } from "node:path";
|
|
34217
|
+
import { relative as relative2 } from "node:path";
|
|
33533
34218
|
function makeSyncMetadataCommand() {
|
|
33534
34219
|
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) => {
|
|
33535
34220
|
const client = new TestPilotClient();
|
|
@@ -33556,7 +34241,7 @@ function makeSyncMetadataCommand() {
|
|
|
33556
34241
|
const cases = infos.filter((item) => item.valid && item.case).map((item) => ({
|
|
33557
34242
|
id: item.case.id,
|
|
33558
34243
|
title: item.case.name,
|
|
33559
|
-
file:
|
|
34244
|
+
file: relative2(process.cwd(), item.file).split("\\").join("/"),
|
|
33560
34245
|
tags: item.case.tags ?? []
|
|
33561
34246
|
}));
|
|
33562
34247
|
const skipped = infos.length - cases.length;
|
|
@@ -33666,7 +34351,7 @@ function makeValidateCommand() {
|
|
|
33666
34351
|
}
|
|
33667
34352
|
|
|
33668
34353
|
// src/cli.ts
|
|
33669
|
-
var CLI_VERSION = "0.
|
|
34354
|
+
var CLI_VERSION = "0.7.0";
|
|
33670
34355
|
async function createCli() {
|
|
33671
34356
|
const program2 = new Command();
|
|
33672
34357
|
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", () => {
|