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