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