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