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/index.js CHANGED
@@ -3094,7 +3094,7 @@ var require_visit = __commonJS({
3094
3094
  var BREAK = Symbol("break visit");
3095
3095
  var SKIP = Symbol("skip children");
3096
3096
  var REMOVE = Symbol("remove node");
3097
- function visit2(node2, visitor) {
3097
+ function visit3(node2, visitor) {
3098
3098
  const visitor_ = initVisitor(visitor);
3099
3099
  if (identity.isDocument(node2)) {
3100
3100
  const cd = visit_(null, node2.contents, visitor_, Object.freeze([node2]));
@@ -3103,9 +3103,9 @@ var require_visit = __commonJS({
3103
3103
  } else
3104
3104
  visit_(null, node2, visitor_, Object.freeze([]));
3105
3105
  }
3106
- visit2.BREAK = BREAK;
3107
- visit2.SKIP = SKIP;
3108
- visit2.REMOVE = REMOVE;
3106
+ visit3.BREAK = BREAK;
3107
+ visit3.SKIP = SKIP;
3108
+ visit3.REMOVE = REMOVE;
3109
3109
  function visit_(key, node2, visitor, path) {
3110
3110
  const ctrl = callVisitor(key, node2, visitor, path);
3111
3111
  if (identity.isNode(ctrl) || identity.isPair(ctrl)) {
@@ -3239,7 +3239,7 @@ var require_visit = __commonJS({
3239
3239
  throw new Error(`Cannot replace node with ${pt} parent`);
3240
3240
  }
3241
3241
  }
3242
- exports.visit = visit2;
3242
+ exports.visit = visit3;
3243
3243
  exports.visitAsync = visitAsync;
3244
3244
  }
3245
3245
  });
@@ -3249,7 +3249,7 @@ var require_directives = __commonJS({
3249
3249
  "../../node_modules/.pnpm/yaml@2.9.0/node_modules/yaml/dist/doc/directives.js"(exports) {
3250
3250
  "use strict";
3251
3251
  var identity = require_identity();
3252
- var visit2 = require_visit();
3252
+ var visit3 = require_visit();
3253
3253
  var escapeChars = {
3254
3254
  "!": "%21",
3255
3255
  ",": "%2C",
@@ -3393,7 +3393,7 @@ var require_directives = __commonJS({
3393
3393
  let tagNames;
3394
3394
  if (doc && tagEntries.length > 0 && identity.isNode(doc.contents)) {
3395
3395
  const tags = {};
3396
- visit2.visit(doc.contents, (_key, node2) => {
3396
+ visit3.visit(doc.contents, (_key, node2) => {
3397
3397
  if (identity.isNode(node2) && node2.tag)
3398
3398
  tags[node2.tag] = true;
3399
3399
  });
@@ -3420,7 +3420,7 @@ var require_anchors = __commonJS({
3420
3420
  "../../node_modules/.pnpm/yaml@2.9.0/node_modules/yaml/dist/doc/anchors.js"(exports) {
3421
3421
  "use strict";
3422
3422
  var identity = require_identity();
3423
- var visit2 = require_visit();
3423
+ var visit3 = require_visit();
3424
3424
  function anchorIsValid(anchor2) {
3425
3425
  if (/[\x00-\x19\s,[\]{}]/.test(anchor2)) {
3426
3426
  const sa = JSON.stringify(anchor2);
@@ -3431,7 +3431,7 @@ var require_anchors = __commonJS({
3431
3431
  }
3432
3432
  function anchorNames(root) {
3433
3433
  const anchors = /* @__PURE__ */ new Set();
3434
- visit2.visit(root, {
3434
+ visit3.visit(root, {
3435
3435
  Value(_key, node2) {
3436
3436
  if (node2.anchor)
3437
3437
  anchors.add(node2.anchor);
@@ -3611,7 +3611,7 @@ var require_Alias = __commonJS({
3611
3611
  "../../node_modules/.pnpm/yaml@2.9.0/node_modules/yaml/dist/nodes/Alias.js"(exports) {
3612
3612
  "use strict";
3613
3613
  var anchors = require_anchors();
3614
- var visit2 = require_visit();
3614
+ var visit3 = require_visit();
3615
3615
  var identity = require_identity();
3616
3616
  var Node = require_Node();
3617
3617
  var toJS = require_toJS();
@@ -3637,7 +3637,7 @@ var require_Alias = __commonJS({
3637
3637
  nodes = ctx.aliasResolveCache;
3638
3638
  } else {
3639
3639
  nodes = [];
3640
- visit2.visit(doc, {
3640
+ visit3.visit(doc, {
3641
3641
  Node: (_key, node2) => {
3642
3642
  if (identity.isAlias(node2) || identity.hasAnchor(node2))
3643
3643
  nodes.push(node2);
@@ -8556,15 +8556,15 @@ var require_cst_visit = __commonJS({
8556
8556
  var BREAK = Symbol("break visit");
8557
8557
  var SKIP = Symbol("skip children");
8558
8558
  var REMOVE = Symbol("remove item");
8559
- function visit2(cst, visitor) {
8559
+ function visit3(cst, visitor) {
8560
8560
  if ("type" in cst && cst.type === "document")
8561
8561
  cst = { start: cst.start, value: cst.value };
8562
8562
  _visit(Object.freeze([]), cst, visitor);
8563
8563
  }
8564
- visit2.BREAK = BREAK;
8565
- visit2.SKIP = SKIP;
8566
- visit2.REMOVE = REMOVE;
8567
- visit2.itemAtPath = (cst, path) => {
8564
+ visit3.BREAK = BREAK;
8565
+ visit3.SKIP = SKIP;
8566
+ visit3.REMOVE = REMOVE;
8567
+ visit3.itemAtPath = (cst, path) => {
8568
8568
  let item = cst;
8569
8569
  for (const [field, index] of path) {
8570
8570
  const tok = item?.[field];
@@ -8575,8 +8575,8 @@ var require_cst_visit = __commonJS({
8575
8575
  }
8576
8576
  return item;
8577
8577
  };
8578
- visit2.parentCollection = (cst, path) => {
8579
- const parent = visit2.itemAtPath(cst, path.slice(0, -1));
8578
+ visit3.parentCollection = (cst, path) => {
8579
+ const parent = visit3.itemAtPath(cst, path.slice(0, -1));
8580
8580
  const field = path[path.length - 1][0];
8581
8581
  const coll = parent?.[field];
8582
8582
  if (coll && "items" in coll)
@@ -8607,7 +8607,7 @@ var require_cst_visit = __commonJS({
8607
8607
  }
8608
8608
  return typeof ctrl === "function" ? ctrl(item, path) : ctrl;
8609
8609
  }
8610
- exports.visit = visit2;
8610
+ exports.visit = visit3;
8611
8611
  }
8612
8612
  });
8613
8613
 
@@ -10323,7 +10323,7 @@ var require_dist = __commonJS({
10323
10323
  var lineCounter = require_line_counter();
10324
10324
  var parser = require_parser();
10325
10325
  var publicApi = require_public_api();
10326
- var visit2 = require_visit();
10326
+ var visit3 = require_visit();
10327
10327
  exports.Composer = composer.Composer;
10328
10328
  exports.Document = Document.Document;
10329
10329
  exports.Schema = Schema.Schema;
@@ -10351,8 +10351,8 @@ var require_dist = __commonJS({
10351
10351
  exports.parseAllDocuments = publicApi.parseAllDocuments;
10352
10352
  exports.parseDocument = publicApi.parseDocument;
10353
10353
  exports.stringify = publicApi.stringify;
10354
- exports.visit = visit2.visit;
10355
- exports.visitAsync = visit2.visitAsync;
10354
+ exports.visit = visit3.visit;
10355
+ exports.visitAsync = visit3.visitAsync;
10356
10356
  }
10357
10357
  });
10358
10358
 
@@ -10449,7 +10449,7 @@ function makeCiCommand() {
10449
10449
  }
10450
10450
 
10451
10451
  // src/commands/init/doctor.ts
10452
- import { existsSync as existsSync7 } from "node:fs";
10452
+ import { existsSync as existsSync8 } from "node:fs";
10453
10453
  import { join as join24 } from "node:path";
10454
10454
 
10455
10455
  // ../../packages/core/src/errors/index.ts
@@ -15513,7 +15513,7 @@ var recursive = /* @__PURE__ */ new WeakMap();
15513
15513
  var NONE = 0;
15514
15514
  var ASSUMED = 1;
15515
15515
  var PROVEN = 2;
15516
- function isRecursive(inst, stack, resolve6) {
15516
+ function isRecursive(inst, stack, resolve7) {
15517
15517
  const cached2 = recursive.get(inst);
15518
15518
  if (cached2 !== void 0)
15519
15519
  return cached2 ? PROVEN : NONE;
@@ -15523,7 +15523,7 @@ function isRecursive(inst, stack, resolve6) {
15523
15523
  let result = NONE;
15524
15524
  const check2 = (child) => {
15525
15525
  if (result !== PROVEN && child?._zod) {
15526
- const answer = isRecursive(child, stack, resolve6);
15526
+ const answer = isRecursive(child, stack, resolve7);
15527
15527
  if (answer > result)
15528
15528
  result = answer;
15529
15529
  }
@@ -15534,7 +15534,7 @@ function isRecursive(inst, stack, resolve6) {
15534
15534
  const desc = Object.getOwnPropertyDescriptor(sh, key);
15535
15535
  if (spread && !desc.enumerable)
15536
15536
  continue;
15537
- const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve6) : NONE;
15537
+ const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve7) : NONE;
15538
15538
  if (child > answer)
15539
15539
  answer = child;
15540
15540
  }
@@ -15601,7 +15601,7 @@ function isRecursive(inst, stack, resolve6) {
15601
15601
  break;
15602
15602
  // `$ZodLazy` caches its inner on the def, so a resolved edge is followed exactly
15603
15603
  case "lazy": {
15604
- const inner = def._cachedInner ?? (resolve6 ? inst._zod.innerType : void 0);
15604
+ const inner = def._cachedInner ?? (resolve7 ? inst._zod.innerType : void 0);
15605
15605
  merge2(inner ? isRecursive(inner, stack, false) : ASSUMED);
15606
15606
  break;
15607
15607
  }
@@ -30167,6 +30167,8 @@ var configSchema = external_exports.strictObject({
30167
30167
  }).optional(),
30168
30168
  /** 环境配置:default 指定默认环境名,其余键为环境名 -> { baseUrl } */
30169
30169
  environment: external_exports.record(external_exports.string(), external_exports.union([external_exports.string().min(1), environmentEntrySchema])).optional(),
30170
+ /** Case 可见的项目变量白名单;值可通过 ${ENV_VAR} 从运行环境注入 */
30171
+ variables: external_exports.record(external_exports.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/), external_exports.string()).optional(),
30170
30172
  /** 项目声明的执行端(playwright / wechatide);仅作声明,实际可用性以 doctor 检测为准 */
30171
30173
  adapters: external_exports.array(external_exports.string().min(1)).optional(),
30172
30174
  workspace: external_exports.strictObject({ default: external_exports.string().min(1).optional() }).optional(),
@@ -30231,6 +30233,9 @@ async function loadTestpilotConfig(cwd = process.cwd()) {
30231
30233
  testDirectory: parsed.test?.directory,
30232
30234
  projectName: parsed.project?.name,
30233
30235
  environment,
30236
+ variables: parsed.variables ? Object.fromEntries(
30237
+ Object.entries(parsed.variables).map(([name, value]) => [name, interpolateEnvRefs(value)])
30238
+ ) : void 0,
30234
30239
  adapters: parsed.adapters,
30235
30240
  workspace: parsed.workspace,
30236
30241
  server: parsed.server ? {
@@ -30341,11 +30346,11 @@ function execToString(command, args) {
30341
30346
  }
30342
30347
 
30343
30348
  // ../../packages/sdk/src/client.ts
30344
- import { isAbsolute as isAbsolute3, join as join19 } from "node:path";
30349
+ import { isAbsolute as isAbsolute4, join as join19 } from "node:path";
30345
30350
 
30346
30351
  // ../../packages/sdk/src/cases.ts
30347
- import { mkdir as mkdir3, readFile as readFile3, readdir, stat as stat2, writeFile as writeFile3 } from "node:fs/promises";
30348
- import { dirname, extname, isAbsolute, join as join6, resolve as resolve2 } from "node:path";
30352
+ import { mkdir as mkdir3, readFile as readFile4, readdir, stat as stat2, writeFile as writeFile3 } from "node:fs/promises";
30353
+ import { dirname as dirname2, extname as extname2, isAbsolute as isAbsolute2, join as join6, resolve as resolve3 } from "node:path";
30349
30354
 
30350
30355
  // ../../packages/dsl/src/schema/index.ts
30351
30356
  var STEP_TARGETS = ["web", "miniapp", "api"];
@@ -30366,6 +30371,12 @@ var locatorSchema = external_exports.strictObject({
30366
30371
  text: external_exports.string().min(1).optional(),
30367
30372
  css: external_exports.string().min(1).optional()
30368
30373
  }).describe("text \u4E0E css \u53EA\u80FD\u63D0\u4F9B\u5176\u4E00");
30374
+ var requestExpectationSchema = external_exports.strictObject({
30375
+ method: external_exports.enum(HTTP_METHODS).optional(),
30376
+ urlContains: external_exports.string().min(1),
30377
+ count: external_exports.number().int().nonnegative(),
30378
+ windowMs: external_exports.number().int().nonnegative().default(1e3)
30379
+ });
30369
30380
  var stepSchema = external_exports.strictObject({
30370
30381
  target: external_exports.enum(STEP_TARGETS),
30371
30382
  action: external_exports.enum(ACTIONS),
@@ -30380,8 +30391,18 @@ var stepSchema = external_exports.strictObject({
30380
30391
  /** api target:请求体;对象按 JSON 序列化,字符串原样发送(支持 ${var} 模板) */
30381
30392
  body: external_exports.union([external_exports.string(), external_exports.record(external_exports.string(), external_exports.unknown())]).optional(),
30382
30393
  variable: external_exports.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/, "\u53D8\u91CF\u540D\u9700\u4EE5\u5B57\u6BCD\u6216\u4E0B\u5212\u7EBF\u5F00\u5934,\u4EC5\u542B\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u4E0B\u5212\u7EBF").optional(),
30383
- timeout: external_exports.number().int().positive().optional()
30394
+ timeout: external_exports.number().int().positive().optional(),
30395
+ /** UI 动作触发的网络请求精确次数断言 */
30396
+ expectRequests: external_exports.array(requestExpectationSchema).min(1).optional()
30397
+ });
30398
+ var datasetReferenceSchema = external_exports.strictObject({
30399
+ file: external_exports.string().min(1),
30400
+ idField: external_exports.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/, "idField \u9700\u4E3A\u5408\u6CD5\u5B57\u6BB5\u540D").default("id")
30384
30401
  });
30402
+ var dependencyPathSchema = external_exports.string().regex(
30403
+ /^(fixture|dataset|account|variable)(?:\.[A-Za-z_][A-Za-z0-9_]*)+$/,
30404
+ "requires \u8DEF\u5F84\u5FC5\u987B\u4EE5 fixture/dataset/account/variable \u5F00\u5934\u5E76\u5305\u542B\u5B57\u6BB5"
30405
+ );
30385
30406
  var caseSchema = external_exports.strictObject({
30386
30407
  id: external_exports.string().regex(/^[a-z0-9][a-z0-9-]*$/, "id \u9700\u4E3A kebab-case(\u5C0F\u5199\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u8FDE\u5B57\u7B26)"),
30387
30408
  name: external_exports.string().min(1, "name \u4E0D\u80FD\u4E3A\u7A7A"),
@@ -30393,7 +30414,12 @@ var caseSchema = external_exports.strictObject({
30393
30414
  /** 引用测试账号标识;凭据由 Environment + Server/CI Secret 管理,禁止写进 Case */
30394
30415
  accountRef: external_exports.string().min(1).optional(),
30395
30416
  tags: external_exports.array(external_exports.string().min(1)).optional(),
30396
- steps: external_exports.array(stepSchema).min(1, "Case \u81F3\u5C11\u5305\u542B\u4E00\u4E2A step")
30417
+ fixtures: external_exports.array(external_exports.string().min(1)).min(1).optional(),
30418
+ datasets: datasetReferenceSchema.optional(),
30419
+ requires: external_exports.array(dependencyPathSchema).min(1).optional(),
30420
+ setup: external_exports.array(stepSchema).min(1).optional(),
30421
+ steps: external_exports.array(stepSchema).min(1, "Case \u81F3\u5C11\u5305\u542B\u4E00\u4E2A step"),
30422
+ teardown: external_exports.array(stepSchema).min(1).optional()
30397
30423
  });
30398
30424
 
30399
30425
  // ../../packages/dsl/src/parser/index.ts
@@ -30444,6 +30470,7 @@ var ADAPTER_SUPPORT = {
30444
30470
  };
30445
30471
  var LOCATOR_REQUIRED = /* @__PURE__ */ new Set(["click", "input", "select", "assert", "extract"]);
30446
30472
  var VALUE_REQUIRED = /* @__PURE__ */ new Set(["input", "select"]);
30473
+ var REQUEST_EXPECTATION_ACTIONS = /* @__PURE__ */ new Set(["navigate", "click", "input", "select"]);
30447
30474
  var FIELD_ALLOWED = {
30448
30475
  url: ["navigate", "request"],
30449
30476
  value: ["input", "select", "extract"],
@@ -30458,12 +30485,21 @@ var VARIABLE_REF = /\$\{([A-Za-z_][A-Za-z0-9_.]*)\}/g;
30458
30485
  function validateCase(data) {
30459
30486
  const issues = [];
30460
30487
  const declared = /* @__PURE__ */ new Set();
30488
+ if (data.fixtures) declared.add("fixture");
30489
+ if (data.datasets) declared.add("dataset");
30490
+ declared.add("variable");
30461
30491
  if (data.accountRef) {
30462
30492
  declared.add("account");
30463
30493
  }
30494
+ data.setup?.forEach((step, index) => {
30495
+ checkStep(step, `setup[${index}]`, declared, issues);
30496
+ });
30464
30497
  data.steps.forEach((step, index) => {
30465
30498
  checkStep(step, `steps[${index}]`, declared, issues);
30466
30499
  });
30500
+ data.teardown?.forEach((step, index) => {
30501
+ checkStep(step, `teardown[${index}]`, declared, issues);
30502
+ });
30467
30503
  return issues;
30468
30504
  }
30469
30505
  function validateCaseSource(source) {
@@ -30538,6 +30574,15 @@ function checkStep(step, at, declared, issues) {
30538
30574
  if (step.action === "wait" && !step.locator && step.timeout === void 0) {
30539
30575
  issues.push({ path: at, code: "semantic", message: "wait \u9700\u8981\u63D0\u4F9B locator(\u7B49\u5F85\u5143\u7D20)\u6216 timeout(\u6BEB\u79D2)" });
30540
30576
  }
30577
+ if (step.expectRequests) {
30578
+ if (step.target === "api" || !REQUEST_EXPECTATION_ACTIONS.has(step.action)) {
30579
+ issues.push({
30580
+ path: `${at}.expectRequests`,
30581
+ code: "semantic",
30582
+ message: "expectRequests \u4EC5\u7528\u4E8E web/miniapp \u7684 navigate/click/input/select"
30583
+ });
30584
+ }
30585
+ }
30541
30586
  checkVariableRefs(step, at, declared, issues);
30542
30587
  }
30543
30588
  function checkLocator(step, at, issues) {
@@ -30570,9 +30615,14 @@ function checkVariableRefs(step, at, declared, issues) {
30570
30615
  collect("value", step.value);
30571
30616
  collect("expected", step.expected);
30572
30617
  collect("url", step.url);
30618
+ collect("locator.text", step.locator?.text);
30619
+ collect("locator.css", step.locator?.css);
30573
30620
  if (typeof step.body === "string") collect("body", step.body);
30574
30621
  else if (step.body) collect("body", JSON.stringify(step.body));
30575
30622
  for (const headerValue of Object.values(step.headers ?? {})) collect("headers", headerValue);
30623
+ for (const [index, expectation] of (step.expectRequests ?? []).entries()) {
30624
+ collect(`expectRequests[${index}].urlContains`, expectation.urlContains);
30625
+ }
30576
30626
  for (const [field, name] of refs) {
30577
30627
  const root = name.split(".")[0] ?? name;
30578
30628
  if (!declared.has(root)) {
@@ -30587,9 +30637,20 @@ function checkVariableRefs(step, at, declared, issues) {
30587
30637
 
30588
30638
  // ../../packages/sdk/src/project.ts
30589
30639
  import { mkdir as mkdir2, readFile as readFile2, stat, writeFile as writeFile2 } from "node:fs/promises";
30590
- import { join as join5, resolve } from "node:path";
30640
+ import { existsSync as existsSync2 } from "node:fs";
30641
+ import { dirname, join as join5, resolve } from "node:path";
30591
30642
  function resolveRoot(root) {
30592
- return resolve(root ?? process.cwd());
30643
+ const start = resolve(root ?? process.cwd());
30644
+ if (root) return start;
30645
+ let current = start;
30646
+ for (; ; ) {
30647
+ if (existsSync2(join5(current, "testpilot.yaml")) || existsSync2(join5(current, TESTPILOT_DIR))) {
30648
+ return current;
30649
+ }
30650
+ const parent = dirname(current);
30651
+ if (parent === current) return start;
30652
+ current = parent;
30653
+ }
30593
30654
  }
30594
30655
  async function loadProjectConfig(root) {
30595
30656
  return loadTestpilotConfig(resolveRoot(root));
@@ -30638,6 +30699,164 @@ async function writeProjectLink(link, root) {
30638
30699
  return file2;
30639
30700
  }
30640
30701
 
30702
+ // ../../packages/sdk/src/case-data.ts
30703
+ var import_yaml3 = __toESM(require_dist(), 1);
30704
+ import { readFile as readFile3, realpath } from "node:fs/promises";
30705
+ import { extname, isAbsolute, relative, resolve as resolve2 } from "node:path";
30706
+ async function loadCaseDataFile(reference, options) {
30707
+ if (isAbsolute(reference)) {
30708
+ throw new Error(`\u6570\u636E\u6587\u4EF6\u5FC5\u987B\u4F7F\u7528\u9879\u76EE\u76F8\u5BF9\u8DEF\u5F84:${reference}`);
30709
+ }
30710
+ const root = resolve2(options.root);
30711
+ const dataDir = resolve2(options.dataDir);
30712
+ const target = resolve2(root, reference);
30713
+ if (!isInside(dataDir, target)) {
30714
+ throw new Error(`\u6570\u636E\u6587\u4EF6\u5FC5\u987B\u4F4D\u4E8E tests/e2e/data \u76EE\u5F55:${reference}`);
30715
+ }
30716
+ const [realDataDir, realTarget] = await Promise.all([
30717
+ realpath(dataDir).catch(() => dataDir),
30718
+ realpath(target).catch((error62) => {
30719
+ throw new Error(
30720
+ `\u8BFB\u53D6\u6570\u636E\u6587\u4EF6\u5931\u8D25:${reference}:${error62 instanceof Error ? error62.message : String(error62)}`
30721
+ );
30722
+ })
30723
+ ]);
30724
+ if (!isInside(realDataDir, realTarget)) {
30725
+ throw new Error(`\u6570\u636E\u6587\u4EF6\u89E3\u6790\u540E\u8D8A\u51FA tests/e2e/data \u76EE\u5F55:${reference}`);
30726
+ }
30727
+ const source = await readFile3(realTarget, "utf8");
30728
+ const extension = extname(realTarget).toLowerCase();
30729
+ try {
30730
+ if (extension === ".yaml" || extension === ".yml") return (0, import_yaml3.parse)(source);
30731
+ if (extension === ".json") return JSON.parse(source);
30732
+ } catch (error62) {
30733
+ throw new Error(
30734
+ `\u89E3\u6790\u6570\u636E\u6587\u4EF6\u5931\u8D25:${reference}:${error62 instanceof Error ? error62.message : String(error62)}`
30735
+ );
30736
+ }
30737
+ throw new Error(`\u6570\u636E\u6587\u4EF6\u4EC5\u652F\u6301 .yaml/.yml/.json:${reference}`);
30738
+ }
30739
+ function isDataObject(value) {
30740
+ return value !== null && typeof value === "object" && !Array.isArray(value);
30741
+ }
30742
+ function mergeFixture(target, source, at = "fixture") {
30743
+ const output2 = { ...target };
30744
+ for (const [key, value] of Object.entries(source)) {
30745
+ const path = `${at}.${key}`;
30746
+ if (!(key in output2)) {
30747
+ output2[key] = value;
30748
+ continue;
30749
+ }
30750
+ const current = output2[key];
30751
+ if (isDataObject(current) && isDataObject(value)) {
30752
+ output2[key] = mergeFixture(current, value, path);
30753
+ continue;
30754
+ }
30755
+ throw new Error(`fixture \u5B57\u6BB5\u51B2\u7A81:${path}`);
30756
+ }
30757
+ return output2;
30758
+ }
30759
+ function flattenVariables(prefix, value) {
30760
+ const output2 = {};
30761
+ visit2(prefix, value, output2);
30762
+ return output2;
30763
+ }
30764
+ function visit2(path, value, output2) {
30765
+ if (value === null || value === void 0) return;
30766
+ if (typeof value === "object") {
30767
+ output2[path] = JSON.stringify(value);
30768
+ if (Array.isArray(value)) {
30769
+ value.forEach((item, index) => visit2(`${path}.${index}`, item, output2));
30770
+ } else {
30771
+ for (const [key, item] of Object.entries(value)) {
30772
+ visit2(`${path}.${key}`, item, output2);
30773
+ }
30774
+ }
30775
+ return;
30776
+ }
30777
+ output2[path] = String(value);
30778
+ }
30779
+ function isInside(base, candidate) {
30780
+ const path = relative(base, candidate);
30781
+ return path === "" || !path.startsWith("..") && !isAbsolute(path);
30782
+ }
30783
+
30784
+ // ../../packages/sdk/src/case-preparation.ts
30785
+ async function prepareCaseExecutions(item, options) {
30786
+ const fixture = await loadFixtures(item.data.fixtures ?? [], options);
30787
+ const rows = await loadDataset(item.data, options);
30788
+ const projectVariables = flattenVariables("variable", options.variables ?? {});
30789
+ const accountVariables = accountValues(item.data, options.accounts);
30790
+ return rows.map(({ row, rowId }, rowIndex) => {
30791
+ const initialVariables = {
30792
+ ...flattenVariables("fixture", fixture),
30793
+ ...flattenVariables("dataset", row),
30794
+ ...projectVariables,
30795
+ ...accountVariables
30796
+ };
30797
+ const missingDependencies = (item.data.requires ?? []).filter(
30798
+ (path) => isUnavailable(initialVariables[path])
30799
+ );
30800
+ return {
30801
+ data: item.data,
30802
+ file: item.file,
30803
+ templateId: item.data.id,
30804
+ rowId,
30805
+ rowIndex,
30806
+ initialVariables,
30807
+ missingDependencies
30808
+ };
30809
+ });
30810
+ }
30811
+ async function loadFixtures(references, options) {
30812
+ let merged = {};
30813
+ for (const reference of references) {
30814
+ const value = await loadCaseDataFile(reference, options);
30815
+ if (!isDataObject(value)) {
30816
+ throw new Error(`fixture \u6587\u4EF6\u6839\u5FC5\u987B\u662F\u5BF9\u8C61:${reference}`);
30817
+ }
30818
+ merged = mergeFixture(merged, value);
30819
+ }
30820
+ return merged;
30821
+ }
30822
+ async function loadDataset(data, options) {
30823
+ if (!data.datasets) return [{ row: {}, rowId: "default" }];
30824
+ const value = await loadCaseDataFile(data.datasets.file, options);
30825
+ if (!Array.isArray(value)) {
30826
+ throw new Error(`dataset \u6587\u4EF6\u6839\u5FC5\u987B\u662F\u5BF9\u8C61\u6570\u7EC4:${data.datasets.file}`);
30827
+ }
30828
+ const seen = /* @__PURE__ */ new Set();
30829
+ return value.map((row, index) => {
30830
+ if (!isDataObject(row)) {
30831
+ throw new Error(`dataset \u7B2C ${index + 1} \u884C\u5FC5\u987B\u662F\u5BF9\u8C61:${data.datasets.file}`);
30832
+ }
30833
+ const rawId = row[data.datasets.idField];
30834
+ if (typeof rawId !== "string" && typeof rawId !== "number" || String(rawId).trim() === "") {
30835
+ throw new Error(
30836
+ `dataset \u7B2C ${index + 1} \u884C\u7F3A\u5C11\u975E\u7A7A row id \u5B57\u6BB5 "${data.datasets.idField}"`
30837
+ );
30838
+ }
30839
+ const rowId = String(rawId);
30840
+ if (seen.has(rowId)) throw new Error(`dataset row id \u91CD\u590D:${rowId}`);
30841
+ seen.add(rowId);
30842
+ return { row, rowId };
30843
+ });
30844
+ }
30845
+ function accountValues(data, accounts) {
30846
+ if (!data.accountRef) return {};
30847
+ const raw = accounts?.[data.accountRef];
30848
+ if (raw === void 0) return {};
30849
+ try {
30850
+ const parsed = JSON.parse(raw);
30851
+ if (isDataObject(parsed)) return flattenVariables("account", parsed);
30852
+ } catch {
30853
+ }
30854
+ return { account: raw };
30855
+ }
30856
+ function isUnavailable(value) {
30857
+ return value === void 0 || value.trim() === "" || /\$\{[A-Za-z_][A-Za-z0-9_]*\}/.test(value);
30858
+ }
30859
+
30641
30860
  // ../../packages/sdk/src/cases.ts
30642
30861
  async function collectCaseFiles(paths) {
30643
30862
  const files = [];
@@ -30666,19 +30885,20 @@ async function collectFromDir(dir) {
30666
30885
  return found;
30667
30886
  }
30668
30887
  function isCaseFile(file2) {
30669
- const ext = extname(file2).toLowerCase();
30888
+ const ext = extname2(file2).toLowerCase();
30670
30889
  return ext === ".yaml" || ext === ".yml";
30671
30890
  }
30672
30891
  function resolveAgainstRoot(root, file2) {
30673
- return isAbsolute(file2) ? file2 : resolve2(resolveRoot(root), file2);
30892
+ return isAbsolute2(file2) ? file2 : resolve3(resolveRoot(root), file2);
30674
30893
  }
30675
30894
  async function collectCases(paths, options = {}) {
30676
30895
  const files = await collectCaseFiles(
30677
30896
  paths.map((input2) => resolveAgainstRoot(options.root, input2))
30678
30897
  );
30679
30898
  const infos = [];
30899
+ const root = resolveRoot(options.root);
30680
30900
  for (const file2 of files) {
30681
- const validation = await validateCaseFile(file2);
30901
+ const validation = await validateCaseFile(file2, root);
30682
30902
  infos.push({
30683
30903
  file: file2,
30684
30904
  valid: validation.ok,
@@ -30690,16 +30910,16 @@ async function collectCases(paths, options = {}) {
30690
30910
  }
30691
30911
  async function readCaseFile(file2, options = {}) {
30692
30912
  const target = resolveAgainstRoot(options.root, file2);
30693
- const source = await readFile3(target, "utf8");
30694
- return { file: target, source, validation: validateCaseSource(source) };
30913
+ const source = await readFile4(target, "utf8");
30914
+ return { file: target, source, validation: await validateCaseDataSource(source, target, resolveRoot(options.root)) };
30695
30915
  }
30696
- async function validateCaseFile(file2) {
30697
- const source = await readFile3(file2, "utf8");
30698
- return validateCaseSource(source);
30916
+ async function validateCaseFile(file2, root) {
30917
+ const source = await readFile4(file2, "utf8");
30918
+ return validateCaseDataSource(source, file2, root);
30699
30919
  }
30700
30920
  async function writeCaseFile(file2, source, options = {}) {
30701
30921
  const target = resolveAgainstRoot(options.root, file2);
30702
- const validation = validateCaseSource(source);
30922
+ const validation = await validateCaseDataSource(source, target, resolveRoot(options.root));
30703
30923
  if (!validation.ok) {
30704
30924
  return { file: target, written: false, validation };
30705
30925
  }
@@ -30709,18 +30929,41 @@ async function writeCaseFile(file2, source, options = {}) {
30709
30929
  throw new Error(`\u7528\u4F8B\u6587\u4EF6\u5DF2\u5B58\u5728:${target}(\u8986\u76D6\u9700\u663E\u5F0F overwrite: true)`);
30710
30930
  }
30711
30931
  }
30712
- await mkdir3(dirname(target), { recursive: true });
30932
+ await mkdir3(dirname2(target), { recursive: true });
30713
30933
  await writeFile3(target, source, "utf8");
30714
30934
  return { file: target, written: true, validation };
30715
30935
  }
30936
+ async function validateCaseDataSource(source, file2, root) {
30937
+ const validation = validateCaseSource(source);
30938
+ if (!validation.ok || !validation.data) return validation;
30939
+ try {
30940
+ await prepareCaseExecutions(
30941
+ { data: validation.data, file: file2 },
30942
+ { root, dataDir: join6(root, "tests", "e2e", "data") }
30943
+ );
30944
+ return validation;
30945
+ } catch (error62) {
30946
+ return {
30947
+ ok: false,
30948
+ data: validation.data,
30949
+ issues: [
30950
+ {
30951
+ path: "(root)",
30952
+ code: "semantic",
30953
+ message: error62 instanceof Error ? error62.message : String(error62)
30954
+ }
30955
+ ]
30956
+ };
30957
+ }
30958
+ }
30716
30959
 
30717
30960
  // ../../packages/sdk/src/reports.ts
30718
- import { readFile as readFile7 } from "node:fs/promises";
30961
+ import { readFile as readFile8 } from "node:fs/promises";
30719
30962
  import { join as join18 } from "node:path";
30720
30963
 
30721
30964
  // ../../packages/reporter/src/report.ts
30722
30965
  import { writeFile as writeFile4 } from "node:fs/promises";
30723
- import { dirname as dirname2, join as join8, resolve as resolve3 } from "node:path";
30966
+ import { dirname as dirname3, join as join8, resolve as resolve4 } from "node:path";
30724
30967
 
30725
30968
  // ../../packages/reporter/src/classify.ts
30726
30969
  var FAILURE_CATEGORIES = [
@@ -30750,14 +30993,14 @@ function classifyFailure(message) {
30750
30993
  }
30751
30994
 
30752
30995
  // ../../packages/reporter/src/history.ts
30753
- import { readFile as readFile4, readdir as readdir2 } from "node:fs/promises";
30996
+ import { readFile as readFile5, readdir as readdir2 } from "node:fs/promises";
30754
30997
  import { join as join7 } from "node:path";
30755
30998
  async function collectHistory(runsRoot2, skipRunId, limit = 12) {
30756
30999
  const entries = (await readdir2(runsRoot2).catch(() => [])).filter((name) => /^\d{8}-\d{3}$/.test(name) && name !== skipRunId).sort().slice(-limit);
30757
31000
  const points = [];
30758
31001
  for (const runId of entries) {
30759
31002
  try {
30760
- const summary = JSON.parse(await readFile4(join7(runsRoot2, runId, "result.json"), "utf8"));
31003
+ const summary = JSON.parse(await readFile5(join7(runsRoot2, runId, "result.json"), "utf8"));
30761
31004
  points.push({
30762
31005
  runId,
30763
31006
  startedAt: summary.startedAt,
@@ -30778,12 +31021,19 @@ function toSummaryView(summary) {
30778
31021
  runId: summary.runId,
30779
31022
  status: summary.status,
30780
31023
  durationMs: summary.durationMs,
30781
- cases: { passed: summary.totals.passed, failed: summary.totals.failed },
31024
+ cases: {
31025
+ templates: summary.totals.templates ?? summary.totals.cases,
31026
+ passed: summary.totals.passed,
31027
+ failed: summary.totals.failed,
31028
+ skipped: summary.totals.skipped ?? 0,
31029
+ warnings: summary.totals.warnings ?? 0
31030
+ },
30782
31031
  steps: {
30783
31032
  total: summary.totals.steps,
30784
31033
  passed: summary.totals.stepsPassed,
30785
31034
  failed: summary.totals.stepsFailed,
30786
- skipped: summary.totals.stepsSkipped
31035
+ skipped: summary.totals.stepsSkipped,
31036
+ warning: summary.totals.stepsWarning ?? 0
30787
31037
  }
30788
31038
  };
30789
31039
  }
@@ -30791,9 +31041,9 @@ function toSummaryView(summary) {
30791
31041
  // ../../packages/reporter/src/report.ts
30792
31042
  var HISTORY_LIMIT = 12;
30793
31043
  async function writeReports(runDir, summary, options = {}) {
30794
- const dir = resolve3(runDir);
31044
+ const dir = resolve4(runDir);
30795
31045
  const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
30796
- const history = options.history ?? await collectHistory(dirname2(dir), summary.runId, options.historyLimit ?? HISTORY_LIMIT);
31046
+ const history = options.history ?? await collectHistory(dirname3(dir), summary.runId, options.historyLimit ?? HISTORY_LIMIT);
30797
31047
  const failures = [];
30798
31048
  const cases = summary.cases.map((item) => {
30799
31049
  const categories = /* @__PURE__ */ new Set();
@@ -30852,7 +31102,7 @@ function renderHtml(data) {
30852
31102
  const view = toSummaryView(data.summary);
30853
31103
  const summary = data.summary;
30854
31104
  const trendSvg = renderTrend(data);
30855
- const casesHtml = summary.cases.map((item) => caseHtml(item)).join("\n");
31105
+ const casesHtml = groupedCasesHtml(summary.cases);
30856
31106
  const chips = categoryChips(data);
30857
31107
  return `<!doctype html>
30858
31108
  <html lang="zh-CN">
@@ -30860,13 +31110,13 @@ function renderHtml(data) {
30860
31110
  <meta charset="utf-8" />
30861
31111
  <title>TestPilot Report ${escapeHtml(summary.runId)}</title>
30862
31112
  <style>
30863
- :root { --ok:#1a7f37; --fail:#cf222e; --skip:#6e7781; --line:#d0d7de; --muted:#59636e; }
31113
+ :root { --ok:#1a7f37; --fail:#cf222e; --warn:#9a6700; --skip:#6e7781; --line:#d0d7de; --muted:#59636e; }
30864
31114
  * { box-sizing: border-box; }
30865
31115
  body { font-family: system-ui, "Segoe UI", sans-serif; margin: 0; color: #1f2328; background: #f6f8fa; }
30866
31116
  .wrap { max-width: 1080px; margin: 0 auto; padding: 20px 16px 48px; }
30867
31117
  h1 { font-size: 20px; margin: 8px 0; } h2 { font-size: 15px; margin: 24px 0 8px; }
30868
31118
  .badge { display: inline-block; padding: 2px 10px; border-radius: 10px; color: #fff; font-size: 12px; }
30869
- .passed { background: var(--ok); } .failed { background: var(--fail); } .skipped { background: var(--skip); }
31119
+ .passed { background: var(--ok); } .failed { background: var(--fail); } .warning { background: var(--warn); } .skipped { background: var(--skip); }
30870
31120
  code { background: #eef1f4; padding: 1px 5px; border-radius: 4px; font-size: 12px; }
30871
31121
  .cards { display: flex; gap: 12px; flex-wrap: wrap; margin: 14px 0; }
30872
31122
  .card { background: #fff; border: 1px solid var(--line); border-radius: 8px; padding: 10px 16px; min-width: 130px; }
@@ -30879,6 +31129,8 @@ function renderHtml(data) {
30879
31129
  .btn { border: 1px solid var(--line); background: #fff; border-radius: 6px; padding: 4px 10px; font-size: 12px; cursor: pointer; }
30880
31130
  #count { color: var(--muted); font-size: 12px; margin-left: auto; }
30881
31131
  details.case { background: #fff; border: 1px solid var(--line); border-radius: 8px; margin: 10px 0; }
31132
+ section.template { border-left: 3px solid var(--line); padding-left: 12px; margin: 14px 0; }
31133
+ section.template > h3 { font-size: 14px; margin: 0 0 6px; }
30882
31134
  details.case > summary { cursor: pointer; padding: 10px 14px; font-size: 14px; list-style: none; }
30883
31135
  details.case > summary::-webkit-details-marker { display: none; }
30884
31136
  details.case > summary:hover { background: #f6f8fa; }
@@ -30901,6 +31153,8 @@ function renderHtml(data) {
30901
31153
  <div class="cards">
30902
31154
  <div class="card"><b>${view.cases.passed}<span style="color:var(--ok)"> \u2713</span></b><span>\u901A\u8FC7\u7528\u4F8B</span></div>
30903
31155
  <div class="card"><b>${view.cases.failed}<span style="color:var(--fail)"> \u2717</span></b><span>\u5931\u8D25\u7528\u4F8B</span></div>
31156
+ <div class="card"><b>${view.cases.skipped} \u21B7</b><span>\u8DF3\u8FC7\u6570\u636E\u884C</span></div>
31157
+ <div class="card"><b>${view.cases.warnings} \u26A0</b><span>\u6E05\u7406\u8B66\u544A</span></div>
30904
31158
  <div class="card"><b>${view.steps.passed}<span style="color:var(--ok)"> \u2713</span></b><span>\u901A\u8FC7\u6B65\u9AA4</span></div>
30905
31159
  <div class="card"><b>${view.steps.failed}<span style="color:var(--fail)"> \u2717</span></b><span>\u5931\u8D25\u6B65\u9AA4</span></div>
30906
31160
  <div class="card"><b>${view.steps.skipped} \u21B7</b><span>\u8DF3\u8FC7\u6B65\u9AA4</span></div>
@@ -30987,26 +31241,44 @@ function renderHtml(data) {
30987
31241
  </body>
30988
31242
  </html>`;
30989
31243
  }
31244
+ function groupedCasesHtml(cases) {
31245
+ const groups = /* @__PURE__ */ new Map();
31246
+ for (const item of cases) {
31247
+ const current = groups.get(item.caseId) ?? [];
31248
+ current.push(item);
31249
+ groups.set(item.caseId, current);
31250
+ }
31251
+ return [...groups.entries()].map(
31252
+ ([caseId, rows]) => `<section class="template">
31253
+ <h3>${escapeHtml(caseId)} <span class="muted">\xB7 ${rows.length} \u4E2A\u6570\u636E\u884C</span></h3>
31254
+ ${rows.map((item) => caseHtml(item)).join("\n")}
31255
+ </section>`
31256
+ ).join("\n");
31257
+ }
30990
31258
  function caseHtml(item) {
30991
31259
  const rows = item.steps.map((step) => stepHtml(step)).join("\n");
30992
31260
  const errorLine = item.error ? `<p class="error">${escapeHtml(item.error)}</p>` : "";
31261
+ const skipLine = item.skipReason ? `<p class="muted">${escapeHtml(item.skipReason)}${item.missingDependencies?.length ? `: ${escapeHtml(item.missingDependencies.join(", "))}` : ""}</p>` : "";
31262
+ const warningLine = item.warnings?.length ? `<p class="error">\u26A0 ${item.warnings.map(escapeHtml).join("<br />\u26A0 ")}</p>` : "";
30993
31263
  const links = [];
30994
31264
  if (item.video) links.push(`<a href="${escapeHtml(item.video)}">\u89C6\u9891</a>`);
30995
31265
  if (item.trace) links.push(`<a href="${escapeHtml(item.trace)}">Trace</a>`);
30996
31266
  const linksLine = links.length > 0 ? `<p>${links.join(" \xB7 ")}</p>` : "";
30997
- const search = escapeHtml(`${item.caseId} ${item.caseName} ${item.file}`).replaceAll('"', "&quot;");
31267
+ const search = escapeHtml(`${item.caseId} ${item.rowId ?? "default"} ${item.caseName} ${item.file}`).replaceAll('"', "&quot;");
30998
31268
  const cats = escapeHtml(item.categories.join(","));
30999
31269
  return `
31000
31270
  <details class="case" data-status="${item.status}" data-cats="${cats}" data-search="${search}">
31001
31271
  <summary>
31002
31272
  <span class="badge ${item.status}">${item.status}</span>
31003
- <b>${escapeHtml(item.caseId)}</b> \xB7 ${escapeHtml(item.caseName)}
31273
+ <b>\u6570\u636E\u884C ${escapeHtml(item.rowId ?? "default")}</b> \xB7 ${escapeHtml(item.caseName)}
31004
31274
  <span class="muted">\xB7 ${item.steps.length} \u6B65 \xB7 ${(item.durationMs / 1e3).toFixed(1)}s</span>
31005
31275
  </summary>
31006
31276
  ${errorLine}
31277
+ ${skipLine}
31278
+ ${warningLine}
31007
31279
  ${linksLine}
31008
31280
  <table>
31009
- <tr><th>#</th><th>target</th><th>action</th><th>\u72B6\u6001</th><th>\u8017\u65F6</th><th>\u8BE6\u60C5</th></tr>
31281
+ <tr><th>#</th><th>\u9636\u6BB5</th><th>target</th><th>action</th><th>\u72B6\u6001</th><th>\u8017\u65F6</th><th>\u8BE6\u60C5</th></tr>
31010
31282
  ${rows}
31011
31283
  </table>
31012
31284
  </details>`;
@@ -31021,8 +31293,14 @@ function stepHtml(step) {
31021
31293
  }
31022
31294
  }
31023
31295
  if (step.screenshot) details.push(`<a href="${escapeHtml(step.screenshot)}">\u622A\u56FE</a>`);
31296
+ for (const assertion of step.requestAssertions ?? []) {
31297
+ details.push(
31298
+ `\u8BF7\u6C42 ${escapeHtml(assertion.method ?? "*")} <code>${escapeHtml(assertion.urlContains)}</code>: ${assertion.actualCount}/${assertion.count}`
31299
+ );
31300
+ }
31024
31301
  return `<tr>
31025
31302
  <td class="num">${step.index}</td>
31303
+ <td>${escapeHtml(step.phase ?? "steps")}</td>
31026
31304
  <td>${escapeHtml(step.target)}</td>
31027
31305
  <td>${escapeHtml(step.action)}</td>
31028
31306
  <td><span class="badge ${step.status}">${step.status}</span></td>
@@ -31079,7 +31357,7 @@ function embedJson(data) {
31079
31357
 
31080
31358
  // ../../packages/reporter/src/summary-report.ts
31081
31359
  import { mkdir as mkdir4, writeFile as writeFile5 } from "node:fs/promises";
31082
- import { join as join9, resolve as resolve4 } from "node:path";
31360
+ import { join as join9, resolve as resolve5 } from "node:path";
31083
31361
  function escapeHtml2(value) {
31084
31362
  return String(value).replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
31085
31363
  }
@@ -31097,6 +31375,8 @@ function buildSummaryData(runs) {
31097
31375
  executions.push({
31098
31376
  runId,
31099
31377
  caseId: item.caseId,
31378
+ rowId: item.rowId,
31379
+ identity: item.rowId && item.rowId !== "default" ? `${item.caseId}#${item.rowId}` : item.caseId,
31100
31380
  caseName: item.caseName,
31101
31381
  status: item.status,
31102
31382
  category,
@@ -31114,10 +31394,11 @@ function buildSummaryData(runs) {
31114
31394
  }
31115
31395
  const byCase = /* @__PURE__ */ new Map();
31116
31396
  for (const exec2 of executions) {
31117
- let agg = byCase.get(exec2.caseId);
31397
+ let agg = byCase.get(exec2.identity);
31118
31398
  if (!agg) {
31119
31399
  agg = {
31120
31400
  caseId: exec2.caseId,
31401
+ rowId: exec2.rowId,
31121
31402
  caseName: exec2.caseName,
31122
31403
  platforms: [],
31123
31404
  runs: 0,
@@ -31127,17 +31408,18 @@ function buildSummaryData(runs) {
31127
31408
  lastStatus: "unknown",
31128
31409
  flaky: false
31129
31410
  };
31130
- byCase.set(exec2.caseId, agg);
31411
+ byCase.set(exec2.identity, agg);
31131
31412
  }
31132
31413
  agg.runs++;
31133
31414
  if (exec2.status === "passed") agg.passed++;
31134
- else agg.failed++;
31415
+ else if (exec2.status === "failed") agg.failed++;
31135
31416
  for (const platform of exec2.platforms) {
31136
31417
  if (!agg.platforms.includes(platform)) agg.platforms.push(platform);
31137
31418
  }
31138
31419
  }
31139
31420
  for (const agg of byCase.values()) {
31140
- const last = [...executions].reverse().find((exec2) => exec2.caseId === agg.caseId);
31421
+ const identity = agg.rowId && agg.rowId !== "default" ? `${agg.caseId}#${agg.rowId}` : agg.caseId;
31422
+ const last = [...executions].reverse().find((exec2) => exec2.identity === identity);
31141
31423
  agg.lastStatus = last?.status ?? "unknown";
31142
31424
  agg.lastCategory = last?.category;
31143
31425
  agg.passRate = agg.runs > 0 ? agg.passed / agg.runs : 0;
@@ -31348,7 +31630,7 @@ function renderSummaryHtml(data) {
31348
31630
  </html>`;
31349
31631
  }
31350
31632
  async function writeSummaryReport(artifactsDir, data) {
31351
- const dir = resolve4(artifactsDir);
31633
+ const dir = resolve5(artifactsDir);
31352
31634
  await mkdir4(dir, { recursive: true });
31353
31635
  const htmlPath = join9(dir, "summary.html");
31354
31636
  const jsonPath = join9(dir, "summary.json");
@@ -31358,8 +31640,8 @@ async function writeSummaryReport(artifactsDir, data) {
31358
31640
  }
31359
31641
 
31360
31642
  // ../../packages/sdk/src/runs.ts
31361
- import { readFile as readFile6, readdir as readdir4 } from "node:fs/promises";
31362
- import { isAbsolute as isAbsolute2, join as join17 } from "node:path";
31643
+ import { readFile as readFile7, readdir as readdir4 } from "node:fs/promises";
31644
+ import { isAbsolute as isAbsolute3, join as join17 } from "node:path";
31363
31645
 
31364
31646
  // ../../packages/execution-engine/src/artifacts/index.ts
31365
31647
  import { mkdir as mkdir9 } from "node:fs/promises";
@@ -31367,7 +31649,7 @@ import { join as join13 } from "node:path";
31367
31649
 
31368
31650
  // ../../packages/evidence/src/log.ts
31369
31651
  import { appendFile, mkdir as mkdir5 } from "node:fs/promises";
31370
- import { dirname as dirname3 } from "node:path";
31652
+ import { dirname as dirname4 } from "node:path";
31371
31653
  var RunLogger = class {
31372
31654
  constructor(filePath) {
31373
31655
  this.filePath = filePath;
@@ -31382,7 +31664,7 @@ var RunLogger = class {
31382
31664
  return this.write("ERROR", message);
31383
31665
  }
31384
31666
  async write(level, message) {
31385
- await mkdir5(dirname3(this.filePath), { recursive: true });
31667
+ await mkdir5(dirname4(this.filePath), { recursive: true });
31386
31668
  const line = `${(/* @__PURE__ */ new Date()).toISOString()} ${level} ${message}
31387
31669
  `;
31388
31670
  await appendFile(this.filePath, line, "utf8");
@@ -31391,30 +31673,30 @@ var RunLogger = class {
31391
31673
 
31392
31674
  // ../../packages/evidence/src/screenshot.ts
31393
31675
  import { mkdir as mkdir6, writeFile as writeFile6 } from "node:fs/promises";
31394
- import { dirname as dirname4, join as join10 } from "node:path";
31676
+ import { dirname as dirname5, join as join10 } from "node:path";
31395
31677
  async function saveScreenshot(dir, fileName, data) {
31396
31678
  const file2 = join10(dir, fileName);
31397
- await mkdir6(dirname4(file2), { recursive: true });
31679
+ await mkdir6(dirname5(file2), { recursive: true });
31398
31680
  await writeFile6(file2, data);
31399
31681
  return file2;
31400
31682
  }
31401
31683
 
31402
31684
  // ../../packages/evidence/src/trace.ts
31403
31685
  import { mkdir as mkdir7, writeFile as writeFile7 } from "node:fs/promises";
31404
- import { dirname as dirname5, join as join11 } from "node:path";
31686
+ import { dirname as dirname6, join as join11 } from "node:path";
31405
31687
  async function saveTrace(dir, fileName, data) {
31406
31688
  const file2 = join11(dir, fileName);
31407
- await mkdir7(dirname5(file2), { recursive: true });
31689
+ await mkdir7(dirname6(file2), { recursive: true });
31408
31690
  await writeFile7(file2, data);
31409
31691
  return file2;
31410
31692
  }
31411
31693
 
31412
31694
  // ../../packages/evidence/src/video.ts
31413
31695
  import { mkdir as mkdir8, writeFile as writeFile8 } from "node:fs/promises";
31414
- import { dirname as dirname6, join as join12 } from "node:path";
31696
+ import { dirname as dirname7, join as join12 } from "node:path";
31415
31697
  async function saveVideo(dir, fileName, data) {
31416
31698
  const file2 = join12(dir, fileName);
31417
- await mkdir8(dirname6(file2), { recursive: true });
31699
+ await mkdir8(dirname7(file2), { recursive: true });
31418
31700
  await writeFile8(file2, data);
31419
31701
  return file2;
31420
31702
  }
@@ -31464,6 +31746,11 @@ var ExecutionContext = class {
31464
31746
  get(name) {
31465
31747
  return this.variables.get(name);
31466
31748
  }
31749
+ seed(values) {
31750
+ for (const [name, value] of Object.entries(values)) {
31751
+ this.variables.set(name, value);
31752
+ }
31753
+ }
31467
31754
  /**
31468
31755
  * 将字符串中的 ${name} 替换为已提取的变量值;未定义的引用保持原样(校验阶段已拦截)。
31469
31756
  * 名字允许点号,用于引擎注入的 account.username 一类凭据变量(用户 extract 变量名不含点号)。
@@ -31528,7 +31815,7 @@ async function ensureRunDir(runsRoot2, runId) {
31528
31815
 
31529
31816
  // ../../packages/execution-engine/src/events/index.ts
31530
31817
  import { appendFile as appendFile2, mkdir as mkdir11 } from "node:fs/promises";
31531
- import { dirname as dirname7 } from "node:path";
31818
+ import { dirname as dirname8 } from "node:path";
31532
31819
  var EventBus = class {
31533
31820
  subscribers = [];
31534
31821
  subscribe(subscriber) {
@@ -31542,7 +31829,7 @@ var EventBus = class {
31542
31829
  };
31543
31830
  function ndjsonSubscriber(filePath) {
31544
31831
  return async (event) => {
31545
- await mkdir11(dirname7(filePath), { recursive: true });
31832
+ await mkdir11(dirname8(filePath), { recursive: true });
31546
31833
  await appendFile2(filePath, `${JSON.stringify(event)}
31547
31834
  `, "utf8");
31548
31835
  };
@@ -31623,11 +31910,19 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
31623
31910
  status: "passed",
31624
31911
  durationMs: 0
31625
31912
  };
31913
+ let captureStarted = false;
31626
31914
  try {
31627
31915
  const locator = step.locator ? {
31628
31916
  text: step.locator.text !== void 0 ? context.resolve(step.locator.text) : void 0,
31629
31917
  css: step.locator.css !== void 0 ? context.resolve(step.locator.css) : void 0
31630
31918
  } : void 0;
31919
+ if (step.expectRequests) {
31920
+ if (!adapter.startRequestCapture || !adapter.stopRequestCapture) {
31921
+ throw new Error(`\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301\u8BF7\u6C42\u6B21\u6570\u65AD\u8A00`);
31922
+ }
31923
+ await adapter.startRequestCapture();
31924
+ captureStarted = true;
31925
+ }
31631
31926
  switch (step.action) {
31632
31927
  case "launch":
31633
31928
  await adapter.launch();
@@ -31698,7 +31993,29 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
31698
31993
  throw new Error(`\u4E0D\u652F\u6301\u7684 action:${String(exhaustive)}`);
31699
31994
  }
31700
31995
  }
31996
+ if (step.expectRequests) {
31997
+ const windowMs = Math.max(...step.expectRequests.map((item) => item.windowMs));
31998
+ if (windowMs > 0) await delay(windowMs);
31999
+ const requests = await adapter.stopRequestCapture();
32000
+ captureStarted = false;
32001
+ result.requestAssertions = buildRequestAssertions(step, requests, context);
32002
+ const mismatches = result.requestAssertions.filter((item) => item.actualCount !== item.count);
32003
+ if (mismatches.length > 0) {
32004
+ throw new Error(
32005
+ mismatches.map(
32006
+ (item) => `\u8BF7\u6C42\u6B21\u6570\u4E0D\u5339\u914D:${item.method ?? "*"} ${item.urlContains},\u5B9E\u9645 ${item.actualCount},\u671F\u671B ${item.count}`
32007
+ ).join("; ")
32008
+ );
32009
+ }
32010
+ }
31701
32011
  } catch (err) {
32012
+ if (captureStarted) {
32013
+ try {
32014
+ await adapter.stopRequestCapture?.();
32015
+ } catch {
32016
+ }
32017
+ captureStarted = false;
32018
+ }
31702
32019
  result.status = "failed";
31703
32020
  result.error = err instanceof Error ? err.message : String(err);
31704
32021
  try {
@@ -31743,92 +32060,195 @@ function resolveDeep(value, context) {
31743
32060
  function snippet(text, limit = SNIPPET_LIMIT) {
31744
32061
  return text.length > limit ? `${text.slice(0, limit)}\u2026` : text;
31745
32062
  }
32063
+ function buildRequestAssertions(step, requests, context) {
32064
+ return (step.expectRequests ?? []).map((expectation) => {
32065
+ const method = expectation.method?.toUpperCase();
32066
+ const urlContains = context.resolve(expectation.urlContains);
32067
+ const matches = requests.filter(
32068
+ (request) => (!method || request.method.toUpperCase() === method) && request.url.includes(urlContains)
32069
+ );
32070
+ return {
32071
+ method,
32072
+ urlContains: expectation.urlContains,
32073
+ count: expectation.count,
32074
+ actualCount: matches.length,
32075
+ windowMs: expectation.windowMs,
32076
+ requests: matches.slice(0, 20).map((request) => ({
32077
+ method: request.method.toUpperCase(),
32078
+ url: sanitizeRequestUrl(request.url)
32079
+ }))
32080
+ };
32081
+ });
32082
+ }
32083
+ function sanitizeRequestUrl(url2) {
32084
+ const queryIndex = url2.indexOf("?");
32085
+ const safe = queryIndex >= 0 ? `${url2.slice(0, queryIndex)}?<redacted>` : url2;
32086
+ return snippet(safe, 300);
32087
+ }
32088
+ function delay(milliseconds) {
32089
+ return new Promise((resolve7) => setTimeout(resolve7, milliseconds));
32090
+ }
31746
32091
 
31747
32092
  // ../../packages/execution-engine/src/engine/execution-engine.ts
31748
32093
  var ExecutionEngine = class {
31749
32094
  async runCase(data, options) {
31750
- const { runId, file: file2, artifacts, bus, resolver } = options;
32095
+ const { runId, file: file2, artifacts, bus, resolver, rowId, rowIndex } = options;
31751
32096
  const startedAt = /* @__PURE__ */ new Date();
31752
32097
  const context = new ExecutionContext();
32098
+ context.seed(options.initialVariables);
31753
32099
  if (data.accountRef && options.accounts?.[data.accountRef] !== void 0) {
31754
32100
  context.seedAccount(options.accounts[data.accountRef]);
31755
32101
  }
31756
32102
  const steps = [];
32103
+ const warnings = [];
31757
32104
  const evidenceAdapters = /* @__PURE__ */ new Map();
31758
32105
  let caseError;
31759
- await bus.emit({ type: "case-started", runId, caseId: data.id, file: file2, ts: startedAt.toISOString() });
31760
- await artifacts.logger.info(`case ${data.id} started (${file2})`);
31761
- for (const [index, step] of data.steps.entries()) {
32106
+ const executionId = safeArtifactName(`${data.id}-${rowId}`);
32107
+ await bus.emit({
32108
+ type: "case-started",
32109
+ runId,
32110
+ caseId: data.id,
32111
+ rowId,
32112
+ file: file2,
32113
+ ts: startedAt.toISOString()
32114
+ });
32115
+ await artifacts.logger.info(`case ${data.id}#${rowId} started (${file2})`);
32116
+ if (options.missingDependencies.length > 0) {
32117
+ const finishedAt2 = /* @__PURE__ */ new Date();
32118
+ const skipped = {
32119
+ caseId: data.id,
32120
+ caseName: data.name,
32121
+ file: file2,
32122
+ rowId,
32123
+ rowIndex,
32124
+ status: "skipped",
32125
+ skipReason: "dependency-not-ready",
32126
+ missingDependencies: options.missingDependencies,
32127
+ steps,
32128
+ startedAt: startedAt.toISOString(),
32129
+ finishedAt: finishedAt2.toISOString(),
32130
+ durationMs: finishedAt2.getTime() - startedAt.getTime()
32131
+ };
31762
32132
  await bus.emit({
31763
- type: "step-started",
32133
+ type: "case-finished",
31764
32134
  runId,
31765
32135
  caseId: data.id,
31766
- index,
31767
- target: step.target,
31768
- action: step.action,
31769
- ts: (/* @__PURE__ */ new Date()).toISOString()
32136
+ rowId,
32137
+ status: "skipped",
32138
+ durationMs: skipped.durationMs,
32139
+ ts: finishedAt2.toISOString()
31770
32140
  });
31771
- let result;
31772
- try {
31773
- const adapter = await (await resolver.session(step.target)).get();
31774
- if (adapter.startEvidence && !evidenceAdapters.has(step.target)) {
31775
- try {
31776
- await adapter.startEvidence(data.id);
31777
- evidenceAdapters.set(step.target, adapter);
31778
- } catch (err) {
31779
- await artifacts.logger.info(
31780
- `evidence start failed for ${step.target}: ${err instanceof Error ? err.message : String(err)}`
31781
- );
32141
+ await artifacts.logger.info(
32142
+ `case ${data.id}#${rowId} skipped: missing ${options.missingDependencies.join(", ")}`
32143
+ );
32144
+ return skipped;
32145
+ }
32146
+ const runPhase = async (phase, phaseSteps) => {
32147
+ let passed = true;
32148
+ for (const [index, step] of phaseSteps.entries()) {
32149
+ await bus.emit({
32150
+ type: "step-started",
32151
+ runId,
32152
+ caseId: data.id,
32153
+ rowId,
32154
+ phase,
32155
+ index,
32156
+ target: step.target,
32157
+ action: step.action,
32158
+ ts: (/* @__PURE__ */ new Date()).toISOString()
32159
+ });
32160
+ let result;
32161
+ try {
32162
+ const adapter = await (await resolver.session(step.target)).get();
32163
+ if (adapter.startEvidence && !evidenceAdapters.has(step.target)) {
32164
+ try {
32165
+ await adapter.startEvidence(executionId);
32166
+ evidenceAdapters.set(step.target, adapter);
32167
+ } catch (err) {
32168
+ await artifacts.logger.info(
32169
+ `evidence start failed for ${step.target}: ${err instanceof Error ? err.message : String(err)}`
32170
+ );
32171
+ }
31782
32172
  }
32173
+ result = await executeStep(step, index, adapter, context, artifacts, {
32174
+ screenshotName: `${executionId}-${phase}-${String(index).padStart(2, "0")}`
32175
+ });
32176
+ } catch (err) {
32177
+ result = {
32178
+ index,
32179
+ target: step.target,
32180
+ action: step.action,
32181
+ status: "failed",
32182
+ durationMs: 0,
32183
+ error: err instanceof Error ? err.message : String(err)
32184
+ };
31783
32185
  }
31784
- result = await executeStep(step, index, adapter, context, artifacts, {
31785
- screenshotName: `${data.id}-step-${String(index).padStart(2, "0")}`
31786
- });
31787
- } catch (err) {
31788
- result = {
32186
+ result.phase = phase;
32187
+ if (phase === "teardown" && result.status === "failed") {
32188
+ result.status = "warning";
32189
+ warnings.push(result.error ?? `teardown step ${index} failed`);
32190
+ }
32191
+ steps.push(result);
32192
+ await artifacts.logger.info(
32193
+ `${phase} step ${index} ${step.action} -> ${result.status}${result.error ? ` - ${result.error}` : ""}`
32194
+ );
32195
+ await bus.emit({
32196
+ type: "step-finished",
32197
+ runId,
32198
+ caseId: data.id,
32199
+ rowId,
32200
+ phase,
31789
32201
  index,
31790
32202
  target: step.target,
31791
32203
  action: step.action,
31792
- status: "failed",
31793
- durationMs: 0,
31794
- error: err instanceof Error ? err.message : String(err)
31795
- };
31796
- }
31797
- steps.push(result);
31798
- await artifacts.logger.info(`step ${index} ${step.action} -> ${result.status}${result.error ? ` - ${result.error}` : ""}`);
31799
- await bus.emit({
31800
- type: "step-finished",
31801
- runId,
31802
- caseId: data.id,
31803
- index,
31804
- target: step.target,
31805
- action: step.action,
31806
- status: result.status,
31807
- durationMs: result.durationMs,
31808
- error: result.error,
31809
- ts: (/* @__PURE__ */ new Date()).toISOString()
31810
- });
31811
- if (result.status === "failed") {
31812
- caseError = result.error;
31813
- for (const [skipIndex, skippedStep] of data.steps.entries()) {
31814
- if (skipIndex <= index) continue;
31815
- steps.push({
31816
- index: skipIndex,
31817
- target: skippedStep.target,
31818
- action: skippedStep.action,
31819
- status: "skipped",
31820
- durationMs: 0
31821
- });
32204
+ status: result.status,
32205
+ durationMs: result.durationMs,
32206
+ error: result.error,
32207
+ ts: (/* @__PURE__ */ new Date()).toISOString()
32208
+ });
32209
+ if (result.status === "failed") {
32210
+ passed = false;
32211
+ caseError = result.error;
32212
+ for (const [skipIndex, skippedStep] of phaseSteps.entries()) {
32213
+ if (skipIndex <= index) continue;
32214
+ steps.push({
32215
+ index: skipIndex,
32216
+ phase,
32217
+ target: skippedStep.target,
32218
+ action: skippedStep.action,
32219
+ status: "skipped",
32220
+ durationMs: 0
32221
+ });
32222
+ }
32223
+ break;
31822
32224
  }
31823
- break;
32225
+ }
32226
+ return passed;
32227
+ };
32228
+ const setupPassed = await runPhase("setup", data.setup ?? []);
32229
+ if (setupPassed) {
32230
+ await runPhase("steps", data.steps);
32231
+ } else {
32232
+ for (const [index, step] of data.steps.entries()) {
32233
+ steps.push({
32234
+ index,
32235
+ phase: "steps",
32236
+ target: step.target,
32237
+ action: step.action,
32238
+ status: "skipped",
32239
+ durationMs: 0
32240
+ });
31824
32241
  }
31825
32242
  }
32243
+ await runPhase("teardown", data.teardown ?? []);
31826
32244
  const finishedAt = /* @__PURE__ */ new Date();
31827
32245
  const status = caseError ? "failed" : "passed";
31828
32246
  const caseResult = {
31829
32247
  caseId: data.id,
31830
32248
  caseName: data.name,
31831
32249
  file: file2,
32250
+ rowId,
32251
+ rowIndex,
31832
32252
  status,
31833
32253
  steps,
31834
32254
  startedAt: startedAt.toISOString(),
@@ -31836,15 +32256,16 @@ var ExecutionEngine = class {
31836
32256
  durationMs: finishedAt.getTime() - startedAt.getTime()
31837
32257
  };
31838
32258
  if (caseError) caseResult.error = caseError;
32259
+ if (warnings.length > 0) caseResult.warnings = warnings;
31839
32260
  for (const [target, adapter] of evidenceAdapters) {
31840
32261
  if (!adapter.stopEvidence) continue;
31841
32262
  try {
31842
- const evidence = await adapter.stopEvidence(data.id);
32263
+ const evidence = await adapter.stopEvidence(executionId);
31843
32264
  if (evidence.video) {
31844
- caseResult.video = await artifacts.saveVideo(`${data.id}-${target}.webm`, evidence.video);
32265
+ caseResult.video = await artifacts.saveVideo(`${executionId}-${target}.webm`, evidence.video);
31845
32266
  }
31846
32267
  if (evidence.trace) {
31847
- caseResult.trace = await artifacts.saveTrace(`${data.id}-${target}.zip`, evidence.trace);
32268
+ caseResult.trace = await artifacts.saveTrace(`${executionId}-${target}.zip`, evidence.trace);
31848
32269
  }
31849
32270
  } catch (err) {
31850
32271
  await artifacts.logger.info(
@@ -31856,33 +32277,45 @@ var ExecutionEngine = class {
31856
32277
  type: "case-finished",
31857
32278
  runId,
31858
32279
  caseId: data.id,
32280
+ rowId,
31859
32281
  status,
31860
32282
  durationMs: finishedAt.getTime() - startedAt.getTime(),
31861
32283
  ts: finishedAt.toISOString()
31862
32284
  });
31863
- await artifacts.logger.info(`case ${data.id} ${status}`);
32285
+ await artifacts.logger.info(`case ${data.id}#${rowId} ${status}`);
31864
32286
  return caseResult;
31865
32287
  }
31866
32288
  };
32289
+ function safeArtifactName(value) {
32290
+ return value.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "case";
32291
+ }
31867
32292
 
31868
32293
  // ../../packages/execution-engine/src/engine/result-collector.ts
31869
32294
  function buildRunSummary(runId, startedAt, cases, options = {}) {
31870
32295
  const finishedAt = /* @__PURE__ */ new Date();
31871
32296
  const totals = {
32297
+ templates: options.templates ?? new Set(cases.map((item) => item.caseId)).size,
31872
32298
  cases: cases.length,
31873
32299
  passed: cases.filter((item) => item.status === "passed").length,
31874
32300
  failed: cases.filter((item) => item.status === "failed").length,
32301
+ skipped: cases.filter((item) => item.status === "skipped").length,
32302
+ warnings: 0,
31875
32303
  steps: 0,
31876
32304
  stepsPassed: 0,
31877
32305
  stepsFailed: 0,
31878
- stepsSkipped: 0
32306
+ stepsSkipped: 0,
32307
+ stepsWarning: 0
31879
32308
  };
31880
32309
  for (const item of cases) {
31881
32310
  for (const step of item.steps) {
31882
32311
  totals.steps++;
31883
32312
  if (step.status === "passed") totals.stepsPassed++;
31884
32313
  else if (step.status === "failed") totals.stepsFailed++;
31885
- else totals.stepsSkipped++;
32314
+ else if (step.status === "skipped") totals.stepsSkipped++;
32315
+ else {
32316
+ totals.stepsWarning++;
32317
+ totals.warnings++;
32318
+ }
31886
32319
  }
31887
32320
  }
31888
32321
  return {
@@ -31890,7 +32323,7 @@ function buildRunSummary(runId, startedAt, cases, options = {}) {
31890
32323
  startedAt: startedAt.toISOString(),
31891
32324
  finishedAt: finishedAt.toISOString(),
31892
32325
  durationMs: finishedAt.getTime() - startedAt.getTime(),
31893
- status: totals.failed > 0 ? "failed" : "passed",
32326
+ status: totals.failed > 0 ? "failed" : totals.passed > 0 || totals.cases === 0 ? "passed" : "skipped",
31894
32327
  ...options.cancelled ? { cancelled: true } : {},
31895
32328
  cases,
31896
32329
  totals
@@ -31899,7 +32332,7 @@ function buildRunSummary(runId, startedAt, cases, options = {}) {
31899
32332
 
31900
32333
  // ../../packages/execution-engine/src/engine/test-runner.ts
31901
32334
  import { writeFile as writeFile9 } from "node:fs/promises";
31902
- import { join as join15, resolve as resolve5 } from "node:path";
32335
+ import { join as join15, resolve as resolve6 } from "node:path";
31903
32336
  var TestRunner = class {
31904
32337
  resolver;
31905
32338
  runsRoot;
@@ -31909,7 +32342,7 @@ var TestRunner = class {
31909
32342
  accounts;
31910
32343
  constructor(options) {
31911
32344
  this.resolver = options.resolver;
31912
- this.runsRoot = resolve5(options.runsRoot ?? join15(process.cwd(), ".testpilot", "artifacts", "runs"));
32345
+ this.runsRoot = resolve6(options.runsRoot ?? join15(process.cwd(), ".testpilot", "artifacts", "runs"));
31913
32346
  this.customRunId = options.runId;
31914
32347
  this.extraSubscriber = options.onEvent;
31915
32348
  this.signal = options.signal;
@@ -31937,6 +32370,11 @@ var TestRunner = class {
31937
32370
  await engine.runCase(item.data, {
31938
32371
  runId,
31939
32372
  file: item.file,
32373
+ templateId: item.templateId ?? item.data.id,
32374
+ rowId: item.rowId ?? "default",
32375
+ rowIndex: item.rowIndex ?? 0,
32376
+ initialVariables: item.initialVariables ?? {},
32377
+ missingDependencies: item.missingDependencies ?? [],
31940
32378
  artifacts,
31941
32379
  bus,
31942
32380
  resolver: this.resolver,
@@ -31946,7 +32384,8 @@ var TestRunner = class {
31946
32384
  }
31947
32385
  const cancelled = this.signal?.aborted === true;
31948
32386
  await this.resolver.closeAll();
31949
- const summary = buildRunSummary(runId, startedAt, results, { cancelled });
32387
+ const templates = new Set(cases.map((item) => item.templateId ?? item.data.id)).size;
32388
+ const summary = buildRunSummary(runId, startedAt, results, { cancelled, templates });
31950
32389
  await bus.emit({
31951
32390
  type: "run-finished",
31952
32391
  runId,
@@ -31988,8 +32427,8 @@ async function openCaseContext(bundle, options, evidenceTmpDir) {
31988
32427
  }
31989
32428
 
31990
32429
  // ../../packages/adapter-playwright/src/page.ts
31991
- import { existsSync as existsSync2 } from "node:fs";
31992
- import { mkdtemp, readFile as readFile5, rm } from "node:fs/promises";
32430
+ import { existsSync as existsSync3 } from "node:fs";
32431
+ import { mkdtemp, readFile as readFile6, rm } from "node:fs/promises";
31993
32432
  import { tmpdir } from "node:os";
31994
32433
  import { join as join16 } from "node:path";
31995
32434
  var PlaywrightAdapter = class {
@@ -31999,6 +32438,7 @@ var PlaywrightAdapter = class {
31999
32438
  }
32000
32439
  target = "web";
32001
32440
  current;
32441
+ requestCapture;
32002
32442
  /** 浏览器在 create() 时已启动;context/page 按用例惰性创建,launch 保持幂等 */
32003
32443
  async launch() {
32004
32444
  }
@@ -32013,6 +32453,7 @@ var PlaywrightAdapter = class {
32013
32453
  }
32014
32454
  /** 结束取证:trace 落 zip、录屏在 context 关闭后完成,返回二者字节 */
32015
32455
  async stopEvidence(_caseId) {
32456
+ await this.stopRequestCapture().catch(() => []);
32016
32457
  if (!this.current) return {};
32017
32458
  const { context, page, evidenceTmpDir } = this.current;
32018
32459
  this.current = void 0;
@@ -32020,11 +32461,28 @@ var PlaywrightAdapter = class {
32020
32461
  await context.tracing.stop({ path: traceZipPath }).catch(() => void 0);
32021
32462
  const recordedVideoPath = await page.video()?.path().catch(() => void 0);
32022
32463
  await context.close().catch(() => void 0);
32023
- const video = recordedVideoPath && existsSync2(recordedVideoPath) ? await readFile5(recordedVideoPath) : void 0;
32024
- const trace = evidenceTmpDir && existsSync2(traceZipPath) ? await readFile5(traceZipPath) : void 0;
32464
+ const video = recordedVideoPath && existsSync3(recordedVideoPath) ? await readFile6(recordedVideoPath) : void 0;
32465
+ const trace = evidenceTmpDir && existsSync3(traceZipPath) ? await readFile6(traceZipPath) : void 0;
32025
32466
  if (evidenceTmpDir) await rm(evidenceTmpDir, { recursive: true, force: true }).catch(() => void 0);
32026
32467
  return { video, trace };
32027
32468
  }
32469
+ async startRequestCapture() {
32470
+ if (this.requestCapture) await this.stopRequestCapture();
32471
+ const page = await this.ensurePage();
32472
+ const requests = [];
32473
+ const listener = (request) => {
32474
+ requests.push({ method: request.method(), url: request.url() });
32475
+ };
32476
+ page.on("request", listener);
32477
+ this.requestCapture = { page, listener, requests };
32478
+ }
32479
+ async stopRequestCapture() {
32480
+ if (!this.requestCapture) return [];
32481
+ const capture = this.requestCapture;
32482
+ this.requestCapture = void 0;
32483
+ capture.page.off("request", capture.listener);
32484
+ return capture.requests;
32485
+ }
32028
32486
  async navigate(url2) {
32029
32487
  const options = this.options;
32030
32488
  const page = await this.ensurePage();
@@ -32086,6 +32544,7 @@ var PlaywrightAdapter = class {
32086
32544
  throw new Error(`\u65E0\u6548\u7684 locator:${JSON.stringify(locator)}`);
32087
32545
  }
32088
32546
  async closeContextQuietly() {
32547
+ await this.stopRequestCapture().catch(() => []);
32089
32548
  if (!this.current) return;
32090
32549
  const { context } = this.current;
32091
32550
  this.current = void 0;
@@ -32124,8 +32583,8 @@ var automatorLauncher = {
32124
32583
  }
32125
32584
  }
32126
32585
  };
32127
- function delay(ms) {
32128
- return new Promise((resolve6) => setTimeout(resolve6, ms));
32586
+ function delay2(ms) {
32587
+ return new Promise((resolve7) => setTimeout(resolve7, ms));
32129
32588
  }
32130
32589
  async function connectWithRetry(wsEndpoint, timeoutMs) {
32131
32590
  const startedAt = Date.now();
@@ -32135,7 +32594,7 @@ async function connectWithRetry(wsEndpoint, timeoutMs) {
32135
32594
  return await automator.connect({ wsEndpoint });
32136
32595
  } catch (err) {
32137
32596
  lastError = err;
32138
- await delay(1e3);
32597
+ await delay2(1e3);
32139
32598
  }
32140
32599
  }
32141
32600
  throw lastError instanceof Error ? lastError : new Error(String(lastError));
@@ -32186,6 +32645,7 @@ var MiniAppAdapter = class {
32186
32645
  }
32187
32646
  target = "miniapp";
32188
32647
  program;
32648
+ requestCapture;
32189
32649
  /** 拉起开发者工具并建立自动化连接;需幂等 */
32190
32650
  async launch() {
32191
32651
  if (this.program) return;
@@ -32206,7 +32666,7 @@ var MiniAppAdapter = class {
32206
32666
  return;
32207
32667
  } catch (err) {
32208
32668
  lastError = err;
32209
- await delay(3e3);
32669
+ await delay2(3e3);
32210
32670
  }
32211
32671
  }
32212
32672
  throw lastError instanceof Error ? lastError : new Error(String(lastError));
@@ -32252,7 +32712,43 @@ var MiniAppAdapter = class {
32252
32712
  return Buffer.from(base643, "base64");
32253
32713
  });
32254
32714
  }
32715
+ async startRequestCapture() {
32716
+ if (this.requestCapture) await this.stopRequestCapture();
32717
+ const program2 = await this.getProgram();
32718
+ const requests = [];
32719
+ const exposedName = `__testpilotRequest_${Date.now()}_${Math.random().toString(36).slice(2)}`;
32720
+ await program2.exposeFunction(exposedName, (value) => {
32721
+ if (value === null || typeof value !== "object") return;
32722
+ const request = value;
32723
+ if (typeof request.url !== "string") return;
32724
+ requests.push({
32725
+ method: typeof request.method === "string" ? request.method : "GET",
32726
+ url: request.url
32727
+ });
32728
+ });
32729
+ await program2.mockWxMethod(
32730
+ "request",
32731
+ function(options, callbackName) {
32732
+ const callbacks = globalThis;
32733
+ callbacks[callbackName]?.({
32734
+ method: typeof options.method === "string" ? options.method : "GET",
32735
+ url: typeof options.url === "string" ? options.url : ""
32736
+ });
32737
+ return this.origin(options);
32738
+ },
32739
+ exposedName
32740
+ );
32741
+ this.requestCapture = { program: program2, exposedName, requests };
32742
+ }
32743
+ async stopRequestCapture() {
32744
+ if (!this.requestCapture) return [];
32745
+ const capture = this.requestCapture;
32746
+ this.requestCapture = void 0;
32747
+ await capture.program.restoreWxMethod("request");
32748
+ return capture.requests;
32749
+ }
32255
32750
  async close() {
32751
+ await this.stopRequestCapture().catch(() => []);
32256
32752
  if (!this.program) return;
32257
32753
  const program2 = this.program;
32258
32754
  this.program = void 0;
@@ -32300,17 +32796,17 @@ var MiniAppAdapter = class {
32300
32796
  if (Date.now() - startedAt > ELEMENT_WAIT_MS) {
32301
32797
  throw new Error(`\u7B49\u5F85\u5143\u7D20\u8D85\u65F6(${ELEMENT_WAIT_MS}ms):${selector}`);
32302
32798
  }
32303
- await delay(500);
32799
+ await delay2(500);
32304
32800
  }
32305
32801
  }
32306
32802
  };
32307
32803
 
32308
32804
  // ../../packages/adapter-wechatide/src/project.ts
32309
- import { existsSync as existsSync3 } from "node:fs";
32805
+ import { existsSync as existsSync4 } from "node:fs";
32310
32806
  async function resolveMiniappConfig(input2) {
32311
32807
  const projectPath = input2.projectPath ?? process.env.TESTPILOT_MINIPROGRAM_PROJECT_PATH;
32312
32808
  if (!projectPath) return void 0;
32313
- if (!existsSync3(projectPath)) {
32809
+ if (!existsSync4(projectPath)) {
32314
32810
  throw new Error(`\u5C0F\u7A0B\u5E8F\u9879\u76EE\u8DEF\u5F84\u4E0D\u5B58\u5728:${projectPath}`);
32315
32811
  }
32316
32812
  const cliPath = input2.cliPath ?? process.env.WECHAT_DEVTOOLS_CLI ?? await findWechatDevToolsCli();
@@ -32485,7 +32981,7 @@ function createDefaultAdapters(config2) {
32485
32981
  async function runCases(options = {}) {
32486
32982
  const root = resolveRoot(options.root);
32487
32983
  const config2 = await loadProjectConfig(root);
32488
- const inputs = options.paths && options.paths.length > 0 ? options.paths.map((input2) => isAbsolute2(input2) ? input2 : join17(root, input2)) : [join17(root, config2.casesDir)];
32984
+ const inputs = options.paths && options.paths.length > 0 ? options.paths.map((input2) => isAbsolute3(input2) ? input2 : join17(root, input2)) : [join17(root, config2.casesDir)];
32489
32985
  const cases = await collectCases(inputs, { root });
32490
32986
  if (cases.length === 0) {
32491
32987
  throw new RunError("ENO_CASES", `\u672A\u627E\u5230\u7528\u4F8B\u6587\u4EF6(\u67E5\u627E:${inputs.join(", ")})`);
@@ -32517,8 +33013,23 @@ async function runCases(options = {}) {
32517
33013
  signal: options.signal,
32518
33014
  accounts: options.accounts
32519
33015
  });
33016
+ const prepared = (await Promise.all(
33017
+ runnable.map(
33018
+ (item) => prepareCaseExecutions(
33019
+ { data: item.case, file: item.file },
33020
+ {
33021
+ root,
33022
+ dataDir: join17(root, "tests", "e2e", "data"),
33023
+ variables: config2.variables,
33024
+ accounts: options.accounts
33025
+ }
33026
+ )
33027
+ )
33028
+ )).flat();
32520
33029
  const summary = await runner.run(
32521
- runnable.map((item) => ({ data: item.case, file: item.file }))
33030
+ prepared.map(
33031
+ (item) => item
33032
+ )
32522
33033
  );
32523
33034
  return { summary, invalid, tagFiltered, missingAccounts };
32524
33035
  }
@@ -32542,20 +33053,20 @@ async function getRun(runId, options = {}) {
32542
33053
  const base = runsRoot(options.root);
32543
33054
  const file2 = join17(base, runId, RESULT_FILE);
32544
33055
  try {
32545
- return JSON.parse(await readFile6(file2, "utf8"));
33056
+ return JSON.parse(await readFile7(file2, "utf8"));
32546
33057
  } catch {
32547
33058
  throw new RunError("ENO_CASES", `\u8BFB\u53D6\u8FD0\u884C\u7ED3\u679C\u5931\u8D25:${file2}(\u8FD0\u884C\u672A\u5B8C\u6210\u6216 runId \u4E0D\u5B58\u5728)`);
32548
33059
  }
32549
33060
  }
32550
33061
  async function getRunEvents(runId, options = {}) {
32551
33062
  const file2 = join17(runsRoot(options.root), runId, "events.ndjson");
32552
- const source = await readFile6(file2, "utf8").catch(() => "");
33063
+ const source = await readFile7(file2, "utf8").catch(() => "");
32553
33064
  return source.split("\n").filter((line) => line.trim().length > 0).map((line) => JSON.parse(line));
32554
33065
  }
32555
33066
  async function readRunMeta(base, runId) {
32556
33067
  try {
32557
33068
  const summary = JSON.parse(
32558
- await readFile6(join17(base, runId, RESULT_FILE), "utf8")
33069
+ await readFile7(join17(base, runId, RESULT_FILE), "utf8")
32559
33070
  );
32560
33071
  return {
32561
33072
  status: summary.status,
@@ -32579,7 +33090,7 @@ async function generateReport(runId, options = {}) {
32579
33090
  const runDir = join18(resolveRoot(options.root), RUNS_DIR, id);
32580
33091
  let summary;
32581
33092
  try {
32582
- summary = JSON.parse(await readFile7(join18(runDir, "result.json"), "utf8"));
33093
+ summary = JSON.parse(await readFile8(join18(runDir, "result.json"), "utf8"));
32583
33094
  } catch {
32584
33095
  throw new Error(`\u8BFB\u53D6\u8FD0\u884C\u7ED3\u679C\u5931\u8D25:${join18(runDir, "result.json")}(\u8FD0\u884C\u672A\u5B8C\u6210\u6216 runId \u4E0D\u5B58\u5728)`);
32585
33096
  }
@@ -32610,13 +33121,13 @@ async function generateSummaryReport(options = {}) {
32610
33121
  }
32611
33122
  async function readReport(runId, options = {}) {
32612
33123
  const file2 = join18(resolveRoot(options.root), RUNS_DIR, runId, "report.json");
32613
- const source = await readFile7(file2, "utf8").catch(() => void 0);
33124
+ const source = await readFile8(file2, "utf8").catch(() => void 0);
32614
33125
  return source ? JSON.parse(source) : void 0;
32615
33126
  }
32616
33127
 
32617
33128
  // ../../packages/sdk/src/client.ts
32618
33129
  function resolveInput(root, input2) {
32619
- return isAbsolute3(input2) ? input2 : join19(root, input2);
33130
+ return isAbsolute4(input2) ? input2 : join19(root, input2);
32620
33131
  }
32621
33132
  var TestPilotClient = class {
32622
33133
  /** 业务项目根目录(绝对路径) */
@@ -32692,13 +33203,13 @@ var TestPilotClient = class {
32692
33203
  };
32693
33204
 
32694
33205
  // src/commands/init/adapter-detector.ts
32695
- import { existsSync as existsSync4, readFileSync } from "node:fs";
33206
+ import { existsSync as existsSync5, readFileSync } from "node:fs";
32696
33207
  import { join as join20 } from "node:path";
32697
33208
  async function detectAdapters(root, projectType) {
32698
33209
  const required2 = projectType === "hybrid" ? ["playwright", "wechatide"] : projectType === "wechat-miniapp" ? ["wechatide"] : projectType === "web" ? ["playwright"] : [];
32699
33210
  const pkg = readPackageJsonDeps(root);
32700
33211
  const hasPlaywrightDep = pkg !== void 0 && ("playwright" in pkg || "@playwright/test" in pkg);
32701
- const playwrightInstalled = hasPlaywrightDep || existsSync4(join20(root, "node_modules", "playwright"));
33212
+ const playwrightInstalled = hasPlaywrightDep || existsSync5(join20(root, "node_modules", "playwright"));
32702
33213
  const devtoolsCli = await findWechatDevToolsCli().catch(() => void 0);
32703
33214
  return [
32704
33215
  {
@@ -32717,7 +33228,7 @@ async function detectAdapters(root, projectType) {
32717
33228
  }
32718
33229
  function readPackageJsonDeps(root) {
32719
33230
  const file2 = join20(root, "package.json");
32720
- if (!existsSync4(file2)) return void 0;
33231
+ if (!existsSync5(file2)) return void 0;
32721
33232
  try {
32722
33233
  const pkg = JSON.parse(readFileSync(file2, "utf8"));
32723
33234
  return { ...pkg.dependencies, ...pkg.devDependencies };
@@ -32727,13 +33238,13 @@ function readPackageJsonDeps(root) {
32727
33238
  }
32728
33239
 
32729
33240
  // src/commands/init/environment-initializer.ts
32730
- import { readFile as readFile8 } from "node:fs/promises";
33241
+ import { readFile as readFile9 } from "node:fs/promises";
32731
33242
  import { join as join21 } from "node:path";
32732
33243
  var ENV_REF_PATTERN = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
32733
33244
  async function collectEnvVarRefs(root) {
32734
33245
  let text;
32735
33246
  try {
32736
- text = await readFile8(join21(root, "testpilot.yaml"), "utf8");
33247
+ text = await readFile9(join21(root, "testpilot.yaml"), "utf8");
32737
33248
  } catch {
32738
33249
  return [];
32739
33250
  }
@@ -32749,7 +33260,7 @@ async function collectEnvVarRefs(root) {
32749
33260
  }
32750
33261
 
32751
33262
  // src/commands/init/project-detector.ts
32752
- import { existsSync as existsSync5, readFileSync as readFileSync2 } from "node:fs";
33263
+ import { existsSync as existsSync6, readFileSync as readFileSync2 } from "node:fs";
32753
33264
  import { basename, join as join22 } from "node:path";
32754
33265
 
32755
33266
  // src/lib/git.ts
@@ -32788,7 +33299,7 @@ var WEB_DEP_MARKERS = [
32788
33299
  var E2E_DIR_CANDIDATES = ["tests/e2e", "test/e2e", "e2e", "cypress"];
32789
33300
  function readPackageJson(root) {
32790
33301
  const file2 = join22(root, "package.json");
32791
- if (!existsSync5(file2)) return void 0;
33302
+ if (!existsSync6(file2)) return void 0;
32792
33303
  try {
32793
33304
  return JSON.parse(readFileSync2(file2, "utf8"));
32794
33305
  } catch {
@@ -32798,8 +33309,8 @@ function readPackageJson(root) {
32798
33309
  function detectProjectType(root) {
32799
33310
  const pkg = readPackageJson(root);
32800
33311
  const deps = { ...pkg?.dependencies, ...pkg?.devDependencies };
32801
- const isWechat = existsSync5(join22(root, "project.config.json")) || existsSync5(join22(root, "app.json")) || existsSync5(join22(root, "miniprogram", "app.json"));
32802
- 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}`)));
33312
+ const isWechat = existsSync6(join22(root, "project.config.json")) || existsSync6(join22(root, "app.json")) || existsSync6(join22(root, "miniprogram", "app.json"));
33313
+ const isWeb = existsSync6(join22(root, "index.html")) || WEB_DEP_MARKERS.some((marker) => deps?.[marker] !== void 0) || [".ts", ".tsx", ".js", ".jsx"].some((ext) => existsSync6(join22(root, "src", `index${ext}`)));
32803
33314
  if (isWechat && isWeb) return "hybrid";
32804
33315
  if (isWechat) return "wechat-miniapp";
32805
33316
  if (isWeb) return "web";
@@ -32808,9 +33319,9 @@ function detectProjectType(root) {
32808
33319
  async function detectProject(root) {
32809
33320
  const pkg = readPackageJson(root);
32810
33321
  const type = detectProjectType(root);
32811
- const language = existsSync5(join22(root, "tsconfig.json")) ? "TypeScript" : pkg || existsSync5(join22(root, "src")) ? "JavaScript" : "Unknown";
32812
- const packageManager = existsSync5(join22(root, "pnpm-lock.yaml")) ? "pnpm" : existsSync5(join22(root, "yarn.lock")) ? "yarn" : existsSync5(join22(root, "package-lock.json")) ? "npm" : "npx";
32813
- const existingE2eDirs = E2E_DIR_CANDIDATES.filter((dir) => existsSync5(join22(root, dir)));
33322
+ const language = existsSync6(join22(root, "tsconfig.json")) ? "TypeScript" : pkg || existsSync6(join22(root, "src")) ? "JavaScript" : "Unknown";
33323
+ const packageManager = existsSync6(join22(root, "pnpm-lock.yaml")) ? "pnpm" : existsSync6(join22(root, "yarn.lock")) ? "yarn" : existsSync6(join22(root, "package-lock.json")) ? "npm" : "npx";
33324
+ const existingE2eDirs = E2E_DIR_CANDIDATES.filter((dir) => existsSync6(join22(root, dir)));
32814
33325
  return {
32815
33326
  root,
32816
33327
  name: pkg?.name ?? basename(root),
@@ -32820,8 +33331,8 @@ async function detectProject(root) {
32820
33331
  git: await readGitInfo(root),
32821
33332
  hasTestsE2e: existingE2eDirs.includes("tests/e2e"),
32822
33333
  existingE2eDirs,
32823
- hasTestpilotConfig: existsSync5(join22(root, "testpilot.yaml")),
32824
- hasProjectLink: existsSync5(join22(root, ".testpilot", "project.json"))
33334
+ hasTestpilotConfig: existsSync6(join22(root, "testpilot.yaml")),
33335
+ hasProjectLink: existsSync6(join22(root, ".testpilot", "project.json"))
32825
33336
  };
32826
33337
  }
32827
33338
  function defaultAdapterFor(type) {
@@ -32836,9 +33347,9 @@ function adaptersFor(type) {
32836
33347
  }
32837
33348
 
32838
33349
  // src/commands/init/skill-installer.ts
32839
- import { existsSync as existsSync6, readFileSync as readFileSync3 } from "node:fs";
33350
+ import { existsSync as existsSync7, readFileSync as readFileSync3 } from "node:fs";
32840
33351
  import { access as access2, cp, mkdir as mkdir12, writeFile as writeFile10 } from "node:fs/promises";
32841
- import { dirname as dirname8, join as join23 } from "node:path";
33352
+ import { dirname as dirname9, join as join23 } from "node:path";
32842
33353
  import { fileURLToPath } from "node:url";
32843
33354
  var CORE_FILES = ["SKILL.md", "manifest.yaml"];
32844
33355
  var CORE_DIRS = ["rules", "workflows"];
@@ -32852,23 +33363,23 @@ async function exists3(path) {
32852
33363
  }
32853
33364
  function readManifestVersion(dir) {
32854
33365
  const manifestPath = join23(dir, "manifest.yaml");
32855
- if (!existsSync6(manifestPath)) return void 0;
33366
+ if (!existsSync7(manifestPath)) return void 0;
32856
33367
  return /^version:\s*([^\s#]+)/m.exec(readFileSync3(manifestPath, "utf8"))?.[1];
32857
33368
  }
32858
33369
  function resolveSkillSourceDir() {
32859
33370
  const fromEnv = process.env.TESTPILOT_SKILL_DIR;
32860
33371
  if (fromEnv) return fromEnv;
32861
33372
  const bundled = fileURLToPath(new URL("./skills/testpilot", import.meta.url));
32862
- if (existsSync6(bundled)) return bundled;
33373
+ if (existsSync7(bundled)) return bundled;
32863
33374
  const inRepo = fileURLToPath(new URL("../../../../../skills/testpilot", import.meta.url));
32864
- return existsSync6(inRepo) ? inRepo : void 0;
33375
+ return existsSync7(inRepo) ? inRepo : void 0;
32865
33376
  }
32866
33377
  async function copyCoreFiles(src, dest) {
32867
33378
  for (const name of CORE_FILES) {
32868
33379
  await cp(join23(src, name), join23(dest, name), { recursive: true, force: true });
32869
33380
  }
32870
33381
  for (const dir of CORE_DIRS) {
32871
- if (existsSync6(join23(src, dir))) {
33382
+ if (existsSync7(join23(src, dir))) {
32872
33383
  await cp(join23(src, dir), join23(dest, dir), { recursive: true, force: true });
32873
33384
  }
32874
33385
  }
@@ -32883,7 +33394,7 @@ async function syncSkillFiles(root, options = {}) {
32883
33394
  "\u65E0\u6CD5\u5B9A\u4F4D TestPilot Skill \u6E90\u76EE\u5F55;\u53EF\u901A\u8FC7\u73AF\u5883\u53D8\u91CF TESTPILOT_SKILL_DIR \u6307\u5B9A skills/testpilot \u8DEF\u5F84"
32884
33395
  );
32885
33396
  }
32886
- await mkdir12(dirname8(dest), { recursive: true });
33397
+ await mkdir12(dirname9(dest), { recursive: true });
32887
33398
  await cp(src, dest, { recursive: true });
32888
33399
  return { core: "created", bundledVersion, dest };
32889
33400
  }
@@ -32957,6 +33468,12 @@ function renderTestConventionsMd() {
32957
33468
  6. \u4F18\u5148\u590D\u7528\u5DF2\u6709 Case
32958
33469
  7. \u65B0\u589E Case \u5FC5\u987B\u901A\u8FC7 csspilot validate
32959
33470
  8. Case ID \u5FC5\u987B\u552F\u4E00
33471
+ 9. fixture/dataset \u4F7F\u7528 YAML \u6216 JSON,\u7EDF\u4E00\u4F4D\u4E8E tests/e2e/data
33472
+ 10. \u9700\u6C42\u89C4\u5219\u77E9\u9635\u4E0E QA \u8986\u76D6\u6620\u5C04\u5199\u5165 tests/e2e/reviews/<\u9700\u6C42>-coverage.md
33473
+ 11. \u53EA\u5728\u6B65\u9AA4\u6A21\u677F\u76F8\u540C\u65F6\u4F7F\u7528 data-driven Case,\u6BCF\u884C\u4FDD\u6301\u4E00\u4E2A\u53EF\u5224\u5B9A\u89C4\u5219/\u6570\u636E\u70B9
33474
+ 12. requires \u53EA\u58F0\u660E\u9879\u76EE\u8BC1\u636E\u652F\u6301\u7684\u4F9D\u8D56,\u4E0D\u5F97\u5957\u7528\u8D26\u53F7/\u4F18\u60E0\u5238/PPM \u7B49\u9ED8\u8BA4\u5B57\u6BB5
33475
+ 13. Case \u4E0D\u5F97\u8BFB\u53D6\u4EFB\u610F\u73AF\u5883\u53D8\u91CF;\u5148\u5728 testpilot.yaml variables \u767D\u540D\u5355\u6620\u5C04\u540E\u4F7F\u7528 \${variable.*}
33476
+ 14. setup/teardown \u4F18\u5148 API;teardown \u5931\u8D25\u4F5C\u4E3A warning,\u4E0D\u6539\u53D8\u4E3B\u6D41\u7A0B\u7ED3\u679C
32960
33477
  `;
32961
33478
  }
32962
33479
  function renderAdaptersMd(adapters) {
@@ -32997,8 +33514,8 @@ async function runDoctor(root) {
32997
33514
  const projectItems = [
32998
33515
  ok(`Project ${info.name}(${info.type})`),
32999
33516
  info.git.commit ? ok(`Git repository(${info.git.branch ?? "detached"} \xB7 ${info.git.commit.slice(0, 7)})`) : warn("Git repository \u672A\u68C0\u6D4B\u5230", "Case Metadata \u65E0\u6CD5\u540C\u6B65\u5230 Server"),
33000
- existsSync7(join24(root, ".testpilot", "project.json")) ? ok(".testpilot/project.json") : error62(".testpilot/project.json", "\u8FD0\u884C npx csspilot init \u751F\u6210"),
33001
- existsSync7(join24(root, "testpilot.yaml")) ? ok("testpilot.yaml") : error62("testpilot.yaml", "\u8FD0\u884C npx csspilot init \u751F\u6210")
33517
+ existsSync8(join24(root, ".testpilot", "project.json")) ? ok(".testpilot/project.json") : error62(".testpilot/project.json", "\u8FD0\u884C npx csspilot init \u751F\u6210"),
33518
+ existsSync8(join24(root, "testpilot.yaml")) ? ok("testpilot.yaml") : error62("testpilot.yaml", "\u8FD0\u884C npx csspilot init \u751F\u6210")
33002
33519
  ];
33003
33520
  push("Project", projectItems);
33004
33521
  const config2 = await loadTestpilotConfig(root).catch(() => void 0);
@@ -33007,8 +33524,8 @@ async function runDoctor(root) {
33007
33524
  const invalidCount = cases.filter((item) => !item.valid).length;
33008
33525
  const testDir = join24(root, "tests", "e2e");
33009
33526
  push("Test", [
33010
- existsSync7(testDir) ? ok("tests/e2e") : error62("tests/e2e", "\u8FD0\u884C npx csspilot init \u521B\u5EFA"),
33011
- existsSync7(join24(root, casesDir)) ? ok(`cases directory(${casesDir})`) : error62(`cases directory(${casesDir})`, "\u8FD0\u884C npx csspilot init \u521B\u5EFA"),
33527
+ existsSync8(testDir) ? ok("tests/e2e") : error62("tests/e2e", "\u8FD0\u884C npx csspilot init \u521B\u5EFA"),
33528
+ existsSync8(join24(root, casesDir)) ? ok(`cases directory(${casesDir})`) : error62(`cases directory(${casesDir})`, "\u8FD0\u884C npx csspilot init \u521B\u5EFA"),
33012
33529
  cases.length === 0 ? warn("Case validation \u8DF3\u8FC7(\u672A\u627E\u5230\u7528\u4F8B)", "\u5728 tests/e2e/cases \u521B\u5EFA Case") : invalidCount === 0 ? ok(`Case validation(${cases.length} \u4E2A\u7528\u4F8B\u5168\u90E8\u6709\u6548)`) : error62(`Case validation(${invalidCount}/${cases.length} \u4E2A\u7528\u4F8B\u6821\u9A8C\u5931\u8D25)`, "\u8FD0\u884C npx csspilot validate \u67E5\u770B\u8BE6\u60C5")
33013
33530
  ]);
33014
33531
  const skillDir = join24(root, ".agents", "skills", "testpilot");
@@ -33017,7 +33534,7 @@ async function runDoctor(root) {
33017
33534
  const bundledSkillVersion = skillSource ? readManifestVersion(skillSource) : void 0;
33018
33535
  const skillItems = [
33019
33536
  installedSkillVersion ? ok(`TestPilot Skill(.agents/skills/testpilot,v${installedSkillVersion})`) : error62("TestPilot Skill", "\u8FD0\u884C npx csspilot init \u5B89\u88C5"),
33020
- 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")
33537
+ existsSync8(join24(skillDir, "references", "project.md")) ? ok("Project testing guide(references/)") : warn("Project testing guide \u7F3A\u5931", "references/ \u7531 init \u751F\u6210;\u53EF\u91CD\u65B0\u8FD0\u884C npx csspilot init")
33021
33538
  ];
33022
33539
  if (installedSkillVersion && bundledSkillVersion && installedSkillVersion !== bundledSkillVersion) {
33023
33540
  skillItems.push(
@@ -33112,7 +33629,7 @@ function makeDoctorCommand() {
33112
33629
  import { join as join27 } from "node:path";
33113
33630
 
33114
33631
  // src/commands/init/config-initializer.ts
33115
- import { access as access3, mkdir as mkdir13, readFile as readFile9, writeFile as writeFile11 } from "node:fs/promises";
33632
+ import { access as access3, mkdir as mkdir13, readFile as readFile10, writeFile as writeFile11 } from "node:fs/promises";
33116
33633
  import { basename as basename2, join as join25 } from "node:path";
33117
33634
  async function exists4(path) {
33118
33635
  try {
@@ -33144,6 +33661,10 @@ environment:
33144
33661
  staging:
33145
33662
  baseUrl: \${STAGING_BASE_URL}
33146
33663
 
33664
+ # Case \u53EF\u89C1\u7684\u9879\u76EE\u53D8\u91CF\u767D\u540D\u5355(\u503C\u6765\u81EA .env / CI Secret;Case \u7528 \${variable.<name>} \u5F15\u7528)
33665
+ # variables:
33666
+ # ppmProjectId: \${PPM_PROJECT_ID}
33667
+
33147
33668
  # \u9879\u76EE\u58F0\u660E\u7684\u6267\u884C\u7AEF(\u5B9E\u9645\u53EF\u7528\u6027\u4EE5 csspilot doctor \u68C0\u6D4B\u4E3A\u51C6)
33148
33669
  adapters:
33149
33670
  ${adapters}
@@ -33194,7 +33715,7 @@ ${entry}
33194
33715
  `, "utf8");
33195
33716
  return "created";
33196
33717
  }
33197
- const content = await readFile9(gitignorePath, "utf8");
33718
+ const content = await readFile10(gitignorePath, "utf8");
33198
33719
  if (content.split(/\r?\n/).some((item) => item.trim() === entry)) {
33199
33720
  return "exists";
33200
33721
  }
@@ -33278,19 +33799,21 @@ function randomId() {
33278
33799
  }
33279
33800
 
33280
33801
  // src/commands/init/test-structure.ts
33281
- import { existsSync as existsSync8, readdirSync as readdirSync2 } from "node:fs";
33802
+ import { existsSync as existsSync9, readdirSync as readdirSync2 } from "node:fs";
33282
33803
  import { mkdir as mkdir14, writeFile as writeFile12 } from "node:fs/promises";
33283
33804
  import { join as join26 } from "node:path";
33284
33805
  async function initTestStructure(root, _info) {
33285
33806
  const dirs = {
33286
33807
  cases: join26(root, "tests", "e2e", "cases"),
33287
33808
  fixtures: join26(root, "tests", "e2e", "fixtures"),
33288
- data: join26(root, "tests", "e2e", "data")
33809
+ data: join26(root, "tests", "e2e", "data"),
33810
+ reviews: join26(root, "tests", "e2e", "reviews")
33289
33811
  };
33290
33812
  const existed = {
33291
- cases: existsSync8(dirs.cases),
33292
- fixtures: existsSync8(dirs.fixtures),
33293
- data: existsSync8(dirs.data)
33813
+ cases: existsSync9(dirs.cases),
33814
+ fixtures: existsSync9(dirs.fixtures),
33815
+ data: existsSync9(dirs.data),
33816
+ reviews: existsSync9(dirs.reviews)
33294
33817
  };
33295
33818
  for (const dir of Object.values(dirs)) {
33296
33819
  await mkdir14(dir, { recursive: true });
@@ -33303,7 +33826,8 @@ async function initTestStructure(root, _info) {
33303
33826
  return {
33304
33827
  cases: existed.cases ? "reused" : "created",
33305
33828
  fixtures: existed.fixtures ? "reused" : "created",
33306
- data: existed.data ? "reused" : "created"
33829
+ data: existed.data ? "reused" : "created",
33830
+ reviews: existed.reviews ? "reused" : "created"
33307
33831
  };
33308
33832
  }
33309
33833
 
@@ -33343,7 +33867,7 @@ async function runInit(root, options = {}) {
33343
33867
  const testStructure = await initTestStructure(root, project);
33344
33868
  const reuseNote = project.existingE2eDirs.filter((dir) => dir !== "tests/e2e");
33345
33869
  console.log(
33346
- testStructure.cases === "reused" ? "\u2713 \u6D4B\u8BD5\u76EE\u5F55 tests/e2e(\u5DF2\u5B58\u5728,\u590D\u7528)" : "\u2713 \u6D4B\u8BD5\u76EE\u5F55 tests/e2e/{cases,fixtures,data}"
33870
+ testStructure.cases === "reused" ? "\u2713 \u6D4B\u8BD5\u76EE\u5F55 tests/e2e(\u5DF2\u5B58\u5728,\u590D\u7528)" : "\u2713 \u6D4B\u8BD5\u76EE\u5F55 tests/e2e/{cases,fixtures,data,reviews}"
33347
33871
  );
33348
33872
  if (reuseNote.length > 0) {
33349
33873
  console.log(` \u26A0 \u68C0\u6D4B\u5230\u5176\u4ED6 E2E \u76EE\u5F55:${reuseNote.join(", ")}(\u4FDD\u7559\u4E0D\u52A8,\u672A\u5F3A\u5236\u8FC1\u79FB)`);
@@ -33486,7 +34010,7 @@ function makeRunCommand() {
33486
34010
  ].filter(Boolean).join(",");
33487
34011
  console.log("");
33488
34012
  console.log(
33489
- `Run ${summary.runId}${summary.cancelled ? "(\u5DF2\u53D6\u6D88)" : ""}: ${summary.status.toUpperCase()} \u2014 \u7528\u4F8B \u2713${summary.totals.passed} \u2717${summary.totals.failed},\u6B65\u9AA4 \u2713${summary.totals.stepsPassed} \u2717${summary.totals.stepsFailed} \u21B7${summary.totals.stepsSkipped}${notes ? `(${notes})` : ""}`
34013
+ `Run ${summary.runId}${summary.cancelled ? "(\u5DF2\u53D6\u6D88)" : ""}: ${summary.status.toUpperCase()} \u2014 \u6A21\u677F ${summary.totals.templates ?? summary.totals.cases},\u6570\u636E\u884C \u2713${summary.totals.passed} \u2717${summary.totals.failed} \u21B7${summary.totals.skipped ?? 0} \u26A0${summary.totals.warnings ?? 0},\u6B65\u9AA4 \u2713${summary.totals.stepsPassed} \u2717${summary.totals.stepsFailed} \u21B7${summary.totals.stepsSkipped} \u26A0${summary.totals.stepsWarning ?? 0}${notes ? `(${notes})` : ""}`
33490
34014
  );
33491
34015
  console.log(`\u4EA7\u7269\u76EE\u5F55:${join28(".testpilot", "artifacts", "runs", summary.runId)}`);
33492
34016
  console.log("\u67E5\u770B\u62A5\u544A:npx csspilot report");
@@ -33513,12 +34037,12 @@ function consoleEventHandler(event) {
33513
34037
  `);
33514
34038
  break;
33515
34039
  case "case-started":
33516
- console.log(`\u25B6 ${event.caseId} (${event.file})`);
34040
+ console.log(`\u25B6 ${event.caseId}#${event.rowId ?? "default"} (${event.file})`);
33517
34041
  break;
33518
34042
  case "step-finished": {
33519
- const mark = event.status === "passed" ? " \u2713" : event.status === "failed" ? " \u2717" : " \u21B7";
34043
+ const mark = event.status === "passed" ? " \u2713" : event.status === "failed" ? " \u2717" : event.status === "warning" ? " \u26A0" : " \u21B7";
33520
34044
  console.log(
33521
- `${mark} ${event.target}/${event.action} ${event.durationMs}ms${event.error ? ` \u2014 ${event.error}` : ""}`
34045
+ `${mark} ${event.phase ?? "steps"} ${event.target}/${event.action} ${event.durationMs}ms${event.error ? ` \u2014 ${event.error}` : ""}`
33522
34046
  );
33523
34047
  break;
33524
34048
  }
@@ -33528,7 +34052,7 @@ function consoleEventHandler(event) {
33528
34052
  }
33529
34053
 
33530
34054
  // src/commands/sync-metadata.ts
33531
- import { relative } from "node:path";
34055
+ import { relative as relative2 } from "node:path";
33532
34056
  function makeSyncMetadataCommand() {
33533
34057
  return new Command("sync-metadata").description("\u540C\u6B65 Case Metadata \u5FEB\u7167\u5230 TestPilot Server(CI / git push \u540E\u4F7F\u7528)").option("--server <url>", "TestPilot Server \u5730\u5740(\u9ED8\u8BA4\u53D6 project.json \u6216 testpilot.yaml)").action(async (options) => {
33534
34058
  const client = new TestPilotClient();
@@ -33555,7 +34079,7 @@ function makeSyncMetadataCommand() {
33555
34079
  const cases = infos.filter((item) => item.valid && item.case).map((item) => ({
33556
34080
  id: item.case.id,
33557
34081
  title: item.case.name,
33558
- file: relative(process.cwd(), item.file).split("\\").join("/"),
34082
+ file: relative2(process.cwd(), item.file).split("\\").join("/"),
33559
34083
  tags: item.case.tags ?? []
33560
34084
  }));
33561
34085
  const skipped = infos.length - cases.length;
@@ -33665,7 +34189,7 @@ function makeValidateCommand() {
33665
34189
  }
33666
34190
 
33667
34191
  // src/cli.ts
33668
- var CLI_VERSION = "0.5.0";
34192
+ var CLI_VERSION = "0.6.0";
33669
34193
  async function createCli() {
33670
34194
  const program2 = new Command();
33671
34195
  program2.name("csspilot").description("TestPilot \u2014 AI Native \u8DE8\u7AEF\u4E1A\u52A1\u6D4B\u8BD5\u57FA\u7840\u8BBE\u65BD CLI").version(CLI_VERSION, "-v, --version", "output the current version").option("-V", "output the current version", () => {