csspilot 0.5.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, resolve6) {
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, resolve6) {
15428
15428
  let result = NONE;
15429
15429
  const check2 = (child) => {
15430
15430
  if (result !== PROVEN && child?._zod) {
15431
- const answer = isRecursive(child, stack, resolve6);
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, resolve6) {
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, resolve6) : 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, resolve6) {
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 ?? (resolve6 ? 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";
30346
+ import { existsSync as existsSync8 } from "node:fs";
30342
30347
  import { join as join24 } from "node:path";
30343
30348
 
30344
30349
  // ../../packages/sdk/src/client.ts
30345
- import { isAbsolute as isAbsolute3, join as join19 } 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,41 @@ 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 readFile7 } from "node:fs/promises";
30962
+ import { readFile as readFile8 } from "node:fs/promises";
30720
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 { dirname as dirname2, join as join8, resolve as resolve3 } from "node:path";
30967
+ import { dirname as dirname3, join as join8, resolve as resolve4 } from "node:path";
30725
30968
 
30726
30969
  // ../../packages/reporter/src/classify.ts
30727
30970
  var FAILURE_CATEGORIES = [
@@ -30751,14 +30994,14 @@ function classifyFailure(message) {
30751
30994
  }
30752
30995
 
30753
30996
  // ../../packages/reporter/src/history.ts
30754
- import { readFile as readFile4, readdir as readdir2 } from "node:fs/promises";
30997
+ import { readFile as readFile5, readdir as readdir2 } from "node:fs/promises";
30755
30998
  import { join as join7 } from "node:path";
30756
30999
  async function collectHistory(runsRoot2, skipRunId, limit = 12) {
30757
31000
  const entries = (await readdir2(runsRoot2).catch(() => [])).filter((name) => /^\d{8}-\d{3}$/.test(name) && name !== skipRunId).sort().slice(-limit);
30758
31001
  const points = [];
30759
31002
  for (const runId of entries) {
30760
31003
  try {
30761
- const summary = JSON.parse(await readFile4(join7(runsRoot2, runId, "result.json"), "utf8"));
31004
+ const summary = JSON.parse(await readFile5(join7(runsRoot2, runId, "result.json"), "utf8"));
30762
31005
  points.push({
30763
31006
  runId,
30764
31007
  startedAt: summary.startedAt,
@@ -30779,12 +31022,19 @@ function toSummaryView(summary) {
30779
31022
  runId: summary.runId,
30780
31023
  status: summary.status,
30781
31024
  durationMs: summary.durationMs,
30782
- 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
+ },
30783
31032
  steps: {
30784
31033
  total: summary.totals.steps,
30785
31034
  passed: summary.totals.stepsPassed,
30786
31035
  failed: summary.totals.stepsFailed,
30787
- skipped: summary.totals.stepsSkipped
31036
+ skipped: summary.totals.stepsSkipped,
31037
+ warning: summary.totals.stepsWarning ?? 0
30788
31038
  }
30789
31039
  };
30790
31040
  }
@@ -30792,9 +31042,9 @@ function toSummaryView(summary) {
30792
31042
  // ../../packages/reporter/src/report.ts
30793
31043
  var HISTORY_LIMIT = 12;
30794
31044
  async function writeReports(runDir, summary, options = {}) {
30795
- const dir = resolve3(runDir);
31045
+ const dir = resolve4(runDir);
30796
31046
  const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
30797
- const history = options.history ?? await collectHistory(dirname2(dir), summary.runId, options.historyLimit ?? HISTORY_LIMIT);
31047
+ const history = options.history ?? await collectHistory(dirname3(dir), summary.runId, options.historyLimit ?? HISTORY_LIMIT);
30798
31048
  const failures = [];
30799
31049
  const cases = summary.cases.map((item) => {
30800
31050
  const categories = /* @__PURE__ */ new Set();
@@ -30853,7 +31103,7 @@ function renderHtml(data) {
30853
31103
  const view = toSummaryView(data.summary);
30854
31104
  const summary = data.summary;
30855
31105
  const trendSvg = renderTrend(data);
30856
- const casesHtml = summary.cases.map((item) => caseHtml(item)).join("\n");
31106
+ const casesHtml = groupedCasesHtml(summary.cases);
30857
31107
  const chips = categoryChips(data);
30858
31108
  return `<!doctype html>
30859
31109
  <html lang="zh-CN">
@@ -30861,13 +31111,13 @@ function renderHtml(data) {
30861
31111
  <meta charset="utf-8" />
30862
31112
  <title>TestPilot Report ${escapeHtml(summary.runId)}</title>
30863
31113
  <style>
30864
- :root { --ok:#1a7f37; --fail:#cf222e; --skip:#6e7781; --line:#d0d7de; --muted:#59636e; }
31114
+ :root { --ok:#1a7f37; --fail:#cf222e; --warn:#9a6700; --skip:#6e7781; --line:#d0d7de; --muted:#59636e; }
30865
31115
  * { box-sizing: border-box; }
30866
31116
  body { font-family: system-ui, "Segoe UI", sans-serif; margin: 0; color: #1f2328; background: #f6f8fa; }
30867
31117
  .wrap { max-width: 1080px; margin: 0 auto; padding: 20px 16px 48px; }
30868
31118
  h1 { font-size: 20px; margin: 8px 0; } h2 { font-size: 15px; margin: 24px 0 8px; }
30869
31119
  .badge { display: inline-block; padding: 2px 10px; border-radius: 10px; color: #fff; font-size: 12px; }
30870
- .passed { background: var(--ok); } .failed { background: var(--fail); } .skipped { background: var(--skip); }
31120
+ .passed { background: var(--ok); } .failed { background: var(--fail); } .warning { background: var(--warn); } .skipped { background: var(--skip); }
30871
31121
  code { background: #eef1f4; padding: 1px 5px; border-radius: 4px; font-size: 12px; }
30872
31122
  .cards { display: flex; gap: 12px; flex-wrap: wrap; margin: 14px 0; }
30873
31123
  .card { background: #fff; border: 1px solid var(--line); border-radius: 8px; padding: 10px 16px; min-width: 130px; }
@@ -30880,6 +31130,8 @@ function renderHtml(data) {
30880
31130
  .btn { border: 1px solid var(--line); background: #fff; border-radius: 6px; padding: 4px 10px; font-size: 12px; cursor: pointer; }
30881
31131
  #count { color: var(--muted); font-size: 12px; margin-left: auto; }
30882
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; }
30883
31135
  details.case > summary { cursor: pointer; padding: 10px 14px; font-size: 14px; list-style: none; }
30884
31136
  details.case > summary::-webkit-details-marker { display: none; }
30885
31137
  details.case > summary:hover { background: #f6f8fa; }
@@ -30902,6 +31154,8 @@ function renderHtml(data) {
30902
31154
  <div class="cards">
30903
31155
  <div class="card"><b>${view.cases.passed}<span style="color:var(--ok)"> \u2713</span></b><span>\u901A\u8FC7\u7528\u4F8B</span></div>
30904
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>
30905
31159
  <div class="card"><b>${view.steps.passed}<span style="color:var(--ok)"> \u2713</span></b><span>\u901A\u8FC7\u6B65\u9AA4</span></div>
30906
31160
  <div class="card"><b>${view.steps.failed}<span style="color:var(--fail)"> \u2717</span></b><span>\u5931\u8D25\u6B65\u9AA4</span></div>
30907
31161
  <div class="card"><b>${view.steps.skipped} \u21B7</b><span>\u8DF3\u8FC7\u6B65\u9AA4</span></div>
@@ -30988,26 +31242,44 @@ function renderHtml(data) {
30988
31242
  </body>
30989
31243
  </html>`;
30990
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
+ }
30991
31259
  function caseHtml(item) {
30992
31260
  const rows = item.steps.map((step) => stepHtml(step)).join("\n");
30993
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>` : "";
30994
31264
  const links = [];
30995
31265
  if (item.video) links.push(`<a href="${escapeHtml(item.video)}">\u89C6\u9891</a>`);
30996
31266
  if (item.trace) links.push(`<a href="${escapeHtml(item.trace)}">Trace</a>`);
30997
31267
  const linksLine = links.length > 0 ? `<p>${links.join(" \xB7 ")}</p>` : "";
30998
- const search = escapeHtml(`${item.caseId} ${item.caseName} ${item.file}`).replaceAll('"', "&quot;");
31268
+ const search = escapeHtml(`${item.caseId} ${item.rowId ?? "default"} ${item.caseName} ${item.file}`).replaceAll('"', "&quot;");
30999
31269
  const cats = escapeHtml(item.categories.join(","));
31000
31270
  return `
31001
31271
  <details class="case" data-status="${item.status}" data-cats="${cats}" data-search="${search}">
31002
31272
  <summary>
31003
31273
  <span class="badge ${item.status}">${item.status}</span>
31004
- <b>${escapeHtml(item.caseId)}</b> \xB7 ${escapeHtml(item.caseName)}
31274
+ <b>\u6570\u636E\u884C ${escapeHtml(item.rowId ?? "default")}</b> \xB7 ${escapeHtml(item.caseName)}
31005
31275
  <span class="muted">\xB7 ${item.steps.length} \u6B65 \xB7 ${(item.durationMs / 1e3).toFixed(1)}s</span>
31006
31276
  </summary>
31007
31277
  ${errorLine}
31278
+ ${skipLine}
31279
+ ${warningLine}
31008
31280
  ${linksLine}
31009
31281
  <table>
31010
- <tr><th>#</th><th>target</th><th>action</th><th>\u72B6\u6001</th><th>\u8017\u65F6</th><th>\u8BE6\u60C5</th></tr>
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>
31011
31283
  ${rows}
31012
31284
  </table>
31013
31285
  </details>`;
@@ -31022,8 +31294,14 @@ function stepHtml(step) {
31022
31294
  }
31023
31295
  }
31024
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
+ }
31025
31302
  return `<tr>
31026
31303
  <td class="num">${step.index}</td>
31304
+ <td>${escapeHtml(step.phase ?? "steps")}</td>
31027
31305
  <td>${escapeHtml(step.target)}</td>
31028
31306
  <td>${escapeHtml(step.action)}</td>
31029
31307
  <td><span class="badge ${step.status}">${step.status}</span></td>
@@ -31080,7 +31358,7 @@ function embedJson(data) {
31080
31358
 
31081
31359
  // ../../packages/reporter/src/summary-report.ts
31082
31360
  import { mkdir as mkdir4, writeFile as writeFile5 } from "node:fs/promises";
31083
- import { join as join9, resolve as resolve4 } from "node:path";
31361
+ import { join as join9, resolve as resolve5 } from "node:path";
31084
31362
  function escapeHtml2(value) {
31085
31363
  return String(value).replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
31086
31364
  }
@@ -31098,6 +31376,8 @@ function buildSummaryData(runs) {
31098
31376
  executions.push({
31099
31377
  runId,
31100
31378
  caseId: item.caseId,
31379
+ rowId: item.rowId,
31380
+ identity: item.rowId && item.rowId !== "default" ? `${item.caseId}#${item.rowId}` : item.caseId,
31101
31381
  caseName: item.caseName,
31102
31382
  status: item.status,
31103
31383
  category,
@@ -31115,10 +31395,11 @@ function buildSummaryData(runs) {
31115
31395
  }
31116
31396
  const byCase = /* @__PURE__ */ new Map();
31117
31397
  for (const exec2 of executions) {
31118
- let agg = byCase.get(exec2.caseId);
31398
+ let agg = byCase.get(exec2.identity);
31119
31399
  if (!agg) {
31120
31400
  agg = {
31121
31401
  caseId: exec2.caseId,
31402
+ rowId: exec2.rowId,
31122
31403
  caseName: exec2.caseName,
31123
31404
  platforms: [],
31124
31405
  runs: 0,
@@ -31128,17 +31409,18 @@ function buildSummaryData(runs) {
31128
31409
  lastStatus: "unknown",
31129
31410
  flaky: false
31130
31411
  };
31131
- byCase.set(exec2.caseId, agg);
31412
+ byCase.set(exec2.identity, agg);
31132
31413
  }
31133
31414
  agg.runs++;
31134
31415
  if (exec2.status === "passed") agg.passed++;
31135
- else agg.failed++;
31416
+ else if (exec2.status === "failed") agg.failed++;
31136
31417
  for (const platform of exec2.platforms) {
31137
31418
  if (!agg.platforms.includes(platform)) agg.platforms.push(platform);
31138
31419
  }
31139
31420
  }
31140
31421
  for (const agg of byCase.values()) {
31141
- const last = [...executions].reverse().find((exec2) => exec2.caseId === agg.caseId);
31422
+ const identity = agg.rowId && agg.rowId !== "default" ? `${agg.caseId}#${agg.rowId}` : agg.caseId;
31423
+ const last = [...executions].reverse().find((exec2) => exec2.identity === identity);
31142
31424
  agg.lastStatus = last?.status ?? "unknown";
31143
31425
  agg.lastCategory = last?.category;
31144
31426
  agg.passRate = agg.runs > 0 ? agg.passed / agg.runs : 0;
@@ -31349,7 +31631,7 @@ function renderSummaryHtml(data) {
31349
31631
  </html>`;
31350
31632
  }
31351
31633
  async function writeSummaryReport(artifactsDir, data) {
31352
- const dir = resolve4(artifactsDir);
31634
+ const dir = resolve5(artifactsDir);
31353
31635
  await mkdir4(dir, { recursive: true });
31354
31636
  const htmlPath = join9(dir, "summary.html");
31355
31637
  const jsonPath = join9(dir, "summary.json");
@@ -31359,8 +31641,8 @@ async function writeSummaryReport(artifactsDir, data) {
31359
31641
  }
31360
31642
 
31361
31643
  // ../../packages/sdk/src/runs.ts
31362
- import { readFile as readFile6, readdir as readdir4 } from "node:fs/promises";
31363
- import { isAbsolute as isAbsolute2, join as join17 } 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";
31364
31646
 
31365
31647
  // ../../packages/execution-engine/src/artifacts/index.ts
31366
31648
  import { mkdir as mkdir9 } from "node:fs/promises";
@@ -31368,7 +31650,7 @@ import { join as join13 } from "node:path";
31368
31650
 
31369
31651
  // ../../packages/evidence/src/log.ts
31370
31652
  import { appendFile, mkdir as mkdir5 } from "node:fs/promises";
31371
- import { dirname as dirname3 } from "node:path";
31653
+ import { dirname as dirname4 } from "node:path";
31372
31654
  var RunLogger = class {
31373
31655
  constructor(filePath) {
31374
31656
  this.filePath = filePath;
@@ -31383,7 +31665,7 @@ var RunLogger = class {
31383
31665
  return this.write("ERROR", message);
31384
31666
  }
31385
31667
  async write(level, message) {
31386
- await mkdir5(dirname3(this.filePath), { recursive: true });
31668
+ await mkdir5(dirname4(this.filePath), { recursive: true });
31387
31669
  const line = `${(/* @__PURE__ */ new Date()).toISOString()} ${level} ${message}
31388
31670
  `;
31389
31671
  await appendFile(this.filePath, line, "utf8");
@@ -31392,30 +31674,30 @@ var RunLogger = class {
31392
31674
 
31393
31675
  // ../../packages/evidence/src/screenshot.ts
31394
31676
  import { mkdir as mkdir6, writeFile as writeFile6 } from "node:fs/promises";
31395
- import { dirname as dirname4, join as join10 } from "node:path";
31677
+ import { dirname as dirname5, join as join10 } from "node:path";
31396
31678
  async function saveScreenshot(dir, fileName, data) {
31397
31679
  const file2 = join10(dir, fileName);
31398
- await mkdir6(dirname4(file2), { recursive: true });
31680
+ await mkdir6(dirname5(file2), { recursive: true });
31399
31681
  await writeFile6(file2, data);
31400
31682
  return file2;
31401
31683
  }
31402
31684
 
31403
31685
  // ../../packages/evidence/src/trace.ts
31404
31686
  import { mkdir as mkdir7, writeFile as writeFile7 } from "node:fs/promises";
31405
- import { dirname as dirname5, join as join11 } from "node:path";
31687
+ import { dirname as dirname6, join as join11 } from "node:path";
31406
31688
  async function saveTrace(dir, fileName, data) {
31407
31689
  const file2 = join11(dir, fileName);
31408
- await mkdir7(dirname5(file2), { recursive: true });
31690
+ await mkdir7(dirname6(file2), { recursive: true });
31409
31691
  await writeFile7(file2, data);
31410
31692
  return file2;
31411
31693
  }
31412
31694
 
31413
31695
  // ../../packages/evidence/src/video.ts
31414
31696
  import { mkdir as mkdir8, writeFile as writeFile8 } from "node:fs/promises";
31415
- import { dirname as dirname6, join as join12 } from "node:path";
31697
+ import { dirname as dirname7, join as join12 } from "node:path";
31416
31698
  async function saveVideo(dir, fileName, data) {
31417
31699
  const file2 = join12(dir, fileName);
31418
- await mkdir8(dirname6(file2), { recursive: true });
31700
+ await mkdir8(dirname7(file2), { recursive: true });
31419
31701
  await writeFile8(file2, data);
31420
31702
  return file2;
31421
31703
  }
@@ -31465,6 +31747,11 @@ var ExecutionContext = class {
31465
31747
  get(name) {
31466
31748
  return this.variables.get(name);
31467
31749
  }
31750
+ seed(values) {
31751
+ for (const [name, value] of Object.entries(values)) {
31752
+ this.variables.set(name, value);
31753
+ }
31754
+ }
31468
31755
  /**
31469
31756
  * 将字符串中的 ${name} 替换为已提取的变量值;未定义的引用保持原样(校验阶段已拦截)。
31470
31757
  * 名字允许点号,用于引擎注入的 account.username 一类凭据变量(用户 extract 变量名不含点号)。
@@ -31529,7 +31816,7 @@ async function ensureRunDir(runsRoot2, runId) {
31529
31816
 
31530
31817
  // ../../packages/execution-engine/src/events/index.ts
31531
31818
  import { appendFile as appendFile2, mkdir as mkdir11 } from "node:fs/promises";
31532
- import { dirname as dirname7 } from "node:path";
31819
+ import { dirname as dirname8 } from "node:path";
31533
31820
  var EventBus = class {
31534
31821
  subscribers = [];
31535
31822
  subscribe(subscriber) {
@@ -31543,7 +31830,7 @@ var EventBus = class {
31543
31830
  };
31544
31831
  function ndjsonSubscriber(filePath) {
31545
31832
  return async (event) => {
31546
- await mkdir11(dirname7(filePath), { recursive: true });
31833
+ await mkdir11(dirname8(filePath), { recursive: true });
31547
31834
  await appendFile2(filePath, `${JSON.stringify(event)}
31548
31835
  `, "utf8");
31549
31836
  };
@@ -31624,11 +31911,19 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
31624
31911
  status: "passed",
31625
31912
  durationMs: 0
31626
31913
  };
31914
+ let captureStarted = false;
31627
31915
  try {
31628
31916
  const locator = step.locator ? {
31629
31917
  text: step.locator.text !== void 0 ? context.resolve(step.locator.text) : void 0,
31630
31918
  css: step.locator.css !== void 0 ? context.resolve(step.locator.css) : void 0
31631
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
+ }
31632
31927
  switch (step.action) {
31633
31928
  case "launch":
31634
31929
  await adapter.launch();
@@ -31699,7 +31994,29 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
31699
31994
  throw new Error(`\u4E0D\u652F\u6301\u7684 action:${String(exhaustive)}`);
31700
31995
  }
31701
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
+ }
31702
32012
  } catch (err) {
32013
+ if (captureStarted) {
32014
+ try {
32015
+ await adapter.stopRequestCapture?.();
32016
+ } catch {
32017
+ }
32018
+ captureStarted = false;
32019
+ }
31703
32020
  result.status = "failed";
31704
32021
  result.error = err instanceof Error ? err.message : String(err);
31705
32022
  try {
@@ -31744,92 +32061,195 @@ function resolveDeep(value, context) {
31744
32061
  function snippet(text, limit = SNIPPET_LIMIT) {
31745
32062
  return text.length > limit ? `${text.slice(0, limit)}\u2026` : text;
31746
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
+ }
31747
32092
 
31748
32093
  // ../../packages/execution-engine/src/engine/execution-engine.ts
31749
32094
  var ExecutionEngine = class {
31750
32095
  async runCase(data, options) {
31751
- const { runId, file: file2, artifacts, bus, resolver } = options;
32096
+ const { runId, file: file2, artifacts, bus, resolver, rowId, rowIndex } = options;
31752
32097
  const startedAt = /* @__PURE__ */ new Date();
31753
32098
  const context = new ExecutionContext();
32099
+ context.seed(options.initialVariables);
31754
32100
  if (data.accountRef && options.accounts?.[data.accountRef] !== void 0) {
31755
32101
  context.seedAccount(options.accounts[data.accountRef]);
31756
32102
  }
31757
32103
  const steps = [];
32104
+ const warnings = [];
31758
32105
  const evidenceAdapters = /* @__PURE__ */ new Map();
31759
32106
  let caseError;
31760
- await bus.emit({ type: "case-started", runId, caseId: data.id, file: file2, ts: startedAt.toISOString() });
31761
- await artifacts.logger.info(`case ${data.id} started (${file2})`);
31762
- 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
+ };
31763
32133
  await bus.emit({
31764
- type: "step-started",
32134
+ type: "case-finished",
31765
32135
  runId,
31766
32136
  caseId: data.id,
31767
- index,
31768
- target: step.target,
31769
- action: step.action,
31770
- ts: (/* @__PURE__ */ new Date()).toISOString()
32137
+ rowId,
32138
+ status: "skipped",
32139
+ durationMs: skipped.durationMs,
32140
+ ts: finishedAt2.toISOString()
31771
32141
  });
31772
- let result;
31773
- try {
31774
- const adapter = await (await resolver.session(step.target)).get();
31775
- if (adapter.startEvidence && !evidenceAdapters.has(step.target)) {
31776
- try {
31777
- await adapter.startEvidence(data.id);
31778
- evidenceAdapters.set(step.target, adapter);
31779
- } catch (err) {
31780
- await artifacts.logger.info(
31781
- `evidence start failed for ${step.target}: ${err instanceof Error ? err.message : String(err)}`
31782
- );
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
+ }
31783
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
+ };
31784
32186
  }
31785
- result = await executeStep(step, index, adapter, context, artifacts, {
31786
- screenshotName: `${data.id}-step-${String(index).padStart(2, "0")}`
31787
- });
31788
- } catch (err) {
31789
- 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,
31790
32202
  index,
31791
32203
  target: step.target,
31792
32204
  action: step.action,
31793
- status: "failed",
31794
- durationMs: 0,
31795
- error: err instanceof Error ? err.message : String(err)
31796
- };
31797
- }
31798
- steps.push(result);
31799
- await artifacts.logger.info(`step ${index} ${step.action} -> ${result.status}${result.error ? ` - ${result.error}` : ""}`);
31800
- await bus.emit({
31801
- type: "step-finished",
31802
- runId,
31803
- caseId: data.id,
31804
- index,
31805
- target: step.target,
31806
- action: step.action,
31807
- status: result.status,
31808
- durationMs: result.durationMs,
31809
- error: result.error,
31810
- ts: (/* @__PURE__ */ new Date()).toISOString()
31811
- });
31812
- if (result.status === "failed") {
31813
- caseError = result.error;
31814
- for (const [skipIndex, skippedStep] of data.steps.entries()) {
31815
- if (skipIndex <= index) continue;
31816
- steps.push({
31817
- index: skipIndex,
31818
- target: skippedStep.target,
31819
- action: skippedStep.action,
31820
- status: "skipped",
31821
- durationMs: 0
31822
- });
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;
31823
32225
  }
31824
- 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
+ });
31825
32242
  }
31826
32243
  }
32244
+ await runPhase("teardown", data.teardown ?? []);
31827
32245
  const finishedAt = /* @__PURE__ */ new Date();
31828
32246
  const status = caseError ? "failed" : "passed";
31829
32247
  const caseResult = {
31830
32248
  caseId: data.id,
31831
32249
  caseName: data.name,
31832
32250
  file: file2,
32251
+ rowId,
32252
+ rowIndex,
31833
32253
  status,
31834
32254
  steps,
31835
32255
  startedAt: startedAt.toISOString(),
@@ -31837,15 +32257,16 @@ var ExecutionEngine = class {
31837
32257
  durationMs: finishedAt.getTime() - startedAt.getTime()
31838
32258
  };
31839
32259
  if (caseError) caseResult.error = caseError;
32260
+ if (warnings.length > 0) caseResult.warnings = warnings;
31840
32261
  for (const [target, adapter] of evidenceAdapters) {
31841
32262
  if (!adapter.stopEvidence) continue;
31842
32263
  try {
31843
- const evidence = await adapter.stopEvidence(data.id);
32264
+ const evidence = await adapter.stopEvidence(executionId);
31844
32265
  if (evidence.video) {
31845
- caseResult.video = await artifacts.saveVideo(`${data.id}-${target}.webm`, evidence.video);
32266
+ caseResult.video = await artifacts.saveVideo(`${executionId}-${target}.webm`, evidence.video);
31846
32267
  }
31847
32268
  if (evidence.trace) {
31848
- caseResult.trace = await artifacts.saveTrace(`${data.id}-${target}.zip`, evidence.trace);
32269
+ caseResult.trace = await artifacts.saveTrace(`${executionId}-${target}.zip`, evidence.trace);
31849
32270
  }
31850
32271
  } catch (err) {
31851
32272
  await artifacts.logger.info(
@@ -31857,33 +32278,45 @@ var ExecutionEngine = class {
31857
32278
  type: "case-finished",
31858
32279
  runId,
31859
32280
  caseId: data.id,
32281
+ rowId,
31860
32282
  status,
31861
32283
  durationMs: finishedAt.getTime() - startedAt.getTime(),
31862
32284
  ts: finishedAt.toISOString()
31863
32285
  });
31864
- await artifacts.logger.info(`case ${data.id} ${status}`);
32286
+ await artifacts.logger.info(`case ${data.id}#${rowId} ${status}`);
31865
32287
  return caseResult;
31866
32288
  }
31867
32289
  };
32290
+ function safeArtifactName(value) {
32291
+ return value.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "case";
32292
+ }
31868
32293
 
31869
32294
  // ../../packages/execution-engine/src/engine/result-collector.ts
31870
32295
  function buildRunSummary(runId, startedAt, cases, options = {}) {
31871
32296
  const finishedAt = /* @__PURE__ */ new Date();
31872
32297
  const totals = {
32298
+ templates: options.templates ?? new Set(cases.map((item) => item.caseId)).size,
31873
32299
  cases: cases.length,
31874
32300
  passed: cases.filter((item) => item.status === "passed").length,
31875
32301
  failed: cases.filter((item) => item.status === "failed").length,
32302
+ skipped: cases.filter((item) => item.status === "skipped").length,
32303
+ warnings: 0,
31876
32304
  steps: 0,
31877
32305
  stepsPassed: 0,
31878
32306
  stepsFailed: 0,
31879
- stepsSkipped: 0
32307
+ stepsSkipped: 0,
32308
+ stepsWarning: 0
31880
32309
  };
31881
32310
  for (const item of cases) {
31882
32311
  for (const step of item.steps) {
31883
32312
  totals.steps++;
31884
32313
  if (step.status === "passed") totals.stepsPassed++;
31885
32314
  else if (step.status === "failed") totals.stepsFailed++;
31886
- else totals.stepsSkipped++;
32315
+ else if (step.status === "skipped") totals.stepsSkipped++;
32316
+ else {
32317
+ totals.stepsWarning++;
32318
+ totals.warnings++;
32319
+ }
31887
32320
  }
31888
32321
  }
31889
32322
  return {
@@ -31891,7 +32324,7 @@ function buildRunSummary(runId, startedAt, cases, options = {}) {
31891
32324
  startedAt: startedAt.toISOString(),
31892
32325
  finishedAt: finishedAt.toISOString(),
31893
32326
  durationMs: finishedAt.getTime() - startedAt.getTime(),
31894
- status: totals.failed > 0 ? "failed" : "passed",
32327
+ status: totals.failed > 0 ? "failed" : totals.passed > 0 || totals.cases === 0 ? "passed" : "skipped",
31895
32328
  ...options.cancelled ? { cancelled: true } : {},
31896
32329
  cases,
31897
32330
  totals
@@ -31900,7 +32333,7 @@ function buildRunSummary(runId, startedAt, cases, options = {}) {
31900
32333
 
31901
32334
  // ../../packages/execution-engine/src/engine/test-runner.ts
31902
32335
  import { writeFile as writeFile9 } from "node:fs/promises";
31903
- import { join as join15, resolve as resolve5 } from "node:path";
32336
+ import { join as join15, resolve as resolve6 } from "node:path";
31904
32337
  var TestRunner = class {
31905
32338
  resolver;
31906
32339
  runsRoot;
@@ -31910,7 +32343,7 @@ var TestRunner = class {
31910
32343
  accounts;
31911
32344
  constructor(options) {
31912
32345
  this.resolver = options.resolver;
31913
- this.runsRoot = resolve5(options.runsRoot ?? join15(process.cwd(), ".testpilot", "artifacts", "runs"));
32346
+ this.runsRoot = resolve6(options.runsRoot ?? join15(process.cwd(), ".testpilot", "artifacts", "runs"));
31914
32347
  this.customRunId = options.runId;
31915
32348
  this.extraSubscriber = options.onEvent;
31916
32349
  this.signal = options.signal;
@@ -31938,6 +32371,11 @@ var TestRunner = class {
31938
32371
  await engine.runCase(item.data, {
31939
32372
  runId,
31940
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 ?? [],
31941
32379
  artifacts,
31942
32380
  bus,
31943
32381
  resolver: this.resolver,
@@ -31947,7 +32385,8 @@ var TestRunner = class {
31947
32385
  }
31948
32386
  const cancelled = this.signal?.aborted === true;
31949
32387
  await this.resolver.closeAll();
31950
- 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 });
31951
32390
  await bus.emit({
31952
32391
  type: "run-finished",
31953
32392
  runId,
@@ -31989,8 +32428,8 @@ async function openCaseContext(bundle, options, evidenceTmpDir) {
31989
32428
  }
31990
32429
 
31991
32430
  // ../../packages/adapter-playwright/src/page.ts
31992
- import { existsSync as existsSync2 } from "node:fs";
31993
- import { mkdtemp, readFile as readFile5, rm } from "node:fs/promises";
32431
+ import { existsSync as existsSync3 } from "node:fs";
32432
+ import { mkdtemp, readFile as readFile6, rm } from "node:fs/promises";
31994
32433
  import { tmpdir } from "node:os";
31995
32434
  import { join as join16 } from "node:path";
31996
32435
  var PlaywrightAdapter = class {
@@ -32000,6 +32439,7 @@ var PlaywrightAdapter = class {
32000
32439
  }
32001
32440
  target = "web";
32002
32441
  current;
32442
+ requestCapture;
32003
32443
  /** 浏览器在 create() 时已启动;context/page 按用例惰性创建,launch 保持幂等 */
32004
32444
  async launch() {
32005
32445
  }
@@ -32014,6 +32454,7 @@ var PlaywrightAdapter = class {
32014
32454
  }
32015
32455
  /** 结束取证:trace 落 zip、录屏在 context 关闭后完成,返回二者字节 */
32016
32456
  async stopEvidence(_caseId) {
32457
+ await this.stopRequestCapture().catch(() => []);
32017
32458
  if (!this.current) return {};
32018
32459
  const { context, page, evidenceTmpDir } = this.current;
32019
32460
  this.current = void 0;
@@ -32021,11 +32462,28 @@ var PlaywrightAdapter = class {
32021
32462
  await context.tracing.stop({ path: traceZipPath }).catch(() => void 0);
32022
32463
  const recordedVideoPath = await page.video()?.path().catch(() => void 0);
32023
32464
  await context.close().catch(() => void 0);
32024
- const video = recordedVideoPath && existsSync2(recordedVideoPath) ? await readFile5(recordedVideoPath) : void 0;
32025
- const trace = evidenceTmpDir && existsSync2(traceZipPath) ? await readFile5(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;
32026
32467
  if (evidenceTmpDir) await rm(evidenceTmpDir, { recursive: true, force: true }).catch(() => void 0);
32027
32468
  return { video, trace };
32028
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
+ }
32029
32487
  async navigate(url2) {
32030
32488
  const options = this.options;
32031
32489
  const page = await this.ensurePage();
@@ -32087,6 +32545,7 @@ var PlaywrightAdapter = class {
32087
32545
  throw new Error(`\u65E0\u6548\u7684 locator:${JSON.stringify(locator)}`);
32088
32546
  }
32089
32547
  async closeContextQuietly() {
32548
+ await this.stopRequestCapture().catch(() => []);
32090
32549
  if (!this.current) return;
32091
32550
  const { context } = this.current;
32092
32551
  this.current = void 0;
@@ -32125,8 +32584,8 @@ var automatorLauncher = {
32125
32584
  }
32126
32585
  }
32127
32586
  };
32128
- function delay(ms) {
32129
- return new Promise((resolve6) => setTimeout(resolve6, ms));
32587
+ function delay2(ms) {
32588
+ return new Promise((resolve7) => setTimeout(resolve7, ms));
32130
32589
  }
32131
32590
  async function connectWithRetry(wsEndpoint, timeoutMs) {
32132
32591
  const startedAt = Date.now();
@@ -32136,7 +32595,7 @@ async function connectWithRetry(wsEndpoint, timeoutMs) {
32136
32595
  return await automator.connect({ wsEndpoint });
32137
32596
  } catch (err) {
32138
32597
  lastError = err;
32139
- await delay(1e3);
32598
+ await delay2(1e3);
32140
32599
  }
32141
32600
  }
32142
32601
  throw lastError instanceof Error ? lastError : new Error(String(lastError));
@@ -32187,6 +32646,7 @@ var MiniAppAdapter = class {
32187
32646
  }
32188
32647
  target = "miniapp";
32189
32648
  program;
32649
+ requestCapture;
32190
32650
  /** 拉起开发者工具并建立自动化连接;需幂等 */
32191
32651
  async launch() {
32192
32652
  if (this.program) return;
@@ -32207,7 +32667,7 @@ var MiniAppAdapter = class {
32207
32667
  return;
32208
32668
  } catch (err) {
32209
32669
  lastError = err;
32210
- await delay(3e3);
32670
+ await delay2(3e3);
32211
32671
  }
32212
32672
  }
32213
32673
  throw lastError instanceof Error ? lastError : new Error(String(lastError));
@@ -32253,7 +32713,43 @@ var MiniAppAdapter = class {
32253
32713
  return Buffer.from(base643, "base64");
32254
32714
  });
32255
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
+ }
32256
32751
  async close() {
32752
+ await this.stopRequestCapture().catch(() => []);
32257
32753
  if (!this.program) return;
32258
32754
  const program2 = this.program;
32259
32755
  this.program = void 0;
@@ -32301,17 +32797,17 @@ var MiniAppAdapter = class {
32301
32797
  if (Date.now() - startedAt > ELEMENT_WAIT_MS) {
32302
32798
  throw new Error(`\u7B49\u5F85\u5143\u7D20\u8D85\u65F6(${ELEMENT_WAIT_MS}ms):${selector}`);
32303
32799
  }
32304
- await delay(500);
32800
+ await delay2(500);
32305
32801
  }
32306
32802
  }
32307
32803
  };
32308
32804
 
32309
32805
  // ../../packages/adapter-wechatide/src/project.ts
32310
- import { existsSync as existsSync3 } from "node:fs";
32806
+ import { existsSync as existsSync4 } from "node:fs";
32311
32807
  async function resolveMiniappConfig(input2) {
32312
32808
  const projectPath = input2.projectPath ?? process.env.TESTPILOT_MINIPROGRAM_PROJECT_PATH;
32313
32809
  if (!projectPath) return void 0;
32314
- if (!existsSync3(projectPath)) {
32810
+ if (!existsSync4(projectPath)) {
32315
32811
  throw new Error(`\u5C0F\u7A0B\u5E8F\u9879\u76EE\u8DEF\u5F84\u4E0D\u5B58\u5728:${projectPath}`);
32316
32812
  }
32317
32813
  const cliPath = input2.cliPath ?? process.env.WECHAT_DEVTOOLS_CLI ?? await findWechatDevToolsCli();
@@ -32486,7 +32982,7 @@ function createDefaultAdapters(config2) {
32486
32982
  async function runCases(options = {}) {
32487
32983
  const root = resolveRoot(options.root);
32488
32984
  const config2 = await loadProjectConfig(root);
32489
- const inputs = options.paths && options.paths.length > 0 ? options.paths.map((input2) => isAbsolute2(input2) ? input2 : join17(root, input2)) : [join17(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)];
32490
32986
  const cases = await collectCases(inputs, { root });
32491
32987
  if (cases.length === 0) {
32492
32988
  throw new RunError("ENO_CASES", `\u672A\u627E\u5230\u7528\u4F8B\u6587\u4EF6(\u67E5\u627E:${inputs.join(", ")})`);
@@ -32518,8 +33014,23 @@ async function runCases(options = {}) {
32518
33014
  signal: options.signal,
32519
33015
  accounts: options.accounts
32520
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();
32521
33030
  const summary = await runner.run(
32522
- runnable.map((item) => ({ data: item.case, file: item.file }))
33031
+ prepared.map(
33032
+ (item) => item
33033
+ )
32523
33034
  );
32524
33035
  return { summary, invalid, tagFiltered, missingAccounts };
32525
33036
  }
@@ -32543,20 +33054,20 @@ async function getRun(runId, options = {}) {
32543
33054
  const base = runsRoot(options.root);
32544
33055
  const file2 = join17(base, runId, RESULT_FILE);
32545
33056
  try {
32546
- return JSON.parse(await readFile6(file2, "utf8"));
33057
+ return JSON.parse(await readFile7(file2, "utf8"));
32547
33058
  } catch {
32548
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)`);
32549
33060
  }
32550
33061
  }
32551
33062
  async function getRunEvents(runId, options = {}) {
32552
33063
  const file2 = join17(runsRoot(options.root), runId, "events.ndjson");
32553
- const source = await readFile6(file2, "utf8").catch(() => "");
33064
+ const source = await readFile7(file2, "utf8").catch(() => "");
32554
33065
  return source.split("\n").filter((line) => line.trim().length > 0).map((line) => JSON.parse(line));
32555
33066
  }
32556
33067
  async function readRunMeta(base, runId) {
32557
33068
  try {
32558
33069
  const summary = JSON.parse(
32559
- await readFile6(join17(base, runId, RESULT_FILE), "utf8")
33070
+ await readFile7(join17(base, runId, RESULT_FILE), "utf8")
32560
33071
  );
32561
33072
  return {
32562
33073
  status: summary.status,
@@ -32580,7 +33091,7 @@ async function generateReport(runId, options = {}) {
32580
33091
  const runDir = join18(resolveRoot(options.root), RUNS_DIR, id);
32581
33092
  let summary;
32582
33093
  try {
32583
- summary = JSON.parse(await readFile7(join18(runDir, "result.json"), "utf8"));
33094
+ summary = JSON.parse(await readFile8(join18(runDir, "result.json"), "utf8"));
32584
33095
  } catch {
32585
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)`);
32586
33097
  }
@@ -32611,13 +33122,13 @@ async function generateSummaryReport(options = {}) {
32611
33122
  }
32612
33123
  async function readReport(runId, options = {}) {
32613
33124
  const file2 = join18(resolveRoot(options.root), RUNS_DIR, runId, "report.json");
32614
- const source = await readFile7(file2, "utf8").catch(() => void 0);
33125
+ const source = await readFile8(file2, "utf8").catch(() => void 0);
32615
33126
  return source ? JSON.parse(source) : void 0;
32616
33127
  }
32617
33128
 
32618
33129
  // ../../packages/sdk/src/client.ts
32619
33130
  function resolveInput(root, input2) {
32620
- return isAbsolute3(input2) ? input2 : join19(root, input2);
33131
+ return isAbsolute4(input2) ? input2 : join19(root, input2);
32621
33132
  }
32622
33133
  var TestPilotClient = class {
32623
33134
  /** 业务项目根目录(绝对路径) */
@@ -32693,13 +33204,13 @@ var TestPilotClient = class {
32693
33204
  };
32694
33205
 
32695
33206
  // src/commands/init/adapter-detector.ts
32696
- import { existsSync as existsSync4, readFileSync } from "node:fs";
33207
+ import { existsSync as existsSync5, readFileSync } from "node:fs";
32697
33208
  import { join as join20 } from "node:path";
32698
33209
  async function detectAdapters(root, projectType) {
32699
33210
  const required2 = projectType === "hybrid" ? ["playwright", "wechatide"] : projectType === "wechat-miniapp" ? ["wechatide"] : projectType === "web" ? ["playwright"] : [];
32700
33211
  const pkg = readPackageJsonDeps(root);
32701
33212
  const hasPlaywrightDep = pkg !== void 0 && ("playwright" in pkg || "@playwright/test" in pkg);
32702
- const playwrightInstalled = hasPlaywrightDep || existsSync4(join20(root, "node_modules", "playwright"));
33213
+ const playwrightInstalled = hasPlaywrightDep || existsSync5(join20(root, "node_modules", "playwright"));
32703
33214
  const devtoolsCli = await findWechatDevToolsCli().catch(() => void 0);
32704
33215
  return [
32705
33216
  {
@@ -32718,7 +33229,7 @@ async function detectAdapters(root, projectType) {
32718
33229
  }
32719
33230
  function readPackageJsonDeps(root) {
32720
33231
  const file2 = join20(root, "package.json");
32721
- if (!existsSync4(file2)) return void 0;
33232
+ if (!existsSync5(file2)) return void 0;
32722
33233
  try {
32723
33234
  const pkg = JSON.parse(readFileSync(file2, "utf8"));
32724
33235
  return { ...pkg.dependencies, ...pkg.devDependencies };
@@ -32728,13 +33239,13 @@ function readPackageJsonDeps(root) {
32728
33239
  }
32729
33240
 
32730
33241
  // src/commands/init/environment-initializer.ts
32731
- import { readFile as readFile8 } from "node:fs/promises";
33242
+ import { readFile as readFile9 } from "node:fs/promises";
32732
33243
  import { join as join21 } from "node:path";
32733
33244
  var ENV_REF_PATTERN = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
32734
33245
  async function collectEnvVarRefs(root) {
32735
33246
  let text;
32736
33247
  try {
32737
- text = await readFile8(join21(root, "testpilot.yaml"), "utf8");
33248
+ text = await readFile9(join21(root, "testpilot.yaml"), "utf8");
32738
33249
  } catch {
32739
33250
  return [];
32740
33251
  }
@@ -32750,7 +33261,7 @@ async function collectEnvVarRefs(root) {
32750
33261
  }
32751
33262
 
32752
33263
  // src/commands/init/project-detector.ts
32753
- import { existsSync as existsSync5, readFileSync as readFileSync2 } from "node:fs";
33264
+ import { existsSync as existsSync6, readFileSync as readFileSync2 } from "node:fs";
32754
33265
  import { basename, join as join22 } from "node:path";
32755
33266
 
32756
33267
  // src/lib/git.ts
@@ -32789,7 +33300,7 @@ var WEB_DEP_MARKERS = [
32789
33300
  var E2E_DIR_CANDIDATES = ["tests/e2e", "test/e2e", "e2e", "cypress"];
32790
33301
  function readPackageJson(root) {
32791
33302
  const file2 = join22(root, "package.json");
32792
- if (!existsSync5(file2)) return void 0;
33303
+ if (!existsSync6(file2)) return void 0;
32793
33304
  try {
32794
33305
  return JSON.parse(readFileSync2(file2, "utf8"));
32795
33306
  } catch {
@@ -32799,8 +33310,8 @@ function readPackageJson(root) {
32799
33310
  function detectProjectType(root) {
32800
33311
  const pkg = readPackageJson(root);
32801
33312
  const deps = { ...pkg?.dependencies, ...pkg?.devDependencies };
32802
- const isWechat = existsSync5(join22(root, "project.config.json")) || existsSync5(join22(root, "app.json")) || existsSync5(join22(root, "miniprogram", "app.json"));
32803
- const isWeb = existsSync5(join22(root, "index.html")) || WEB_DEP_MARKERS.some((marker) => deps?.[marker] !== void 0) || [".ts", ".tsx", ".js", ".jsx"].some((ext) => existsSync5(join22(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}`)));
32804
33315
  if (isWechat && isWeb) return "hybrid";
32805
33316
  if (isWechat) return "wechat-miniapp";
32806
33317
  if (isWeb) return "web";
@@ -32809,9 +33320,9 @@ function detectProjectType(root) {
32809
33320
  async function detectProject(root) {
32810
33321
  const pkg = readPackageJson(root);
32811
33322
  const type = detectProjectType(root);
32812
- const language = existsSync5(join22(root, "tsconfig.json")) ? "TypeScript" : pkg || existsSync5(join22(root, "src")) ? "JavaScript" : "Unknown";
32813
- const packageManager = existsSync5(join22(root, "pnpm-lock.yaml")) ? "pnpm" : existsSync5(join22(root, "yarn.lock")) ? "yarn" : existsSync5(join22(root, "package-lock.json")) ? "npm" : "npx";
32814
- const existingE2eDirs = E2E_DIR_CANDIDATES.filter((dir) => existsSync5(join22(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)));
32815
33326
  return {
32816
33327
  root,
32817
33328
  name: pkg?.name ?? basename(root),
@@ -32821,8 +33332,8 @@ async function detectProject(root) {
32821
33332
  git: await readGitInfo(root),
32822
33333
  hasTestsE2e: existingE2eDirs.includes("tests/e2e"),
32823
33334
  existingE2eDirs,
32824
- hasTestpilotConfig: existsSync5(join22(root, "testpilot.yaml")),
32825
- hasProjectLink: existsSync5(join22(root, ".testpilot", "project.json"))
33335
+ hasTestpilotConfig: existsSync6(join22(root, "testpilot.yaml")),
33336
+ hasProjectLink: existsSync6(join22(root, ".testpilot", "project.json"))
32826
33337
  };
32827
33338
  }
32828
33339
  function defaultAdapterFor(type) {
@@ -32837,9 +33348,9 @@ function adaptersFor(type) {
32837
33348
  }
32838
33349
 
32839
33350
  // src/commands/init/skill-installer.ts
32840
- import { existsSync as existsSync6, readFileSync as readFileSync3 } from "node:fs";
33351
+ import { existsSync as existsSync7, readFileSync as readFileSync3 } from "node:fs";
32841
33352
  import { access as access2, cp, mkdir as mkdir12, writeFile as writeFile10 } from "node:fs/promises";
32842
- import { dirname as dirname8, join as join23 } from "node:path";
33353
+ import { dirname as dirname9, join as join23 } from "node:path";
32843
33354
  import { fileURLToPath } from "node:url";
32844
33355
  var CORE_FILES = ["SKILL.md", "manifest.yaml"];
32845
33356
  var CORE_DIRS = ["rules", "workflows"];
@@ -32853,23 +33364,23 @@ async function exists3(path) {
32853
33364
  }
32854
33365
  function readManifestVersion(dir) {
32855
33366
  const manifestPath = join23(dir, "manifest.yaml");
32856
- if (!existsSync6(manifestPath)) return void 0;
33367
+ if (!existsSync7(manifestPath)) return void 0;
32857
33368
  return /^version:\s*([^\s#]+)/m.exec(readFileSync3(manifestPath, "utf8"))?.[1];
32858
33369
  }
32859
33370
  function resolveSkillSourceDir() {
32860
33371
  const fromEnv = process.env.TESTPILOT_SKILL_DIR;
32861
33372
  if (fromEnv) return fromEnv;
32862
33373
  const bundled = fileURLToPath(new URL("./skills/testpilot", import.meta.url));
32863
- if (existsSync6(bundled)) return bundled;
33374
+ if (existsSync7(bundled)) return bundled;
32864
33375
  const inRepo = fileURLToPath(new URL("../../../../../skills/testpilot", import.meta.url));
32865
- return existsSync6(inRepo) ? inRepo : void 0;
33376
+ return existsSync7(inRepo) ? inRepo : void 0;
32866
33377
  }
32867
33378
  async function copyCoreFiles(src, dest) {
32868
33379
  for (const name of CORE_FILES) {
32869
33380
  await cp(join23(src, name), join23(dest, name), { recursive: true, force: true });
32870
33381
  }
32871
33382
  for (const dir of CORE_DIRS) {
32872
- if (existsSync6(join23(src, dir))) {
33383
+ if (existsSync7(join23(src, dir))) {
32873
33384
  await cp(join23(src, dir), join23(dest, dir), { recursive: true, force: true });
32874
33385
  }
32875
33386
  }
@@ -32884,7 +33395,7 @@ async function syncSkillFiles(root, options = {}) {
32884
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"
32885
33396
  );
32886
33397
  }
32887
- await mkdir12(dirname8(dest), { recursive: true });
33398
+ await mkdir12(dirname9(dest), { recursive: true });
32888
33399
  await cp(src, dest, { recursive: true });
32889
33400
  return { core: "created", bundledVersion, dest };
32890
33401
  }
@@ -32958,6 +33469,12 @@ function renderTestConventionsMd() {
32958
33469
  6. \u4F18\u5148\u590D\u7528\u5DF2\u6709 Case
32959
33470
  7. \u65B0\u589E Case \u5FC5\u987B\u901A\u8FC7 csspilot validate
32960
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
32961
33478
  `;
32962
33479
  }
32963
33480
  function renderAdaptersMd(adapters) {
@@ -32998,8 +33515,8 @@ async function runDoctor(root) {
32998
33515
  const projectItems = [
32999
33516
  ok(`Project ${info.name}(${info.type})`),
33000
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"),
33001
- existsSync7(join24(root, ".testpilot", "project.json")) ? ok(".testpilot/project.json") : error62(".testpilot/project.json", "\u8FD0\u884C npx csspilot init \u751F\u6210"),
33002
- existsSync7(join24(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")
33003
33520
  ];
33004
33521
  push("Project", projectItems);
33005
33522
  const config2 = await loadTestpilotConfig(root).catch(() => void 0);
@@ -33008,8 +33525,8 @@ async function runDoctor(root) {
33008
33525
  const invalidCount = cases.filter((item) => !item.valid).length;
33009
33526
  const testDir = join24(root, "tests", "e2e");
33010
33527
  push("Test", [
33011
- existsSync7(testDir) ? ok("tests/e2e") : error62("tests/e2e", "\u8FD0\u884C npx csspilot init \u521B\u5EFA"),
33012
- existsSync7(join24(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"),
33013
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")
33014
33531
  ]);
33015
33532
  const skillDir = join24(root, ".agents", "skills", "testpilot");
@@ -33018,7 +33535,7 @@ async function runDoctor(root) {
33018
33535
  const bundledSkillVersion = skillSource ? readManifestVersion(skillSource) : void 0;
33019
33536
  const skillItems = [
33020
33537
  installedSkillVersion ? ok(`TestPilot Skill(.agents/skills/testpilot,v${installedSkillVersion})`) : error62("TestPilot Skill", "\u8FD0\u884C npx csspilot init \u5B89\u88C5"),
33021
- existsSync7(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")
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")
33022
33539
  ];
33023
33540
  if (installedSkillVersion && bundledSkillVersion && installedSkillVersion !== bundledSkillVersion) {
33024
33541
  skillItems.push(
@@ -33113,7 +33630,7 @@ function makeDoctorCommand() {
33113
33630
  import { join as join27 } from "node:path";
33114
33631
 
33115
33632
  // src/commands/init/config-initializer.ts
33116
- import { access as access3, mkdir as mkdir13, readFile as readFile9, writeFile as writeFile11 } from "node:fs/promises";
33633
+ import { access as access3, mkdir as mkdir13, readFile as readFile10, writeFile as writeFile11 } from "node:fs/promises";
33117
33634
  import { basename as basename2, join as join25 } from "node:path";
33118
33635
  async function exists4(path) {
33119
33636
  try {
@@ -33145,6 +33662,10 @@ environment:
33145
33662
  staging:
33146
33663
  baseUrl: \${STAGING_BASE_URL}
33147
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
+
33148
33669
  # \u9879\u76EE\u58F0\u660E\u7684\u6267\u884C\u7AEF(\u5B9E\u9645\u53EF\u7528\u6027\u4EE5 csspilot doctor \u68C0\u6D4B\u4E3A\u51C6)
33149
33670
  adapters:
33150
33671
  ${adapters}
@@ -33195,7 +33716,7 @@ ${entry}
33195
33716
  `, "utf8");
33196
33717
  return "created";
33197
33718
  }
33198
- const content = await readFile9(gitignorePath, "utf8");
33719
+ const content = await readFile10(gitignorePath, "utf8");
33199
33720
  if (content.split(/\r?\n/).some((item) => item.trim() === entry)) {
33200
33721
  return "exists";
33201
33722
  }
@@ -33279,19 +33800,21 @@ function randomId() {
33279
33800
  }
33280
33801
 
33281
33802
  // src/commands/init/test-structure.ts
33282
- import { existsSync as existsSync8, readdirSync as readdirSync2 } from "node:fs";
33803
+ import { existsSync as existsSync9, readdirSync as readdirSync2 } from "node:fs";
33283
33804
  import { mkdir as mkdir14, writeFile as writeFile12 } from "node:fs/promises";
33284
33805
  import { join as join26 } from "node:path";
33285
33806
  async function initTestStructure(root, _info) {
33286
33807
  const dirs = {
33287
33808
  cases: join26(root, "tests", "e2e", "cases"),
33288
33809
  fixtures: join26(root, "tests", "e2e", "fixtures"),
33289
- data: join26(root, "tests", "e2e", "data")
33810
+ data: join26(root, "tests", "e2e", "data"),
33811
+ reviews: join26(root, "tests", "e2e", "reviews")
33290
33812
  };
33291
33813
  const existed = {
33292
- cases: existsSync8(dirs.cases),
33293
- fixtures: existsSync8(dirs.fixtures),
33294
- data: existsSync8(dirs.data)
33814
+ cases: existsSync9(dirs.cases),
33815
+ fixtures: existsSync9(dirs.fixtures),
33816
+ data: existsSync9(dirs.data),
33817
+ reviews: existsSync9(dirs.reviews)
33295
33818
  };
33296
33819
  for (const dir of Object.values(dirs)) {
33297
33820
  await mkdir14(dir, { recursive: true });
@@ -33304,7 +33827,8 @@ async function initTestStructure(root, _info) {
33304
33827
  return {
33305
33828
  cases: existed.cases ? "reused" : "created",
33306
33829
  fixtures: existed.fixtures ? "reused" : "created",
33307
- data: existed.data ? "reused" : "created"
33830
+ data: existed.data ? "reused" : "created",
33831
+ reviews: existed.reviews ? "reused" : "created"
33308
33832
  };
33309
33833
  }
33310
33834
 
@@ -33344,7 +33868,7 @@ async function runInit(root, options = {}) {
33344
33868
  const testStructure = await initTestStructure(root, project);
33345
33869
  const reuseNote = project.existingE2eDirs.filter((dir) => dir !== "tests/e2e");
33346
33870
  console.log(
33347
- 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}"
33348
33872
  );
33349
33873
  if (reuseNote.length > 0) {
33350
33874
  console.log(` \u26A0 \u68C0\u6D4B\u5230\u5176\u4ED6 E2E \u76EE\u5F55:${reuseNote.join(", ")}(\u4FDD\u7559\u4E0D\u52A8,\u672A\u5F3A\u5236\u8FC1\u79FB)`);
@@ -33487,7 +34011,7 @@ function makeRunCommand() {
33487
34011
  ].filter(Boolean).join(",");
33488
34012
  console.log("");
33489
34013
  console.log(
33490
- `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})` : ""}`
33491
34015
  );
33492
34016
  console.log(`\u4EA7\u7269\u76EE\u5F55:${join28(".testpilot", "artifacts", "runs", summary.runId)}`);
33493
34017
  console.log("\u67E5\u770B\u62A5\u544A:npx csspilot report");
@@ -33514,12 +34038,12 @@ function consoleEventHandler(event) {
33514
34038
  `);
33515
34039
  break;
33516
34040
  case "case-started":
33517
- console.log(`\u25B6 ${event.caseId} (${event.file})`);
34041
+ console.log(`\u25B6 ${event.caseId}#${event.rowId ?? "default"} (${event.file})`);
33518
34042
  break;
33519
34043
  case "step-finished": {
33520
- 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";
33521
34045
  console.log(
33522
- `${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}` : ""}`
33523
34047
  );
33524
34048
  break;
33525
34049
  }
@@ -33529,7 +34053,7 @@ function consoleEventHandler(event) {
33529
34053
  }
33530
34054
 
33531
34055
  // src/commands/sync-metadata.ts
33532
- import { relative } from "node:path";
34056
+ import { relative as relative2 } from "node:path";
33533
34057
  function makeSyncMetadataCommand() {
33534
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) => {
33535
34059
  const client = new TestPilotClient();
@@ -33556,7 +34080,7 @@ function makeSyncMetadataCommand() {
33556
34080
  const cases = infos.filter((item) => item.valid && item.case).map((item) => ({
33557
34081
  id: item.case.id,
33558
34082
  title: item.case.name,
33559
- file: relative(process.cwd(), item.file).split("\\").join("/"),
34083
+ file: relative2(process.cwd(), item.file).split("\\").join("/"),
33560
34084
  tags: item.case.tags ?? []
33561
34085
  }));
33562
34086
  const skipped = infos.length - cases.length;
@@ -33666,7 +34190,7 @@ function makeValidateCommand() {
33666
34190
  }
33667
34191
 
33668
34192
  // src/cli.ts
33669
- var CLI_VERSION = "0.5.0";
34193
+ var CLI_VERSION = "0.6.0";
33670
34194
  async function createCli() {
33671
34195
  const program2 = new Command();
33672
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", () => {