csspilot 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -8102,7 +8102,7 @@ var require_composer = __commonJS({
8102
8102
  var node_process = __require("process");
8103
8103
  var directives = require_directives();
8104
8104
  var Document = require_Document();
8105
- var errors = require_errors();
8105
+ var errors2 = require_errors();
8106
8106
  var identity = require_identity();
8107
8107
  var composeDoc = require_compose_doc();
8108
8108
  var resolveEnd = require_resolve_end();
@@ -8149,9 +8149,9 @@ var require_composer = __commonJS({
8149
8149
  this.onError = (source, code, message, warning) => {
8150
8150
  const pos = getErrorPos(source);
8151
8151
  if (warning)
8152
- this.warnings.push(new errors.YAMLWarning(pos, code, message));
8152
+ this.warnings.push(new errors2.YAMLWarning(pos, code, message));
8153
8153
  else
8154
- this.errors.push(new errors.YAMLParseError(pos, code, message));
8154
+ this.errors.push(new errors2.YAMLParseError(pos, code, message));
8155
8155
  };
8156
8156
  this.directives = new directives.Directives({ version: options.version || "1.2" });
8157
8157
  this.options = options;
@@ -8249,7 +8249,7 @@ ${cb}` : comment;
8249
8249
  break;
8250
8250
  case "error": {
8251
8251
  const msg = token.source ? `${token.message}: ${JSON.stringify(token.source)}` : token.message;
8252
- const error62 = new errors.YAMLParseError(getErrorPos(token), "UNEXPECTED_TOKEN", msg);
8252
+ const error62 = new errors2.YAMLParseError(getErrorPos(token), "UNEXPECTED_TOKEN", msg);
8253
8253
  if (this.atDirectives || !this.doc)
8254
8254
  this.errors.push(error62);
8255
8255
  else
@@ -8259,7 +8259,7 @@ ${cb}` : comment;
8259
8259
  case "doc-end": {
8260
8260
  if (!this.doc) {
8261
8261
  const msg = "Unexpected doc-end without preceding document";
8262
- this.errors.push(new errors.YAMLParseError(getErrorPos(token), "UNEXPECTED_TOKEN", msg));
8262
+ this.errors.push(new errors2.YAMLParseError(getErrorPos(token), "UNEXPECTED_TOKEN", msg));
8263
8263
  break;
8264
8264
  }
8265
8265
  this.doc.directives.docEnd = true;
@@ -8274,7 +8274,7 @@ ${end.comment}` : end.comment;
8274
8274
  break;
8275
8275
  }
8276
8276
  default:
8277
- this.errors.push(new errors.YAMLParseError(getErrorPos(token), "UNEXPECTED_TOKEN", `Unsupported token ${token.type}`));
8277
+ this.errors.push(new errors2.YAMLParseError(getErrorPos(token), "UNEXPECTED_TOKEN", `Unsupported token ${token.type}`));
8278
8278
  }
8279
8279
  }
8280
8280
  /**
@@ -8309,7 +8309,7 @@ var require_cst_scalar = __commonJS({
8309
8309
  "use strict";
8310
8310
  var resolveBlockScalar = require_resolve_block_scalar();
8311
8311
  var resolveFlowScalar = require_resolve_flow_scalar();
8312
- var errors = require_errors();
8312
+ var errors2 = require_errors();
8313
8313
  var stringifyString = require_stringifyString();
8314
8314
  function resolveAsScalar(token, strict = true, onError) {
8315
8315
  if (token) {
@@ -8318,7 +8318,7 @@ var require_cst_scalar = __commonJS({
8318
8318
  if (onError)
8319
8319
  onError(offset, code, message);
8320
8320
  else
8321
- throw new errors.YAMLParseError([offset, offset + 1], code, message);
8321
+ throw new errors2.YAMLParseError([offset, offset + 1], code, message);
8322
8322
  };
8323
8323
  switch (token.type) {
8324
8324
  case "scalar":
@@ -10213,7 +10213,7 @@ var require_public_api = __commonJS({
10213
10213
  "use strict";
10214
10214
  var composer = require_composer();
10215
10215
  var Document = require_Document();
10216
- var errors = require_errors();
10216
+ var errors2 = require_errors();
10217
10217
  var log = require_log();
10218
10218
  var identity = require_identity();
10219
10219
  var lineCounter = require_line_counter();
@@ -10230,8 +10230,8 @@ var require_public_api = __commonJS({
10230
10230
  const docs = Array.from(composer$1.compose(parser$1.parse(source)));
10231
10231
  if (prettyErrors && lineCounter2)
10232
10232
  for (const doc of docs) {
10233
- doc.errors.forEach(errors.prettifyError(source, lineCounter2));
10234
- doc.warnings.forEach(errors.prettifyError(source, lineCounter2));
10233
+ doc.errors.forEach(errors2.prettifyError(source, lineCounter2));
10234
+ doc.warnings.forEach(errors2.prettifyError(source, lineCounter2));
10235
10235
  }
10236
10236
  if (docs.length > 0)
10237
10237
  return docs;
@@ -10246,13 +10246,13 @@ var require_public_api = __commonJS({
10246
10246
  if (!doc)
10247
10247
  doc = _doc;
10248
10248
  else if (doc.options.logLevel !== "silent") {
10249
- doc.errors.push(new errors.YAMLParseError(_doc.range.slice(0, 2), "MULTIPLE_DOCS", "Source contains multiple documents; please use YAML.parseAllDocuments()"));
10249
+ doc.errors.push(new errors2.YAMLParseError(_doc.range.slice(0, 2), "MULTIPLE_DOCS", "Source contains multiple documents; please use YAML.parseAllDocuments()"));
10250
10250
  break;
10251
10251
  }
10252
10252
  }
10253
10253
  if (prettyErrors && lineCounter2) {
10254
- doc.errors.forEach(errors.prettifyError(source, lineCounter2));
10255
- doc.warnings.forEach(errors.prettifyError(source, lineCounter2));
10254
+ doc.errors.forEach(errors2.prettifyError(source, lineCounter2));
10255
+ doc.warnings.forEach(errors2.prettifyError(source, lineCounter2));
10256
10256
  }
10257
10257
  return doc;
10258
10258
  }
@@ -10311,7 +10311,7 @@ var require_dist = __commonJS({
10311
10311
  var composer = require_composer();
10312
10312
  var Document = require_Document();
10313
10313
  var Schema = require_Schema();
10314
- var errors = require_errors();
10314
+ var errors2 = require_errors();
10315
10315
  var Alias = require_Alias();
10316
10316
  var identity = require_identity();
10317
10317
  var Pair = require_Pair();
@@ -10327,9 +10327,9 @@ var require_dist = __commonJS({
10327
10327
  exports.Composer = composer.Composer;
10328
10328
  exports.Document = Document.Document;
10329
10329
  exports.Schema = Schema.Schema;
10330
- exports.YAMLError = errors.YAMLError;
10331
- exports.YAMLParseError = errors.YAMLParseError;
10332
- exports.YAMLWarning = errors.YAMLWarning;
10330
+ exports.YAMLError = errors2.YAMLError;
10331
+ exports.YAMLParseError = errors2.YAMLParseError;
10332
+ exports.YAMLWarning = errors2.YAMLWarning;
10333
10333
  exports.Alias = Alias.Alias;
10334
10334
  exports.isAlias = identity.isAlias;
10335
10335
  exports.isCollection = identity.isCollection;
@@ -10462,6 +10462,28 @@ var TestPilotError = class extends Error {
10462
10462
  }
10463
10463
  };
10464
10464
 
10465
+ // ../../packages/core/src/verdict.ts
10466
+ var INFRA_CODES = /* @__PURE__ */ new Set([
10467
+ "ENVIRONMENT_UNAVAILABLE",
10468
+ "ADAPTER_NOT_FOUND",
10469
+ "UNSUPPORTED_ACTION",
10470
+ "FIXTURE_MISSING",
10471
+ "ACCOUNT_UNAVAILABLE",
10472
+ "CONFIG_MISSING"
10473
+ ]);
10474
+ var UNVERIFIED_CODES = /* @__PURE__ */ new Set(["PREREQ_VARIABLE_MISSING"]);
10475
+ function verdictFromErrorCode(code) {
10476
+ if (code !== void 0 && INFRA_CODES.has(code)) return "infra";
10477
+ if (code !== void 0 && UNVERIFIED_CODES.has(code)) return "unverified";
10478
+ return "product";
10479
+ }
10480
+ function caseStatusFromErrorCode(code) {
10481
+ const verdict = verdictFromErrorCode(code);
10482
+ if (verdict === "infra") return "blocked";
10483
+ if (verdict === "unverified") return "inconclusive";
10484
+ return "failed";
10485
+ }
10486
+
10465
10487
  // ../../packages/core/src/constants/index.ts
10466
10488
  import { join as join2 } from "node:path";
10467
10489
  var TESTPILOT_DIR = ".testpilot";
@@ -30365,7 +30387,11 @@ var ACTIONS = [
30365
30387
  "assert",
30366
30388
  "extract",
30367
30389
  "screenshot",
30368
- "request"
30390
+ "request",
30391
+ "callMethod",
30392
+ "mockWxApi",
30393
+ "trigger",
30394
+ "setData"
30369
30395
  ];
30370
30396
  var locatorSchema = external_exports.strictObject({
30371
30397
  text: external_exports.string().min(1).optional(),
@@ -30393,7 +30419,25 @@ var stepSchema = external_exports.strictObject({
30393
30419
  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(),
30394
30420
  timeout: external_exports.number().int().positive().optional(),
30395
30421
  /** UI 动作触发的网络请求精确次数断言 */
30396
- expectRequests: external_exports.array(requestExpectationSchema).min(1).optional()
30422
+ expectRequests: external_exports.array(requestExpectationSchema).min(1).optional(),
30423
+ /** callMethod:要调用的 Page / 组件方法名(仅 miniapp;页面级调用不写 locator) */
30424
+ methodName: external_exports.string().min(1).optional(),
30425
+ /** callMethod 的入参(支持 ${var} 模板) */
30426
+ args: external_exports.array(external_exports.unknown()).optional(),
30427
+ /** mockWxApi:要 mock 的 wx.* API 名(如 showModal / request) */
30428
+ wxApi: external_exports.string().min(1).optional(),
30429
+ /** mockWxApi:mock 返回值(对象按原样注入;capture 时真实调用先上报再返回 mock 值) */
30430
+ result: external_exports.record(external_exports.string(), external_exports.unknown()).optional(),
30431
+ /** mockWxApi:捕获真实调用参数的变量名(${<capture>.options} / ${<capture>.called}) */
30432
+ capture: external_exports.string().min(1).optional(),
30433
+ /** trigger:触发的事件类型(如 getphonenumber;空 detail 即"用户拒绝/取消"类分支) */
30434
+ eventType: external_exports.string().min(1).optional(),
30435
+ /** trigger:作为事件 e.detail 注入(值支持 ${var} 模板) */
30436
+ detail: external_exports.record(external_exports.string(), external_exports.unknown()).optional(),
30437
+ /** setData:写入页面 data 的补丁(值支持 ${var} 模板) */
30438
+ patch: external_exports.record(external_exports.string(), external_exports.unknown()).optional(),
30439
+ /** assert:页面 data 路径断言(与 locator 互斥,仅 miniapp) */
30440
+ dataPath: external_exports.string().min(1).optional()
30397
30441
  });
30398
30442
  var datasetReferenceSchema = external_exports.strictObject({
30399
30443
  file: external_exports.string().min(1),
@@ -30466,7 +30510,11 @@ var ADAPTER_SUPPORT = {
30466
30510
  assert: ["web", "miniapp", "api"],
30467
30511
  extract: ["web", "miniapp", "api"],
30468
30512
  screenshot: ["web", "miniapp"],
30469
- request: ["api"]
30513
+ request: ["api"],
30514
+ callMethod: ["miniapp"],
30515
+ mockWxApi: ["miniapp"],
30516
+ trigger: ["miniapp"],
30517
+ setData: ["miniapp"]
30470
30518
  };
30471
30519
  var LOCATOR_REQUIRED = /* @__PURE__ */ new Set(["click", "input", "select", "assert", "extract"]);
30472
30520
  var VALUE_REQUIRED = /* @__PURE__ */ new Set(["input", "select"]);
@@ -30479,7 +30527,16 @@ var FIELD_ALLOWED = {
30479
30527
  timeout: ["wait"],
30480
30528
  method: ["request"],
30481
30529
  headers: ["request"],
30482
- body: ["request"]
30530
+ body: ["request"],
30531
+ methodName: ["callMethod"],
30532
+ args: ["callMethod"],
30533
+ wxApi: ["mockWxApi"],
30534
+ result: ["mockWxApi"],
30535
+ capture: ["mockWxApi"],
30536
+ eventType: ["trigger"],
30537
+ detail: ["trigger"],
30538
+ patch: ["setData"],
30539
+ dataPath: ["assert"]
30483
30540
  };
30484
30541
  var VARIABLE_REF = /\$\{([A-Za-z_][A-Za-z0-9_.]*)\}/g;
30485
30542
  function validateCase(data) {
@@ -30543,6 +30600,23 @@ function checkStep(step, at, declared, issues) {
30543
30600
  });
30544
30601
  }
30545
30602
  }
30603
+ if (step.action === "mockWxApi") {
30604
+ if (!step.wxApi) {
30605
+ issues.push({ path: `${at}.wxApi`, code: "semantic", message: "mockWxApi \u5FC5\u987B\u63D0\u4F9B wxApi(\u8981 mock \u7684 wx.* API \u540D)" });
30606
+ }
30607
+ if (!step.result) {
30608
+ issues.push({ path: `${at}.result`, code: "semantic", message: "mockWxApi \u5FC5\u987B\u63D0\u4F9B result(mock \u8FD4\u56DE\u503C)" });
30609
+ }
30610
+ }
30611
+ if (step.action === "trigger" && !step.eventType) {
30612
+ issues.push({ path: `${at}.eventType`, code: "semantic", message: "trigger \u5FC5\u987B\u63D0\u4F9B eventType(\u89E6\u53D1\u7684\u4E8B\u4EF6\u7C7B\u578B)" });
30613
+ }
30614
+ if (step.action === "setData" && (!step.patch || Object.keys(step.patch).length === 0)) {
30615
+ issues.push({ path: `${at}.patch`, code: "semantic", message: "setData \u5FC5\u987B\u63D0\u4F9B patch(\u5199\u5165\u9875\u9762 data \u7684\u8865\u4E01)" });
30616
+ }
30617
+ if (step.dataPath !== void 0 && step.target !== "miniapp") {
30618
+ issues.push({ path: `${at}.dataPath`, code: "semantic", message: "dataPath \u65AD\u8A00\u4EC5 miniapp \u652F\u6301(web \u7AEF\u7528 locator)" });
30619
+ }
30546
30620
  if (step.action === "navigate" && !step.url) {
30547
30621
  issues.push({ path: `${at}.url`, code: "semantic", message: "navigate \u5FC5\u987B\u63D0\u4F9B url" });
30548
30622
  }
@@ -30574,6 +30648,13 @@ function checkStep(step, at, declared, issues) {
30574
30648
  if (step.action === "wait" && !step.locator && step.timeout === void 0) {
30575
30649
  issues.push({ path: at, code: "semantic", message: "wait \u9700\u8981\u63D0\u4F9B locator(\u7B49\u5F85\u5143\u7D20)\u6216 timeout(\u6BEB\u79D2)" });
30576
30650
  }
30651
+ if (step.action === "callMethod" && !step.methodName) {
30652
+ issues.push({
30653
+ path: `${at}.methodName`,
30654
+ code: "semantic",
30655
+ message: "callMethod \u5FC5\u987B\u63D0\u4F9B methodName(\u9875\u9762\u6216\u7EC4\u4EF6\u7684\u65B9\u6CD5\u540D)"
30656
+ });
30657
+ }
30577
30658
  if (step.expectRequests) {
30578
30659
  if (step.target === "api" || !REQUEST_EXPECTATION_ACTIONS.has(step.action)) {
30579
30660
  issues.push({
@@ -30615,11 +30696,17 @@ function checkVariableRefs(step, at, declared, issues) {
30615
30696
  collect("value", step.value);
30616
30697
  collect("expected", step.expected);
30617
30698
  collect("url", step.url);
30699
+ collect("methodName", step.methodName);
30618
30700
  collect("locator.text", step.locator?.text);
30619
30701
  collect("locator.css", step.locator?.css);
30620
30702
  if (typeof step.body === "string") collect("body", step.body);
30621
30703
  else if (step.body) collect("body", JSON.stringify(step.body));
30622
30704
  for (const headerValue of Object.values(step.headers ?? {})) collect("headers", headerValue);
30705
+ if (Array.isArray(step.args)) {
30706
+ step.args.forEach((arg, index) => {
30707
+ if (typeof arg === "string") collect(`args[${index}]`, arg);
30708
+ });
30709
+ }
30623
30710
  for (const [index, expectation] of (step.expectRequests ?? []).entries()) {
30624
30711
  collect(`expectRequests[${index}].urlContains`, expectation.urlContains);
30625
30712
  }
@@ -30716,6 +30803,9 @@ async function loadCaseDataFile(reference, options) {
30716
30803
  const [realDataDir, realTarget] = await Promise.all([
30717
30804
  realpath(dataDir).catch(() => dataDir),
30718
30805
  realpath(target).catch((error62) => {
30806
+ if (error62 instanceof Error && error62.code === "ENOENT") {
30807
+ throw new TestPilotError("FIXTURE_MISSING", `\u6570\u636E\u6587\u4EF6\u4E0D\u5B58\u5728:${reference}`);
30808
+ }
30719
30809
  throw new Error(
30720
30810
  `\u8BFB\u53D6\u6570\u636E\u6587\u4EF6\u5931\u8D25:${reference}:${error62 instanceof Error ? error62.message : String(error62)}`
30721
30811
  );
@@ -30943,6 +31033,9 @@ async function validateCaseDataSource(source, file2, root) {
30943
31033
  );
30944
31034
  return validation;
30945
31035
  } catch (error62) {
31036
+ if (error62 instanceof TestPilotError && error62.code === "FIXTURE_MISSING") {
31037
+ return validation;
31038
+ }
30946
31039
  return {
30947
31040
  ok: false,
30948
31041
  data: validation.data,
@@ -30991,6 +31084,24 @@ function classifyFailure(message) {
30991
31084
  }
30992
31085
  return "other";
30993
31086
  }
31087
+ var CODE_CATEGORIES = {
31088
+ ASSERTION_FAILED: "assert",
31089
+ REQUEST_COUNT_MISMATCH: "assert",
31090
+ STATUS_MISMATCH: "assert",
31091
+ STEP_TIMEOUT: "assert",
31092
+ ELEMENT_NOT_FOUND: "element",
31093
+ ADAPTER_NOT_FOUND: "adapter",
31094
+ UNSUPPORTED_ACTION: "adapter",
31095
+ CONFIG_MISSING: "adapter",
31096
+ ENVIRONMENT_UNAVAILABLE: "env",
31097
+ FIXTURE_MISSING: "env",
31098
+ ACCOUNT_UNAVAILABLE: "env",
31099
+ PREREQ_VARIABLE_MISSING: "other"
31100
+ };
31101
+ function classifyFromCode(code) {
31102
+ if (code === void 0) return void 0;
31103
+ return CODE_CATEGORIES[code];
31104
+ }
30994
31105
 
30995
31106
  // ../../packages/reporter/src/history.ts
30996
31107
  import { readFile as readFile5, readdir as readdir2 } from "node:fs/promises";
@@ -31048,7 +31159,7 @@ async function writeReports(runDir, summary, options = {}) {
31048
31159
  const cases = summary.cases.map((item) => {
31049
31160
  const categories = /* @__PURE__ */ new Set();
31050
31161
  const steps = item.steps.map((step) => {
31051
- const category = step.status === "failed" ? classifyFailure(step.error) : void 0;
31162
+ const category = classifyFromCode(step.errorCode) ?? (step.status === "failed" ? classifyFailure(step.error) : void 0);
31052
31163
  if (category) {
31053
31164
  categories.add(category);
31054
31165
  failures.push({
@@ -31056,7 +31167,8 @@ async function writeReports(runDir, summary, options = {}) {
31056
31167
  label: categoryLabel(category),
31057
31168
  caseId: item.caseId,
31058
31169
  index: step.index,
31059
- message: step.error ?? ""
31170
+ message: step.error ?? "",
31171
+ verdict: verdictFromErrorCode(step.errorCode ?? item.errorCode)
31060
31172
  });
31061
31173
  }
31062
31174
  return { ...step, category };
@@ -31079,6 +31191,20 @@ async function writeReports(runDir, summary, options = {}) {
31079
31191
  await writeFile4(htmlPath, renderHtml(data), "utf8");
31080
31192
  return { json: jsonPath, html: htmlPath };
31081
31193
  }
31194
+ var VERDICT_GROUPS = [
31195
+ { verdict: "product", title: "\u4EA7\u54C1\u5931\u8D25" },
31196
+ { verdict: "infra", title: "\u73AF\u5883\u4E0E\u524D\u7F6E(\u975E\u4EA7\u54C1\u56DE\u5F52)" },
31197
+ { verdict: "unverified", title: "\u672A\u9A8C\u8BC1" }
31198
+ ];
31199
+ function verdictGroupsHtml(data) {
31200
+ return VERDICT_GROUPS.map((group) => {
31201
+ const items = data.failures.filter((failure2) => failure2.verdict === group.verdict);
31202
+ const list = items.length > 0 ? items.map(
31203
+ (failure2) => `<li><code>${escapeHtml(failure2.caseId)}</code> #${failure2.index} ${escapeHtml(failure2.message.slice(0, 160))}</li>`
31204
+ ).join("\n") : '<li class="muted">\u65E0</li>';
31205
+ return `<div class="verdict-group"><h3>${escapeHtml(group.title)}(${items.length})</h3><ul>${list}</ul></div>`;
31206
+ }).join("\n");
31207
+ }
31082
31208
  function categoryChips(data) {
31083
31209
  const counts = /* @__PURE__ */ new Map();
31084
31210
  for (const failure2 of data.failures) {
@@ -31104,19 +31230,20 @@ function renderHtml(data) {
31104
31230
  const trendSvg = renderTrend(data);
31105
31231
  const casesHtml = groupedCasesHtml(summary.cases);
31106
31232
  const chips = categoryChips(data);
31233
+ const verdictHtml = verdictGroupsHtml(data);
31107
31234
  return `<!doctype html>
31108
31235
  <html lang="zh-CN">
31109
31236
  <head>
31110
31237
  <meta charset="utf-8" />
31111
31238
  <title>TestPilot Report ${escapeHtml(summary.runId)}</title>
31112
31239
  <style>
31113
- :root { --ok:#1a7f37; --fail:#cf222e; --warn:#9a6700; --skip:#6e7781; --line:#d0d7de; --muted:#59636e; }
31240
+ :root { --ok:#1a7f37; --fail:#cf222e; --warn:#9a6700; --skip:#6e7781; --blocked:#8250df; --inconclusive:#9a6700; --line:#d0d7de; --muted:#59636e; }
31114
31241
  * { box-sizing: border-box; }
31115
31242
  body { font-family: system-ui, "Segoe UI", sans-serif; margin: 0; color: #1f2328; background: #f6f8fa; }
31116
31243
  .wrap { max-width: 1080px; margin: 0 auto; padding: 20px 16px 48px; }
31117
31244
  h1 { font-size: 20px; margin: 8px 0; } h2 { font-size: 15px; margin: 24px 0 8px; }
31118
31245
  .badge { display: inline-block; padding: 2px 10px; border-radius: 10px; color: #fff; font-size: 12px; }
31119
- .passed { background: var(--ok); } .failed { background: var(--fail); } .warning { background: var(--warn); } .skipped { background: var(--skip); }
31246
+ .passed { background: var(--ok); } .failed { background: var(--fail); } .warning { background: var(--warn); } .skipped { background: var(--skip); } .blocked { background: var(--blocked); } .inconclusive { background: var(--inconclusive); }
31120
31247
  code { background: #eef1f4; padding: 1px 5px; border-radius: 4px; font-size: 12px; }
31121
31248
  .cards { display: flex; gap: 12px; flex-wrap: wrap; margin: 14px 0; }
31122
31249
  .card { background: #fff; border: 1px solid var(--line); border-radius: 8px; padding: 10px 16px; min-width: 130px; }
@@ -31143,6 +31270,11 @@ function renderHtml(data) {
31143
31270
  .muted { color: var(--muted); }
31144
31271
  .hide { display: none !important; }
31145
31272
  .meta { color: var(--muted); font-size: 12px; margin: 6px 0; }
31273
+ .verdicts { display: flex; gap: 14px; flex-wrap: wrap; }
31274
+ .verdict-group { background: #fff; border: 1px solid var(--line); border-radius: 8px; padding: 10px 14px; flex: 1; min-width: 220px; }
31275
+ .verdict-group h3 { font-size: 13px; margin: 0 0 6px; }
31276
+ .verdict-group ul { margin: 0; padding-left: 18px; font-size: 12px; }
31277
+ .verdict-group li { margin: 3px 0; }
31146
31278
  </style>
31147
31279
  </head>
31148
31280
  <body>
@@ -31163,6 +31295,9 @@ function renderHtml(data) {
31163
31295
  <h2>\u5386\u53F2\u8D8B\u52BF(\u6700\u8FD1 ${data.history.length + 1} \u6B21\u8FD0\u884C)</h2>
31164
31296
  <div class="trend">${trendSvg}</div>
31165
31297
 
31298
+ <h2>\u5931\u8D25\u4E09\u6876</h2>
31299
+ <div class="verdicts">${verdictHtml}</div>
31300
+
31166
31301
  <h2>\u5931\u8D25\u5206\u7C7B</h2>
31167
31302
  <div class="toolbar" id="cat-chips">${chips}</div>
31168
31303
 
@@ -31371,7 +31506,7 @@ function buildSummaryData(runs) {
31371
31506
  for (const item of summary.cases) {
31372
31507
  const platforms = [...new Set(item.steps.map((step) => step.target))];
31373
31508
  const failedStep = item.steps.find((step) => step.status === "failed");
31374
- const category = item.status === "failed" ? classifyFailure(failedStep?.error) : void 0;
31509
+ const category = classifyFromCode(item.errorCode ?? failedStep?.errorCode) ?? (item.status === "failed" ? classifyFailure(failedStep?.error) : void 0);
31375
31510
  executions.push({
31376
31511
  runId,
31377
31512
  caseId: item.caseId,
@@ -31434,6 +31569,10 @@ function buildSummaryData(runs) {
31434
31569
  })).filter((item) => item.count > 0);
31435
31570
  const durations = runs.map((item) => item.summary.durationMs).filter((value) => typeof value === "number");
31436
31571
  return {
31572
+ buckets: {
31573
+ blocked: executions.filter((exec2) => exec2.status === "blocked").length,
31574
+ inconclusive: executions.filter((exec2) => exec2.status === "inconclusive").length
31575
+ },
31437
31576
  window: {
31438
31577
  runs: runs.length,
31439
31578
  from: runPoints[0]?.startedAt,
@@ -31518,7 +31657,7 @@ function renderSummaryHtml(data) {
31518
31657
  th.num, td.num { text-align: right; }
31519
31658
  code { background: #eef1f4; padding: 1px 5px; border-radius: 4px; font-size: 12px; cursor: pointer; }
31520
31659
  .badge { display: inline-block; padding: 1px 9px; border-radius: 10px; color: #fff; font-size: 11px; }
31521
- .passed { background: #1a7f37; } .failed { background: #cf222e; } .unknown { background: #6e7781; }
31660
+ .passed { background: #1a7f37; } .failed { background: #cf222e; } .unknown { background: #6e7781; } .blocked { background: #8250df; } .inconclusive { background: #9a6700; }
31522
31661
  .toolbar { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin: 10px 0; }
31523
31662
  .chip { border: 1px solid var(--line); background: #fff; border-radius: 14px; padding: 3px 12px; font-size: 12px; cursor: pointer; }
31524
31663
  .chip.active { background: #0969da; color: #fff; border-color: #0969da; }
@@ -31544,6 +31683,8 @@ function renderSummaryHtml(data) {
31544
31683
  <div class="card"><b>${rateText}</b><span>\u6574\u4F53\u901A\u8FC7\u7387</span></div>
31545
31684
  <div class="card"><b>${(kpis.avgDurationMs / 1e3).toFixed(1)}s</b><span>\u5E73\u5747\u8FD0\u884C\u8017\u65F6</span></div>
31546
31685
  <div class="card"><b>${kpis.flakyCount}</b><span>Flaky \u7528\u4F8B</span></div>
31686
+ ${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>` : ""}
31687
+ ${data.buckets.inconclusive > 0 ? `<div class="card"><b>${data.buckets.inconclusive} ?</b><span>\u672A\u9A8C\u8BC1(\u8BC1\u636E\u4E0D\u8DB3)</span></div>` : ""}
31547
31688
  </div>
31548
31689
 
31549
31690
  <h2>\u8D8B\u52BF(\u6BCF\u6B21\u8FD0\u884C\u7ED3\u679C\u4E0E\u901A\u8FC7\u7387,\u60AC\u6D6E\u770B\u8BE6\u60C5)</h2>
@@ -31752,14 +31893,23 @@ var ExecutionContext = class {
31752
31893
  }
31753
31894
  }
31754
31895
  /**
31755
- * 将字符串中的 ${name} 替换为已提取的变量值;未定义的引用保持原样(校验阶段已拦截)。
31756
- * 名字允许点号,用于引擎注入的 account.username 一类凭据变量(用户 extract 变量名不含点号)。
31896
+ * 将字符串中的 ${name} 替换为已提取的变量值;未定义的引用抛 TestPilotError——
31897
+ * 校验阶段已静态拦截"从未声明"的引用,运行时未命中即数据未兑现
31898
+ * (前置 extract 未执行或 fixture/dataset/凭据缺失),按三态归 unverified/account 桶。
31899
+ * 凭据桶仅限 `account` 整值或 `account.*` 字段;用户变量撞 account 前缀(如 accountId)
31900
+ * 不误归凭据桶。名字允许点号,用于引擎注入的 account.username 一类凭据变量
31901
+ * (用户 extract 变量名不含点号)。
31757
31902
  */
31758
31903
  resolve(template) {
31759
- return template.replace(
31760
- /\$\{([A-Za-z_][A-Za-z0-9_.]*)\}/g,
31761
- (match, name) => this.variables.get(name) ?? match
31762
- );
31904
+ return template.replace(/\$\{([A-Za-z_][A-Za-z0-9_.]*)\}/g, (_match, name) => {
31905
+ const value = this.variables.get(name);
31906
+ if (value !== void 0) return value;
31907
+ const isAccount = name === "account" || name.startsWith("account.");
31908
+ throw new TestPilotError(
31909
+ isAccount ? "ACCOUNT_UNAVAILABLE" : "PREREQ_VARIABLE_MISSING",
31910
+ `\u53D8\u91CF "${name}" \u672A\u5B9A\u4E49(\u524D\u7F6E extract \u672A\u6267\u884C\u6216 fixture/dataset/\u51ED\u636E\u7F3A\u5931)`
31911
+ );
31912
+ });
31763
31913
  }
31764
31914
  /**
31765
31915
  * 注入 accountRef 凭据:值为 JSON 对象时按 `${account.<字段>}` 展开为字符串,
@@ -31784,6 +31934,26 @@ var ExecutionContext = class {
31784
31934
  }
31785
31935
  this.variables.set("account", rawValue);
31786
31936
  }
31937
+ /**
31938
+ * 注入结构化对象(如 mockWxApi 的捕获结果):按点号路径展开为平铺字符串变量。
31939
+ * 例如 seedObject('showModalCall', { called: true, args: { content: 'x' } }) 后,
31940
+ * `${showModalCall.called}` 与 `${showModalCall.args.content}` 均可解析。
31941
+ */
31942
+ seedObject(name, value) {
31943
+ const flatten = (prefix, input2) => {
31944
+ if (input2 !== null && typeof input2 === "object" && !Array.isArray(input2)) {
31945
+ const entries = Object.entries(input2);
31946
+ if (entries.length === 0) {
31947
+ this.variables.set(prefix, "");
31948
+ return;
31949
+ }
31950
+ for (const [key, item] of entries) flatten(`${prefix}.${key}`, item);
31951
+ return;
31952
+ }
31953
+ this.variables.set(prefix, input2 === void 0 ? "" : String(input2));
31954
+ };
31955
+ flatten(name, value);
31956
+ }
31787
31957
  };
31788
31958
 
31789
31959
  // ../../packages/execution-engine/src/lifecycle/index.ts
@@ -31918,7 +32088,7 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
31918
32088
  } : void 0;
31919
32089
  if (step.expectRequests) {
31920
32090
  if (!adapter.startRequestCapture || !adapter.stopRequestCapture) {
31921
- throw new Error(`\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301\u8BF7\u6C42\u6B21\u6570\u65AD\u8A00`);
32091
+ throw new TestPilotError("UNSUPPORTED_ACTION", `\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301\u8BF7\u6C42\u6B21\u6570\u65AD\u8A00`);
31922
32092
  }
31923
32093
  await adapter.startRequestCapture();
31924
32094
  captureStarted = true;
@@ -31942,14 +32112,22 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
31942
32112
  case "wait":
31943
32113
  await adapter.wait(locator, step.timeout);
31944
32114
  break;
31945
- case "assert":
32115
+ case "assert": {
32116
+ if (step.dataPath !== void 0) {
32117
+ if (!adapter.assertData) {
32118
+ throw new TestPilotError("UNSUPPORTED_ACTION", `\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301 dataPath \u65AD\u8A00`);
32119
+ }
32120
+ await adapter.assertData(context.resolve(step.dataPath), context.resolve(step.expected ?? ""));
32121
+ break;
32122
+ }
31946
32123
  if (step.target === "api") {
31947
- if (!adapter.assertResponse) throw new Error("\u5F53\u524D adapter \u4E0D\u652F\u6301 api \u65AD\u8A00");
32124
+ if (!adapter.assertResponse) throw new TestPilotError("UNSUPPORTED_ACTION", "\u5F53\u524D adapter \u4E0D\u652F\u6301 api \u65AD\u8A00");
31948
32125
  await adapter.assertResponse(context.resolve(step.expected ?? ""));
31949
32126
  } else {
31950
32127
  await adapter.assert(locator, context.resolve(step.expected ?? ""));
31951
32128
  }
31952
32129
  break;
32130
+ }
31953
32131
  case "extract": {
31954
32132
  const value = step.target === "api" ? await extractApiResponse(adapter, context.resolve(step.value ?? "")) : await adapter.extract(locator);
31955
32133
  if (step.variable) {
@@ -31964,7 +32142,7 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
31964
32142
  break;
31965
32143
  }
31966
32144
  case "request": {
31967
- if (!adapter.request) throw new Error("\u5F53\u524D adapter \u4E0D\u652F\u6301 api request");
32145
+ if (!adapter.request) throw new TestPilotError("UNSUPPORTED_ACTION", "\u5F53\u524D adapter \u4E0D\u652F\u6301 api request");
31968
32146
  const method = step.method ?? "GET";
31969
32147
  const url2 = context.resolve(step.url ?? "");
31970
32148
  const headers = resolveHeaders(step.headers, context);
@@ -31981,16 +32159,53 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
31981
32159
  if (step.expected !== void 0) {
31982
32160
  const expectedStatus = context.resolve(step.expected);
31983
32161
  if (String(response.status) !== expectedStatus) {
31984
- throw new Error(
32162
+ throw new TestPilotError(
32163
+ "STATUS_MISMATCH",
31985
32164
  `HTTP \u72B6\u6001\u7801 ${response.status} ${response.statusText},\u671F\u671B ${expectedStatus}:${snippet(response.bodyText, 200)}`
31986
32165
  );
31987
32166
  }
31988
32167
  }
31989
32168
  break;
31990
32169
  }
32170
+ case "callMethod": {
32171
+ if (typeof adapter.callMethod !== "function") {
32172
+ throw new TestPilotError("UNSUPPORTED_ACTION", `\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301 callMethod`);
32173
+ }
32174
+ const methodName = context.resolve(step.methodName ?? "");
32175
+ if (!methodName) throw new TestPilotError("MISSING_METHOD_NAME", "callMethod \u5FC5\u987B\u63D0\u4F9B methodName");
32176
+ const callArgs = (step.args ?? []).map((arg) => resolveDeep(arg, context));
32177
+ await adapter.callMethod(locator, methodName, callArgs);
32178
+ break;
32179
+ }
32180
+ case "mockWxApi": {
32181
+ if (typeof adapter.mockWxApi !== "function") {
32182
+ throw new TestPilotError("UNSUPPORTED_ACTION", `\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301 mockWxApi`);
32183
+ }
32184
+ await adapter.mockWxApi({
32185
+ api: context.resolve(step.wxApi ?? ""),
32186
+ result: step.result ? resolveDeep(step.result, context) : void 0,
32187
+ capture: step.capture
32188
+ });
32189
+ break;
32190
+ }
32191
+ case "trigger": {
32192
+ if (typeof adapter.trigger !== "function") {
32193
+ throw new TestPilotError("UNSUPPORTED_ACTION", `\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301 trigger`);
32194
+ }
32195
+ if (!locator) throw new TestPilotError("MISSING_LOCATOR", "trigger \u5FC5\u987B\u63D0\u4F9B locator(\u8981\u89E6\u53D1\u4E8B\u4EF6\u7684\u7EC4\u4EF6)");
32196
+ await adapter.trigger(locator, context.resolve(step.eventType ?? ""), step.detail ? resolveDeep(step.detail, context) : void 0);
32197
+ break;
32198
+ }
32199
+ case "setData": {
32200
+ if (typeof adapter.setData !== "function") {
32201
+ throw new TestPilotError("UNSUPPORTED_ACTION", `\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301 setData`);
32202
+ }
32203
+ await adapter.setData(resolveDeep(step.patch ?? {}, context));
32204
+ break;
32205
+ }
31991
32206
  default: {
31992
32207
  const exhaustive = step.action;
31993
- throw new Error(`\u4E0D\u652F\u6301\u7684 action:${String(exhaustive)}`);
32208
+ throw new TestPilotError("UNSUPPORTED_ACTION", `\u4E0D\u652F\u6301\u7684 action:${String(exhaustive)}`);
31994
32209
  }
31995
32210
  }
31996
32211
  if (step.expectRequests) {
@@ -32001,7 +32216,8 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
32001
32216
  result.requestAssertions = buildRequestAssertions(step, requests, context);
32002
32217
  const mismatches = result.requestAssertions.filter((item) => item.actualCount !== item.count);
32003
32218
  if (mismatches.length > 0) {
32004
- throw new Error(
32219
+ throw new TestPilotError(
32220
+ "REQUEST_COUNT_MISMATCH",
32005
32221
  mismatches.map(
32006
32222
  (item) => `\u8BF7\u6C42\u6B21\u6570\u4E0D\u5339\u914D:${item.method ?? "*"} ${item.urlContains},\u5B9E\u9645 ${item.actualCount},\u671F\u671B ${item.count}`
32007
32223
  ).join("; ")
@@ -32018,6 +32234,7 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
32018
32234
  }
32019
32235
  result.status = "failed";
32020
32236
  result.error = err instanceof Error ? err.message : String(err);
32237
+ result.errorCode = err instanceof TestPilotError ? err.code : void 0;
32021
32238
  try {
32022
32239
  const data = await adapter.screenshot();
32023
32240
  result.screenshot = await artifacts.saveStepScreenshot(`${options.screenshotName}-failed.png`, data);
@@ -32028,8 +32245,8 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
32028
32245
  return result;
32029
32246
  }
32030
32247
  async function extractApiResponse(adapter, path) {
32031
- if (!adapter.extractResponse) throw new Error("\u5F53\u524D adapter \u4E0D\u652F\u6301 api \u63D0\u53D6");
32032
- if (!path) throw new Error("api \u7AEF extract \u9700\u8981\u63D0\u4F9B value(\u70B9\u53F7 JSON path)");
32248
+ if (!adapter.extractResponse) throw new TestPilotError("UNSUPPORTED_ACTION", "\u5F53\u524D adapter \u4E0D\u652F\u6301 api \u63D0\u53D6");
32249
+ if (!path) throw new TestPilotError("UNSUPPORTED_ACTION", "api \u7AEF extract \u9700\u8981\u63D0\u4F9B value(\u70B9\u53F7 JSON path)");
32033
32250
  return adapter.extractResponse(path);
32034
32251
  }
32035
32252
  function resolveHeaders(headers, context) {
@@ -32103,6 +32320,7 @@ var ExecutionEngine = class {
32103
32320
  const warnings = [];
32104
32321
  const evidenceAdapters = /* @__PURE__ */ new Map();
32105
32322
  let caseError;
32323
+ let caseErrorCode;
32106
32324
  const executionId = safeArtifactName(`${data.id}-${rowId}`);
32107
32325
  await bus.emit({
32108
32326
  type: "case-started",
@@ -32173,6 +32391,12 @@ var ExecutionEngine = class {
32173
32391
  result = await executeStep(step, index, adapter, context, artifacts, {
32174
32392
  screenshotName: `${executionId}-${phase}-${String(index).padStart(2, "0")}`
32175
32393
  });
32394
+ if (step.capture !== void 0 && typeof adapter.readWxApiCaptures === "function") {
32395
+ const captures = adapter.readWxApiCaptures();
32396
+ if (captures && captures.calls.length > 0) {
32397
+ context.seedObject(step.capture, captures.calls[0]?.options ?? null);
32398
+ }
32399
+ }
32176
32400
  } catch (err) {
32177
32401
  result = {
32178
32402
  index,
@@ -32180,7 +32404,8 @@ var ExecutionEngine = class {
32180
32404
  action: step.action,
32181
32405
  status: "failed",
32182
32406
  durationMs: 0,
32183
- error: err instanceof Error ? err.message : String(err)
32407
+ error: err instanceof Error ? err.message : String(err),
32408
+ errorCode: err instanceof TestPilotError ? err.code : void 0
32184
32409
  };
32185
32410
  }
32186
32411
  result.phase = phase;
@@ -32209,6 +32434,7 @@ var ExecutionEngine = class {
32209
32434
  if (result.status === "failed") {
32210
32435
  passed = false;
32211
32436
  caseError = result.error;
32437
+ caseErrorCode = result.errorCode;
32212
32438
  for (const [skipIndex, skippedStep] of phaseSteps.entries()) {
32213
32439
  if (skipIndex <= index) continue;
32214
32440
  steps.push({
@@ -32225,24 +32451,42 @@ var ExecutionEngine = class {
32225
32451
  }
32226
32452
  return passed;
32227
32453
  };
32228
- const setupPassed = await runPhase("setup", data.setup ?? []);
32229
- if (setupPassed) {
32230
- await runPhase("steps", data.steps);
32231
- } else {
32232
- for (const [index, step] of data.steps.entries()) {
32454
+ if (options.blockedError) {
32455
+ const first = data.setup?.[0] ?? data.steps[0];
32456
+ if (first) {
32233
32457
  steps.push({
32234
- index,
32235
- phase: "steps",
32236
- target: step.target,
32237
- action: step.action,
32238
- status: "skipped",
32239
- durationMs: 0
32458
+ index: 0,
32459
+ phase: "setup",
32460
+ target: first.target,
32461
+ action: first.action,
32462
+ status: "failed",
32463
+ durationMs: 0,
32464
+ error: options.blockedError.message,
32465
+ errorCode: options.blockedError.code
32240
32466
  });
32241
32467
  }
32468
+ caseError = options.blockedError.message;
32469
+ caseErrorCode = options.blockedError.code;
32470
+ } else {
32471
+ const setupPassed = await runPhase("setup", data.setup ?? []);
32472
+ if (setupPassed) {
32473
+ await runPhase("steps", data.steps);
32474
+ } else {
32475
+ for (const [index, step] of data.steps.entries()) {
32476
+ steps.push({
32477
+ index,
32478
+ phase: "steps",
32479
+ target: step.target,
32480
+ action: step.action,
32481
+ status: "skipped",
32482
+ durationMs: 0
32483
+ });
32484
+ }
32485
+ }
32486
+ await runPhase("teardown", data.teardown ?? []);
32242
32487
  }
32243
- await runPhase("teardown", data.teardown ?? []);
32244
32488
  const finishedAt = /* @__PURE__ */ new Date();
32245
- const status = caseError ? "failed" : "passed";
32489
+ const status = caseError ? caseStatusFromErrorCode(caseErrorCode) : "passed";
32246
32490
  const caseResult = {
32247
32491
  caseId: data.id,
32248
32492
  caseName: data.name,
@@ -32256,6 +32500,7 @@ var ExecutionEngine = class {
32256
32500
  durationMs: finishedAt.getTime() - startedAt.getTime()
32257
32501
  };
32258
32502
  if (caseError) caseResult.error = caseError;
32503
+ if (caseErrorCode) caseResult.errorCode = caseErrorCode;
32259
32504
  if (warnings.length > 0) caseResult.warnings = warnings;
32260
32505
  for (const [target, adapter] of evidenceAdapters) {
32261
32506
  if (!adapter.stopEvidence) continue;
@@ -32273,6 +32518,9 @@ var ExecutionEngine = class {
32273
32518
  );
32274
32519
  }
32275
32520
  }
32521
+ for (const [, adapter] of evidenceAdapters) {
32522
+ await adapter.restoreWxMock?.().catch(() => void 0);
32523
+ }
32276
32524
  await bus.emit({
32277
32525
  type: "case-finished",
32278
32526
  runId,
@@ -32299,6 +32547,8 @@ function buildRunSummary(runId, startedAt, cases, options = {}) {
32299
32547
  passed: cases.filter((item) => item.status === "passed").length,
32300
32548
  failed: cases.filter((item) => item.status === "failed").length,
32301
32549
  skipped: cases.filter((item) => item.status === "skipped").length,
32550
+ ...cases.some((item) => item.status === "blocked") ? { blocked: cases.filter((item) => item.status === "blocked").length } : {},
32551
+ ...cases.some((item) => item.status === "inconclusive") ? { inconclusive: cases.filter((item) => item.status === "inconclusive").length } : {},
32302
32552
  warnings: 0,
32303
32553
  steps: 0,
32304
32554
  stepsPassed: 0,
@@ -32323,7 +32573,8 @@ function buildRunSummary(runId, startedAt, cases, options = {}) {
32323
32573
  startedAt: startedAt.toISOString(),
32324
32574
  finishedAt: finishedAt.toISOString(),
32325
32575
  durationMs: finishedAt.getTime() - startedAt.getTime(),
32326
- status: totals.failed > 0 ? "failed" : totals.passed > 0 || totals.cases === 0 ? "passed" : "skipped",
32576
+ // run 状态按三桶优先级推导:产品失败 > 证据不足 > 环境阻塞 > 通过 > 跳过
32577
+ status: totals.failed > 0 ? "failed" : (totals.inconclusive ?? 0) > 0 ? "inconclusive" : (totals.blocked ?? 0) > 0 ? "blocked" : totals.passed > 0 || totals.cases === 0 ? "passed" : "skipped",
32327
32578
  ...options.cancelled ? { cancelled: true } : {},
32328
32579
  cases,
32329
32580
  totals
@@ -32375,6 +32626,7 @@ var TestRunner = class {
32375
32626
  rowIndex: item.rowIndex ?? 0,
32376
32627
  initialVariables: item.initialVariables ?? {},
32377
32628
  missingDependencies: item.missingDependencies ?? [],
32629
+ blockedError: item.blockedError,
32378
32630
  artifacts,
32379
32631
  bus,
32380
32632
  resolver: this.resolver,
@@ -32431,6 +32683,11 @@ import { existsSync as existsSync3 } from "node:fs";
32431
32683
  import { mkdtemp, readFile as readFile6, rm } from "node:fs/promises";
32432
32684
  import { tmpdir } from "node:os";
32433
32685
  import { join as join16 } from "node:path";
32686
+ import { errors } from "playwright";
32687
+ var TEXT_WAIT_MS = 1e4;
32688
+ function delay2(ms) {
32689
+ return new Promise((resolve7) => setTimeout(resolve7, ms));
32690
+ }
32434
32691
  var PlaywrightAdapter = class {
32435
32692
  constructor(bundle, options = {}) {
32436
32693
  this.bundle = bundle;
@@ -32487,36 +32744,63 @@ var PlaywrightAdapter = class {
32487
32744
  const options = this.options;
32488
32745
  const page = await this.ensurePage();
32489
32746
  const target = options.baseUrl && !/^https?:\/\//i.test(url2) ? new URL(url2, options.baseUrl).toString() : url2;
32490
- await page.goto(target);
32747
+ try {
32748
+ await page.goto(target);
32749
+ } catch (err) {
32750
+ if (err instanceof errors.TimeoutError) {
32751
+ throw new TestPilotError("STEP_TIMEOUT", err.message, { cause: err });
32752
+ }
32753
+ if (err instanceof Error && err.message.includes("net::ERR")) {
32754
+ throw new TestPilotError("ENVIRONMENT_UNAVAILABLE", err.message, { cause: err });
32755
+ }
32756
+ throw err;
32757
+ }
32491
32758
  }
32492
32759
  async click(locator) {
32493
- await (await this.toLocator(locator)).click();
32760
+ const target = await this.toLocator(locator);
32761
+ await this.coded(() => target.click());
32494
32762
  }
32495
32763
  async input(locator, value) {
32496
- await (await this.toLocator(locator)).fill(value);
32764
+ const target = await this.toLocator(locator);
32765
+ await this.coded(() => target.fill(value));
32497
32766
  }
32498
32767
  async select(locator, value) {
32499
- await (await this.toLocator(locator)).selectOption(value);
32768
+ const target = await this.toLocator(locator);
32769
+ await this.coded(() => target.selectOption(value));
32500
32770
  }
32501
32771
  async wait(locator, timeoutMs) {
32502
32772
  const page = await this.ensurePage();
32503
32773
  if (locator) {
32504
- await this.toLocatorOn(page, locator).waitFor({ state: "visible", timeout: timeoutMs });
32774
+ await this.coded(() => this.toLocatorOn(page, locator).waitFor({ state: "visible", timeout: timeoutMs }));
32505
32775
  return;
32506
32776
  }
32507
32777
  await page.waitForTimeout(timeoutMs ?? 1e3);
32508
32778
  }
32509
32779
  async assert(locator, expected) {
32510
32780
  const target = await this.toLocator(locator);
32511
- await target.waitFor({ state: "visible" });
32512
- const actual = (await target.innerText()).trim();
32513
32781
  const wanted = expected.trim();
32514
- if (!actual.includes(wanted)) {
32515
- throw new Error(`\u65AD\u8A00\u5931\u8D25:\u671F\u671B\u5305\u542B "${wanted}",\u5B9E\u9645\u4E3A "${actual.slice(0, 200)}"`);
32782
+ const startedAt = Date.now();
32783
+ for (; ; ) {
32784
+ await this.coded(() => target.waitFor({ state: "visible" }));
32785
+ const actual = (await this.coded(() => target.innerText())).trim();
32786
+ if (actual.includes(wanted)) return;
32787
+ if (Date.now() - startedAt > TEXT_WAIT_MS) {
32788
+ throw new TestPilotError(
32789
+ "ASSERTION_FAILED",
32790
+ `\u65AD\u8A00\u5931\u8D25(\u7B49\u5F85 ${TEXT_WAIT_MS}ms):\u671F\u671B\u5305\u542B "${wanted}",\u5B9E\u9645\u4E3A "${actual.slice(0, 200)}"`
32791
+ );
32792
+ }
32793
+ await delay2(300);
32516
32794
  }
32517
32795
  }
32518
32796
  async extract(locator) {
32519
- return (await this.toLocator(locator)).innerText().then((text) => text.trim());
32797
+ const target = await this.toLocator(locator);
32798
+ const startedAt = Date.now();
32799
+ for (; ; ) {
32800
+ const text = (await this.coded(() => target.innerText())).trim();
32801
+ if (text.length > 0 || Date.now() - startedAt > TEXT_WAIT_MS) return text;
32802
+ await delay2(300);
32803
+ }
32520
32804
  }
32521
32805
  async screenshot() {
32522
32806
  return (await this.ensurePage()).screenshot();
@@ -32534,6 +32818,17 @@ var PlaywrightAdapter = class {
32534
32818
  async toLocator(locator) {
32535
32819
  return this.toLocatorOn(await this.ensurePage(), locator);
32536
32820
  }
32821
+ /** 定位类操作的统一兜底:等待超时按「元素没等到」归类(可能是回归也可能是 selector 腐烂,不允许静默) */
32822
+ async coded(fn) {
32823
+ try {
32824
+ return await fn();
32825
+ } catch (err) {
32826
+ if (err instanceof errors.TimeoutError) {
32827
+ throw new TestPilotError("ELEMENT_NOT_FOUND", err.message, { cause: err });
32828
+ }
32829
+ throw err;
32830
+ }
32831
+ }
32537
32832
  toLocatorOn(page, locator) {
32538
32833
  if (locator.text !== void 0) {
32539
32834
  return page.getByText(locator.text, { exact: true });
@@ -32570,6 +32865,8 @@ import net from "node:net";
32570
32865
  var AUTO_PORT_FALLBACK_WAIT_MS = 9e4;
32571
32866
  var NAVIGATE_RETRY_WINDOW_MS = 45e3;
32572
32867
  var ELEMENT_WAIT_MS = 1e4;
32868
+ var TEXT_WAIT_MS2 = 1e4;
32869
+ var PAGE_READY_WAIT_MS = 3e4;
32573
32870
  var CONNECTION_LOST = /connection closed|not connected/i;
32574
32871
  var automatorLauncher = {
32575
32872
  launch: async (options) => {
@@ -32583,7 +32880,7 @@ var automatorLauncher = {
32583
32880
  }
32584
32881
  }
32585
32882
  };
32586
- function delay2(ms) {
32883
+ function delay3(ms) {
32587
32884
  return new Promise((resolve7) => setTimeout(resolve7, ms));
32588
32885
  }
32589
32886
  async function connectWithRetry(wsEndpoint, timeoutMs) {
@@ -32594,7 +32891,7 @@ async function connectWithRetry(wsEndpoint, timeoutMs) {
32594
32891
  return await automator.connect({ wsEndpoint });
32595
32892
  } catch (err) {
32596
32893
  lastError = err;
32597
- await delay2(1e3);
32894
+ await delay3(1e3);
32598
32895
  }
32599
32896
  }
32600
32897
  throw lastError instanceof Error ? lastError : new Error(String(lastError));
@@ -32611,12 +32908,12 @@ function startCliAuto(cliPath, projectPath, port) {
32611
32908
  child.stderr?.on("data", (chunk) => {
32612
32909
  output2 += chunk.toString("utf8");
32613
32910
  });
32614
- child.once("error", (err) => rejectPromise(new Error(`\u62C9\u8D77\u5F00\u53D1\u8005\u5DE5\u5177 cli \u5931\u8D25:${err.message}`)));
32911
+ child.once("error", (err) => rejectPromise(new TestPilotError("ENVIRONMENT_UNAVAILABLE", `\u62C9\u8D77\u5F00\u53D1\u8005\u5DE5\u5177 cli \u5931\u8D25:${err.message}`)));
32615
32912
  child.once("exit", (code) => {
32616
32913
  if (code === 0) {
32617
32914
  resolvePromise();
32618
32915
  } else {
32619
- rejectPromise(new Error(`cli auto \u9000\u51FA\u7801 ${code}:${output2.slice(-400)}`));
32916
+ rejectPromise(new TestPilotError("ENVIRONMENT_UNAVAILABLE", `cli auto \u9000\u51FA\u7801 ${code}:${output2.slice(-400)}`));
32620
32917
  }
32621
32918
  });
32622
32919
  const startedAt = Date.now();
@@ -32629,7 +32926,7 @@ function startCliAuto(cliPath, projectPath, port) {
32629
32926
  socket.once("error", () => {
32630
32927
  socket.destroy();
32631
32928
  if (Date.now() - startedAt > AUTO_PORT_FALLBACK_WAIT_MS) {
32632
- 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`));
32929
+ 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`));
32633
32930
  } else {
32634
32931
  setTimeout(poll, 500);
32635
32932
  }
@@ -32663,14 +32960,31 @@ var MiniAppAdapter = class {
32663
32960
  await this.runWithReconnect(async () => {
32664
32961
  await (await this.getProgram()).reLaunch(url2);
32665
32962
  });
32963
+ await this.waitForPageReady();
32666
32964
  return;
32667
32965
  } catch (err) {
32668
32966
  lastError = err;
32669
- await delay2(3e3);
32967
+ await delay3(3e3);
32670
32968
  }
32671
32969
  }
32672
32970
  throw lastError instanceof Error ? lastError : new Error(String(lastError));
32673
32971
  }
32972
+ /** 等待页面就绪:模拟器编译期间 currentPage / data 会失败或挂起,轮询至可响应 */
32973
+ async waitForPageReady(timeoutMs = PAGE_READY_WAIT_MS) {
32974
+ const startedAt = Date.now();
32975
+ for (; ; ) {
32976
+ const page = await (await this.getProgram()).currentPage();
32977
+ const data = await page.data().catch(() => void 0);
32978
+ if (data !== void 0 && data !== null) return;
32979
+ if (Date.now() - startedAt > timeoutMs) {
32980
+ throw new TestPilotError(
32981
+ "PAGE_READY_TIMEOUT",
32982
+ `\u7B49\u5F85\u9875\u9762\u5C31\u7EEA\u8D85\u65F6(${timeoutMs}ms):\u6A21\u62DF\u5668\u53EF\u80FD\u4ECD\u5728\u7F16\u8BD1,\u6216\u9879\u76EE\u672A\u88AB\u4FE1\u4EFB`
32983
+ );
32984
+ }
32985
+ await delay3(300);
32986
+ }
32987
+ }
32674
32988
  async click(locator) {
32675
32989
  await this.runWithReconnect(async () => (await this.element(locator)).tap());
32676
32990
  }
@@ -32678,7 +32992,7 @@ var MiniAppAdapter = class {
32678
32992
  await this.runWithReconnect(async () => (await this.element(locator)).input(value));
32679
32993
  }
32680
32994
  async select() {
32681
- throw new Error("\u5C0F\u7A0B\u5E8F\u7AEF\u6682\u4E0D\u652F\u6301 select(picker \u4EA4\u4E92),\u8BF7\u6539\u7528 click \u5B8C\u6210\u9009\u62E9");
32995
+ 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");
32682
32996
  }
32683
32997
  async wait(locator, timeoutMs) {
32684
32998
  await this.runWithReconnect(async () => {
@@ -32693,17 +33007,100 @@ var MiniAppAdapter = class {
32693
33007
  async assert(locator, expected) {
32694
33008
  await this.runWithReconnect(async () => {
32695
33009
  const element = await this.element(locator);
32696
- const actual = (await element.text()).trim();
32697
33010
  const wanted = expected.trim();
32698
- if (!actual.includes(wanted)) {
32699
- throw new Error(`\u65AD\u8A00\u5931\u8D25:\u671F\u671B\u5305\u542B "${wanted}",\u5B9E\u9645\u4E3A "${actual.slice(0, 200)}"`);
33011
+ const startedAt = Date.now();
33012
+ for (; ; ) {
33013
+ const actual = (await element.text()).trim();
33014
+ if (actual.includes(wanted)) return;
33015
+ if (Date.now() - startedAt > TEXT_WAIT_MS2) {
33016
+ throw new TestPilotError(
33017
+ "ASSERTION_FAILED",
33018
+ `\u65AD\u8A00\u5931\u8D25(\u7B49\u5F85 ${TEXT_WAIT_MS2}ms):\u671F\u671B\u5305\u542B "${wanted}",\u5B9E\u9645\u4E3A "${actual.slice(0, 200)}"`
33019
+ );
33020
+ }
33021
+ await delay3(300);
32700
33022
  }
32701
33023
  });
32702
33024
  }
32703
33025
  async extract(locator) {
32704
33026
  return this.runWithReconnect(async () => {
32705
33027
  const element = await this.element(locator);
32706
- return (await element.text()).trim();
33028
+ const startedAt = Date.now();
33029
+ for (; ; ) {
33030
+ const text = (await element.text()).trim();
33031
+ if (text.length > 0 || Date.now() - startedAt > TEXT_WAIT_MS2) return text;
33032
+ await delay3(300);
33033
+ }
33034
+ });
33035
+ }
33036
+ /** 调用 Page / 组件方法;不传 locator 时为页面级调用(Page 实例方法) */
33037
+ async callMethod(locator, methodName, args = []) {
33038
+ return this.runWithReconnect(async () => {
33039
+ if (locator) {
33040
+ const element = await this.element(locator);
33041
+ return element.callMethod(methodName, ...args);
33042
+ }
33043
+ const page = await (await this.getProgram()).currentPage();
33044
+ return page.callMethod(methodName, ...args);
33045
+ });
33046
+ }
33047
+ // ---- miniapp 深度测试(V0.2)----
33048
+ wxMock;
33049
+ /** mock 任意 wx.* API:拦截调用并返回 mock 值;capture 记录真实调用参数(最近调用在前) */
33050
+ async mockWxApi(options) {
33051
+ if (this.wxMock?.api === options.api) await this.restoreWxMock();
33052
+ const program2 = await this.getProgram();
33053
+ const captured = [];
33054
+ this.wxMock = { api: options.api, captured };
33055
+ const callbackName = options.capture ? `__tpWxCapture_${options.capture}` : void 0;
33056
+ if (callbackName) {
33057
+ await program2.exposeFunction(callbackName, (payload) => {
33058
+ if (payload !== null && typeof payload === "object") {
33059
+ captured.push(payload);
33060
+ }
33061
+ });
33062
+ }
33063
+ await program2.mockWxMethod(
33064
+ options.api,
33065
+ function(invokeOptions, cbName) {
33066
+ const callbacks = globalThis;
33067
+ callbacks[cbName]?.({ options: invokeOptions });
33068
+ return options.result ?? this.origin(invokeOptions);
33069
+ },
33070
+ callbackName
33071
+ );
33072
+ }
33073
+ /** 读取 mock 期间捕获的调用参数(最近调用在前) */
33074
+ readCapturedWxApiCalls(api) {
33075
+ return this.wxMock?.api === api ? [...this.wxMock.captured].reverse() : [];
33076
+ }
33077
+ /** 恢复 mock 的 wx API(用例结束由引擎调用) */
33078
+ async restoreWxMock() {
33079
+ const mock = this.wxMock;
33080
+ if (!mock) return;
33081
+ this.wxMock = void 0;
33082
+ if (!this.program) return;
33083
+ await this.program.restoreWxMethod(mock.api).catch(() => void 0);
33084
+ }
33085
+ /** 触发组件事件(trigger;detail 作为 e.detail 注入,空 detail 即"用户拒绝/取消"类分支) */
33086
+ async trigger(locator, eventType, detail) {
33087
+ await this.runWithReconnect(async () => {
33088
+ const element = await this.element(locator);
33089
+ await element.trigger(eventType, detail);
33090
+ });
33091
+ }
33092
+ /** 读取页面 data(点号路径,缺省为整个 data 对象) */
33093
+ async getData(dataPath) {
33094
+ return this.runWithReconnect(async () => {
33095
+ const page = await (await this.getProgram()).currentPage();
33096
+ return page.data(dataPath);
33097
+ });
33098
+ }
33099
+ /** 写入页面 data 补丁(setData 驱动 onShow/onPullDownRefresh 等真实链路) */
33100
+ async setData(patch) {
33101
+ await this.runWithReconnect(async () => {
33102
+ const page = await (await this.getProgram()).currentPage();
33103
+ await page.setData(patch);
32707
33104
  });
32708
33105
  }
32709
33106
  async screenshot() {
@@ -32794,9 +33191,9 @@ var MiniAppAdapter = class {
32794
33191
  const element = await page.$(selector);
32795
33192
  if (element) return element;
32796
33193
  if (Date.now() - startedAt > ELEMENT_WAIT_MS) {
32797
- throw new Error(`\u7B49\u5F85\u5143\u7D20\u8D85\u65F6(${ELEMENT_WAIT_MS}ms):${selector}`);
33194
+ throw new TestPilotError("ELEMENT_NOT_FOUND", `\u7B49\u5F85\u5143\u7D20\u8D85\u65F6(${ELEMENT_WAIT_MS}ms):${selector}`);
32798
33195
  }
32799
- await delay2(500);
33196
+ await delay3(500);
32800
33197
  }
32801
33198
  }
32802
33199
  };
@@ -32872,7 +33269,7 @@ var ApiAdapter = class {
32872
33269
  signal: AbortSignal.timeout(this.options.timeoutMs ?? 15e3)
32873
33270
  });
32874
33271
  } catch (err) {
32875
- throw new Error(`API \u8BF7\u6C42\u5931\u8D25 ${request.method.toUpperCase()} ${url2}:${err instanceof Error ? err.message : String(err)}`);
33272
+ throw new TestPilotError("ENVIRONMENT_UNAVAILABLE", `API \u8BF7\u6C42\u5931\u8D25 ${request.method.toUpperCase()} ${url2}:${err instanceof Error ? err.message : String(err)}`, { cause: err });
32876
33273
  }
32877
33274
  const bodyText = await res.text();
32878
33275
  let json2;
@@ -32894,7 +33291,7 @@ var ApiAdapter = class {
32894
33291
  if (!this.last) throw new Error("\u5C1A\u672A\u53D1\u8D77\u4EFB\u4F55 API \u8BF7\u6C42,\u65E0\u6CD5\u65AD\u8A00\u54CD\u5E94");
32895
33292
  if (!this.last.bodyText.includes(expected)) {
32896
33293
  const snippet2 = this.last.bodyText.slice(0, 200);
32897
- throw new Error(`\u54CD\u5E94\u4F53\u4E0D\u5305\u542B "${expected}":HTTP ${this.last.status} ${snippet2}`);
33294
+ throw new TestPilotError("ASSERTION_FAILED", `\u54CD\u5E94\u4F53\u4E0D\u5305\u542B "${expected}":HTTP ${this.last.status} ${snippet2}`);
32898
33295
  }
32899
33296
  }
32900
33297
  async extractResponse(path) {
@@ -33023,7 +33420,23 @@ async function runCases(options = {}) {
33023
33420
  variables: config2.variables,
33024
33421
  accounts: options.accounts
33025
33422
  }
33026
- )
33423
+ ).catch((err) => {
33424
+ if (err instanceof TestPilotError && err.code === "FIXTURE_MISSING") {
33425
+ return [
33426
+ {
33427
+ data: item.case,
33428
+ file: item.file,
33429
+ templateId: item.case.id,
33430
+ rowId: "default",
33431
+ rowIndex: 0,
33432
+ initialVariables: {},
33433
+ missingDependencies: [],
33434
+ blockedError: { code: err.code, message: err.message }
33435
+ }
33436
+ ];
33437
+ }
33438
+ throw err;
33439
+ })
33027
33440
  )
33028
33441
  )).flat();
33029
33442
  const summary = await runner.run(
@@ -33260,7 +33673,7 @@ async function collectEnvVarRefs(root) {
33260
33673
  }
33261
33674
 
33262
33675
  // src/commands/init/project-detector.ts
33263
- import { existsSync as existsSync6, readFileSync as readFileSync2 } from "node:fs";
33676
+ import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync2 } from "node:fs";
33264
33677
  import { basename, join as join22 } from "node:path";
33265
33678
 
33266
33679
  // src/lib/git.ts
@@ -33316,10 +33729,31 @@ function detectProjectType(root) {
33316
33729
  if (isWeb) return "web";
33317
33730
  return "unknown";
33318
33731
  }
33732
+ function detectLanguageFromSources(root) {
33733
+ const SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "miniprogram_npm", "dist", ".git"]);
33734
+ const found = { ts: false, js: false };
33735
+ const walk = (dir, depth) => {
33736
+ if (depth > 4 || found.ts && found.js) return;
33737
+ for (const entry of readdirSync2(dir, { withFileTypes: true })) {
33738
+ if (entry.name.startsWith(".")) continue;
33739
+ const full = join22(dir, entry.name);
33740
+ if (entry.isDirectory()) {
33741
+ if (!SKIP_DIRS.has(entry.name)) walk(full, depth + 1);
33742
+ continue;
33743
+ }
33744
+ if (/\.(ts|tsx)$/.test(entry.name)) found.ts = true;
33745
+ else if (/\.(js|cjs|mjs|wxml)$/.test(entry.name)) found.js = true;
33746
+ }
33747
+ };
33748
+ walk(root, 0);
33749
+ if (found.ts) return "TypeScript";
33750
+ if (found.js) return "JavaScript";
33751
+ return "Unknown";
33752
+ }
33319
33753
  async function detectProject(root) {
33320
33754
  const pkg = readPackageJson(root);
33321
33755
  const type = detectProjectType(root);
33322
- const language = existsSync6(join22(root, "tsconfig.json")) ? "TypeScript" : pkg || existsSync6(join22(root, "src")) ? "JavaScript" : "Unknown";
33756
+ const language = existsSync6(join22(root, "tsconfig.json")) ? "TypeScript" : pkg || existsSync6(join22(root, "src")) ? "JavaScript" : detectLanguageFromSources(root);
33323
33757
  const packageManager = existsSync6(join22(root, "pnpm-lock.yaml")) ? "pnpm" : existsSync6(join22(root, "yarn.lock")) ? "yarn" : existsSync6(join22(root, "package-lock.json")) ? "npm" : "npx";
33324
33758
  const existingE2eDirs = E2E_DIR_CANDIDATES.filter((dir) => existsSync6(join22(root, dir)));
33325
33759
  return {
@@ -33799,7 +34233,7 @@ function randomId() {
33799
34233
  }
33800
34234
 
33801
34235
  // src/commands/init/test-structure.ts
33802
- import { existsSync as existsSync9, readdirSync as readdirSync2 } from "node:fs";
34236
+ import { existsSync as existsSync9, readdirSync as readdirSync3 } from "node:fs";
33803
34237
  import { mkdir as mkdir14, writeFile as writeFile12 } from "node:fs/promises";
33804
34238
  import { join as join26 } from "node:path";
33805
34239
  async function initTestStructure(root, _info) {
@@ -33819,7 +34253,7 @@ async function initTestStructure(root, _info) {
33819
34253
  await mkdir14(dir, { recursive: true });
33820
34254
  }
33821
34255
  for (const [name, dir] of Object.entries(dirs)) {
33822
- if (!existed[name] && readdirSync2(dir).length === 0) {
34256
+ if (!existed[name] && readdirSync3(dir).length === 0) {
33823
34257
  await writeFile12(join26(dir, ".gitkeep"), "", "utf8").catch(() => void 0);
33824
34258
  }
33825
34259
  }
@@ -33989,6 +34423,11 @@ function openInBrowser(file2) {
33989
34423
 
33990
34424
  // src/commands/run.ts
33991
34425
  import { join as join28 } from "node:path";
34426
+ function exitCodeForSummary(status) {
34427
+ if (status === "failed" || status === "inconclusive") return 1;
34428
+ if (status === "blocked") return 2;
34429
+ return 0;
34430
+ }
33992
34431
  function makeRunCommand() {
33993
34432
  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) => {
33994
34433
  const client = new TestPilotClient();
@@ -34010,11 +34449,11 @@ function makeRunCommand() {
34010
34449
  ].filter(Boolean).join(",");
34011
34450
  console.log("");
34012
34451
  console.log(
34013
- `Run ${summary.runId}${summary.cancelled ? "(\u5DF2\u53D6\u6D88)" : ""}: ${summary.status.toUpperCase()} \u2014 \u6A21\u677F ${summary.totals.templates ?? summary.totals.cases},\u6570\u636E\u884C \u2713${summary.totals.passed} \u2717${summary.totals.failed} \u21B7${summary.totals.skipped ?? 0} \u26A0${summary.totals.warnings ?? 0},\u6B65\u9AA4 \u2713${summary.totals.stepsPassed} \u2717${summary.totals.stepsFailed} \u21B7${summary.totals.stepsSkipped} \u26A0${summary.totals.stepsWarning ?? 0}${notes ? `(${notes})` : ""}`
34452
+ `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})` : ""}`
34014
34453
  );
34015
34454
  console.log(`\u4EA7\u7269\u76EE\u5F55:${join28(".testpilot", "artifacts", "runs", summary.runId)}`);
34016
34455
  console.log("\u67E5\u770B\u62A5\u544A:npx csspilot report");
34017
- if (summary.status === "failed") process.exitCode = 1;
34456
+ process.exitCode = exitCodeForSummary(summary.status);
34018
34457
  } catch (err) {
34019
34458
  console.error(err instanceof Error ? err.message : err);
34020
34459
  process.exitCode = 1;
@@ -34189,7 +34628,7 @@ function makeValidateCommand() {
34189
34628
  }
34190
34629
 
34191
34630
  // src/cli.ts
34192
- var CLI_VERSION = "0.6.0";
34631
+ var CLI_VERSION = "0.7.1";
34193
34632
  async function createCli() {
34194
34633
  const program2 = new Command();
34195
34634
  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", () => {