csspilot 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -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";
@@ -30716,6 +30738,9 @@ async function loadCaseDataFile(reference, options) {
30716
30738
  const [realDataDir, realTarget] = await Promise.all([
30717
30739
  realpath(dataDir).catch(() => dataDir),
30718
30740
  realpath(target).catch((error62) => {
30741
+ if (error62 instanceof Error && error62.code === "ENOENT") {
30742
+ throw new TestPilotError("FIXTURE_MISSING", `\u6570\u636E\u6587\u4EF6\u4E0D\u5B58\u5728:${reference}`);
30743
+ }
30719
30744
  throw new Error(
30720
30745
  `\u8BFB\u53D6\u6570\u636E\u6587\u4EF6\u5931\u8D25:${reference}:${error62 instanceof Error ? error62.message : String(error62)}`
30721
30746
  );
@@ -30943,6 +30968,9 @@ async function validateCaseDataSource(source, file2, root) {
30943
30968
  );
30944
30969
  return validation;
30945
30970
  } catch (error62) {
30971
+ if (error62 instanceof TestPilotError && error62.code === "FIXTURE_MISSING") {
30972
+ return validation;
30973
+ }
30946
30974
  return {
30947
30975
  ok: false,
30948
30976
  data: validation.data,
@@ -30991,6 +31019,24 @@ function classifyFailure(message) {
30991
31019
  }
30992
31020
  return "other";
30993
31021
  }
31022
+ var CODE_CATEGORIES = {
31023
+ ASSERTION_FAILED: "assert",
31024
+ REQUEST_COUNT_MISMATCH: "assert",
31025
+ STATUS_MISMATCH: "assert",
31026
+ STEP_TIMEOUT: "assert",
31027
+ ELEMENT_NOT_FOUND: "element",
31028
+ ADAPTER_NOT_FOUND: "adapter",
31029
+ UNSUPPORTED_ACTION: "adapter",
31030
+ CONFIG_MISSING: "adapter",
31031
+ ENVIRONMENT_UNAVAILABLE: "env",
31032
+ FIXTURE_MISSING: "env",
31033
+ ACCOUNT_UNAVAILABLE: "env",
31034
+ PREREQ_VARIABLE_MISSING: "other"
31035
+ };
31036
+ function classifyFromCode(code) {
31037
+ if (code === void 0) return void 0;
31038
+ return CODE_CATEGORIES[code];
31039
+ }
30994
31040
 
30995
31041
  // ../../packages/reporter/src/history.ts
30996
31042
  import { readFile as readFile5, readdir as readdir2 } from "node:fs/promises";
@@ -31048,7 +31094,7 @@ async function writeReports(runDir, summary, options = {}) {
31048
31094
  const cases = summary.cases.map((item) => {
31049
31095
  const categories = /* @__PURE__ */ new Set();
31050
31096
  const steps = item.steps.map((step) => {
31051
- const category = step.status === "failed" ? classifyFailure(step.error) : void 0;
31097
+ const category = classifyFromCode(step.errorCode) ?? (step.status === "failed" ? classifyFailure(step.error) : void 0);
31052
31098
  if (category) {
31053
31099
  categories.add(category);
31054
31100
  failures.push({
@@ -31056,7 +31102,8 @@ async function writeReports(runDir, summary, options = {}) {
31056
31102
  label: categoryLabel(category),
31057
31103
  caseId: item.caseId,
31058
31104
  index: step.index,
31059
- message: step.error ?? ""
31105
+ message: step.error ?? "",
31106
+ verdict: verdictFromErrorCode(step.errorCode ?? item.errorCode)
31060
31107
  });
31061
31108
  }
31062
31109
  return { ...step, category };
@@ -31079,6 +31126,20 @@ async function writeReports(runDir, summary, options = {}) {
31079
31126
  await writeFile4(htmlPath, renderHtml(data), "utf8");
31080
31127
  return { json: jsonPath, html: htmlPath };
31081
31128
  }
31129
+ var VERDICT_GROUPS = [
31130
+ { verdict: "product", title: "\u4EA7\u54C1\u5931\u8D25" },
31131
+ { verdict: "infra", title: "\u73AF\u5883\u4E0E\u524D\u7F6E(\u975E\u4EA7\u54C1\u56DE\u5F52)" },
31132
+ { verdict: "unverified", title: "\u672A\u9A8C\u8BC1" }
31133
+ ];
31134
+ function verdictGroupsHtml(data) {
31135
+ return VERDICT_GROUPS.map((group) => {
31136
+ const items = data.failures.filter((failure2) => failure2.verdict === group.verdict);
31137
+ const list = items.length > 0 ? items.map(
31138
+ (failure2) => `<li><code>${escapeHtml(failure2.caseId)}</code> #${failure2.index} ${escapeHtml(failure2.message.slice(0, 160))}</li>`
31139
+ ).join("\n") : '<li class="muted">\u65E0</li>';
31140
+ return `<div class="verdict-group"><h3>${escapeHtml(group.title)}(${items.length})</h3><ul>${list}</ul></div>`;
31141
+ }).join("\n");
31142
+ }
31082
31143
  function categoryChips(data) {
31083
31144
  const counts = /* @__PURE__ */ new Map();
31084
31145
  for (const failure2 of data.failures) {
@@ -31104,19 +31165,20 @@ function renderHtml(data) {
31104
31165
  const trendSvg = renderTrend(data);
31105
31166
  const casesHtml = groupedCasesHtml(summary.cases);
31106
31167
  const chips = categoryChips(data);
31168
+ const verdictHtml = verdictGroupsHtml(data);
31107
31169
  return `<!doctype html>
31108
31170
  <html lang="zh-CN">
31109
31171
  <head>
31110
31172
  <meta charset="utf-8" />
31111
31173
  <title>TestPilot Report ${escapeHtml(summary.runId)}</title>
31112
31174
  <style>
31113
- :root { --ok:#1a7f37; --fail:#cf222e; --warn:#9a6700; --skip:#6e7781; --line:#d0d7de; --muted:#59636e; }
31175
+ :root { --ok:#1a7f37; --fail:#cf222e; --warn:#9a6700; --skip:#6e7781; --blocked:#8250df; --inconclusive:#9a6700; --line:#d0d7de; --muted:#59636e; }
31114
31176
  * { box-sizing: border-box; }
31115
31177
  body { font-family: system-ui, "Segoe UI", sans-serif; margin: 0; color: #1f2328; background: #f6f8fa; }
31116
31178
  .wrap { max-width: 1080px; margin: 0 auto; padding: 20px 16px 48px; }
31117
31179
  h1 { font-size: 20px; margin: 8px 0; } h2 { font-size: 15px; margin: 24px 0 8px; }
31118
31180
  .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); }
31181
+ .passed { background: var(--ok); } .failed { background: var(--fail); } .warning { background: var(--warn); } .skipped { background: var(--skip); } .blocked { background: var(--blocked); } .inconclusive { background: var(--inconclusive); }
31120
31182
  code { background: #eef1f4; padding: 1px 5px; border-radius: 4px; font-size: 12px; }
31121
31183
  .cards { display: flex; gap: 12px; flex-wrap: wrap; margin: 14px 0; }
31122
31184
  .card { background: #fff; border: 1px solid var(--line); border-radius: 8px; padding: 10px 16px; min-width: 130px; }
@@ -31143,6 +31205,11 @@ function renderHtml(data) {
31143
31205
  .muted { color: var(--muted); }
31144
31206
  .hide { display: none !important; }
31145
31207
  .meta { color: var(--muted); font-size: 12px; margin: 6px 0; }
31208
+ .verdicts { display: flex; gap: 14px; flex-wrap: wrap; }
31209
+ .verdict-group { background: #fff; border: 1px solid var(--line); border-radius: 8px; padding: 10px 14px; flex: 1; min-width: 220px; }
31210
+ .verdict-group h3 { font-size: 13px; margin: 0 0 6px; }
31211
+ .verdict-group ul { margin: 0; padding-left: 18px; font-size: 12px; }
31212
+ .verdict-group li { margin: 3px 0; }
31146
31213
  </style>
31147
31214
  </head>
31148
31215
  <body>
@@ -31163,6 +31230,9 @@ function renderHtml(data) {
31163
31230
  <h2>\u5386\u53F2\u8D8B\u52BF(\u6700\u8FD1 ${data.history.length + 1} \u6B21\u8FD0\u884C)</h2>
31164
31231
  <div class="trend">${trendSvg}</div>
31165
31232
 
31233
+ <h2>\u5931\u8D25\u4E09\u6876</h2>
31234
+ <div class="verdicts">${verdictHtml}</div>
31235
+
31166
31236
  <h2>\u5931\u8D25\u5206\u7C7B</h2>
31167
31237
  <div class="toolbar" id="cat-chips">${chips}</div>
31168
31238
 
@@ -31371,7 +31441,7 @@ function buildSummaryData(runs) {
31371
31441
  for (const item of summary.cases) {
31372
31442
  const platforms = [...new Set(item.steps.map((step) => step.target))];
31373
31443
  const failedStep = item.steps.find((step) => step.status === "failed");
31374
- const category = item.status === "failed" ? classifyFailure(failedStep?.error) : void 0;
31444
+ const category = classifyFromCode(item.errorCode ?? failedStep?.errorCode) ?? (item.status === "failed" ? classifyFailure(failedStep?.error) : void 0);
31375
31445
  executions.push({
31376
31446
  runId,
31377
31447
  caseId: item.caseId,
@@ -31434,6 +31504,10 @@ function buildSummaryData(runs) {
31434
31504
  })).filter((item) => item.count > 0);
31435
31505
  const durations = runs.map((item) => item.summary.durationMs).filter((value) => typeof value === "number");
31436
31506
  return {
31507
+ buckets: {
31508
+ blocked: executions.filter((exec2) => exec2.status === "blocked").length,
31509
+ inconclusive: executions.filter((exec2) => exec2.status === "inconclusive").length
31510
+ },
31437
31511
  window: {
31438
31512
  runs: runs.length,
31439
31513
  from: runPoints[0]?.startedAt,
@@ -31518,7 +31592,7 @@ function renderSummaryHtml(data) {
31518
31592
  th.num, td.num { text-align: right; }
31519
31593
  code { background: #eef1f4; padding: 1px 5px; border-radius: 4px; font-size: 12px; cursor: pointer; }
31520
31594
  .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; }
31595
+ .passed { background: #1a7f37; } .failed { background: #cf222e; } .unknown { background: #6e7781; } .blocked { background: #8250df; } .inconclusive { background: #9a6700; }
31522
31596
  .toolbar { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin: 10px 0; }
31523
31597
  .chip { border: 1px solid var(--line); background: #fff; border-radius: 14px; padding: 3px 12px; font-size: 12px; cursor: pointer; }
31524
31598
  .chip.active { background: #0969da; color: #fff; border-color: #0969da; }
@@ -31544,6 +31618,8 @@ function renderSummaryHtml(data) {
31544
31618
  <div class="card"><b>${rateText}</b><span>\u6574\u4F53\u901A\u8FC7\u7387</span></div>
31545
31619
  <div class="card"><b>${(kpis.avgDurationMs / 1e3).toFixed(1)}s</b><span>\u5E73\u5747\u8FD0\u884C\u8017\u65F6</span></div>
31546
31620
  <div class="card"><b>${kpis.flakyCount}</b><span>Flaky \u7528\u4F8B</span></div>
31621
+ ${data.buckets.blocked > 0 ? `<div class="card"><b>${data.buckets.blocked} \u2298</b><span>\u73AF\u5883/\u524D\u7F6E\u963B\u585E(\u975E\u4EA7\u54C1)</span></div>` : ""}
31622
+ ${data.buckets.inconclusive > 0 ? `<div class="card"><b>${data.buckets.inconclusive} ?</b><span>\u672A\u9A8C\u8BC1(\u8BC1\u636E\u4E0D\u8DB3)</span></div>` : ""}
31547
31623
  </div>
31548
31624
 
31549
31625
  <h2>\u8D8B\u52BF(\u6BCF\u6B21\u8FD0\u884C\u7ED3\u679C\u4E0E\u901A\u8FC7\u7387,\u60AC\u6D6E\u770B\u8BE6\u60C5)</h2>
@@ -31752,14 +31828,23 @@ var ExecutionContext = class {
31752
31828
  }
31753
31829
  }
31754
31830
  /**
31755
- * 将字符串中的 ${name} 替换为已提取的变量值;未定义的引用保持原样(校验阶段已拦截)。
31756
- * 名字允许点号,用于引擎注入的 account.username 一类凭据变量(用户 extract 变量名不含点号)。
31831
+ * 将字符串中的 ${name} 替换为已提取的变量值;未定义的引用抛 TestPilotError——
31832
+ * 校验阶段已静态拦截"从未声明"的引用,运行时未命中即数据未兑现
31833
+ * (前置 extract 未执行或 fixture/dataset/凭据缺失),按三态归 unverified/account 桶。
31834
+ * 凭据桶仅限 `account` 整值或 `account.*` 字段;用户变量撞 account 前缀(如 accountId)
31835
+ * 不误归凭据桶。名字允许点号,用于引擎注入的 account.username 一类凭据变量
31836
+ * (用户 extract 变量名不含点号)。
31757
31837
  */
31758
31838
  resolve(template) {
31759
- return template.replace(
31760
- /\$\{([A-Za-z_][A-Za-z0-9_.]*)\}/g,
31761
- (match, name) => this.variables.get(name) ?? match
31762
- );
31839
+ return template.replace(/\$\{([A-Za-z_][A-Za-z0-9_.]*)\}/g, (_match, name) => {
31840
+ const value = this.variables.get(name);
31841
+ if (value !== void 0) return value;
31842
+ const isAccount = name === "account" || name.startsWith("account.");
31843
+ throw new TestPilotError(
31844
+ isAccount ? "ACCOUNT_UNAVAILABLE" : "PREREQ_VARIABLE_MISSING",
31845
+ `\u53D8\u91CF "${name}" \u672A\u5B9A\u4E49(\u524D\u7F6E extract \u672A\u6267\u884C\u6216 fixture/dataset/\u51ED\u636E\u7F3A\u5931)`
31846
+ );
31847
+ });
31763
31848
  }
31764
31849
  /**
31765
31850
  * 注入 accountRef 凭据:值为 JSON 对象时按 `${account.<字段>}` 展开为字符串,
@@ -31918,7 +32003,7 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
31918
32003
  } : void 0;
31919
32004
  if (step.expectRequests) {
31920
32005
  if (!adapter.startRequestCapture || !adapter.stopRequestCapture) {
31921
- throw new Error(`\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301\u8BF7\u6C42\u6B21\u6570\u65AD\u8A00`);
32006
+ throw new TestPilotError("UNSUPPORTED_ACTION", `\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301\u8BF7\u6C42\u6B21\u6570\u65AD\u8A00`);
31922
32007
  }
31923
32008
  await adapter.startRequestCapture();
31924
32009
  captureStarted = true;
@@ -31944,7 +32029,7 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
31944
32029
  break;
31945
32030
  case "assert":
31946
32031
  if (step.target === "api") {
31947
- if (!adapter.assertResponse) throw new Error("\u5F53\u524D adapter \u4E0D\u652F\u6301 api \u65AD\u8A00");
32032
+ if (!adapter.assertResponse) throw new TestPilotError("UNSUPPORTED_ACTION", "\u5F53\u524D adapter \u4E0D\u652F\u6301 api \u65AD\u8A00");
31948
32033
  await adapter.assertResponse(context.resolve(step.expected ?? ""));
31949
32034
  } else {
31950
32035
  await adapter.assert(locator, context.resolve(step.expected ?? ""));
@@ -31964,7 +32049,7 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
31964
32049
  break;
31965
32050
  }
31966
32051
  case "request": {
31967
- if (!adapter.request) throw new Error("\u5F53\u524D adapter \u4E0D\u652F\u6301 api request");
32052
+ if (!adapter.request) throw new TestPilotError("UNSUPPORTED_ACTION", "\u5F53\u524D adapter \u4E0D\u652F\u6301 api request");
31968
32053
  const method = step.method ?? "GET";
31969
32054
  const url2 = context.resolve(step.url ?? "");
31970
32055
  const headers = resolveHeaders(step.headers, context);
@@ -31981,7 +32066,8 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
31981
32066
  if (step.expected !== void 0) {
31982
32067
  const expectedStatus = context.resolve(step.expected);
31983
32068
  if (String(response.status) !== expectedStatus) {
31984
- throw new Error(
32069
+ throw new TestPilotError(
32070
+ "STATUS_MISMATCH",
31985
32071
  `HTTP \u72B6\u6001\u7801 ${response.status} ${response.statusText},\u671F\u671B ${expectedStatus}:${snippet(response.bodyText, 200)}`
31986
32072
  );
31987
32073
  }
@@ -31990,7 +32076,7 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
31990
32076
  }
31991
32077
  default: {
31992
32078
  const exhaustive = step.action;
31993
- throw new Error(`\u4E0D\u652F\u6301\u7684 action:${String(exhaustive)}`);
32079
+ throw new TestPilotError("UNSUPPORTED_ACTION", `\u4E0D\u652F\u6301\u7684 action:${String(exhaustive)}`);
31994
32080
  }
31995
32081
  }
31996
32082
  if (step.expectRequests) {
@@ -32001,7 +32087,8 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
32001
32087
  result.requestAssertions = buildRequestAssertions(step, requests, context);
32002
32088
  const mismatches = result.requestAssertions.filter((item) => item.actualCount !== item.count);
32003
32089
  if (mismatches.length > 0) {
32004
- throw new Error(
32090
+ throw new TestPilotError(
32091
+ "REQUEST_COUNT_MISMATCH",
32005
32092
  mismatches.map(
32006
32093
  (item) => `\u8BF7\u6C42\u6B21\u6570\u4E0D\u5339\u914D:${item.method ?? "*"} ${item.urlContains},\u5B9E\u9645 ${item.actualCount},\u671F\u671B ${item.count}`
32007
32094
  ).join("; ")
@@ -32018,6 +32105,7 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
32018
32105
  }
32019
32106
  result.status = "failed";
32020
32107
  result.error = err instanceof Error ? err.message : String(err);
32108
+ result.errorCode = err instanceof TestPilotError ? err.code : void 0;
32021
32109
  try {
32022
32110
  const data = await adapter.screenshot();
32023
32111
  result.screenshot = await artifacts.saveStepScreenshot(`${options.screenshotName}-failed.png`, data);
@@ -32028,8 +32116,8 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
32028
32116
  return result;
32029
32117
  }
32030
32118
  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)");
32119
+ if (!adapter.extractResponse) throw new TestPilotError("UNSUPPORTED_ACTION", "\u5F53\u524D adapter \u4E0D\u652F\u6301 api \u63D0\u53D6");
32120
+ if (!path) throw new TestPilotError("UNSUPPORTED_ACTION", "api \u7AEF extract \u9700\u8981\u63D0\u4F9B value(\u70B9\u53F7 JSON path)");
32033
32121
  return adapter.extractResponse(path);
32034
32122
  }
32035
32123
  function resolveHeaders(headers, context) {
@@ -32103,6 +32191,7 @@ var ExecutionEngine = class {
32103
32191
  const warnings = [];
32104
32192
  const evidenceAdapters = /* @__PURE__ */ new Map();
32105
32193
  let caseError;
32194
+ let caseErrorCode;
32106
32195
  const executionId = safeArtifactName(`${data.id}-${rowId}`);
32107
32196
  await bus.emit({
32108
32197
  type: "case-started",
@@ -32180,7 +32269,8 @@ var ExecutionEngine = class {
32180
32269
  action: step.action,
32181
32270
  status: "failed",
32182
32271
  durationMs: 0,
32183
- error: err instanceof Error ? err.message : String(err)
32272
+ error: err instanceof Error ? err.message : String(err),
32273
+ errorCode: err instanceof TestPilotError ? err.code : void 0
32184
32274
  };
32185
32275
  }
32186
32276
  result.phase = phase;
@@ -32209,6 +32299,7 @@ var ExecutionEngine = class {
32209
32299
  if (result.status === "failed") {
32210
32300
  passed = false;
32211
32301
  caseError = result.error;
32302
+ caseErrorCode = result.errorCode;
32212
32303
  for (const [skipIndex, skippedStep] of phaseSteps.entries()) {
32213
32304
  if (skipIndex <= index) continue;
32214
32305
  steps.push({
@@ -32225,24 +32316,42 @@ var ExecutionEngine = class {
32225
32316
  }
32226
32317
  return passed;
32227
32318
  };
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()) {
32319
+ if (options.blockedError) {
32320
+ const first = data.setup?.[0] ?? data.steps[0];
32321
+ if (first) {
32233
32322
  steps.push({
32234
- index,
32235
- phase: "steps",
32236
- target: step.target,
32237
- action: step.action,
32238
- status: "skipped",
32239
- durationMs: 0
32323
+ index: 0,
32324
+ phase: "setup",
32325
+ target: first.target,
32326
+ action: first.action,
32327
+ status: "failed",
32328
+ durationMs: 0,
32329
+ error: options.blockedError.message,
32330
+ errorCode: options.blockedError.code
32240
32331
  });
32241
32332
  }
32333
+ caseError = options.blockedError.message;
32334
+ caseErrorCode = options.blockedError.code;
32335
+ } else {
32336
+ const setupPassed = await runPhase("setup", data.setup ?? []);
32337
+ if (setupPassed) {
32338
+ await runPhase("steps", data.steps);
32339
+ } else {
32340
+ for (const [index, step] of data.steps.entries()) {
32341
+ steps.push({
32342
+ index,
32343
+ phase: "steps",
32344
+ target: step.target,
32345
+ action: step.action,
32346
+ status: "skipped",
32347
+ durationMs: 0
32348
+ });
32349
+ }
32350
+ }
32351
+ await runPhase("teardown", data.teardown ?? []);
32242
32352
  }
32243
- await runPhase("teardown", data.teardown ?? []);
32244
32353
  const finishedAt = /* @__PURE__ */ new Date();
32245
- const status = caseError ? "failed" : "passed";
32354
+ const status = caseError ? caseStatusFromErrorCode(caseErrorCode) : "passed";
32246
32355
  const caseResult = {
32247
32356
  caseId: data.id,
32248
32357
  caseName: data.name,
@@ -32256,6 +32365,7 @@ var ExecutionEngine = class {
32256
32365
  durationMs: finishedAt.getTime() - startedAt.getTime()
32257
32366
  };
32258
32367
  if (caseError) caseResult.error = caseError;
32368
+ if (caseErrorCode) caseResult.errorCode = caseErrorCode;
32259
32369
  if (warnings.length > 0) caseResult.warnings = warnings;
32260
32370
  for (const [target, adapter] of evidenceAdapters) {
32261
32371
  if (!adapter.stopEvidence) continue;
@@ -32299,6 +32409,8 @@ function buildRunSummary(runId, startedAt, cases, options = {}) {
32299
32409
  passed: cases.filter((item) => item.status === "passed").length,
32300
32410
  failed: cases.filter((item) => item.status === "failed").length,
32301
32411
  skipped: cases.filter((item) => item.status === "skipped").length,
32412
+ ...cases.some((item) => item.status === "blocked") ? { blocked: cases.filter((item) => item.status === "blocked").length } : {},
32413
+ ...cases.some((item) => item.status === "inconclusive") ? { inconclusive: cases.filter((item) => item.status === "inconclusive").length } : {},
32302
32414
  warnings: 0,
32303
32415
  steps: 0,
32304
32416
  stepsPassed: 0,
@@ -32323,7 +32435,8 @@ function buildRunSummary(runId, startedAt, cases, options = {}) {
32323
32435
  startedAt: startedAt.toISOString(),
32324
32436
  finishedAt: finishedAt.toISOString(),
32325
32437
  durationMs: finishedAt.getTime() - startedAt.getTime(),
32326
- status: totals.failed > 0 ? "failed" : totals.passed > 0 || totals.cases === 0 ? "passed" : "skipped",
32438
+ // run 状态按三桶优先级推导:产品失败 > 证据不足 > 环境阻塞 > 通过 > 跳过
32439
+ status: totals.failed > 0 ? "failed" : (totals.inconclusive ?? 0) > 0 ? "inconclusive" : (totals.blocked ?? 0) > 0 ? "blocked" : totals.passed > 0 || totals.cases === 0 ? "passed" : "skipped",
32327
32440
  ...options.cancelled ? { cancelled: true } : {},
32328
32441
  cases,
32329
32442
  totals
@@ -32375,6 +32488,7 @@ var TestRunner = class {
32375
32488
  rowIndex: item.rowIndex ?? 0,
32376
32489
  initialVariables: item.initialVariables ?? {},
32377
32490
  missingDependencies: item.missingDependencies ?? [],
32491
+ blockedError: item.blockedError,
32378
32492
  artifacts,
32379
32493
  bus,
32380
32494
  resolver: this.resolver,
@@ -32431,6 +32545,7 @@ import { existsSync as existsSync3 } from "node:fs";
32431
32545
  import { mkdtemp, readFile as readFile6, rm } from "node:fs/promises";
32432
32546
  import { tmpdir } from "node:os";
32433
32547
  import { join as join16 } from "node:path";
32548
+ import { errors } from "playwright";
32434
32549
  var PlaywrightAdapter = class {
32435
32550
  constructor(bundle, options = {}) {
32436
32551
  this.bundle = bundle;
@@ -32487,36 +32602,50 @@ var PlaywrightAdapter = class {
32487
32602
  const options = this.options;
32488
32603
  const page = await this.ensurePage();
32489
32604
  const target = options.baseUrl && !/^https?:\/\//i.test(url2) ? new URL(url2, options.baseUrl).toString() : url2;
32490
- await page.goto(target);
32605
+ try {
32606
+ await page.goto(target);
32607
+ } catch (err) {
32608
+ if (err instanceof errors.TimeoutError) {
32609
+ throw new TestPilotError("STEP_TIMEOUT", err.message, { cause: err });
32610
+ }
32611
+ if (err instanceof Error && err.message.includes("net::ERR")) {
32612
+ throw new TestPilotError("ENVIRONMENT_UNAVAILABLE", err.message, { cause: err });
32613
+ }
32614
+ throw err;
32615
+ }
32491
32616
  }
32492
32617
  async click(locator) {
32493
- await (await this.toLocator(locator)).click();
32618
+ const target = await this.toLocator(locator);
32619
+ await this.coded(() => target.click());
32494
32620
  }
32495
32621
  async input(locator, value) {
32496
- await (await this.toLocator(locator)).fill(value);
32622
+ const target = await this.toLocator(locator);
32623
+ await this.coded(() => target.fill(value));
32497
32624
  }
32498
32625
  async select(locator, value) {
32499
- await (await this.toLocator(locator)).selectOption(value);
32626
+ const target = await this.toLocator(locator);
32627
+ await this.coded(() => target.selectOption(value));
32500
32628
  }
32501
32629
  async wait(locator, timeoutMs) {
32502
32630
  const page = await this.ensurePage();
32503
32631
  if (locator) {
32504
- await this.toLocatorOn(page, locator).waitFor({ state: "visible", timeout: timeoutMs });
32632
+ await this.coded(() => this.toLocatorOn(page, locator).waitFor({ state: "visible", timeout: timeoutMs }));
32505
32633
  return;
32506
32634
  }
32507
32635
  await page.waitForTimeout(timeoutMs ?? 1e3);
32508
32636
  }
32509
32637
  async assert(locator, expected) {
32510
32638
  const target = await this.toLocator(locator);
32511
- await target.waitFor({ state: "visible" });
32512
- const actual = (await target.innerText()).trim();
32639
+ await this.coded(() => target.waitFor({ state: "visible" }));
32640
+ const actual = (await this.coded(() => target.innerText())).trim();
32513
32641
  const wanted = expected.trim();
32514
32642
  if (!actual.includes(wanted)) {
32515
- throw new Error(`\u65AD\u8A00\u5931\u8D25:\u671F\u671B\u5305\u542B "${wanted}",\u5B9E\u9645\u4E3A "${actual.slice(0, 200)}"`);
32643
+ throw new TestPilotError("ASSERTION_FAILED", `\u65AD\u8A00\u5931\u8D25:\u671F\u671B\u5305\u542B "${wanted}",\u5B9E\u9645\u4E3A "${actual.slice(0, 200)}"`);
32516
32644
  }
32517
32645
  }
32518
32646
  async extract(locator) {
32519
- return (await this.toLocator(locator)).innerText().then((text) => text.trim());
32647
+ const target = await this.toLocator(locator);
32648
+ return this.coded(() => target.innerText()).then((text) => text.trim());
32520
32649
  }
32521
32650
  async screenshot() {
32522
32651
  return (await this.ensurePage()).screenshot();
@@ -32534,6 +32663,17 @@ var PlaywrightAdapter = class {
32534
32663
  async toLocator(locator) {
32535
32664
  return this.toLocatorOn(await this.ensurePage(), locator);
32536
32665
  }
32666
+ /** 定位类操作的统一兜底:等待超时按「元素没等到」归类(可能是回归也可能是 selector 腐烂,不允许静默) */
32667
+ async coded(fn) {
32668
+ try {
32669
+ return await fn();
32670
+ } catch (err) {
32671
+ if (err instanceof errors.TimeoutError) {
32672
+ throw new TestPilotError("ELEMENT_NOT_FOUND", err.message, { cause: err });
32673
+ }
32674
+ throw err;
32675
+ }
32676
+ }
32537
32677
  toLocatorOn(page, locator) {
32538
32678
  if (locator.text !== void 0) {
32539
32679
  return page.getByText(locator.text, { exact: true });
@@ -32611,12 +32751,12 @@ function startCliAuto(cliPath, projectPath, port) {
32611
32751
  child.stderr?.on("data", (chunk) => {
32612
32752
  output2 += chunk.toString("utf8");
32613
32753
  });
32614
- child.once("error", (err) => rejectPromise(new Error(`\u62C9\u8D77\u5F00\u53D1\u8005\u5DE5\u5177 cli \u5931\u8D25:${err.message}`)));
32754
+ child.once("error", (err) => rejectPromise(new TestPilotError("ENVIRONMENT_UNAVAILABLE", `\u62C9\u8D77\u5F00\u53D1\u8005\u5DE5\u5177 cli \u5931\u8D25:${err.message}`)));
32615
32755
  child.once("exit", (code) => {
32616
32756
  if (code === 0) {
32617
32757
  resolvePromise();
32618
32758
  } else {
32619
- rejectPromise(new Error(`cli auto \u9000\u51FA\u7801 ${code}:${output2.slice(-400)}`));
32759
+ rejectPromise(new TestPilotError("ENVIRONMENT_UNAVAILABLE", `cli auto \u9000\u51FA\u7801 ${code}:${output2.slice(-400)}`));
32620
32760
  }
32621
32761
  });
32622
32762
  const startedAt = Date.now();
@@ -32629,7 +32769,7 @@ function startCliAuto(cliPath, projectPath, port) {
32629
32769
  socket.once("error", () => {
32630
32770
  socket.destroy();
32631
32771
  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`));
32772
+ rejectPromise(new TestPilotError("ENVIRONMENT_UNAVAILABLE", `\u7B49\u5F85\u5F00\u53D1\u8005\u5DE5\u5177\u81EA\u52A8\u5316\u7AEF\u53E3 ${port} \u8D85\u65F6;\u8BF7\u786E\u8BA4:1) \u5DE5\u5177\u5DF2\u767B\u5F55 2) \u8BBE\u7F6E->\u5B89\u5168\u8BBE\u7F6E->\u670D\u52A1\u7AEF\u53E3\u5DF2\u5F00\u542F`));
32633
32773
  } else {
32634
32774
  setTimeout(poll, 500);
32635
32775
  }
@@ -32678,7 +32818,7 @@ var MiniAppAdapter = class {
32678
32818
  await this.runWithReconnect(async () => (await this.element(locator)).input(value));
32679
32819
  }
32680
32820
  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");
32821
+ throw new TestPilotError("UNSUPPORTED_ACTION", "\u5C0F\u7A0B\u5E8F\u7AEF\u6682\u4E0D\u652F\u6301 select(picker \u4EA4\u4E92),\u8BF7\u6539\u7528 click \u5B8C\u6210\u9009\u62E9");
32682
32822
  }
32683
32823
  async wait(locator, timeoutMs) {
32684
32824
  await this.runWithReconnect(async () => {
@@ -32696,7 +32836,7 @@ var MiniAppAdapter = class {
32696
32836
  const actual = (await element.text()).trim();
32697
32837
  const wanted = expected.trim();
32698
32838
  if (!actual.includes(wanted)) {
32699
- throw new Error(`\u65AD\u8A00\u5931\u8D25:\u671F\u671B\u5305\u542B "${wanted}",\u5B9E\u9645\u4E3A "${actual.slice(0, 200)}"`);
32839
+ throw new TestPilotError("ASSERTION_FAILED", `\u65AD\u8A00\u5931\u8D25:\u671F\u671B\u5305\u542B "${wanted}",\u5B9E\u9645\u4E3A "${actual.slice(0, 200)}"`);
32700
32840
  }
32701
32841
  });
32702
32842
  }
@@ -32794,7 +32934,7 @@ var MiniAppAdapter = class {
32794
32934
  const element = await page.$(selector);
32795
32935
  if (element) return element;
32796
32936
  if (Date.now() - startedAt > ELEMENT_WAIT_MS) {
32797
- throw new Error(`\u7B49\u5F85\u5143\u7D20\u8D85\u65F6(${ELEMENT_WAIT_MS}ms):${selector}`);
32937
+ throw new TestPilotError("ELEMENT_NOT_FOUND", `\u7B49\u5F85\u5143\u7D20\u8D85\u65F6(${ELEMENT_WAIT_MS}ms):${selector}`);
32798
32938
  }
32799
32939
  await delay2(500);
32800
32940
  }
@@ -32872,7 +33012,7 @@ var ApiAdapter = class {
32872
33012
  signal: AbortSignal.timeout(this.options.timeoutMs ?? 15e3)
32873
33013
  });
32874
33014
  } catch (err) {
32875
- throw new Error(`API \u8BF7\u6C42\u5931\u8D25 ${request.method.toUpperCase()} ${url2}:${err instanceof Error ? err.message : String(err)}`);
33015
+ throw new TestPilotError("ENVIRONMENT_UNAVAILABLE", `API \u8BF7\u6C42\u5931\u8D25 ${request.method.toUpperCase()} ${url2}:${err instanceof Error ? err.message : String(err)}`, { cause: err });
32876
33016
  }
32877
33017
  const bodyText = await res.text();
32878
33018
  let json2;
@@ -32894,7 +33034,7 @@ var ApiAdapter = class {
32894
33034
  if (!this.last) throw new Error("\u5C1A\u672A\u53D1\u8D77\u4EFB\u4F55 API \u8BF7\u6C42,\u65E0\u6CD5\u65AD\u8A00\u54CD\u5E94");
32895
33035
  if (!this.last.bodyText.includes(expected)) {
32896
33036
  const snippet2 = this.last.bodyText.slice(0, 200);
32897
- throw new Error(`\u54CD\u5E94\u4F53\u4E0D\u5305\u542B "${expected}":HTTP ${this.last.status} ${snippet2}`);
33037
+ throw new TestPilotError("ASSERTION_FAILED", `\u54CD\u5E94\u4F53\u4E0D\u5305\u542B "${expected}":HTTP ${this.last.status} ${snippet2}`);
32898
33038
  }
32899
33039
  }
32900
33040
  async extractResponse(path) {
@@ -33023,7 +33163,23 @@ async function runCases(options = {}) {
33023
33163
  variables: config2.variables,
33024
33164
  accounts: options.accounts
33025
33165
  }
33026
- )
33166
+ ).catch((err) => {
33167
+ if (err instanceof TestPilotError && err.code === "FIXTURE_MISSING") {
33168
+ return [
33169
+ {
33170
+ data: item.case,
33171
+ file: item.file,
33172
+ templateId: item.case.id,
33173
+ rowId: "default",
33174
+ rowIndex: 0,
33175
+ initialVariables: {},
33176
+ missingDependencies: [],
33177
+ blockedError: { code: err.code, message: err.message }
33178
+ }
33179
+ ];
33180
+ }
33181
+ throw err;
33182
+ })
33027
33183
  )
33028
33184
  )).flat();
33029
33185
  const summary = await runner.run(
@@ -33989,6 +34145,11 @@ function openInBrowser(file2) {
33989
34145
 
33990
34146
  // src/commands/run.ts
33991
34147
  import { join as join28 } from "node:path";
34148
+ function exitCodeForSummary(status) {
34149
+ if (status === "failed" || status === "inconclusive") return 1;
34150
+ if (status === "blocked") return 2;
34151
+ return 0;
34152
+ }
33992
34153
  function makeRunCommand() {
33993
34154
  return new Command("run").description("\u6267\u884C\u6D4B\u8BD5\u7528\u4F8B(\u9ED8\u8BA4\u8FD0\u884C testpilot.yaml \u914D\u7F6E\u7684 Case)").argument("[paths...]", "\u7528\u4F8B\u6587\u4EF6\u6216\u76EE\u5F55,\u9ED8\u8BA4\u6309\u9879\u76EE\u914D\u7F6E").option("--tag <tag>", "\u6309\u6807\u7B7E\u8FC7\u6EE4\u7528\u4F8B").action(async (paths, options) => {
33994
34155
  const client = new TestPilotClient();
@@ -34010,11 +34171,11 @@ function makeRunCommand() {
34010
34171
  ].filter(Boolean).join(",");
34011
34172
  console.log("");
34012
34173
  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})` : ""}`
34174
+ `Run ${summary.runId}${summary.cancelled ? "(\u5DF2\u53D6\u6D88)" : ""}: ${summary.status.toUpperCase()} \u2014 \u6A21\u677F ${summary.totals.templates ?? summary.totals.cases},\u6570\u636E\u884C \u2713${summary.totals.passed} \u2717${summary.totals.failed} \u2298${summary.totals.blocked ?? 0} ?${summary.totals.inconclusive ?? 0} \u21B7${summary.totals.skipped ?? 0} \u26A0${summary.totals.warnings ?? 0},\u6B65\u9AA4 \u2713${summary.totals.stepsPassed} \u2717${summary.totals.stepsFailed} \u21B7${summary.totals.stepsSkipped} \u26A0${summary.totals.stepsWarning ?? 0}${notes ? `(${notes})` : ""}`
34014
34175
  );
34015
34176
  console.log(`\u4EA7\u7269\u76EE\u5F55:${join28(".testpilot", "artifacts", "runs", summary.runId)}`);
34016
34177
  console.log("\u67E5\u770B\u62A5\u544A:npx csspilot report");
34017
- if (summary.status === "failed") process.exitCode = 1;
34178
+ process.exitCode = exitCodeForSummary(summary.status);
34018
34179
  } catch (err) {
34019
34180
  console.error(err instanceof Error ? err.message : err);
34020
34181
  process.exitCode = 1;
@@ -34189,7 +34350,7 @@ function makeValidateCommand() {
34189
34350
  }
34190
34351
 
34191
34352
  // src/cli.ts
34192
- var CLI_VERSION = "0.6.0";
34353
+ var CLI_VERSION = "0.7.0";
34193
34354
  async function createCli() {
34194
34355
  const program2 = new Command();
34195
34356
  program2.name("csspilot").description("TestPilot \u2014 AI Native \u8DE8\u7AEF\u4E1A\u52A1\u6D4B\u8BD5\u57FA\u7840\u8BBE\u65BD CLI").version(CLI_VERSION, "-v, --version", "output the current version").option("-V", "output the current version", () => {