csspilot 0.4.0 → 0.6.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
@@ -3094,7 +3094,7 @@ var require_visit = __commonJS({
3094
3094
  var BREAK = Symbol("break visit");
3095
3095
  var SKIP = Symbol("skip children");
3096
3096
  var REMOVE = Symbol("remove node");
3097
- function visit2(node2, visitor) {
3097
+ function visit3(node2, visitor) {
3098
3098
  const visitor_ = initVisitor(visitor);
3099
3099
  if (identity.isDocument(node2)) {
3100
3100
  const cd = visit_(null, node2.contents, visitor_, Object.freeze([node2]));
@@ -3103,9 +3103,9 @@ var require_visit = __commonJS({
3103
3103
  } else
3104
3104
  visit_(null, node2, visitor_, Object.freeze([]));
3105
3105
  }
3106
- visit2.BREAK = BREAK;
3107
- visit2.SKIP = SKIP;
3108
- visit2.REMOVE = REMOVE;
3106
+ visit3.BREAK = BREAK;
3107
+ visit3.SKIP = SKIP;
3108
+ visit3.REMOVE = REMOVE;
3109
3109
  function visit_(key, node2, visitor, path) {
3110
3110
  const ctrl = callVisitor(key, node2, visitor, path);
3111
3111
  if (identity.isNode(ctrl) || identity.isPair(ctrl)) {
@@ -3239,7 +3239,7 @@ var require_visit = __commonJS({
3239
3239
  throw new Error(`Cannot replace node with ${pt} parent`);
3240
3240
  }
3241
3241
  }
3242
- exports.visit = visit2;
3242
+ exports.visit = visit3;
3243
3243
  exports.visitAsync = visitAsync;
3244
3244
  }
3245
3245
  });
@@ -3249,7 +3249,7 @@ var require_directives = __commonJS({
3249
3249
  "../../node_modules/.pnpm/yaml@2.9.0/node_modules/yaml/dist/doc/directives.js"(exports) {
3250
3250
  "use strict";
3251
3251
  var identity = require_identity();
3252
- var visit2 = require_visit();
3252
+ var visit3 = require_visit();
3253
3253
  var escapeChars = {
3254
3254
  "!": "%21",
3255
3255
  ",": "%2C",
@@ -3393,7 +3393,7 @@ var require_directives = __commonJS({
3393
3393
  let tagNames;
3394
3394
  if (doc && tagEntries.length > 0 && identity.isNode(doc.contents)) {
3395
3395
  const tags = {};
3396
- visit2.visit(doc.contents, (_key, node2) => {
3396
+ visit3.visit(doc.contents, (_key, node2) => {
3397
3397
  if (identity.isNode(node2) && node2.tag)
3398
3398
  tags[node2.tag] = true;
3399
3399
  });
@@ -3420,7 +3420,7 @@ var require_anchors = __commonJS({
3420
3420
  "../../node_modules/.pnpm/yaml@2.9.0/node_modules/yaml/dist/doc/anchors.js"(exports) {
3421
3421
  "use strict";
3422
3422
  var identity = require_identity();
3423
- var visit2 = require_visit();
3423
+ var visit3 = require_visit();
3424
3424
  function anchorIsValid(anchor2) {
3425
3425
  if (/[\x00-\x19\s,[\]{}]/.test(anchor2)) {
3426
3426
  const sa = JSON.stringify(anchor2);
@@ -3431,7 +3431,7 @@ var require_anchors = __commonJS({
3431
3431
  }
3432
3432
  function anchorNames(root) {
3433
3433
  const anchors = /* @__PURE__ */ new Set();
3434
- visit2.visit(root, {
3434
+ visit3.visit(root, {
3435
3435
  Value(_key, node2) {
3436
3436
  if (node2.anchor)
3437
3437
  anchors.add(node2.anchor);
@@ -3611,7 +3611,7 @@ var require_Alias = __commonJS({
3611
3611
  "../../node_modules/.pnpm/yaml@2.9.0/node_modules/yaml/dist/nodes/Alias.js"(exports) {
3612
3612
  "use strict";
3613
3613
  var anchors = require_anchors();
3614
- var visit2 = require_visit();
3614
+ var visit3 = require_visit();
3615
3615
  var identity = require_identity();
3616
3616
  var Node = require_Node();
3617
3617
  var toJS = require_toJS();
@@ -3637,7 +3637,7 @@ var require_Alias = __commonJS({
3637
3637
  nodes = ctx.aliasResolveCache;
3638
3638
  } else {
3639
3639
  nodes = [];
3640
- visit2.visit(doc, {
3640
+ visit3.visit(doc, {
3641
3641
  Node: (_key, node2) => {
3642
3642
  if (identity.isAlias(node2) || identity.hasAnchor(node2))
3643
3643
  nodes.push(node2);
@@ -8556,15 +8556,15 @@ var require_cst_visit = __commonJS({
8556
8556
  var BREAK = Symbol("break visit");
8557
8557
  var SKIP = Symbol("skip children");
8558
8558
  var REMOVE = Symbol("remove item");
8559
- function visit2(cst, visitor) {
8559
+ function visit3(cst, visitor) {
8560
8560
  if ("type" in cst && cst.type === "document")
8561
8561
  cst = { start: cst.start, value: cst.value };
8562
8562
  _visit(Object.freeze([]), cst, visitor);
8563
8563
  }
8564
- visit2.BREAK = BREAK;
8565
- visit2.SKIP = SKIP;
8566
- visit2.REMOVE = REMOVE;
8567
- visit2.itemAtPath = (cst, path) => {
8564
+ visit3.BREAK = BREAK;
8565
+ visit3.SKIP = SKIP;
8566
+ visit3.REMOVE = REMOVE;
8567
+ visit3.itemAtPath = (cst, path) => {
8568
8568
  let item = cst;
8569
8569
  for (const [field, index] of path) {
8570
8570
  const tok = item?.[field];
@@ -8575,8 +8575,8 @@ var require_cst_visit = __commonJS({
8575
8575
  }
8576
8576
  return item;
8577
8577
  };
8578
- visit2.parentCollection = (cst, path) => {
8579
- const parent = visit2.itemAtPath(cst, path.slice(0, -1));
8578
+ visit3.parentCollection = (cst, path) => {
8579
+ const parent = visit3.itemAtPath(cst, path.slice(0, -1));
8580
8580
  const field = path[path.length - 1][0];
8581
8581
  const coll = parent?.[field];
8582
8582
  if (coll && "items" in coll)
@@ -8607,7 +8607,7 @@ var require_cst_visit = __commonJS({
8607
8607
  }
8608
8608
  return typeof ctrl === "function" ? ctrl(item, path) : ctrl;
8609
8609
  }
8610
- exports.visit = visit2;
8610
+ exports.visit = visit3;
8611
8611
  }
8612
8612
  });
8613
8613
 
@@ -10323,7 +10323,7 @@ var require_dist = __commonJS({
10323
10323
  var lineCounter = require_line_counter();
10324
10324
  var parser = require_parser();
10325
10325
  var publicApi = require_public_api();
10326
- var visit2 = require_visit();
10326
+ var visit3 = require_visit();
10327
10327
  exports.Composer = composer.Composer;
10328
10328
  exports.Document = Document.Document;
10329
10329
  exports.Schema = Schema.Schema;
@@ -10351,8 +10351,8 @@ var require_dist = __commonJS({
10351
10351
  exports.parseAllDocuments = publicApi.parseAllDocuments;
10352
10352
  exports.parseDocument = publicApi.parseDocument;
10353
10353
  exports.stringify = publicApi.stringify;
10354
- exports.visit = visit2.visit;
10355
- exports.visitAsync = visit2.visitAsync;
10354
+ exports.visit = visit3.visit;
10355
+ exports.visitAsync = visit3.visitAsync;
10356
10356
  }
10357
10357
  });
10358
10358
 
@@ -10449,8 +10449,8 @@ function makeCiCommand() {
10449
10449
  }
10450
10450
 
10451
10451
  // src/commands/init/doctor.ts
10452
- import { existsSync as existsSync7 } from "node:fs";
10453
- import { join as join22 } from "node:path";
10452
+ import { existsSync as existsSync8 } from "node:fs";
10453
+ import { join as join24 } from "node:path";
10454
10454
 
10455
10455
  // ../../packages/core/src/errors/index.ts
10456
10456
  var TestPilotError = class extends Error {
@@ -15513,7 +15513,7 @@ var recursive = /* @__PURE__ */ new WeakMap();
15513
15513
  var NONE = 0;
15514
15514
  var ASSUMED = 1;
15515
15515
  var PROVEN = 2;
15516
- function isRecursive(inst, stack, resolve5) {
15516
+ function isRecursive(inst, stack, resolve7) {
15517
15517
  const cached2 = recursive.get(inst);
15518
15518
  if (cached2 !== void 0)
15519
15519
  return cached2 ? PROVEN : NONE;
@@ -15523,7 +15523,7 @@ function isRecursive(inst, stack, resolve5) {
15523
15523
  let result = NONE;
15524
15524
  const check2 = (child) => {
15525
15525
  if (result !== PROVEN && child?._zod) {
15526
- const answer = isRecursive(child, stack, resolve5);
15526
+ const answer = isRecursive(child, stack, resolve7);
15527
15527
  if (answer > result)
15528
15528
  result = answer;
15529
15529
  }
@@ -15534,7 +15534,7 @@ function isRecursive(inst, stack, resolve5) {
15534
15534
  const desc = Object.getOwnPropertyDescriptor(sh, key);
15535
15535
  if (spread && !desc.enumerable)
15536
15536
  continue;
15537
- const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve5) : NONE;
15537
+ const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve7) : NONE;
15538
15538
  if (child > answer)
15539
15539
  answer = child;
15540
15540
  }
@@ -15601,7 +15601,7 @@ function isRecursive(inst, stack, resolve5) {
15601
15601
  break;
15602
15602
  // `$ZodLazy` caches its inner on the def, so a resolved edge is followed exactly
15603
15603
  case "lazy": {
15604
- const inner = def._cachedInner ?? (resolve5 ? inst._zod.innerType : void 0);
15604
+ const inner = def._cachedInner ?? (resolve7 ? inst._zod.innerType : void 0);
15605
15605
  merge2(inner ? isRecursive(inner, stack, false) : ASSUMED);
15606
15606
  break;
15607
15607
  }
@@ -30167,6 +30167,8 @@ var configSchema = external_exports.strictObject({
30167
30167
  }).optional(),
30168
30168
  /** 环境配置:default 指定默认环境名,其余键为环境名 -> { baseUrl } */
30169
30169
  environment: external_exports.record(external_exports.string(), external_exports.union([external_exports.string().min(1), environmentEntrySchema])).optional(),
30170
+ /** Case 可见的项目变量白名单;值可通过 ${ENV_VAR} 从运行环境注入 */
30171
+ variables: external_exports.record(external_exports.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/), external_exports.string()).optional(),
30170
30172
  /** 项目声明的执行端(playwright / wechatide);仅作声明,实际可用性以 doctor 检测为准 */
30171
30173
  adapters: external_exports.array(external_exports.string().min(1)).optional(),
30172
30174
  workspace: external_exports.strictObject({ default: external_exports.string().min(1).optional() }).optional(),
@@ -30231,6 +30233,9 @@ async function loadTestpilotConfig(cwd = process.cwd()) {
30231
30233
  testDirectory: parsed.test?.directory,
30232
30234
  projectName: parsed.project?.name,
30233
30235
  environment,
30236
+ variables: parsed.variables ? Object.fromEntries(
30237
+ Object.entries(parsed.variables).map(([name, value]) => [name, interpolateEnvRefs(value)])
30238
+ ) : void 0,
30234
30239
  adapters: parsed.adapters,
30235
30240
  workspace: parsed.workspace,
30236
30241
  server: parsed.server ? {
@@ -30341,11 +30346,11 @@ function execToString(command, args) {
30341
30346
  }
30342
30347
 
30343
30348
  // ../../packages/sdk/src/client.ts
30344
- import { isAbsolute as isAbsolute3, join as join17 } from "node:path";
30349
+ import { isAbsolute as isAbsolute4, join as join19 } from "node:path";
30345
30350
 
30346
30351
  // ../../packages/sdk/src/cases.ts
30347
- import { mkdir as mkdir3, readFile as readFile3, readdir, stat as stat2, writeFile as writeFile3 } from "node:fs/promises";
30348
- import { dirname, extname, isAbsolute, join as join6, resolve as resolve2 } from "node:path";
30352
+ import { mkdir as mkdir3, readFile as readFile4, readdir, stat as stat2, writeFile as writeFile3 } from "node:fs/promises";
30353
+ import { dirname as dirname2, extname as extname2, isAbsolute as isAbsolute2, join as join6, resolve as resolve3 } from "node:path";
30349
30354
 
30350
30355
  // ../../packages/dsl/src/schema/index.ts
30351
30356
  var STEP_TARGETS = ["web", "miniapp", "api"];
@@ -30366,6 +30371,12 @@ var locatorSchema = external_exports.strictObject({
30366
30371
  text: external_exports.string().min(1).optional(),
30367
30372
  css: external_exports.string().min(1).optional()
30368
30373
  }).describe("text \u4E0E css \u53EA\u80FD\u63D0\u4F9B\u5176\u4E00");
30374
+ var requestExpectationSchema = external_exports.strictObject({
30375
+ method: external_exports.enum(HTTP_METHODS).optional(),
30376
+ urlContains: external_exports.string().min(1),
30377
+ count: external_exports.number().int().nonnegative(),
30378
+ windowMs: external_exports.number().int().nonnegative().default(1e3)
30379
+ });
30369
30380
  var stepSchema = external_exports.strictObject({
30370
30381
  target: external_exports.enum(STEP_TARGETS),
30371
30382
  action: external_exports.enum(ACTIONS),
@@ -30380,8 +30391,18 @@ var stepSchema = external_exports.strictObject({
30380
30391
  /** api target:请求体;对象按 JSON 序列化,字符串原样发送(支持 ${var} 模板) */
30381
30392
  body: external_exports.union([external_exports.string(), external_exports.record(external_exports.string(), external_exports.unknown())]).optional(),
30382
30393
  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(),
30383
- timeout: external_exports.number().int().positive().optional()
30394
+ timeout: external_exports.number().int().positive().optional(),
30395
+ /** UI 动作触发的网络请求精确次数断言 */
30396
+ expectRequests: external_exports.array(requestExpectationSchema).min(1).optional()
30397
+ });
30398
+ var datasetReferenceSchema = external_exports.strictObject({
30399
+ file: external_exports.string().min(1),
30400
+ idField: external_exports.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/, "idField \u9700\u4E3A\u5408\u6CD5\u5B57\u6BB5\u540D").default("id")
30384
30401
  });
30402
+ var dependencyPathSchema = external_exports.string().regex(
30403
+ /^(fixture|dataset|account|variable)(?:\.[A-Za-z_][A-Za-z0-9_]*)+$/,
30404
+ "requires \u8DEF\u5F84\u5FC5\u987B\u4EE5 fixture/dataset/account/variable \u5F00\u5934\u5E76\u5305\u542B\u5B57\u6BB5"
30405
+ );
30385
30406
  var caseSchema = external_exports.strictObject({
30386
30407
  id: external_exports.string().regex(/^[a-z0-9][a-z0-9-]*$/, "id \u9700\u4E3A kebab-case(\u5C0F\u5199\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u8FDE\u5B57\u7B26)"),
30387
30408
  name: external_exports.string().min(1, "name \u4E0D\u80FD\u4E3A\u7A7A"),
@@ -30393,7 +30414,12 @@ var caseSchema = external_exports.strictObject({
30393
30414
  /** 引用测试账号标识;凭据由 Environment + Server/CI Secret 管理,禁止写进 Case */
30394
30415
  accountRef: external_exports.string().min(1).optional(),
30395
30416
  tags: external_exports.array(external_exports.string().min(1)).optional(),
30396
- steps: external_exports.array(stepSchema).min(1, "Case \u81F3\u5C11\u5305\u542B\u4E00\u4E2A step")
30417
+ fixtures: external_exports.array(external_exports.string().min(1)).min(1).optional(),
30418
+ datasets: datasetReferenceSchema.optional(),
30419
+ requires: external_exports.array(dependencyPathSchema).min(1).optional(),
30420
+ setup: external_exports.array(stepSchema).min(1).optional(),
30421
+ steps: external_exports.array(stepSchema).min(1, "Case \u81F3\u5C11\u5305\u542B\u4E00\u4E2A step"),
30422
+ teardown: external_exports.array(stepSchema).min(1).optional()
30397
30423
  });
30398
30424
 
30399
30425
  // ../../packages/dsl/src/parser/index.ts
@@ -30444,6 +30470,7 @@ var ADAPTER_SUPPORT = {
30444
30470
  };
30445
30471
  var LOCATOR_REQUIRED = /* @__PURE__ */ new Set(["click", "input", "select", "assert", "extract"]);
30446
30472
  var VALUE_REQUIRED = /* @__PURE__ */ new Set(["input", "select"]);
30473
+ var REQUEST_EXPECTATION_ACTIONS = /* @__PURE__ */ new Set(["navigate", "click", "input", "select"]);
30447
30474
  var FIELD_ALLOWED = {
30448
30475
  url: ["navigate", "request"],
30449
30476
  value: ["input", "select", "extract"],
@@ -30458,12 +30485,21 @@ var VARIABLE_REF = /\$\{([A-Za-z_][A-Za-z0-9_.]*)\}/g;
30458
30485
  function validateCase(data) {
30459
30486
  const issues = [];
30460
30487
  const declared = /* @__PURE__ */ new Set();
30488
+ if (data.fixtures) declared.add("fixture");
30489
+ if (data.datasets) declared.add("dataset");
30490
+ declared.add("variable");
30461
30491
  if (data.accountRef) {
30462
30492
  declared.add("account");
30463
30493
  }
30494
+ data.setup?.forEach((step, index) => {
30495
+ checkStep(step, `setup[${index}]`, declared, issues);
30496
+ });
30464
30497
  data.steps.forEach((step, index) => {
30465
30498
  checkStep(step, `steps[${index}]`, declared, issues);
30466
30499
  });
30500
+ data.teardown?.forEach((step, index) => {
30501
+ checkStep(step, `teardown[${index}]`, declared, issues);
30502
+ });
30467
30503
  return issues;
30468
30504
  }
30469
30505
  function validateCaseSource(source) {
@@ -30538,6 +30574,15 @@ function checkStep(step, at, declared, issues) {
30538
30574
  if (step.action === "wait" && !step.locator && step.timeout === void 0) {
30539
30575
  issues.push({ path: at, code: "semantic", message: "wait \u9700\u8981\u63D0\u4F9B locator(\u7B49\u5F85\u5143\u7D20)\u6216 timeout(\u6BEB\u79D2)" });
30540
30576
  }
30577
+ if (step.expectRequests) {
30578
+ if (step.target === "api" || !REQUEST_EXPECTATION_ACTIONS.has(step.action)) {
30579
+ issues.push({
30580
+ path: `${at}.expectRequests`,
30581
+ code: "semantic",
30582
+ message: "expectRequests \u4EC5\u7528\u4E8E web/miniapp \u7684 navigate/click/input/select"
30583
+ });
30584
+ }
30585
+ }
30541
30586
  checkVariableRefs(step, at, declared, issues);
30542
30587
  }
30543
30588
  function checkLocator(step, at, issues) {
@@ -30570,9 +30615,14 @@ function checkVariableRefs(step, at, declared, issues) {
30570
30615
  collect("value", step.value);
30571
30616
  collect("expected", step.expected);
30572
30617
  collect("url", step.url);
30618
+ collect("locator.text", step.locator?.text);
30619
+ collect("locator.css", step.locator?.css);
30573
30620
  if (typeof step.body === "string") collect("body", step.body);
30574
30621
  else if (step.body) collect("body", JSON.stringify(step.body));
30575
30622
  for (const headerValue of Object.values(step.headers ?? {})) collect("headers", headerValue);
30623
+ for (const [index, expectation] of (step.expectRequests ?? []).entries()) {
30624
+ collect(`expectRequests[${index}].urlContains`, expectation.urlContains);
30625
+ }
30576
30626
  for (const [field, name] of refs) {
30577
30627
  const root = name.split(".")[0] ?? name;
30578
30628
  if (!declared.has(root)) {
@@ -30587,9 +30637,20 @@ function checkVariableRefs(step, at, declared, issues) {
30587
30637
 
30588
30638
  // ../../packages/sdk/src/project.ts
30589
30639
  import { mkdir as mkdir2, readFile as readFile2, stat, writeFile as writeFile2 } from "node:fs/promises";
30590
- import { join as join5, resolve } from "node:path";
30640
+ import { existsSync as existsSync2 } from "node:fs";
30641
+ import { dirname, join as join5, resolve } from "node:path";
30591
30642
  function resolveRoot(root) {
30592
- return resolve(root ?? process.cwd());
30643
+ const start = resolve(root ?? process.cwd());
30644
+ if (root) return start;
30645
+ let current = start;
30646
+ for (; ; ) {
30647
+ if (existsSync2(join5(current, "testpilot.yaml")) || existsSync2(join5(current, TESTPILOT_DIR))) {
30648
+ return current;
30649
+ }
30650
+ const parent = dirname(current);
30651
+ if (parent === current) return start;
30652
+ current = parent;
30653
+ }
30593
30654
  }
30594
30655
  async function loadProjectConfig(root) {
30595
30656
  return loadTestpilotConfig(resolveRoot(root));
@@ -30638,6 +30699,164 @@ async function writeProjectLink(link, root) {
30638
30699
  return file2;
30639
30700
  }
30640
30701
 
30702
+ // ../../packages/sdk/src/case-data.ts
30703
+ var import_yaml3 = __toESM(require_dist(), 1);
30704
+ import { readFile as readFile3, realpath } from "node:fs/promises";
30705
+ import { extname, isAbsolute, relative, resolve as resolve2 } from "node:path";
30706
+ async function loadCaseDataFile(reference, options) {
30707
+ if (isAbsolute(reference)) {
30708
+ throw new Error(`\u6570\u636E\u6587\u4EF6\u5FC5\u987B\u4F7F\u7528\u9879\u76EE\u76F8\u5BF9\u8DEF\u5F84:${reference}`);
30709
+ }
30710
+ const root = resolve2(options.root);
30711
+ const dataDir = resolve2(options.dataDir);
30712
+ const target = resolve2(root, reference);
30713
+ if (!isInside(dataDir, target)) {
30714
+ throw new Error(`\u6570\u636E\u6587\u4EF6\u5FC5\u987B\u4F4D\u4E8E tests/e2e/data \u76EE\u5F55:${reference}`);
30715
+ }
30716
+ const [realDataDir, realTarget] = await Promise.all([
30717
+ realpath(dataDir).catch(() => dataDir),
30718
+ realpath(target).catch((error62) => {
30719
+ throw new Error(
30720
+ `\u8BFB\u53D6\u6570\u636E\u6587\u4EF6\u5931\u8D25:${reference}:${error62 instanceof Error ? error62.message : String(error62)}`
30721
+ );
30722
+ })
30723
+ ]);
30724
+ if (!isInside(realDataDir, realTarget)) {
30725
+ throw new Error(`\u6570\u636E\u6587\u4EF6\u89E3\u6790\u540E\u8D8A\u51FA tests/e2e/data \u76EE\u5F55:${reference}`);
30726
+ }
30727
+ const source = await readFile3(realTarget, "utf8");
30728
+ const extension = extname(realTarget).toLowerCase();
30729
+ try {
30730
+ if (extension === ".yaml" || extension === ".yml") return (0, import_yaml3.parse)(source);
30731
+ if (extension === ".json") return JSON.parse(source);
30732
+ } catch (error62) {
30733
+ throw new Error(
30734
+ `\u89E3\u6790\u6570\u636E\u6587\u4EF6\u5931\u8D25:${reference}:${error62 instanceof Error ? error62.message : String(error62)}`
30735
+ );
30736
+ }
30737
+ throw new Error(`\u6570\u636E\u6587\u4EF6\u4EC5\u652F\u6301 .yaml/.yml/.json:${reference}`);
30738
+ }
30739
+ function isDataObject(value) {
30740
+ return value !== null && typeof value === "object" && !Array.isArray(value);
30741
+ }
30742
+ function mergeFixture(target, source, at = "fixture") {
30743
+ const output2 = { ...target };
30744
+ for (const [key, value] of Object.entries(source)) {
30745
+ const path = `${at}.${key}`;
30746
+ if (!(key in output2)) {
30747
+ output2[key] = value;
30748
+ continue;
30749
+ }
30750
+ const current = output2[key];
30751
+ if (isDataObject(current) && isDataObject(value)) {
30752
+ output2[key] = mergeFixture(current, value, path);
30753
+ continue;
30754
+ }
30755
+ throw new Error(`fixture \u5B57\u6BB5\u51B2\u7A81:${path}`);
30756
+ }
30757
+ return output2;
30758
+ }
30759
+ function flattenVariables(prefix, value) {
30760
+ const output2 = {};
30761
+ visit2(prefix, value, output2);
30762
+ return output2;
30763
+ }
30764
+ function visit2(path, value, output2) {
30765
+ if (value === null || value === void 0) return;
30766
+ if (typeof value === "object") {
30767
+ output2[path] = JSON.stringify(value);
30768
+ if (Array.isArray(value)) {
30769
+ value.forEach((item, index) => visit2(`${path}.${index}`, item, output2));
30770
+ } else {
30771
+ for (const [key, item] of Object.entries(value)) {
30772
+ visit2(`${path}.${key}`, item, output2);
30773
+ }
30774
+ }
30775
+ return;
30776
+ }
30777
+ output2[path] = String(value);
30778
+ }
30779
+ function isInside(base, candidate) {
30780
+ const path = relative(base, candidate);
30781
+ return path === "" || !path.startsWith("..") && !isAbsolute(path);
30782
+ }
30783
+
30784
+ // ../../packages/sdk/src/case-preparation.ts
30785
+ async function prepareCaseExecutions(item, options) {
30786
+ const fixture = await loadFixtures(item.data.fixtures ?? [], options);
30787
+ const rows = await loadDataset(item.data, options);
30788
+ const projectVariables = flattenVariables("variable", options.variables ?? {});
30789
+ const accountVariables = accountValues(item.data, options.accounts);
30790
+ return rows.map(({ row, rowId }, rowIndex) => {
30791
+ const initialVariables = {
30792
+ ...flattenVariables("fixture", fixture),
30793
+ ...flattenVariables("dataset", row),
30794
+ ...projectVariables,
30795
+ ...accountVariables
30796
+ };
30797
+ const missingDependencies = (item.data.requires ?? []).filter(
30798
+ (path) => isUnavailable(initialVariables[path])
30799
+ );
30800
+ return {
30801
+ data: item.data,
30802
+ file: item.file,
30803
+ templateId: item.data.id,
30804
+ rowId,
30805
+ rowIndex,
30806
+ initialVariables,
30807
+ missingDependencies
30808
+ };
30809
+ });
30810
+ }
30811
+ async function loadFixtures(references, options) {
30812
+ let merged = {};
30813
+ for (const reference of references) {
30814
+ const value = await loadCaseDataFile(reference, options);
30815
+ if (!isDataObject(value)) {
30816
+ throw new Error(`fixture \u6587\u4EF6\u6839\u5FC5\u987B\u662F\u5BF9\u8C61:${reference}`);
30817
+ }
30818
+ merged = mergeFixture(merged, value);
30819
+ }
30820
+ return merged;
30821
+ }
30822
+ async function loadDataset(data, options) {
30823
+ if (!data.datasets) return [{ row: {}, rowId: "default" }];
30824
+ const value = await loadCaseDataFile(data.datasets.file, options);
30825
+ if (!Array.isArray(value)) {
30826
+ throw new Error(`dataset \u6587\u4EF6\u6839\u5FC5\u987B\u662F\u5BF9\u8C61\u6570\u7EC4:${data.datasets.file}`);
30827
+ }
30828
+ const seen = /* @__PURE__ */ new Set();
30829
+ return value.map((row, index) => {
30830
+ if (!isDataObject(row)) {
30831
+ throw new Error(`dataset \u7B2C ${index + 1} \u884C\u5FC5\u987B\u662F\u5BF9\u8C61:${data.datasets.file}`);
30832
+ }
30833
+ const rawId = row[data.datasets.idField];
30834
+ if (typeof rawId !== "string" && typeof rawId !== "number" || String(rawId).trim() === "") {
30835
+ throw new Error(
30836
+ `dataset \u7B2C ${index + 1} \u884C\u7F3A\u5C11\u975E\u7A7A row id \u5B57\u6BB5 "${data.datasets.idField}"`
30837
+ );
30838
+ }
30839
+ const rowId = String(rawId);
30840
+ if (seen.has(rowId)) throw new Error(`dataset row id \u91CD\u590D:${rowId}`);
30841
+ seen.add(rowId);
30842
+ return { row, rowId };
30843
+ });
30844
+ }
30845
+ function accountValues(data, accounts) {
30846
+ if (!data.accountRef) return {};
30847
+ const raw = accounts?.[data.accountRef];
30848
+ if (raw === void 0) return {};
30849
+ try {
30850
+ const parsed = JSON.parse(raw);
30851
+ if (isDataObject(parsed)) return flattenVariables("account", parsed);
30852
+ } catch {
30853
+ }
30854
+ return { account: raw };
30855
+ }
30856
+ function isUnavailable(value) {
30857
+ return value === void 0 || value.trim() === "" || /\$\{[A-Za-z_][A-Za-z0-9_]*\}/.test(value);
30858
+ }
30859
+
30641
30860
  // ../../packages/sdk/src/cases.ts
30642
30861
  async function collectCaseFiles(paths) {
30643
30862
  const files = [];
@@ -30666,19 +30885,20 @@ async function collectFromDir(dir) {
30666
30885
  return found;
30667
30886
  }
30668
30887
  function isCaseFile(file2) {
30669
- const ext = extname(file2).toLowerCase();
30888
+ const ext = extname2(file2).toLowerCase();
30670
30889
  return ext === ".yaml" || ext === ".yml";
30671
30890
  }
30672
30891
  function resolveAgainstRoot(root, file2) {
30673
- return isAbsolute(file2) ? file2 : resolve2(resolveRoot(root), file2);
30892
+ return isAbsolute2(file2) ? file2 : resolve3(resolveRoot(root), file2);
30674
30893
  }
30675
30894
  async function collectCases(paths, options = {}) {
30676
30895
  const files = await collectCaseFiles(
30677
30896
  paths.map((input2) => resolveAgainstRoot(options.root, input2))
30678
30897
  );
30679
30898
  const infos = [];
30899
+ const root = resolveRoot(options.root);
30680
30900
  for (const file2 of files) {
30681
- const validation = await validateCaseFile(file2);
30901
+ const validation = await validateCaseFile(file2, root);
30682
30902
  infos.push({
30683
30903
  file: file2,
30684
30904
  valid: validation.ok,
@@ -30690,16 +30910,16 @@ async function collectCases(paths, options = {}) {
30690
30910
  }
30691
30911
  async function readCaseFile(file2, options = {}) {
30692
30912
  const target = resolveAgainstRoot(options.root, file2);
30693
- const source = await readFile3(target, "utf8");
30694
- return { file: target, source, validation: validateCaseSource(source) };
30913
+ const source = await readFile4(target, "utf8");
30914
+ return { file: target, source, validation: await validateCaseDataSource(source, target, resolveRoot(options.root)) };
30695
30915
  }
30696
- async function validateCaseFile(file2) {
30697
- const source = await readFile3(file2, "utf8");
30698
- return validateCaseSource(source);
30916
+ async function validateCaseFile(file2, root) {
30917
+ const source = await readFile4(file2, "utf8");
30918
+ return validateCaseDataSource(source, file2, root);
30699
30919
  }
30700
30920
  async function writeCaseFile(file2, source, options = {}) {
30701
30921
  const target = resolveAgainstRoot(options.root, file2);
30702
- const validation = validateCaseSource(source);
30922
+ const validation = await validateCaseDataSource(source, target, resolveRoot(options.root));
30703
30923
  if (!validation.ok) {
30704
30924
  return { file: target, written: false, validation };
30705
30925
  }
@@ -30709,18 +30929,91 @@ async function writeCaseFile(file2, source, options = {}) {
30709
30929
  throw new Error(`\u7528\u4F8B\u6587\u4EF6\u5DF2\u5B58\u5728:${target}(\u8986\u76D6\u9700\u663E\u5F0F overwrite: true)`);
30710
30930
  }
30711
30931
  }
30712
- await mkdir3(dirname(target), { recursive: true });
30932
+ await mkdir3(dirname2(target), { recursive: true });
30713
30933
  await writeFile3(target, source, "utf8");
30714
30934
  return { file: target, written: true, validation };
30715
30935
  }
30936
+ async function validateCaseDataSource(source, file2, root) {
30937
+ const validation = validateCaseSource(source);
30938
+ if (!validation.ok || !validation.data) return validation;
30939
+ try {
30940
+ await prepareCaseExecutions(
30941
+ { data: validation.data, file: file2 },
30942
+ { root, dataDir: join6(root, "tests", "e2e", "data") }
30943
+ );
30944
+ return validation;
30945
+ } catch (error62) {
30946
+ return {
30947
+ ok: false,
30948
+ data: validation.data,
30949
+ issues: [
30950
+ {
30951
+ path: "(root)",
30952
+ code: "semantic",
30953
+ message: error62 instanceof Error ? error62.message : String(error62)
30954
+ }
30955
+ ]
30956
+ };
30957
+ }
30958
+ }
30716
30959
 
30717
30960
  // ../../packages/sdk/src/reports.ts
30718
- import { readFile as readFile6 } from "node:fs/promises";
30719
- import { join as join16 } from "node:path";
30961
+ import { readFile as readFile8 } from "node:fs/promises";
30962
+ import { join as join18 } from "node:path";
30720
30963
 
30721
30964
  // ../../packages/reporter/src/report.ts
30722
30965
  import { writeFile as writeFile4 } from "node:fs/promises";
30723
- import { join as join7, resolve as resolve3 } from "node:path";
30966
+ import { dirname as dirname3, join as join8, resolve as resolve4 } from "node:path";
30967
+
30968
+ // ../../packages/reporter/src/classify.ts
30969
+ var FAILURE_CATEGORIES = [
30970
+ { id: "assert", label: "\u65AD\u8A00\u5931\u8D25" },
30971
+ { id: "element", label: "\u5143\u7D20\u672A\u627E\u5230" },
30972
+ { id: "adapter", label: "Adapter \u95EE\u9898" },
30973
+ { id: "env", label: "\u73AF\u5883 / \u8FDE\u63A5" },
30974
+ { id: "timeout", label: "\u8D85\u65F6" },
30975
+ { id: "other", label: "\u5176\u4ED6" }
30976
+ ];
30977
+ function categoryLabel(id) {
30978
+ return FAILURE_CATEGORIES.find((item) => item.id === id)?.label ?? id;
30979
+ }
30980
+ var RULES = [
30981
+ { id: "assert", pattern: /断言失败|AssertionError|expect\(.+\)\.to/i },
30982
+ { id: "element", pattern: /未找到元素|等待元素超时|waiting for locator|waiting for selector|element not found/i },
30983
+ { id: "adapter", pattern: /未注册 target|ADAPTER_NOT_FOUND|无法定位 TestPilot Skill/i },
30984
+ { id: "env", pattern: /net::ERR|ECONNREFUSED|Connection closed|Failed to launch|服务端口|不可达|ENOTFOUND|EACCES/i },
30985
+ { id: "timeout", pattern: /timeout|超时/i }
30986
+ ];
30987
+ function classifyFailure(message) {
30988
+ if (!message) return "other";
30989
+ for (const rule of RULES) {
30990
+ if (rule.pattern.test(message)) return rule.id;
30991
+ }
30992
+ return "other";
30993
+ }
30994
+
30995
+ // ../../packages/reporter/src/history.ts
30996
+ import { readFile as readFile5, readdir as readdir2 } from "node:fs/promises";
30997
+ import { join as join7 } from "node:path";
30998
+ async function collectHistory(runsRoot2, skipRunId, limit = 12) {
30999
+ const entries = (await readdir2(runsRoot2).catch(() => [])).filter((name) => /^\d{8}-\d{3}$/.test(name) && name !== skipRunId).sort().slice(-limit);
31000
+ const points = [];
31001
+ for (const runId of entries) {
31002
+ try {
31003
+ const summary = JSON.parse(await readFile5(join7(runsRoot2, runId, "result.json"), "utf8"));
31004
+ points.push({
31005
+ runId,
31006
+ startedAt: summary.startedAt,
31007
+ status: summary.status ?? "unknown",
31008
+ passed: summary.totals?.passed ?? 0,
31009
+ failed: summary.totals?.failed ?? 0,
31010
+ durationMs: summary.durationMs
31011
+ });
31012
+ } catch {
31013
+ }
31014
+ }
31015
+ return points;
31016
+ }
30724
31017
 
30725
31018
  // ../../packages/reporter/src/summary.ts
30726
31019
  function toSummaryView(summary) {
@@ -30728,81 +31021,286 @@ function toSummaryView(summary) {
30728
31021
  runId: summary.runId,
30729
31022
  status: summary.status,
30730
31023
  durationMs: summary.durationMs,
30731
- cases: { passed: summary.totals.passed, failed: summary.totals.failed },
31024
+ cases: {
31025
+ templates: summary.totals.templates ?? summary.totals.cases,
31026
+ passed: summary.totals.passed,
31027
+ failed: summary.totals.failed,
31028
+ skipped: summary.totals.skipped ?? 0,
31029
+ warnings: summary.totals.warnings ?? 0
31030
+ },
30732
31031
  steps: {
30733
31032
  total: summary.totals.steps,
30734
31033
  passed: summary.totals.stepsPassed,
30735
31034
  failed: summary.totals.stepsFailed,
30736
- skipped: summary.totals.stepsSkipped
31035
+ skipped: summary.totals.stepsSkipped,
31036
+ warning: summary.totals.stepsWarning ?? 0
30737
31037
  }
30738
31038
  };
30739
31039
  }
30740
31040
 
30741
31041
  // ../../packages/reporter/src/report.ts
30742
- async function writeReports(runDir, summary) {
30743
- const dir = resolve3(runDir);
30744
- const jsonPath = join7(dir, "report.json");
30745
- const htmlPath = join7(dir, "report.html");
30746
- const payload = { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), summary };
30747
- await writeFile4(jsonPath, JSON.stringify(payload, null, 2), "utf8");
30748
- await writeFile4(htmlPath, renderHtml(summary), "utf8");
31042
+ var HISTORY_LIMIT = 12;
31043
+ async function writeReports(runDir, summary, options = {}) {
31044
+ const dir = resolve4(runDir);
31045
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
31046
+ const history = options.history ?? await collectHistory(dirname3(dir), summary.runId, options.historyLimit ?? HISTORY_LIMIT);
31047
+ const failures = [];
31048
+ const cases = summary.cases.map((item) => {
31049
+ const categories = /* @__PURE__ */ new Set();
31050
+ const steps = item.steps.map((step) => {
31051
+ const category = step.status === "failed" ? classifyFailure(step.error) : void 0;
31052
+ if (category) {
31053
+ categories.add(category);
31054
+ failures.push({
31055
+ category,
31056
+ label: categoryLabel(category),
31057
+ caseId: item.caseId,
31058
+ index: step.index,
31059
+ message: step.error ?? ""
31060
+ });
31061
+ }
31062
+ return { ...step, category };
31063
+ });
31064
+ return { ...item, steps, categories: [...categories] };
31065
+ });
31066
+ const categoryLabels = {};
31067
+ for (const item of FAILURE_CATEGORIES) categoryLabels[item.id] = item.label;
31068
+ const data = {
31069
+ generatedAt,
31070
+ summary: { ...summary, cases },
31071
+ history,
31072
+ failures,
31073
+ view: toSummaryView(summary),
31074
+ categoryLabels
31075
+ };
31076
+ const jsonPath = join8(dir, "report.json");
31077
+ const htmlPath = join8(dir, "report.html");
31078
+ await writeFile4(jsonPath, JSON.stringify(data, null, 2), "utf8");
31079
+ await writeFile4(htmlPath, renderHtml(data), "utf8");
30749
31080
  return { json: jsonPath, html: htmlPath };
30750
31081
  }
30751
- function renderHtml(summary) {
30752
- const view = toSummaryView(summary);
30753
- const casesHtml = summary.cases.map((item) => caseHtml(item)).join("\n");
31082
+ function categoryChips(data) {
31083
+ const counts = /* @__PURE__ */ new Map();
31084
+ for (const failure2 of data.failures) {
31085
+ counts.set(failure2.category, (counts.get(failure2.category) ?? 0) + 1);
31086
+ }
31087
+ const chips = [
31088
+ '<span class="chip active" data-group="status" data-value="all">\u5168\u90E8\u72B6\u6001</span>',
31089
+ '<span class="chip" data-group="status" data-value="failed">\u4EC5\u5931\u8D25</span>'
31090
+ ];
31091
+ for (const item of FAILURE_CATEGORIES) {
31092
+ const count = counts.get(item.id) ?? 0;
31093
+ if (count > 0) {
31094
+ chips.push(
31095
+ `<span class="chip" data-group="cat" data-value="${item.id}">${escapeHtml(item.label)}(${count})</span>`
31096
+ );
31097
+ }
31098
+ }
31099
+ return chips.join("\n");
31100
+ }
31101
+ function renderHtml(data) {
31102
+ const view = toSummaryView(data.summary);
31103
+ const summary = data.summary;
31104
+ const trendSvg = renderTrend(data);
31105
+ const casesHtml = groupedCasesHtml(summary.cases);
31106
+ const chips = categoryChips(data);
30754
31107
  return `<!doctype html>
30755
31108
  <html lang="zh-CN">
30756
31109
  <head>
30757
31110
  <meta charset="utf-8" />
30758
31111
  <title>TestPilot Report ${escapeHtml(summary.runId)}</title>
30759
31112
  <style>
30760
- body { font-family: system-ui, sans-serif; margin: 24px; color: #1f2328; }
30761
- h1 { font-size: 20px; } h2 { font-size: 16px; margin-top: 24px; }
30762
- .badge { padding: 2px 10px; border-radius: 10px; color: #fff; font-size: 13px; }
30763
- .passed { background: #1a7f37; } .failed { background: #cf222e; } .skipped { background: #6e7781; }
30764
- table { border-collapse: collapse; width: 100%; margin: 8px 0 16px; font-size: 13px; }
30765
- th, td { border: 1px solid #d0d7de; padding: 6px 10px; text-align: left; }
30766
- td.num { text-align: right; }
30767
- .error { color: #cf222e; white-space: pre-wrap; }
30768
- a { color: #0969da; }
30769
- code { background: #f6f8fa; padding: 1px 4px; border-radius: 4px; }
31113
+ :root { --ok:#1a7f37; --fail:#cf222e; --warn:#9a6700; --skip:#6e7781; --line:#d0d7de; --muted:#59636e; }
31114
+ * { box-sizing: border-box; }
31115
+ body { font-family: system-ui, "Segoe UI", sans-serif; margin: 0; color: #1f2328; background: #f6f8fa; }
31116
+ .wrap { max-width: 1080px; margin: 0 auto; padding: 20px 16px 48px; }
31117
+ h1 { font-size: 20px; margin: 8px 0; } h2 { font-size: 15px; margin: 24px 0 8px; }
31118
+ .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); }
31120
+ code { background: #eef1f4; padding: 1px 5px; border-radius: 4px; font-size: 12px; }
31121
+ .cards { display: flex; gap: 12px; flex-wrap: wrap; margin: 14px 0; }
31122
+ .card { background: #fff; border: 1px solid var(--line); border-radius: 8px; padding: 10px 16px; min-width: 130px; }
31123
+ .card b { font-size: 20px; display: block; }
31124
+ .card span { color: var(--muted); font-size: 12px; }
31125
+ .toolbar { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin: 10px 0; }
31126
+ .chip { border: 1px solid var(--line); background: #fff; border-radius: 14px; padding: 3px 12px; font-size: 12px; cursor: pointer; }
31127
+ .chip.active { background: #0969da; color: #fff; border-color: #0969da; }
31128
+ input#q { flex: 1; min-width: 180px; padding: 5px 10px; border: 1px solid var(--line); border-radius: 6px; font-size: 13px; }
31129
+ .btn { border: 1px solid var(--line); background: #fff; border-radius: 6px; padding: 4px 10px; font-size: 12px; cursor: pointer; }
31130
+ #count { color: var(--muted); font-size: 12px; margin-left: auto; }
31131
+ details.case { background: #fff; border: 1px solid var(--line); border-radius: 8px; margin: 10px 0; }
31132
+ section.template { border-left: 3px solid var(--line); padding-left: 12px; margin: 14px 0; }
31133
+ section.template > h3 { font-size: 14px; margin: 0 0 6px; }
31134
+ details.case > summary { cursor: pointer; padding: 10px 14px; font-size: 14px; list-style: none; }
31135
+ details.case > summary::-webkit-details-marker { display: none; }
31136
+ details.case > summary:hover { background: #f6f8fa; }
31137
+ table { border-collapse: collapse; width: 100%; font-size: 13px; }
31138
+ th, td { border-top: 1px solid var(--line); padding: 6px 10px; text-align: left; vertical-align: top; }
31139
+ td.num { text-align: right; white-space: nowrap; }
31140
+ .error { color: var(--fail); white-space: pre-wrap; }
31141
+ a { color: #0969da; text-decoration: none; }
31142
+ .trend { background: #fff; border: 1px solid var(--line); border-radius: 8px; padding: 12px; overflow-x: auto; }
31143
+ .muted { color: var(--muted); }
31144
+ .hide { display: none !important; }
31145
+ .meta { color: var(--muted); font-size: 12px; margin: 6px 0; }
30770
31146
  </style>
30771
31147
  </head>
30772
31148
  <body>
30773
- <h1>TestPilot \u62A5\u544A <span class="badge ${summary.status}">${summary.status}</span></h1>
30774
- <p>run <code>${escapeHtml(summary.runId)}</code> \xB7 \u7528\u4F8B ${view.cases.passed}\u2713 / ${view.cases.failed}\u2717 \xB7 \u6B65\u9AA4 ${view.steps.passed}\u2713 ${view.steps.failed}\u2717 ${view.steps.skipped}\u21B7 \xB7 \u8017\u65F6 ${(view.durationMs / 1e3).toFixed(1)}s</p>
30775
- ${casesHtml}
31149
+ <div class="wrap">
31150
+ <h1>TestPilot \u6D4B\u8BD5\u62A5\u544A <span class="badge ${summary.status}">${summary.status}</span></h1>
31151
+ <p class="meta">run <code>${escapeHtml(summary.runId)}</code> \xB7 \u751F\u6210\u4E8E ${escapeHtml(data.generatedAt)} \xB7 \u8017\u65F6 ${(view.durationMs / 1e3).toFixed(1)}s</p>
31152
+
31153
+ <div class="cards">
31154
+ <div class="card"><b>${view.cases.passed}<span style="color:var(--ok)"> \u2713</span></b><span>\u901A\u8FC7\u7528\u4F8B</span></div>
31155
+ <div class="card"><b>${view.cases.failed}<span style="color:var(--fail)"> \u2717</span></b><span>\u5931\u8D25\u7528\u4F8B</span></div>
31156
+ <div class="card"><b>${view.cases.skipped} \u21B7</b><span>\u8DF3\u8FC7\u6570\u636E\u884C</span></div>
31157
+ <div class="card"><b>${view.cases.warnings} \u26A0</b><span>\u6E05\u7406\u8B66\u544A</span></div>
31158
+ <div class="card"><b>${view.steps.passed}<span style="color:var(--ok)"> \u2713</span></b><span>\u901A\u8FC7\u6B65\u9AA4</span></div>
31159
+ <div class="card"><b>${view.steps.failed}<span style="color:var(--fail)"> \u2717</span></b><span>\u5931\u8D25\u6B65\u9AA4</span></div>
31160
+ <div class="card"><b>${view.steps.skipped} \u21B7</b><span>\u8DF3\u8FC7\u6B65\u9AA4</span></div>
31161
+ </div>
31162
+
31163
+ <h2>\u5386\u53F2\u8D8B\u52BF(\u6700\u8FD1 ${data.history.length + 1} \u6B21\u8FD0\u884C)</h2>
31164
+ <div class="trend">${trendSvg}</div>
31165
+
31166
+ <h2>\u5931\u8D25\u5206\u7C7B</h2>
31167
+ <div class="toolbar" id="cat-chips">${chips}</div>
31168
+
31169
+ <h2>\u7528\u4F8B</h2>
31170
+ <div class="toolbar">
31171
+ <input id="q" placeholder="\u641C\u7D22\u7528\u4F8B id / \u540D\u79F0 / \u6587\u4EF6\u2026" />
31172
+ <button class="btn" id="expand">\u5C55\u5F00\u5168\u90E8</button>
31173
+ <button class="btn" id="collapse">\u6536\u8D77\u5168\u90E8</button>
31174
+ <span id="count"></span>
31175
+ </div>
31176
+ <div id="cases">${casesHtml}</div>
31177
+ </div>
31178
+
31179
+ <script id="report-data" type="application/json">${embedJson(data)}</script>
31180
+ <script>
31181
+ (function () {
31182
+ var data = JSON.parse(document.getElementById('report-data').textContent);
31183
+ var state = { status: 'all', cat: 'all', q: '' };
31184
+ var labels = data.categoryLabels || {};
31185
+
31186
+ function setChip(group, value) {
31187
+ document.querySelectorAll('.chip[data-group="' + group + '"]').forEach(function (chip) {
31188
+ chip.classList.toggle('active', chip.getAttribute('data-value') === value);
31189
+ });
31190
+ }
31191
+
31192
+ function matches(card) {
31193
+ if (state.status !== 'all' && card.getAttribute('data-status') !== state.status) return false;
31194
+ if (state.cat !== 'all' && (',' + (card.getAttribute('data-cats') || '') + ',').indexOf(',' + state.cat + ',') === -1) return false;
31195
+ if (state.q) {
31196
+ var text = (card.getAttribute('data-search') || '').toLowerCase();
31197
+ if (text.indexOf(state.q.toLowerCase()) === -1) return false;
31198
+ }
31199
+ return true;
31200
+ }
31201
+
31202
+ function apply() {
31203
+ var cards = document.querySelectorAll('details.case');
31204
+ var shown = 0;
31205
+ cards.forEach(function (card) {
31206
+ var show = matches(card);
31207
+ card.classList.toggle('hide', !show);
31208
+ if (show) shown++;
31209
+ });
31210
+ var count = document.getElementById('count');
31211
+ if (count) count.textContent = shown + ' / ' + cards.length + ' \u7528\u4F8B';
31212
+ }
31213
+
31214
+ document.querySelectorAll('.chip').forEach(function (chip) {
31215
+ chip.addEventListener('click', function () {
31216
+ var group = chip.getAttribute('data-group');
31217
+ var value = chip.getAttribute('data-value');
31218
+ if (group === 'status') state.status = value;
31219
+ if (group === 'cat') state.cat = value;
31220
+ setChip(group, value);
31221
+ apply();
31222
+ });
31223
+ });
31224
+ var q = document.getElementById('q');
31225
+ if (q) q.addEventListener('input', function () { state.q = q.value.trim(); apply(); });
31226
+ var expand = document.getElementById('expand');
31227
+ if (expand) expand.addEventListener('click', function () {
31228
+ document.querySelectorAll('details.case').forEach(function (c) { c.open = true; });
31229
+ });
31230
+ var collapse = document.getElementById('collapse');
31231
+ if (collapse) collapse.addEventListener('click', function () {
31232
+ document.querySelectorAll('details.case').forEach(function (c) { c.open = false; });
31233
+ });
31234
+
31235
+ function failedSteps(card) {
31236
+ return data.summary.cases.find(function (c) { return c.caseId === card; });
31237
+ }
31238
+ apply();
31239
+ })();
31240
+ </script>
30776
31241
  </body>
30777
31242
  </html>`;
30778
31243
  }
31244
+ function groupedCasesHtml(cases) {
31245
+ const groups = /* @__PURE__ */ new Map();
31246
+ for (const item of cases) {
31247
+ const current = groups.get(item.caseId) ?? [];
31248
+ current.push(item);
31249
+ groups.set(item.caseId, current);
31250
+ }
31251
+ return [...groups.entries()].map(
31252
+ ([caseId, rows]) => `<section class="template">
31253
+ <h3>${escapeHtml(caseId)} <span class="muted">\xB7 ${rows.length} \u4E2A\u6570\u636E\u884C</span></h3>
31254
+ ${rows.map((item) => caseHtml(item)).join("\n")}
31255
+ </section>`
31256
+ ).join("\n");
31257
+ }
30779
31258
  function caseHtml(item) {
30780
31259
  const rows = item.steps.map((step) => stepHtml(step)).join("\n");
30781
31260
  const errorLine = item.error ? `<p class="error">${escapeHtml(item.error)}</p>` : "";
31261
+ const skipLine = item.skipReason ? `<p class="muted">${escapeHtml(item.skipReason)}${item.missingDependencies?.length ? `: ${escapeHtml(item.missingDependencies.join(", "))}` : ""}</p>` : "";
31262
+ const warningLine = item.warnings?.length ? `<p class="error">\u26A0 ${item.warnings.map(escapeHtml).join("<br />\u26A0 ")}</p>` : "";
30782
31263
  const links = [];
30783
31264
  if (item.video) links.push(`<a href="${escapeHtml(item.video)}">\u89C6\u9891</a>`);
30784
31265
  if (item.trace) links.push(`<a href="${escapeHtml(item.trace)}">Trace</a>`);
30785
31266
  const linksLine = links.length > 0 ? `<p>${links.join(" \xB7 ")}</p>` : "";
31267
+ const search = escapeHtml(`${item.caseId} ${item.rowId ?? "default"} ${item.caseName} ${item.file}`).replaceAll('"', "&quot;");
31268
+ const cats = escapeHtml(item.categories.join(","));
30786
31269
  return `
30787
- <h2>${escapeHtml(item.caseId)} \xB7 ${escapeHtml(item.caseName)} <span class="badge ${item.status}">${item.status}</span></h2>
30788
- ${errorLine}
30789
- ${linksLine}
30790
- <table>
30791
- <tr><th>#</th><th>target</th><th>action</th><th>\u72B6\u6001</th><th>\u8017\u65F6</th><th>\u8BE6\u60C5</th></tr>
30792
- ${rows}
30793
- </table>`;
31270
+ <details class="case" data-status="${item.status}" data-cats="${cats}" data-search="${search}">
31271
+ <summary>
31272
+ <span class="badge ${item.status}">${item.status}</span>
31273
+ <b>\u6570\u636E\u884C ${escapeHtml(item.rowId ?? "default")}</b> \xB7 ${escapeHtml(item.caseName)}
31274
+ <span class="muted">\xB7 ${item.steps.length} \u6B65 \xB7 ${(item.durationMs / 1e3).toFixed(1)}s</span>
31275
+ </summary>
31276
+ ${errorLine}
31277
+ ${skipLine}
31278
+ ${warningLine}
31279
+ ${linksLine}
31280
+ <table>
31281
+ <tr><th>#</th><th>\u9636\u6BB5</th><th>target</th><th>action</th><th>\u72B6\u6001</th><th>\u8017\u65F6</th><th>\u8BE6\u60C5</th></tr>
31282
+ ${rows}
31283
+ </table>
31284
+ </details>`;
30794
31285
  }
30795
31286
  function stepHtml(step) {
30796
31287
  const details = [];
30797
31288
  if (step.error) details.push(`<span class="error">${escapeHtml(step.error)}</span>`);
31289
+ if (step.category) details.push(`<span class="muted">\u5206\u7C7B: ${escapeHtml(categoryLabel(step.category))}</span>`);
30798
31290
  if (step.extracted) {
30799
31291
  for (const [name, value] of Object.entries(step.extracted)) {
30800
31292
  details.push(`\u53D8\u91CF ${escapeHtml(name)} = <code>${escapeHtml(value)}</code>`);
30801
31293
  }
30802
31294
  }
30803
31295
  if (step.screenshot) details.push(`<a href="${escapeHtml(step.screenshot)}">\u622A\u56FE</a>`);
31296
+ for (const assertion of step.requestAssertions ?? []) {
31297
+ details.push(
31298
+ `\u8BF7\u6C42 ${escapeHtml(assertion.method ?? "*")} <code>${escapeHtml(assertion.urlContains)}</code>: ${assertion.actualCount}/${assertion.count}`
31299
+ );
31300
+ }
30804
31301
  return `<tr>
30805
31302
  <td class="num">${step.index}</td>
31303
+ <td>${escapeHtml(step.phase ?? "steps")}</td>
30806
31304
  <td>${escapeHtml(step.target)}</td>
30807
31305
  <td>${escapeHtml(step.action)}</td>
30808
31306
  <td><span class="badge ${step.status}">${step.status}</span></td>
@@ -30810,21 +31308,348 @@ function stepHtml(step) {
30810
31308
  <td>${details.join("<br />") || "-"}</td>
30811
31309
  </tr>`;
30812
31310
  }
31311
+ function renderTrend(data) {
31312
+ const points = [
31313
+ ...data.history.map((h) => ({ runId: h.runId, passed: h.passed, failed: h.failed, status: h.status })),
31314
+ {
31315
+ runId: data.summary.runId,
31316
+ passed: data.summary.totals.passed,
31317
+ failed: data.summary.totals.failed,
31318
+ status: data.summary.status
31319
+ }
31320
+ ];
31321
+ if (points.length === 0) return '<p class="muted">\u6682\u65E0\u5386\u53F2\u6570\u636E</p>';
31322
+ const max = Math.max(1, ...points.map((p) => p.passed + p.failed));
31323
+ const barW = 34;
31324
+ const gap = 14;
31325
+ const chartH = 90;
31326
+ const labelH = 34;
31327
+ const width = points.length * (barW + gap) + gap;
31328
+ const height = chartH + labelH;
31329
+ const bars = points.map((point, index) => {
31330
+ const total = point.passed + point.failed;
31331
+ const scale = (count) => total === 0 ? 0 : count / max * chartH;
31332
+ const passedH = scale(point.passed);
31333
+ const failedH = scale(point.failed);
31334
+ const x = gap + index * (barW + gap);
31335
+ const passedY = chartH - passedH;
31336
+ const failedY = passedY - failedH;
31337
+ const tip = `${point.runId}: \u2713${point.passed} \u2717${point.failed}`;
31338
+ return `<g>
31339
+ <title>${escapeHtml(tip)}</title>
31340
+ <rect x="${x}" y="${chartH + 4}" width="${barW}" height="1" fill="var(--line)"></rect>
31341
+ ${passedH > 0 ? `<rect x="${x}" y="${passedY.toFixed(1)}" width="${barW}" height="${passedH.toFixed(1)}" fill="var(--ok)" rx="2"></rect>` : ""}
31342
+ ${failedH > 0 ? `<rect x="${x}" y="${failedY.toFixed(1)}" width="${barW}" height="${failedH.toFixed(1)}" fill="var(--fail)" rx="2"></rect>` : ""}
31343
+ <text x="${x + barW / 2}" y="${chartH + 20}" font-size="9" fill="var(--muted)" text-anchor="middle">${escapeHtml(shortId(point.runId))}</text>
31344
+ </g>`;
31345
+ }).join("\n");
31346
+ return `<svg width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" xmlns="http://www.w3.org/2000/svg">${bars}</svg>`;
31347
+ }
31348
+ function shortId(runId) {
31349
+ return runId.replace(/^\d{8}-/, "");
31350
+ }
30813
31351
  function escapeHtml(input2) {
30814
31352
  return input2.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
30815
31353
  }
31354
+ function embedJson(data) {
31355
+ return JSON.stringify(data).replaceAll("<", "\\u003c");
31356
+ }
31357
+
31358
+ // ../../packages/reporter/src/summary-report.ts
31359
+ import { mkdir as mkdir4, writeFile as writeFile5 } from "node:fs/promises";
31360
+ import { join as join9, resolve as resolve5 } from "node:path";
31361
+ function escapeHtml2(value) {
31362
+ return String(value).replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
31363
+ }
31364
+ function pct(value) {
31365
+ return (value * 100).toFixed(1) + "%";
31366
+ }
31367
+ function buildSummaryData(runs) {
31368
+ const executions = [];
31369
+ const runPoints = [];
31370
+ for (const { runId, summary } of runs) {
31371
+ for (const item of summary.cases) {
31372
+ const platforms = [...new Set(item.steps.map((step) => step.target))];
31373
+ const failedStep = item.steps.find((step) => step.status === "failed");
31374
+ const category = item.status === "failed" ? classifyFailure(failedStep?.error) : void 0;
31375
+ executions.push({
31376
+ runId,
31377
+ caseId: item.caseId,
31378
+ rowId: item.rowId,
31379
+ identity: item.rowId && item.rowId !== "default" ? `${item.caseId}#${item.rowId}` : item.caseId,
31380
+ caseName: item.caseName,
31381
+ status: item.status,
31382
+ category,
31383
+ platforms
31384
+ });
31385
+ }
31386
+ runPoints.push({
31387
+ runId,
31388
+ startedAt: summary.startedAt,
31389
+ status: summary.status,
31390
+ passed: summary.totals.passed,
31391
+ failed: summary.totals.failed,
31392
+ passRate: summary.totals.cases > 0 ? summary.totals.passed / summary.totals.cases : 0
31393
+ });
31394
+ }
31395
+ const byCase = /* @__PURE__ */ new Map();
31396
+ for (const exec2 of executions) {
31397
+ let agg = byCase.get(exec2.identity);
31398
+ if (!agg) {
31399
+ agg = {
31400
+ caseId: exec2.caseId,
31401
+ rowId: exec2.rowId,
31402
+ caseName: exec2.caseName,
31403
+ platforms: [],
31404
+ runs: 0,
31405
+ passed: 0,
31406
+ failed: 0,
31407
+ passRate: 0,
31408
+ lastStatus: "unknown",
31409
+ flaky: false
31410
+ };
31411
+ byCase.set(exec2.identity, agg);
31412
+ }
31413
+ agg.runs++;
31414
+ if (exec2.status === "passed") agg.passed++;
31415
+ else if (exec2.status === "failed") agg.failed++;
31416
+ for (const platform of exec2.platforms) {
31417
+ if (!agg.platforms.includes(platform)) agg.platforms.push(platform);
31418
+ }
31419
+ }
31420
+ for (const agg of byCase.values()) {
31421
+ const identity = agg.rowId && agg.rowId !== "default" ? `${agg.caseId}#${agg.rowId}` : agg.caseId;
31422
+ const last = [...executions].reverse().find((exec2) => exec2.identity === identity);
31423
+ agg.lastStatus = last?.status ?? "unknown";
31424
+ agg.lastCategory = last?.category;
31425
+ agg.passRate = agg.runs > 0 ? agg.passed / agg.runs : 0;
31426
+ agg.flaky = agg.failed > 0 && agg.passed > 0;
31427
+ }
31428
+ const cases = [...byCase.values()];
31429
+ const flaky = cases.filter((item) => item.flaky);
31430
+ const categories = FAILURE_CATEGORIES.map((item) => ({
31431
+ id: item.id,
31432
+ label: item.label,
31433
+ count: executions.filter((exec2) => exec2.status === "failed" && exec2.category === item.id).length
31434
+ })).filter((item) => item.count > 0);
31435
+ const durations = runs.map((item) => item.summary.durationMs).filter((value) => typeof value === "number");
31436
+ return {
31437
+ window: {
31438
+ runs: runs.length,
31439
+ from: runPoints[0]?.startedAt,
31440
+ to: runPoints.at(-1)?.startedAt
31441
+ },
31442
+ kpis: {
31443
+ runs: runs.length,
31444
+ executions: executions.length,
31445
+ overallRate: executions.length > 0 ? executions.filter((exec2) => exec2.status === "passed").length / executions.length : 0,
31446
+ avgDurationMs: durations.length > 0 ? durations.reduce((sum, value) => sum + value, 0) / durations.length : 0,
31447
+ flakyCount: flaky.length
31448
+ },
31449
+ runs: runPoints,
31450
+ cases,
31451
+ platforms: [...new Set(cases.flatMap((item) => item.platforms))].sort(),
31452
+ categories,
31453
+ categoryLabels: Object.fromEntries(FAILURE_CATEGORIES.map((item) => [item.id, item.label]))
31454
+ };
31455
+ }
31456
+ function renderSummaryTrend(runs) {
31457
+ if (runs.length === 0) return '<p class="muted">\u7EDF\u8BA1\u7A97\u53E3\u5185\u6CA1\u6709\u8FD0\u884C\u6570\u636E</p>';
31458
+ const barW = 30;
31459
+ const gap = 8;
31460
+ const chartH = 96;
31461
+ const width = runs.length * (barW + gap) + gap;
31462
+ const height = chartH + 30;
31463
+ const max = Math.max(1, ...runs.map((run) => run.passed + run.failed));
31464
+ const bars = runs.map((run, index) => {
31465
+ const x = gap + index * (barW + gap);
31466
+ const passedH = run.passed / max * chartH;
31467
+ const failedH = run.failed / max * chartH;
31468
+ const passedY = chartH - passedH;
31469
+ const failedY = passedY - failedH;
31470
+ const total = run.passed + run.failed;
31471
+ const rate = total > 0 ? Math.round(run.passed / total * 100) : 0;
31472
+ const label = run.runId.replace(/^\d{8}-/, "");
31473
+ return "<g><title>" + escapeHtml2(run.runId) + ": \u2713" + run.passed + " \u2717" + run.failed + '</title><rect x="' + x + '" y="' + passedY.toFixed(1) + '" width="' + barW + '" height="' + passedH.toFixed(1) + '" fill="#1a7f37" rx="2"></rect><rect x="' + x + '" y="' + failedY.toFixed(1) + '" width="' + barW + '" height="' + failedH.toFixed(1) + '" fill="#cf222e" rx="2"></rect><text x="' + (x + barW / 2) + '" y="' + (chartH + 12) + '" font-size="8" fill="#59636e" text-anchor="middle">' + escapeHtml2(label) + '</text><text x="' + (x + barW / 2) + '" y="' + (chartH + 24) + '" font-size="8" fill="#59636e" text-anchor="middle">' + rate + "%</text></g>";
31474
+ }).join("\n");
31475
+ return '<svg width="' + width + '" height="' + height + '" viewBox="0 0 ' + width + " " + height + '" xmlns="http://www.w3.org/2000/svg">' + bars + "</svg>";
31476
+ }
31477
+ function renderSummaryHtml(data) {
31478
+ const kpis = data.kpis;
31479
+ const rateText = pct(kpis.overallRate);
31480
+ const trendSvg = renderSummaryTrend(data.runs);
31481
+ const catBars = data.categories.map(
31482
+ (item) => '<div class="hbar-row" data-cat="' + item.id + '" title="\u70B9\u51FB\u7B5B\u9009\u7528\u4F8B\u660E\u7EC6"><span class="hbar-label">' + escapeHtml2(item.label) + '</span><span class="hbar-track"><span class="hbar" style="width:' + (kpis.executions > 0 ? Math.round(item.count / kpis.executions * 100) : 0) + '%"></span></span><span class="hbar-num">' + item.count + "</span></div>"
31483
+ ).join("\n");
31484
+ const catHtml = catBars || '<p class="muted">\u7EDF\u8BA1\u7A97\u53E3\u5185\u65E0\u5931\u8D25</p>';
31485
+ const flaky = data.cases.filter((item) => item.flaky);
31486
+ const flakyRows = flaky.map(
31487
+ (item) => '<tr data-platform="' + escapeHtml2(item.platforms.join(" ")) + '" data-q="' + escapeHtml2(item.caseId + " " + item.caseName) + '"><td><code class="flaky-link" data-q="' + escapeHtml2(item.caseId) + '">' + escapeHtml2(item.caseId) + "</code></td><td>" + escapeHtml2(item.caseName) + "</td><td>" + escapeHtml2(item.platforms.join(" + ")) + '</td><td class="num">' + item.failed + " / " + item.runs + '</td><td class="num">' + pct(item.passRate) + '</td><td><span class="badge ' + item.lastStatus + '">' + item.lastStatus + "</span></td></tr>"
31488
+ ).join("\n");
31489
+ const platformChips = ["all", ...data.platforms].map(
31490
+ (platform) => '<span class="chip' + (platform === "all" ? " active" : "") + '" data-platform="' + platform + '">' + (platform === "all" ? "\u5168\u90E8\u5E73\u53F0" : platform) + "</span>"
31491
+ ).join("\n");
31492
+ const detailRows = data.cases.map(
31493
+ (item) => '<tr data-platform="' + escapeHtml2(item.platforms.join(" ")) + '" data-cat="' + escapeHtml2(item.lastCategory ?? "") + '" data-q="' + escapeHtml2(item.caseId + " " + item.caseName) + '" data-pass="' + item.passRate + '" data-runs="' + item.runs + '" data-failed="' + item.failed + '"><td><code>' + escapeHtml2(item.caseId) + "</code></td><td>" + escapeHtml2(item.caseName) + "</td><td>" + escapeHtml2(item.platforms.join(" + ")) + '</td><td class="num">' + item.runs + '</td><td class="num">' + item.passed + '</td><td class="num">' + item.failed + '</td><td><span class="mini-track"><span class="mini ' + (item.failed > 0 ? "warn" : "ok") + '" style="width:' + Math.round(item.passRate * 60) + 'px"></span></span> ' + pct(item.passRate) + "</td><td>" + (item.lastCategory ? escapeHtml2(categoryLabel(item.lastCategory)) : "-") + '</td><td><span class="badge ' + item.lastStatus + '">' + item.lastStatus + "</span></td></tr>"
31494
+ ).join("\n");
31495
+ const range = (data.window.from ?? "").slice(0, 10) + " \u2192 " + (data.window.to ?? "").slice(0, 10);
31496
+ return `<!doctype html>
31497
+ <html lang="zh-CN">
31498
+ <head>
31499
+ <meta charset="utf-8" />
31500
+ <title>TestPilot \u6C47\u603B\u62A5\u544A</title>
31501
+ <style>
31502
+ :root { --ok:#1a7f37; --fail:#cf222e; --line:#d0d7de; --muted:#59636e; }
31503
+ * { box-sizing: border-box; }
31504
+ body { font-family: system-ui, "Segoe UI", sans-serif; margin: 0; color: #1f2328; background: #f6f8fa; }
31505
+ .wrap { max-width: 1120px; margin: 0 auto; padding: 20px 16px 56px; }
31506
+ h1 { font-size: 21px; margin: 8px 0; } h2 { font-size: 15px; margin: 26px 0 8px; }
31507
+ .meta { color: var(--muted); font-size: 12px; margin: 4px 0 12px; }
31508
+ .cards { display: flex; gap: 12px; flex-wrap: wrap; margin: 14px 0; }
31509
+ .card { background: #fff; border: 1px solid var(--line); border-radius: 8px; padding: 12px 18px; min-width: 140px; }
31510
+ .card b { font-size: 22px; display: block; } .card span { color: var(--muted); font-size: 12px; }
31511
+ .row2 { display: flex; gap: 14px; flex-wrap: wrap; align-items: stretch; }
31512
+ .panel { background: #fff; border: 1px solid var(--line); border-radius: 8px; padding: 14px; }
31513
+ .panel h3 { margin: 0 0 8px; font-size: 13px; color: var(--muted); font-weight: 500; }
31514
+ .trend { overflow-x: auto; flex: 1; min-width: 320px; }
31515
+ table { border-collapse: collapse; width: 100%; font-size: 13px; background: #fff; }
31516
+ th, td { border-bottom: 1px solid var(--line); padding: 7px 10px; text-align: left; }
31517
+ th { color: var(--muted); font-weight: 500; cursor: pointer; white-space: nowrap; }
31518
+ th.num, td.num { text-align: right; }
31519
+ code { background: #eef1f4; padding: 1px 5px; border-radius: 4px; font-size: 12px; cursor: pointer; }
31520
+ .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; }
31522
+ .toolbar { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin: 10px 0; }
31523
+ .chip { border: 1px solid var(--line); background: #fff; border-radius: 14px; padding: 3px 12px; font-size: 12px; cursor: pointer; }
31524
+ .chip.active { background: #0969da; color: #fff; border-color: #0969da; }
31525
+ input#q { flex: 1; min-width: 160px; padding: 5px 10px; border: 1px solid var(--line); border-radius: 6px; font-size: 13px; }
31526
+ #count { color: var(--muted); font-size: 12px; margin-left: auto; }
31527
+ .hbar-row { display: flex; align-items: center; gap: 10px; margin: 7px 0; cursor: pointer; }
31528
+ .hbar-label { width: 90px; font-size: 13px; } .hbar-num { width: 30px; text-align: right; font-size: 13px; }
31529
+ .hbar-track { flex: 1; background: #eef1f4; border-radius: 5px; height: 12px; overflow: hidden; }
31530
+ .hbar { display: block; height: 100%; background: var(--fail); }
31531
+ .mini-track { display: inline-block; width: 60px; height: 7px; background: #eef1f4; border-radius: 4px; overflow: hidden; vertical-align: middle; }
31532
+ .mini { display: block; height: 100%; } .mini.ok { background: var(--ok); } .mini.warn { background: #bf8700; }
31533
+ .muted { color: var(--muted); } .hide { display: none !important; }
31534
+ </style>
31535
+ </head>
31536
+ <body>
31537
+ <div class="wrap">
31538
+ <h1>TestPilot \u6C47\u603B\u62A5\u544A</h1>
31539
+ <p class="meta">\u7EDF\u8BA1\u8303\u56F4:\u6700\u8FD1 ${data.window.runs} \u6B21\u8FD0\u884C(${escapeHtml2(range)})\xB7 \u751F\u6210\u4E8E ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}</p>
31540
+
31541
+ <div class="cards">
31542
+ <div class="card"><b>${kpis.runs}</b><span>\u8FD0\u884C\u6B21\u6570</span></div>
31543
+ <div class="card"><b>${kpis.executions}</b><span>\u7528\u4F8B\u6267\u884C\u6B21\u6570</span></div>
31544
+ <div class="card"><b>${rateText}</b><span>\u6574\u4F53\u901A\u8FC7\u7387</span></div>
31545
+ <div class="card"><b>${(kpis.avgDurationMs / 1e3).toFixed(1)}s</b><span>\u5E73\u5747\u8FD0\u884C\u8017\u65F6</span></div>
31546
+ <div class="card"><b>${kpis.flakyCount}</b><span>Flaky \u7528\u4F8B</span></div>
31547
+ </div>
31548
+
31549
+ <h2>\u8D8B\u52BF(\u6BCF\u6B21\u8FD0\u884C\u7ED3\u679C\u4E0E\u901A\u8FC7\u7387,\u60AC\u6D6E\u770B\u8BE6\u60C5)</h2>
31550
+ <div class="panel trend">${trendSvg}</div>
31551
+
31552
+ <h2>\u5931\u8D25\u5206\u7C7B\u5206\u5E03(\u70B9\u51FB\u7B5B\u9009\u660E\u7EC6)</h2>
31553
+ <div class="panel">${catHtml}</div>
31554
+
31555
+ <h2>Flaky \u699C(\u53C8\u8FC7\u53C8\u6302;\u70B9\u7528\u4F8B\u540D\u5B9A\u4F4D\u660E\u7EC6)</h2>
31556
+ <div class="panel"><table>
31557
+ <tr><th>\u7528\u4F8B</th><th>\u540D\u79F0</th><th>\u5E73\u53F0</th><th class="num">\u5931\u8D25 / \u53C2\u4E0E</th><th class="num">\u901A\u8FC7\u7387</th><th>\u6700\u8FD1\u72B6\u6001</th></tr>
31558
+ ${flakyRows || '<tr><td colspan="6" class="muted">\u7EDF\u8BA1\u7A97\u53E3\u5185\u65E0 Flaky \u7528\u4F8B</td></tr>'}
31559
+ </table></div>
31560
+
31561
+ <h2>\u7528\u4F8B\u660E\u7EC6</h2>
31562
+ <div class="toolbar">
31563
+ ${platformChips}
31564
+ <input id="q" placeholder="\u641C\u7D22\u7528\u4F8B id / \u540D\u79F0\u2026" />
31565
+ <span id="count"></span>
31566
+ </div>
31567
+ <table id="detail">
31568
+ <thead><tr>
31569
+ <th>\u7528\u4F8B</th><th>\u540D\u79F0</th><th>\u5E73\u53F0</th>
31570
+ <th class="num" data-sort="runs">\u8FD0\u884C\u6B21\u6570</th><th class="num" data-sort="passed">\u901A\u8FC7</th><th class="num" data-sort="failed">\u5931\u8D25</th>
31571
+ <th data-sort="passRate">\u901A\u8FC7\u7387 \u25BE</th><th>\u6700\u8FD1\u5206\u7C7B</th><th>\u6700\u8FD1\u72B6\u6001</th>
31572
+ </tr></thead>
31573
+ <tbody>${detailRows}</tbody>
31574
+ </table>
31575
+ </div>
31576
+
31577
+ <script id="summary-data" type="application/json">${JSON.stringify(data).replaceAll("<", "\\u003c")}</script>
31578
+ <script>
31579
+ (function () {
31580
+ var rows = Array.prototype.slice.call(document.querySelectorAll('#detail tbody tr'));
31581
+ var state = { platform: 'all', cat: 'all', q: '', sortKey: 'passRate', sortDir: -1 };
31582
+
31583
+ function apply() {
31584
+ var shown = 0;
31585
+ rows.forEach(function (row) {
31586
+ var show = true;
31587
+ if (state.platform !== 'all' && row.getAttribute('data-platform').split(' ').indexOf(state.platform) === -1) show = false;
31588
+ if (state.cat !== 'all' && (row.getAttribute('data-cat') || '') !== state.cat) show = false;
31589
+ if (state.q && row.getAttribute('data-q').toLowerCase().indexOf(state.q.toLowerCase()) === -1) show = false;
31590
+ row.classList.toggle('hide', !show);
31591
+ if (show) shown++;
31592
+ });
31593
+ document.getElementById('count').textContent = shown + ' / ' + rows.length + ' \u7528\u4F8B';
31594
+ }
31595
+
31596
+ document.querySelectorAll('.chip[data-platform]').forEach(function (chip) {
31597
+ chip.addEventListener('click', function () {
31598
+ document.querySelectorAll('.chip[data-platform]').forEach(function (c) { c.classList.remove('active'); });
31599
+ chip.classList.add('active');
31600
+ state.platform = chip.getAttribute('data-platform');
31601
+ apply();
31602
+ });
31603
+ });
31604
+ document.querySelectorAll('.hbar-row').forEach(function (bar) {
31605
+ bar.addEventListener('click', function () {
31606
+ state.cat = bar.getAttribute('data-cat');
31607
+ apply();
31608
+ });
31609
+ });
31610
+ var q = document.getElementById('q');
31611
+ q.addEventListener('input', function () { state.q = q.value.trim(); apply(); });
31612
+
31613
+ document.querySelectorAll('#detail th[data-sort]').forEach(function (th) {
31614
+ th.addEventListener('click', function () {
31615
+ var key = th.getAttribute('data-sort');
31616
+ state.sortDir = state.sortKey === key ? -state.sortDir : -1;
31617
+ state.sortKey = key;
31618
+ var sorted = rows.slice().sort(function (a, b) {
31619
+ return (Number(a.getAttribute('data-' + key)) - Number(b.getAttribute('data-' + key))) * state.sortDir;
31620
+ });
31621
+ var tbody = document.querySelector('#detail tbody');
31622
+ sorted.forEach(function (row) { tbody.appendChild(row); });
31623
+ });
31624
+ });
31625
+
31626
+ apply();
31627
+ })();
31628
+ </script>
31629
+ </body>
31630
+ </html>`;
31631
+ }
31632
+ async function writeSummaryReport(artifactsDir, data) {
31633
+ const dir = resolve5(artifactsDir);
31634
+ await mkdir4(dir, { recursive: true });
31635
+ const htmlPath = join9(dir, "summary.html");
31636
+ const jsonPath = join9(dir, "summary.json");
31637
+ await writeFile5(jsonPath, JSON.stringify(data, null, 2), "utf8");
31638
+ await writeFile5(htmlPath, renderSummaryHtml(data), "utf8");
31639
+ return { html: htmlPath, json: jsonPath };
31640
+ }
30816
31641
 
30817
31642
  // ../../packages/sdk/src/runs.ts
30818
- import { readFile as readFile5, readdir as readdir3 } from "node:fs/promises";
30819
- import { isAbsolute as isAbsolute2, join as join15 } from "node:path";
31643
+ import { readFile as readFile7, readdir as readdir4 } from "node:fs/promises";
31644
+ import { isAbsolute as isAbsolute3, join as join17 } from "node:path";
30820
31645
 
30821
31646
  // ../../packages/execution-engine/src/artifacts/index.ts
30822
- import { mkdir as mkdir8 } from "node:fs/promises";
30823
- import { join as join11 } from "node:path";
31647
+ import { mkdir as mkdir9 } from "node:fs/promises";
31648
+ import { join as join13 } from "node:path";
30824
31649
 
30825
31650
  // ../../packages/evidence/src/log.ts
30826
- import { appendFile, mkdir as mkdir4 } from "node:fs/promises";
30827
- import { dirname as dirname2 } from "node:path";
31651
+ import { appendFile, mkdir as mkdir5 } from "node:fs/promises";
31652
+ import { dirname as dirname4 } from "node:path";
30828
31653
  var RunLogger = class {
30829
31654
  constructor(filePath) {
30830
31655
  this.filePath = filePath;
@@ -30839,7 +31664,7 @@ var RunLogger = class {
30839
31664
  return this.write("ERROR", message);
30840
31665
  }
30841
31666
  async write(level, message) {
30842
- await mkdir4(dirname2(this.filePath), { recursive: true });
31667
+ await mkdir5(dirname4(this.filePath), { recursive: true });
30843
31668
  const line = `${(/* @__PURE__ */ new Date()).toISOString()} ${level} ${message}
30844
31669
  `;
30845
31670
  await appendFile(this.filePath, line, "utf8");
@@ -30847,32 +31672,32 @@ var RunLogger = class {
30847
31672
  };
30848
31673
 
30849
31674
  // ../../packages/evidence/src/screenshot.ts
30850
- import { mkdir as mkdir5, writeFile as writeFile5 } from "node:fs/promises";
30851
- import { dirname as dirname3, join as join8 } from "node:path";
31675
+ import { mkdir as mkdir6, writeFile as writeFile6 } from "node:fs/promises";
31676
+ import { dirname as dirname5, join as join10 } from "node:path";
30852
31677
  async function saveScreenshot(dir, fileName, data) {
30853
- const file2 = join8(dir, fileName);
30854
- await mkdir5(dirname3(file2), { recursive: true });
30855
- await writeFile5(file2, data);
31678
+ const file2 = join10(dir, fileName);
31679
+ await mkdir6(dirname5(file2), { recursive: true });
31680
+ await writeFile6(file2, data);
30856
31681
  return file2;
30857
31682
  }
30858
31683
 
30859
31684
  // ../../packages/evidence/src/trace.ts
30860
- import { mkdir as mkdir6, writeFile as writeFile6 } from "node:fs/promises";
30861
- import { dirname as dirname4, join as join9 } from "node:path";
31685
+ import { mkdir as mkdir7, writeFile as writeFile7 } from "node:fs/promises";
31686
+ import { dirname as dirname6, join as join11 } from "node:path";
30862
31687
  async function saveTrace(dir, fileName, data) {
30863
- const file2 = join9(dir, fileName);
30864
- await mkdir6(dirname4(file2), { recursive: true });
30865
- await writeFile6(file2, data);
31688
+ const file2 = join11(dir, fileName);
31689
+ await mkdir7(dirname6(file2), { recursive: true });
31690
+ await writeFile7(file2, data);
30866
31691
  return file2;
30867
31692
  }
30868
31693
 
30869
31694
  // ../../packages/evidence/src/video.ts
30870
- import { mkdir as mkdir7, writeFile as writeFile7 } from "node:fs/promises";
30871
- import { dirname as dirname5, join as join10 } from "node:path";
31695
+ import { mkdir as mkdir8, writeFile as writeFile8 } from "node:fs/promises";
31696
+ import { dirname as dirname7, join as join12 } from "node:path";
30872
31697
  async function saveVideo(dir, fileName, data) {
30873
- const file2 = join10(dir, fileName);
30874
- await mkdir7(dirname5(file2), { recursive: true });
30875
- await writeFile7(file2, data);
31698
+ const file2 = join12(dir, fileName);
31699
+ await mkdir8(dirname7(file2), { recursive: true });
31700
+ await writeFile8(file2, data);
30876
31701
  return file2;
30877
31702
  }
30878
31703
 
@@ -30880,11 +31705,11 @@ async function saveVideo(dir, fileName, data) {
30880
31705
  var ArtifactManager = class {
30881
31706
  constructor(runDir) {
30882
31707
  this.runDir = runDir;
30883
- this.screenshotsDir = join11(runDir, "screenshots");
30884
- this.videosDir = join11(runDir, "videos");
30885
- this.tracesDir = join11(runDir, "traces");
30886
- this.logsDir = join11(runDir, "logs");
30887
- this.logger = new RunLogger(join11(this.logsDir, "run.log"));
31708
+ this.screenshotsDir = join13(runDir, "screenshots");
31709
+ this.videosDir = join13(runDir, "videos");
31710
+ this.tracesDir = join13(runDir, "traces");
31711
+ this.logsDir = join13(runDir, "logs");
31712
+ this.logger = new RunLogger(join13(this.logsDir, "run.log"));
30888
31713
  }
30889
31714
  screenshotsDir;
30890
31715
  videosDir;
@@ -30892,8 +31717,8 @@ var ArtifactManager = class {
30892
31717
  logsDir;
30893
31718
  logger;
30894
31719
  async prepare() {
30895
- await mkdir8(this.screenshotsDir, { recursive: true });
30896
- await mkdir8(this.logsDir, { recursive: true });
31720
+ await mkdir9(this.screenshotsDir, { recursive: true });
31721
+ await mkdir9(this.logsDir, { recursive: true });
30897
31722
  }
30898
31723
  /** 保存步骤截图,返回相对 run 目录的路径(screenshots/xxx.png,统一正斜杠供报告链接使用) */
30899
31724
  async saveStepScreenshot(fileName, data) {
@@ -30921,6 +31746,11 @@ var ExecutionContext = class {
30921
31746
  get(name) {
30922
31747
  return this.variables.get(name);
30923
31748
  }
31749
+ seed(values) {
31750
+ for (const [name, value] of Object.entries(values)) {
31751
+ this.variables.set(name, value);
31752
+ }
31753
+ }
30924
31754
  /**
30925
31755
  * 将字符串中的 ${name} 替换为已提取的变量值;未定义的引用保持原样(校验阶段已拦截)。
30926
31756
  * 名字允许点号,用于引擎注入的 account.username 一类凭据变量(用户 extract 变量名不含点号)。
@@ -30957,8 +31787,8 @@ var ExecutionContext = class {
30957
31787
  };
30958
31788
 
30959
31789
  // ../../packages/execution-engine/src/lifecycle/index.ts
30960
- import { mkdir as mkdir9, readdir as readdir2 } from "node:fs/promises";
30961
- import { join as join12 } from "node:path";
31790
+ import { mkdir as mkdir10, readdir as readdir3 } from "node:fs/promises";
31791
+ import { join as join14 } from "node:path";
30962
31792
  function dateStamp(date5 = /* @__PURE__ */ new Date()) {
30963
31793
  const year = date5.getFullYear();
30964
31794
  const month = String(date5.getMonth() + 1).padStart(2, "0");
@@ -30967,7 +31797,7 @@ function dateStamp(date5 = /* @__PURE__ */ new Date()) {
30967
31797
  }
30968
31798
  async function allocateRunId(runsRoot2) {
30969
31799
  const stamp = dateStamp();
30970
- const existing = await readdir2(runsRoot2).catch(() => []);
31800
+ const existing = await readdir3(runsRoot2).catch(() => []);
30971
31801
  let max = 0;
30972
31802
  for (const name of existing) {
30973
31803
  const match = /^(\d{8})-(\d{3})$/.exec(name);
@@ -30978,14 +31808,14 @@ async function allocateRunId(runsRoot2) {
30978
31808
  return `${stamp}-${String(max + 1).padStart(3, "0")}`;
30979
31809
  }
30980
31810
  async function ensureRunDir(runsRoot2, runId) {
30981
- const runDir = join12(runsRoot2, runId);
30982
- await mkdir9(runDir, { recursive: true });
31811
+ const runDir = join14(runsRoot2, runId);
31812
+ await mkdir10(runDir, { recursive: true });
30983
31813
  return runDir;
30984
31814
  }
30985
31815
 
30986
31816
  // ../../packages/execution-engine/src/events/index.ts
30987
- import { appendFile as appendFile2, mkdir as mkdir10 } from "node:fs/promises";
30988
- import { dirname as dirname6 } from "node:path";
31817
+ import { appendFile as appendFile2, mkdir as mkdir11 } from "node:fs/promises";
31818
+ import { dirname as dirname8 } from "node:path";
30989
31819
  var EventBus = class {
30990
31820
  subscribers = [];
30991
31821
  subscribe(subscriber) {
@@ -30999,7 +31829,7 @@ var EventBus = class {
30999
31829
  };
31000
31830
  function ndjsonSubscriber(filePath) {
31001
31831
  return async (event) => {
31002
- await mkdir10(dirname6(filePath), { recursive: true });
31832
+ await mkdir11(dirname8(filePath), { recursive: true });
31003
31833
  await appendFile2(filePath, `${JSON.stringify(event)}
31004
31834
  `, "utf8");
31005
31835
  };
@@ -31080,11 +31910,19 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
31080
31910
  status: "passed",
31081
31911
  durationMs: 0
31082
31912
  };
31913
+ let captureStarted = false;
31083
31914
  try {
31084
31915
  const locator = step.locator ? {
31085
31916
  text: step.locator.text !== void 0 ? context.resolve(step.locator.text) : void 0,
31086
31917
  css: step.locator.css !== void 0 ? context.resolve(step.locator.css) : void 0
31087
31918
  } : void 0;
31919
+ if (step.expectRequests) {
31920
+ if (!adapter.startRequestCapture || !adapter.stopRequestCapture) {
31921
+ throw new Error(`\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301\u8BF7\u6C42\u6B21\u6570\u65AD\u8A00`);
31922
+ }
31923
+ await adapter.startRequestCapture();
31924
+ captureStarted = true;
31925
+ }
31088
31926
  switch (step.action) {
31089
31927
  case "launch":
31090
31928
  await adapter.launch();
@@ -31155,7 +31993,29 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
31155
31993
  throw new Error(`\u4E0D\u652F\u6301\u7684 action:${String(exhaustive)}`);
31156
31994
  }
31157
31995
  }
31996
+ if (step.expectRequests) {
31997
+ const windowMs = Math.max(...step.expectRequests.map((item) => item.windowMs));
31998
+ if (windowMs > 0) await delay(windowMs);
31999
+ const requests = await adapter.stopRequestCapture();
32000
+ captureStarted = false;
32001
+ result.requestAssertions = buildRequestAssertions(step, requests, context);
32002
+ const mismatches = result.requestAssertions.filter((item) => item.actualCount !== item.count);
32003
+ if (mismatches.length > 0) {
32004
+ throw new Error(
32005
+ mismatches.map(
32006
+ (item) => `\u8BF7\u6C42\u6B21\u6570\u4E0D\u5339\u914D:${item.method ?? "*"} ${item.urlContains},\u5B9E\u9645 ${item.actualCount},\u671F\u671B ${item.count}`
32007
+ ).join("; ")
32008
+ );
32009
+ }
32010
+ }
31158
32011
  } catch (err) {
32012
+ if (captureStarted) {
32013
+ try {
32014
+ await adapter.stopRequestCapture?.();
32015
+ } catch {
32016
+ }
32017
+ captureStarted = false;
32018
+ }
31159
32019
  result.status = "failed";
31160
32020
  result.error = err instanceof Error ? err.message : String(err);
31161
32021
  try {
@@ -31200,92 +32060,195 @@ function resolveDeep(value, context) {
31200
32060
  function snippet(text, limit = SNIPPET_LIMIT) {
31201
32061
  return text.length > limit ? `${text.slice(0, limit)}\u2026` : text;
31202
32062
  }
32063
+ function buildRequestAssertions(step, requests, context) {
32064
+ return (step.expectRequests ?? []).map((expectation) => {
32065
+ const method = expectation.method?.toUpperCase();
32066
+ const urlContains = context.resolve(expectation.urlContains);
32067
+ const matches = requests.filter(
32068
+ (request) => (!method || request.method.toUpperCase() === method) && request.url.includes(urlContains)
32069
+ );
32070
+ return {
32071
+ method,
32072
+ urlContains: expectation.urlContains,
32073
+ count: expectation.count,
32074
+ actualCount: matches.length,
32075
+ windowMs: expectation.windowMs,
32076
+ requests: matches.slice(0, 20).map((request) => ({
32077
+ method: request.method.toUpperCase(),
32078
+ url: sanitizeRequestUrl(request.url)
32079
+ }))
32080
+ };
32081
+ });
32082
+ }
32083
+ function sanitizeRequestUrl(url2) {
32084
+ const queryIndex = url2.indexOf("?");
32085
+ const safe = queryIndex >= 0 ? `${url2.slice(0, queryIndex)}?<redacted>` : url2;
32086
+ return snippet(safe, 300);
32087
+ }
32088
+ function delay(milliseconds) {
32089
+ return new Promise((resolve7) => setTimeout(resolve7, milliseconds));
32090
+ }
31203
32091
 
31204
32092
  // ../../packages/execution-engine/src/engine/execution-engine.ts
31205
32093
  var ExecutionEngine = class {
31206
32094
  async runCase(data, options) {
31207
- const { runId, file: file2, artifacts, bus, resolver } = options;
32095
+ const { runId, file: file2, artifacts, bus, resolver, rowId, rowIndex } = options;
31208
32096
  const startedAt = /* @__PURE__ */ new Date();
31209
32097
  const context = new ExecutionContext();
32098
+ context.seed(options.initialVariables);
31210
32099
  if (data.accountRef && options.accounts?.[data.accountRef] !== void 0) {
31211
32100
  context.seedAccount(options.accounts[data.accountRef]);
31212
32101
  }
31213
32102
  const steps = [];
32103
+ const warnings = [];
31214
32104
  const evidenceAdapters = /* @__PURE__ */ new Map();
31215
32105
  let caseError;
31216
- await bus.emit({ type: "case-started", runId, caseId: data.id, file: file2, ts: startedAt.toISOString() });
31217
- await artifacts.logger.info(`case ${data.id} started (${file2})`);
31218
- for (const [index, step] of data.steps.entries()) {
32106
+ const executionId = safeArtifactName(`${data.id}-${rowId}`);
32107
+ await bus.emit({
32108
+ type: "case-started",
32109
+ runId,
32110
+ caseId: data.id,
32111
+ rowId,
32112
+ file: file2,
32113
+ ts: startedAt.toISOString()
32114
+ });
32115
+ await artifacts.logger.info(`case ${data.id}#${rowId} started (${file2})`);
32116
+ if (options.missingDependencies.length > 0) {
32117
+ const finishedAt2 = /* @__PURE__ */ new Date();
32118
+ const skipped = {
32119
+ caseId: data.id,
32120
+ caseName: data.name,
32121
+ file: file2,
32122
+ rowId,
32123
+ rowIndex,
32124
+ status: "skipped",
32125
+ skipReason: "dependency-not-ready",
32126
+ missingDependencies: options.missingDependencies,
32127
+ steps,
32128
+ startedAt: startedAt.toISOString(),
32129
+ finishedAt: finishedAt2.toISOString(),
32130
+ durationMs: finishedAt2.getTime() - startedAt.getTime()
32131
+ };
31219
32132
  await bus.emit({
31220
- type: "step-started",
32133
+ type: "case-finished",
31221
32134
  runId,
31222
32135
  caseId: data.id,
31223
- index,
31224
- target: step.target,
31225
- action: step.action,
31226
- ts: (/* @__PURE__ */ new Date()).toISOString()
32136
+ rowId,
32137
+ status: "skipped",
32138
+ durationMs: skipped.durationMs,
32139
+ ts: finishedAt2.toISOString()
31227
32140
  });
31228
- let result;
31229
- try {
31230
- const adapter = await (await resolver.session(step.target)).get();
31231
- if (adapter.startEvidence && !evidenceAdapters.has(step.target)) {
31232
- try {
31233
- await adapter.startEvidence(data.id);
31234
- evidenceAdapters.set(step.target, adapter);
31235
- } catch (err) {
31236
- await artifacts.logger.info(
31237
- `evidence start failed for ${step.target}: ${err instanceof Error ? err.message : String(err)}`
31238
- );
32141
+ await artifacts.logger.info(
32142
+ `case ${data.id}#${rowId} skipped: missing ${options.missingDependencies.join(", ")}`
32143
+ );
32144
+ return skipped;
32145
+ }
32146
+ const runPhase = async (phase, phaseSteps) => {
32147
+ let passed = true;
32148
+ for (const [index, step] of phaseSteps.entries()) {
32149
+ await bus.emit({
32150
+ type: "step-started",
32151
+ runId,
32152
+ caseId: data.id,
32153
+ rowId,
32154
+ phase,
32155
+ index,
32156
+ target: step.target,
32157
+ action: step.action,
32158
+ ts: (/* @__PURE__ */ new Date()).toISOString()
32159
+ });
32160
+ let result;
32161
+ try {
32162
+ const adapter = await (await resolver.session(step.target)).get();
32163
+ if (adapter.startEvidence && !evidenceAdapters.has(step.target)) {
32164
+ try {
32165
+ await adapter.startEvidence(executionId);
32166
+ evidenceAdapters.set(step.target, adapter);
32167
+ } catch (err) {
32168
+ await artifacts.logger.info(
32169
+ `evidence start failed for ${step.target}: ${err instanceof Error ? err.message : String(err)}`
32170
+ );
32171
+ }
31239
32172
  }
32173
+ result = await executeStep(step, index, adapter, context, artifacts, {
32174
+ screenshotName: `${executionId}-${phase}-${String(index).padStart(2, "0")}`
32175
+ });
32176
+ } catch (err) {
32177
+ result = {
32178
+ index,
32179
+ target: step.target,
32180
+ action: step.action,
32181
+ status: "failed",
32182
+ durationMs: 0,
32183
+ error: err instanceof Error ? err.message : String(err)
32184
+ };
31240
32185
  }
31241
- result = await executeStep(step, index, adapter, context, artifacts, {
31242
- screenshotName: `${data.id}-step-${String(index).padStart(2, "0")}`
31243
- });
31244
- } catch (err) {
31245
- result = {
32186
+ result.phase = phase;
32187
+ if (phase === "teardown" && result.status === "failed") {
32188
+ result.status = "warning";
32189
+ warnings.push(result.error ?? `teardown step ${index} failed`);
32190
+ }
32191
+ steps.push(result);
32192
+ await artifacts.logger.info(
32193
+ `${phase} step ${index} ${step.action} -> ${result.status}${result.error ? ` - ${result.error}` : ""}`
32194
+ );
32195
+ await bus.emit({
32196
+ type: "step-finished",
32197
+ runId,
32198
+ caseId: data.id,
32199
+ rowId,
32200
+ phase,
31246
32201
  index,
31247
32202
  target: step.target,
31248
32203
  action: step.action,
31249
- status: "failed",
31250
- durationMs: 0,
31251
- error: err instanceof Error ? err.message : String(err)
31252
- };
31253
- }
31254
- steps.push(result);
31255
- await artifacts.logger.info(`step ${index} ${step.action} -> ${result.status}${result.error ? ` - ${result.error}` : ""}`);
31256
- await bus.emit({
31257
- type: "step-finished",
31258
- runId,
31259
- caseId: data.id,
31260
- index,
31261
- target: step.target,
31262
- action: step.action,
31263
- status: result.status,
31264
- durationMs: result.durationMs,
31265
- error: result.error,
31266
- ts: (/* @__PURE__ */ new Date()).toISOString()
31267
- });
31268
- if (result.status === "failed") {
31269
- caseError = result.error;
31270
- for (const [skipIndex, skippedStep] of data.steps.entries()) {
31271
- if (skipIndex <= index) continue;
31272
- steps.push({
31273
- index: skipIndex,
31274
- target: skippedStep.target,
31275
- action: skippedStep.action,
31276
- status: "skipped",
31277
- durationMs: 0
31278
- });
32204
+ status: result.status,
32205
+ durationMs: result.durationMs,
32206
+ error: result.error,
32207
+ ts: (/* @__PURE__ */ new Date()).toISOString()
32208
+ });
32209
+ if (result.status === "failed") {
32210
+ passed = false;
32211
+ caseError = result.error;
32212
+ for (const [skipIndex, skippedStep] of phaseSteps.entries()) {
32213
+ if (skipIndex <= index) continue;
32214
+ steps.push({
32215
+ index: skipIndex,
32216
+ phase,
32217
+ target: skippedStep.target,
32218
+ action: skippedStep.action,
32219
+ status: "skipped",
32220
+ durationMs: 0
32221
+ });
32222
+ }
32223
+ break;
31279
32224
  }
31280
- break;
32225
+ }
32226
+ return passed;
32227
+ };
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()) {
32233
+ steps.push({
32234
+ index,
32235
+ phase: "steps",
32236
+ target: step.target,
32237
+ action: step.action,
32238
+ status: "skipped",
32239
+ durationMs: 0
32240
+ });
31281
32241
  }
31282
32242
  }
32243
+ await runPhase("teardown", data.teardown ?? []);
31283
32244
  const finishedAt = /* @__PURE__ */ new Date();
31284
32245
  const status = caseError ? "failed" : "passed";
31285
32246
  const caseResult = {
31286
32247
  caseId: data.id,
31287
32248
  caseName: data.name,
31288
32249
  file: file2,
32250
+ rowId,
32251
+ rowIndex,
31289
32252
  status,
31290
32253
  steps,
31291
32254
  startedAt: startedAt.toISOString(),
@@ -31293,15 +32256,16 @@ var ExecutionEngine = class {
31293
32256
  durationMs: finishedAt.getTime() - startedAt.getTime()
31294
32257
  };
31295
32258
  if (caseError) caseResult.error = caseError;
32259
+ if (warnings.length > 0) caseResult.warnings = warnings;
31296
32260
  for (const [target, adapter] of evidenceAdapters) {
31297
32261
  if (!adapter.stopEvidence) continue;
31298
32262
  try {
31299
- const evidence = await adapter.stopEvidence(data.id);
32263
+ const evidence = await adapter.stopEvidence(executionId);
31300
32264
  if (evidence.video) {
31301
- caseResult.video = await artifacts.saveVideo(`${data.id}-${target}.webm`, evidence.video);
32265
+ caseResult.video = await artifacts.saveVideo(`${executionId}-${target}.webm`, evidence.video);
31302
32266
  }
31303
32267
  if (evidence.trace) {
31304
- caseResult.trace = await artifacts.saveTrace(`${data.id}-${target}.zip`, evidence.trace);
32268
+ caseResult.trace = await artifacts.saveTrace(`${executionId}-${target}.zip`, evidence.trace);
31305
32269
  }
31306
32270
  } catch (err) {
31307
32271
  await artifacts.logger.info(
@@ -31313,33 +32277,45 @@ var ExecutionEngine = class {
31313
32277
  type: "case-finished",
31314
32278
  runId,
31315
32279
  caseId: data.id,
32280
+ rowId,
31316
32281
  status,
31317
32282
  durationMs: finishedAt.getTime() - startedAt.getTime(),
31318
32283
  ts: finishedAt.toISOString()
31319
32284
  });
31320
- await artifacts.logger.info(`case ${data.id} ${status}`);
32285
+ await artifacts.logger.info(`case ${data.id}#${rowId} ${status}`);
31321
32286
  return caseResult;
31322
32287
  }
31323
32288
  };
32289
+ function safeArtifactName(value) {
32290
+ return value.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "case";
32291
+ }
31324
32292
 
31325
32293
  // ../../packages/execution-engine/src/engine/result-collector.ts
31326
32294
  function buildRunSummary(runId, startedAt, cases, options = {}) {
31327
32295
  const finishedAt = /* @__PURE__ */ new Date();
31328
32296
  const totals = {
32297
+ templates: options.templates ?? new Set(cases.map((item) => item.caseId)).size,
31329
32298
  cases: cases.length,
31330
32299
  passed: cases.filter((item) => item.status === "passed").length,
31331
32300
  failed: cases.filter((item) => item.status === "failed").length,
32301
+ skipped: cases.filter((item) => item.status === "skipped").length,
32302
+ warnings: 0,
31332
32303
  steps: 0,
31333
32304
  stepsPassed: 0,
31334
32305
  stepsFailed: 0,
31335
- stepsSkipped: 0
32306
+ stepsSkipped: 0,
32307
+ stepsWarning: 0
31336
32308
  };
31337
32309
  for (const item of cases) {
31338
32310
  for (const step of item.steps) {
31339
32311
  totals.steps++;
31340
32312
  if (step.status === "passed") totals.stepsPassed++;
31341
32313
  else if (step.status === "failed") totals.stepsFailed++;
31342
- else totals.stepsSkipped++;
32314
+ else if (step.status === "skipped") totals.stepsSkipped++;
32315
+ else {
32316
+ totals.stepsWarning++;
32317
+ totals.warnings++;
32318
+ }
31343
32319
  }
31344
32320
  }
31345
32321
  return {
@@ -31347,7 +32323,7 @@ function buildRunSummary(runId, startedAt, cases, options = {}) {
31347
32323
  startedAt: startedAt.toISOString(),
31348
32324
  finishedAt: finishedAt.toISOString(),
31349
32325
  durationMs: finishedAt.getTime() - startedAt.getTime(),
31350
- status: totals.failed > 0 ? "failed" : "passed",
32326
+ status: totals.failed > 0 ? "failed" : totals.passed > 0 || totals.cases === 0 ? "passed" : "skipped",
31351
32327
  ...options.cancelled ? { cancelled: true } : {},
31352
32328
  cases,
31353
32329
  totals
@@ -31355,8 +32331,8 @@ function buildRunSummary(runId, startedAt, cases, options = {}) {
31355
32331
  }
31356
32332
 
31357
32333
  // ../../packages/execution-engine/src/engine/test-runner.ts
31358
- import { writeFile as writeFile8 } from "node:fs/promises";
31359
- import { join as join13, resolve as resolve4 } from "node:path";
32334
+ import { writeFile as writeFile9 } from "node:fs/promises";
32335
+ import { join as join15, resolve as resolve6 } from "node:path";
31360
32336
  var TestRunner = class {
31361
32337
  resolver;
31362
32338
  runsRoot;
@@ -31366,7 +32342,7 @@ var TestRunner = class {
31366
32342
  accounts;
31367
32343
  constructor(options) {
31368
32344
  this.resolver = options.resolver;
31369
- this.runsRoot = resolve4(options.runsRoot ?? join13(process.cwd(), ".testpilot", "artifacts", "runs"));
32345
+ this.runsRoot = resolve6(options.runsRoot ?? join15(process.cwd(), ".testpilot", "artifacts", "runs"));
31370
32346
  this.customRunId = options.runId;
31371
32347
  this.extraSubscriber = options.onEvent;
31372
32348
  this.signal = options.signal;
@@ -31379,7 +32355,7 @@ var TestRunner = class {
31379
32355
  const artifacts = new ArtifactManager(runDir);
31380
32356
  await artifacts.prepare();
31381
32357
  const bus = new EventBus();
31382
- bus.subscribe(ndjsonSubscriber(join13(runDir, "events.ndjson")));
32358
+ bus.subscribe(ndjsonSubscriber(join15(runDir, "events.ndjson")));
31383
32359
  if (this.extraSubscriber) bus.subscribe(this.extraSubscriber);
31384
32360
  await bus.emit({ type: "run-started", runId, totalCases: cases.length, ts: startedAt.toISOString() });
31385
32361
  await artifacts.logger.info(`run ${runId} started with ${cases.length} case(s)`);
@@ -31394,6 +32370,11 @@ var TestRunner = class {
31394
32370
  await engine.runCase(item.data, {
31395
32371
  runId,
31396
32372
  file: item.file,
32373
+ templateId: item.templateId ?? item.data.id,
32374
+ rowId: item.rowId ?? "default",
32375
+ rowIndex: item.rowIndex ?? 0,
32376
+ initialVariables: item.initialVariables ?? {},
32377
+ missingDependencies: item.missingDependencies ?? [],
31397
32378
  artifacts,
31398
32379
  bus,
31399
32380
  resolver: this.resolver,
@@ -31403,7 +32384,8 @@ var TestRunner = class {
31403
32384
  }
31404
32385
  const cancelled = this.signal?.aborted === true;
31405
32386
  await this.resolver.closeAll();
31406
- const summary = buildRunSummary(runId, startedAt, results, { cancelled });
32387
+ const templates = new Set(cases.map((item) => item.templateId ?? item.data.id)).size;
32388
+ const summary = buildRunSummary(runId, startedAt, results, { cancelled, templates });
31407
32389
  await bus.emit({
31408
32390
  type: "run-finished",
31409
32391
  runId,
@@ -31412,7 +32394,7 @@ var TestRunner = class {
31412
32394
  ts: (/* @__PURE__ */ new Date()).toISOString()
31413
32395
  });
31414
32396
  await artifacts.logger.info(`run ${runId} finished: ${summary.status}`);
31415
- await writeFile8(join13(runDir, RESULT_FILE), JSON.stringify(summary, null, 2), "utf8");
32397
+ await writeFile9(join15(runDir, RESULT_FILE), JSON.stringify(summary, null, 2), "utf8");
31416
32398
  return summary;
31417
32399
  }
31418
32400
  };
@@ -31445,10 +32427,10 @@ async function openCaseContext(bundle, options, evidenceTmpDir) {
31445
32427
  }
31446
32428
 
31447
32429
  // ../../packages/adapter-playwright/src/page.ts
31448
- import { existsSync as existsSync2 } from "node:fs";
31449
- import { mkdtemp, readFile as readFile4, rm } from "node:fs/promises";
32430
+ import { existsSync as existsSync3 } from "node:fs";
32431
+ import { mkdtemp, readFile as readFile6, rm } from "node:fs/promises";
31450
32432
  import { tmpdir } from "node:os";
31451
- import { join as join14 } from "node:path";
32433
+ import { join as join16 } from "node:path";
31452
32434
  var PlaywrightAdapter = class {
31453
32435
  constructor(bundle, options = {}) {
31454
32436
  this.bundle = bundle;
@@ -31456,13 +32438,14 @@ var PlaywrightAdapter = class {
31456
32438
  }
31457
32439
  target = "web";
31458
32440
  current;
32441
+ requestCapture;
31459
32442
  /** 浏览器在 create() 时已启动;context/page 按用例惰性创建,launch 保持幂等 */
31460
32443
  async launch() {
31461
32444
  }
31462
32445
  /** 开始用例级取证:为本用例创建独立 context(录屏 + trace) */
31463
32446
  async startEvidence(_caseId) {
31464
32447
  await this.closeContextQuietly();
31465
- const evidenceTmpDir = await mkdtemp(join14(tmpdir(), "testpilot-pw-"));
32448
+ const evidenceTmpDir = await mkdtemp(join16(tmpdir(), "testpilot-pw-"));
31466
32449
  this.current = {
31467
32450
  ...await openCaseContext(this.bundle, this.options, evidenceTmpDir),
31468
32451
  evidenceTmpDir
@@ -31470,18 +32453,36 @@ var PlaywrightAdapter = class {
31470
32453
  }
31471
32454
  /** 结束取证:trace 落 zip、录屏在 context 关闭后完成,返回二者字节 */
31472
32455
  async stopEvidence(_caseId) {
32456
+ await this.stopRequestCapture().catch(() => []);
31473
32457
  if (!this.current) return {};
31474
32458
  const { context, page, evidenceTmpDir } = this.current;
31475
32459
  this.current = void 0;
31476
- const traceZipPath = join14(evidenceTmpDir ?? tmpdir(), "trace.zip");
32460
+ const traceZipPath = join16(evidenceTmpDir ?? tmpdir(), "trace.zip");
31477
32461
  await context.tracing.stop({ path: traceZipPath }).catch(() => void 0);
31478
32462
  const recordedVideoPath = await page.video()?.path().catch(() => void 0);
31479
32463
  await context.close().catch(() => void 0);
31480
- const video = recordedVideoPath && existsSync2(recordedVideoPath) ? await readFile4(recordedVideoPath) : void 0;
31481
- const trace = evidenceTmpDir && existsSync2(traceZipPath) ? await readFile4(traceZipPath) : void 0;
32464
+ const video = recordedVideoPath && existsSync3(recordedVideoPath) ? await readFile6(recordedVideoPath) : void 0;
32465
+ const trace = evidenceTmpDir && existsSync3(traceZipPath) ? await readFile6(traceZipPath) : void 0;
31482
32466
  if (evidenceTmpDir) await rm(evidenceTmpDir, { recursive: true, force: true }).catch(() => void 0);
31483
32467
  return { video, trace };
31484
32468
  }
32469
+ async startRequestCapture() {
32470
+ if (this.requestCapture) await this.stopRequestCapture();
32471
+ const page = await this.ensurePage();
32472
+ const requests = [];
32473
+ const listener = (request) => {
32474
+ requests.push({ method: request.method(), url: request.url() });
32475
+ };
32476
+ page.on("request", listener);
32477
+ this.requestCapture = { page, listener, requests };
32478
+ }
32479
+ async stopRequestCapture() {
32480
+ if (!this.requestCapture) return [];
32481
+ const capture = this.requestCapture;
32482
+ this.requestCapture = void 0;
32483
+ capture.page.off("request", capture.listener);
32484
+ return capture.requests;
32485
+ }
31485
32486
  async navigate(url2) {
31486
32487
  const options = this.options;
31487
32488
  const page = await this.ensurePage();
@@ -31543,6 +32544,7 @@ var PlaywrightAdapter = class {
31543
32544
  throw new Error(`\u65E0\u6548\u7684 locator:${JSON.stringify(locator)}`);
31544
32545
  }
31545
32546
  async closeContextQuietly() {
32547
+ await this.stopRequestCapture().catch(() => []);
31546
32548
  if (!this.current) return;
31547
32549
  const { context } = this.current;
31548
32550
  this.current = void 0;
@@ -31581,8 +32583,8 @@ var automatorLauncher = {
31581
32583
  }
31582
32584
  }
31583
32585
  };
31584
- function delay(ms) {
31585
- return new Promise((resolve5) => setTimeout(resolve5, ms));
32586
+ function delay2(ms) {
32587
+ return new Promise((resolve7) => setTimeout(resolve7, ms));
31586
32588
  }
31587
32589
  async function connectWithRetry(wsEndpoint, timeoutMs) {
31588
32590
  const startedAt = Date.now();
@@ -31592,7 +32594,7 @@ async function connectWithRetry(wsEndpoint, timeoutMs) {
31592
32594
  return await automator.connect({ wsEndpoint });
31593
32595
  } catch (err) {
31594
32596
  lastError = err;
31595
- await delay(1e3);
32597
+ await delay2(1e3);
31596
32598
  }
31597
32599
  }
31598
32600
  throw lastError instanceof Error ? lastError : new Error(String(lastError));
@@ -31643,6 +32645,7 @@ var MiniAppAdapter = class {
31643
32645
  }
31644
32646
  target = "miniapp";
31645
32647
  program;
32648
+ requestCapture;
31646
32649
  /** 拉起开发者工具并建立自动化连接;需幂等 */
31647
32650
  async launch() {
31648
32651
  if (this.program) return;
@@ -31663,7 +32666,7 @@ var MiniAppAdapter = class {
31663
32666
  return;
31664
32667
  } catch (err) {
31665
32668
  lastError = err;
31666
- await delay(3e3);
32669
+ await delay2(3e3);
31667
32670
  }
31668
32671
  }
31669
32672
  throw lastError instanceof Error ? lastError : new Error(String(lastError));
@@ -31709,7 +32712,43 @@ var MiniAppAdapter = class {
31709
32712
  return Buffer.from(base643, "base64");
31710
32713
  });
31711
32714
  }
32715
+ async startRequestCapture() {
32716
+ if (this.requestCapture) await this.stopRequestCapture();
32717
+ const program2 = await this.getProgram();
32718
+ const requests = [];
32719
+ const exposedName = `__testpilotRequest_${Date.now()}_${Math.random().toString(36).slice(2)}`;
32720
+ await program2.exposeFunction(exposedName, (value) => {
32721
+ if (value === null || typeof value !== "object") return;
32722
+ const request = value;
32723
+ if (typeof request.url !== "string") return;
32724
+ requests.push({
32725
+ method: typeof request.method === "string" ? request.method : "GET",
32726
+ url: request.url
32727
+ });
32728
+ });
32729
+ await program2.mockWxMethod(
32730
+ "request",
32731
+ function(options, callbackName) {
32732
+ const callbacks = globalThis;
32733
+ callbacks[callbackName]?.({
32734
+ method: typeof options.method === "string" ? options.method : "GET",
32735
+ url: typeof options.url === "string" ? options.url : ""
32736
+ });
32737
+ return this.origin(options);
32738
+ },
32739
+ exposedName
32740
+ );
32741
+ this.requestCapture = { program: program2, exposedName, requests };
32742
+ }
32743
+ async stopRequestCapture() {
32744
+ if (!this.requestCapture) return [];
32745
+ const capture = this.requestCapture;
32746
+ this.requestCapture = void 0;
32747
+ await capture.program.restoreWxMethod("request");
32748
+ return capture.requests;
32749
+ }
31712
32750
  async close() {
32751
+ await this.stopRequestCapture().catch(() => []);
31713
32752
  if (!this.program) return;
31714
32753
  const program2 = this.program;
31715
32754
  this.program = void 0;
@@ -31757,17 +32796,17 @@ var MiniAppAdapter = class {
31757
32796
  if (Date.now() - startedAt > ELEMENT_WAIT_MS) {
31758
32797
  throw new Error(`\u7B49\u5F85\u5143\u7D20\u8D85\u65F6(${ELEMENT_WAIT_MS}ms):${selector}`);
31759
32798
  }
31760
- await delay(500);
32799
+ await delay2(500);
31761
32800
  }
31762
32801
  }
31763
32802
  };
31764
32803
 
31765
32804
  // ../../packages/adapter-wechatide/src/project.ts
31766
- import { existsSync as existsSync3 } from "node:fs";
32805
+ import { existsSync as existsSync4 } from "node:fs";
31767
32806
  async function resolveMiniappConfig(input2) {
31768
32807
  const projectPath = input2.projectPath ?? process.env.TESTPILOT_MINIPROGRAM_PROJECT_PATH;
31769
32808
  if (!projectPath) return void 0;
31770
- if (!existsSync3(projectPath)) {
32809
+ if (!existsSync4(projectPath)) {
31771
32810
  throw new Error(`\u5C0F\u7A0B\u5E8F\u9879\u76EE\u8DEF\u5F84\u4E0D\u5B58\u5728:${projectPath}`);
31772
32811
  }
31773
32812
  const cliPath = input2.cliPath ?? process.env.WECHAT_DEVTOOLS_CLI ?? await findWechatDevToolsCli();
@@ -31942,7 +32981,7 @@ function createDefaultAdapters(config2) {
31942
32981
  async function runCases(options = {}) {
31943
32982
  const root = resolveRoot(options.root);
31944
32983
  const config2 = await loadProjectConfig(root);
31945
- const inputs = options.paths && options.paths.length > 0 ? options.paths.map((input2) => isAbsolute2(input2) ? input2 : join15(root, input2)) : [join15(root, config2.casesDir)];
32984
+ const inputs = options.paths && options.paths.length > 0 ? options.paths.map((input2) => isAbsolute3(input2) ? input2 : join17(root, input2)) : [join17(root, config2.casesDir)];
31946
32985
  const cases = await collectCases(inputs, { root });
31947
32986
  if (cases.length === 0) {
31948
32987
  throw new RunError("ENO_CASES", `\u672A\u627E\u5230\u7528\u4F8B\u6587\u4EF6(\u67E5\u627E:${inputs.join(", ")})`);
@@ -31968,26 +33007,41 @@ async function runCases(options = {}) {
31968
33007
  ];
31969
33008
  const runner = new TestRunner({
31970
33009
  resolver,
31971
- runsRoot: join15(root, RUNS_DIR),
33010
+ runsRoot: join17(root, RUNS_DIR),
31972
33011
  runId: options.runId,
31973
33012
  onEvent: options.onEvent,
31974
33013
  signal: options.signal,
31975
33014
  accounts: options.accounts
31976
33015
  });
33016
+ const prepared = (await Promise.all(
33017
+ runnable.map(
33018
+ (item) => prepareCaseExecutions(
33019
+ { data: item.case, file: item.file },
33020
+ {
33021
+ root,
33022
+ dataDir: join17(root, "tests", "e2e", "data"),
33023
+ variables: config2.variables,
33024
+ accounts: options.accounts
33025
+ }
33026
+ )
33027
+ )
33028
+ )).flat();
31977
33029
  const summary = await runner.run(
31978
- runnable.map((item) => ({ data: item.case, file: item.file }))
33030
+ prepared.map(
33031
+ (item) => item
33032
+ )
31979
33033
  );
31980
33034
  return { summary, invalid, tagFiltered, missingAccounts };
31981
33035
  }
31982
33036
  function runsRoot(root) {
31983
- return join15(resolveRoot(root), RUNS_DIR);
33037
+ return join17(resolveRoot(root), RUNS_DIR);
31984
33038
  }
31985
33039
  async function listRuns(options = {}) {
31986
33040
  const base = runsRoot(options.root);
31987
- const entries = await readdir3(base).catch(() => []);
33041
+ const entries = await readdir4(base).catch(() => []);
31988
33042
  const metas = [];
31989
33043
  for (const entry of entries.filter((name) => /^\d{8}-\d{3}$/.test(name)).sort().reverse()) {
31990
- metas.push({ ...await readRunMeta(base, entry), runId: entry, dir: join15(base, entry) });
33044
+ metas.push({ ...await readRunMeta(base, entry), runId: entry, dir: join17(base, entry) });
31991
33045
  }
31992
33046
  return metas;
31993
33047
  }
@@ -31997,22 +33051,22 @@ async function latestRunId(options = {}) {
31997
33051
  }
31998
33052
  async function getRun(runId, options = {}) {
31999
33053
  const base = runsRoot(options.root);
32000
- const file2 = join15(base, runId, RESULT_FILE);
33054
+ const file2 = join17(base, runId, RESULT_FILE);
32001
33055
  try {
32002
- return JSON.parse(await readFile5(file2, "utf8"));
33056
+ return JSON.parse(await readFile7(file2, "utf8"));
32003
33057
  } catch {
32004
33058
  throw new RunError("ENO_CASES", `\u8BFB\u53D6\u8FD0\u884C\u7ED3\u679C\u5931\u8D25:${file2}(\u8FD0\u884C\u672A\u5B8C\u6210\u6216 runId \u4E0D\u5B58\u5728)`);
32005
33059
  }
32006
33060
  }
32007
33061
  async function getRunEvents(runId, options = {}) {
32008
- const file2 = join15(runsRoot(options.root), runId, "events.ndjson");
32009
- const source = await readFile5(file2, "utf8").catch(() => "");
33062
+ const file2 = join17(runsRoot(options.root), runId, "events.ndjson");
33063
+ const source = await readFile7(file2, "utf8").catch(() => "");
32010
33064
  return source.split("\n").filter((line) => line.trim().length > 0).map((line) => JSON.parse(line));
32011
33065
  }
32012
33066
  async function readRunMeta(base, runId) {
32013
33067
  try {
32014
33068
  const summary = JSON.parse(
32015
- await readFile5(join15(base, runId, RESULT_FILE), "utf8")
33069
+ await readFile7(join17(base, runId, RESULT_FILE), "utf8")
32016
33070
  );
32017
33071
  return {
32018
33072
  status: summary.status,
@@ -32033,25 +33087,47 @@ async function generateReport(runId, options = {}) {
32033
33087
  if (!id) {
32034
33088
  throw new Error("\u672A\u627E\u5230\u8FD0\u884C\u8BB0\u5F55(\u5148\u8FD0\u884C\u4E00\u6B21\u7528\u4F8B)");
32035
33089
  }
32036
- const runDir = join16(resolveRoot(options.root), RUNS_DIR, id);
33090
+ const runDir = join18(resolveRoot(options.root), RUNS_DIR, id);
32037
33091
  let summary;
32038
33092
  try {
32039
- summary = JSON.parse(await readFile6(join16(runDir, "result.json"), "utf8"));
33093
+ summary = JSON.parse(await readFile8(join18(runDir, "result.json"), "utf8"));
32040
33094
  } catch {
32041
- throw new Error(`\u8BFB\u53D6\u8FD0\u884C\u7ED3\u679C\u5931\u8D25:${join16(runDir, "result.json")}(\u8FD0\u884C\u672A\u5B8C\u6210\u6216 runId \u4E0D\u5B58\u5728)`);
33095
+ throw new Error(`\u8BFB\u53D6\u8FD0\u884C\u7ED3\u679C\u5931\u8D25:${join18(runDir, "result.json")}(\u8FD0\u884C\u672A\u5B8C\u6210\u6216 runId \u4E0D\u5B58\u5728)`);
32042
33096
  }
32043
33097
  const written = await writeReports(runDir, summary);
32044
33098
  return { runId: id, json: written.json, html: written.html };
32045
33099
  }
33100
+ async function generateSummaryReport(options = {}) {
33101
+ const root = resolveRoot(options.root);
33102
+ const limit = options.last && options.last > 0 ? options.last : 30;
33103
+ const metas = await listRuns({ root });
33104
+ if (metas.length === 0) {
33105
+ throw new Error("\u7EDF\u8BA1\u7A97\u53E3\u5185\u6CA1\u6709\u4EFB\u4F55\u8FD0\u884C\u8BB0\u5F55(\u5148\u8FD0\u884C npx csspilot run)");
33106
+ }
33107
+ const runs = [];
33108
+ for (const meta3 of metas.slice(0, limit)) {
33109
+ try {
33110
+ runs.push({ runId: meta3.runId, summary: await getRun(meta3.runId, { root }) });
33111
+ } catch {
33112
+ }
33113
+ }
33114
+ if (runs.length === 0) {
33115
+ throw new Error("\u7EDF\u8BA1\u7A97\u53E3\u5185\u7684\u8FD0\u884C\u7ED3\u679C\u5747\u4E0D\u53EF\u8BFB");
33116
+ }
33117
+ const data = buildSummaryData(runs);
33118
+ const artifactsDir = join18(root, ".testpilot", "artifacts");
33119
+ const written = await writeSummaryReport(artifactsDir, data);
33120
+ return { html: written.html, json: written.json, data, window: { runs: runs.length } };
33121
+ }
32046
33122
  async function readReport(runId, options = {}) {
32047
- const file2 = join16(resolveRoot(options.root), RUNS_DIR, runId, "report.json");
32048
- const source = await readFile6(file2, "utf8").catch(() => void 0);
33123
+ const file2 = join18(resolveRoot(options.root), RUNS_DIR, runId, "report.json");
33124
+ const source = await readFile8(file2, "utf8").catch(() => void 0);
32049
33125
  return source ? JSON.parse(source) : void 0;
32050
33126
  }
32051
33127
 
32052
33128
  // ../../packages/sdk/src/client.ts
32053
33129
  function resolveInput(root, input2) {
32054
- return isAbsolute3(input2) ? input2 : join17(root, input2);
33130
+ return isAbsolute4(input2) ? input2 : join19(root, input2);
32055
33131
  }
32056
33132
  var TestPilotClient = class {
32057
33133
  /** 业务项目根目录(绝对路径) */
@@ -32116,6 +33192,10 @@ var TestPilotClient = class {
32116
33192
  generateReport(runId) {
32117
33193
  return generateReport(runId, { root: this.root });
32118
33194
  }
33195
+ /** 生成跨 run 汇总报告(summary.html + summary.json) */
33196
+ generateSummaryReport(options = {}) {
33197
+ return generateSummaryReport({ ...options, root: this.root });
33198
+ }
32119
33199
  /** 读取已生成的报告 */
32120
33200
  readReport(runId) {
32121
33201
  return readReport(runId, { root: this.root });
@@ -32123,13 +33203,13 @@ var TestPilotClient = class {
32123
33203
  };
32124
33204
 
32125
33205
  // src/commands/init/adapter-detector.ts
32126
- import { existsSync as existsSync4, readFileSync } from "node:fs";
32127
- import { join as join18 } from "node:path";
33206
+ import { existsSync as existsSync5, readFileSync } from "node:fs";
33207
+ import { join as join20 } from "node:path";
32128
33208
  async function detectAdapters(root, projectType) {
32129
33209
  const required2 = projectType === "hybrid" ? ["playwright", "wechatide"] : projectType === "wechat-miniapp" ? ["wechatide"] : projectType === "web" ? ["playwright"] : [];
32130
33210
  const pkg = readPackageJsonDeps(root);
32131
33211
  const hasPlaywrightDep = pkg !== void 0 && ("playwright" in pkg || "@playwright/test" in pkg);
32132
- const playwrightInstalled = hasPlaywrightDep || existsSync4(join18(root, "node_modules", "playwright"));
33212
+ const playwrightInstalled = hasPlaywrightDep || existsSync5(join20(root, "node_modules", "playwright"));
32133
33213
  const devtoolsCli = await findWechatDevToolsCli().catch(() => void 0);
32134
33214
  return [
32135
33215
  {
@@ -32147,8 +33227,8 @@ async function detectAdapters(root, projectType) {
32147
33227
  ];
32148
33228
  }
32149
33229
  function readPackageJsonDeps(root) {
32150
- const file2 = join18(root, "package.json");
32151
- if (!existsSync4(file2)) return void 0;
33230
+ const file2 = join20(root, "package.json");
33231
+ if (!existsSync5(file2)) return void 0;
32152
33232
  try {
32153
33233
  const pkg = JSON.parse(readFileSync(file2, "utf8"));
32154
33234
  return { ...pkg.dependencies, ...pkg.devDependencies };
@@ -32158,13 +33238,13 @@ function readPackageJsonDeps(root) {
32158
33238
  }
32159
33239
 
32160
33240
  // src/commands/init/environment-initializer.ts
32161
- import { readFile as readFile7 } from "node:fs/promises";
32162
- import { join as join19 } from "node:path";
33241
+ import { readFile as readFile9 } from "node:fs/promises";
33242
+ import { join as join21 } from "node:path";
32163
33243
  var ENV_REF_PATTERN = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
32164
33244
  async function collectEnvVarRefs(root) {
32165
33245
  let text;
32166
33246
  try {
32167
- text = await readFile7(join19(root, "testpilot.yaml"), "utf8");
33247
+ text = await readFile9(join21(root, "testpilot.yaml"), "utf8");
32168
33248
  } catch {
32169
33249
  return [];
32170
33250
  }
@@ -32180,8 +33260,8 @@ async function collectEnvVarRefs(root) {
32180
33260
  }
32181
33261
 
32182
33262
  // src/commands/init/project-detector.ts
32183
- import { existsSync as existsSync5, readFileSync as readFileSync2 } from "node:fs";
32184
- import { basename, join as join20 } from "node:path";
33263
+ import { existsSync as existsSync6, readFileSync as readFileSync2 } from "node:fs";
33264
+ import { basename, join as join22 } from "node:path";
32185
33265
 
32186
33266
  // src/lib/git.ts
32187
33267
  import { execFile as execFile2 } from "node:child_process";
@@ -32218,8 +33298,8 @@ var WEB_DEP_MARKERS = [
32218
33298
  ];
32219
33299
  var E2E_DIR_CANDIDATES = ["tests/e2e", "test/e2e", "e2e", "cypress"];
32220
33300
  function readPackageJson(root) {
32221
- const file2 = join20(root, "package.json");
32222
- if (!existsSync5(file2)) return void 0;
33301
+ const file2 = join22(root, "package.json");
33302
+ if (!existsSync6(file2)) return void 0;
32223
33303
  try {
32224
33304
  return JSON.parse(readFileSync2(file2, "utf8"));
32225
33305
  } catch {
@@ -32229,8 +33309,8 @@ function readPackageJson(root) {
32229
33309
  function detectProjectType(root) {
32230
33310
  const pkg = readPackageJson(root);
32231
33311
  const deps = { ...pkg?.dependencies, ...pkg?.devDependencies };
32232
- const isWechat = existsSync5(join20(root, "project.config.json")) || existsSync5(join20(root, "app.json")) || existsSync5(join20(root, "miniprogram", "app.json"));
32233
- const isWeb = existsSync5(join20(root, "index.html")) || WEB_DEP_MARKERS.some((marker) => deps?.[marker] !== void 0) || [".ts", ".tsx", ".js", ".jsx"].some((ext) => existsSync5(join20(root, "src", `index${ext}`)));
33312
+ const isWechat = existsSync6(join22(root, "project.config.json")) || existsSync6(join22(root, "app.json")) || existsSync6(join22(root, "miniprogram", "app.json"));
33313
+ const isWeb = existsSync6(join22(root, "index.html")) || WEB_DEP_MARKERS.some((marker) => deps?.[marker] !== void 0) || [".ts", ".tsx", ".js", ".jsx"].some((ext) => existsSync6(join22(root, "src", `index${ext}`)));
32234
33314
  if (isWechat && isWeb) return "hybrid";
32235
33315
  if (isWechat) return "wechat-miniapp";
32236
33316
  if (isWeb) return "web";
@@ -32239,9 +33319,9 @@ function detectProjectType(root) {
32239
33319
  async function detectProject(root) {
32240
33320
  const pkg = readPackageJson(root);
32241
33321
  const type = detectProjectType(root);
32242
- const language = existsSync5(join20(root, "tsconfig.json")) ? "TypeScript" : pkg || existsSync5(join20(root, "src")) ? "JavaScript" : "Unknown";
32243
- const packageManager = existsSync5(join20(root, "pnpm-lock.yaml")) ? "pnpm" : existsSync5(join20(root, "yarn.lock")) ? "yarn" : existsSync5(join20(root, "package-lock.json")) ? "npm" : "npx";
32244
- const existingE2eDirs = E2E_DIR_CANDIDATES.filter((dir) => existsSync5(join20(root, dir)));
33322
+ const language = existsSync6(join22(root, "tsconfig.json")) ? "TypeScript" : pkg || existsSync6(join22(root, "src")) ? "JavaScript" : "Unknown";
33323
+ const packageManager = existsSync6(join22(root, "pnpm-lock.yaml")) ? "pnpm" : existsSync6(join22(root, "yarn.lock")) ? "yarn" : existsSync6(join22(root, "package-lock.json")) ? "npm" : "npx";
33324
+ const existingE2eDirs = E2E_DIR_CANDIDATES.filter((dir) => existsSync6(join22(root, dir)));
32245
33325
  return {
32246
33326
  root,
32247
33327
  name: pkg?.name ?? basename(root),
@@ -32251,8 +33331,8 @@ async function detectProject(root) {
32251
33331
  git: await readGitInfo(root),
32252
33332
  hasTestsE2e: existingE2eDirs.includes("tests/e2e"),
32253
33333
  existingE2eDirs,
32254
- hasTestpilotConfig: existsSync5(join20(root, "testpilot.yaml")),
32255
- hasProjectLink: existsSync5(join20(root, ".testpilot", "project.json"))
33334
+ hasTestpilotConfig: existsSync6(join22(root, "testpilot.yaml")),
33335
+ hasProjectLink: existsSync6(join22(root, ".testpilot", "project.json"))
32256
33336
  };
32257
33337
  }
32258
33338
  function defaultAdapterFor(type) {
@@ -32267,9 +33347,9 @@ function adaptersFor(type) {
32267
33347
  }
32268
33348
 
32269
33349
  // src/commands/init/skill-installer.ts
32270
- import { existsSync as existsSync6, readFileSync as readFileSync3 } from "node:fs";
32271
- import { access as access2, cp, mkdir as mkdir11, writeFile as writeFile9 } from "node:fs/promises";
32272
- import { dirname as dirname7, join as join21 } from "node:path";
33350
+ import { existsSync as existsSync7, readFileSync as readFileSync3 } from "node:fs";
33351
+ import { access as access2, cp, mkdir as mkdir12, writeFile as writeFile10 } from "node:fs/promises";
33352
+ import { dirname as dirname9, join as join23 } from "node:path";
32273
33353
  import { fileURLToPath } from "node:url";
32274
33354
  var CORE_FILES = ["SKILL.md", "manifest.yaml"];
32275
33355
  var CORE_DIRS = ["rules", "workflows"];
@@ -32282,39 +33362,39 @@ async function exists3(path) {
32282
33362
  }
32283
33363
  }
32284
33364
  function readManifestVersion(dir) {
32285
- const manifestPath = join21(dir, "manifest.yaml");
32286
- if (!existsSync6(manifestPath)) return void 0;
33365
+ const manifestPath = join23(dir, "manifest.yaml");
33366
+ if (!existsSync7(manifestPath)) return void 0;
32287
33367
  return /^version:\s*([^\s#]+)/m.exec(readFileSync3(manifestPath, "utf8"))?.[1];
32288
33368
  }
32289
33369
  function resolveSkillSourceDir() {
32290
33370
  const fromEnv = process.env.TESTPILOT_SKILL_DIR;
32291
33371
  if (fromEnv) return fromEnv;
32292
33372
  const bundled = fileURLToPath(new URL("./skills/testpilot", import.meta.url));
32293
- if (existsSync6(bundled)) return bundled;
33373
+ if (existsSync7(bundled)) return bundled;
32294
33374
  const inRepo = fileURLToPath(new URL("../../../../../skills/testpilot", import.meta.url));
32295
- return existsSync6(inRepo) ? inRepo : void 0;
33375
+ return existsSync7(inRepo) ? inRepo : void 0;
32296
33376
  }
32297
33377
  async function copyCoreFiles(src, dest) {
32298
33378
  for (const name of CORE_FILES) {
32299
- await cp(join21(src, name), join21(dest, name), { recursive: true, force: true });
33379
+ await cp(join23(src, name), join23(dest, name), { recursive: true, force: true });
32300
33380
  }
32301
33381
  for (const dir of CORE_DIRS) {
32302
- if (existsSync6(join21(src, dir))) {
32303
- await cp(join21(src, dir), join21(dest, dir), { recursive: true, force: true });
33382
+ if (existsSync7(join23(src, dir))) {
33383
+ await cp(join23(src, dir), join23(dest, dir), { recursive: true, force: true });
32304
33384
  }
32305
33385
  }
32306
33386
  }
32307
33387
  async function syncSkillFiles(root, options = {}) {
32308
- const dest = join21(root, ".agents", "skills", "testpilot");
33388
+ const dest = join23(root, ".agents", "skills", "testpilot");
32309
33389
  const bundledVersion = readManifestVersion(resolveSkillSourceDir() ?? "");
32310
- if (!await exists3(join21(dest, "manifest.yaml"))) {
33390
+ if (!await exists3(join23(dest, "manifest.yaml"))) {
32311
33391
  const src = resolveSkillSourceDir();
32312
33392
  if (!src) {
32313
33393
  throw new Error(
32314
33394
  "\u65E0\u6CD5\u5B9A\u4F4D TestPilot Skill \u6E90\u76EE\u5F55;\u53EF\u901A\u8FC7\u73AF\u5883\u53D8\u91CF TESTPILOT_SKILL_DIR \u6307\u5B9A skills/testpilot \u8DEF\u5F84"
32315
33395
  );
32316
33396
  }
32317
- await mkdir11(dirname7(dest), { recursive: true });
33397
+ await mkdir12(dirname9(dest), { recursive: true });
32318
33398
  await cp(src, dest, { recursive: true });
32319
33399
  return { core: "created", bundledVersion, dest };
32320
33400
  }
@@ -32335,14 +33415,14 @@ async function syncSkillFiles(root, options = {}) {
32335
33415
  };
32336
33416
  }
32337
33417
  async function installSkill(root, info, adapters) {
32338
- const dest = join21(root, ".agents", "skills", "testpilot");
33418
+ const dest = join23(root, ".agents", "skills", "testpilot");
32339
33419
  const sync = await syncSkillFiles(root);
32340
33420
  const core = sync.core === "created" ? "created" : sync.core === "updated" ? "updated" : "reused";
32341
33421
  const coreVersion = sync.core === "updated" || sync.core === "created" ? { from: sync.installedVersion, to: sync.bundledVersion } : void 0;
32342
33422
  const config2 = await loadTestpilotConfig(root).catch(() => void 0);
32343
33423
  const defaultEnvironment = typeof config2?.environment?.default === "string" ? config2.environment.default : "\u672A\u8BBE\u7F6E";
32344
33424
  const defaultWorkspace = config2?.workspace?.default ?? "\u672A\u8BBE\u7F6E(\u53EF\u5728 yaml workspace.default \u914D\u7F6E)";
32345
- const refsDir = join21(dest, "references");
33425
+ const refsDir = join23(dest, "references");
32346
33426
  const files = [
32347
33427
  ["project.md", () => renderProjectMd(info, adapters, defaultEnvironment, defaultWorkspace)],
32348
33428
  ["test-conventions.md", renderTestConventionsMd],
@@ -32350,10 +33430,10 @@ async function installSkill(root, info, adapters) {
32350
33430
  ];
32351
33431
  let references = "reused";
32352
33432
  for (const [name, render] of files) {
32353
- const file2 = join21(refsDir, name);
33433
+ const file2 = join23(refsDir, name);
32354
33434
  if (await exists3(file2)) continue;
32355
- await mkdir11(refsDir, { recursive: true });
32356
- await writeFile9(file2, render(), "utf8");
33435
+ await mkdir12(refsDir, { recursive: true });
33436
+ await writeFile10(file2, render(), "utf8");
32357
33437
  references = "created";
32358
33438
  }
32359
33439
  return coreVersion ? { core, coreVersion, references, dest } : { core, references, dest };
@@ -32388,6 +33468,12 @@ function renderTestConventionsMd() {
32388
33468
  6. \u4F18\u5148\u590D\u7528\u5DF2\u6709 Case
32389
33469
  7. \u65B0\u589E Case \u5FC5\u987B\u901A\u8FC7 csspilot validate
32390
33470
  8. Case ID \u5FC5\u987B\u552F\u4E00
33471
+ 9. fixture/dataset \u4F7F\u7528 YAML \u6216 JSON,\u7EDF\u4E00\u4F4D\u4E8E tests/e2e/data
33472
+ 10. \u9700\u6C42\u89C4\u5219\u77E9\u9635\u4E0E QA \u8986\u76D6\u6620\u5C04\u5199\u5165 tests/e2e/reviews/<\u9700\u6C42>-coverage.md
33473
+ 11. \u53EA\u5728\u6B65\u9AA4\u6A21\u677F\u76F8\u540C\u65F6\u4F7F\u7528 data-driven Case,\u6BCF\u884C\u4FDD\u6301\u4E00\u4E2A\u53EF\u5224\u5B9A\u89C4\u5219/\u6570\u636E\u70B9
33474
+ 12. requires \u53EA\u58F0\u660E\u9879\u76EE\u8BC1\u636E\u652F\u6301\u7684\u4F9D\u8D56,\u4E0D\u5F97\u5957\u7528\u8D26\u53F7/\u4F18\u60E0\u5238/PPM \u7B49\u9ED8\u8BA4\u5B57\u6BB5
33475
+ 13. Case \u4E0D\u5F97\u8BFB\u53D6\u4EFB\u610F\u73AF\u5883\u53D8\u91CF;\u5148\u5728 testpilot.yaml variables \u767D\u540D\u5355\u6620\u5C04\u540E\u4F7F\u7528 \${variable.*}
33476
+ 14. setup/teardown \u4F18\u5148 API;teardown \u5931\u8D25\u4F5C\u4E3A warning,\u4E0D\u6539\u53D8\u4E3B\u6D41\u7A0B\u7ED3\u679C
32391
33477
  `;
32392
33478
  }
32393
33479
  function renderAdaptersMd(adapters) {
@@ -32428,27 +33514,27 @@ async function runDoctor(root) {
32428
33514
  const projectItems = [
32429
33515
  ok(`Project ${info.name}(${info.type})`),
32430
33516
  info.git.commit ? ok(`Git repository(${info.git.branch ?? "detached"} \xB7 ${info.git.commit.slice(0, 7)})`) : warn("Git repository \u672A\u68C0\u6D4B\u5230", "Case Metadata \u65E0\u6CD5\u540C\u6B65\u5230 Server"),
32431
- existsSync7(join22(root, ".testpilot", "project.json")) ? ok(".testpilot/project.json") : error62(".testpilot/project.json", "\u8FD0\u884C npx csspilot init \u751F\u6210"),
32432
- existsSync7(join22(root, "testpilot.yaml")) ? ok("testpilot.yaml") : error62("testpilot.yaml", "\u8FD0\u884C npx csspilot init \u751F\u6210")
33517
+ existsSync8(join24(root, ".testpilot", "project.json")) ? ok(".testpilot/project.json") : error62(".testpilot/project.json", "\u8FD0\u884C npx csspilot init \u751F\u6210"),
33518
+ existsSync8(join24(root, "testpilot.yaml")) ? ok("testpilot.yaml") : error62("testpilot.yaml", "\u8FD0\u884C npx csspilot init \u751F\u6210")
32433
33519
  ];
32434
33520
  push("Project", projectItems);
32435
33521
  const config2 = await loadTestpilotConfig(root).catch(() => void 0);
32436
33522
  const casesDir = config2?.casesDir ?? "tests/e2e/cases";
32437
- const cases = await collectCases([join22(root, casesDir)], { root }).catch(() => []);
33523
+ const cases = await collectCases([join24(root, casesDir)], { root }).catch(() => []);
32438
33524
  const invalidCount = cases.filter((item) => !item.valid).length;
32439
- const testDir = join22(root, "tests", "e2e");
33525
+ const testDir = join24(root, "tests", "e2e");
32440
33526
  push("Test", [
32441
- existsSync7(testDir) ? ok("tests/e2e") : error62("tests/e2e", "\u8FD0\u884C npx csspilot init \u521B\u5EFA"),
32442
- existsSync7(join22(root, casesDir)) ? ok(`cases directory(${casesDir})`) : error62(`cases directory(${casesDir})`, "\u8FD0\u884C npx csspilot init \u521B\u5EFA"),
33527
+ existsSync8(testDir) ? ok("tests/e2e") : error62("tests/e2e", "\u8FD0\u884C npx csspilot init \u521B\u5EFA"),
33528
+ existsSync8(join24(root, casesDir)) ? ok(`cases directory(${casesDir})`) : error62(`cases directory(${casesDir})`, "\u8FD0\u884C npx csspilot init \u521B\u5EFA"),
32443
33529
  cases.length === 0 ? warn("Case validation \u8DF3\u8FC7(\u672A\u627E\u5230\u7528\u4F8B)", "\u5728 tests/e2e/cases \u521B\u5EFA Case") : invalidCount === 0 ? ok(`Case validation(${cases.length} \u4E2A\u7528\u4F8B\u5168\u90E8\u6709\u6548)`) : error62(`Case validation(${invalidCount}/${cases.length} \u4E2A\u7528\u4F8B\u6821\u9A8C\u5931\u8D25)`, "\u8FD0\u884C npx csspilot validate \u67E5\u770B\u8BE6\u60C5")
32444
33530
  ]);
32445
- const skillDir = join22(root, ".agents", "skills", "testpilot");
33531
+ const skillDir = join24(root, ".agents", "skills", "testpilot");
32446
33532
  const installedSkillVersion = readManifestVersion(skillDir);
32447
33533
  const skillSource = resolveSkillSourceDir();
32448
33534
  const bundledSkillVersion = skillSource ? readManifestVersion(skillSource) : void 0;
32449
33535
  const skillItems = [
32450
33536
  installedSkillVersion ? ok(`TestPilot Skill(.agents/skills/testpilot,v${installedSkillVersion})`) : error62("TestPilot Skill", "\u8FD0\u884C npx csspilot init \u5B89\u88C5"),
32451
- existsSync7(join22(skillDir, "references", "project.md")) ? ok("Project testing guide(references/)") : warn("Project testing guide \u7F3A\u5931", "references/ \u7531 init \u751F\u6210;\u53EF\u91CD\u65B0\u8FD0\u884C npx csspilot init")
33537
+ existsSync8(join24(skillDir, "references", "project.md")) ? ok("Project testing guide(references/)") : warn("Project testing guide \u7F3A\u5931", "references/ \u7531 init \u751F\u6210;\u53EF\u91CD\u65B0\u8FD0\u884C npx csspilot init")
32452
33538
  ];
32453
33539
  if (installedSkillVersion && bundledSkillVersion && installedSkillVersion !== bundledSkillVersion) {
32454
33540
  skillItems.push(
@@ -32540,11 +33626,11 @@ function makeDoctorCommand() {
32540
33626
  }
32541
33627
 
32542
33628
  // src/commands/init/index.ts
32543
- import { join as join25 } from "node:path";
33629
+ import { join as join27 } from "node:path";
32544
33630
 
32545
33631
  // src/commands/init/config-initializer.ts
32546
- import { access as access3, mkdir as mkdir12, readFile as readFile8, writeFile as writeFile10 } from "node:fs/promises";
32547
- import { basename as basename2, join as join23 } from "node:path";
33632
+ import { access as access3, mkdir as mkdir13, readFile as readFile10, writeFile as writeFile11 } from "node:fs/promises";
33633
+ import { basename as basename2, join as join25 } from "node:path";
32548
33634
  async function exists4(path) {
32549
33635
  try {
32550
33636
  await access3(path);
@@ -32575,6 +33661,10 @@ environment:
32575
33661
  staging:
32576
33662
  baseUrl: \${STAGING_BASE_URL}
32577
33663
 
33664
+ # Case \u53EF\u89C1\u7684\u9879\u76EE\u53D8\u91CF\u767D\u540D\u5355(\u503C\u6765\u81EA .env / CI Secret;Case \u7528 \${variable.<name>} \u5F15\u7528)
33665
+ # variables:
33666
+ # ppmProjectId: \${PPM_PROJECT_ID}
33667
+
32578
33668
  # \u9879\u76EE\u58F0\u660E\u7684\u6267\u884C\u7AEF(\u5B9E\u9645\u53EF\u7528\u6027\u4EE5 csspilot doctor \u68C0\u6D4B\u4E3A\u51C6)
32579
33669
  adapters:
32580
33670
  ${adapters}
@@ -32618,42 +33708,42 @@ STAGING_BASE_URL=https://staging.example.com
32618
33708
  `;
32619
33709
  }
32620
33710
  async function ensureGitignoreEntry(root, entry, comment = "TestPilot \u672C\u5730\u73AF\u5883\u53D8\u91CF") {
32621
- const gitignorePath = join23(root, ".gitignore");
33711
+ const gitignorePath = join25(root, ".gitignore");
32622
33712
  if (!await exists4(gitignorePath)) {
32623
- await writeFile10(gitignorePath, `# ${comment}
33713
+ await writeFile11(gitignorePath, `# ${comment}
32624
33714
  ${entry}
32625
33715
  `, "utf8");
32626
33716
  return "created";
32627
33717
  }
32628
- const content = await readFile8(gitignorePath, "utf8");
33718
+ const content = await readFile10(gitignorePath, "utf8");
32629
33719
  if (content.split(/\r?\n/).some((item) => item.trim() === entry)) {
32630
33720
  return "exists";
32631
33721
  }
32632
33722
  const separator = content.endsWith("\n") || content === "" ? "" : "\n";
32633
- await writeFile10(gitignorePath, `${content}${separator}# ${comment}
33723
+ await writeFile11(gitignorePath, `${content}${separator}# ${comment}
32634
33724
  ${entry}
32635
33725
  `, "utf8");
32636
33726
  return "updated";
32637
33727
  }
32638
33728
  async function initTestPilotConfig(root, info, adapters) {
32639
- const yamlPath = join23(root, "testpilot.yaml");
33729
+ const yamlPath = join25(root, "testpilot.yaml");
32640
33730
  let yaml = "reused";
32641
33731
  if (!await exists4(yamlPath)) {
32642
- await writeFile10(yamlPath, renderTestpilotYaml(info), "utf8");
33732
+ await writeFile11(yamlPath, renderTestpilotYaml(info), "utf8");
32643
33733
  yaml = "created";
32644
33734
  }
32645
- const envExamplePath = join23(root, ".env.example");
33735
+ const envExamplePath = join25(root, ".env.example");
32646
33736
  let envExample = "reused";
32647
33737
  if (!await exists4(envExamplePath)) {
32648
- await writeFile10(envExamplePath, renderDotEnvExample(), "utf8");
33738
+ await writeFile11(envExamplePath, renderDotEnvExample(), "utf8");
32649
33739
  envExample = "created";
32650
33740
  }
32651
33741
  const gitignore = await ensureGitignoreEntry(root, ".env");
32652
- const runtimeDir = join23(root, ".testpilot", "artifacts");
32653
- await mkdir12(runtimeDir, { recursive: true });
32654
- const keep = join23(runtimeDir, ".gitkeep");
33742
+ const runtimeDir = join25(root, ".testpilot", "artifacts");
33743
+ await mkdir13(runtimeDir, { recursive: true });
33744
+ const keep = join25(runtimeDir, ".gitkeep");
32655
33745
  if (!await exists4(keep)) {
32656
- await writeFile10(keep, "", "utf8");
33746
+ await writeFile11(keep, "", "utf8");
32657
33747
  }
32658
33748
  const existing = await readProjectLink(root);
32659
33749
  const defaultAdapter = defaultAdapterFor(info.type);
@@ -32674,7 +33764,7 @@ async function initTestPilotConfig(root, info, adapters) {
32674
33764
  const projectLinkFile = await writeProjectLink(link, root);
32675
33765
  return {
32676
33766
  yaml,
32677
- runtimeDir: join23(root, ".testpilot"),
33767
+ runtimeDir: join25(root, ".testpilot"),
32678
33768
  projectLink: link,
32679
33769
  projectLinkFile,
32680
33770
  envExample,
@@ -32709,32 +33799,35 @@ function randomId() {
32709
33799
  }
32710
33800
 
32711
33801
  // src/commands/init/test-structure.ts
32712
- import { existsSync as existsSync8, readdirSync as readdirSync2 } from "node:fs";
32713
- import { mkdir as mkdir13, writeFile as writeFile11 } from "node:fs/promises";
32714
- import { join as join24 } from "node:path";
33802
+ import { existsSync as existsSync9, readdirSync as readdirSync2 } from "node:fs";
33803
+ import { mkdir as mkdir14, writeFile as writeFile12 } from "node:fs/promises";
33804
+ import { join as join26 } from "node:path";
32715
33805
  async function initTestStructure(root, _info) {
32716
33806
  const dirs = {
32717
- cases: join24(root, "tests", "e2e", "cases"),
32718
- fixtures: join24(root, "tests", "e2e", "fixtures"),
32719
- data: join24(root, "tests", "e2e", "data")
33807
+ cases: join26(root, "tests", "e2e", "cases"),
33808
+ fixtures: join26(root, "tests", "e2e", "fixtures"),
33809
+ data: join26(root, "tests", "e2e", "data"),
33810
+ reviews: join26(root, "tests", "e2e", "reviews")
32720
33811
  };
32721
33812
  const existed = {
32722
- cases: existsSync8(dirs.cases),
32723
- fixtures: existsSync8(dirs.fixtures),
32724
- data: existsSync8(dirs.data)
33813
+ cases: existsSync9(dirs.cases),
33814
+ fixtures: existsSync9(dirs.fixtures),
33815
+ data: existsSync9(dirs.data),
33816
+ reviews: existsSync9(dirs.reviews)
32725
33817
  };
32726
33818
  for (const dir of Object.values(dirs)) {
32727
- await mkdir13(dir, { recursive: true });
33819
+ await mkdir14(dir, { recursive: true });
32728
33820
  }
32729
33821
  for (const [name, dir] of Object.entries(dirs)) {
32730
33822
  if (!existed[name] && readdirSync2(dir).length === 0) {
32731
- await writeFile11(join24(dir, ".gitkeep"), "", "utf8").catch(() => void 0);
33823
+ await writeFile12(join26(dir, ".gitkeep"), "", "utf8").catch(() => void 0);
32732
33824
  }
32733
33825
  }
32734
33826
  return {
32735
33827
  cases: existed.cases ? "reused" : "created",
32736
33828
  fixtures: existed.fixtures ? "reused" : "created",
32737
- data: existed.data ? "reused" : "created"
33829
+ data: existed.data ? "reused" : "created",
33830
+ reviews: existed.reviews ? "reused" : "created"
32738
33831
  };
32739
33832
  }
32740
33833
 
@@ -32762,7 +33855,7 @@ async function runInit(root, options = {}) {
32762
33855
  console.log(
32763
33856
  config2.yaml === "created" ? "\u2713 \u9879\u76EE\u914D\u7F6E testpilot.yaml" : "\u2713 \u9879\u76EE\u914D\u7F6E testpilot.yaml(\u5DF2\u5B58\u5728,\u4FDD\u7559)"
32764
33857
  );
32765
- console.log(`\u2713 Runtime \u76EE\u5F55 ${join25(".testpilot", "artifacts")}`);
33858
+ console.log(`\u2713 Runtime \u76EE\u5F55 ${join27(".testpilot", "artifacts")}`);
32766
33859
  const serverMode = config2.projectLink.serverUrl ? `Server:${config2.projectLink.serverUrl}` : "\u672C\u5730\u6A21\u5F0F";
32767
33860
  console.log(`\u2713 \u9879\u76EE\u5173\u8054 ${config2.projectLink.projectId}(${serverMode})`);
32768
33861
  console.log(
@@ -32774,7 +33867,7 @@ async function runInit(root, options = {}) {
32774
33867
  const testStructure = await initTestStructure(root, project);
32775
33868
  const reuseNote = project.existingE2eDirs.filter((dir) => dir !== "tests/e2e");
32776
33869
  console.log(
32777
- testStructure.cases === "reused" ? "\u2713 \u6D4B\u8BD5\u76EE\u5F55 tests/e2e(\u5DF2\u5B58\u5728,\u590D\u7528)" : "\u2713 \u6D4B\u8BD5\u76EE\u5F55 tests/e2e/{cases,fixtures,data}"
33870
+ testStructure.cases === "reused" ? "\u2713 \u6D4B\u8BD5\u76EE\u5F55 tests/e2e(\u5DF2\u5B58\u5728,\u590D\u7528)" : "\u2713 \u6D4B\u8BD5\u76EE\u5F55 tests/e2e/{cases,fixtures,data,reviews}"
32778
33871
  );
32779
33872
  if (reuseNote.length > 0) {
32780
33873
  console.log(` \u26A0 \u68C0\u6D4B\u5230\u5176\u4ED6 E2E \u76EE\u5F55:${reuseNote.join(", ")}(\u4FDD\u7559\u4E0D\u52A8,\u672A\u5F3A\u5236\u8FC1\u79FB)`);
@@ -32786,7 +33879,7 @@ async function runInit(root, options = {}) {
32786
33879
  );
32787
33880
  } else {
32788
33881
  console.log(
32789
- skill.core === "created" ? `\u2713 Skill \u5B89\u88C5 ${join25(".agents", "skills", "testpilot")}` : "\u2713 Skill \u5B89\u88C5 .agents/skills/testpilot/(\u5DF2\u5B58\u5728,\u8DF3\u8FC7)"
33882
+ skill.core === "created" ? `\u2713 Skill \u5B89\u88C5 ${join27(".agents", "skills", "testpilot")}` : "\u2713 Skill \u5B89\u88C5 .agents/skills/testpilot/(\u5DF2\u5B58\u5728,\u8DF3\u8FC7)"
32790
33883
  );
32791
33884
  }
32792
33885
  console.log(
@@ -32863,9 +33956,20 @@ function makeListCommand() {
32863
33956
  // src/commands/report.ts
32864
33957
  import { exec } from "node:child_process";
32865
33958
  function makeReportCommand() {
32866
- return new Command("report").description("\u751F\u6210\u6D4B\u8BD5\u62A5\u544A(JSON + HTML)").option("--run <id>", "\u8FD0\u884C\u8BB0\u5F55 ID,\u9ED8\u8BA4\u6700\u8FD1\u4E00\u6B21").option("--open", "\u751F\u6210\u540E\u5728\u6D4F\u89C8\u5668\u6253\u5F00", false).action(async (options) => {
33959
+ return new Command("report").description("\u751F\u6210\u6D4B\u8BD5\u62A5\u544A(JSON + HTML)").option("--run <id>", "\u8FD0\u884C\u8BB0\u5F55 ID,\u9ED8\u8BA4\u6700\u8FD1\u4E00\u6B21").option("--open", "\u751F\u6210\u540E\u5728\u6D4F\u89C8\u5668\u6253\u5F00", false).option("--summary", "\u751F\u6210\u8DE8 run \u6C47\u603B\u62A5\u544A(summary.html + summary.json)", false).option("--last <n>", "\u6C47\u603B\u7EDF\u8BA1\u7A97\u53E3:\u6700\u8FD1 N \u6B21\u8FD0\u884C", "30").action(async (options) => {
32867
33960
  const client = new TestPilotClient();
32868
33961
  try {
33962
+ if (options.summary) {
33963
+ const result = await client.generateSummaryReport({
33964
+ last: Number(options.last) > 0 ? Number(options.last) : void 0
33965
+ });
33966
+ console.log(
33967
+ `\u2713 summary.html ${result.html}(\u7EDF\u8BA1\u6700\u8FD1 ${result.window.runs} \u6B21\u8FD0\u884C,\u6574\u4F53\u901A\u8FC7\u7387 ${(result.data.kpis.overallRate * 100).toFixed(1)}%)`
33968
+ );
33969
+ console.log(`\u2713 summary.json ${result.json}`);
33970
+ if (options.open) await openInBrowser(result.html);
33971
+ return;
33972
+ }
32869
33973
  const report = await client.generateReport(options.run);
32870
33974
  console.log(`\u2713 report.json ${report.json}`);
32871
33975
  console.log(`\u2713 report.html ${report.html}`);
@@ -32884,7 +33988,7 @@ function openInBrowser(file2) {
32884
33988
  }
32885
33989
 
32886
33990
  // src/commands/run.ts
32887
- import { join as join26 } from "node:path";
33991
+ import { join as join28 } from "node:path";
32888
33992
  function makeRunCommand() {
32889
33993
  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) => {
32890
33994
  const client = new TestPilotClient();
@@ -32906,9 +34010,9 @@ function makeRunCommand() {
32906
34010
  ].filter(Boolean).join(",");
32907
34011
  console.log("");
32908
34012
  console.log(
32909
- `Run ${summary.runId}${summary.cancelled ? "(\u5DF2\u53D6\u6D88)" : ""}: ${summary.status.toUpperCase()} \u2014 \u7528\u4F8B \u2713${summary.totals.passed} \u2717${summary.totals.failed},\u6B65\u9AA4 \u2713${summary.totals.stepsPassed} \u2717${summary.totals.stepsFailed} \u21B7${summary.totals.stepsSkipped}${notes ? `(${notes})` : ""}`
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})` : ""}`
32910
34014
  );
32911
- console.log(`\u4EA7\u7269\u76EE\u5F55:${join26(".testpilot", "artifacts", "runs", summary.runId)}`);
34015
+ console.log(`\u4EA7\u7269\u76EE\u5F55:${join28(".testpilot", "artifacts", "runs", summary.runId)}`);
32912
34016
  console.log("\u67E5\u770B\u62A5\u544A:npx csspilot report");
32913
34017
  if (summary.status === "failed") process.exitCode = 1;
32914
34018
  } catch (err) {
@@ -32933,12 +34037,12 @@ function consoleEventHandler(event) {
32933
34037
  `);
32934
34038
  break;
32935
34039
  case "case-started":
32936
- console.log(`\u25B6 ${event.caseId} (${event.file})`);
34040
+ console.log(`\u25B6 ${event.caseId}#${event.rowId ?? "default"} (${event.file})`);
32937
34041
  break;
32938
34042
  case "step-finished": {
32939
- const mark = event.status === "passed" ? " \u2713" : event.status === "failed" ? " \u2717" : " \u21B7";
34043
+ const mark = event.status === "passed" ? " \u2713" : event.status === "failed" ? " \u2717" : event.status === "warning" ? " \u26A0" : " \u21B7";
32940
34044
  console.log(
32941
- `${mark} ${event.target}/${event.action} ${event.durationMs}ms${event.error ? ` \u2014 ${event.error}` : ""}`
34045
+ `${mark} ${event.phase ?? "steps"} ${event.target}/${event.action} ${event.durationMs}ms${event.error ? ` \u2014 ${event.error}` : ""}`
32942
34046
  );
32943
34047
  break;
32944
34048
  }
@@ -32948,7 +34052,7 @@ function consoleEventHandler(event) {
32948
34052
  }
32949
34053
 
32950
34054
  // src/commands/sync-metadata.ts
32951
- import { relative } from "node:path";
34055
+ import { relative as relative2 } from "node:path";
32952
34056
  function makeSyncMetadataCommand() {
32953
34057
  return new Command("sync-metadata").description("\u540C\u6B65 Case Metadata \u5FEB\u7167\u5230 TestPilot Server(CI / git push \u540E\u4F7F\u7528)").option("--server <url>", "TestPilot Server \u5730\u5740(\u9ED8\u8BA4\u53D6 project.json \u6216 testpilot.yaml)").action(async (options) => {
32954
34058
  const client = new TestPilotClient();
@@ -32975,7 +34079,7 @@ function makeSyncMetadataCommand() {
32975
34079
  const cases = infos.filter((item) => item.valid && item.case).map((item) => ({
32976
34080
  id: item.case.id,
32977
34081
  title: item.case.name,
32978
- file: relative(process.cwd(), item.file).split("\\").join("/"),
34082
+ file: relative2(process.cwd(), item.file).split("\\").join("/"),
32979
34083
  tags: item.case.tags ?? []
32980
34084
  }));
32981
34085
  const skipped = infos.length - cases.length;
@@ -33019,12 +34123,12 @@ function makeSyncMetadataCommand() {
33019
34123
  }
33020
34124
 
33021
34125
  // src/commands/update.ts
33022
- import { writeFile as writeFile12 } from "node:fs/promises";
33023
- import { join as join27 } from "node:path";
34126
+ import { writeFile as writeFile13 } from "node:fs/promises";
34127
+ import { join as join29 } from "node:path";
33024
34128
  function makeUpdateCommand() {
33025
34129
  const update = new Command("update").description("\u540C\u6B65 TestPilot \u63A5\u5165\u7269\u5230\u5F53\u524D CLI \u7248\u672C(Skill / .env.example;\u4E0D\u89E6\u78B0 .env \u4E0E\u7528\u4F8B)").option("--force", "Skill \u7248\u672C\u4E00\u81F4\u65F6\u4E5F\u5F3A\u5236\u8986\u76D6\u672C\u5730\u4FEE\u6539", false).action(async (options) => {
33026
34130
  const root = process.cwd();
33027
- const dest = join27(".agents", "skills", "testpilot");
34131
+ const dest = join29(".agents", "skills", "testpilot");
33028
34132
  const skill = await syncSkillFiles(root, { force: options.force });
33029
34133
  switch (skill.core) {
33030
34134
  case "created":
@@ -33038,7 +34142,7 @@ function makeUpdateCommand() {
33038
34142
  default:
33039
34143
  console.log(`\u2713 Skill \u5DF2\u662F\u6700\u65B0 v${skill.bundledVersion ?? "?"}`);
33040
34144
  }
33041
- await writeFile12(join27(root, ".env.example"), renderDotEnvExample(), "utf8");
34145
+ await writeFile13(join29(root, ".env.example"), renderDotEnvExample(), "utf8");
33042
34146
  console.log("\u2713 .env.example \u5DF2\u540C\u6B65\u4E3A\u5F53\u524D\u7248\u672C\u7684\u793A\u4F8B(.env \u4E0D\u53D7\u5F71\u54CD)");
33043
34147
  const gitignore = await ensureGitignoreEntry(root, ".env");
33044
34148
  console.log(
@@ -33085,10 +34189,13 @@ function makeValidateCommand() {
33085
34189
  }
33086
34190
 
33087
34191
  // src/cli.ts
33088
- var CLI_VERSION = "0.4.0";
34192
+ var CLI_VERSION = "0.6.0";
33089
34193
  async function createCli() {
33090
34194
  const program2 = new Command();
33091
- program2.name("csspilot").description("TestPilot \u2014 AI Native \u8DE8\u7AEF\u4E1A\u52A1\u6D4B\u8BD5\u57FA\u7840\u8BBE\u65BD CLI").version(CLI_VERSION).addCommand(makeInitCommand()).addCommand(makeValidateCommand()).addCommand(makeListCommand()).addCommand(makeRunCommand()).addCommand(makeReportCommand()).addCommand(makeSyncMetadataCommand()).addCommand(makeDoctorCommand()).addCommand(makeUpdateCommand()).addCommand(makeCiCommand());
34195
+ 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", () => {
34196
+ console.log(CLI_VERSION);
34197
+ process.exit(0);
34198
+ }).addCommand(makeInitCommand()).addCommand(makeValidateCommand()).addCommand(makeListCommand()).addCommand(makeRunCommand()).addCommand(makeReportCommand()).addCommand(makeSyncMetadataCommand()).addCommand(makeDoctorCommand()).addCommand(makeUpdateCommand()).addCommand(makeCiCommand());
33092
34199
  return program2;
33093
34200
  }
33094
34201
  export {