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