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