csspilot 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +367 -49
- package/dist/index.js +367 -49
- package/dist/skills/testpilot/SKILL.md +1 -0
- package/dist/skills/testpilot/manifest.yaml +4 -1
- package/dist/skills/testpilot/rules/dsl.md +50 -12
- package/package.json +1 -1
- /package/dist/skills/testpilot/workflows/{run.md → run-case.md} +0 -0
package/dist/index.js
CHANGED
|
@@ -30173,6 +30173,11 @@ var configSchema = external_exports.strictObject({
|
|
|
30173
30173
|
/** TestPilot Control Plane 地址(sync-metadata / init 关联项目时使用) */
|
|
30174
30174
|
server: external_exports.strictObject({ baseUrl: external_exports.string().min(1).optional() }).optional(),
|
|
30175
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(),
|
|
30176
30181
|
miniapp: external_exports.strictObject({
|
|
30177
30182
|
projectPath: external_exports.string().min(1).optional(),
|
|
30178
30183
|
cliPath: external_exports.string().min(1).optional()
|
|
@@ -30205,6 +30210,7 @@ async function loadTestpilotConfig(cwd = process.cwd()) {
|
|
|
30205
30210
|
workspace: result.data.workspace,
|
|
30206
30211
|
server: result.data.server,
|
|
30207
30212
|
web: result.data.web,
|
|
30213
|
+
api: result.data.api,
|
|
30208
30214
|
miniapp: result.data.miniapp
|
|
30209
30215
|
};
|
|
30210
30216
|
}
|
|
@@ -30307,7 +30313,8 @@ import { mkdir as mkdir3, readFile as readFile3, readdir, stat as stat2, writeFi
|
|
|
30307
30313
|
import { dirname, extname, isAbsolute, join as join6, resolve as resolve2 } from "node:path";
|
|
30308
30314
|
|
|
30309
30315
|
// ../../packages/dsl/src/schema/index.ts
|
|
30310
|
-
var STEP_TARGETS = ["web", "miniapp"];
|
|
30316
|
+
var STEP_TARGETS = ["web", "miniapp", "api"];
|
|
30317
|
+
var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"];
|
|
30311
30318
|
var ACTIONS = [
|
|
30312
30319
|
"launch",
|
|
30313
30320
|
"navigate",
|
|
@@ -30317,7 +30324,8 @@ var ACTIONS = [
|
|
|
30317
30324
|
"wait",
|
|
30318
30325
|
"assert",
|
|
30319
30326
|
"extract",
|
|
30320
|
-
"screenshot"
|
|
30327
|
+
"screenshot",
|
|
30328
|
+
"request"
|
|
30321
30329
|
];
|
|
30322
30330
|
var locatorSchema = external_exports.strictObject({
|
|
30323
30331
|
text: external_exports.string().min(1).optional(),
|
|
@@ -30330,6 +30338,12 @@ var stepSchema = external_exports.strictObject({
|
|
|
30330
30338
|
url: external_exports.string().min(1).optional(),
|
|
30331
30339
|
value: external_exports.string().optional(),
|
|
30332
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(),
|
|
30333
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(),
|
|
30334
30348
|
timeout: external_exports.number().int().positive().optional()
|
|
30335
30349
|
});
|
|
@@ -30388,23 +30402,30 @@ var ADAPTER_SUPPORT = {
|
|
|
30388
30402
|
input: ["web", "miniapp"],
|
|
30389
30403
|
select: ["web"],
|
|
30390
30404
|
wait: ["web", "miniapp"],
|
|
30391
|
-
assert: ["web", "miniapp"],
|
|
30392
|
-
extract: ["web", "miniapp"],
|
|
30393
|
-
screenshot: ["web", "miniapp"]
|
|
30405
|
+
assert: ["web", "miniapp", "api"],
|
|
30406
|
+
extract: ["web", "miniapp", "api"],
|
|
30407
|
+
screenshot: ["web", "miniapp"],
|
|
30408
|
+
request: ["api"]
|
|
30394
30409
|
};
|
|
30395
30410
|
var LOCATOR_REQUIRED = /* @__PURE__ */ new Set(["click", "input", "select", "assert", "extract"]);
|
|
30396
30411
|
var VALUE_REQUIRED = /* @__PURE__ */ new Set(["input", "select"]);
|
|
30397
30412
|
var FIELD_ALLOWED = {
|
|
30398
|
-
url: ["navigate"],
|
|
30399
|
-
value: ["input", "select"],
|
|
30400
|
-
expected: ["assert"],
|
|
30413
|
+
url: ["navigate", "request"],
|
|
30414
|
+
value: ["input", "select", "extract"],
|
|
30415
|
+
expected: ["assert", "request"],
|
|
30401
30416
|
variable: ["extract"],
|
|
30402
|
-
timeout: ["wait"]
|
|
30417
|
+
timeout: ["wait"],
|
|
30418
|
+
method: ["request"],
|
|
30419
|
+
headers: ["request"],
|
|
30420
|
+
body: ["request"]
|
|
30403
30421
|
};
|
|
30404
|
-
var VARIABLE_REF = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
30422
|
+
var VARIABLE_REF = /\$\{([A-Za-z_][A-Za-z0-9_.]*)\}/g;
|
|
30405
30423
|
function validateCase(data) {
|
|
30406
30424
|
const issues = [];
|
|
30407
30425
|
const declared = /* @__PURE__ */ new Set();
|
|
30426
|
+
if (data.accountRef) {
|
|
30427
|
+
declared.add("account");
|
|
30428
|
+
}
|
|
30408
30429
|
data.steps.forEach((step, index) => {
|
|
30409
30430
|
checkStep(step, `steps[${index}]`, declared, issues);
|
|
30410
30431
|
});
|
|
@@ -30427,7 +30448,13 @@ function checkStep(step, at, declared, issues) {
|
|
|
30427
30448
|
message: `action "${step.action}" \u4E0D\u652F\u6301 target "${step.target}"(\u652F\u6301:${supported.join(" / ")})`
|
|
30428
30449
|
});
|
|
30429
30450
|
}
|
|
30430
|
-
|
|
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
|
+
}
|
|
30431
30458
|
if (step.target === "miniapp" && step.locator?.text !== void 0) {
|
|
30432
30459
|
issues.push({
|
|
30433
30460
|
path: `${at}.locator.text`,
|
|
@@ -30448,9 +30475,15 @@ function checkStep(step, at, declared, issues) {
|
|
|
30448
30475
|
if (step.action === "navigate" && !step.url) {
|
|
30449
30476
|
issues.push({ path: `${at}.url`, code: "semantic", message: "navigate \u5FC5\u987B\u63D0\u4F9B url" });
|
|
30450
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
|
+
}
|
|
30451
30481
|
if (VALUE_REQUIRED.has(step.action) && !step.value) {
|
|
30452
30482
|
issues.push({ path: `${at}.value`, code: "semantic", message: `action "${step.action}" \u5FC5\u987B\u63D0\u4F9B value` });
|
|
30453
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
|
+
}
|
|
30454
30487
|
if (step.action === "assert" && !step.expected) {
|
|
30455
30488
|
issues.push({ path: `${at}.expected`, code: "semantic", message: "assert \u5FC5\u987B\u63D0\u4F9B expected" });
|
|
30456
30489
|
}
|
|
@@ -30492,18 +30525,27 @@ function checkLocator(step, at, issues) {
|
|
|
30492
30525
|
}
|
|
30493
30526
|
}
|
|
30494
30527
|
function checkVariableRefs(step, at, declared, issues) {
|
|
30495
|
-
|
|
30496
|
-
|
|
30497
|
-
if (!raw)
|
|
30528
|
+
const refs = [];
|
|
30529
|
+
const collect = (field, raw) => {
|
|
30530
|
+
if (!raw) return;
|
|
30498
30531
|
for (const match of raw.matchAll(VARIABLE_REF)) {
|
|
30499
|
-
|
|
30500
|
-
|
|
30501
|
-
|
|
30502
|
-
|
|
30503
|
-
|
|
30504
|
-
|
|
30505
|
-
|
|
30506
|
-
|
|
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
|
+
});
|
|
30507
30549
|
}
|
|
30508
30550
|
}
|
|
30509
30551
|
}
|
|
@@ -30844,13 +30886,39 @@ var ExecutionContext = class {
|
|
|
30844
30886
|
get(name) {
|
|
30845
30887
|
return this.variables.get(name);
|
|
30846
30888
|
}
|
|
30847
|
-
/**
|
|
30889
|
+
/**
|
|
30890
|
+
* 将字符串中的 ${name} 替换为已提取的变量值;未定义的引用保持原样(校验阶段已拦截)。
|
|
30891
|
+
* 名字允许点号,用于引擎注入的 account.username 一类凭据变量(用户 extract 变量名不含点号)。
|
|
30892
|
+
*/
|
|
30848
30893
|
resolve(template) {
|
|
30849
30894
|
return template.replace(
|
|
30850
|
-
/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g,
|
|
30895
|
+
/\$\{([A-Za-z_][A-Za-z0-9_.]*)\}/g,
|
|
30851
30896
|
(match, name) => this.variables.get(name) ?? match
|
|
30852
30897
|
);
|
|
30853
30898
|
}
|
|
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);
|
|
30921
|
+
}
|
|
30854
30922
|
};
|
|
30855
30923
|
|
|
30856
30924
|
// ../../packages/execution-engine/src/lifecycle/index.ts
|
|
@@ -30902,6 +30970,16 @@ function ndjsonSubscriber(filePath) {
|
|
|
30902
30970
|
};
|
|
30903
30971
|
}
|
|
30904
30972
|
|
|
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;
|
|
30979
|
+
}
|
|
30980
|
+
return redacted;
|
|
30981
|
+
}
|
|
30982
|
+
|
|
30905
30983
|
// ../../packages/adapter-core/src/session.ts
|
|
30906
30984
|
var AdapterSession = class {
|
|
30907
30985
|
constructor(target, factory) {
|
|
@@ -30957,6 +31035,7 @@ var AdapterResolver = class {
|
|
|
30957
31035
|
};
|
|
30958
31036
|
|
|
30959
31037
|
// ../../packages/execution-engine/src/engine/step-executor.ts
|
|
31038
|
+
var SNIPPET_LIMIT = 400;
|
|
30960
31039
|
async function executeStep(step, index, adapter, context, artifacts, options) {
|
|
30961
31040
|
const startedAt = Date.now();
|
|
30962
31041
|
const result = {
|
|
@@ -30991,10 +31070,15 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
|
|
|
30991
31070
|
await adapter.wait(locator, step.timeout);
|
|
30992
31071
|
break;
|
|
30993
31072
|
case "assert":
|
|
30994
|
-
|
|
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
|
+
}
|
|
30995
31079
|
break;
|
|
30996
31080
|
case "extract": {
|
|
30997
|
-
const value = await adapter.extract(locator);
|
|
31081
|
+
const value = step.target === "api" ? await extractApiResponse(adapter, context.resolve(step.value ?? "")) : await adapter.extract(locator);
|
|
30998
31082
|
if (step.variable) {
|
|
30999
31083
|
context.set(step.variable, value);
|
|
31000
31084
|
result.extracted = { [step.variable]: value };
|
|
@@ -31006,6 +31090,31 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
|
|
|
31006
31090
|
result.screenshot = await artifacts.saveStepScreenshot(`${options.screenshotName}.png`, data);
|
|
31007
31091
|
break;
|
|
31008
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
|
+
}
|
|
31009
31118
|
default: {
|
|
31010
31119
|
const exhaustive = step.action;
|
|
31011
31120
|
throw new Error(`\u4E0D\u652F\u6301\u7684 action:${String(exhaustive)}`);
|
|
@@ -31023,6 +31132,39 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
|
|
|
31023
31132
|
result.durationMs = Date.now() - startedAt;
|
|
31024
31133
|
return result;
|
|
31025
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
|
+
}
|
|
31026
31168
|
|
|
31027
31169
|
// ../../packages/execution-engine/src/engine/execution-engine.ts
|
|
31028
31170
|
var ExecutionEngine = class {
|
|
@@ -31030,6 +31172,9 @@ var ExecutionEngine = class {
|
|
|
31030
31172
|
const { runId, file: file2, artifacts, bus, resolver } = options;
|
|
31031
31173
|
const startedAt = /* @__PURE__ */ new Date();
|
|
31032
31174
|
const context = new ExecutionContext();
|
|
31175
|
+
if (data.accountRef && options.accounts?.[data.accountRef] !== void 0) {
|
|
31176
|
+
context.seedAccount(options.accounts[data.accountRef]);
|
|
31177
|
+
}
|
|
31033
31178
|
const steps = [];
|
|
31034
31179
|
const evidenceAdapters = /* @__PURE__ */ new Map();
|
|
31035
31180
|
let caseError;
|
|
@@ -31183,12 +31328,14 @@ var TestRunner = class {
|
|
|
31183
31328
|
customRunId;
|
|
31184
31329
|
extraSubscriber;
|
|
31185
31330
|
signal;
|
|
31331
|
+
accounts;
|
|
31186
31332
|
constructor(options) {
|
|
31187
31333
|
this.resolver = options.resolver;
|
|
31188
31334
|
this.runsRoot = resolve4(options.runsRoot ?? join13(process.cwd(), ".testpilot", "artifacts", "runs"));
|
|
31189
31335
|
this.customRunId = options.runId;
|
|
31190
31336
|
this.extraSubscriber = options.onEvent;
|
|
31191
31337
|
this.signal = options.signal;
|
|
31338
|
+
this.accounts = options.accounts;
|
|
31192
31339
|
}
|
|
31193
31340
|
async run(cases) {
|
|
31194
31341
|
const startedAt = /* @__PURE__ */ new Date();
|
|
@@ -31214,7 +31361,8 @@ var TestRunner = class {
|
|
|
31214
31361
|
file: item.file,
|
|
31215
31362
|
artifacts,
|
|
31216
31363
|
bus,
|
|
31217
|
-
resolver: this.resolver
|
|
31364
|
+
resolver: this.resolver,
|
|
31365
|
+
accounts: this.accounts
|
|
31218
31366
|
})
|
|
31219
31367
|
);
|
|
31220
31368
|
}
|
|
@@ -31611,6 +31759,132 @@ function wechatideAdapterFactory(input2 = {}) {
|
|
|
31611
31759
|
};
|
|
31612
31760
|
}
|
|
31613
31761
|
|
|
31762
|
+
// ../../packages/adapter-api/src/index.ts
|
|
31763
|
+
var ApiAdapter = class {
|
|
31764
|
+
constructor(options = {}) {
|
|
31765
|
+
this.options = options;
|
|
31766
|
+
}
|
|
31767
|
+
target = "api";
|
|
31768
|
+
last;
|
|
31769
|
+
async launch() {
|
|
31770
|
+
}
|
|
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);
|
|
31848
|
+
}
|
|
31849
|
+
async close() {
|
|
31850
|
+
this.last = void 0;
|
|
31851
|
+
}
|
|
31852
|
+
// ---- UI 能力:api 端不支持 ----
|
|
31853
|
+
unsupported(action) {
|
|
31854
|
+
throw new TestPilotError("UNSUPPORTED_ACTION", `api \u7AEF\u4E0D\u652F\u6301 "${action}"(\u4EC5\u652F\u6301 request / assert / extract)`);
|
|
31855
|
+
}
|
|
31856
|
+
async navigate() {
|
|
31857
|
+
this.unsupported("navigate");
|
|
31858
|
+
}
|
|
31859
|
+
async click(_locator) {
|
|
31860
|
+
this.unsupported("click");
|
|
31861
|
+
}
|
|
31862
|
+
async input(_locator, _value) {
|
|
31863
|
+
this.unsupported("input");
|
|
31864
|
+
}
|
|
31865
|
+
async select(_locator, _value) {
|
|
31866
|
+
this.unsupported("select");
|
|
31867
|
+
}
|
|
31868
|
+
async wait(_locator, _timeoutMs) {
|
|
31869
|
+
this.unsupported("wait");
|
|
31870
|
+
}
|
|
31871
|
+
async assert(_locator, _expected) {
|
|
31872
|
+
this.unsupported("assert(locator)");
|
|
31873
|
+
}
|
|
31874
|
+
async extract(_locator) {
|
|
31875
|
+
this.unsupported("extract(locator)");
|
|
31876
|
+
}
|
|
31877
|
+
async screenshot() {
|
|
31878
|
+
this.unsupported("screenshot");
|
|
31879
|
+
}
|
|
31880
|
+
};
|
|
31881
|
+
function apiAdapterFactory(options = {}) {
|
|
31882
|
+
return {
|
|
31883
|
+
target: "api",
|
|
31884
|
+
create: async () => new ApiAdapter(options)
|
|
31885
|
+
};
|
|
31886
|
+
}
|
|
31887
|
+
|
|
31614
31888
|
// ../../packages/sdk/src/runs.ts
|
|
31615
31889
|
var RunError = class extends Error {
|
|
31616
31890
|
code;
|
|
@@ -31626,7 +31900,8 @@ function createDefaultAdapters(config2) {
|
|
|
31626
31900
|
wechatideAdapterFactory({
|
|
31627
31901
|
projectPath: config2.miniapp?.projectPath,
|
|
31628
31902
|
cliPath: config2.miniapp?.cliPath
|
|
31629
|
-
})
|
|
31903
|
+
}),
|
|
31904
|
+
apiAdapterFactory({ baseUrl: config2.api?.baseUrl, timeoutMs: config2.api?.timeoutMs })
|
|
31630
31905
|
];
|
|
31631
31906
|
}
|
|
31632
31907
|
async function runCases(options = {}) {
|
|
@@ -31651,17 +31926,23 @@ async function runCases(options = {}) {
|
|
|
31651
31926
|
const factories = options.adapters ?? createDefaultAdapters(config2);
|
|
31652
31927
|
const resolver = new AdapterResolver();
|
|
31653
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
|
+
];
|
|
31654
31934
|
const runner = new TestRunner({
|
|
31655
31935
|
resolver,
|
|
31656
31936
|
runsRoot: join15(root, RUNS_DIR),
|
|
31657
31937
|
runId: options.runId,
|
|
31658
31938
|
onEvent: options.onEvent,
|
|
31659
|
-
signal: options.signal
|
|
31939
|
+
signal: options.signal,
|
|
31940
|
+
accounts: options.accounts
|
|
31660
31941
|
});
|
|
31661
31942
|
const summary = await runner.run(
|
|
31662
31943
|
runnable.map((item) => ({ data: item.case, file: item.file }))
|
|
31663
31944
|
);
|
|
31664
|
-
return { summary, invalid, tagFiltered };
|
|
31945
|
+
return { summary, invalid, tagFiltered, missingAccounts };
|
|
31665
31946
|
}
|
|
31666
31947
|
function runsRoot(root) {
|
|
31667
31948
|
return join15(resolveRoot(root), RUNS_DIR);
|
|
@@ -31955,11 +32236,18 @@ async function runDoctor(root) {
|
|
|
31955
32236
|
const ok = (label) => ({ status: "ok", label });
|
|
31956
32237
|
const warn = (label, hint) => ({ status: "warn", label, hint });
|
|
31957
32238
|
const error62 = (label, hint) => ({ status: "error", label, hint });
|
|
32239
|
+
const info = await detectProject(root);
|
|
32240
|
+
const runtimeItems = [];
|
|
31958
32241
|
const nodeMajor = Number(process.versions.node.split(".")[0]);
|
|
31959
|
-
push(
|
|
32242
|
+
runtimeItems.push(
|
|
31960
32243
|
nodeMajor >= 22 ? ok(`Node.js ${process.versions.node}`) : error62(`Node.js ${process.versions.node}`, "\u9700\u8981 Node.js >= 22")
|
|
31961
|
-
|
|
31962
|
-
|
|
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);
|
|
31963
32251
|
const projectItems = [
|
|
31964
32252
|
ok(`Project ${info.name}(${info.type})`),
|
|
31965
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"),
|
|
@@ -31969,7 +32257,7 @@ async function runDoctor(root) {
|
|
|
31969
32257
|
push("Project", projectItems);
|
|
31970
32258
|
const config2 = await loadTestpilotConfig(root).catch(() => void 0);
|
|
31971
32259
|
const casesDir = config2?.casesDir ?? "tests/e2e/cases";
|
|
31972
|
-
const cases = await collectCases([casesDir], { root }).catch(() => []);
|
|
32260
|
+
const cases = await collectCases([join21(root, casesDir)], { root }).catch(() => []);
|
|
31973
32261
|
const invalidCount = cases.filter((item) => !item.valid).length;
|
|
31974
32262
|
const testDir = join21(root, "tests", "e2e");
|
|
31975
32263
|
push("Test", [
|
|
@@ -31990,15 +32278,26 @@ async function runDoctor(root) {
|
|
|
31990
32278
|
)
|
|
31991
32279
|
);
|
|
31992
32280
|
const refs = await collectEnvVarRefs(root);
|
|
31993
|
-
|
|
31994
|
-
|
|
31995
|
-
|
|
31996
|
-
|
|
31997
|
-
|
|
31998
|
-
|
|
31999
|
-
|
|
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
|
+
);
|
|
32000
32298
|
}
|
|
32001
|
-
|
|
32299
|
+
if (envItems.length > 0) push("Environment", envItems);
|
|
32300
|
+
const serverUrl = serverConfigured ? process.env.TESTPILOT_SERVER_URL ?? config2?.server?.baseUrl : void 0;
|
|
32002
32301
|
if (serverUrl) {
|
|
32003
32302
|
const link = await readProjectLink(root);
|
|
32004
32303
|
const serverItems = [];
|
|
@@ -32185,7 +32484,7 @@ async function exists4(path) {
|
|
|
32185
32484
|
function resolveSkillSourceDir() {
|
|
32186
32485
|
const fromEnv = process.env.TESTPILOT_SKILL_DIR;
|
|
32187
32486
|
if (fromEnv) return fromEnv;
|
|
32188
|
-
const bundled = fileURLToPath(new URL("
|
|
32487
|
+
const bundled = fileURLToPath(new URL("./skills/testpilot", import.meta.url));
|
|
32189
32488
|
if (existsSync7(bundled)) return bundled;
|
|
32190
32489
|
const inRepo = fileURLToPath(new URL("../../../../../skills/testpilot", import.meta.url));
|
|
32191
32490
|
return existsSync7(inRepo) ? inRepo : void 0;
|
|
@@ -32204,9 +32503,12 @@ async function installSkill(root, info, adapters) {
|
|
|
32204
32503
|
await cp(src, dest, { recursive: true });
|
|
32205
32504
|
core = "created";
|
|
32206
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)";
|
|
32207
32509
|
const refsDir = join23(dest, "references");
|
|
32208
32510
|
const files = [
|
|
32209
|
-
["project.md", () => renderProjectMd(info, adapters)],
|
|
32511
|
+
["project.md", () => renderProjectMd(info, adapters, defaultEnvironment, defaultWorkspace)],
|
|
32210
32512
|
["test-conventions.md", renderTestConventionsMd],
|
|
32211
32513
|
["adapters.md", () => renderAdaptersMd(adapters)]
|
|
32212
32514
|
];
|
|
@@ -32220,7 +32522,7 @@ async function installSkill(root, info, adapters) {
|
|
|
32220
32522
|
}
|
|
32221
32523
|
return { core, references, dest };
|
|
32222
32524
|
}
|
|
32223
|
-
function renderProjectMd(info, adapters) {
|
|
32525
|
+
function renderProjectMd(info, adapters, defaultEnvironment, defaultWorkspace) {
|
|
32224
32526
|
const available = adapters.filter((item) => item.available).map((item) => item.id);
|
|
32225
32527
|
return `# Project
|
|
32226
32528
|
|
|
@@ -32233,8 +32535,8 @@ function renderProjectMd(info, adapters) {
|
|
|
32233
32535
|
- Test directory: tests/e2e
|
|
32234
32536
|
- Case directory: tests/e2e/cases
|
|
32235
32537
|
- Available adapters: ${available.length > 0 ? available.join(", ") : "(none detected)"}
|
|
32236
|
-
- Default environment:
|
|
32237
|
-
- Default workspace:
|
|
32538
|
+
- Default environment: ${defaultEnvironment}
|
|
32539
|
+
- Default workspace: ${defaultWorkspace}
|
|
32238
32540
|
`;
|
|
32239
32541
|
}
|
|
32240
32542
|
function renderTestConventionsMd() {
|
|
@@ -32352,6 +32654,7 @@ async function runInit(root, options = {}) {
|
|
|
32352
32654
|
printDoctor(doctor);
|
|
32353
32655
|
console.log("\u4E0B\u4E00\u6B65:");
|
|
32354
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");
|
|
32355
32658
|
console.log(" 2. \u5728 tests/e2e/cases/ \u521B\u5EFA Case(\u9075\u5FAA references/test-conventions.md)");
|
|
32356
32659
|
console.log(" 3. npx csspilot validate && npx csspilot run");
|
|
32357
32660
|
return { project, adapters, config: config2, testStructure, skill, doctor };
|
|
@@ -32434,11 +32737,17 @@ function makeRunCommand() {
|
|
|
32434
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) => {
|
|
32435
32738
|
const client = new TestPilotClient();
|
|
32436
32739
|
try {
|
|
32437
|
-
const { summary, invalid, tagFiltered } = await client.runCases({
|
|
32740
|
+
const { summary, invalid, tagFiltered, missingAccounts } = await client.runCases({
|
|
32438
32741
|
paths,
|
|
32439
32742
|
tag: options.tag,
|
|
32743
|
+
accounts: collectAccountEnv(),
|
|
32440
32744
|
onEvent: consoleEventHandler
|
|
32441
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
|
+
}
|
|
32442
32751
|
const notes = [
|
|
32443
32752
|
invalid.length > 0 ? `${invalid.length} \u4E2A\u6821\u9A8C\u5931\u8D25\u8DF3\u8FC7` : "",
|
|
32444
32753
|
tagFiltered > 0 ? `${tagFiltered} \u4E2A\u88AB --tag \u8FC7\u6EE4` : ""
|
|
@@ -32456,6 +32765,15 @@ function makeRunCommand() {
|
|
|
32456
32765
|
}
|
|
32457
32766
|
});
|
|
32458
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
|
+
}
|
|
32459
32777
|
function consoleEventHandler(event) {
|
|
32460
32778
|
switch (event.type) {
|
|
32461
32779
|
case "run-started":
|
|
@@ -32584,7 +32902,7 @@ function makeValidateCommand() {
|
|
|
32584
32902
|
}
|
|
32585
32903
|
|
|
32586
32904
|
// src/cli.ts
|
|
32587
|
-
var CLI_VERSION = "0.
|
|
32905
|
+
var CLI_VERSION = "0.3.1";
|
|
32588
32906
|
async function createCli() {
|
|
32589
32907
|
const program2 = new Command();
|
|
32590
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());
|