csspilot 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -10374,10 +10374,84 @@ var {
10374
10374
  Help
10375
10375
  } = import_index.default;
10376
10376
 
10377
- // src/commands/doctor.ts
10378
- import { existsSync as existsSync2 } from "node:fs";
10379
- import { createRequire } from "node:module";
10380
- import { join as join5 } from "node:path";
10377
+ // src/commands/init/ci-initializer.ts
10378
+ import { access, mkdir, writeFile } from "node:fs/promises";
10379
+ import { join } from "node:path";
10380
+ async function exists(path) {
10381
+ try {
10382
+ await access(path);
10383
+ return true;
10384
+ } catch {
10385
+ return false;
10386
+ }
10387
+ }
10388
+ async function detectCi(root) {
10389
+ const workflow = join(root, ".github", "workflows", "testpilot.yml");
10390
+ return {
10391
+ githubWorkflows: await exists(join(root, ".github", "workflows")),
10392
+ gitlabCi: await exists(join(root, ".gitlab-ci.yml")),
10393
+ hasTestpilotWorkflow: await exists(workflow)
10394
+ };
10395
+ }
10396
+ var CI_TEMPLATE = `# csspilot ci init \u751F\u6210;Git Push -> TestPilot Server \u7684\u6B63\u5F0F\u540C\u6B65\u8FB9\u754C
10397
+ name: TestPilot
10398
+
10399
+ on:
10400
+ push:
10401
+ branches: [main]
10402
+
10403
+ jobs:
10404
+ testpilot:
10405
+ runs-on: ubuntu-latest
10406
+ env:
10407
+ TESTPILOT_SERVER_URL: \${{ secrets.TESTPILOT_SERVER_URL }}
10408
+ steps:
10409
+ - uses: actions/checkout@v4
10410
+
10411
+ - uses: actions/setup-node@v4
10412
+ with:
10413
+ node-version: 22
10414
+
10415
+ - name: Validate cases
10416
+ run: npx -y csspilot@latest validate
10417
+
10418
+ # sync-metadata \u662F Git Push -> Server \u7684\u6B63\u5F0F\u540C\u6B65\u8FB9\u754C;\u672A\u914D\u7F6E Server \u65F6\u8DF3\u8FC7
10419
+ - name: Sync case metadata
10420
+ if: \${{ env.TESTPILOT_SERVER_URL != '' }}
10421
+ run: npx -y csspilot@latest sync-metadata
10422
+
10423
+ # \u9700\u8981\u5728 CI \u91CC\u8DD1\u7528\u4F8B\u65F6\u518D\u542F\u7528(Server \u89E6\u53D1\u6216\u672C\u5730\u89E6\u53D1\u5747\u53EF)
10424
+ # - name: Run cases
10425
+ # run: npx -y csspilot@latest run
10426
+ `;
10427
+ async function initCi(root) {
10428
+ const file2 = join(root, ".github", "workflows", "testpilot.yml");
10429
+ if (await exists(file2)) {
10430
+ return { created: false, file: file2 };
10431
+ }
10432
+ await mkdir(join(root, ".github", "workflows"), { recursive: true });
10433
+ await writeFile(file2, CI_TEMPLATE, "utf8");
10434
+ return { created: true, file: file2 };
10435
+ }
10436
+
10437
+ // src/commands/ci.ts
10438
+ function makeCiCommand() {
10439
+ const ci = new Command("ci").description("CI \u96C6\u6210(\u751F\u6210\u6A21\u677F,\u4E0D\u4FEE\u6539\u5DF2\u6709 CI)");
10440
+ ci.command("init").description("\u751F\u6210 .github/workflows/testpilot.yml(validate + sync-metadata;\u5DF2\u6709\u5219\u8DF3\u8FC7)").action(async () => {
10441
+ const result = await initCi(process.cwd());
10442
+ if (result.created) {
10443
+ console.log(`\u2713 CI \u5DE5\u4F5C\u6D41\u5DF2\u751F\u6210 ${result.file}`);
10444
+ console.log(" Git Push \u540E\u81EA\u52A8\u6267\u884C csspilot validate \u4E0E sync-metadata(\u9700\u914D\u7F6E TESTPILOT_SERVER_URL Secret)");
10445
+ } else {
10446
+ console.log(`\u2713 CI \u5DE5\u4F5C\u6D41\u5DF2\u5B58\u5728 ${result.file}(\u8DF3\u8FC7)`);
10447
+ }
10448
+ });
10449
+ return ci;
10450
+ }
10451
+
10452
+ // src/commands/init/doctor.ts
10453
+ import { existsSync as existsSync6 } from "node:fs";
10454
+ import { join as join21 } from "node:path";
10381
10455
 
10382
10456
  // ../../packages/core/src/errors/index.ts
10383
10457
  var TestPilotError = class extends Error {
@@ -10390,7 +10464,7 @@ var TestPilotError = class extends Error {
10390
10464
  };
10391
10465
 
10392
10466
  // ../../packages/core/src/constants/index.ts
10393
- import { join } from "node:path";
10467
+ import { join as join2 } from "node:path";
10394
10468
  var TESTPILOT_DIR = ".testpilot";
10395
10469
  var RUNS_DIR = `${TESTPILOT_DIR}/artifacts/runs`;
10396
10470
  var RESULT_FILE = "result.json";
@@ -10399,14 +10473,14 @@ var DEFAULT_STEP_TIMEOUT_MS = 15e3;
10399
10473
  var WECHAT_DEVTOOLS_CLI_PATHS = [
10400
10474
  "C:/Program Files (x86)/Tencent/\u5FAE\u4FE1web\u5F00\u53D1\u8005\u5DE5\u5177/cli.bat",
10401
10475
  "C:/Program Files/Tencent/\u5FAE\u4FE1web\u5F00\u53D1\u8005\u5DE5\u5177/cli.bat",
10402
- join(process.env.LOCALAPPDATA ?? ".", "\u5FAE\u4FE1web\u5F00\u53D1\u8005\u5DE5\u5177", "cli.bat"),
10476
+ join2(process.env.LOCALAPPDATA ?? ".", "\u5FAE\u4FE1web\u5F00\u53D1\u8005\u5DE5\u5177", "cli.bat"),
10403
10477
  "/Applications/wechatwebdevtools.app/Contents/MacOS/cli"
10404
10478
  ];
10405
10479
 
10406
10480
  // ../../packages/core/src/config-loader.ts
10407
10481
  var import_yaml = __toESM(require_dist(), 1);
10408
10482
  import { readFile } from "node:fs/promises";
10409
- import { join as join2 } from "node:path";
10483
+ import { join as join3 } from "node:path";
10410
10484
 
10411
10485
  // ../../node_modules/.pnpm/zod@4.6.1/node_modules/zod/v4/classic/external.js
10412
10486
  var external_exports = {};
@@ -15440,7 +15514,7 @@ var recursive = /* @__PURE__ */ new WeakMap();
15440
15514
  var NONE = 0;
15441
15515
  var ASSUMED = 1;
15442
15516
  var PROVEN = 2;
15443
- function isRecursive(inst, stack, resolve3) {
15517
+ function isRecursive(inst, stack, resolve5) {
15444
15518
  const cached2 = recursive.get(inst);
15445
15519
  if (cached2 !== void 0)
15446
15520
  return cached2 ? PROVEN : NONE;
@@ -15450,7 +15524,7 @@ function isRecursive(inst, stack, resolve3) {
15450
15524
  let result = NONE;
15451
15525
  const check2 = (child) => {
15452
15526
  if (result !== PROVEN && child?._zod) {
15453
- const answer = isRecursive(child, stack, resolve3);
15527
+ const answer = isRecursive(child, stack, resolve5);
15454
15528
  if (answer > result)
15455
15529
  result = answer;
15456
15530
  }
@@ -15461,7 +15535,7 @@ function isRecursive(inst, stack, resolve3) {
15461
15535
  const desc = Object.getOwnPropertyDescriptor(sh, key);
15462
15536
  if (spread && !desc.enumerable)
15463
15537
  continue;
15464
- const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve3) : NONE;
15538
+ const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve5) : NONE;
15465
15539
  if (child > answer)
15466
15540
  answer = child;
15467
15541
  }
@@ -15528,7 +15602,7 @@ function isRecursive(inst, stack, resolve3) {
15528
15602
  break;
15529
15603
  // `$ZodLazy` caches its inner on the def, so a resolved edge is followed exactly
15530
15604
  case "lazy": {
15531
- const inner = def._cachedInner ?? (resolve3 ? inst._zod.innerType : void 0);
15605
+ const inner = def._cachedInner ?? (resolve5 ? inst._zod.innerType : void 0);
15532
15606
  merge2(inner ? isRecursive(inner, stack, false) : ASSUMED);
15533
15607
  break;
15534
15608
  }
@@ -30081,8 +30155,24 @@ function date4(params) {
30081
30155
  }
30082
30156
 
30083
30157
  // ../../packages/core/src/config-loader.ts
30158
+ var environmentEntrySchema = external_exports.strictObject({ baseUrl: external_exports.string().min(1).optional() }).optional();
30084
30159
  var configSchema = external_exports.strictObject({
30160
+ /** 配置文件版本(csspilot init 生成为 1) */
30161
+ version: external_exports.number().optional(),
30162
+ /** 旧式用例目录字段;与 test.caseDirectory 等价,二选一 */
30085
30163
  casesDir: external_exports.string().min(1).optional(),
30164
+ project: external_exports.strictObject({ name: external_exports.string().min(1).optional() }).optional(),
30165
+ test: external_exports.strictObject({
30166
+ directory: external_exports.string().min(1).optional(),
30167
+ caseDirectory: external_exports.string().min(1).optional()
30168
+ }).optional(),
30169
+ /** 环境配置:default 指定默认环境名,其余键为环境名 -> { baseUrl } */
30170
+ environment: external_exports.record(external_exports.string(), external_exports.union([external_exports.string().min(1), environmentEntrySchema])).optional(),
30171
+ /** 项目声明的执行端(playwright / wechatide);仅作声明,实际可用性以 doctor 检测为准 */
30172
+ adapters: external_exports.array(external_exports.string().min(1)).optional(),
30173
+ workspace: external_exports.strictObject({ default: external_exports.string().min(1).optional() }).optional(),
30174
+ /** TestPilot Control Plane 地址(sync-metadata / init 关联项目时使用) */
30175
+ server: external_exports.strictObject({ baseUrl: external_exports.string().min(1).optional() }).optional(),
30086
30176
  web: external_exports.strictObject({ baseUrl: external_exports.string().min(1).optional() }).optional(),
30087
30177
  miniapp: external_exports.strictObject({
30088
30178
  projectPath: external_exports.string().min(1).optional(),
@@ -30090,7 +30180,7 @@ var configSchema = external_exports.strictObject({
30090
30180
  }).optional()
30091
30181
  });
30092
30182
  async function loadTestpilotConfig(cwd = process.cwd()) {
30093
- const file2 = join2(cwd, "testpilot.yaml");
30183
+ const file2 = join3(cwd, "testpilot.yaml");
30094
30184
  let raw;
30095
30185
  try {
30096
30186
  raw = (0, import_yaml.parse)(await readFile(file2, "utf8"));
@@ -30106,7 +30196,15 @@ async function loadTestpilotConfig(cwd = process.cwd()) {
30106
30196
  throw new Error(`testpilot.yaml \u914D\u7F6E\u4E0D\u5408\u6CD5:${detail}`);
30107
30197
  }
30108
30198
  return {
30109
- casesDir: result.data.casesDir ?? DEFAULT_CASES_DIR,
30199
+ version: result.data.version,
30200
+ // 用例目录:旧字段 casesDir 优先,其次 test.caseDirectory,最后默认值
30201
+ casesDir: result.data.casesDir ?? result.data.test?.caseDirectory ?? DEFAULT_CASES_DIR,
30202
+ testDirectory: result.data.test?.directory,
30203
+ projectName: result.data.project?.name,
30204
+ environment: result.data.environment,
30205
+ adapters: result.data.adapters,
30206
+ workspace: result.data.workspace,
30207
+ server: result.data.server,
30110
30208
  web: result.data.web,
30111
30209
  miniapp: result.data.miniapp
30112
30210
  };
@@ -30115,7 +30213,7 @@ async function loadTestpilotConfig(cwd = process.cwd()) {
30115
30213
  // ../../packages/core/src/wechat-devtools.ts
30116
30214
  import { execFile } from "node:child_process";
30117
30215
  import { existsSync, readdirSync } from "node:fs";
30118
- import { join as join3 } from "node:path";
30216
+ import { join as join4 } from "node:path";
30119
30217
  var CLI_NAMES = ["cli.bat", "cli"];
30120
30218
  var REGISTRY_UNINSTALL_KEYS = [
30121
30219
  "HKLM\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall",
@@ -30131,7 +30229,7 @@ async function findWechatDevToolsCli() {
30131
30229
  if (process.platform !== "win32") return void 0;
30132
30230
  for (const dir of candidateInstallDirs()) {
30133
30231
  for (const name of CLI_NAMES) {
30134
- const cli = join3(dir, name);
30232
+ const cli = join4(dir, name);
30135
30233
  if (existsSync(cli)) return cli;
30136
30234
  }
30137
30235
  }
@@ -30144,19 +30242,19 @@ function candidateInstallDirs() {
30144
30242
  const drive = `${String.fromCharCode(code)}:\\`;
30145
30243
  if (!existsSync(drive)) continue;
30146
30244
  tencentParents.push(
30147
- join3(drive, "Program Files (x86)", "Tencent"),
30148
- join3(drive, "Program Files", "Tencent")
30245
+ join4(drive, "Program Files (x86)", "Tencent"),
30246
+ join4(drive, "Program Files", "Tencent")
30149
30247
  );
30150
30248
  }
30151
30249
  if (process.env.LOCALAPPDATA) {
30152
- tencentParents.push(join3(process.env.LOCALAPPDATA, "Programs"));
30250
+ tencentParents.push(join4(process.env.LOCALAPPDATA, "Programs"));
30153
30251
  }
30154
30252
  for (const parent of tencentParents) {
30155
30253
  if (!existsSync(parent)) continue;
30156
30254
  try {
30157
30255
  for (const entry of readdirSync(parent)) {
30158
30256
  if (/微信web开发者工具|wechatwebdevtools/i.test(entry)) {
30159
- dirs.push(join3(parent, entry));
30257
+ dirs.push(join4(parent, entry));
30160
30258
  }
30161
30259
  }
30162
30260
  } catch {
@@ -30179,7 +30277,7 @@ async function findFromRegistry() {
30179
30277
  const installDir = dirBefore(raw, /微信开发者工具\.exe|卸载微信开发者工具\.exe/i);
30180
30278
  if (!installDir) continue;
30181
30279
  for (const name of CLI_NAMES) {
30182
- const cli = join3(installDir, name);
30280
+ const cli = join4(installDir, name);
30183
30281
  if (existsSync(cli)) return cli;
30184
30282
  }
30185
30283
  }
@@ -30202,185 +30300,12 @@ function execToString(command, args) {
30202
30300
  });
30203
30301
  }
30204
30302
 
30205
- // src/lib/cases.ts
30206
- import { readdir, stat } from "node:fs/promises";
30207
- import { extname, join as join4 } from "node:path";
30208
- var DEFAULT_CASES_DIR2 = "tests/e2e/cases";
30209
- async function collectCaseFiles(paths) {
30210
- const files = [];
30211
- for (const input2 of paths) {
30212
- const info = await stat(input2).catch(() => void 0);
30213
- if (!info) continue;
30214
- if (info.isDirectory()) {
30215
- files.push(...await collectFromDir(input2));
30216
- } else if (isCaseFile(input2)) {
30217
- files.push(input2);
30218
- }
30219
- }
30220
- return [...new Set(files)].sort();
30221
- }
30222
- async function collectFromDir(dir) {
30223
- const entries = await readdir(dir, { withFileTypes: true });
30224
- const found = [];
30225
- for (const entry of entries) {
30226
- const full = join4(dir, entry.name);
30227
- if (entry.isDirectory()) {
30228
- found.push(...await collectFromDir(full));
30229
- } else if (isCaseFile(entry.name)) {
30230
- found.push(full);
30231
- }
30232
- }
30233
- return found;
30234
- }
30235
- function isCaseFile(file2) {
30236
- const ext = extname(file2).toLowerCase();
30237
- return ext === ".yaml" || ext === ".yml";
30238
- }
30239
-
30240
- // src/commands/doctor.ts
30241
- function makeDoctorCommand() {
30242
- return new Command("doctor").description("\u68C0\u67E5\u672C\u5730\u73AF\u5883(Node / Playwright / WeChat DevTools / \u9879\u76EE\u914D\u7F6E)").action(async () => {
30243
- let problems = 0;
30244
- const ok = (label) => console.log(`\u2713 ${label}`);
30245
- const fail = (label, hint) => {
30246
- problems++;
30247
- console.log(`\u2717 ${label}${hint ? `(${hint})` : ""}`);
30248
- };
30249
- console.log("TestPilot Doctor");
30250
- console.log("");
30251
- const nodeMajor = Number(process.versions.node.split(".")[0]);
30252
- if (nodeMajor >= 22) ok(`Node.js ${process.versions.node}`);
30253
- else fail(`Node.js ${process.versions.node}`, "\u9700\u8981 Node.js >= 22");
30254
- ok("TestPilot CLI (0.1.0)");
30255
- if (existsSync2("testpilot.yaml")) ok("testpilot.yaml");
30256
- else fail("testpilot.yaml", "\u8FD0\u884C npx csspilot init \u751F\u6210");
30257
- const caseFiles = await collectCaseFiles([DEFAULT_CASES_DIR2]).catch(() => []);
30258
- if (caseFiles.length > 0) ok(`${DEFAULT_CASES_DIR2} (${caseFiles.length} \u4E2A\u7528\u4F8B)`);
30259
- else fail(DEFAULT_CASES_DIR2, "\u672A\u627E\u5230\u7528\u4F8B");
30260
- if (existsSync2(".ai/skills/testpilot/manifest.yaml")) ok("TestPilot Skill (.ai/skills/testpilot)");
30261
- else fail("TestPilot Skill", "\u8FD0\u884C npx csspilot init \u5B89\u88C5");
30262
- console.log("");
30263
- console.log("Web");
30264
- const playwright = resolvePlaywright();
30265
- if (!playwright) {
30266
- fail("Playwright", "pnpm add -D playwright \u6216 @playwright/test");
30267
- fail("Chromium");
30268
- } else {
30269
- ok(`Playwright (${playwright.version})`);
30270
- if (chromiumInstalled(playwright.module)) ok("Chromium");
30271
- else fail("Chromium", "pnpm exec playwright install chromium");
30272
- }
30273
- console.log("");
30274
- console.log("Mini Program");
30275
- const devtools = await findWechatDevToolsCli();
30276
- if (devtools) ok(`WeChat Developer Tools (${devtools})`);
30277
- else fail("WeChat Developer Tools", "\u672A\u627E\u5230,\u53EF\u8BBE\u7F6E WECHAT_DEVTOOLS_CLI \u6307\u5411 cli \u53EF\u6267\u884C\u6587\u4EF6");
30278
- console.log("");
30279
- console.log(problems === 0 ? "Environment looks good." : `\u53D1\u73B0 ${problems} \u4E2A\u95EE\u9898,\u89C1\u4E0A\u8868\u3002`);
30280
- });
30281
- }
30282
- function resolvePlaywright() {
30283
- const require2 = createRequire(join5(process.cwd(), "package.json"));
30284
- for (const name of ["playwright", "@playwright/test"]) {
30285
- try {
30286
- const module = require2(name);
30287
- const version2 = require2(`${name}/package.json`).version;
30288
- return { module, version: version2 };
30289
- } catch {
30290
- continue;
30291
- }
30292
- }
30293
- return void 0;
30294
- }
30295
- function chromiumInstalled(module) {
30296
- try {
30297
- const path = module.chromium.executablePath();
30298
- return path !== "" && existsSync2(path);
30299
- } catch {
30300
- return false;
30301
- }
30302
- }
30303
-
30304
- // src/commands/init.ts
30305
- import { existsSync as existsSync3 } from "node:fs";
30306
- import { access, cp, mkdir, writeFile } from "node:fs/promises";
30307
- import { dirname, join as join6 } from "node:path";
30308
- import { fileURLToPath } from "node:url";
30309
- var TESTPILOT_YAML_TEMPLATE = `# TestPilot \u9879\u76EE\u914D\u7F6E
30310
- # validate / list / run \u9ED8\u8BA4\u5728\u8FD9\u4E2A\u76EE\u5F55\u67E5\u627E *.yaml \u7528\u4F8B
30311
- casesDir: tests/e2e/cases
30312
-
30313
- # Web \u7AEF:navigate \u76F8\u5BF9 url \u7684\u57FA\u51C6\u5730\u5740
30314
- # web:
30315
- # baseUrl: http://127.0.0.1:8080
30316
-
30317
- # \u5C0F\u7A0B\u5E8F\u7AEF:\u586B\u5165\u5C0F\u7A0B\u5E8F\u9879\u76EE\u76EE\u5F55\u540E\u5373\u53EF\u6267\u884C target: miniapp \u7684\u6B65\u9AA4
30318
- # miniapp:
30319
- # projectPath: path/to/miniprogram
30320
- # cliPath: C:/Program Files (x86)/Tencent/\u5FAE\u4FE1web\u5F00\u53D1\u8005\u5DE5\u5177/cli.bat
30321
- `;
30322
- async function exists(path) {
30323
- try {
30324
- await access(path);
30325
- return true;
30326
- } catch {
30327
- return false;
30328
- }
30329
- }
30330
- function resolveSkillSourceDir() {
30331
- const fromEnv = process.env.TESTPILOT_SKILL_DIR;
30332
- if (fromEnv) return fromEnv;
30333
- const bundled = fileURLToPath(new URL("./skills/testpilot", import.meta.url));
30334
- if (existsSync3(bundled)) return bundled;
30335
- const inRepo = fileURLToPath(new URL("../../../../skills/testpilot", import.meta.url));
30336
- return existsSync3(inRepo) ? inRepo : void 0;
30337
- }
30338
- function makeInitCommand() {
30339
- return new Command("init").description("\u628A TestPilot \u63A5\u5165\u5F53\u524D\u4E1A\u52A1\u9879\u76EE").action(async () => {
30340
- const cwd = process.cwd();
30341
- const skillDest = join6(cwd, ".ai", "skills", "testpilot");
30342
- if (await exists(join6(skillDest, "manifest.yaml"))) {
30343
- console.log("\u2713 Skill \u5B89\u88C5 .ai/skills/testpilot/(\u5DF2\u5B58\u5728,\u8DF3\u8FC7)");
30344
- } else {
30345
- const skillSrc = resolveSkillSourceDir();
30346
- if (!skillSrc) {
30347
- console.error("\u2717 \u65E0\u6CD5\u5B9A\u4F4D TestPilot Skill \u6E90\u76EE\u5F55");
30348
- console.error(" \u53EF\u901A\u8FC7\u73AF\u5883\u53D8\u91CF TESTPILOT_SKILL_DIR \u6307\u5B9A skills/testpilot \u8DEF\u5F84\u540E\u91CD\u8BD5");
30349
- process.exitCode = 1;
30350
- return;
30351
- }
30352
- await mkdir(dirname(skillDest), { recursive: true });
30353
- await cp(skillSrc, skillDest, { recursive: true });
30354
- console.log("\u2713 Skill \u5B89\u88C5 .ai/skills/testpilot/");
30355
- }
30356
- await mkdir(join6(cwd, ".testpilot", "artifacts"), { recursive: true });
30357
- const keep = join6(cwd, ".testpilot", "artifacts", ".gitkeep");
30358
- if (!await exists(keep)) {
30359
- await writeFile(keep, "", "utf8");
30360
- }
30361
- console.log("\u2713 Runtime \u76EE\u5F55 .testpilot/");
30362
- for (const dir of ["tests/e2e/cases", "tests/e2e/fixtures", "tests/e2e/data"]) {
30363
- await mkdir(join6(cwd, dir), { recursive: true });
30364
- }
30365
- console.log("\u2713 \u6D4B\u8BD5\u76EE\u5F55 tests/e2e/{cases,fixtures,data}");
30366
- const configPath = join6(cwd, "testpilot.yaml");
30367
- if (await exists(configPath)) {
30368
- console.log("\u2713 \u9879\u76EE\u914D\u7F6E testpilot.yaml(\u5DF2\u5B58\u5728,\u8DF3\u8FC7)");
30369
- } else {
30370
- await writeFile(configPath, TESTPILOT_YAML_TEMPLATE, "utf8");
30371
- console.log("\u2713 \u9879\u76EE\u914D\u7F6E testpilot.yaml");
30372
- }
30373
- console.log("");
30374
- console.log("\u4E0B\u4E00\u6B65:");
30375
- console.log(" 1. \u8BA9 AI \u9605\u8BFB .ai/skills/testpilot/SKILL.md");
30376
- console.log(" 2. \u5728 tests/e2e/cases/ \u521B\u5EFA Case(\u9075\u5FAA SKILL.md \u4E0E rules/)");
30377
- console.log(" 3. npx csspilot validate");
30378
- console.log(" 4. npx csspilot run");
30379
- });
30380
- }
30303
+ // ../../packages/sdk/src/client.ts
30304
+ import { isAbsolute as isAbsolute3, join as join17 } from "node:path";
30381
30305
 
30382
- // src/commands/list.ts
30383
- import { readFile as readFile2 } from "node:fs/promises";
30306
+ // ../../packages/sdk/src/cases.ts
30307
+ import { mkdir as mkdir3, readFile as readFile3, readdir, stat as stat2, writeFile as writeFile3 } from "node:fs/promises";
30308
+ import { dirname, extname, isAbsolute, join as join6, resolve as resolve2 } from "node:path";
30384
30309
 
30385
30310
  // ../../packages/dsl/src/schema/index.ts
30386
30311
  var STEP_TARGETS = ["web", "miniapp"];
@@ -30413,6 +30338,12 @@ var caseSchema = external_exports.strictObject({
30413
30338
  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)"),
30414
30339
  name: external_exports.string().min(1, "name \u4E0D\u80FD\u4E3A\u7A7A"),
30415
30340
  description: external_exports.string().optional(),
30341
+ /** 引用 Test Workspace(多系统环境组合);Case 只表达测试意图,环境由 Workspace 提供 */
30342
+ workspace: external_exports.string().min(1).optional(),
30343
+ /** 引用项目环境(testpilot.yaml environment 段);Case 不写死地址 */
30344
+ environment: external_exports.string().min(1).optional(),
30345
+ /** 引用测试账号标识;凭据由 Environment + Server/CI Secret 管理,禁止写进 Case */
30346
+ accountRef: external_exports.string().min(1).optional(),
30416
30347
  tags: external_exports.array(external_exports.string().min(1)).optional(),
30417
30348
  steps: external_exports.array(stepSchema).min(1, "Case \u81F3\u5C11\u5305\u542B\u4E00\u4E2A step")
30418
30349
  });
@@ -30578,61 +30509,142 @@ function checkVariableRefs(step, at, declared, issues) {
30578
30509
  }
30579
30510
  }
30580
30511
 
30581
- // src/commands/list.ts
30582
- function makeListCommand() {
30583
- return new Command("list").description("\u5217\u51FA\u6D4B\u8BD5\u7528\u4F8B").option("--tag <tag>", "\u6309\u6807\u7B7E\u8FC7\u6EE4").action(async (options) => {
30584
- const tag = options.tag;
30585
- const config2 = await loadTestpilotConfig().catch((err) => {
30586
- console.error(err.message);
30587
- process.exitCode = 1;
30588
- return void 0;
30589
- });
30590
- if (!config2) return;
30591
- const files = await collectCaseFiles([config2.casesDir]);
30592
- if (files.length === 0) {
30593
- console.error(`\u672A\u627E\u5230\u7528\u4F8B\u76EE\u5F55 ${config2.casesDir}`);
30594
- console.error("\u63D0\u793A:\u5148\u8FD0\u884C npx csspilot init \u63A5\u5165 TestPilot");
30595
- process.exitCode = 1;
30596
- return;
30597
- }
30598
- const cases = [];
30599
- const broken = [];
30600
- for (const file2 of files) {
30601
- const source = await readFile2(file2, "utf8");
30602
- const parsed = parseCase(source);
30603
- if (parsed.ok) {
30604
- cases.push({ id: parsed.data.id, file: file2, tags: parsed.data.tags ?? [] });
30605
- } else {
30606
- broken.push({ file: file2, message: parsed.issues[0]?.message ?? "\u89E3\u6790\u5931\u8D25" });
30607
- }
30512
+ // ../../packages/sdk/src/project.ts
30513
+ import { mkdir as mkdir2, readFile as readFile2, stat, writeFile as writeFile2 } from "node:fs/promises";
30514
+ import { join as join5, resolve } from "node:path";
30515
+ function resolveRoot(root) {
30516
+ return resolve(root ?? process.cwd());
30517
+ }
30518
+ async function loadProjectConfig(root) {
30519
+ return loadTestpilotConfig(resolveRoot(root));
30520
+ }
30521
+ async function inspectProject(root) {
30522
+ const dir = resolveRoot(root);
30523
+ const config2 = await loadTestpilotConfig(dir);
30524
+ const casesDir = join5(dir, config2.casesDir);
30525
+ const artifactsDir = join5(dir, TESTPILOT_DIR, "artifacts");
30526
+ const [hasConfig, casesDirExists, skillInstalled] = await Promise.all([
30527
+ exists2(join5(dir, "testpilot.yaml")),
30528
+ exists2(casesDir),
30529
+ exists2(join5(dir, ".agents", "skills", "testpilot"))
30530
+ ]);
30531
+ return {
30532
+ root: dir,
30533
+ hasConfig,
30534
+ config: config2,
30535
+ casesDir,
30536
+ casesDirExists,
30537
+ artifactsDir,
30538
+ runsDir: join5(artifactsDir, "runs"),
30539
+ skillInstalled,
30540
+ targets: config2.miniapp ? ["web", "miniapp"] : ["web"]
30541
+ };
30542
+ }
30543
+ async function exists2(path) {
30544
+ return await stat(path).catch(() => void 0) !== void 0;
30545
+ }
30546
+ var PROJECT_LINK_FILE = join5(TESTPILOT_DIR, "project.json");
30547
+ async function readProjectLink(root) {
30548
+ const file2 = join5(resolveRoot(root), PROJECT_LINK_FILE);
30549
+ try {
30550
+ const raw = JSON.parse(await readFile2(file2, "utf8"));
30551
+ if (!raw.projectId) return void 0;
30552
+ return raw;
30553
+ } catch {
30554
+ return void 0;
30555
+ }
30556
+ }
30557
+ async function writeProjectLink(link, root) {
30558
+ const file2 = join5(resolveRoot(root), PROJECT_LINK_FILE);
30559
+ await mkdir2(join5(file2, ".."), { recursive: true });
30560
+ await writeFile2(file2, `${JSON.stringify(link, null, 2)}
30561
+ `, "utf8");
30562
+ return file2;
30563
+ }
30564
+
30565
+ // ../../packages/sdk/src/cases.ts
30566
+ async function collectCaseFiles(paths) {
30567
+ const files = [];
30568
+ for (const input2 of paths) {
30569
+ const info = await stat2(input2).catch(() => void 0);
30570
+ if (!info) continue;
30571
+ if (info.isDirectory()) {
30572
+ files.push(...await collectFromDir(input2));
30573
+ } else if (isCaseFile(input2)) {
30574
+ files.push(input2);
30608
30575
  }
30609
- const visible = tag ? cases.filter((item) => item.tags.includes(tag)) : cases;
30610
- console.log("TestPilot Cases");
30611
- console.log("");
30612
- for (const item of visible) {
30613
- const tagLabel = item.tags.length > 0 ? ` [${item.tags.join(", ")}]` : "";
30614
- console.log(`\u2713 ${item.id}${tagLabel}`);
30615
- console.log(` ${item.file}`);
30616
- console.log("");
30576
+ }
30577
+ return [...new Set(files)].sort();
30578
+ }
30579
+ async function collectFromDir(dir) {
30580
+ const entries = await readdir(dir, { withFileTypes: true });
30581
+ const found = [];
30582
+ for (const entry of entries) {
30583
+ const full = join6(dir, entry.name);
30584
+ if (entry.isDirectory()) {
30585
+ found.push(...await collectFromDir(full));
30586
+ } else if (isCaseFile(entry.name)) {
30587
+ found.push(full);
30617
30588
  }
30618
- for (const item of broken) {
30619
- console.log(`\u2717 ${item.file}`);
30620
- console.log(` ${item.message}`);
30621
- console.log("");
30589
+ }
30590
+ return found;
30591
+ }
30592
+ function isCaseFile(file2) {
30593
+ const ext = extname(file2).toLowerCase();
30594
+ return ext === ".yaml" || ext === ".yml";
30595
+ }
30596
+ function resolveAgainstRoot(root, file2) {
30597
+ return isAbsolute(file2) ? file2 : resolve2(resolveRoot(root), file2);
30598
+ }
30599
+ async function collectCases(paths, options = {}) {
30600
+ const files = await collectCaseFiles(
30601
+ paths.map((input2) => resolveAgainstRoot(options.root, input2))
30602
+ );
30603
+ const infos = [];
30604
+ for (const file2 of files) {
30605
+ const validation = await validateCaseFile(file2);
30606
+ infos.push({
30607
+ file: file2,
30608
+ valid: validation.ok,
30609
+ case: validation.data,
30610
+ issues: validation.issues
30611
+ });
30612
+ }
30613
+ return infos;
30614
+ }
30615
+ async function readCaseFile(file2, options = {}) {
30616
+ const target = resolveAgainstRoot(options.root, file2);
30617
+ const source = await readFile3(target, "utf8");
30618
+ return { file: target, source, validation: validateCaseSource(source) };
30619
+ }
30620
+ async function validateCaseFile(file2) {
30621
+ const source = await readFile3(file2, "utf8");
30622
+ return validateCaseSource(source);
30623
+ }
30624
+ async function writeCaseFile(file2, source, options = {}) {
30625
+ const target = resolveAgainstRoot(options.root, file2);
30626
+ const validation = validateCaseSource(source);
30627
+ if (!validation.ok) {
30628
+ return { file: target, written: false, validation };
30629
+ }
30630
+ if (!options.overwrite) {
30631
+ const existing = await stat2(target).catch(() => void 0);
30632
+ if (existing) {
30633
+ throw new Error(`\u7528\u4F8B\u6587\u4EF6\u5DF2\u5B58\u5728:${target}(\u8986\u76D6\u9700\u663E\u5F0F overwrite: true)`);
30622
30634
  }
30623
- const brokenNote = broken.length > 0 ? `,${broken.length} \u4E2A\u6587\u4EF6\u89E3\u6790\u5931\u8D25` : "";
30624
- console.log(`\u5171 ${visible.length} \u4E2A\u7528\u4F8B${tag ? `(tag: ${tag})` : ""}${brokenNote}`);
30625
- });
30635
+ }
30636
+ await mkdir3(dirname(target), { recursive: true });
30637
+ await writeFile3(target, source, "utf8");
30638
+ return { file: target, written: true, validation };
30626
30639
  }
30627
30640
 
30628
- // src/commands/report.ts
30629
- import { exec } from "node:child_process";
30630
- import { readFile as readFile3, readdir as readdir2 } from "node:fs/promises";
30631
- import { join as join8 } from "node:path";
30641
+ // ../../packages/sdk/src/reports.ts
30642
+ import { readFile as readFile6 } from "node:fs/promises";
30643
+ import { join as join16 } from "node:path";
30632
30644
 
30633
30645
  // ../../packages/reporter/src/report.ts
30634
- import { writeFile as writeFile2 } from "node:fs/promises";
30635
- import { join as join7, resolve } from "node:path";
30646
+ import { writeFile as writeFile4 } from "node:fs/promises";
30647
+ import { join as join7, resolve as resolve3 } from "node:path";
30636
30648
 
30637
30649
  // ../../packages/reporter/src/summary.ts
30638
30650
  function toSummaryView(summary) {
@@ -30652,12 +30664,12 @@ function toSummaryView(summary) {
30652
30664
 
30653
30665
  // ../../packages/reporter/src/report.ts
30654
30666
  async function writeReports(runDir, summary) {
30655
- const dir = resolve(runDir);
30667
+ const dir = resolve3(runDir);
30656
30668
  const jsonPath = join7(dir, "report.json");
30657
30669
  const htmlPath = join7(dir, "report.html");
30658
30670
  const payload = { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), summary };
30659
- await writeFile2(jsonPath, JSON.stringify(payload, null, 2), "utf8");
30660
- await writeFile2(htmlPath, renderHtml(summary), "utf8");
30671
+ await writeFile4(jsonPath, JSON.stringify(payload, null, 2), "utf8");
30672
+ await writeFile4(htmlPath, renderHtml(summary), "utf8");
30661
30673
  return { json: jsonPath, html: htmlPath };
30662
30674
  }
30663
30675
  function renderHtml(summary) {
@@ -30726,91 +30738,549 @@ function escapeHtml(input2) {
30726
30738
  return input2.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
30727
30739
  }
30728
30740
 
30729
- // src/commands/report.ts
30730
- function makeReportCommand() {
30731
- return new Command("report").description("\u751F\u6210\u6D4B\u8BD5\u62A5\u544A(JSON + HTML)").option("--run <id>", "\u8FD0\u884C\u8BB0\u5F55 ID,\u9ED8\u8BA4\u6700\u8FD1\u4E00\u6B21").option("--open", "\u751F\u6210\u540E\u5728\u6D4F\u89C8\u5668\u6253\u5F00", false).action(async (options) => {
30732
- const runId = options.run ?? await latestRunId();
30733
- if (!runId) {
30734
- console.error(`\u672A\u627E\u5230\u8FD0\u884C\u8BB0\u5F55(${RUNS_DIR} \u4E3A\u7A7A,\u5148\u8FD0\u884C npx csspilot run)`);
30735
- process.exitCode = 1;
30736
- return;
30737
- }
30738
- const runDir = join8(RUNS_DIR, runId);
30739
- let summary;
30740
- try {
30741
- summary = JSON.parse(await readFile3(join8(runDir, RESULT_FILE), "utf8"));
30742
- } catch {
30743
- console.error(`\u8BFB\u53D6\u8FD0\u884C\u7ED3\u679C\u5931\u8D25:${join8(runDir, RESULT_FILE)}`);
30744
- process.exitCode = 1;
30745
- return;
30746
- }
30747
- const written = await writeReports(runDir, summary);
30748
- console.log(`\u2713 report.json ${written.json}`);
30749
- console.log(`\u2713 report.html ${written.html}`);
30750
- if (options.open) await openInBrowser(written.html);
30751
- });
30752
- }
30753
- async function latestRunId() {
30754
- const entries = await readdir2(RUNS_DIR).catch(() => []);
30755
- return entries.sort().at(-1);
30756
- }
30757
- function openInBrowser(file2) {
30758
- const command = process.platform === "win32" ? `start "" "${file2}"` : process.platform === "darwin" ? `open "${file2}"` : `xdg-open "${file2}"`;
30759
- return new Promise((resolvePromise) => {
30760
- exec(command, () => resolvePromise());
30761
- });
30762
- }
30741
+ // ../../packages/sdk/src/runs.ts
30742
+ import { readFile as readFile5, readdir as readdir3 } from "node:fs/promises";
30743
+ import { isAbsolute as isAbsolute2, join as join15 } from "node:path";
30763
30744
 
30764
- // src/commands/run.ts
30765
- import { readFile as readFile5 } from "node:fs/promises";
30766
- import { join as join16 } from "node:path";
30745
+ // ../../packages/execution-engine/src/artifacts/index.ts
30746
+ import { mkdir as mkdir8 } from "node:fs/promises";
30747
+ import { join as join11 } from "node:path";
30767
30748
 
30768
- // ../../packages/adapter-playwright/src/browser.ts
30769
- import { chromium } from "playwright";
30770
- function resolveHeadless(options) {
30771
- if (options.headless !== void 0) return options.headless;
30772
- return process.env.TESTPILOT_HEADLESS !== "false";
30773
- }
30774
- function stepTimeout(options) {
30775
- const fromEnv = Number(process.env.TESTPILOT_STEP_TIMEOUT);
30776
- if (options.timeout !== void 0) return options.timeout;
30777
- return Number.isFinite(fromEnv) && fromEnv > 0 ? fromEnv : DEFAULT_STEP_TIMEOUT_MS;
30778
- }
30779
- async function launchBrowser(options = {}) {
30780
- const browser = await chromium.launch({ headless: resolveHeadless(options) });
30781
- return { browser };
30782
- }
30783
- async function openCaseContext(bundle, options, evidenceTmpDir) {
30784
- const context = await bundle.browser.newContext({
30785
- ...evidenceTmpDir ? { recordVideo: { dir: evidenceTmpDir } } : {}
30786
- });
30787
- context.setDefaultTimeout(stepTimeout(options));
30788
- if (evidenceTmpDir) {
30789
- await context.tracing.start({ screenshots: true, snapshots: true });
30749
+ // ../../packages/evidence/src/log.ts
30750
+ import { appendFile, mkdir as mkdir4 } from "node:fs/promises";
30751
+ import { dirname as dirname2 } from "node:path";
30752
+ var RunLogger = class {
30753
+ constructor(filePath) {
30754
+ this.filePath = filePath;
30790
30755
  }
30791
- const page = await context.newPage();
30792
- return { context, page };
30793
- }
30794
-
30795
- // ../../packages/adapter-playwright/src/page.ts
30796
- import { existsSync as existsSync4 } from "node:fs";
30797
- import { mkdtemp, readFile as readFile4, rm } from "node:fs/promises";
30798
- import { tmpdir } from "node:os";
30799
- import { join as join9 } from "node:path";
30800
- var PlaywrightAdapter = class {
30801
- constructor(bundle, options = {}) {
30802
- this.bundle = bundle;
30803
- this.options = options;
30756
+ info(message) {
30757
+ return this.write("INFO", message);
30804
30758
  }
30805
- target = "web";
30806
- current;
30759
+ warn(message) {
30760
+ return this.write("WARN", message);
30761
+ }
30762
+ error(message) {
30763
+ return this.write("ERROR", message);
30764
+ }
30765
+ async write(level, message) {
30766
+ await mkdir4(dirname2(this.filePath), { recursive: true });
30767
+ const line = `${(/* @__PURE__ */ new Date()).toISOString()} ${level} ${message}
30768
+ `;
30769
+ await appendFile(this.filePath, line, "utf8");
30770
+ }
30771
+ };
30772
+
30773
+ // ../../packages/evidence/src/screenshot.ts
30774
+ import { mkdir as mkdir5, writeFile as writeFile5 } from "node:fs/promises";
30775
+ import { dirname as dirname3, join as join8 } from "node:path";
30776
+ async function saveScreenshot(dir, fileName, data) {
30777
+ const file2 = join8(dir, fileName);
30778
+ await mkdir5(dirname3(file2), { recursive: true });
30779
+ await writeFile5(file2, data);
30780
+ return file2;
30781
+ }
30782
+
30783
+ // ../../packages/evidence/src/trace.ts
30784
+ import { mkdir as mkdir6, writeFile as writeFile6 } from "node:fs/promises";
30785
+ import { dirname as dirname4, join as join9 } from "node:path";
30786
+ async function saveTrace(dir, fileName, data) {
30787
+ const file2 = join9(dir, fileName);
30788
+ await mkdir6(dirname4(file2), { recursive: true });
30789
+ await writeFile6(file2, data);
30790
+ return file2;
30791
+ }
30792
+
30793
+ // ../../packages/evidence/src/video.ts
30794
+ import { mkdir as mkdir7, writeFile as writeFile7 } from "node:fs/promises";
30795
+ import { dirname as dirname5, join as join10 } from "node:path";
30796
+ async function saveVideo(dir, fileName, data) {
30797
+ const file2 = join10(dir, fileName);
30798
+ await mkdir7(dirname5(file2), { recursive: true });
30799
+ await writeFile7(file2, data);
30800
+ return file2;
30801
+ }
30802
+
30803
+ // ../../packages/execution-engine/src/artifacts/index.ts
30804
+ var ArtifactManager = class {
30805
+ constructor(runDir) {
30806
+ this.runDir = runDir;
30807
+ this.screenshotsDir = join11(runDir, "screenshots");
30808
+ this.videosDir = join11(runDir, "videos");
30809
+ this.tracesDir = join11(runDir, "traces");
30810
+ this.logsDir = join11(runDir, "logs");
30811
+ this.logger = new RunLogger(join11(this.logsDir, "run.log"));
30812
+ }
30813
+ screenshotsDir;
30814
+ videosDir;
30815
+ tracesDir;
30816
+ logsDir;
30817
+ logger;
30818
+ async prepare() {
30819
+ await mkdir8(this.screenshotsDir, { recursive: true });
30820
+ await mkdir8(this.logsDir, { recursive: true });
30821
+ }
30822
+ /** 保存步骤截图,返回相对 run 目录的路径(screenshots/xxx.png,统一正斜杠供报告链接使用) */
30823
+ async saveStepScreenshot(fileName, data) {
30824
+ await saveScreenshot(this.screenshotsDir, fileName, data);
30825
+ return `screenshots/${fileName}`;
30826
+ }
30827
+ /** 保存用例录屏,返回相对 run 目录的路径(videos/xxx.webm) */
30828
+ async saveVideo(fileName, data) {
30829
+ await saveVideo(this.videosDir, fileName, data);
30830
+ return `videos/${fileName}`;
30831
+ }
30832
+ /** 保存用例 trace 包,返回相对 run 目录的路径(traces/xxx.zip) */
30833
+ async saveTrace(fileName, data) {
30834
+ await saveTrace(this.tracesDir, fileName, data);
30835
+ return `traces/${fileName}`;
30836
+ }
30837
+ };
30838
+
30839
+ // ../../packages/execution-engine/src/context/index.ts
30840
+ var ExecutionContext = class {
30841
+ variables = /* @__PURE__ */ new Map();
30842
+ set(name, value) {
30843
+ this.variables.set(name, value);
30844
+ }
30845
+ get(name) {
30846
+ return this.variables.get(name);
30847
+ }
30848
+ /** 将字符串中的 ${name} 替换为已提取的变量值;未定义的引用保持原样(校验阶段已拦截) */
30849
+ resolve(template) {
30850
+ return template.replace(
30851
+ /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g,
30852
+ (match, name) => this.variables.get(name) ?? match
30853
+ );
30854
+ }
30855
+ };
30856
+
30857
+ // ../../packages/execution-engine/src/lifecycle/index.ts
30858
+ import { mkdir as mkdir9, readdir as readdir2 } from "node:fs/promises";
30859
+ import { join as join12 } from "node:path";
30860
+ function dateStamp(date5 = /* @__PURE__ */ new Date()) {
30861
+ const year = date5.getFullYear();
30862
+ const month = String(date5.getMonth() + 1).padStart(2, "0");
30863
+ const day = String(date5.getDate()).padStart(2, "0");
30864
+ return `${year}${month}${day}`;
30865
+ }
30866
+ async function allocateRunId(runsRoot2) {
30867
+ const stamp = dateStamp();
30868
+ const existing = await readdir2(runsRoot2).catch(() => []);
30869
+ let max = 0;
30870
+ for (const name of existing) {
30871
+ const match = /^(\d{8})-(\d{3})$/.exec(name);
30872
+ if (match && match[1] === stamp) {
30873
+ max = Math.max(max, Number(match[2]));
30874
+ }
30875
+ }
30876
+ return `${stamp}-${String(max + 1).padStart(3, "0")}`;
30877
+ }
30878
+ async function ensureRunDir(runsRoot2, runId) {
30879
+ const runDir = join12(runsRoot2, runId);
30880
+ await mkdir9(runDir, { recursive: true });
30881
+ return runDir;
30882
+ }
30883
+
30884
+ // ../../packages/execution-engine/src/events/index.ts
30885
+ import { appendFile as appendFile2, mkdir as mkdir10 } from "node:fs/promises";
30886
+ import { dirname as dirname6 } from "node:path";
30887
+ var EventBus = class {
30888
+ subscribers = [];
30889
+ subscribe(subscriber) {
30890
+ this.subscribers.push(subscriber);
30891
+ }
30892
+ async emit(event) {
30893
+ for (const subscriber of this.subscribers) {
30894
+ await subscriber(event);
30895
+ }
30896
+ }
30897
+ };
30898
+ function ndjsonSubscriber(filePath) {
30899
+ return async (event) => {
30900
+ await mkdir10(dirname6(filePath), { recursive: true });
30901
+ await appendFile2(filePath, `${JSON.stringify(event)}
30902
+ `, "utf8");
30903
+ };
30904
+ }
30905
+
30906
+ // ../../packages/adapter-core/src/session.ts
30907
+ var AdapterSession = class {
30908
+ constructor(target, factory) {
30909
+ this.target = target;
30910
+ this.factory = factory;
30911
+ }
30912
+ adapter;
30913
+ get opened() {
30914
+ return this.adapter !== void 0;
30915
+ }
30916
+ /** 惰性创建;已创建则复用 */
30917
+ async get() {
30918
+ if (!this.adapter) {
30919
+ this.adapter = await this.factory.create();
30920
+ await this.adapter.launch();
30921
+ }
30922
+ return this.adapter;
30923
+ }
30924
+ async close() {
30925
+ if (this.adapter) {
30926
+ await this.adapter.close().catch(() => void 0);
30927
+ this.adapter = void 0;
30928
+ }
30929
+ }
30930
+ };
30931
+
30932
+ // ../../packages/execution-engine/src/engine/adapter-resolver.ts
30933
+ var AdapterResolver = class {
30934
+ factories = /* @__PURE__ */ new Map();
30935
+ sessions = /* @__PURE__ */ new Map();
30936
+ register(factory) {
30937
+ this.factories.set(factory.target, factory);
30938
+ }
30939
+ /** 取得(或创建)target 的会话 */
30940
+ async session(target) {
30941
+ let session = this.sessions.get(target);
30942
+ if (!session) {
30943
+ const factory = this.factories.get(target);
30944
+ if (!factory) {
30945
+ throw new TestPilotError("ADAPTER_NOT_FOUND", `\u672A\u6CE8\u518C target "${target}" \u7684 adapter,\u8BE5\u7AEF\u6B65\u9AA4\u65E0\u6CD5\u6267\u884C`);
30946
+ }
30947
+ session = new AdapterSession(target, factory);
30948
+ this.sessions.set(target, session);
30949
+ }
30950
+ return session;
30951
+ }
30952
+ async closeAll() {
30953
+ for (const session of this.sessions.values()) {
30954
+ await session.close();
30955
+ }
30956
+ this.sessions.clear();
30957
+ }
30958
+ };
30959
+
30960
+ // ../../packages/execution-engine/src/engine/step-executor.ts
30961
+ async function executeStep(step, index, adapter, context, artifacts, options) {
30962
+ const startedAt = Date.now();
30963
+ const result = {
30964
+ index,
30965
+ target: step.target,
30966
+ action: step.action,
30967
+ status: "passed",
30968
+ durationMs: 0
30969
+ };
30970
+ try {
30971
+ const locator = step.locator ? {
30972
+ text: step.locator.text !== void 0 ? context.resolve(step.locator.text) : void 0,
30973
+ css: step.locator.css !== void 0 ? context.resolve(step.locator.css) : void 0
30974
+ } : void 0;
30975
+ switch (step.action) {
30976
+ case "launch":
30977
+ await adapter.launch();
30978
+ break;
30979
+ case "navigate":
30980
+ await adapter.navigate(context.resolve(step.url ?? ""));
30981
+ break;
30982
+ case "click":
30983
+ await adapter.click(locator);
30984
+ break;
30985
+ case "input":
30986
+ await adapter.input(locator, context.resolve(step.value ?? ""));
30987
+ break;
30988
+ case "select":
30989
+ await adapter.select(locator, context.resolve(step.value ?? ""));
30990
+ break;
30991
+ case "wait":
30992
+ await adapter.wait(locator, step.timeout);
30993
+ break;
30994
+ case "assert":
30995
+ await adapter.assert(locator, context.resolve(step.expected ?? ""));
30996
+ break;
30997
+ case "extract": {
30998
+ const value = await adapter.extract(locator);
30999
+ if (step.variable) {
31000
+ context.set(step.variable, value);
31001
+ result.extracted = { [step.variable]: value };
31002
+ }
31003
+ break;
31004
+ }
31005
+ case "screenshot": {
31006
+ const data = await adapter.screenshot();
31007
+ result.screenshot = await artifacts.saveStepScreenshot(`${options.screenshotName}.png`, data);
31008
+ break;
31009
+ }
31010
+ default: {
31011
+ const exhaustive = step.action;
31012
+ throw new Error(`\u4E0D\u652F\u6301\u7684 action:${String(exhaustive)}`);
31013
+ }
31014
+ }
31015
+ } catch (err) {
31016
+ result.status = "failed";
31017
+ result.error = err instanceof Error ? err.message : String(err);
31018
+ try {
31019
+ const data = await adapter.screenshot();
31020
+ result.screenshot = await artifacts.saveStepScreenshot(`${options.screenshotName}-failed.png`, data);
31021
+ } catch {
31022
+ }
31023
+ }
31024
+ result.durationMs = Date.now() - startedAt;
31025
+ return result;
31026
+ }
31027
+
31028
+ // ../../packages/execution-engine/src/engine/execution-engine.ts
31029
+ var ExecutionEngine = class {
31030
+ async runCase(data, options) {
31031
+ const { runId, file: file2, artifacts, bus, resolver } = options;
31032
+ const startedAt = /* @__PURE__ */ new Date();
31033
+ const context = new ExecutionContext();
31034
+ const steps = [];
31035
+ const evidenceAdapters = /* @__PURE__ */ new Map();
31036
+ let caseError;
31037
+ await bus.emit({ type: "case-started", runId, caseId: data.id, file: file2, ts: startedAt.toISOString() });
31038
+ await artifacts.logger.info(`case ${data.id} started (${file2})`);
31039
+ for (const [index, step] of data.steps.entries()) {
31040
+ await bus.emit({
31041
+ type: "step-started",
31042
+ runId,
31043
+ caseId: data.id,
31044
+ index,
31045
+ target: step.target,
31046
+ action: step.action,
31047
+ ts: (/* @__PURE__ */ new Date()).toISOString()
31048
+ });
31049
+ let result;
31050
+ try {
31051
+ const adapter = await (await resolver.session(step.target)).get();
31052
+ if (adapter.startEvidence && !evidenceAdapters.has(step.target)) {
31053
+ try {
31054
+ await adapter.startEvidence(data.id);
31055
+ evidenceAdapters.set(step.target, adapter);
31056
+ } catch (err) {
31057
+ await artifacts.logger.info(
31058
+ `evidence start failed for ${step.target}: ${err instanceof Error ? err.message : String(err)}`
31059
+ );
31060
+ }
31061
+ }
31062
+ result = await executeStep(step, index, adapter, context, artifacts, {
31063
+ screenshotName: `${data.id}-step-${String(index).padStart(2, "0")}`
31064
+ });
31065
+ } catch (err) {
31066
+ result = {
31067
+ index,
31068
+ target: step.target,
31069
+ action: step.action,
31070
+ status: "failed",
31071
+ durationMs: 0,
31072
+ error: err instanceof Error ? err.message : String(err)
31073
+ };
31074
+ }
31075
+ steps.push(result);
31076
+ await artifacts.logger.info(`step ${index} ${step.action} -> ${result.status}${result.error ? ` - ${result.error}` : ""}`);
31077
+ await bus.emit({
31078
+ type: "step-finished",
31079
+ runId,
31080
+ caseId: data.id,
31081
+ index,
31082
+ target: step.target,
31083
+ action: step.action,
31084
+ status: result.status,
31085
+ durationMs: result.durationMs,
31086
+ error: result.error,
31087
+ ts: (/* @__PURE__ */ new Date()).toISOString()
31088
+ });
31089
+ if (result.status === "failed") {
31090
+ caseError = result.error;
31091
+ for (const [skipIndex, skippedStep] of data.steps.entries()) {
31092
+ if (skipIndex <= index) continue;
31093
+ steps.push({
31094
+ index: skipIndex,
31095
+ target: skippedStep.target,
31096
+ action: skippedStep.action,
31097
+ status: "skipped",
31098
+ durationMs: 0
31099
+ });
31100
+ }
31101
+ break;
31102
+ }
31103
+ }
31104
+ const finishedAt = /* @__PURE__ */ new Date();
31105
+ const status = caseError ? "failed" : "passed";
31106
+ const caseResult = {
31107
+ caseId: data.id,
31108
+ caseName: data.name,
31109
+ file: file2,
31110
+ status,
31111
+ steps,
31112
+ startedAt: startedAt.toISOString(),
31113
+ finishedAt: finishedAt.toISOString(),
31114
+ durationMs: finishedAt.getTime() - startedAt.getTime()
31115
+ };
31116
+ if (caseError) caseResult.error = caseError;
31117
+ for (const [target, adapter] of evidenceAdapters) {
31118
+ if (!adapter.stopEvidence) continue;
31119
+ try {
31120
+ const evidence = await adapter.stopEvidence(data.id);
31121
+ if (evidence.video) {
31122
+ caseResult.video = await artifacts.saveVideo(`${data.id}-${target}.webm`, evidence.video);
31123
+ }
31124
+ if (evidence.trace) {
31125
+ caseResult.trace = await artifacts.saveTrace(`${data.id}-${target}.zip`, evidence.trace);
31126
+ }
31127
+ } catch (err) {
31128
+ await artifacts.logger.info(
31129
+ `evidence stop failed for ${target}: ${err instanceof Error ? err.message : String(err)}`
31130
+ );
31131
+ }
31132
+ }
31133
+ await bus.emit({
31134
+ type: "case-finished",
31135
+ runId,
31136
+ caseId: data.id,
31137
+ status,
31138
+ durationMs: finishedAt.getTime() - startedAt.getTime(),
31139
+ ts: finishedAt.toISOString()
31140
+ });
31141
+ await artifacts.logger.info(`case ${data.id} ${status}`);
31142
+ return caseResult;
31143
+ }
31144
+ };
31145
+
31146
+ // ../../packages/execution-engine/src/engine/result-collector.ts
31147
+ function buildRunSummary(runId, startedAt, cases, options = {}) {
31148
+ const finishedAt = /* @__PURE__ */ new Date();
31149
+ const totals = {
31150
+ cases: cases.length,
31151
+ passed: cases.filter((item) => item.status === "passed").length,
31152
+ failed: cases.filter((item) => item.status === "failed").length,
31153
+ steps: 0,
31154
+ stepsPassed: 0,
31155
+ stepsFailed: 0,
31156
+ stepsSkipped: 0
31157
+ };
31158
+ for (const item of cases) {
31159
+ for (const step of item.steps) {
31160
+ totals.steps++;
31161
+ if (step.status === "passed") totals.stepsPassed++;
31162
+ else if (step.status === "failed") totals.stepsFailed++;
31163
+ else totals.stepsSkipped++;
31164
+ }
31165
+ }
31166
+ return {
31167
+ runId,
31168
+ startedAt: startedAt.toISOString(),
31169
+ finishedAt: finishedAt.toISOString(),
31170
+ durationMs: finishedAt.getTime() - startedAt.getTime(),
31171
+ status: totals.failed > 0 ? "failed" : "passed",
31172
+ ...options.cancelled ? { cancelled: true } : {},
31173
+ cases,
31174
+ totals
31175
+ };
31176
+ }
31177
+
31178
+ // ../../packages/execution-engine/src/engine/test-runner.ts
31179
+ import { writeFile as writeFile8 } from "node:fs/promises";
31180
+ import { join as join13, resolve as resolve4 } from "node:path";
31181
+ var TestRunner = class {
31182
+ resolver;
31183
+ runsRoot;
31184
+ customRunId;
31185
+ extraSubscriber;
31186
+ signal;
31187
+ constructor(options) {
31188
+ this.resolver = options.resolver;
31189
+ this.runsRoot = resolve4(options.runsRoot ?? join13(process.cwd(), ".testpilot", "artifacts", "runs"));
31190
+ this.customRunId = options.runId;
31191
+ this.extraSubscriber = options.onEvent;
31192
+ this.signal = options.signal;
31193
+ }
31194
+ async run(cases) {
31195
+ const startedAt = /* @__PURE__ */ new Date();
31196
+ const runId = this.customRunId ?? await allocateRunId(this.runsRoot);
31197
+ const runDir = await ensureRunDir(this.runsRoot, runId);
31198
+ const artifacts = new ArtifactManager(runDir);
31199
+ await artifacts.prepare();
31200
+ const bus = new EventBus();
31201
+ bus.subscribe(ndjsonSubscriber(join13(runDir, "events.ndjson")));
31202
+ if (this.extraSubscriber) bus.subscribe(this.extraSubscriber);
31203
+ await bus.emit({ type: "run-started", runId, totalCases: cases.length, ts: startedAt.toISOString() });
31204
+ await artifacts.logger.info(`run ${runId} started with ${cases.length} case(s)`);
31205
+ const engine = new ExecutionEngine();
31206
+ const results = [];
31207
+ for (const item of cases) {
31208
+ if (this.signal?.aborted) {
31209
+ await artifacts.logger.info(`run ${runId} cancelled before case ${item.data.id}`);
31210
+ break;
31211
+ }
31212
+ results.push(
31213
+ await engine.runCase(item.data, {
31214
+ runId,
31215
+ file: item.file,
31216
+ artifacts,
31217
+ bus,
31218
+ resolver: this.resolver
31219
+ })
31220
+ );
31221
+ }
31222
+ const cancelled = this.signal?.aborted === true;
31223
+ await this.resolver.closeAll();
31224
+ const summary = buildRunSummary(runId, startedAt, results, { cancelled });
31225
+ await bus.emit({
31226
+ type: "run-finished",
31227
+ runId,
31228
+ status: summary.status,
31229
+ totals: { cases: summary.totals.cases, passed: summary.totals.passed, failed: summary.totals.failed },
31230
+ ts: (/* @__PURE__ */ new Date()).toISOString()
31231
+ });
31232
+ await artifacts.logger.info(`run ${runId} finished: ${summary.status}`);
31233
+ await writeFile8(join13(runDir, RESULT_FILE), JSON.stringify(summary, null, 2), "utf8");
31234
+ return summary;
31235
+ }
31236
+ };
31237
+
31238
+ // ../../packages/adapter-playwright/src/browser.ts
31239
+ import { chromium } from "playwright";
31240
+ function resolveHeadless(options) {
31241
+ if (options.headless !== void 0) return options.headless;
31242
+ return process.env.TESTPILOT_HEADLESS !== "false";
31243
+ }
31244
+ function stepTimeout(options) {
31245
+ const fromEnv = Number(process.env.TESTPILOT_STEP_TIMEOUT);
31246
+ if (options.timeout !== void 0) return options.timeout;
31247
+ return Number.isFinite(fromEnv) && fromEnv > 0 ? fromEnv : DEFAULT_STEP_TIMEOUT_MS;
31248
+ }
31249
+ async function launchBrowser(options = {}) {
31250
+ const browser = await chromium.launch({ headless: resolveHeadless(options) });
31251
+ return { browser };
31252
+ }
31253
+ async function openCaseContext(bundle, options, evidenceTmpDir) {
31254
+ const context = await bundle.browser.newContext({
31255
+ ...evidenceTmpDir ? { recordVideo: { dir: evidenceTmpDir } } : {}
31256
+ });
31257
+ context.setDefaultTimeout(stepTimeout(options));
31258
+ if (evidenceTmpDir) {
31259
+ await context.tracing.start({ screenshots: true, snapshots: true });
31260
+ }
31261
+ const page = await context.newPage();
31262
+ return { context, page };
31263
+ }
31264
+
31265
+ // ../../packages/adapter-playwright/src/page.ts
31266
+ import { existsSync as existsSync2 } from "node:fs";
31267
+ import { mkdtemp, readFile as readFile4, rm } from "node:fs/promises";
31268
+ import { tmpdir } from "node:os";
31269
+ import { join as join14 } from "node:path";
31270
+ var PlaywrightAdapter = class {
31271
+ constructor(bundle, options = {}) {
31272
+ this.bundle = bundle;
31273
+ this.options = options;
31274
+ }
31275
+ target = "web";
31276
+ current;
30807
31277
  /** 浏览器在 create() 时已启动;context/page 按用例惰性创建,launch 保持幂等 */
30808
31278
  async launch() {
30809
31279
  }
30810
31280
  /** 开始用例级取证:为本用例创建独立 context(录屏 + trace) */
30811
31281
  async startEvidence(_caseId) {
30812
31282
  await this.closeContextQuietly();
30813
- const evidenceTmpDir = await mkdtemp(join9(tmpdir(), "testpilot-pw-"));
31283
+ const evidenceTmpDir = await mkdtemp(join14(tmpdir(), "testpilot-pw-"));
30814
31284
  this.current = {
30815
31285
  ...await openCaseContext(this.bundle, this.options, evidenceTmpDir),
30816
31286
  evidenceTmpDir
@@ -30821,12 +31291,12 @@ var PlaywrightAdapter = class {
30821
31291
  if (!this.current) return {};
30822
31292
  const { context, page, evidenceTmpDir } = this.current;
30823
31293
  this.current = void 0;
30824
- const traceZipPath = join9(evidenceTmpDir ?? tmpdir(), "trace.zip");
31294
+ const traceZipPath = join14(evidenceTmpDir ?? tmpdir(), "trace.zip");
30825
31295
  await context.tracing.stop({ path: traceZipPath }).catch(() => void 0);
30826
31296
  const recordedVideoPath = await page.video()?.path().catch(() => void 0);
30827
31297
  await context.close().catch(() => void 0);
30828
- const video = recordedVideoPath && existsSync4(recordedVideoPath) ? await readFile4(recordedVideoPath) : void 0;
30829
- const trace = evidenceTmpDir && existsSync4(traceZipPath) ? await readFile4(traceZipPath) : void 0;
31298
+ const video = recordedVideoPath && existsSync2(recordedVideoPath) ? await readFile4(recordedVideoPath) : void 0;
31299
+ const trace = evidenceTmpDir && existsSync2(traceZipPath) ? await readFile4(traceZipPath) : void 0;
30830
31300
  if (evidenceTmpDir) await rm(evidenceTmpDir, { recursive: true, force: true }).catch(() => void 0);
30831
31301
  return { video, trace };
30832
31302
  }
@@ -30930,7 +31400,7 @@ var automatorLauncher = {
30930
31400
  }
30931
31401
  };
30932
31402
  function delay(ms) {
30933
- return new Promise((resolve3) => setTimeout(resolve3, ms));
31403
+ return new Promise((resolve5) => setTimeout(resolve5, ms));
30934
31404
  }
30935
31405
  async function connectWithRetry(wsEndpoint, timeoutMs) {
30936
31406
  const startedAt = Date.now();
@@ -31111,11 +31581,11 @@ var MiniAppAdapter = class {
31111
31581
  };
31112
31582
 
31113
31583
  // ../../packages/adapter-wechatide/src/project.ts
31114
- import { existsSync as existsSync5 } from "node:fs";
31584
+ import { existsSync as existsSync3 } from "node:fs";
31115
31585
  async function resolveMiniappConfig(input2) {
31116
31586
  const projectPath = input2.projectPath ?? process.env.TESTPILOT_MINIPROGRAM_PROJECT_PATH;
31117
31587
  if (!projectPath) return void 0;
31118
- if (!existsSync5(projectPath)) {
31588
+ if (!existsSync3(projectPath)) {
31119
31589
  throw new Error(`\u5C0F\u7A0B\u5E8F\u9879\u76EE\u8DEF\u5F84\u4E0D\u5B58\u5728:${projectPath}`);
31120
31590
  }
31121
31591
  const cliPath = input2.cliPath ?? process.env.WECHAT_DEVTOOLS_CLI ?? await findWechatDevToolsCli();
@@ -31139,564 +31609,860 @@ function wechatideAdapterFactory(input2 = {}) {
31139
31609
  }
31140
31610
  return new MiniAppAdapter(config2);
31141
31611
  }
31142
- };
31143
- }
31144
-
31145
- // ../../packages/execution-engine/src/artifacts/index.ts
31146
- import { mkdir as mkdir6 } from "node:fs/promises";
31147
- import { join as join13 } from "node:path";
31148
-
31149
- // ../../packages/evidence/src/log.ts
31150
- import { appendFile, mkdir as mkdir2 } from "node:fs/promises";
31151
- import { dirname as dirname2 } from "node:path";
31152
- var RunLogger = class {
31153
- constructor(filePath) {
31154
- this.filePath = filePath;
31155
- }
31156
- info(message) {
31157
- return this.write("INFO", message);
31158
- }
31159
- warn(message) {
31160
- return this.write("WARN", message);
31161
- }
31162
- error(message) {
31163
- return this.write("ERROR", message);
31164
- }
31165
- async write(level, message) {
31166
- await mkdir2(dirname2(this.filePath), { recursive: true });
31167
- const line = `${(/* @__PURE__ */ new Date()).toISOString()} ${level} ${message}
31168
- `;
31169
- await appendFile(this.filePath, line, "utf8");
31170
- }
31171
- };
31172
-
31173
- // ../../packages/evidence/src/screenshot.ts
31174
- import { mkdir as mkdir3, writeFile as writeFile3 } from "node:fs/promises";
31175
- import { dirname as dirname3, join as join10 } from "node:path";
31176
- async function saveScreenshot(dir, fileName, data) {
31177
- const file2 = join10(dir, fileName);
31178
- await mkdir3(dirname3(file2), { recursive: true });
31179
- await writeFile3(file2, data);
31180
- return file2;
31181
- }
31182
-
31183
- // ../../packages/evidence/src/trace.ts
31184
- import { mkdir as mkdir4, writeFile as writeFile4 } from "node:fs/promises";
31185
- import { dirname as dirname4, join as join11 } from "node:path";
31186
- async function saveTrace(dir, fileName, data) {
31187
- const file2 = join11(dir, fileName);
31188
- await mkdir4(dirname4(file2), { recursive: true });
31189
- await writeFile4(file2, data);
31190
- return file2;
31191
- }
31192
-
31193
- // ../../packages/evidence/src/video.ts
31194
- import { mkdir as mkdir5, writeFile as writeFile5 } from "node:fs/promises";
31195
- import { dirname as dirname5, join as join12 } from "node:path";
31196
- async function saveVideo(dir, fileName, data) {
31197
- const file2 = join12(dir, fileName);
31198
- await mkdir5(dirname5(file2), { recursive: true });
31199
- await writeFile5(file2, data);
31200
- return file2;
31201
- }
31202
-
31203
- // ../../packages/execution-engine/src/artifacts/index.ts
31204
- var ArtifactManager = class {
31205
- constructor(runDir) {
31206
- this.runDir = runDir;
31207
- this.screenshotsDir = join13(runDir, "screenshots");
31208
- this.videosDir = join13(runDir, "videos");
31209
- this.tracesDir = join13(runDir, "traces");
31210
- this.logsDir = join13(runDir, "logs");
31211
- this.logger = new RunLogger(join13(this.logsDir, "run.log"));
31212
- }
31213
- screenshotsDir;
31214
- videosDir;
31215
- tracesDir;
31216
- logsDir;
31217
- logger;
31218
- async prepare() {
31219
- await mkdir6(this.screenshotsDir, { recursive: true });
31220
- await mkdir6(this.logsDir, { recursive: true });
31221
- }
31222
- /** 保存步骤截图,返回相对 run 目录的路径(screenshots/xxx.png,统一正斜杠供报告链接使用) */
31223
- async saveStepScreenshot(fileName, data) {
31224
- await saveScreenshot(this.screenshotsDir, fileName, data);
31225
- return `screenshots/${fileName}`;
31226
- }
31227
- /** 保存用例录屏,返回相对 run 目录的路径(videos/xxx.webm) */
31228
- async saveVideo(fileName, data) {
31229
- await saveVideo(this.videosDir, fileName, data);
31230
- return `videos/${fileName}`;
31231
- }
31232
- /** 保存用例 trace 包,返回相对 run 目录的路径(traces/xxx.zip) */
31233
- async saveTrace(fileName, data) {
31234
- await saveTrace(this.tracesDir, fileName, data);
31235
- return `traces/${fileName}`;
31236
- }
31237
- };
31238
-
31239
- // ../../packages/execution-engine/src/context/index.ts
31240
- var ExecutionContext = class {
31241
- variables = /* @__PURE__ */ new Map();
31242
- set(name, value) {
31243
- this.variables.set(name, value);
31244
- }
31245
- get(name) {
31246
- return this.variables.get(name);
31247
- }
31248
- /** 将字符串中的 ${name} 替换为已提取的变量值;未定义的引用保持原样(校验阶段已拦截) */
31249
- resolve(template) {
31250
- return template.replace(
31251
- /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g,
31252
- (match, name) => this.variables.get(name) ?? match
31253
- );
31254
- }
31255
- };
31256
-
31257
- // ../../packages/execution-engine/src/lifecycle/index.ts
31258
- import { mkdir as mkdir7, readdir as readdir3 } from "node:fs/promises";
31259
- import { join as join14 } from "node:path";
31260
- function dateStamp(date5 = /* @__PURE__ */ new Date()) {
31261
- const year = date5.getFullYear();
31262
- const month = String(date5.getMonth() + 1).padStart(2, "0");
31263
- const day = String(date5.getDate()).padStart(2, "0");
31264
- return `${year}${month}${day}`;
31265
- }
31266
- async function allocateRunId(runsRoot) {
31267
- const stamp = dateStamp();
31268
- const existing = await readdir3(runsRoot).catch(() => []);
31269
- let max = 0;
31270
- for (const name of existing) {
31271
- const match = /^(\d{8})-(\d{3})$/.exec(name);
31272
- if (match && match[1] === stamp) {
31273
- max = Math.max(max, Number(match[2]));
31274
- }
31275
- }
31276
- return `${stamp}-${String(max + 1).padStart(3, "0")}`;
31277
- }
31278
- async function ensureRunDir(runsRoot, runId) {
31279
- const runDir = join14(runsRoot, runId);
31280
- await mkdir7(runDir, { recursive: true });
31281
- return runDir;
31612
+ };
31282
31613
  }
31283
31614
 
31284
- // ../../packages/execution-engine/src/events/index.ts
31285
- import { appendFile as appendFile2, mkdir as mkdir8 } from "node:fs/promises";
31286
- import { dirname as dirname6 } from "node:path";
31287
- var EventBus = class {
31288
- subscribers = [];
31289
- subscribe(subscriber) {
31290
- this.subscribers.push(subscriber);
31291
- }
31292
- async emit(event) {
31293
- for (const subscriber of this.subscribers) {
31294
- await subscriber(event);
31295
- }
31615
+ // ../../packages/sdk/src/runs.ts
31616
+ var RunError = class extends Error {
31617
+ code;
31618
+ constructor(code, message) {
31619
+ super(message);
31620
+ this.name = "RunError";
31621
+ this.code = code;
31296
31622
  }
31297
31623
  };
31298
- function ndjsonSubscriber(filePath) {
31299
- return async (event) => {
31300
- await mkdir8(dirname6(filePath), { recursive: true });
31301
- await appendFile2(filePath, `${JSON.stringify(event)}
31302
- `, "utf8");
31303
- };
31624
+ function createDefaultAdapters(config2) {
31625
+ return [
31626
+ playwrightAdapterFactory({ baseUrl: config2.web?.baseUrl }),
31627
+ wechatideAdapterFactory({
31628
+ projectPath: config2.miniapp?.projectPath,
31629
+ cliPath: config2.miniapp?.cliPath
31630
+ })
31631
+ ];
31632
+ }
31633
+ async function runCases(options = {}) {
31634
+ const root = resolveRoot(options.root);
31635
+ const config2 = await loadProjectConfig(root);
31636
+ const inputs = options.paths && options.paths.length > 0 ? options.paths.map((input2) => isAbsolute2(input2) ? input2 : join15(root, input2)) : [join15(root, config2.casesDir)];
31637
+ const cases = await collectCases(inputs, { root });
31638
+ if (cases.length === 0) {
31639
+ throw new RunError("ENO_CASES", `\u672A\u627E\u5230\u7528\u4F8B\u6587\u4EF6(\u67E5\u627E:${inputs.join(", ")})`);
31640
+ }
31641
+ const invalid = cases.filter((item) => !item.valid);
31642
+ const runnable = cases.filter(
31643
+ (item) => item.valid && item.case && (!options.tag || (item.case.tags ?? []).includes(options.tag))
31644
+ );
31645
+ const tagFiltered = cases.length - invalid.length - runnable.length;
31646
+ if (runnable.length === 0) {
31647
+ throw new RunError(
31648
+ "EINVALID_CASES",
31649
+ invalid.length > 0 ? "\u6240\u6709\u7528\u4F8B\u6821\u9A8C\u5931\u8D25,\u65E0\u53EF\u6267\u884C\u7528\u4F8B" : "\u6CA1\u6709\u5339\u914D\u7684\u7528\u4F8B"
31650
+ );
31651
+ }
31652
+ const factories = options.adapters ?? createDefaultAdapters(config2);
31653
+ const resolver = new AdapterResolver();
31654
+ for (const factory of factories) resolver.register(factory);
31655
+ const runner = new TestRunner({
31656
+ resolver,
31657
+ runsRoot: join15(root, RUNS_DIR),
31658
+ runId: options.runId,
31659
+ onEvent: options.onEvent,
31660
+ signal: options.signal
31661
+ });
31662
+ const summary = await runner.run(
31663
+ runnable.map((item) => ({ data: item.case, file: item.file }))
31664
+ );
31665
+ return { summary, invalid, tagFiltered };
31666
+ }
31667
+ function runsRoot(root) {
31668
+ return join15(resolveRoot(root), RUNS_DIR);
31669
+ }
31670
+ async function listRuns(options = {}) {
31671
+ const base = runsRoot(options.root);
31672
+ const entries = await readdir3(base).catch(() => []);
31673
+ const metas = [];
31674
+ for (const entry of entries.filter((name) => /^\d{8}-\d{3}$/.test(name)).sort().reverse()) {
31675
+ metas.push({ ...await readRunMeta(base, entry), runId: entry, dir: join15(base, entry) });
31676
+ }
31677
+ return metas;
31678
+ }
31679
+ async function latestRunId(options = {}) {
31680
+ const runs = await listRuns(options);
31681
+ return runs[0]?.runId;
31682
+ }
31683
+ async function getRun(runId, options = {}) {
31684
+ const base = runsRoot(options.root);
31685
+ const file2 = join15(base, runId, RESULT_FILE);
31686
+ try {
31687
+ return JSON.parse(await readFile5(file2, "utf8"));
31688
+ } catch {
31689
+ throw new RunError("ENO_CASES", `\u8BFB\u53D6\u8FD0\u884C\u7ED3\u679C\u5931\u8D25:${file2}(\u8FD0\u884C\u672A\u5B8C\u6210\u6216 runId \u4E0D\u5B58\u5728)`);
31690
+ }
31691
+ }
31692
+ async function getRunEvents(runId, options = {}) {
31693
+ const file2 = join15(runsRoot(options.root), runId, "events.ndjson");
31694
+ const source = await readFile5(file2, "utf8").catch(() => "");
31695
+ return source.split("\n").filter((line) => line.trim().length > 0).map((line) => JSON.parse(line));
31696
+ }
31697
+ async function readRunMeta(base, runId) {
31698
+ try {
31699
+ const summary = JSON.parse(
31700
+ await readFile5(join15(base, runId, RESULT_FILE), "utf8")
31701
+ );
31702
+ return {
31703
+ status: summary.status,
31704
+ cancelled: summary.cancelled,
31705
+ startedAt: summary.startedAt,
31706
+ finishedAt: summary.finishedAt,
31707
+ durationMs: summary.durationMs,
31708
+ totals: summary.totals
31709
+ };
31710
+ } catch {
31711
+ return { status: "unknown" };
31712
+ }
31304
31713
  }
31305
31714
 
31306
- // ../../packages/execution-engine/src/scheduler/index.ts
31307
- async function runSequential(items, handler) {
31308
- const results = [];
31309
- for (const [index, item] of items.entries()) {
31310
- results.push(await handler(item, index));
31715
+ // ../../packages/sdk/src/reports.ts
31716
+ async function generateReport(runId, options = {}) {
31717
+ const id = runId ?? await latestRunId(options);
31718
+ if (!id) {
31719
+ throw new Error("\u672A\u627E\u5230\u8FD0\u884C\u8BB0\u5F55(\u5148\u8FD0\u884C\u4E00\u6B21\u7528\u4F8B)");
31720
+ }
31721
+ const runDir = join16(resolveRoot(options.root), RUNS_DIR, id);
31722
+ let summary;
31723
+ try {
31724
+ summary = JSON.parse(await readFile6(join16(runDir, "result.json"), "utf8"));
31725
+ } catch {
31726
+ throw new Error(`\u8BFB\u53D6\u8FD0\u884C\u7ED3\u679C\u5931\u8D25:${join16(runDir, "result.json")}(\u8FD0\u884C\u672A\u5B8C\u6210\u6216 runId \u4E0D\u5B58\u5728)`);
31311
31727
  }
31312
- return results;
31728
+ const written = await writeReports(runDir, summary);
31729
+ return { runId: id, json: written.json, html: written.html };
31730
+ }
31731
+ async function readReport(runId, options = {}) {
31732
+ const file2 = join16(resolveRoot(options.root), RUNS_DIR, runId, "report.json");
31733
+ const source = await readFile6(file2, "utf8").catch(() => void 0);
31734
+ return source ? JSON.parse(source) : void 0;
31313
31735
  }
31314
31736
 
31315
- // ../../packages/adapter-core/src/session.ts
31316
- var AdapterSession = class {
31317
- constructor(target, factory) {
31318
- this.target = target;
31319
- this.factory = factory;
31737
+ // ../../packages/sdk/src/client.ts
31738
+ function resolveInput(root, input2) {
31739
+ return isAbsolute3(input2) ? input2 : join17(root, input2);
31740
+ }
31741
+ var TestPilotClient = class {
31742
+ /** 业务项目根目录(绝对路径) */
31743
+ root;
31744
+ constructor(options = {}) {
31745
+ this.root = resolveRoot(options.root);
31746
+ }
31747
+ // ---- project ----
31748
+ /** 读取项目配置(testpilot.yaml 不存在时返回默认配置) */
31749
+ getConfig() {
31750
+ return loadProjectConfig(this.root);
31751
+ }
31752
+ /** 项目自检:目录结构 / 配置 / Skill 安装状态 / 可用执行端 */
31753
+ inspect() {
31754
+ return inspectProject(this.root);
31755
+ }
31756
+ /** 读取本地项目与 Server Project 的关联(.testpilot/project.json) */
31757
+ getProjectLink() {
31758
+ return readProjectLink(this.root);
31759
+ }
31760
+ /** 写入本地项目与 Server Project 的关联 */
31761
+ linkProject(link) {
31762
+ return writeProjectLink(link, this.root);
31763
+ }
31764
+ // ---- cases ----
31765
+ /** 收集并校验用例;paths 相对本项目根目录,默认按配置的 casesDir */
31766
+ async listCases(paths, tag) {
31767
+ const config2 = await this.getConfig();
31768
+ const inputs = paths && paths.length > 0 ? paths : [config2.casesDir];
31769
+ const infos = await collectCases(inputs.map((input2) => resolveInput(this.root, input2)), {
31770
+ root: this.root
31771
+ });
31772
+ return tag ? infos.filter((item) => item.case?.tags?.includes(tag)) : infos;
31320
31773
  }
31321
- adapter;
31322
- get opened() {
31323
- return this.adapter !== void 0;
31774
+ /** 读取单个用例:source 原文 + 校验结果 */
31775
+ readCase(file2) {
31776
+ return readCaseFile(file2, { root: this.root });
31324
31777
  }
31325
- /** 惰性创建;已创建则复用 */
31326
- async get() {
31327
- if (!this.adapter) {
31328
- this.adapter = await this.factory.create();
31329
- await this.adapter.launch();
31330
- }
31331
- return this.adapter;
31778
+ /** 写入用例(供 Agent 的 create_case 工具使用):先校验后落盘 */
31779
+ writeCase(file2, source, options = {}) {
31780
+ return writeCaseFile(file2, source, { root: this.root, ...options });
31332
31781
  }
31333
- async close() {
31334
- if (this.adapter) {
31335
- await this.adapter.close().catch(() => void 0);
31336
- this.adapter = void 0;
31337
- }
31782
+ // ---- runs ----
31783
+ /** 执行一次运行 */
31784
+ runCases(options = {}) {
31785
+ return runCases({ ...options, root: this.root });
31338
31786
  }
31339
- };
31340
-
31341
- // ../../packages/execution-engine/src/engine/adapter-resolver.ts
31342
- var AdapterResolver = class {
31343
- factories = /* @__PURE__ */ new Map();
31344
- sessions = /* @__PURE__ */ new Map();
31345
- register(factory) {
31346
- this.factories.set(factory.target, factory);
31787
+ /** 历史运行列表(最近的在前) */
31788
+ listRuns() {
31789
+ return listRuns({ root: this.root });
31347
31790
  }
31348
- /** 取得(或创建)target 的会话 */
31349
- async session(target) {
31350
- let session = this.sessions.get(target);
31351
- if (!session) {
31352
- const factory = this.factories.get(target);
31353
- if (!factory) {
31354
- throw new TestPilotError("ADAPTER_NOT_FOUND", `\u672A\u6CE8\u518C target "${target}" \u7684 adapter,\u8BE5\u7AEF\u6B65\u9AA4\u65E0\u6CD5\u6267\u884C`);
31355
- }
31356
- session = new AdapterSession(target, factory);
31357
- this.sessions.set(target, session);
31358
- }
31359
- return session;
31791
+ /** 读取一次运行的汇总结果(result.json) */
31792
+ getRun(runId) {
31793
+ return getRun(runId, { root: this.root });
31360
31794
  }
31361
- async closeAll() {
31362
- for (const session of this.sessions.values()) {
31363
- await session.close();
31364
- }
31365
- this.sessions.clear();
31795
+ /** 读取一次运行的完整事件流(events.ndjson) */
31796
+ getRunEvents(runId) {
31797
+ return getRunEvents(runId, { root: this.root });
31798
+ }
31799
+ // ---- reports ----
31800
+ /** 生成报告(默认最近一次运行) */
31801
+ generateReport(runId) {
31802
+ return generateReport(runId, { root: this.root });
31803
+ }
31804
+ /** 读取已生成的报告 */
31805
+ readReport(runId) {
31806
+ return readReport(runId, { root: this.root });
31366
31807
  }
31367
31808
  };
31368
31809
 
31369
- // ../../packages/execution-engine/src/engine/step-executor.ts
31370
- async function executeStep(step, index, adapter, context, artifacts, options) {
31371
- const startedAt = Date.now();
31372
- const result = {
31373
- index,
31374
- target: step.target,
31375
- action: step.action,
31376
- status: "passed",
31377
- durationMs: 0
31378
- };
31379
- try {
31380
- const locator = step.locator ? {
31381
- text: step.locator.text !== void 0 ? context.resolve(step.locator.text) : void 0,
31382
- css: step.locator.css !== void 0 ? context.resolve(step.locator.css) : void 0
31383
- } : void 0;
31384
- switch (step.action) {
31385
- case "launch":
31386
- await adapter.launch();
31387
- break;
31388
- case "navigate":
31389
- await adapter.navigate(context.resolve(step.url ?? ""));
31390
- break;
31391
- case "click":
31392
- await adapter.click(locator);
31393
- break;
31394
- case "input":
31395
- await adapter.input(locator, context.resolve(step.value ?? ""));
31396
- break;
31397
- case "select":
31398
- await adapter.select(locator, context.resolve(step.value ?? ""));
31399
- break;
31400
- case "wait":
31401
- await adapter.wait(locator, step.timeout);
31402
- break;
31403
- case "assert":
31404
- await adapter.assert(locator, context.resolve(step.expected ?? ""));
31405
- break;
31406
- case "extract": {
31407
- const value = await adapter.extract(locator);
31408
- if (step.variable) {
31409
- context.set(step.variable, value);
31410
- result.extracted = { [step.variable]: value };
31411
- }
31412
- break;
31413
- }
31414
- case "screenshot": {
31415
- const data = await adapter.screenshot();
31416
- result.screenshot = await artifacts.saveStepScreenshot(`${options.screenshotName}.png`, data);
31417
- break;
31418
- }
31419
- default: {
31420
- const exhaustive = step.action;
31421
- throw new Error(`\u4E0D\u652F\u6301\u7684 action:${String(exhaustive)}`);
31422
- }
31810
+ // src/commands/init/adapter-detector.ts
31811
+ import { existsSync as existsSync4, readFileSync } from "node:fs";
31812
+ import { join as join18 } from "node:path";
31813
+ async function detectAdapters(root, projectType) {
31814
+ const required2 = projectType === "hybrid" ? ["playwright", "wechatide"] : projectType === "wechat-miniapp" ? ["wechatide"] : projectType === "web" ? ["playwright"] : [];
31815
+ const pkg = readPackageJsonDeps(root);
31816
+ const hasPlaywrightDep = pkg !== void 0 && ("playwright" in pkg || "@playwright/test" in pkg);
31817
+ const playwrightInstalled = hasPlaywrightDep || existsSync4(join18(root, "node_modules", "playwright"));
31818
+ const devtoolsCli = await findWechatDevToolsCli().catch(() => void 0);
31819
+ return [
31820
+ {
31821
+ id: "playwright",
31822
+ available: playwrightInstalled,
31823
+ detail: playwrightInstalled ? pkg?.["playwright"] ?? pkg?.["@playwright/test"] ?? "\u5DF2\u5B89\u88C5" : "\u672A\u68C0\u6D4B\u5230(pnpm add -D playwright)",
31824
+ required: required2.includes("playwright")
31825
+ },
31826
+ {
31827
+ id: "wechatide",
31828
+ available: devtoolsCli !== void 0,
31829
+ detail: devtoolsCli ?? "\u672A\u627E\u5230\u5FAE\u4FE1\u5F00\u53D1\u8005\u5DE5\u5177(\u53EF\u8BBE\u7F6E WECHAT_DEVTOOLS_CLI \u6307\u5411 cli \u53EF\u6267\u884C\u6587\u4EF6)",
31830
+ required: required2.includes("wechatide")
31423
31831
  }
31424
- } catch (err) {
31425
- result.status = "failed";
31426
- result.error = err instanceof Error ? err.message : String(err);
31832
+ ];
31833
+ }
31834
+ function readPackageJsonDeps(root) {
31835
+ const file2 = join18(root, "package.json");
31836
+ if (!existsSync4(file2)) return void 0;
31837
+ try {
31838
+ const pkg = JSON.parse(readFileSync(file2, "utf8"));
31839
+ return { ...pkg.dependencies, ...pkg.devDependencies };
31840
+ } catch {
31841
+ return void 0;
31842
+ }
31843
+ }
31844
+
31845
+ // src/commands/init/environment-initializer.ts
31846
+ import { readFile as readFile7 } from "node:fs/promises";
31847
+ import { join as join19 } from "node:path";
31848
+ var ENV_REF_PATTERN = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
31849
+ async function collectEnvVarRefs(root) {
31850
+ let text;
31851
+ try {
31852
+ text = await readFile7(join19(root, "testpilot.yaml"), "utf8");
31853
+ } catch {
31854
+ return [];
31855
+ }
31856
+ const names = /* @__PURE__ */ new Set();
31857
+ for (const match of text.matchAll(ENV_REF_PATTERN)) {
31858
+ names.add(match[1]);
31859
+ }
31860
+ return [...names].sort();
31861
+ }
31862
+
31863
+ // src/commands/init/project-detector.ts
31864
+ import { existsSync as existsSync5, readFileSync as readFileSync2 } from "node:fs";
31865
+ import { basename, join as join20 } from "node:path";
31866
+
31867
+ // src/lib/git.ts
31868
+ import { execFile as execFile2 } from "node:child_process";
31869
+ import { promisify } from "node:util";
31870
+ var execFileAsync = promisify(execFile2);
31871
+ async function readGitInfo(root) {
31872
+ const run = async (args) => {
31427
31873
  try {
31428
- const data = await adapter.screenshot();
31429
- result.screenshot = await artifacts.saveStepScreenshot(`${options.screenshotName}-failed.png`, data);
31874
+ const { stdout } = await execFileAsync("git", args, { cwd: root });
31875
+ return stdout.trim() || void 0;
31430
31876
  } catch {
31877
+ return void 0;
31431
31878
  }
31879
+ };
31880
+ const [branch, commit, repositoryUrl] = await Promise.all([
31881
+ run(["rev-parse", "--abbrev-ref", "HEAD"]),
31882
+ run(["rev-parse", "HEAD"]),
31883
+ run(["remote", "get-url", "origin"])
31884
+ ]);
31885
+ return { branch, commit, repositoryUrl };
31886
+ }
31887
+
31888
+ // src/commands/init/project-detector.ts
31889
+ var WEB_DEP_MARKERS = [
31890
+ "playwright",
31891
+ "@playwright/test",
31892
+ "vite",
31893
+ "next",
31894
+ "react",
31895
+ "vue",
31896
+ "@angular/core",
31897
+ "nuxt",
31898
+ "svelte"
31899
+ ];
31900
+ var E2E_DIR_CANDIDATES = ["tests/e2e", "test/e2e", "e2e", "cypress"];
31901
+ function readPackageJson(root) {
31902
+ const file2 = join20(root, "package.json");
31903
+ if (!existsSync5(file2)) return void 0;
31904
+ try {
31905
+ return JSON.parse(readFileSync2(file2, "utf8"));
31906
+ } catch {
31907
+ return void 0;
31432
31908
  }
31433
- result.durationMs = Date.now() - startedAt;
31434
- return result;
31909
+ }
31910
+ function detectProjectType(root) {
31911
+ const pkg = readPackageJson(root);
31912
+ const deps = { ...pkg?.dependencies, ...pkg?.devDependencies };
31913
+ const isWechat = existsSync5(join20(root, "project.config.json")) || existsSync5(join20(root, "app.json")) || existsSync5(join20(root, "miniprogram", "app.json"));
31914
+ const isWeb = existsSync5(join20(root, "index.html")) || WEB_DEP_MARKERS.some((marker) => deps?.[marker] !== void 0) || [".ts", ".tsx", ".js", ".jsx"].some((ext) => existsSync5(join20(root, "src", `index${ext}`)));
31915
+ if (isWechat && isWeb) return "hybrid";
31916
+ if (isWechat) return "wechat-miniapp";
31917
+ if (isWeb) return "web";
31918
+ return "unknown";
31919
+ }
31920
+ async function detectProject(root) {
31921
+ const pkg = readPackageJson(root);
31922
+ const type = detectProjectType(root);
31923
+ const language = existsSync5(join20(root, "tsconfig.json")) ? "TypeScript" : pkg || existsSync5(join20(root, "src")) ? "JavaScript" : "Unknown";
31924
+ const packageManager = existsSync5(join20(root, "pnpm-lock.yaml")) ? "pnpm" : existsSync5(join20(root, "yarn.lock")) ? "yarn" : existsSync5(join20(root, "package-lock.json")) ? "npm" : "npx";
31925
+ const existingE2eDirs = E2E_DIR_CANDIDATES.filter((dir) => existsSync5(join20(root, dir)));
31926
+ return {
31927
+ root,
31928
+ name: pkg?.name ?? basename(root),
31929
+ type,
31930
+ language,
31931
+ packageManager,
31932
+ git: await readGitInfo(root),
31933
+ hasTestsE2e: existingE2eDirs.includes("tests/e2e"),
31934
+ existingE2eDirs,
31935
+ hasTestpilotConfig: existsSync5(join20(root, "testpilot.yaml")),
31936
+ hasProjectLink: existsSync5(join20(root, ".testpilot", "project.json"))
31937
+ };
31938
+ }
31939
+ function defaultAdapterFor(type) {
31940
+ if (type === "wechat-miniapp") return "wechatide";
31941
+ return "playwright";
31942
+ }
31943
+ function adaptersFor(type) {
31944
+ if (type === "hybrid") return ["playwright", "wechatide"];
31945
+ if (type === "wechat-miniapp") return ["wechatide"];
31946
+ if (type === "web") return ["playwright"];
31947
+ return ["playwright"];
31435
31948
  }
31436
31949
 
31437
- // ../../packages/execution-engine/src/engine/execution-engine.ts
31438
- var ExecutionEngine = class {
31439
- async runCase(data, options) {
31440
- const { runId, file: file2, artifacts, bus, resolver } = options;
31441
- const startedAt = /* @__PURE__ */ new Date();
31442
- const context = new ExecutionContext();
31443
- const steps = [];
31444
- const evidenceAdapters = /* @__PURE__ */ new Map();
31445
- let caseError;
31446
- await bus.emit({ type: "case-started", runId, caseId: data.id, file: file2, ts: startedAt.toISOString() });
31447
- await artifacts.logger.info(`case ${data.id} started (${file2})`);
31448
- for (const [index, step] of data.steps.entries()) {
31449
- await bus.emit({
31450
- type: "step-started",
31451
- runId,
31452
- caseId: data.id,
31453
- index,
31454
- target: step.target,
31455
- action: step.action,
31456
- ts: (/* @__PURE__ */ new Date()).toISOString()
31457
- });
31458
- let result;
31459
- try {
31460
- const adapter = await (await resolver.session(step.target)).get();
31461
- if (adapter.startEvidence && !evidenceAdapters.has(step.target)) {
31462
- try {
31463
- await adapter.startEvidence(data.id);
31464
- evidenceAdapters.set(step.target, adapter);
31465
- } catch (err) {
31466
- await artifacts.logger.info(
31467
- `evidence start failed for ${step.target}: ${err instanceof Error ? err.message : String(err)}`
31468
- );
31469
- }
31470
- }
31471
- result = await executeStep(step, index, adapter, context, artifacts, {
31472
- screenshotName: `${data.id}-step-${String(index).padStart(2, "0")}`
31473
- });
31474
- } catch (err) {
31475
- result = {
31476
- index,
31477
- target: step.target,
31478
- action: step.action,
31479
- status: "failed",
31480
- durationMs: 0,
31481
- error: err instanceof Error ? err.message : String(err)
31482
- };
31483
- }
31484
- steps.push(result);
31485
- await artifacts.logger.info(`step ${index} ${step.action} -> ${result.status}${result.error ? ` - ${result.error}` : ""}`);
31486
- await bus.emit({
31487
- type: "step-finished",
31488
- runId,
31489
- caseId: data.id,
31490
- index,
31491
- target: step.target,
31492
- action: step.action,
31493
- status: result.status,
31494
- durationMs: result.durationMs,
31495
- error: result.error,
31496
- ts: (/* @__PURE__ */ new Date()).toISOString()
31950
+ // src/commands/init/doctor.ts
31951
+ async function runDoctor(root) {
31952
+ const sections = [];
31953
+ const push = (name, items) => {
31954
+ if (items.length > 0) sections.push({ name, items });
31955
+ };
31956
+ const ok = (label) => ({ status: "ok", label });
31957
+ const warn = (label, hint) => ({ status: "warn", label, hint });
31958
+ const error62 = (label, hint) => ({ status: "error", label, hint });
31959
+ const nodeMajor = Number(process.versions.node.split(".")[0]);
31960
+ push("Runtime", [
31961
+ nodeMajor >= 22 ? ok(`Node.js ${process.versions.node}`) : error62(`Node.js ${process.versions.node}`, "\u9700\u8981 Node.js >= 22")
31962
+ ]);
31963
+ const info = await detectProject(root);
31964
+ const projectItems = [
31965
+ ok(`Project ${info.name}(${info.type})`),
31966
+ 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"),
31967
+ existsSync6(join21(root, ".testpilot", "project.json")) ? ok(".testpilot/project.json") : error62(".testpilot/project.json", "\u8FD0\u884C npx csspilot init \u751F\u6210"),
31968
+ existsSync6(join21(root, "testpilot.yaml")) ? ok("testpilot.yaml") : error62("testpilot.yaml", "\u8FD0\u884C npx csspilot init \u751F\u6210")
31969
+ ];
31970
+ push("Project", projectItems);
31971
+ const config2 = await loadTestpilotConfig(root).catch(() => void 0);
31972
+ const casesDir = config2?.casesDir ?? "tests/e2e/cases";
31973
+ const cases = await collectCases([casesDir], { root }).catch(() => []);
31974
+ const invalidCount = cases.filter((item) => !item.valid).length;
31975
+ const testDir = join21(root, "tests", "e2e");
31976
+ push("Test", [
31977
+ existsSync6(testDir) ? ok("tests/e2e") : error62("tests/e2e", "\u8FD0\u884C npx csspilot init \u521B\u5EFA"),
31978
+ existsSync6(join21(root, casesDir)) ? ok(`cases directory(${casesDir})`) : error62(`cases directory(${casesDir})`, "\u8FD0\u884C npx csspilot init \u521B\u5EFA"),
31979
+ 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")
31980
+ ]);
31981
+ const skillDir = join21(root, ".agents", "skills", "testpilot");
31982
+ push("Agent", [
31983
+ existsSync6(join21(skillDir, "manifest.yaml")) ? ok("TestPilot Skill(.agents/skills/testpilot)") : error62("TestPilot Skill", "\u8FD0\u884C npx csspilot init \u5B89\u88C5"),
31984
+ existsSync6(join21(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")
31985
+ ]);
31986
+ const adapters = await detectAdapters(root, info.type);
31987
+ push(
31988
+ "Adapters",
31989
+ adapters.map(
31990
+ (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)
31991
+ )
31992
+ );
31993
+ const refs = await collectEnvVarRefs(root);
31994
+ if (refs.length > 0) {
31995
+ push(
31996
+ "Environment",
31997
+ refs.map(
31998
+ (name) => process.env[name] !== void 0 ? ok(name) : error62(`${name} missing`, "\u5728\u8FD0\u884C\u73AF\u5883(.env / CI Secret)\u4E2D\u8BBE\u7F6E")
31999
+ )
32000
+ );
32001
+ }
32002
+ const serverUrl = process.env.TESTPILOT_SERVER_URL ?? config2?.server?.baseUrl;
32003
+ if (serverUrl) {
32004
+ const link = await readProjectLink(root);
32005
+ const serverItems = [];
32006
+ try {
32007
+ const res = await fetch(`${serverUrl.replace(/\/$/, "")}/api/health`, {
32008
+ signal: AbortSignal.timeout(3e3)
31497
32009
  });
31498
- if (result.status === "failed") {
31499
- caseError = result.error;
31500
- for (const [skipIndex, skippedStep] of data.steps.entries()) {
31501
- if (skipIndex <= index) continue;
31502
- steps.push({
31503
- index: skipIndex,
31504
- target: skippedStep.target,
31505
- action: skippedStep.action,
31506
- status: "skipped",
31507
- durationMs: 0
31508
- });
31509
- }
31510
- break;
31511
- }
32010
+ serverItems.push(res.ok ? ok(`Connected(${serverUrl})`) : error62(`Server ${res.status}`, serverUrl));
32011
+ } catch (err) {
32012
+ serverItems.push(
32013
+ error62(`Server \u4E0D\u53EF\u8FBE(${serverUrl})`, err instanceof Error ? err.message : String(err))
32014
+ );
31512
32015
  }
31513
- const finishedAt = /* @__PURE__ */ new Date();
31514
- const status = caseError ? "failed" : "passed";
31515
- const caseResult = {
31516
- caseId: data.id,
31517
- caseName: data.name,
31518
- file: file2,
31519
- status,
31520
- steps,
31521
- startedAt: startedAt.toISOString(),
31522
- finishedAt: finishedAt.toISOString(),
31523
- durationMs: finishedAt.getTime() - startedAt.getTime()
31524
- };
31525
- if (caseError) caseResult.error = caseError;
31526
- for (const [target, adapter] of evidenceAdapters) {
31527
- if (!adapter.stopEvidence) continue;
31528
- try {
31529
- const evidence = await adapter.stopEvidence(data.id);
31530
- if (evidence.video) {
31531
- caseResult.video = await artifacts.saveVideo(`${data.id}-${target}.webm`, evidence.video);
31532
- }
31533
- if (evidence.trace) {
31534
- caseResult.trace = await artifacts.saveTrace(`${data.id}-${target}.zip`, evidence.trace);
31535
- }
31536
- } catch (err) {
31537
- await artifacts.logger.info(
31538
- `evidence stop failed for ${target}: ${err instanceof Error ? err.message : String(err)}`
31539
- );
31540
- }
32016
+ if (link?.projectId) serverItems.push(ok(`Project association(${link.projectId})`));
32017
+ else serverItems.push(warn("Project association \u7F3A\u5931", "\u8FD0\u884C npx csspilot init \u91CD\u65B0\u5173\u8054"));
32018
+ push("Server", serverItems);
32019
+ }
32020
+ const all = sections.flatMap((section) => section.items);
32021
+ return {
32022
+ sections,
32023
+ errors: all.filter((item) => item.status === "error").length,
32024
+ warnings: all.filter((item) => item.status === "warn").length
32025
+ };
32026
+ }
32027
+ var MARK = { ok: "\u2714", warn: "\u26A0", error: "\u2717" };
32028
+ function printDoctor(report) {
32029
+ console.log("TestPilot Doctor");
32030
+ console.log("");
32031
+ for (const section of report.sections) {
32032
+ console.log(section.name);
32033
+ console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
32034
+ for (const item of section.items) {
32035
+ const hint = item.hint ? `(${item.hint})` : "";
32036
+ console.log(`${MARK[item.status]} ${item.label}${hint}`);
31541
32037
  }
31542
- await bus.emit({
31543
- type: "case-finished",
31544
- runId,
31545
- caseId: data.id,
31546
- status,
31547
- durationMs: finishedAt.getTime() - startedAt.getTime(),
31548
- ts: finishedAt.toISOString()
32038
+ console.log("");
32039
+ }
32040
+ console.log("Result");
32041
+ console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
32042
+ console.log(`${report.errors} error${report.errors === 1 ? "" : "s"}`);
32043
+ console.log(`${report.warnings} warning${report.warnings === 1 ? "" : "s"}`);
32044
+ }
32045
+
32046
+ // src/commands/doctor.ts
32047
+ function makeDoctorCommand() {
32048
+ return new Command("doctor").description("TestPilot \u5065\u5EB7\u68C0\u67E5(Runtime / \u9879\u76EE / \u7528\u4F8B / Skill / Adapter / \u73AF\u5883\u53D8\u91CF / Server)").action(async () => {
32049
+ const report = await runDoctor(process.cwd());
32050
+ printDoctor(report);
32051
+ if (report.errors > 0) process.exitCode = 1;
32052
+ });
32053
+ }
32054
+
32055
+ // src/commands/init/index.ts
32056
+ import { join as join25 } from "node:path";
32057
+
32058
+ // src/commands/init/config-initializer.ts
32059
+ import { access as access2, mkdir as mkdir11, writeFile as writeFile9 } from "node:fs/promises";
32060
+ import { basename as basename2, join as join22 } from "node:path";
32061
+ async function exists3(path) {
32062
+ try {
32063
+ await access2(path);
32064
+ return true;
32065
+ } catch {
32066
+ return false;
32067
+ }
32068
+ }
32069
+ var TEST_DIR = "tests/e2e";
32070
+ var CASE_DIR = "tests/e2e/cases";
32071
+ function renderTestpilotYaml(info) {
32072
+ const adapters = adaptersFor(info.type).map((item) => ` - ${item}`).join("\n");
32073
+ return `# TestPilot \u9879\u76EE\u914D\u7F6E(Source of Truth)
32074
+ version: 1
32075
+
32076
+ project:
32077
+ name: ${info.name}
32078
+
32079
+ test:
32080
+ directory: ${TEST_DIR}
32081
+ caseDirectory: ${CASE_DIR}
32082
+
32083
+ # \u73AF\u5883\u914D\u7F6E:Case \u7528 environment: <\u540D\u79F0> \u5F15\u7528;baseUrl \u7531\u73AF\u5883\u53D8\u91CF\u6CE8\u5165,\u7981\u6B62\u5199\u660E\u6587
32084
+ environment:
32085
+ default: test
32086
+ test:
32087
+ baseUrl: \${TEST_BASE_URL}
32088
+ staging:
32089
+ baseUrl: \${STAGING_BASE_URL}
32090
+
32091
+ # \u9879\u76EE\u58F0\u660E\u7684\u6267\u884C\u7AEF(\u5B9E\u9645\u53EF\u7528\u6027\u4EE5 csspilot doctor \u68C0\u6D4B\u4E3A\u51C6)
32092
+ adapters:
32093
+ ${adapters}
32094
+
32095
+ # \u9ED8\u8BA4 Workspace(\u591A\u7CFB\u7EDF\u73AF\u5883\u7EC4\u5408);Case \u4E5F\u53EF\u7528 workspace \u5B57\u6BB5\u6309\u9700\u58F0\u660E
32096
+ # workspace:
32097
+ # default: my-workspace
32098
+
32099
+ # TestPilot Server(Control Plane)\u5730\u5740;init \u5173\u8054\u9879\u76EE\u4E0E sync-metadata \u4F7F\u7528
32100
+ # \u4E5F\u53EF\u7528\u73AF\u5883\u53D8\u91CF TESTPILOT_SERVER_URL \u8986\u76D6
32101
+ # server:
32102
+ # baseUrl: http://127.0.0.1:3000
32103
+
32104
+ # Web \u7AEF:navigate \u76F8\u5BF9 url \u7684\u57FA\u51C6\u5730\u5740
32105
+ # web:
32106
+ # baseUrl: http://127.0.0.1:8080
32107
+
32108
+ # \u5C0F\u7A0B\u5E8F\u7AEF:\u586B\u5165\u5C0F\u7A0B\u5E8F\u9879\u76EE\u76EE\u5F55\u540E\u5373\u53EF\u6267\u884C target: miniapp \u7684\u6B65\u9AA4
32109
+ # miniapp:
32110
+ # projectPath: path/to/miniprogram
32111
+ # cliPath: C:/Program Files (x86)/Tencent/\u5FAE\u4FE1web\u5F00\u53D1\u8005\u5DE5\u5177/cli.bat
32112
+ `;
32113
+ }
32114
+ async function initTestPilotConfig(root, info, adapters) {
32115
+ const yamlPath = join22(root, "testpilot.yaml");
32116
+ let yaml = "reused";
32117
+ if (!await exists3(yamlPath)) {
32118
+ await writeFile9(yamlPath, renderTestpilotYaml(info), "utf8");
32119
+ yaml = "created";
32120
+ }
32121
+ const runtimeDir = join22(root, ".testpilot", "artifacts");
32122
+ await mkdir11(runtimeDir, { recursive: true });
32123
+ const keep = join22(runtimeDir, ".gitkeep");
32124
+ if (!await exists3(keep)) {
32125
+ await writeFile9(keep, "", "utf8");
32126
+ }
32127
+ const existing = await readProjectLink(root);
32128
+ const defaultAdapter = defaultAdapterFor(info.type);
32129
+ const serverUrl = process.env.TESTPILOT_SERVER_URL ?? void 0;
32130
+ const link = existing ? { ...existing, version: existing.version ?? 1 } : {
32131
+ ...await createProjectOnServer(root, info, adapters, serverUrl),
32132
+ version: 1,
32133
+ testDir: TEST_DIR,
32134
+ caseDir: CASE_DIR,
32135
+ defaultAdapter
32136
+ };
32137
+ if (link.testDir === void 0 || link.caseDir === void 0 || link.defaultAdapter === void 0) {
32138
+ link.testDir ??= TEST_DIR;
32139
+ link.caseDir ??= CASE_DIR;
32140
+ link.defaultAdapter ??= defaultAdapter;
32141
+ }
32142
+ if (link.serverUrl === void 0 && serverUrl) link.serverUrl = serverUrl;
32143
+ const projectLinkFile = await writeProjectLink(link, root);
32144
+ return { yaml, runtimeDir: join22(root, ".testpilot"), projectLink: link, projectLinkFile };
32145
+ }
32146
+ async function createProjectOnServer(root, info, _adapters, serverUrl) {
32147
+ const git = info.git;
32148
+ const base = { repositoryUrl: git.repositoryUrl, branch: git.branch };
32149
+ if (!serverUrl) return { ...base, projectId: `proj_${randomId()}` };
32150
+ try {
32151
+ const res = await fetch(`${serverUrl.replace(/\/$/, "")}/api/projects`, {
32152
+ method: "POST",
32153
+ headers: { "content-type": "application/json" },
32154
+ body: JSON.stringify({
32155
+ name: info.name || basename2(root),
32156
+ rootPath: root,
32157
+ repositoryUrl: git.repositoryUrl,
32158
+ defaultBranch: git.branch
32159
+ })
31549
32160
  });
31550
- await artifacts.logger.info(`case ${data.id} ${status}`);
31551
- return caseResult;
32161
+ if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
32162
+ const project = await res.json();
32163
+ return { ...base, serverUrl, projectId: `proj_${project.id}` };
32164
+ } catch (err) {
32165
+ console.error(`\u2717 \u9879\u76EE\u5173\u8054 Server(${serverUrl})\u4E0D\u53EF\u8FBE:${err instanceof Error ? err.message : err}`);
32166
+ return { ...base, serverUrl, projectId: `proj_${randomId()}` };
31552
32167
  }
31553
- };
32168
+ }
32169
+ function randomId() {
32170
+ return Math.random().toString(16).slice(2, 10);
32171
+ }
31554
32172
 
31555
- // ../../packages/execution-engine/src/engine/result-collector.ts
31556
- function buildRunSummary(runId, startedAt, cases) {
31557
- const finishedAt = /* @__PURE__ */ new Date();
31558
- const totals = {
31559
- cases: cases.length,
31560
- passed: cases.filter((item) => item.status === "passed").length,
31561
- failed: cases.filter((item) => item.status === "failed").length,
31562
- steps: 0,
31563
- stepsPassed: 0,
31564
- stepsFailed: 0,
31565
- stepsSkipped: 0
32173
+ // src/commands/init/skill-installer.ts
32174
+ import { existsSync as existsSync7 } from "node:fs";
32175
+ import { access as access3, cp, mkdir as mkdir12, writeFile as writeFile10 } from "node:fs/promises";
32176
+ import { dirname as dirname7, join as join23 } from "node:path";
32177
+ import { fileURLToPath } from "node:url";
32178
+ async function exists4(path) {
32179
+ try {
32180
+ await access3(path);
32181
+ return true;
32182
+ } catch {
32183
+ return false;
32184
+ }
32185
+ }
32186
+ function resolveSkillSourceDir() {
32187
+ const fromEnv = process.env.TESTPILOT_SKILL_DIR;
32188
+ if (fromEnv) return fromEnv;
32189
+ const bundled = fileURLToPath(new URL("../../skills/testpilot", import.meta.url));
32190
+ if (existsSync7(bundled)) return bundled;
32191
+ const inRepo = fileURLToPath(new URL("../../../../../skills/testpilot", import.meta.url));
32192
+ return existsSync7(inRepo) ? inRepo : void 0;
32193
+ }
32194
+ async function installSkill(root, info, adapters) {
32195
+ const dest = join23(root, ".agents", "skills", "testpilot");
32196
+ let core = "reused";
32197
+ if (!await exists4(join23(dest, "manifest.yaml"))) {
32198
+ const src = resolveSkillSourceDir();
32199
+ if (!src) {
32200
+ throw new Error(
32201
+ "\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"
32202
+ );
32203
+ }
32204
+ await mkdir12(dirname7(dest), { recursive: true });
32205
+ await cp(src, dest, { recursive: true });
32206
+ core = "created";
32207
+ }
32208
+ const refsDir = join23(dest, "references");
32209
+ const files = [
32210
+ ["project.md", () => renderProjectMd(info, adapters)],
32211
+ ["test-conventions.md", renderTestConventionsMd],
32212
+ ["adapters.md", () => renderAdaptersMd(adapters)]
32213
+ ];
32214
+ let references = "reused";
32215
+ for (const [name, render] of files) {
32216
+ const file2 = join23(refsDir, name);
32217
+ if (await exists4(file2)) continue;
32218
+ await mkdir12(refsDir, { recursive: true });
32219
+ await writeFile10(file2, render(), "utf8");
32220
+ references = "created";
32221
+ }
32222
+ return { core, references, dest };
32223
+ }
32224
+ function renderProjectMd(info, adapters) {
32225
+ const available = adapters.filter((item) => item.available).map((item) => item.id);
32226
+ return `# Project
32227
+
32228
+ \u81EA\u52A8\u751F\u6210\u4E8E \`csspilot init\`,\u63CF\u8FF0\u672C\u9879\u76EE\u7684 TestPilot \u63A5\u5165\u4FE1\u606F;Agent \u4F18\u5148\u4ECE\u8FD9\u91CC\u4E86\u89E3\u9879\u76EE\u3002
32229
+
32230
+ - Project name: ${info.name}
32231
+ - Project type: ${info.type}
32232
+ - Language: ${info.language}
32233
+ - Package manager: ${info.packageManager}
32234
+ - Test directory: tests/e2e
32235
+ - Case directory: tests/e2e/cases
32236
+ - Available adapters: ${available.length > 0 ? available.join(", ") : "(none detected)"}
32237
+ - Default environment: test(testpilot.yaml environment.default)
32238
+ - Default workspace: \u672A\u8BBE\u7F6E(Case \u53EF\u6309\u9700\u58F0\u660E workspace \u5B57\u6BB5)
32239
+ `;
32240
+ }
32241
+ function renderTestConventionsMd() {
32242
+ return `# Test Conventions
32243
+
32244
+ \u672C\u9879\u76EE E2E \u6D4B\u8BD5\u7EA6\u5B9A,Agent \u751F\u6210/\u4FEE\u6539 Case \u65F6\u5FC5\u987B\u9075\u5B88:
32245
+
32246
+ 1. E2E Case \u4F4D\u4E8E tests/e2e/cases
32247
+ 2. Case \u4F7F\u7528 TestPilot DSL
32248
+ 3. \u4E0D\u5141\u8BB8\u76F4\u63A5\u8C03\u7528 Playwright
32249
+ 4. \u4E0D\u5141\u8BB8\u76F4\u63A5\u8C03\u7528 WeChatIDE
32250
+ 5. \u4E0D\u5141\u8BB8\u5728 Case \u4E2D\u5199\u660E\u6587\u5BC6\u7801(\u7528 accountRef \u5F15\u7528)
32251
+ 6. \u4F18\u5148\u590D\u7528\u5DF2\u6709 Case
32252
+ 7. \u65B0\u589E Case \u5FC5\u987B\u901A\u8FC7 csspilot validate
32253
+ 8. Case ID \u5FC5\u987B\u552F\u4E00
32254
+ `;
32255
+ }
32256
+ function renderAdaptersMd(adapters) {
32257
+ const lines = adapters.map(
32258
+ (item) => `- ${item.id}: ${item.available ? "\u53EF\u7528" : "\u4E0D\u53EF\u7528"}${item.required ? "(\u9879\u76EE\u9700\u8981)" : "(\u53EF\u9009)"} \u2014 ${item.detail}`
32259
+ );
32260
+ return `# Adapters
32261
+
32262
+ \u6267\u884C\u7AEF\u68C0\u6D4B\u7ED3\u679C(\u4EE5 \`csspilot doctor\` \u6700\u65B0\u8F93\u51FA\u4E3A\u51C6):
32263
+
32264
+ ${lines.join("\n")}
32265
+
32266
+ Agent \u6C38\u8FDC\u4E0D\u76F4\u63A5\u64CD\u4F5C Adapter;Case \u7ECF Execution Engine \u8C03\u5EA6\u5230\u5BF9\u5E94 Adapter \u6267\u884C\u3002
32267
+ `;
32268
+ }
32269
+
32270
+ // src/commands/init/test-structure.ts
32271
+ import { existsSync as existsSync8, readdirSync as readdirSync2 } from "node:fs";
32272
+ import { mkdir as mkdir13, writeFile as writeFile11 } from "node:fs/promises";
32273
+ import { join as join24 } from "node:path";
32274
+ async function initTestStructure(root, _info) {
32275
+ const dirs = {
32276
+ cases: join24(root, "tests", "e2e", "cases"),
32277
+ fixtures: join24(root, "tests", "e2e", "fixtures"),
32278
+ data: join24(root, "tests", "e2e", "data")
31566
32279
  };
31567
- for (const item of cases) {
31568
- for (const step of item.steps) {
31569
- totals.steps++;
31570
- if (step.status === "passed") totals.stepsPassed++;
31571
- else if (step.status === "failed") totals.stepsFailed++;
31572
- else totals.stepsSkipped++;
32280
+ const existed = {
32281
+ cases: existsSync8(dirs.cases),
32282
+ fixtures: existsSync8(dirs.fixtures),
32283
+ data: existsSync8(dirs.data)
32284
+ };
32285
+ for (const dir of Object.values(dirs)) {
32286
+ await mkdir13(dir, { recursive: true });
32287
+ }
32288
+ for (const [name, dir] of Object.entries(dirs)) {
32289
+ if (!existed[name] && readdirSync2(dir).length === 0) {
32290
+ await writeFile11(join24(dir, ".gitkeep"), "", "utf8").catch(() => void 0);
31573
32291
  }
31574
32292
  }
31575
32293
  return {
31576
- runId,
31577
- startedAt: startedAt.toISOString(),
31578
- finishedAt: finishedAt.toISOString(),
31579
- durationMs: finishedAt.getTime() - startedAt.getTime(),
31580
- status: totals.failed > 0 ? "failed" : "passed",
31581
- cases,
31582
- totals
32294
+ cases: existed.cases ? "reused" : "created",
32295
+ fixtures: existed.fixtures ? "reused" : "created",
32296
+ data: existed.data ? "reused" : "created"
31583
32297
  };
31584
32298
  }
31585
32299
 
31586
- // ../../packages/execution-engine/src/engine/test-runner.ts
31587
- import { writeFile as writeFile6 } from "node:fs/promises";
31588
- import { join as join15, resolve as resolve2 } from "node:path";
31589
- var TestRunner = class {
31590
- resolver;
31591
- runsRoot;
31592
- extraSubscriber;
31593
- constructor(options) {
31594
- this.resolver = options.resolver;
31595
- this.runsRoot = resolve2(options.runsRoot ?? join15(process.cwd(), ".testpilot", "artifacts", "runs"));
31596
- this.extraSubscriber = options.onEvent;
31597
- }
31598
- async run(cases) {
31599
- const startedAt = /* @__PURE__ */ new Date();
31600
- const runId = await allocateRunId(this.runsRoot);
31601
- const runDir = await ensureRunDir(this.runsRoot, runId);
31602
- const artifacts = new ArtifactManager(runDir);
31603
- await artifacts.prepare();
31604
- const bus = new EventBus();
31605
- bus.subscribe(ndjsonSubscriber(join15(runDir, "events.ndjson")));
31606
- if (this.extraSubscriber) bus.subscribe(this.extraSubscriber);
31607
- await bus.emit({ type: "run-started", runId, totalCases: cases.length, ts: startedAt.toISOString() });
31608
- await artifacts.logger.info(`run ${runId} started with ${cases.length} case(s)`);
31609
- const engine = new ExecutionEngine();
31610
- const results = await runSequential(
31611
- cases,
31612
- (item) => engine.runCase(item.data, {
31613
- runId,
31614
- file: item.file,
31615
- artifacts,
31616
- bus,
31617
- resolver: this.resolver
31618
- })
31619
- );
31620
- await this.resolver.closeAll();
31621
- const summary = buildRunSummary(runId, startedAt, results);
31622
- await bus.emit({
31623
- type: "run-finished",
31624
- runId,
31625
- status: summary.status,
31626
- totals: { cases: summary.totals.cases, passed: summary.totals.passed, failed: summary.totals.failed },
31627
- ts: (/* @__PURE__ */ new Date()).toISOString()
31628
- });
31629
- await artifacts.logger.info(`run ${runId} finished: ${summary.status}`);
31630
- await writeFile6(join15(runDir, RESULT_FILE), JSON.stringify(summary, null, 2), "utf8");
31631
- return summary;
32300
+ // src/commands/init/index.ts
32301
+ async function runInit(root, options = {}) {
32302
+ if (options.serverUrl !== void 0) {
32303
+ if (options.serverUrl === null) delete process.env.TESTPILOT_SERVER_URL;
32304
+ else process.env.TESTPILOT_SERVER_URL = options.serverUrl;
32305
+ }
32306
+ const project = await detectProject(root);
32307
+ console.log(`\u2714 Project: ${project.name}`);
32308
+ console.log(`\u2714 Type: ${project.type}`);
32309
+ console.log(`\u2714 Language: ${project.language}`);
32310
+ console.log(`\u2714 Package Manager: ${project.packageManager}`);
32311
+ console.log(project.git.commit ? `\u2714 Git repository ${project.git.branch ?? "(detached)"}` : "\u2717 Git repository \u672A\u68C0\u6D4B\u5230");
32312
+ const adapters = await detectAdapters(root, project.type);
32313
+ console.log("");
32314
+ console.log("Testing adapters:");
32315
+ for (const adapter of adapters) {
32316
+ const mark = adapter.available ? "\u2713" : "\u2717";
32317
+ console.log(`${mark} ${adapter.id}${adapter.available ? "" : " not detected"} \u2014 ${adapter.detail}`);
32318
+ }
32319
+ console.log("");
32320
+ const config2 = await initTestPilotConfig(root, project, adapters);
32321
+ console.log(
32322
+ config2.yaml === "created" ? "\u2713 \u9879\u76EE\u914D\u7F6E testpilot.yaml" : "\u2713 \u9879\u76EE\u914D\u7F6E testpilot.yaml(\u5DF2\u5B58\u5728,\u4FDD\u7559)"
32323
+ );
32324
+ console.log(`\u2713 Runtime \u76EE\u5F55 ${join25(".testpilot", "artifacts")}`);
32325
+ const serverMode = config2.projectLink.serverUrl ? `Server:${config2.projectLink.serverUrl}` : "\u672C\u5730\u6A21\u5F0F";
32326
+ console.log(`\u2713 \u9879\u76EE\u5173\u8054 ${config2.projectLink.projectId}(${serverMode})`);
32327
+ const testStructure = await initTestStructure(root, project);
32328
+ const reuseNote = project.existingE2eDirs.filter((dir) => dir !== "tests/e2e");
32329
+ console.log(
32330
+ 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}"
32331
+ );
32332
+ if (reuseNote.length > 0) {
32333
+ console.log(` \u26A0 \u68C0\u6D4B\u5230\u5176\u4ED6 E2E \u76EE\u5F55:${reuseNote.join(", ")}(\u4FDD\u7559\u4E0D\u52A8,\u672A\u5F3A\u5236\u8FC1\u79FB)`);
32334
+ }
32335
+ const skill = await installSkill(root, project, adapters);
32336
+ console.log(
32337
+ skill.core === "created" ? `\u2713 Skill \u5B89\u88C5 ${join25(".agents", "skills", "testpilot")}` : "\u2713 Skill \u5B89\u88C5 .agents/skills/testpilot/(\u5DF2\u5B58\u5728,\u8DF3\u8FC7)"
32338
+ );
32339
+ console.log(
32340
+ skill.references === "created" ? "\u2713 Agent \u4E0A\u4E0B\u6587 references/{project,test-conventions,adapters}.md" : "\u2713 Agent \u4E0A\u4E0B\u6587 references/(\u5DF2\u5B58\u5728,\u4FDD\u7559)"
32341
+ );
32342
+ console.log("\u2713 \u73AF\u5883\u914D\u7F6E environment \u6BB5(test/staging),baseUrl \u7531 TEST_BASE_URL \u7B49\u53D8\u91CF\u6CE8\u5165");
32343
+ const ci = await detectCi(root);
32344
+ if (ci.hasTestpilotWorkflow) {
32345
+ console.log("\u2713 CI .github/workflows/testpilot.yml(\u5DF2\u5B58\u5728)");
32346
+ } else if (ci.githubWorkflows || ci.gitlabCi) {
32347
+ console.log("\u26A0 CI \u68C0\u6D4B\u5230\u5DF2\u6709 CI \u914D\u7F6E,\u672A\u6539\u52A8;\u53EF\u8FD0\u884C npx csspilot ci init \u751F\u6210 TestPilot \u5DE5\u4F5C\u6D41");
32348
+ } else {
32349
+ console.log("\u2139 CI \u672A\u68C0\u6D4B\u5230 CI;\u53EF\u8FD0\u884C npx csspilot ci init \u751F\u6210\u6A21\u677F");
31632
32350
  }
31633
- };
32351
+ console.log("");
32352
+ const doctor = await runDoctor(root);
32353
+ printDoctor(doctor);
32354
+ console.log("\u4E0B\u4E00\u6B65:");
32355
+ console.log(" 1. \u8BA9 AI \u9605\u8BFB .agents/skills/testpilot/SKILL.md(\u9879\u76EE\u4E0A\u4E0B\u6587\u89C1 references/)");
32356
+ console.log(" 2. \u5728 tests/e2e/cases/ \u521B\u5EFA Case(\u9075\u5FAA references/test-conventions.md)");
32357
+ console.log(" 3. npx csspilot validate && npx csspilot run");
32358
+ return { project, adapters, config: config2, testStructure, skill, doctor };
32359
+ }
32360
+ function makeInitCommand() {
32361
+ return new Command("init").description("\u628A TestPilot \u63A5\u5165\u5F53\u524D\u4E1A\u52A1\u9879\u76EE(\u5E42\u7B49,\u53EF\u91CD\u590D\u6267\u884C)").option("--server <url>", "TestPilot Server \u5730\u5740(\u9ED8\u8BA4\u8BFB TESTPILOT_SERVER_URL / testpilot.yaml)").action(async (options) => {
32362
+ try {
32363
+ await runInit(process.cwd(), options.server ? { serverUrl: options.server } : {});
32364
+ } catch (err) {
32365
+ console.error(`\u2717 init \u5931\u8D25:${err instanceof Error ? err.message : String(err)}`);
32366
+ process.exitCode = 1;
32367
+ }
32368
+ });
32369
+ }
31634
32370
 
31635
- // src/commands/run.ts
31636
- function makeRunCommand() {
31637
- return new Command("run").description("\u6267\u884C\u6D4B\u8BD5\u7528\u4F8B(\u9ED8\u8BA4\u8FD0\u884C testpilot.yaml \u914D\u7F6E\u7684 Case)").argument("[paths...]", "\u7528\u4F8B\u6587\u4EF6\u6216\u76EE\u5F55,\u9ED8\u8BA4\u6309\u9879\u76EE\u914D\u7F6E").option("--tag <tag>", "\u6309\u6807\u7B7E\u8FC7\u6EE4\u7528\u4F8B").action(async (paths, options) => {
31638
- const config2 = await loadTestpilotConfig().catch((err) => {
32371
+ // src/commands/list.ts
32372
+ function makeListCommand() {
32373
+ return new Command("list").description("\u5217\u51FA\u6D4B\u8BD5\u7528\u4F8B").option("--tag <tag>", "\u6309\u6807\u7B7E\u8FC7\u6EE4").action(async (options) => {
32374
+ const client = new TestPilotClient();
32375
+ const infos = await client.listCases(void 0, options.tag).catch((err) => {
31639
32376
  console.error(err.message);
31640
32377
  process.exitCode = 1;
31641
32378
  return void 0;
31642
32379
  });
31643
- if (!config2) return;
31644
- const inputs = paths && paths.length > 0 ? paths : [config2.casesDir];
31645
- const files = await collectCaseFiles(inputs);
31646
- if (files.length === 0) {
31647
- console.error(`\u672A\u627E\u5230\u7528\u4F8B\u6587\u4EF6(\u67E5\u627E:${inputs.join(", ")})`);
32380
+ if (!infos) return;
32381
+ if (infos.length === 0) {
32382
+ const config2 = await client.getConfig();
32383
+ console.error(`\u672A\u627E\u5230\u7528\u4F8B\u76EE\u5F55 ${config2.casesDir}`);
32384
+ console.error("\u63D0\u793A:\u5148\u8FD0\u884C npx csspilot init \u63A5\u5165 TestPilot");
31648
32385
  process.exitCode = 1;
31649
32386
  return;
31650
32387
  }
31651
- const cases = [];
31652
- let invalid = 0;
31653
- let tagFiltered = 0;
31654
- for (const file2 of files) {
31655
- const source = await readFile5(file2, "utf8");
31656
- const result = validateCaseSource(source);
31657
- if (!result.ok || !result.data) {
31658
- invalid++;
31659
- console.log(`\u2717 ${file2}(\u6821\u9A8C\u672A\u901A\u8FC7,\u5DF2\u8DF3\u8FC7;\u8FD0\u884C npx csspilot validate \u67E5\u770B\u8BE6\u60C5)`);
31660
- continue;
31661
- }
31662
- if (options.tag && !(result.data.tags ?? []).includes(options.tag)) {
31663
- tagFiltered++;
31664
- continue;
32388
+ console.log("TestPilot Cases");
32389
+ console.log("");
32390
+ for (const item of infos) {
32391
+ if (item.valid) {
32392
+ const tags = item.case?.tags ?? [];
32393
+ const tagLabel = tags.length > 0 ? ` [${tags.join(", ")}]` : "";
32394
+ console.log(`\u2713 ${item.case?.id}${tagLabel}`);
32395
+ console.log(` ${item.file}`);
32396
+ } else {
32397
+ console.log(`\u2717 ${item.file}`);
32398
+ console.log(` ${item.issues[0]?.message ?? "\u89E3\u6790\u5931\u8D25"}`);
31665
32399
  }
31666
- cases.push({ data: result.data, file: file2 });
32400
+ console.log("");
32401
+ }
32402
+ const broken = infos.filter((item) => !item.valid).length;
32403
+ const brokenNote = broken > 0 ? `,${broken} \u4E2A\u6587\u4EF6\u89E3\u6790\u5931\u8D25` : "";
32404
+ const visible = infos.length - broken;
32405
+ console.log(`\u5171 ${visible} \u4E2A\u7528\u4F8B${options.tag ? `(tag: ${options.tag})` : ""}${brokenNote}`);
32406
+ });
32407
+ }
32408
+
32409
+ // src/commands/report.ts
32410
+ import { exec } from "node:child_process";
32411
+ function makeReportCommand() {
32412
+ return new Command("report").description("\u751F\u6210\u6D4B\u8BD5\u62A5\u544A(JSON + HTML)").option("--run <id>", "\u8FD0\u884C\u8BB0\u5F55 ID,\u9ED8\u8BA4\u6700\u8FD1\u4E00\u6B21").option("--open", "\u751F\u6210\u540E\u5728\u6D4F\u89C8\u5668\u6253\u5F00", false).action(async (options) => {
32413
+ const client = new TestPilotClient();
32414
+ try {
32415
+ const report = await client.generateReport(options.run);
32416
+ console.log(`\u2713 report.json ${report.json}`);
32417
+ console.log(`\u2713 report.html ${report.html}`);
32418
+ if (options.open) await openInBrowser(report.html);
32419
+ } catch (err) {
32420
+ console.error(err instanceof Error ? err.message : err);
32421
+ process.exitCode = 1;
31667
32422
  }
31668
- if (cases.length === 0) {
31669
- console.error("\u6CA1\u6709\u53EF\u6267\u884C\u7684\u7528\u4F8B");
32423
+ });
32424
+ }
32425
+ function openInBrowser(file2) {
32426
+ const command = process.platform === "win32" ? `start "" "${file2}"` : process.platform === "darwin" ? `open "${file2}"` : `xdg-open "${file2}"`;
32427
+ return new Promise((resolvePromise) => {
32428
+ exec(command, () => resolvePromise());
32429
+ });
32430
+ }
32431
+
32432
+ // src/commands/run.ts
32433
+ import { join as join26 } from "node:path";
32434
+ function makeRunCommand() {
32435
+ return new Command("run").description("\u6267\u884C\u6D4B\u8BD5\u7528\u4F8B(\u9ED8\u8BA4\u8FD0\u884C testpilot.yaml \u914D\u7F6E\u7684 Case)").argument("[paths...]", "\u7528\u4F8B\u6587\u4EF6\u6216\u76EE\u5F55,\u9ED8\u8BA4\u6309\u9879\u76EE\u914D\u7F6E").option("--tag <tag>", "\u6309\u6807\u7B7E\u8FC7\u6EE4\u7528\u4F8B").action(async (paths, options) => {
32436
+ const client = new TestPilotClient();
32437
+ try {
32438
+ const { summary, invalid, tagFiltered } = await client.runCases({
32439
+ paths,
32440
+ tag: options.tag,
32441
+ onEvent: consoleEventHandler
32442
+ });
32443
+ const notes = [
32444
+ invalid.length > 0 ? `${invalid.length} \u4E2A\u6821\u9A8C\u5931\u8D25\u8DF3\u8FC7` : "",
32445
+ tagFiltered > 0 ? `${tagFiltered} \u4E2A\u88AB --tag \u8FC7\u6EE4` : ""
32446
+ ].filter(Boolean).join(",");
32447
+ console.log("");
32448
+ console.log(
32449
+ `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})` : ""}`
32450
+ );
32451
+ console.log(`\u4EA7\u7269\u76EE\u5F55:${join26(".testpilot", "artifacts", "runs", summary.runId)}`);
32452
+ console.log("\u67E5\u770B\u62A5\u544A:npx csspilot report");
32453
+ if (summary.status === "failed") process.exitCode = 1;
32454
+ } catch (err) {
32455
+ console.error(err instanceof Error ? err.message : err);
31670
32456
  process.exitCode = 1;
31671
- return;
31672
32457
  }
31673
- const resolver = new AdapterResolver();
31674
- resolver.register(playwrightAdapterFactory({ baseUrl: config2.web?.baseUrl }));
31675
- resolver.register(
31676
- wechatideAdapterFactory({
31677
- projectPath: config2.miniapp?.projectPath,
31678
- cliPath: config2.miniapp?.cliPath
31679
- })
31680
- );
31681
- const runner = new TestRunner({ resolver, onEvent: consoleEventHandler });
31682
- const notes = [
31683
- invalid > 0 ? `${invalid} \u4E2A\u6821\u9A8C\u5931\u8D25\u8DF3\u8FC7` : "",
31684
- tagFiltered > 0 ? `${tagFiltered} \u4E2A\u88AB --tag \u8FC7\u6EE4` : ""
31685
- ].filter(Boolean).join(",");
31686
- console.log(`\u8FD0\u884C ${cases.length} \u4E2A\u7528\u4F8B${notes ? `(${notes})` : ""}
31687
- `);
31688
- const summary = await runner.run(cases);
31689
- console.log("");
31690
- console.log(
31691
- `Run ${summary.runId}: ${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}`
31692
- );
31693
- console.log(`\u4EA7\u7269\u76EE\u5F55:${join16(".testpilot", "artifacts", "runs", summary.runId)}`);
31694
- console.log("\u67E5\u770B\u62A5\u544A:npx csspilot report");
31695
- if (summary.status === "failed") process.exitCode = 1;
31696
32458
  });
31697
32459
  }
31698
32460
  function consoleEventHandler(event) {
31699
32461
  switch (event.type) {
32462
+ case "run-started":
32463
+ console.log(`\u8FD0\u884C ${event.totalCases} \u4E2A\u7528\u4F8B
32464
+ `);
32465
+ break;
31700
32466
  case "case-started":
31701
32467
  console.log(`\u25B6 ${event.caseId} (${event.file})`);
31702
32468
  break;
@@ -31712,49 +32478,117 @@ function consoleEventHandler(event) {
31712
32478
  }
31713
32479
  }
31714
32480
 
32481
+ // src/commands/sync-metadata.ts
32482
+ import { relative } from "node:path";
32483
+ function makeSyncMetadataCommand() {
32484
+ 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) => {
32485
+ const client = new TestPilotClient();
32486
+ const link = await client.getProjectLink();
32487
+ if (!link) {
32488
+ console.error("\u672A\u627E\u5230 .testpilot/project.json(\u5148\u8FD0\u884C npx csspilot init \u5173\u8054\u9879\u76EE)");
32489
+ process.exitCode = 1;
32490
+ return;
32491
+ }
32492
+ const config2 = await client.getConfig().catch(() => void 0);
32493
+ const serverUrl = options.server ?? process.env.TESTPILOT_SERVER_URL ?? link.serverUrl ?? config2?.server?.baseUrl;
32494
+ if (!serverUrl) {
32495
+ console.error("\u672A\u914D\u7F6E TestPilot Server \u5730\u5740(testpilot.yaml server.baseUrl \u6216 TESTPILOT_SERVER_URL)");
32496
+ process.exitCode = 1;
32497
+ return;
32498
+ }
32499
+ const git = await readGitInfo(process.cwd());
32500
+ if (!git.commit || !git.branch) {
32501
+ console.error("\u5F53\u524D\u76EE\u5F55\u4E0D\u662F Git \u4ED3\u5E93(\u6216\u6CA1\u6709\u4EFB\u4F55\u63D0\u4EA4);sync-metadata \u4EE5 Git Commit \u4E3A\u540C\u6B65\u5355\u5143");
32502
+ process.exitCode = 1;
32503
+ return;
32504
+ }
32505
+ const infos = await client.listCases();
32506
+ const cases = infos.filter((item) => item.valid && item.case).map((item) => ({
32507
+ id: item.case.id,
32508
+ title: item.case.name,
32509
+ file: relative(process.cwd(), item.file).split("\\").join("/"),
32510
+ tags: item.case.tags ?? []
32511
+ }));
32512
+ const skipped = infos.length - cases.length;
32513
+ const numericId = /(\d+)$/.exec(link.projectId ?? "")?.[1];
32514
+ if (!numericId) {
32515
+ console.error(
32516
+ `\u9879\u76EE\u5173\u8054 ${link.projectId} \u4E0D\u662F Server \u6CE8\u518C\u7684\u9879\u76EE(\u672C\u5730\u6A21\u5F0F);\u8BF7\u91CD\u65B0\u8FD0\u884C npx csspilot init \u5E76\u914D\u7F6E server.baseUrl`
32517
+ );
32518
+ process.exitCode = 1;
32519
+ return;
32520
+ }
32521
+ try {
32522
+ const res = await fetch(
32523
+ `${serverUrl.replace(/\/$/, "")}/api/projects/${numericId}/cases/sync`,
32524
+ {
32525
+ method: "POST",
32526
+ headers: { "content-type": "application/json" },
32527
+ body: JSON.stringify({
32528
+ repository: git.repositoryUrl,
32529
+ branch: git.branch,
32530
+ commit: git.commit,
32531
+ cases
32532
+ })
32533
+ }
32534
+ );
32535
+ if (!res.ok) {
32536
+ throw new Error(`${res.status} ${(await res.text()).slice(0, 200)}`);
32537
+ }
32538
+ const result = await res.json();
32539
+ console.log(`\u2713 \u5DF2\u540C\u6B65 ${serverUrl}`);
32540
+ console.log(` \u5206\u652F/\u63D0\u4EA4:${result.branch} \xB7 ${result.commit.slice(0, 7)}`);
32541
+ console.log(
32542
+ ` \u65B0\u589E ${result.added} \xB7 \u66F4\u65B0 ${result.updated} \xB7 \u5220\u9664 ${result.deleted},\u5F53\u524D active \u5171 ${result.activeTotal} \u4E2A`
32543
+ );
32544
+ if (skipped > 0) console.log(` (\u8DF3\u8FC7 ${skipped} \u4E2A\u6821\u9A8C\u5931\u8D25\u7684\u7528\u4F8B\u6587\u4EF6)`);
32545
+ } catch (err) {
32546
+ console.error(`\u2717 \u540C\u6B65\u5931\u8D25:${err instanceof Error ? err.message : err}`);
32547
+ process.exitCode = 1;
32548
+ }
32549
+ });
32550
+ }
32551
+
31715
32552
  // src/commands/validate.ts
31716
- import { readFile as readFile6 } from "node:fs/promises";
31717
32553
  function makeValidateCommand() {
31718
32554
  return new Command("validate").description("\u6821\u9A8C\u7528\u4F8B DSL").argument("[paths...]", "\u7528\u4F8B\u6587\u4EF6\u6216\u76EE\u5F55,\u9ED8\u8BA4\u6309\u9879\u76EE\u914D\u7F6E").action(async (paths) => {
31719
- const config2 = await loadTestpilotConfig().catch((err) => {
32555
+ const client = new TestPilotClient();
32556
+ const infos = await client.listCases(paths).catch((err) => {
31720
32557
  console.error(err.message);
31721
32558
  process.exitCode = 1;
31722
32559
  return void 0;
31723
32560
  });
31724
- if (!config2) return;
31725
- const inputs = paths && paths.length > 0 ? paths : [config2.casesDir];
31726
- const files = await collectCaseFiles(inputs);
31727
- if (files.length === 0) {
31728
- console.error(`\u672A\u627E\u5230\u7528\u4F8B\u6587\u4EF6(\u67E5\u627E:${inputs.join(", ")})`);
32561
+ if (!infos) return;
32562
+ if (infos.length === 0) {
32563
+ console.error(`\u672A\u627E\u5230\u7528\u4F8B\u6587\u4EF6(\u67E5\u627E:${(paths ?? []).join(", ") || "\u9879\u76EE\u914D\u7F6E\u7684 casesDir"})`);
31729
32564
  console.error("\u63D0\u793A:\u5148\u8FD0\u884C npx csspilot init \u63A5\u5165 TestPilot");
31730
32565
  process.exitCode = 1;
31731
32566
  return;
31732
32567
  }
31733
32568
  let failed = 0;
31734
- for (const file2 of files) {
31735
- const source = await readFile6(file2, "utf8");
31736
- const result = validateCaseSource(source);
31737
- if (result.ok) {
31738
- console.log(`\u2713 ${result.data?.id ?? "(unknown)"} ${file2}`);
32569
+ for (const item of infos) {
32570
+ if (item.valid) {
32571
+ console.log(`\u2713 ${item.case?.id ?? "(unknown)"} ${item.file}`);
31739
32572
  } else {
31740
32573
  failed++;
31741
- console.log(`\u2717 ${file2}`);
31742
- for (const issue2 of result.issues) {
32574
+ console.log(`\u2717 ${item.file}`);
32575
+ for (const issue2 of item.issues) {
31743
32576
  const prefix = issue2.code === "yaml" ? "" : `${issue2.path}: `;
31744
32577
  console.log(` - ${prefix}${issue2.message}`);
31745
32578
  }
31746
32579
  }
31747
32580
  }
31748
32581
  console.log("");
31749
- console.log(`${files.length - failed}/${files.length} \u4E2A\u7528\u4F8B\u901A\u8FC7\u6821\u9A8C`);
32582
+ console.log(`${infos.length - failed}/${infos.length} \u4E2A\u7528\u4F8B\u901A\u8FC7\u6821\u9A8C`);
31750
32583
  if (failed > 0) process.exitCode = 1;
31751
32584
  });
31752
32585
  }
31753
32586
 
31754
32587
  // src/cli.ts
32588
+ var CLI_VERSION = "0.2.0";
31755
32589
  async function createCli() {
31756
32590
  const program2 = new Command();
31757
- program2.name("csspilot").description("TestPilot \u2014 AI Native \u8DE8\u7AEF\u4E1A\u52A1\u6D4B\u8BD5\u57FA\u7840\u8BBE\u65BD").version("0.1.0").addCommand(makeInitCommand()).addCommand(makeValidateCommand()).addCommand(makeListCommand()).addCommand(makeRunCommand()).addCommand(makeReportCommand()).addCommand(makeDoctorCommand());
32591
+ program2.name("csspilot").description("TestPilot \u2014 AI Native \u8DE8\u7AEF\u4E1A\u52A1\u6D4B\u8BD5\u57FA\u7840\u8BBE\u65BD CLI").version(CLI_VERSION).addCommand(makeInitCommand()).addCommand(makeValidateCommand()).addCommand(makeListCommand()).addCommand(makeRunCommand()).addCommand(makeReportCommand()).addCommand(makeSyncMetadataCommand()).addCommand(makeDoctorCommand()).addCommand(makeCiCommand());
31758
32592
  return program2;
31759
32593
  }
31760
32594