csspilot 0.7.0 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -30115,9 +30115,10 @@ var configSchema = external_exports.strictObject({
30115
30115
  function interpolateEnvRefs(value) {
30116
30116
  return value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (match, name) => process.env[name] ?? match);
30117
30117
  }
30118
+ var ENV_TESTING_FILE = ".env.testing";
30118
30119
  function loadProjectEnvFile(root = process.cwd()) {
30119
30120
  try {
30120
- process.loadEnvFile(join2(root, ".env"));
30121
+ process.loadEnvFile(join2(root, ENV_TESTING_FILE));
30121
30122
  } catch {
30122
30123
  }
30123
30124
  }
@@ -30183,8 +30184,8 @@ async function loadTestpilotConfig(cwd = process.cwd()) {
30183
30184
  }
30184
30185
 
30185
30186
  // ../../packages/core/src/wechat-devtools.ts
30186
- import { execFile } from "node:child_process";
30187
- import { existsSync, readdirSync } from "node:fs";
30187
+ import { exec, execFile } from "node:child_process";
30188
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
30188
30189
  import { join as join3 } from "node:path";
30189
30190
  var CLI_NAMES = ["cli.bat", "cli"];
30190
30191
  var REGISTRY_UNINSTALL_KEYS = [
@@ -30271,6 +30272,42 @@ function execToString(command, args) {
30271
30272
  });
30272
30273
  });
30273
30274
  }
30275
+ async function isWechatDevToolsLoggedIn(cliPath) {
30276
+ const output2 = await execCliToString(cliPath, ["islogin"]).catch(() => void 0);
30277
+ if (output2 === void 0) return void 0;
30278
+ if (/is\s*login\s*[:=]?\s*(true|yes|1)\b/i.test(output2)) return true;
30279
+ if (/is\s*login\s*[:=]?\s*(false|no|0)\b/i.test(output2)) return false;
30280
+ if (/\btrue\b/i.test(output2)) return true;
30281
+ if (/\bfalse\b/i.test(output2)) return false;
30282
+ return void 0;
30283
+ }
30284
+ function execCliToString(cliPath, args) {
30285
+ return new Promise((resolvePromise, rejectPromise) => {
30286
+ const quote = (value) => /\s/.test(value) ? `"${value}"` : value;
30287
+ exec(
30288
+ `"${cliPath}" ${args.map(quote).join(" ")}`,
30289
+ { windowsHide: true, timeout: 3e4, encoding: "buffer" },
30290
+ (err, stdout, stderr) => {
30291
+ const text = `${stdout.toString("utf8")}${stderr.toString("utf8")}`.trim();
30292
+ if (err && !text) {
30293
+ rejectPromise(err);
30294
+ return;
30295
+ }
30296
+ resolvePromise(text);
30297
+ }
30298
+ );
30299
+ });
30300
+ }
30301
+ function readMiniappProjectAppid(projectPath) {
30302
+ const configPath = join3(projectPath, "project.config.json");
30303
+ if (!existsSync(configPath)) return void 0;
30304
+ try {
30305
+ const parsed = JSON.parse(readFileSync(configPath, "utf8"));
30306
+ return { appid: typeof parsed.appid === "string" ? parsed.appid : void 0, configPath };
30307
+ } catch {
30308
+ return void 0;
30309
+ }
30310
+ }
30274
30311
 
30275
30312
  // ../../node_modules/.pnpm/commander@12.1.0/node_modules/commander/esm.mjs
30276
30313
  var import_index = __toESM(require_commander(), 1);
@@ -30388,8 +30425,20 @@ var ACTIONS = [
30388
30425
  "assert",
30389
30426
  "extract",
30390
30427
  "screenshot",
30391
- "request"
30428
+ "request",
30429
+ "callMethod",
30430
+ "mockWxApi",
30431
+ "trigger",
30432
+ "setData",
30433
+ "longpress",
30434
+ "callWxApi",
30435
+ "touch",
30436
+ "scroll",
30437
+ "swipeTo",
30438
+ "slideTo",
30439
+ "moveTo"
30392
30440
  ];
30441
+ var TOUCH_PHASES = ["touchstart", "touchmove", "touchend"];
30393
30442
  var locatorSchema = external_exports.strictObject({
30394
30443
  text: external_exports.string().min(1).optional(),
30395
30444
  css: external_exports.string().min(1).optional()
@@ -30405,7 +30454,7 @@ var stepSchema = external_exports.strictObject({
30405
30454
  action: external_exports.enum(ACTIONS),
30406
30455
  locator: locatorSchema.optional(),
30407
30456
  url: external_exports.string().min(1).optional(),
30408
- value: external_exports.string().optional(),
30457
+ value: external_exports.union([external_exports.string(), external_exports.number()]).optional(),
30409
30458
  expected: external_exports.string().optional(),
30410
30459
  /** api target:request 的 HTTP 方法(默认 GET) */
30411
30460
  method: external_exports.enum(HTTP_METHODS).optional(),
@@ -30416,7 +30465,44 @@ var stepSchema = external_exports.strictObject({
30416
30465
  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(),
30417
30466
  timeout: external_exports.number().int().positive().optional(),
30418
30467
  /** UI 动作触发的网络请求精确次数断言 */
30419
- expectRequests: external_exports.array(requestExpectationSchema).min(1).optional()
30468
+ expectRequests: external_exports.array(requestExpectationSchema).min(1).optional(),
30469
+ /** callMethod:要调用的 Page / 组件方法名(仅 miniapp;页面级调用不写 locator) */
30470
+ methodName: external_exports.string().min(1).optional(),
30471
+ /** callMethod 的入参(支持 ${var} 模板) */
30472
+ args: external_exports.array(external_exports.unknown()).optional(),
30473
+ /** mockWxApi:要 mock 的 wx.* API 名(如 showModal / request) */
30474
+ wxApi: external_exports.string().min(1).optional(),
30475
+ /** mockWxApi:mock 返回值(对象按原样注入;capture 时真实调用先上报再返回 mock 值) */
30476
+ result: external_exports.record(external_exports.string(), external_exports.unknown()).optional(),
30477
+ /** mockWxApi:捕获真实调用参数的变量名(${<capture>.options} / ${<capture>.called}) */
30478
+ capture: external_exports.string().min(1).optional(),
30479
+ /** trigger:触发的事件类型(如 getphonenumber;空 detail 即"用户拒绝/取消"类分支) */
30480
+ eventType: external_exports.string().min(1).optional(),
30481
+ /** trigger:作为事件 e.detail 注入(值支持 ${var} 模板) */
30482
+ detail: external_exports.record(external_exports.string(), external_exports.unknown()).optional(),
30483
+ /** setData:写入页面 data 的补丁(值支持 ${var} 模板);提供 locator 时为组件级 setData */
30484
+ patch: external_exports.record(external_exports.string(), external_exports.unknown()).optional(),
30485
+ /** assert:页面 data 路径断言(与 locator/attribute/property/count 互斥,仅 miniapp) */
30486
+ dataPath: external_exports.string().min(1).optional(),
30487
+ /** assert:元素 attribute 名(与 expected 组合做属性断言;web/miniapp) */
30488
+ attribute: external_exports.string().min(1).optional(),
30489
+ /** assert:元素 property 名(自定义组件实例属性;仅 miniapp) */
30490
+ property: external_exports.string().min(1).optional(),
30491
+ /** assert:匹配元素的精确数量(0 即"元素不存在"断言;与 expected/attribute/property/dataPath 互斥) */
30492
+ count: external_exports.number().int().nonnegative().optional(),
30493
+ /** touch:触摸阶段 */
30494
+ phase: external_exports.enum(TOUCH_PHASES).optional(),
30495
+ /** touch:触摸点数组(元素如 [{identifier: 0, x: 100, y: 200}],值支持 ${var} 模板) */
30496
+ touches: external_exports.array(external_exports.record(external_exports.string(), external_exports.unknown())).min(1).optional(),
30497
+ /** touch:changedTouches 触摸点数组;缺省复用 touches */
30498
+ changedTouches: external_exports.array(external_exports.record(external_exports.string(), external_exports.unknown())).optional(),
30499
+ /** scroll:滚动位置(scroll-view 内滚动提供 locator;缺省滚动页面) */
30500
+ scrollTop: external_exports.number().optional(),
30501
+ /** swipeTo:swiper 目标页索引 */
30502
+ index: external_exports.number().int().nonnegative().optional(),
30503
+ /** moveTo:movable-view 目标坐标 */
30504
+ x: external_exports.number().optional(),
30505
+ y: external_exports.number().optional()
30420
30506
  });
30421
30507
  var datasetReferenceSchema = external_exports.strictObject({
30422
30508
  file: external_exports.string().min(1),
@@ -30489,20 +30575,61 @@ var ADAPTER_SUPPORT = {
30489
30575
  assert: ["web", "miniapp", "api"],
30490
30576
  extract: ["web", "miniapp", "api"],
30491
30577
  screenshot: ["web", "miniapp"],
30492
- request: ["api"]
30578
+ request: ["api"],
30579
+ callMethod: ["miniapp"],
30580
+ mockWxApi: ["miniapp"],
30581
+ trigger: ["miniapp"],
30582
+ setData: ["miniapp"],
30583
+ longpress: ["web", "miniapp"],
30584
+ callWxApi: ["miniapp"],
30585
+ touch: ["miniapp"],
30586
+ scroll: ["miniapp"],
30587
+ swipeTo: ["miniapp"],
30588
+ slideTo: ["miniapp"],
30589
+ moveTo: ["miniapp"]
30493
30590
  };
30494
- var LOCATOR_REQUIRED = /* @__PURE__ */ new Set(["click", "input", "select", "assert", "extract"]);
30495
- var VALUE_REQUIRED = /* @__PURE__ */ new Set(["input", "select"]);
30591
+ var LOCATOR_REQUIRED = /* @__PURE__ */ new Set([
30592
+ "click",
30593
+ "input",
30594
+ "select",
30595
+ "assert",
30596
+ "extract",
30597
+ "longpress",
30598
+ "touch",
30599
+ "swipeTo",
30600
+ "slideTo",
30601
+ "moveTo"
30602
+ ]);
30603
+ var VALUE_REQUIRED = /* @__PURE__ */ new Set(["input", "select", "slideTo"]);
30496
30604
  var REQUEST_EXPECTATION_ACTIONS = /* @__PURE__ */ new Set(["navigate", "click", "input", "select"]);
30497
30605
  var FIELD_ALLOWED = {
30498
30606
  url: ["navigate", "request"],
30499
- value: ["input", "select", "extract"],
30607
+ value: ["input", "select", "extract", "slideTo"],
30500
30608
  expected: ["assert", "request"],
30501
- variable: ["extract"],
30609
+ variable: ["extract", "callWxApi"],
30502
30610
  timeout: ["wait"],
30503
30611
  method: ["request"],
30504
30612
  headers: ["request"],
30505
- body: ["request"]
30613
+ body: ["request"],
30614
+ methodName: ["callMethod"],
30615
+ args: ["callMethod", "callWxApi"],
30616
+ wxApi: ["mockWxApi", "callWxApi"],
30617
+ result: ["mockWxApi"],
30618
+ capture: ["mockWxApi"],
30619
+ eventType: ["trigger"],
30620
+ detail: ["trigger"],
30621
+ patch: ["setData"],
30622
+ dataPath: ["assert"],
30623
+ attribute: ["assert"],
30624
+ property: ["assert"],
30625
+ count: ["assert"],
30626
+ phase: ["touch"],
30627
+ touches: ["touch"],
30628
+ changedTouches: ["touch"],
30629
+ scrollTop: ["scroll"],
30630
+ index: ["swipeTo"],
30631
+ x: ["moveTo"],
30632
+ y: ["moveTo"]
30506
30633
  };
30507
30634
  var VARIABLE_REF = /\$\{([A-Za-z_][A-Za-z0-9_.]*)\}/g;
30508
30635
  function validateCase(data) {
@@ -30566,19 +30693,82 @@ function checkStep(step, at, declared, issues) {
30566
30693
  });
30567
30694
  }
30568
30695
  }
30696
+ if (step.action === "callWxApi" && !step.wxApi) {
30697
+ issues.push({ path: `${at}.wxApi`, code: "semantic", message: "callWxApi \u5FC5\u987B\u63D0\u4F9B wxApi(\u8981\u4E3B\u52A8\u8C03\u7528\u7684 wx.* API \u540D,\u5982 setStorageSync)" });
30698
+ }
30699
+ if (step.action === "touch") {
30700
+ if (!step.phase) {
30701
+ issues.push({ path: `${at}.phase`, code: "semantic", message: "touch \u5FC5\u987B\u63D0\u4F9B phase(touchstart / touchmove / touchend)" });
30702
+ }
30703
+ if (!step.touches || step.touches.length === 0) {
30704
+ issues.push({ path: `${at}.touches`, code: "semantic", message: "touch \u5FC5\u987B\u63D0\u4F9B touches(\u89E6\u6478\u70B9\u6570\u7EC4,\u5982 [{identifier: 0, x: 0, y: 0}])" });
30705
+ }
30706
+ }
30707
+ if (step.action === "scroll" && step.scrollTop === void 0) {
30708
+ issues.push({ path: `${at}.scrollTop`, code: "semantic", message: "scroll \u5FC5\u987B\u63D0\u4F9B scrollTop(\u6EDA\u52A8\u4F4D\u7F6E\u50CF\u7D20)" });
30709
+ }
30710
+ if (step.action === "swipeTo" && step.index === void 0) {
30711
+ issues.push({ path: `${at}.index`, code: "semantic", message: "swipeTo \u5FC5\u987B\u63D0\u4F9B index(swiper \u76EE\u6807\u9875\u7D22\u5F15)" });
30712
+ }
30713
+ if (step.action === "moveTo" && (step.x === void 0 || step.y === void 0)) {
30714
+ issues.push({ path: `${at}.x`, code: "semantic", message: "moveTo \u5FC5\u987B\u63D0\u4F9B x \u4E0E y(movable-view \u76EE\u6807\u5750\u6807)" });
30715
+ }
30716
+ if (step.action === "assert") {
30717
+ const modes = [
30718
+ step.dataPath !== void 0,
30719
+ step.attribute !== void 0,
30720
+ step.property !== void 0,
30721
+ step.count !== void 0
30722
+ ].filter(Boolean).length;
30723
+ if (modes > 1) {
30724
+ issues.push({ path: `${at}.assert`, code: "semantic", message: "assert \u7684 dataPath / attribute / property / count \u53EA\u80FD\u63D0\u4F9B\u5176\u4E00(\u7F3A\u7701\u4E3A\u5143\u7D20\u6587\u672C\u65AD\u8A00)" });
30725
+ }
30726
+ if (step.property !== void 0 && step.target !== "miniapp") {
30727
+ issues.push({ path: `${at}.property`, code: "semantic", message: "property \u65AD\u8A00\u4EC5 miniapp \u652F\u6301(\u81EA\u5B9A\u4E49\u7EC4\u4EF6\u5B9E\u4F8B\u5C5E\u6027)" });
30728
+ }
30729
+ if ((step.attribute !== void 0 || step.property !== void 0) && !step.locator) {
30730
+ issues.push({ path: `${at}.locator`, code: "semantic", message: "attribute / property \u65AD\u8A00\u5FC5\u987B\u63D0\u4F9B locator" });
30731
+ }
30732
+ if (step.count !== void 0 && !step.locator) {
30733
+ issues.push({ path: `${at}.locator`, code: "semantic", message: "count \u65AD\u8A00\u5FC5\u987B\u63D0\u4F9B locator(\u7EDF\u8BA1\u5339\u914D\u5143\u7D20)" });
30734
+ }
30735
+ if (step.count !== void 0 && step.expected !== void 0) {
30736
+ issues.push({ path: `${at}.assert`, code: "semantic", message: "count \u65AD\u8A00\u4EE5 count \u4E3A\u671F\u671B\u503C,\u4E0D\u9700\u8981 expected" });
30737
+ }
30738
+ if (step.target === "api" && (step.attribute !== void 0 || step.property !== void 0 || step.count !== void 0)) {
30739
+ issues.push({ path: `${at}.assert`, code: "semantic", message: "api \u7AEF assert \u4EC5\u652F\u6301 expected(\u54CD\u5E94\u4F53\u5305\u542B)" });
30740
+ }
30741
+ if (step.dataPath !== void 0 && step.target !== "miniapp") {
30742
+ issues.push({ path: `${at}.dataPath`, code: "semantic", message: "dataPath \u65AD\u8A00\u4EC5 miniapp \u652F\u6301(web \u7AEF\u7528 locator)" });
30743
+ }
30744
+ }
30745
+ if (step.action === "mockWxApi") {
30746
+ if (!step.wxApi) {
30747
+ issues.push({ path: `${at}.wxApi`, code: "semantic", message: "mockWxApi \u5FC5\u987B\u63D0\u4F9B wxApi(\u8981 mock \u7684 wx.* API \u540D)" });
30748
+ }
30749
+ if (!step.result) {
30750
+ issues.push({ path: `${at}.result`, code: "semantic", message: "mockWxApi \u5FC5\u987B\u63D0\u4F9B result(mock \u8FD4\u56DE\u503C)" });
30751
+ }
30752
+ }
30753
+ if (step.action === "trigger" && !step.eventType) {
30754
+ issues.push({ path: `${at}.eventType`, code: "semantic", message: "trigger \u5FC5\u987B\u63D0\u4F9B eventType(\u89E6\u53D1\u7684\u4E8B\u4EF6\u7C7B\u578B)" });
30755
+ }
30756
+ if (step.action === "setData" && (!step.patch || Object.keys(step.patch).length === 0)) {
30757
+ issues.push({ path: `${at}.patch`, code: "semantic", message: "setData \u5FC5\u987B\u63D0\u4F9B patch(\u5199\u5165\u9875\u9762 data \u7684\u8865\u4E01)" });
30758
+ }
30569
30759
  if (step.action === "navigate" && !step.url) {
30570
30760
  issues.push({ path: `${at}.url`, code: "semantic", message: "navigate \u5FC5\u987B\u63D0\u4F9B url" });
30571
30761
  }
30572
30762
  if (step.action === "request" && !step.url) {
30573
30763
  issues.push({ path: `${at}.url`, code: "semantic", message: "request \u5FC5\u987B\u63D0\u4F9B url(\u76F8\u5BF9\u8DEF\u5F84\u6309 api.baseUrl \u89E3\u6790)" });
30574
30764
  }
30575
- if (VALUE_REQUIRED.has(step.action) && !step.value) {
30765
+ if (VALUE_REQUIRED.has(step.action) && (step.value === void 0 || step.value === "")) {
30576
30766
  issues.push({ path: `${at}.value`, code: "semantic", message: `action "${step.action}" \u5FC5\u987B\u63D0\u4F9B value` });
30577
30767
  }
30578
30768
  if (step.action === "extract" && step.target === "api" && !step.value) {
30579
30769
  issues.push({ path: `${at}.value`, code: "semantic", message: "api \u7AEF extract \u5FC5\u987B\u63D0\u4F9B value(\u70B9\u53F7 JSON path,\u5982 data.orderId)" });
30580
30770
  }
30581
- if (step.action === "assert" && !step.expected) {
30771
+ if (step.action === "assert" && step.count === void 0 && !step.expected) {
30582
30772
  issues.push({ path: `${at}.expected`, code: "semantic", message: "assert \u5FC5\u987B\u63D0\u4F9B expected" });
30583
30773
  }
30584
30774
  if (step.action === "extract") {
@@ -30597,6 +30787,13 @@ function checkStep(step, at, declared, issues) {
30597
30787
  if (step.action === "wait" && !step.locator && step.timeout === void 0) {
30598
30788
  issues.push({ path: at, code: "semantic", message: "wait \u9700\u8981\u63D0\u4F9B locator(\u7B49\u5F85\u5143\u7D20)\u6216 timeout(\u6BEB\u79D2)" });
30599
30789
  }
30790
+ if (step.action === "callMethod" && !step.methodName) {
30791
+ issues.push({
30792
+ path: `${at}.methodName`,
30793
+ code: "semantic",
30794
+ message: "callMethod \u5FC5\u987B\u63D0\u4F9B methodName(\u9875\u9762\u6216\u7EC4\u4EF6\u7684\u65B9\u6CD5\u540D)"
30795
+ });
30796
+ }
30600
30797
  if (step.expectRequests) {
30601
30798
  if (step.target === "api" || !REQUEST_EXPECTATION_ACTIONS.has(step.action)) {
30602
30799
  issues.push({
@@ -30611,6 +30808,7 @@ function checkStep(step, at, declared, issues) {
30611
30808
  function checkLocator(step, at, issues) {
30612
30809
  const locator = step.locator;
30613
30810
  if (!locator) {
30811
+ if (step.action === "assert" && step.dataPath !== void 0) return;
30614
30812
  if (LOCATOR_REQUIRED.has(step.action)) {
30615
30813
  issues.push({
30616
30814
  path: `${at}.locator`,
@@ -30635,14 +30833,20 @@ function checkVariableRefs(step, at, declared, issues) {
30635
30833
  if (match[1]) refs.push([field, match[1]]);
30636
30834
  }
30637
30835
  };
30638
- collect("value", step.value);
30836
+ collect("value", step.value === void 0 ? void 0 : String(step.value));
30639
30837
  collect("expected", step.expected);
30640
30838
  collect("url", step.url);
30839
+ collect("methodName", step.methodName);
30641
30840
  collect("locator.text", step.locator?.text);
30642
30841
  collect("locator.css", step.locator?.css);
30643
30842
  if (typeof step.body === "string") collect("body", step.body);
30644
30843
  else if (step.body) collect("body", JSON.stringify(step.body));
30645
30844
  for (const headerValue of Object.values(step.headers ?? {})) collect("headers", headerValue);
30845
+ if (Array.isArray(step.args)) {
30846
+ step.args.forEach((arg, index) => {
30847
+ if (typeof arg === "string") collect(`args[${index}]`, arg);
30848
+ });
30849
+ }
30646
30850
  for (const [index, expectation] of (step.expectRequests ?? []).entries()) {
30647
30851
  collect(`expectRequests[${index}].urlContains`, expectation.urlContains);
30648
30852
  }
@@ -31464,13 +31668,13 @@ function buildSummaryData(runs) {
31464
31668
  });
31465
31669
  }
31466
31670
  const byCase = /* @__PURE__ */ new Map();
31467
- for (const exec2 of executions) {
31468
- let agg = byCase.get(exec2.identity);
31671
+ for (const exec3 of executions) {
31672
+ let agg = byCase.get(exec3.identity);
31469
31673
  if (!agg) {
31470
31674
  agg = {
31471
- caseId: exec2.caseId,
31472
- rowId: exec2.rowId,
31473
- caseName: exec2.caseName,
31675
+ caseId: exec3.caseId,
31676
+ rowId: exec3.rowId,
31677
+ caseName: exec3.caseName,
31474
31678
  platforms: [],
31475
31679
  runs: 0,
31476
31680
  passed: 0,
@@ -31479,18 +31683,18 @@ function buildSummaryData(runs) {
31479
31683
  lastStatus: "unknown",
31480
31684
  flaky: false
31481
31685
  };
31482
- byCase.set(exec2.identity, agg);
31686
+ byCase.set(exec3.identity, agg);
31483
31687
  }
31484
31688
  agg.runs++;
31485
- if (exec2.status === "passed") agg.passed++;
31486
- else if (exec2.status === "failed") agg.failed++;
31487
- for (const platform of exec2.platforms) {
31689
+ if (exec3.status === "passed") agg.passed++;
31690
+ else if (exec3.status === "failed") agg.failed++;
31691
+ for (const platform of exec3.platforms) {
31488
31692
  if (!agg.platforms.includes(platform)) agg.platforms.push(platform);
31489
31693
  }
31490
31694
  }
31491
31695
  for (const agg of byCase.values()) {
31492
31696
  const identity = agg.rowId && agg.rowId !== "default" ? `${agg.caseId}#${agg.rowId}` : agg.caseId;
31493
- const last = [...executions].reverse().find((exec2) => exec2.identity === identity);
31697
+ const last = [...executions].reverse().find((exec3) => exec3.identity === identity);
31494
31698
  agg.lastStatus = last?.status ?? "unknown";
31495
31699
  agg.lastCategory = last?.category;
31496
31700
  agg.passRate = agg.runs > 0 ? agg.passed / agg.runs : 0;
@@ -31501,13 +31705,13 @@ function buildSummaryData(runs) {
31501
31705
  const categories = FAILURE_CATEGORIES.map((item) => ({
31502
31706
  id: item.id,
31503
31707
  label: item.label,
31504
- count: executions.filter((exec2) => exec2.status === "failed" && exec2.category === item.id).length
31708
+ count: executions.filter((exec3) => exec3.status === "failed" && exec3.category === item.id).length
31505
31709
  })).filter((item) => item.count > 0);
31506
31710
  const durations = runs.map((item) => item.summary.durationMs).filter((value) => typeof value === "number");
31507
31711
  return {
31508
31712
  buckets: {
31509
- blocked: executions.filter((exec2) => exec2.status === "blocked").length,
31510
- inconclusive: executions.filter((exec2) => exec2.status === "inconclusive").length
31713
+ blocked: executions.filter((exec3) => exec3.status === "blocked").length,
31714
+ inconclusive: executions.filter((exec3) => exec3.status === "inconclusive").length
31511
31715
  },
31512
31716
  window: {
31513
31717
  runs: runs.length,
@@ -31517,7 +31721,7 @@ function buildSummaryData(runs) {
31517
31721
  kpis: {
31518
31722
  runs: runs.length,
31519
31723
  executions: executions.length,
31520
- overallRate: executions.length > 0 ? executions.filter((exec2) => exec2.status === "passed").length / executions.length : 0,
31724
+ overallRate: executions.length > 0 ? executions.filter((exec3) => exec3.status === "passed").length / executions.length : 0,
31521
31725
  avgDurationMs: durations.length > 0 ? durations.reduce((sum, value) => sum + value, 0) / durations.length : 0,
31522
31726
  flakyCount: flaky.length
31523
31727
  },
@@ -31721,7 +31925,7 @@ import { readFile as readFile7, readdir as readdir4 } from "node:fs/promises";
31721
31925
  import { isAbsolute as isAbsolute3, join as join17 } from "node:path";
31722
31926
 
31723
31927
  // ../../packages/execution-engine/src/artifacts/index.ts
31724
- import { mkdir as mkdir9 } from "node:fs/promises";
31928
+ import { mkdir as mkdir9, writeFile as writeFile9 } from "node:fs/promises";
31725
31929
  import { join as join13 } from "node:path";
31726
31930
 
31727
31931
  // ../../packages/evidence/src/log.ts
@@ -31812,6 +32016,12 @@ var ArtifactManager = class {
31812
32016
  await saveTrace(this.tracesDir, fileName, data);
31813
32017
  return `traces/${fileName}`;
31814
32018
  }
32019
+ /** 保存失败用例的运行时诊断(console 尾部/异常/页面栈/systemInfo),返回相对 run 目录的路径 */
32020
+ async saveDiagnostics(fileName, data) {
32021
+ await mkdir9(this.logsDir, { recursive: true });
32022
+ await writeFile9(join13(this.logsDir, fileName), JSON.stringify(data, null, 2), "utf8");
32023
+ return `logs/${fileName}`;
32024
+ }
31815
32025
  };
31816
32026
 
31817
32027
  // ../../packages/execution-engine/src/context/index.ts
@@ -31870,6 +32080,26 @@ var ExecutionContext = class {
31870
32080
  }
31871
32081
  this.variables.set("account", rawValue);
31872
32082
  }
32083
+ /**
32084
+ * 注入结构化对象(如 mockWxApi 的捕获结果):按点号路径展开为平铺字符串变量。
32085
+ * 例如 seedObject('showModalCall', { called: true, args: { content: 'x' } }) 后,
32086
+ * `${showModalCall.called}` 与 `${showModalCall.args.content}` 均可解析。
32087
+ */
32088
+ seedObject(name, value) {
32089
+ const flatten = (prefix, input2) => {
32090
+ if (input2 !== null && typeof input2 === "object" && !Array.isArray(input2)) {
32091
+ const entries = Object.entries(input2);
32092
+ if (entries.length === 0) {
32093
+ this.variables.set(prefix, "");
32094
+ return;
32095
+ }
32096
+ for (const [key, item] of entries) flatten(`${prefix}.${key}`, item);
32097
+ return;
32098
+ }
32099
+ this.variables.set(prefix, input2 === void 0 ? "" : String(input2));
32100
+ };
32101
+ flatten(name, value);
32102
+ }
31873
32103
  };
31874
32104
 
31875
32105
  // ../../packages/execution-engine/src/lifecycle/index.ts
@@ -31931,6 +32161,14 @@ function redactHeaders(headers) {
31931
32161
  return redacted;
31932
32162
  }
31933
32163
 
32164
+ // ../../packages/adapter-core/src/locator.ts
32165
+ function describeLocator(locator) {
32166
+ if (!locator) return "-";
32167
+ if (locator.text !== void 0) return `text="${locator.text}"`;
32168
+ if (locator.css !== void 0) return `css="${locator.css}"`;
32169
+ return "-";
32170
+ }
32171
+
31934
32172
  // ../../packages/adapter-core/src/session.ts
31935
32173
  var AdapterSession = class {
31936
32174
  constructor(target, factory) {
@@ -31987,6 +32225,8 @@ var AdapterResolver = class {
31987
32225
 
31988
32226
  // ../../packages/execution-engine/src/engine/step-executor.ts
31989
32227
  var SNIPPET_LIMIT = 400;
32228
+ var ASSERT_POLL_MS = 1e4;
32229
+ var ABSENT_SETTLE_MS = 2e3;
31990
32230
  async function executeStep(step, index, adapter, context, artifacts, options) {
31991
32231
  const startedAt = Date.now();
31992
32232
  const result = {
@@ -32020,15 +32260,33 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
32020
32260
  await adapter.click(locator);
32021
32261
  break;
32022
32262
  case "input":
32023
- await adapter.input(locator, context.resolve(step.value ?? ""));
32263
+ await adapter.input(locator, resolveValueText(step.value, context));
32024
32264
  break;
32025
32265
  case "select":
32026
- await adapter.select(locator, context.resolve(step.value ?? ""));
32266
+ await adapter.select(locator, resolveValueText(step.value, context));
32027
32267
  break;
32028
32268
  case "wait":
32029
32269
  await adapter.wait(locator, step.timeout);
32030
32270
  break;
32031
- case "assert":
32271
+ case "assert": {
32272
+ if (step.count !== void 0) {
32273
+ if (!adapter.countElements) {
32274
+ throw new TestPilotError("UNSUPPORTED_ACTION", `\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301 count \u65AD\u8A00`);
32275
+ }
32276
+ await assertElementCount(adapter, locator, step.count);
32277
+ break;
32278
+ }
32279
+ if (step.attribute !== void 0 || step.property !== void 0) {
32280
+ await assertElementAttribute(adapter, locator, step, context);
32281
+ break;
32282
+ }
32283
+ if (step.dataPath !== void 0) {
32284
+ if (!adapter.assertData) {
32285
+ throw new TestPilotError("UNSUPPORTED_ACTION", `\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301 dataPath \u65AD\u8A00`);
32286
+ }
32287
+ await adapter.assertData(context.resolve(step.dataPath), context.resolve(step.expected ?? ""));
32288
+ break;
32289
+ }
32032
32290
  if (step.target === "api") {
32033
32291
  if (!adapter.assertResponse) throw new TestPilotError("UNSUPPORTED_ACTION", "\u5F53\u524D adapter \u4E0D\u652F\u6301 api \u65AD\u8A00");
32034
32292
  await adapter.assertResponse(context.resolve(step.expected ?? ""));
@@ -32036,8 +32294,9 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
32036
32294
  await adapter.assert(locator, context.resolve(step.expected ?? ""));
32037
32295
  }
32038
32296
  break;
32297
+ }
32039
32298
  case "extract": {
32040
- const value = step.target === "api" ? await extractApiResponse(adapter, context.resolve(step.value ?? "")) : await adapter.extract(locator);
32299
+ const value = step.target === "api" ? await extractApiResponse(adapter, resolveValueText(step.value, context)) : await adapter.extract(locator);
32041
32300
  if (step.variable) {
32042
32301
  context.set(step.variable, value);
32043
32302
  result.extracted = { [step.variable]: value };
@@ -32075,6 +32334,104 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
32075
32334
  }
32076
32335
  break;
32077
32336
  }
32337
+ case "callMethod": {
32338
+ if (typeof adapter.callMethod !== "function") {
32339
+ throw new TestPilotError("UNSUPPORTED_ACTION", `\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301 callMethod`);
32340
+ }
32341
+ const methodName = context.resolve(step.methodName ?? "");
32342
+ if (!methodName) throw new TestPilotError("MISSING_METHOD_NAME", "callMethod \u5FC5\u987B\u63D0\u4F9B methodName");
32343
+ const callArgs = (step.args ?? []).map((arg) => resolveDeep(arg, context));
32344
+ await adapter.callMethod(locator, methodName, callArgs);
32345
+ break;
32346
+ }
32347
+ case "mockWxApi": {
32348
+ if (typeof adapter.mockWxApi !== "function") {
32349
+ throw new TestPilotError("UNSUPPORTED_ACTION", `\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301 mockWxApi`);
32350
+ }
32351
+ await adapter.mockWxApi({
32352
+ api: context.resolve(step.wxApi ?? ""),
32353
+ result: step.result ? resolveDeep(step.result, context) : void 0,
32354
+ capture: step.capture
32355
+ });
32356
+ break;
32357
+ }
32358
+ case "trigger": {
32359
+ if (typeof adapter.trigger !== "function") {
32360
+ throw new TestPilotError("UNSUPPORTED_ACTION", `\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301 trigger`);
32361
+ }
32362
+ if (!locator) throw new TestPilotError("MISSING_LOCATOR", "trigger \u5FC5\u987B\u63D0\u4F9B locator(\u8981\u89E6\u53D1\u4E8B\u4EF6\u7684\u7EC4\u4EF6)");
32363
+ await adapter.trigger(locator, context.resolve(step.eventType ?? ""), step.detail ? resolveDeep(step.detail, context) : void 0);
32364
+ break;
32365
+ }
32366
+ case "setData": {
32367
+ if (typeof adapter.setData !== "function") {
32368
+ throw new TestPilotError("UNSUPPORTED_ACTION", `\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301 setData`);
32369
+ }
32370
+ await adapter.setData(resolveDeep(step.patch ?? {}, context), locator);
32371
+ break;
32372
+ }
32373
+ case "longpress": {
32374
+ if (typeof adapter.longpress !== "function") {
32375
+ throw new TestPilotError("UNSUPPORTED_ACTION", `\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301 longpress`);
32376
+ }
32377
+ await adapter.longpress(locator);
32378
+ break;
32379
+ }
32380
+ case "callWxApi": {
32381
+ if (typeof adapter.callWxApi !== "function") {
32382
+ throw new TestPilotError("UNSUPPORTED_ACTION", `\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301 callWxApi`);
32383
+ }
32384
+ const wxApi = context.resolve(step.wxApi ?? "");
32385
+ if (!wxApi) throw new TestPilotError("MISSING_WX_API", "callWxApi \u5FC5\u987B\u63D0\u4F9B wxApi");
32386
+ const callArgs = (step.args ?? []).map((arg) => resolveDeep(arg, context));
32387
+ const returned = await adapter.callWxApi(wxApi, callArgs);
32388
+ if (step.variable) {
32389
+ const captured = typeof returned === "string" ? returned : JSON.stringify(returned) ?? "";
32390
+ context.set(step.variable, captured);
32391
+ result.extracted = { [step.variable]: captured };
32392
+ }
32393
+ break;
32394
+ }
32395
+ case "touch": {
32396
+ if (typeof adapter.touch !== "function") {
32397
+ throw new TestPilotError("UNSUPPORTED_ACTION", `\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301 touch`);
32398
+ }
32399
+ if (!locator) throw new TestPilotError("MISSING_LOCATOR", "touch \u5FC5\u987B\u63D0\u4F9B locator");
32400
+ const touches = (step.touches ?? []).map((point) => resolveDeep(point, context));
32401
+ const changed = (step.changedTouches ?? step.touches ?? []).map(
32402
+ (point) => resolveDeep(point, context)
32403
+ );
32404
+ await adapter.touch(locator, step.phase ?? "", touches, changed);
32405
+ break;
32406
+ }
32407
+ case "scroll": {
32408
+ if (typeof adapter.scroll !== "function") {
32409
+ throw new TestPilotError("UNSUPPORTED_ACTION", `\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301 scroll`);
32410
+ }
32411
+ await adapter.scroll(locator, step.scrollTop ?? 0);
32412
+ break;
32413
+ }
32414
+ case "swipeTo": {
32415
+ if (typeof adapter.swipeTo !== "function") {
32416
+ throw new TestPilotError("UNSUPPORTED_ACTION", `\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301 swipeTo`);
32417
+ }
32418
+ await adapter.swipeTo(locator, step.index ?? 0);
32419
+ break;
32420
+ }
32421
+ case "slideTo": {
32422
+ if (typeof adapter.slideTo !== "function") {
32423
+ throw new TestPilotError("UNSUPPORTED_ACTION", `\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301 slideTo`);
32424
+ }
32425
+ await adapter.slideTo(locator, Number(resolveValueText(step.value, context)));
32426
+ break;
32427
+ }
32428
+ case "moveTo": {
32429
+ if (typeof adapter.moveTo !== "function") {
32430
+ throw new TestPilotError("UNSUPPORTED_ACTION", `\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301 moveTo`);
32431
+ }
32432
+ await adapter.moveTo(locator, step.x ?? 0, step.y ?? 0);
32433
+ break;
32434
+ }
32078
32435
  default: {
32079
32436
  const exhaustive = step.action;
32080
32437
  throw new TestPilotError("UNSUPPORTED_ACTION", `\u4E0D\u652F\u6301\u7684 action:${String(exhaustive)}`);
@@ -32121,6 +32478,42 @@ async function extractApiResponse(adapter, path) {
32121
32478
  if (!path) throw new TestPilotError("UNSUPPORTED_ACTION", "api \u7AEF extract \u9700\u8981\u63D0\u4F9B value(\u70B9\u53F7 JSON path)");
32122
32479
  return adapter.extractResponse(path);
32123
32480
  }
32481
+ function resolveValueText(value, context) {
32482
+ return value === void 0 ? "" : context.resolve(String(value));
32483
+ }
32484
+ async function assertElementCount(adapter, locator, expected) {
32485
+ const windowMs = expected > 0 ? ASSERT_POLL_MS : ABSENT_SETTLE_MS;
32486
+ const startedAt = Date.now();
32487
+ for (; ; ) {
32488
+ const actual = await adapter.countElements(locator);
32489
+ if (actual === expected) return;
32490
+ if (Date.now() - startedAt > windowMs) {
32491
+ throw new TestPilotError(
32492
+ "ASSERTION_FAILED",
32493
+ `\u5143\u7D20\u6570\u91CF\u65AD\u8A00\u5931\u8D25(\u7B49\u5F85 ${windowMs}ms):${describeLocator(locator)} \u5B9E\u9645 ${actual},\u671F\u671B ${expected}`
32494
+ );
32495
+ }
32496
+ await delay(300);
32497
+ }
32498
+ }
32499
+ async function assertElementAttribute(adapter, locator, step, context) {
32500
+ const useProperty = step.property !== void 0;
32501
+ const name = (useProperty ? step.property : step.attribute) ?? "";
32502
+ const wanted = context.resolve(step.expected ?? "").trim();
32503
+ const startedAt = Date.now();
32504
+ for (; ; ) {
32505
+ const actual = useProperty ? await adapter.getElementProperty(locator, name) : await adapter.getElementAttribute(locator, name);
32506
+ const actualText = actual === void 0 || actual === null ? "<absent>" : String(actual);
32507
+ if (actualText.trim() === wanted) return;
32508
+ if (Date.now() - startedAt > ASSERT_POLL_MS) {
32509
+ throw new TestPilotError(
32510
+ "ASSERTION_FAILED",
32511
+ `\u5C5E\u6027\u65AD\u8A00\u5931\u8D25(\u7B49\u5F85 ${ASSERT_POLL_MS}ms):${describeLocator(locator)} \u7684 ${useProperty ? "property" : "attribute"} "${name}" \u671F\u671B "${wanted}",\u5B9E\u9645\u4E3A "${actualText.slice(0, 200)}"`
32512
+ );
32513
+ }
32514
+ await delay(300);
32515
+ }
32516
+ }
32124
32517
  function resolveHeaders(headers, context) {
32125
32518
  if (!headers) return void 0;
32126
32519
  const resolved = {};
@@ -32263,6 +32656,12 @@ var ExecutionEngine = class {
32263
32656
  result = await executeStep(step, index, adapter, context, artifacts, {
32264
32657
  screenshotName: `${executionId}-${phase}-${String(index).padStart(2, "0")}`
32265
32658
  });
32659
+ if (step.capture !== void 0 && typeof adapter.readWxApiCaptures === "function") {
32660
+ const captures = adapter.readWxApiCaptures();
32661
+ if (captures && captures.calls.length > 0) {
32662
+ context.seedObject(step.capture, captures.calls[0]?.options ?? null);
32663
+ }
32664
+ }
32266
32665
  } catch (err) {
32267
32666
  result = {
32268
32667
  index,
@@ -32384,6 +32783,27 @@ var ExecutionEngine = class {
32384
32783
  );
32385
32784
  }
32386
32785
  }
32786
+ for (const [, adapter] of evidenceAdapters) {
32787
+ await adapter.restoreWxMock?.().catch(() => void 0);
32788
+ }
32789
+ if (status !== "passed") {
32790
+ for (const [target, adapter] of evidenceAdapters) {
32791
+ if (typeof adapter.readDiagnostics !== "function") continue;
32792
+ try {
32793
+ const diagnostics = await adapter.readDiagnostics();
32794
+ const hasContent = diagnostics.consoleLogs.length > 0 || diagnostics.exceptions.length > 0 || (diagnostics.pageStack?.length ?? 0) > 0 || diagnostics.systemInfo !== void 0;
32795
+ if (!hasContent) continue;
32796
+ caseResult.diagnostics = await artifacts.saveDiagnostics(
32797
+ `${executionId}-${target}.json`,
32798
+ diagnostics
32799
+ );
32800
+ } catch (err) {
32801
+ await artifacts.logger.info(
32802
+ `diagnostics dump failed for ${target}: ${err instanceof Error ? err.message : String(err)}`
32803
+ );
32804
+ }
32805
+ }
32806
+ }
32387
32807
  await bus.emit({
32388
32808
  type: "case-finished",
32389
32809
  runId,
@@ -32445,7 +32865,7 @@ function buildRunSummary(runId, startedAt, cases, options = {}) {
32445
32865
  }
32446
32866
 
32447
32867
  // ../../packages/execution-engine/src/engine/test-runner.ts
32448
- import { writeFile as writeFile9 } from "node:fs/promises";
32868
+ import { writeFile as writeFile10 } from "node:fs/promises";
32449
32869
  import { join as join15, resolve as resolve6 } from "node:path";
32450
32870
  var TestRunner = class {
32451
32871
  resolver;
@@ -32509,7 +32929,7 @@ var TestRunner = class {
32509
32929
  ts: (/* @__PURE__ */ new Date()).toISOString()
32510
32930
  });
32511
32931
  await artifacts.logger.info(`run ${runId} finished: ${summary.status}`);
32512
- await writeFile9(join15(runDir, RESULT_FILE), JSON.stringify(summary, null, 2), "utf8");
32932
+ await writeFile10(join15(runDir, RESULT_FILE), JSON.stringify(summary, null, 2), "utf8");
32513
32933
  return summary;
32514
32934
  }
32515
32935
  };
@@ -32547,6 +32967,11 @@ import { mkdtemp, readFile as readFile6, rm } from "node:fs/promises";
32547
32967
  import { tmpdir } from "node:os";
32548
32968
  import { join as join16 } from "node:path";
32549
32969
  import { errors } from "playwright";
32970
+ var TEXT_WAIT_MS = 1e4;
32971
+ var LONG_PRESS_MS = 600;
32972
+ function delay2(ms) {
32973
+ return new Promise((resolve7) => setTimeout(resolve7, ms));
32974
+ }
32550
32975
  var PlaywrightAdapter = class {
32551
32976
  constructor(bundle, options = {}) {
32552
32977
  this.bundle = bundle;
@@ -32619,6 +33044,21 @@ var PlaywrightAdapter = class {
32619
33044
  const target = await this.toLocator(locator);
32620
33045
  await this.coded(() => target.click());
32621
33046
  }
33047
+ /** 长按元素:web 端没有原生 longpress,以按住 LONG_PRESS_MS 的 click 近似 */
33048
+ async longpress(locator) {
33049
+ const target = await this.toLocator(locator);
33050
+ await this.coded(() => target.click({ delay: LONG_PRESS_MS }));
33051
+ }
33052
+ /** 统计匹配元素数量(单次读取,不等待;重试语义由引擎负责) */
33053
+ async countElements(locator) {
33054
+ const target = await this.toLocator(locator);
33055
+ return target.count();
33056
+ }
33057
+ /** 读取元素 attribute(不存在时为 null) */
33058
+ async getElementAttribute(locator, name) {
33059
+ const target = await this.toLocator(locator);
33060
+ return this.coded(() => target.getAttribute(name));
33061
+ }
32622
33062
  async input(locator, value) {
32623
33063
  const target = await this.toLocator(locator);
32624
33064
  await this.coded(() => target.fill(value));
@@ -32637,16 +33077,29 @@ var PlaywrightAdapter = class {
32637
33077
  }
32638
33078
  async assert(locator, expected) {
32639
33079
  const target = await this.toLocator(locator);
32640
- await this.coded(() => target.waitFor({ state: "visible" }));
32641
- const actual = (await this.coded(() => target.innerText())).trim();
32642
33080
  const wanted = expected.trim();
32643
- if (!actual.includes(wanted)) {
32644
- throw new TestPilotError("ASSERTION_FAILED", `\u65AD\u8A00\u5931\u8D25:\u671F\u671B\u5305\u542B "${wanted}",\u5B9E\u9645\u4E3A "${actual.slice(0, 200)}"`);
33081
+ const startedAt = Date.now();
33082
+ for (; ; ) {
33083
+ await this.coded(() => target.waitFor({ state: "visible" }));
33084
+ const actual = (await this.coded(() => target.innerText())).trim();
33085
+ if (actual.includes(wanted)) return;
33086
+ if (Date.now() - startedAt > TEXT_WAIT_MS) {
33087
+ throw new TestPilotError(
33088
+ "ASSERTION_FAILED",
33089
+ `\u65AD\u8A00\u5931\u8D25(\u7B49\u5F85 ${TEXT_WAIT_MS}ms):\u671F\u671B\u5305\u542B "${wanted}",\u5B9E\u9645\u4E3A "${actual.slice(0, 200)}"`
33090
+ );
33091
+ }
33092
+ await delay2(300);
32645
33093
  }
32646
33094
  }
32647
33095
  async extract(locator) {
32648
33096
  const target = await this.toLocator(locator);
32649
- return this.coded(() => target.innerText()).then((text) => text.trim());
33097
+ const startedAt = Date.now();
33098
+ for (; ; ) {
33099
+ const text = (await this.coded(() => target.innerText())).trim();
33100
+ if (text.length > 0 || Date.now() - startedAt > TEXT_WAIT_MS) return text;
33101
+ await delay2(300);
33102
+ }
32650
33103
  }
32651
33104
  async screenshot() {
32652
33105
  return (await this.ensurePage()).screenshot();
@@ -32711,7 +33164,11 @@ import net from "node:net";
32711
33164
  var AUTO_PORT_FALLBACK_WAIT_MS = 9e4;
32712
33165
  var NAVIGATE_RETRY_WINDOW_MS = 45e3;
32713
33166
  var ELEMENT_WAIT_MS = 1e4;
33167
+ var TEXT_WAIT_MS2 = 1e4;
33168
+ var PAGE_READY_WAIT_MS = 3e4;
32714
33169
  var CONNECTION_LOST = /connection closed|not connected/i;
33170
+ var DIAGNOSTICS_BUFFER_MAX = 200;
33171
+ var CONSOLE_TEXT_LIMIT = 500;
32715
33172
  var automatorLauncher = {
32716
33173
  launch: async (options) => {
32717
33174
  try {
@@ -32724,9 +33181,23 @@ var automatorLauncher = {
32724
33181
  }
32725
33182
  }
32726
33183
  };
32727
- function delay2(ms) {
33184
+ function delay3(ms) {
32728
33185
  return new Promise((resolve7) => setTimeout(resolve7, ms));
32729
33186
  }
33187
+ function serializeConsoleArg(value) {
33188
+ if (typeof value === "string") return value;
33189
+ try {
33190
+ return JSON.stringify(value) ?? String(value);
33191
+ } catch {
33192
+ return String(value);
33193
+ }
33194
+ }
33195
+ function stringifyDataValue(value) {
33196
+ if (value === void 0 || value === null) return "";
33197
+ if (typeof value === "string") return value;
33198
+ if (typeof value === "object") return JSON.stringify(value);
33199
+ return String(value);
33200
+ }
32730
33201
  async function connectWithRetry(wsEndpoint, timeoutMs) {
32731
33202
  const startedAt = Date.now();
32732
33203
  let lastError;
@@ -32735,7 +33206,7 @@ async function connectWithRetry(wsEndpoint, timeoutMs) {
32735
33206
  return await automator.connect({ wsEndpoint });
32736
33207
  } catch (err) {
32737
33208
  lastError = err;
32738
- await delay2(1e3);
33209
+ await delay3(1e3);
32739
33210
  }
32740
33211
  }
32741
33212
  throw lastError instanceof Error ? lastError : new Error(String(lastError));
@@ -32787,6 +33258,10 @@ var MiniAppAdapter = class {
32787
33258
  target = "miniapp";
32788
33259
  program;
32789
33260
  requestCapture;
33261
+ /** console / exception 环形缓冲(失败取证的数据源);重连后继续累积 */
33262
+ consoleLogs = [];
33263
+ exceptions = [];
33264
+ diagnosticsAttachedTo;
32790
33265
  /** 拉起开发者工具并建立自动化连接;需幂等 */
32791
33266
  async launch() {
32792
33267
  if (this.program) return;
@@ -32795,6 +33270,7 @@ var MiniAppAdapter = class {
32795
33270
  };
32796
33271
  if (this.config.cliPath) launchOptions.cliPath = this.config.cliPath;
32797
33272
  this.program = await this.launcher.launch(launchOptions);
33273
+ this.attachDiagnostics(this.program);
32798
33274
  }
32799
33275
  async navigate(url2) {
32800
33276
  const startedAt = Date.now();
@@ -32804,14 +33280,31 @@ var MiniAppAdapter = class {
32804
33280
  await this.runWithReconnect(async () => {
32805
33281
  await (await this.getProgram()).reLaunch(url2);
32806
33282
  });
33283
+ await this.waitForPageReady();
32807
33284
  return;
32808
33285
  } catch (err) {
32809
33286
  lastError = err;
32810
- await delay2(3e3);
33287
+ await delay3(3e3);
32811
33288
  }
32812
33289
  }
32813
33290
  throw lastError instanceof Error ? lastError : new Error(String(lastError));
32814
33291
  }
33292
+ /** 等待页面就绪:模拟器编译期间 currentPage / data 会失败或挂起,轮询至可响应 */
33293
+ async waitForPageReady(timeoutMs = PAGE_READY_WAIT_MS) {
33294
+ const startedAt = Date.now();
33295
+ for (; ; ) {
33296
+ const page = await (await this.getProgram()).currentPage();
33297
+ const data = await page.data().catch(() => void 0);
33298
+ if (data !== void 0 && data !== null) return;
33299
+ if (Date.now() - startedAt > timeoutMs) {
33300
+ throw new TestPilotError(
33301
+ "PAGE_READY_TIMEOUT",
33302
+ `\u7B49\u5F85\u9875\u9762\u5C31\u7EEA\u8D85\u65F6(${timeoutMs}ms):\u6A21\u62DF\u5668\u53EF\u80FD\u4ECD\u5728\u7F16\u8BD1,\u6216\u9879\u76EE\u672A\u88AB\u4FE1\u4EFB`
33303
+ );
33304
+ }
33305
+ await delay3(300);
33306
+ }
33307
+ }
32815
33308
  async click(locator) {
32816
33309
  await this.runWithReconnect(async () => (await this.element(locator)).tap());
32817
33310
  }
@@ -32834,19 +33327,240 @@ var MiniAppAdapter = class {
32834
33327
  async assert(locator, expected) {
32835
33328
  await this.runWithReconnect(async () => {
32836
33329
  const element = await this.element(locator);
32837
- const actual = (await element.text()).trim();
32838
33330
  const wanted = expected.trim();
32839
- if (!actual.includes(wanted)) {
32840
- throw new TestPilotError("ASSERTION_FAILED", `\u65AD\u8A00\u5931\u8D25:\u671F\u671B\u5305\u542B "${wanted}",\u5B9E\u9645\u4E3A "${actual.slice(0, 200)}"`);
33331
+ const startedAt = Date.now();
33332
+ for (; ; ) {
33333
+ const actual = (await element.text()).trim();
33334
+ if (actual.includes(wanted)) return;
33335
+ if (Date.now() - startedAt > TEXT_WAIT_MS2) {
33336
+ throw new TestPilotError(
33337
+ "ASSERTION_FAILED",
33338
+ `\u65AD\u8A00\u5931\u8D25(\u7B49\u5F85 ${TEXT_WAIT_MS2}ms):\u671F\u671B\u5305\u542B "${wanted}",\u5B9E\u9645\u4E3A "${actual.slice(0, 200)}"`
33339
+ );
33340
+ }
33341
+ await delay3(300);
32841
33342
  }
32842
33343
  });
32843
33344
  }
32844
33345
  async extract(locator) {
32845
33346
  return this.runWithReconnect(async () => {
32846
33347
  const element = await this.element(locator);
32847
- return (await element.text()).trim();
33348
+ const startedAt = Date.now();
33349
+ for (; ; ) {
33350
+ const text = (await element.text()).trim();
33351
+ if (text.length > 0 || Date.now() - startedAt > TEXT_WAIT_MS2) return text;
33352
+ await delay3(300);
33353
+ }
33354
+ });
33355
+ }
33356
+ /** 调用 Page / 组件方法;不传 locator 时为页面级调用(Page 实例方法) */
33357
+ async callMethod(locator, methodName, args = []) {
33358
+ return this.runWithReconnect(async () => {
33359
+ if (locator) {
33360
+ const element = await this.element(locator);
33361
+ return element.callMethod(methodName, ...args);
33362
+ }
33363
+ const page = await (await this.getProgram()).currentPage();
33364
+ return page.callMethod(methodName, ...args);
33365
+ });
33366
+ }
33367
+ // ---- miniapp 深度测试(V0.2)----
33368
+ wxMock;
33369
+ /** mock 任意 wx.* API:拦截调用并返回 mock 值;capture 记录真实调用参数(最近调用在前) */
33370
+ async mockWxApi(options) {
33371
+ if (this.wxMock?.api === options.api) await this.restoreWxMock();
33372
+ const program2 = await this.getProgram();
33373
+ const captured = [];
33374
+ this.wxMock = { api: options.api, captured };
33375
+ const callbackName = options.capture ? `__tpWxCapture_${options.capture}` : void 0;
33376
+ if (callbackName) {
33377
+ await program2.exposeFunction(callbackName, (payload) => {
33378
+ if (payload !== null && typeof payload === "object") {
33379
+ captured.push(payload);
33380
+ }
33381
+ });
33382
+ }
33383
+ await program2.mockWxMethod(
33384
+ options.api,
33385
+ function(invokeOptions, cbName) {
33386
+ const callbacks = globalThis;
33387
+ callbacks[cbName]?.({ options: invokeOptions });
33388
+ return options.result ?? this.origin(invokeOptions);
33389
+ },
33390
+ callbackName
33391
+ );
33392
+ }
33393
+ /** 读取 mock 期间捕获的调用参数(引擎在带 capture 的 mockWxApi step 后调用;最近调用在前) */
33394
+ readWxApiCaptures() {
33395
+ if (!this.wxMock) return void 0;
33396
+ return { name: this.wxMock.api, calls: [...this.wxMock.captured].reverse() };
33397
+ }
33398
+ /** @deprecated 引擎已改用 readWxApiCaptures();保留按 API 名读取的旧入口 */
33399
+ readCapturedWxApiCalls(api) {
33400
+ return this.wxMock?.api === api ? [...this.wxMock.captured].reverse() : [];
33401
+ }
33402
+ /** 恢复 mock 的 wx API(用例结束由引擎调用) */
33403
+ async restoreWxMock() {
33404
+ const mock = this.wxMock;
33405
+ if (!mock) return;
33406
+ this.wxMock = void 0;
33407
+ if (!this.program) return;
33408
+ await this.program.restoreWxMethod(mock.api).catch(() => void 0);
33409
+ }
33410
+ /** 触发组件事件(trigger;detail 作为 e.detail 注入,空 detail 即"用户拒绝/取消"类分支) */
33411
+ async trigger(locator, eventType, detail) {
33412
+ await this.runWithReconnect(async () => {
33413
+ const element = await this.element(locator);
33414
+ await element.trigger(eventType, detail);
33415
+ });
33416
+ }
33417
+ /** 读取页面 data(点号路径,缺省为整个 data 对象) */
33418
+ async getData(dataPath) {
33419
+ return this.runWithReconnect(async () => {
33420
+ const page = await (await this.getProgram()).currentPage();
33421
+ return page.data(dataPath);
33422
+ });
33423
+ }
33424
+ /** 写入 data 补丁(setData 驱动 onShow/onPullDownRefresh 等真实链路);locator 提供时为组件级 */
33425
+ async setData(patch, locator) {
33426
+ await this.runWithReconnect(async () => {
33427
+ if (locator) {
33428
+ await (await this.element(locator)).setData(patch);
33429
+ return;
33430
+ }
33431
+ const page = await (await this.getProgram()).currentPage();
33432
+ await page.setData(patch);
32848
33433
  });
32849
33434
  }
33435
+ /** 断言页面 data 路径的值包含 expected(数据绑定未完成时轮询等待) */
33436
+ async assertData(dataPath, expected) {
33437
+ await this.runWithReconnect(async () => {
33438
+ const page = await (await this.getProgram()).currentPage();
33439
+ const wanted = expected.trim();
33440
+ const startedAt = Date.now();
33441
+ for (; ; ) {
33442
+ const actual = stringifyDataValue(await page.data(dataPath));
33443
+ if (actual.includes(wanted)) return;
33444
+ if (Date.now() - startedAt > TEXT_WAIT_MS2) {
33445
+ throw new TestPilotError(
33446
+ "ASSERTION_FAILED",
33447
+ `\u9875\u9762 data \u65AD\u8A00\u5931\u8D25(\u7B49\u5F85 ${TEXT_WAIT_MS2}ms):data.${dataPath} \u671F\u671B\u5305\u542B "${wanted}",\u5B9E\u9645\u4E3A "${actual.slice(0, 200)}"`
33448
+ );
33449
+ }
33450
+ await delay3(300);
33451
+ }
33452
+ });
33453
+ }
33454
+ // ---- miniapp 交互扩展(对照官方 automator Element / MiniProgram API)----
33455
+ /** 长按元素 */
33456
+ async longpress(locator) {
33457
+ await this.runWithReconnect(async () => (await this.element(locator)).longpress());
33458
+ }
33459
+ /** 主动调用 wx.* API(如 setStorageSync / clearStorageSync 造前置状态) */
33460
+ async callWxApi(api, args = []) {
33461
+ return this.runWithReconnect(async () => {
33462
+ const program2 = await this.getProgram();
33463
+ if (typeof program2.callWxMethod !== "function") {
33464
+ throw new TestPilotError(
33465
+ "UNSUPPORTED_ACTION",
33466
+ "\u5F53\u524D miniprogram-automator \u4E0D\u652F\u6301 callWxMethod,\u8BF7\u5347\u7EA7\u5FAE\u4FE1\u5F00\u53D1\u8005\u5DE5\u5177\u540E\u91CD\u8BD5"
33467
+ );
33468
+ }
33469
+ return program2.callWxMethod(api, ...args);
33470
+ });
33471
+ }
33472
+ /** 派发触摸事件(touchstart / touchmove / touchend) */
33473
+ async touch(locator, phase, touches, changedTouches) {
33474
+ await this.runWithReconnect(async () => {
33475
+ const element = await this.element(locator);
33476
+ const options = { touches: [...touches], changedTouches: [...changedTouches] };
33477
+ if (phase === "touchstart") await element.touchstart(options);
33478
+ else if (phase === "touchmove") await element.touchmove(options);
33479
+ else if (phase === "touchend") await element.touchend(options);
33480
+ else throw new TestPilotError("UNSUPPORTED_ACTION", `\u672A\u77E5\u89E6\u6478\u9636\u6BB5:${phase}`);
33481
+ });
33482
+ }
33483
+ /** 滚动:locator 提供时滚动 scroll-view 元素,缺省滚动页面 */
33484
+ async scroll(locator, scrollTop) {
33485
+ await this.runWithReconnect(async () => {
33486
+ if (locator) {
33487
+ await (await this.element(locator)).scrollTo(scrollTop);
33488
+ return;
33489
+ }
33490
+ const program2 = await this.getProgram();
33491
+ if (typeof program2.pageScrollTo !== "function") {
33492
+ throw new TestPilotError(
33493
+ "UNSUPPORTED_ACTION",
33494
+ "\u5F53\u524D miniprogram-automator \u4E0D\u652F\u6301 pageScrollTo,\u8BF7\u5347\u7EA7\u5FAE\u4FE1\u5F00\u53D1\u8005\u5DE5\u5177\u540E\u91CD\u8BD5"
33495
+ );
33496
+ }
33497
+ await program2.pageScrollTo(scrollTop);
33498
+ });
33499
+ }
33500
+ /** swiper 切换到指定页 */
33501
+ async swipeTo(locator, index) {
33502
+ await this.runWithReconnect(async () => (await this.element(locator)).swipeTo(index));
33503
+ }
33504
+ /** slider 滑到指定值 */
33505
+ async slideTo(locator, value) {
33506
+ await this.runWithReconnect(async () => (await this.element(locator)).slideTo(value));
33507
+ }
33508
+ /** movable-view 移动到指定坐标 */
33509
+ async moveTo(locator, x, y) {
33510
+ await this.runWithReconnect(async () => (await this.element(locator)).moveTo(x, y));
33511
+ }
33512
+ /** 统计匹配元素数量(单次读取;存在性/计数断言的重试语义由引擎负责) */
33513
+ async countElements(locator) {
33514
+ return this.runWithReconnect(async () => {
33515
+ const page = await (await this.getProgram()).currentPage();
33516
+ const elements = await page.$$(this.cssSelector(locator));
33517
+ return elements.length;
33518
+ });
33519
+ }
33520
+ /** 读取元素 attribute(不存在时为 undefined/null) */
33521
+ async getElementAttribute(locator, name) {
33522
+ return this.runWithReconnect(async () => (await this.element(locator)).attribute(name));
33523
+ }
33524
+ /** 读取元素 property(miniapp 自定义组件实例属性) */
33525
+ async getElementProperty(locator, name) {
33526
+ return this.runWithReconnect(async () => (await this.element(locator)).property(name));
33527
+ }
33528
+ /** 导出运行时诊断:console 尾部 / 异常 / 页面栈 / systemInfo(失败取证由引擎落盘) */
33529
+ async readDiagnostics() {
33530
+ const program2 = this.program;
33531
+ const pageStack = await program2?.pageStack?.().catch(() => void 0);
33532
+ const systemInfo = await program2?.systemInfo?.().catch(() => void 0);
33533
+ return {
33534
+ consoleLogs: [...this.consoleLogs],
33535
+ exceptions: [...this.exceptions],
33536
+ pageStack,
33537
+ systemInfo
33538
+ };
33539
+ }
33540
+ /** 订阅小程序 console / exception 事件(每个 program 实例只挂一次) */
33541
+ attachDiagnostics(program2) {
33542
+ if (this.diagnosticsAttachedTo === program2 || typeof program2.on !== "function") return;
33543
+ this.diagnosticsAttachedTo = program2;
33544
+ program2.on("console", (payload) => {
33545
+ const record2 = payload ?? {};
33546
+ const args = Array.isArray(record2.args) ? record2.args : [];
33547
+ this.pushConsoleLog({
33548
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
33549
+ type: typeof record2.type === "string" ? record2.type : "log",
33550
+ text: args.map(serializeConsoleArg).join(" ").slice(0, CONSOLE_TEXT_LIMIT)
33551
+ });
33552
+ });
33553
+ program2.on("exception", (payload) => {
33554
+ const record2 = payload ?? {};
33555
+ const message = typeof record2.message === "string" ? record2.message : String(payload);
33556
+ this.exceptions.push(message.slice(0, CONSOLE_TEXT_LIMIT));
33557
+ if (this.exceptions.length > DIAGNOSTICS_BUFFER_MAX) this.exceptions.shift();
33558
+ });
33559
+ }
33560
+ pushConsoleLog(entry) {
33561
+ this.consoleLogs.push(entry);
33562
+ if (this.consoleLogs.length > DIAGNOSTICS_BUFFER_MAX) this.consoleLogs.shift();
33563
+ }
32850
33564
  async screenshot() {
32851
33565
  return this.runWithReconnect(async () => {
32852
33566
  const base643 = await (await this.getProgram()).screenshot();
@@ -32937,7 +33651,7 @@ var MiniAppAdapter = class {
32937
33651
  if (Date.now() - startedAt > ELEMENT_WAIT_MS) {
32938
33652
  throw new TestPilotError("ELEMENT_NOT_FOUND", `\u7B49\u5F85\u5143\u7D20\u8D85\u65F6(${ELEMENT_WAIT_MS}ms):${selector}`);
32939
33653
  }
32940
- await delay2(500);
33654
+ await delay3(500);
32941
33655
  }
32942
33656
  }
32943
33657
  };
@@ -33360,7 +34074,7 @@ var TestPilotClient = class {
33360
34074
  };
33361
34075
 
33362
34076
  // src/commands/init/adapter-detector.ts
33363
- import { existsSync as existsSync5, readFileSync } from "node:fs";
34077
+ import { existsSync as existsSync5, readFileSync as readFileSync2 } from "node:fs";
33364
34078
  import { join as join20 } from "node:path";
33365
34079
  async function detectAdapters(root, projectType) {
33366
34080
  const required2 = projectType === "hybrid" ? ["playwright", "wechatide"] : projectType === "wechat-miniapp" ? ["wechatide"] : projectType === "web" ? ["playwright"] : [];
@@ -33387,7 +34101,7 @@ function readPackageJsonDeps(root) {
33387
34101
  const file2 = join20(root, "package.json");
33388
34102
  if (!existsSync5(file2)) return void 0;
33389
34103
  try {
33390
- const pkg = JSON.parse(readFileSync(file2, "utf8"));
34104
+ const pkg = JSON.parse(readFileSync2(file2, "utf8"));
33391
34105
  return { ...pkg.dependencies, ...pkg.devDependencies };
33392
34106
  } catch {
33393
34107
  return void 0;
@@ -33417,7 +34131,7 @@ async function collectEnvVarRefs(root) {
33417
34131
  }
33418
34132
 
33419
34133
  // src/commands/init/project-detector.ts
33420
- import { existsSync as existsSync6, readFileSync as readFileSync2 } from "node:fs";
34134
+ import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "node:fs";
33421
34135
  import { basename, join as join22 } from "node:path";
33422
34136
 
33423
34137
  // src/lib/git.ts
@@ -33458,7 +34172,7 @@ function readPackageJson(root) {
33458
34172
  const file2 = join22(root, "package.json");
33459
34173
  if (!existsSync6(file2)) return void 0;
33460
34174
  try {
33461
- return JSON.parse(readFileSync2(file2, "utf8"));
34175
+ return JSON.parse(readFileSync3(file2, "utf8"));
33462
34176
  } catch {
33463
34177
  return void 0;
33464
34178
  }
@@ -33473,10 +34187,31 @@ function detectProjectType(root) {
33473
34187
  if (isWeb) return "web";
33474
34188
  return "unknown";
33475
34189
  }
34190
+ function detectLanguageFromSources(root) {
34191
+ const SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "miniprogram_npm", "dist", ".git"]);
34192
+ const found = { ts: false, js: false };
34193
+ const walk = (dir, depth) => {
34194
+ if (depth > 4 || found.ts && found.js) return;
34195
+ for (const entry of readdirSync2(dir, { withFileTypes: true })) {
34196
+ if (entry.name.startsWith(".")) continue;
34197
+ const full = join22(dir, entry.name);
34198
+ if (entry.isDirectory()) {
34199
+ if (!SKIP_DIRS.has(entry.name)) walk(full, depth + 1);
34200
+ continue;
34201
+ }
34202
+ if (/\.(ts|tsx)$/.test(entry.name)) found.ts = true;
34203
+ else if (/\.(js|cjs|mjs|wxml)$/.test(entry.name)) found.js = true;
34204
+ }
34205
+ };
34206
+ walk(root, 0);
34207
+ if (found.ts) return "TypeScript";
34208
+ if (found.js) return "JavaScript";
34209
+ return "Unknown";
34210
+ }
33476
34211
  async function detectProject(root) {
33477
34212
  const pkg = readPackageJson(root);
33478
34213
  const type = detectProjectType(root);
33479
- const language = existsSync6(join22(root, "tsconfig.json")) ? "TypeScript" : pkg || existsSync6(join22(root, "src")) ? "JavaScript" : "Unknown";
34214
+ const language = existsSync6(join22(root, "tsconfig.json")) ? "TypeScript" : pkg || existsSync6(join22(root, "src")) ? "JavaScript" : detectLanguageFromSources(root);
33480
34215
  const packageManager = existsSync6(join22(root, "pnpm-lock.yaml")) ? "pnpm" : existsSync6(join22(root, "yarn.lock")) ? "yarn" : existsSync6(join22(root, "package-lock.json")) ? "npm" : "npx";
33481
34216
  const existingE2eDirs = E2E_DIR_CANDIDATES.filter((dir) => existsSync6(join22(root, dir)));
33482
34217
  return {
@@ -33504,8 +34239,8 @@ function adaptersFor(type) {
33504
34239
  }
33505
34240
 
33506
34241
  // src/commands/init/skill-installer.ts
33507
- import { existsSync as existsSync7, readFileSync as readFileSync3 } from "node:fs";
33508
- import { access as access2, cp, mkdir as mkdir12, writeFile as writeFile10 } from "node:fs/promises";
34242
+ import { existsSync as existsSync7, readFileSync as readFileSync4 } from "node:fs";
34243
+ import { access as access2, cp, mkdir as mkdir12, writeFile as writeFile11 } from "node:fs/promises";
33509
34244
  import { dirname as dirname9, join as join23 } from "node:path";
33510
34245
  import { fileURLToPath } from "node:url";
33511
34246
  var CORE_FILES = ["SKILL.md", "manifest.yaml"];
@@ -33521,7 +34256,7 @@ async function exists3(path) {
33521
34256
  function readManifestVersion(dir) {
33522
34257
  const manifestPath = join23(dir, "manifest.yaml");
33523
34258
  if (!existsSync7(manifestPath)) return void 0;
33524
- return /^version:\s*([^\s#]+)/m.exec(readFileSync3(manifestPath, "utf8"))?.[1];
34259
+ return /^version:\s*([^\s#]+)/m.exec(readFileSync4(manifestPath, "utf8"))?.[1];
33525
34260
  }
33526
34261
  function resolveSkillSourceDir() {
33527
34262
  const fromEnv = process.env.TESTPILOT_SKILL_DIR;
@@ -33590,7 +34325,7 @@ async function installSkill(root, info, adapters) {
33590
34325
  const file2 = join23(refsDir, name);
33591
34326
  if (await exists3(file2)) continue;
33592
34327
  await mkdir12(refsDir, { recursive: true });
33593
- await writeFile10(file2, render(), "utf8");
34328
+ await writeFile11(file2, render(), "utf8");
33594
34329
  references = "created";
33595
34330
  }
33596
34331
  return coreVersion ? { core, coreVersion, references, dest } : { core, references, dest };
@@ -33709,9 +34444,44 @@ async function runDoctor(root) {
33709
34444
  (item) => item.available ? ok(`${item.id}(${item.detail})`) : item.required ? warn(`${item.id} \u4E0D\u53EF\u7528`, item.detail) : warn(`${item.id} \u672A\u5B89\u88C5(\u53EF\u9009)`, item.detail)
33710
34445
  )
33711
34446
  );
34447
+ const miniappProjectPath = config2?.miniapp?.projectPath;
34448
+ if (miniappProjectPath) {
34449
+ const miniappItems = [];
34450
+ const cliPath = process.env.WECHAT_DEVTOOLS_CLI ?? config2?.miniapp?.cliPath ?? await findWechatDevToolsCli();
34451
+ if (!cliPath) {
34452
+ miniappItems.push(
34453
+ warn("\u5FAE\u4FE1\u5F00\u53D1\u8005\u5DE5\u5177 CLI \u672A\u627E\u5230", "\u5B89\u88C5\u5FAE\u4FE1\u5F00\u53D1\u8005\u5DE5\u5177,\u6216\u8BBE\u7F6E WECHAT_DEVTOOLS_CLI \u6307\u5411 cli")
34454
+ );
34455
+ } else {
34456
+ miniappItems.push(ok(`\u5F00\u53D1\u8005\u5DE5\u5177 CLI(${cliPath})`));
34457
+ const loggedIn = await isWechatDevToolsLoggedIn(cliPath);
34458
+ if (loggedIn === true) miniappItems.push(ok("\u5F00\u53D1\u8005\u5DE5\u5177\u5DF2\u767B\u5F55"));
34459
+ else if (loggedIn === false)
34460
+ miniappItems.push(
34461
+ warn("\u5F00\u53D1\u8005\u5DE5\u5177\u672A\u767B\u5F55", "\u6253\u5F00\u5DE5\u5177\u626B\u7801\u767B\u5F55;CI \u65E0\u4EBA\u503C\u5B88\u7684\u767B\u5F55\u6001\u7EF4\u62A4\u89C1 docs/miniapp-testing.md")
34462
+ );
34463
+ else
34464
+ miniappItems.push(
34465
+ warn("\u5F00\u53D1\u8005\u5DE5\u5177\u767B\u5F55\u6001\u672A\u77E5", "cli islogin \u65E0\u6709\u6548\u8F93\u51FA;\u8BF7\u786E\u8BA4\u5DE5\u5177\u5DF2\u767B\u5F55\u4E14 \u5B89\u5168\u8BBE\u7F6E->\u670D\u52A1\u7AEF\u53E3 \u5DF2\u5F00\u542F")
34466
+ );
34467
+ }
34468
+ const appidInfo = readMiniappProjectAppid(miniappProjectPath);
34469
+ if (!appidInfo) {
34470
+ miniappItems.push(warn("project.config.json \u7F3A\u5931", `${miniappProjectPath} \u4E0B\u672A\u627E\u5230,\u81EA\u52A8\u5316\u65E0\u6CD5\u5B9A\u4F4D\u9879\u76EE`));
34471
+ } else if (!appidInfo.appid) {
34472
+ miniappItems.push(warn("project.config.json \u7F3A\u5C11 appid", "\u81EA\u52A8\u5316\u4E0E\u9884\u89C8\u80FD\u529B\u53D7\u9650,\u8BF7\u586B\u5199\u771F\u5B9E appid"));
34473
+ } else if (appidInfo.appid === "touristappid") {
34474
+ miniappItems.push(
34475
+ warn("appid \u4E3A touristappid(\u6E38\u5BA2\u6A21\u5F0F)", "\u90E8\u5206\u81EA\u52A8\u5316/\u9884\u89C8\u80FD\u529B\u53D7\u9650,\u8BF7\u5728 project.config.json \u586B\u5199\u771F\u5B9E appid")
34476
+ );
34477
+ } else {
34478
+ miniappItems.push(ok(`\u5C0F\u7A0B\u5E8F appid(${appidInfo.appid})`));
34479
+ }
34480
+ push("Miniapp IDE", miniappItems);
34481
+ }
33712
34482
  const refs = await collectEnvVarRefs(root);
33713
34483
  const envItems = refs.map(
33714
- (name) => process.env[name] !== void 0 ? ok(name) : error62(`${name} missing`, "\u5728\u8FD0\u884C\u73AF\u5883(.env / CI Secret)\u4E2D\u8BBE\u7F6E")
34484
+ (name) => process.env[name] !== void 0 ? ok(name) : error62(`${name} missing`, "\u5728 .env.testing(\u65E7\u7248\u672C\u4E3A .env,\u8BF7\u8FC1\u79FB)\u6216 CI Secret \u4E2D\u8BBE\u7F6E")
33715
34485
  );
33716
34486
  const serverConfigured = (process.env.TESTPILOT_SERVER_URL ?? config2?.server?.baseUrl) !== void 0;
33717
34487
  const accountRefs = [
@@ -33786,7 +34556,7 @@ function makeDoctorCommand() {
33786
34556
  import { join as join27 } from "node:path";
33787
34557
 
33788
34558
  // src/commands/init/config-initializer.ts
33789
- import { access as access3, mkdir as mkdir13, readFile as readFile10, writeFile as writeFile11 } from "node:fs/promises";
34559
+ import { access as access3, mkdir as mkdir13, readFile as readFile10, writeFile as writeFile12 } from "node:fs/promises";
33790
34560
  import { basename as basename2, join as join25 } from "node:path";
33791
34561
  async function exists4(path) {
33792
34562
  try {
@@ -33818,7 +34588,7 @@ environment:
33818
34588
  staging:
33819
34589
  baseUrl: \${STAGING_BASE_URL}
33820
34590
 
33821
- # Case \u53EF\u89C1\u7684\u9879\u76EE\u53D8\u91CF\u767D\u540D\u5355(\u503C\u6765\u81EA .env / CI Secret;Case \u7528 \${variable.<name>} \u5F15\u7528)
34591
+ # Case \u53EF\u89C1\u7684\u9879\u76EE\u53D8\u91CF\u767D\u540D\u5355(\u503C\u6765\u81EA .env.testing / CI Secret;Case \u7528 \${variable.<name>} \u5F15\u7528)
33822
34592
  # variables:
33823
34593
  # ppmProjectId: \${PPM_PROJECT_ID}
33824
34594
 
@@ -33835,19 +34605,20 @@ ${adapters}
33835
34605
  # server:
33836
34606
  # baseUrl: http://127.0.0.1:3000
33837
34607
 
33838
- # Web \u7AEF:navigate \u76F8\u5BF9 url \u7684\u57FA\u51C6\u5730\u5740(\u6BCF\u4EBA\u7684\u5730\u5740\u4E0D\u540C,\u503C\u653E .env)
34608
+ # Web \u7AEF:navigate \u76F8\u5BF9 url \u7684\u57FA\u51C6\u5730\u5740(\u6BCF\u4EBA\u7684\u5730\u5740\u4E0D\u540C,\u503C\u653E .env.testing)
33839
34609
  # web:
33840
34610
  # baseUrl: \${WEB_BASE_URL}
33841
34611
 
33842
- # \u5C0F\u7A0B\u5E8F\u7AEF:\u586B\u5165\u5C0F\u7A0B\u5E8F\u9879\u76EE\u76EE\u5F55\u540E\u5373\u53EF\u6267\u884C target: miniapp \u7684\u6B65\u9AA4(\u8DEF\u5F84\u6BCF\u4EBA\u4E0D\u540C,\u8D70 .env)
34612
+ # \u5C0F\u7A0B\u5E8F\u7AEF:\u586B\u5165\u5C0F\u7A0B\u5E8F\u9879\u76EE\u76EE\u5F55\u540E\u5373\u53EF\u6267\u884C target: miniapp \u7684\u6B65\u9AA4(\u8DEF\u5F84\u6BCF\u4EBA\u4E0D\u540C,\u8D70 .env.testing)
33843
34613
  # miniapp:
33844
34614
  # projectPath: \${MINIAPP_PROJECT_PATH}
33845
34615
  # cliPath: \${MINIAPP_CLI_PATH}
33846
34616
  `;
33847
34617
  }
33848
- function renderDotEnvExample() {
33849
- return `# TestPilot \u73AF\u5883\u53D8\u91CF\u793A\u4F8B:\u590D\u5236\u4E3A .env \u5E76\u586B\u5165\u771F\u5B9E\u503C(.env \u542B\u654F\u611F\u4FE1\u606F,\u4E0D\u8981\u63D0\u4EA4\u5230 Git)
33850
- # \u4F18\u5148\u7EA7:\u771F\u5B9E\u73AF\u5883\u53D8\u91CF > .env \u6587\u4EF6;testpilot.yaml \u4E2D\u7684 \${VAR} \u5F15\u7528\u8FD0\u884C\u65F6\u81EA\u52A8\u6CE8\u5165
34618
+ function renderDotEnvTesting() {
34619
+ return `# TestPilot \u6D4B\u8BD5\u73AF\u5883\u53D8\u91CF(CLI \u81EA\u52A8\u52A0\u8F7D\u672C\u6587\u4EF6,\u76F4\u63A5\u586B\u5165\u771F\u5B9E\u503C\u5373\u53EF,\u65E0\u9700\u590D\u5236)
34620
+ # \u53EA\u653E\u6D4B\u8BD5\u76F8\u5173\u53D8\u91CF,\u4E0E\u524D\u7AEF\u9879\u76EE\u81EA\u8EAB\u7684 .env \u4E92\u4E0D\u5F71\u54CD;\u672C\u6587\u4EF6\u542B\u6D4B\u8BD5\u51ED\u636E,\u5DF2\u52A0\u5165 .gitignore
34621
+ # \u4F18\u5148\u7EA7:\u771F\u5B9E\u73AF\u5883\u53D8\u91CF > \u672C\u6587\u4EF6;testpilot.yaml \u4E2D\u7684 \${VAR} \u5F15\u7528\u8FD0\u884C\u65F6\u81EA\u52A8\u6CE8\u5165
33851
34622
 
33852
34623
  # \u73AF\u5883\u914D\u7F6E(environment \u6BB5)
33853
34624
  TEST_BASE_URL=http://127.0.0.1:8080
@@ -33867,7 +34638,7 @@ STAGING_BASE_URL=https://staging.example.com
33867
34638
  async function ensureGitignoreEntry(root, entry, comment = "TestPilot \u672C\u5730\u73AF\u5883\u53D8\u91CF") {
33868
34639
  const gitignorePath = join25(root, ".gitignore");
33869
34640
  if (!await exists4(gitignorePath)) {
33870
- await writeFile11(gitignorePath, `# ${comment}
34641
+ await writeFile12(gitignorePath, `# ${comment}
33871
34642
  ${entry}
33872
34643
  `, "utf8");
33873
34644
  return "created";
@@ -33877,7 +34648,7 @@ ${entry}
33877
34648
  return "exists";
33878
34649
  }
33879
34650
  const separator = content.endsWith("\n") || content === "" ? "" : "\n";
33880
- await writeFile11(gitignorePath, `${content}${separator}# ${comment}
34651
+ await writeFile12(gitignorePath, `${content}${separator}# ${comment}
33881
34652
  ${entry}
33882
34653
  `, "utf8");
33883
34654
  return "updated";
@@ -33886,21 +34657,22 @@ async function initTestPilotConfig(root, info, adapters) {
33886
34657
  const yamlPath = join25(root, "testpilot.yaml");
33887
34658
  let yaml = "reused";
33888
34659
  if (!await exists4(yamlPath)) {
33889
- await writeFile11(yamlPath, renderTestpilotYaml(info), "utf8");
34660
+ await writeFile12(yamlPath, renderTestpilotYaml(info), "utf8");
33890
34661
  yaml = "created";
33891
34662
  }
33892
- const envExamplePath = join25(root, ".env.example");
33893
- let envExample = "reused";
33894
- if (!await exists4(envExamplePath)) {
33895
- await writeFile11(envExamplePath, renderDotEnvExample(), "utf8");
33896
- envExample = "created";
34663
+ const envTestingPath = join25(root, ENV_TESTING_FILE);
34664
+ let envTesting = "reused";
34665
+ if (!await exists4(envTestingPath)) {
34666
+ await writeFile12(envTestingPath, renderDotEnvTesting(), "utf8");
34667
+ envTesting = "created";
33897
34668
  }
33898
- const gitignore = await ensureGitignoreEntry(root, ".env");
34669
+ const gitignore = await ensureGitignoreEntry(root, ENV_TESTING_FILE);
34670
+ await ensureGitignoreEntry(root, ".env");
33899
34671
  const runtimeDir = join25(root, ".testpilot", "artifacts");
33900
34672
  await mkdir13(runtimeDir, { recursive: true });
33901
34673
  const keep = join25(runtimeDir, ".gitkeep");
33902
34674
  if (!await exists4(keep)) {
33903
- await writeFile11(keep, "", "utf8");
34675
+ await writeFile12(keep, "", "utf8");
33904
34676
  }
33905
34677
  const existing = await readProjectLink(root);
33906
34678
  const defaultAdapter = defaultAdapterFor(info.type);
@@ -33924,7 +34696,7 @@ async function initTestPilotConfig(root, info, adapters) {
33924
34696
  runtimeDir: join25(root, ".testpilot"),
33925
34697
  projectLink: link,
33926
34698
  projectLinkFile,
33927
- envExample,
34699
+ envTesting,
33928
34700
  gitignore
33929
34701
  };
33930
34702
  }
@@ -33956,8 +34728,8 @@ function randomId() {
33956
34728
  }
33957
34729
 
33958
34730
  // src/commands/init/test-structure.ts
33959
- import { existsSync as existsSync9, readdirSync as readdirSync2 } from "node:fs";
33960
- import { mkdir as mkdir14, writeFile as writeFile12 } from "node:fs/promises";
34731
+ import { existsSync as existsSync9, readdirSync as readdirSync3 } from "node:fs";
34732
+ import { mkdir as mkdir14, writeFile as writeFile13 } from "node:fs/promises";
33961
34733
  import { join as join26 } from "node:path";
33962
34734
  async function initTestStructure(root, _info) {
33963
34735
  const dirs = {
@@ -33976,8 +34748,8 @@ async function initTestStructure(root, _info) {
33976
34748
  await mkdir14(dir, { recursive: true });
33977
34749
  }
33978
34750
  for (const [name, dir] of Object.entries(dirs)) {
33979
- if (!existed[name] && readdirSync2(dir).length === 0) {
33980
- await writeFile12(join26(dir, ".gitkeep"), "", "utf8").catch(() => void 0);
34751
+ if (!existed[name] && readdirSync3(dir).length === 0) {
34752
+ await writeFile13(join26(dir, ".gitkeep"), "", "utf8").catch(() => void 0);
33981
34753
  }
33982
34754
  }
33983
34755
  return {
@@ -34016,10 +34788,10 @@ async function runInit(root, options = {}) {
34016
34788
  const serverMode = config2.projectLink.serverUrl ? `Server:${config2.projectLink.serverUrl}` : "\u672C\u5730\u6A21\u5F0F";
34017
34789
  console.log(`\u2713 \u9879\u76EE\u5173\u8054 ${config2.projectLink.projectId}(${serverMode})`);
34018
34790
  console.log(
34019
- config2.envExample === "created" ? "\u2713 \u73AF\u5883\u53D8\u91CF\u793A\u4F8B .env.example(\u590D\u5236\u4E3A .env \u586B\u5165\u771F\u5B9E\u503C)" : "\u2713 \u73AF\u5883\u53D8\u91CF\u793A\u4F8B .env.example(\u5DF2\u5B58\u5728,\u4FDD\u7559)"
34791
+ config2.envTesting === "created" ? "\u2713 \u6D4B\u8BD5\u73AF\u5883\u53D8\u91CF .env.testing(\u76F4\u63A5\u586B\u5165\u771F\u5B9E\u503C,CLI \u81EA\u52A8\u52A0\u8F7D)" : "\u2713 \u6D4B\u8BD5\u73AF\u5883\u53D8\u91CF .env.testing(\u5DF2\u5B58\u5728,\u4FDD\u7559)"
34020
34792
  );
34021
34793
  console.log(
34022
- config2.gitignore === "exists" ? "\u2713 Git \u5FFD\u7565 .env(\u5DF2\u5728 .gitignore)" : `\u2713 Git \u5FFD\u7565 .env(\u5DF2\u5199\u5165 .gitignore)`
34794
+ config2.gitignore === "exists" ? "\u2713 Git \u5FFD\u7565 .env.testing(\u5DF2\u5728 .gitignore)" : `\u2713 Git \u5FFD\u7565 .env.testing(\u5DF2\u5199\u5165 .gitignore)`
34023
34795
  );
34024
34796
  const testStructure = await initTestStructure(root, project);
34025
34797
  const reuseNote = project.existingE2eDirs.filter((dir) => dir !== "tests/e2e");
@@ -34111,7 +34883,7 @@ function makeListCommand() {
34111
34883
  }
34112
34884
 
34113
34885
  // src/commands/report.ts
34114
- import { exec } from "node:child_process";
34886
+ import { exec as exec2 } from "node:child_process";
34115
34887
  function makeReportCommand() {
34116
34888
  return new Command("report").description("\u751F\u6210\u6D4B\u8BD5\u62A5\u544A(JSON + HTML)").option("--run <id>", "\u8FD0\u884C\u8BB0\u5F55 ID,\u9ED8\u8BA4\u6700\u8FD1\u4E00\u6B21").option("--open", "\u751F\u6210\u540E\u5728\u6D4F\u89C8\u5668\u6253\u5F00", false).option("--summary", "\u751F\u6210\u8DE8 run \u6C47\u603B\u62A5\u544A(summary.html + summary.json)", false).option("--last <n>", "\u6C47\u603B\u7EDF\u8BA1\u7A97\u53E3:\u6700\u8FD1 N \u6B21\u8FD0\u884C", "30").action(async (options) => {
34117
34889
  const client = new TestPilotClient();
@@ -34140,7 +34912,7 @@ function makeReportCommand() {
34140
34912
  function openInBrowser(file2) {
34141
34913
  const command = process.platform === "win32" ? `start "" "${file2}"` : process.platform === "darwin" ? `open "${file2}"` : `xdg-open "${file2}"`;
34142
34914
  return new Promise((resolvePromise) => {
34143
- exec(command, () => resolvePromise());
34915
+ exec2(command, () => resolvePromise());
34144
34916
  });
34145
34917
  }
34146
34918
 
@@ -34285,10 +35057,13 @@ function makeSyncMetadataCommand() {
34285
35057
  }
34286
35058
 
34287
35059
  // src/commands/update.ts
34288
- import { writeFile as writeFile13 } from "node:fs/promises";
35060
+ import { readFile as readFile11, writeFile as writeFile14 } from "node:fs/promises";
34289
35061
  import { join as join29 } from "node:path";
35062
+ function varNames(content) {
35063
+ return new Set([...content.matchAll(/^#?\s*([A-Z][A-Z0-9_]+)=/gm)].map((match) => match[1]));
35064
+ }
34290
35065
  function makeUpdateCommand() {
34291
- const update = new Command("update").description("\u540C\u6B65 TestPilot \u63A5\u5165\u7269\u5230\u5F53\u524D CLI \u7248\u672C(Skill / .env.example;\u4E0D\u89E6\u78B0 .env \u4E0E\u7528\u4F8B)").option("--force", "Skill \u7248\u672C\u4E00\u81F4\u65F6\u4E5F\u5F3A\u5236\u8986\u76D6\u672C\u5730\u4FEE\u6539", false).action(async (options) => {
35066
+ const update = new Command("update").description("\u540C\u6B65 TestPilot \u63A5\u5165\u7269\u5230\u5F53\u524D CLI \u7248\u672C(Skill / .env.testing;\u4E0D\u89E6\u78B0 .env \u4E0E\u7528\u4F8B)").option("--force", "Skill \u7248\u672C\u4E00\u81F4\u65F6\u4E5F\u5F3A\u5236\u8986\u76D6\u672C\u5730\u4FEE\u6539", false).action(async (options) => {
34292
35067
  const root = process.cwd();
34293
35068
  const dest = join29(".agents", "skills", "testpilot");
34294
35069
  const skill = await syncSkillFiles(root, { force: options.force });
@@ -34304,13 +35079,25 @@ function makeUpdateCommand() {
34304
35079
  default:
34305
35080
  console.log(`\u2713 Skill \u5DF2\u662F\u6700\u65B0 v${skill.bundledVersion ?? "?"}`);
34306
35081
  }
34307
- await writeFile13(join29(root, ".env.example"), renderDotEnvExample(), "utf8");
34308
- console.log("\u2713 .env.example \u5DF2\u540C\u6B65\u4E3A\u5F53\u524D\u7248\u672C\u7684\u793A\u4F8B(.env \u4E0D\u53D7\u5F71\u54CD)");
34309
- const gitignore = await ensureGitignoreEntry(root, ".env");
35082
+ const envPath = join29(root, ENV_TESTING_FILE);
35083
+ const template = renderDotEnvTesting();
35084
+ const existing = await readFile11(envPath, "utf8").catch(() => void 0);
35085
+ if (existing === void 0) {
35086
+ await writeFile14(envPath, template, "utf8");
35087
+ console.log(`\u2713 ${ENV_TESTING_FILE} \u5DF2\u751F\u6210(\u76F4\u63A5\u586B\u5165\u771F\u5B9E\u503C,CLI \u81EA\u52A8\u52A0\u8F7D)`);
35088
+ } else {
35089
+ console.log(`\u2713 ${ENV_TESTING_FILE} \u5DF2\u5B58\u5728,\u4FDD\u7559\u7528\u6237\u5185\u5BB9`);
35090
+ const missing = [...varNames(template)].filter((name) => !varNames(existing).has(name));
35091
+ if (missing.length > 0) {
35092
+ console.log(` \u63D0\u793A:\u5F53\u524D\u7248\u672C\u6A21\u677F\u65B0\u589E\u53D8\u91CF,\u53EF\u6309\u9700\u8865\u5165:${missing.join(", ")}`);
35093
+ }
35094
+ }
35095
+ const gitignore = await ensureGitignoreEntry(root, ENV_TESTING_FILE);
35096
+ await ensureGitignoreEntry(root, ".env");
34310
35097
  console.log(
34311
- gitignore === "exists" ? "\u2713 .gitignore \u5DF2\u5FFD\u7565 .env" : "\u2713 .gitignore \u5DF2\u8FFD\u52A0\u5FFD\u7565 .env"
35098
+ gitignore === "exists" ? `\u2713 .gitignore \u5DF2\u5FFD\u7565 ${ENV_TESTING_FILE}` : `\u2713 .gitignore \u5DF2\u8FFD\u52A0\u5FFD\u7565 ${ENV_TESTING_FILE}`
34312
35099
  );
34313
- console.log("\u63D0\u793A:\u63D0\u4EA4 .agents/skills/ \u4E0E .env.example \u7684\u53D8\u66F4,\u56E2\u961F\u4E0E AI \u5373\u53EF\u4F7F\u7528\u6700\u65B0\u80FD\u529B");
35100
+ console.log("\u63D0\u793A:\u63D0\u4EA4 .agents/skills/ \u7684\u53D8\u66F4,\u56E2\u961F\u4E0E AI \u5373\u53EF\u4F7F\u7528\u6700\u65B0\u80FD\u529B");
34314
35101
  });
34315
35102
  return update;
34316
35103
  }
@@ -34351,7 +35138,7 @@ function makeValidateCommand() {
34351
35138
  }
34352
35139
 
34353
35140
  // src/cli.ts
34354
- var CLI_VERSION = "0.7.0";
35141
+ var CLI_VERSION = "0.7.2";
34355
35142
  async function createCli() {
34356
35143
  const program2 = new Command();
34357
35144
  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", () => {