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