csspilot 0.2.0 → 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/dist/bin.js +366 -48
- package/dist/index.js +366 -48
- 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/bin.js
CHANGED
|
@@ -30174,6 +30174,11 @@ var configSchema = external_exports.strictObject({
|
|
|
30174
30174
|
/** TestPilot Control Plane 地址(sync-metadata / init 关联项目时使用) */
|
|
30175
30175
|
server: external_exports.strictObject({ baseUrl: external_exports.string().min(1).optional() }).optional(),
|
|
30176
30176
|
web: external_exports.strictObject({ baseUrl: external_exports.string().min(1).optional() }).optional(),
|
|
30177
|
+
/** API 执行端(target: api):相对 url 的基准地址与超时 */
|
|
30178
|
+
api: external_exports.strictObject({
|
|
30179
|
+
baseUrl: external_exports.string().min(1).optional(),
|
|
30180
|
+
timeoutMs: external_exports.number().int().positive().optional()
|
|
30181
|
+
}).optional(),
|
|
30177
30182
|
miniapp: external_exports.strictObject({
|
|
30178
30183
|
projectPath: external_exports.string().min(1).optional(),
|
|
30179
30184
|
cliPath: external_exports.string().min(1).optional()
|
|
@@ -30206,6 +30211,7 @@ async function loadTestpilotConfig(cwd = process.cwd()) {
|
|
|
30206
30211
|
workspace: result.data.workspace,
|
|
30207
30212
|
server: result.data.server,
|
|
30208
30213
|
web: result.data.web,
|
|
30214
|
+
api: result.data.api,
|
|
30209
30215
|
miniapp: result.data.miniapp
|
|
30210
30216
|
};
|
|
30211
30217
|
}
|
|
@@ -30308,7 +30314,8 @@ import { mkdir as mkdir3, readFile as readFile3, readdir, stat as stat2, writeFi
|
|
|
30308
30314
|
import { dirname, extname, isAbsolute, join as join6, resolve as resolve2 } from "node:path";
|
|
30309
30315
|
|
|
30310
30316
|
// ../../packages/dsl/src/schema/index.ts
|
|
30311
|
-
var STEP_TARGETS = ["web", "miniapp"];
|
|
30317
|
+
var STEP_TARGETS = ["web", "miniapp", "api"];
|
|
30318
|
+
var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"];
|
|
30312
30319
|
var ACTIONS = [
|
|
30313
30320
|
"launch",
|
|
30314
30321
|
"navigate",
|
|
@@ -30318,7 +30325,8 @@ var ACTIONS = [
|
|
|
30318
30325
|
"wait",
|
|
30319
30326
|
"assert",
|
|
30320
30327
|
"extract",
|
|
30321
|
-
"screenshot"
|
|
30328
|
+
"screenshot",
|
|
30329
|
+
"request"
|
|
30322
30330
|
];
|
|
30323
30331
|
var locatorSchema = external_exports.strictObject({
|
|
30324
30332
|
text: external_exports.string().min(1).optional(),
|
|
@@ -30331,6 +30339,12 @@ var stepSchema = external_exports.strictObject({
|
|
|
30331
30339
|
url: external_exports.string().min(1).optional(),
|
|
30332
30340
|
value: external_exports.string().optional(),
|
|
30333
30341
|
expected: external_exports.string().optional(),
|
|
30342
|
+
/** api target:request 的 HTTP 方法(默认 GET) */
|
|
30343
|
+
method: external_exports.enum(HTTP_METHODS).optional(),
|
|
30344
|
+
/** api target:request 的请求头(值支持 ${var} 模板) */
|
|
30345
|
+
headers: external_exports.record(external_exports.string(), external_exports.string()).optional(),
|
|
30346
|
+
/** api target:请求体;对象按 JSON 序列化,字符串原样发送(支持 ${var} 模板) */
|
|
30347
|
+
body: external_exports.union([external_exports.string(), external_exports.record(external_exports.string(), external_exports.unknown())]).optional(),
|
|
30334
30348
|
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(),
|
|
30335
30349
|
timeout: external_exports.number().int().positive().optional()
|
|
30336
30350
|
});
|
|
@@ -30389,23 +30403,30 @@ var ADAPTER_SUPPORT = {
|
|
|
30389
30403
|
input: ["web", "miniapp"],
|
|
30390
30404
|
select: ["web"],
|
|
30391
30405
|
wait: ["web", "miniapp"],
|
|
30392
|
-
assert: ["web", "miniapp"],
|
|
30393
|
-
extract: ["web", "miniapp"],
|
|
30394
|
-
screenshot: ["web", "miniapp"]
|
|
30406
|
+
assert: ["web", "miniapp", "api"],
|
|
30407
|
+
extract: ["web", "miniapp", "api"],
|
|
30408
|
+
screenshot: ["web", "miniapp"],
|
|
30409
|
+
request: ["api"]
|
|
30395
30410
|
};
|
|
30396
30411
|
var LOCATOR_REQUIRED = /* @__PURE__ */ new Set(["click", "input", "select", "assert", "extract"]);
|
|
30397
30412
|
var VALUE_REQUIRED = /* @__PURE__ */ new Set(["input", "select"]);
|
|
30398
30413
|
var FIELD_ALLOWED = {
|
|
30399
|
-
url: ["navigate"],
|
|
30400
|
-
value: ["input", "select"],
|
|
30401
|
-
expected: ["assert"],
|
|
30414
|
+
url: ["navigate", "request"],
|
|
30415
|
+
value: ["input", "select", "extract"],
|
|
30416
|
+
expected: ["assert", "request"],
|
|
30402
30417
|
variable: ["extract"],
|
|
30403
|
-
timeout: ["wait"]
|
|
30418
|
+
timeout: ["wait"],
|
|
30419
|
+
method: ["request"],
|
|
30420
|
+
headers: ["request"],
|
|
30421
|
+
body: ["request"]
|
|
30404
30422
|
};
|
|
30405
|
-
var VARIABLE_REF = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
30423
|
+
var VARIABLE_REF = /\$\{([A-Za-z_][A-Za-z0-9_.]*)\}/g;
|
|
30406
30424
|
function validateCase(data) {
|
|
30407
30425
|
const issues = [];
|
|
30408
30426
|
const declared = /* @__PURE__ */ new Set();
|
|
30427
|
+
if (data.accountRef) {
|
|
30428
|
+
declared.add("account");
|
|
30429
|
+
}
|
|
30409
30430
|
data.steps.forEach((step, index) => {
|
|
30410
30431
|
checkStep(step, `steps[${index}]`, declared, issues);
|
|
30411
30432
|
});
|
|
@@ -30428,7 +30449,13 @@ function checkStep(step, at, declared, issues) {
|
|
|
30428
30449
|
message: `action "${step.action}" \u4E0D\u652F\u6301 target "${step.target}"(\u652F\u6301:${supported.join(" / ")})`
|
|
30429
30450
|
});
|
|
30430
30451
|
}
|
|
30431
|
-
|
|
30452
|
+
if (step.target === "api") {
|
|
30453
|
+
if (step.locator) {
|
|
30454
|
+
issues.push({ path: `${at}.locator`, code: "semantic", message: "api \u7AEF\u4E0D\u652F\u6301 locator,\u65AD\u8A00\u7528 expected,\u63D0\u53D6\u7528 value(JSON path)" });
|
|
30455
|
+
}
|
|
30456
|
+
} else {
|
|
30457
|
+
checkLocator(step, at, issues);
|
|
30458
|
+
}
|
|
30432
30459
|
if (step.target === "miniapp" && step.locator?.text !== void 0) {
|
|
30433
30460
|
issues.push({
|
|
30434
30461
|
path: `${at}.locator.text`,
|
|
@@ -30449,9 +30476,15 @@ function checkStep(step, at, declared, issues) {
|
|
|
30449
30476
|
if (step.action === "navigate" && !step.url) {
|
|
30450
30477
|
issues.push({ path: `${at}.url`, code: "semantic", message: "navigate \u5FC5\u987B\u63D0\u4F9B url" });
|
|
30451
30478
|
}
|
|
30479
|
+
if (step.action === "request" && !step.url) {
|
|
30480
|
+
issues.push({ path: `${at}.url`, code: "semantic", message: "request \u5FC5\u987B\u63D0\u4F9B url(\u76F8\u5BF9\u8DEF\u5F84\u6309 api.baseUrl \u89E3\u6790)" });
|
|
30481
|
+
}
|
|
30452
30482
|
if (VALUE_REQUIRED.has(step.action) && !step.value) {
|
|
30453
30483
|
issues.push({ path: `${at}.value`, code: "semantic", message: `action "${step.action}" \u5FC5\u987B\u63D0\u4F9B value` });
|
|
30454
30484
|
}
|
|
30485
|
+
if (step.action === "extract" && step.target === "api" && !step.value) {
|
|
30486
|
+
issues.push({ path: `${at}.value`, code: "semantic", message: "api \u7AEF extract \u5FC5\u987B\u63D0\u4F9B value(\u70B9\u53F7 JSON path,\u5982 data.orderId)" });
|
|
30487
|
+
}
|
|
30455
30488
|
if (step.action === "assert" && !step.expected) {
|
|
30456
30489
|
issues.push({ path: `${at}.expected`, code: "semantic", message: "assert \u5FC5\u987B\u63D0\u4F9B expected" });
|
|
30457
30490
|
}
|
|
@@ -30493,18 +30526,27 @@ function checkLocator(step, at, issues) {
|
|
|
30493
30526
|
}
|
|
30494
30527
|
}
|
|
30495
30528
|
function checkVariableRefs(step, at, declared, issues) {
|
|
30496
|
-
|
|
30497
|
-
|
|
30498
|
-
if (!raw)
|
|
30529
|
+
const refs = [];
|
|
30530
|
+
const collect = (field, raw) => {
|
|
30531
|
+
if (!raw) return;
|
|
30499
30532
|
for (const match of raw.matchAll(VARIABLE_REF)) {
|
|
30500
|
-
|
|
30501
|
-
|
|
30502
|
-
|
|
30503
|
-
|
|
30504
|
-
|
|
30505
|
-
|
|
30506
|
-
|
|
30507
|
-
|
|
30533
|
+
if (match[1]) refs.push([field, match[1]]);
|
|
30534
|
+
}
|
|
30535
|
+
};
|
|
30536
|
+
collect("value", step.value);
|
|
30537
|
+
collect("expected", step.expected);
|
|
30538
|
+
collect("url", step.url);
|
|
30539
|
+
if (typeof step.body === "string") collect("body", step.body);
|
|
30540
|
+
else if (step.body) collect("body", JSON.stringify(step.body));
|
|
30541
|
+
for (const headerValue of Object.values(step.headers ?? {})) collect("headers", headerValue);
|
|
30542
|
+
for (const [field, name] of refs) {
|
|
30543
|
+
const root = name.split(".")[0] ?? name;
|
|
30544
|
+
if (!declared.has(root)) {
|
|
30545
|
+
issues.push({
|
|
30546
|
+
path: `${at}.${field}`,
|
|
30547
|
+
code: "semantic",
|
|
30548
|
+
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.*}`
|
|
30549
|
+
});
|
|
30508
30550
|
}
|
|
30509
30551
|
}
|
|
30510
30552
|
}
|
|
@@ -30845,13 +30887,39 @@ var ExecutionContext = class {
|
|
|
30845
30887
|
get(name) {
|
|
30846
30888
|
return this.variables.get(name);
|
|
30847
30889
|
}
|
|
30848
|
-
/**
|
|
30890
|
+
/**
|
|
30891
|
+
* 将字符串中的 ${name} 替换为已提取的变量值;未定义的引用保持原样(校验阶段已拦截)。
|
|
30892
|
+
* 名字允许点号,用于引擎注入的 account.username 一类凭据变量(用户 extract 变量名不含点号)。
|
|
30893
|
+
*/
|
|
30849
30894
|
resolve(template) {
|
|
30850
30895
|
return template.replace(
|
|
30851
|
-
/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g,
|
|
30896
|
+
/\$\{([A-Za-z_][A-Za-z0-9_.]*)\}/g,
|
|
30852
30897
|
(match, name) => this.variables.get(name) ?? match
|
|
30853
30898
|
);
|
|
30854
30899
|
}
|
|
30900
|
+
/**
|
|
30901
|
+
* 注入 accountRef 凭据:值为 JSON 对象时按 `${account.<字段>}` 展开为字符串,
|
|
30902
|
+
* 非 JSON 值整条注入为 `${account}`。凭据不进日志/取证,只进模板解析。
|
|
30903
|
+
*/
|
|
30904
|
+
seedAccount(rawValue) {
|
|
30905
|
+
let parsed;
|
|
30906
|
+
try {
|
|
30907
|
+
parsed = JSON.parse(rawValue);
|
|
30908
|
+
} catch {
|
|
30909
|
+
parsed = void 0;
|
|
30910
|
+
}
|
|
30911
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
30912
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
30913
|
+
if (value === null || value === void 0) continue;
|
|
30914
|
+
this.variables.set(
|
|
30915
|
+
`account.${key}`,
|
|
30916
|
+
typeof value === "object" ? JSON.stringify(value) : String(value)
|
|
30917
|
+
);
|
|
30918
|
+
}
|
|
30919
|
+
return;
|
|
30920
|
+
}
|
|
30921
|
+
this.variables.set("account", rawValue);
|
|
30922
|
+
}
|
|
30855
30923
|
};
|
|
30856
30924
|
|
|
30857
30925
|
// ../../packages/execution-engine/src/lifecycle/index.ts
|
|
@@ -30903,6 +30971,16 @@ function ndjsonSubscriber(filePath) {
|
|
|
30903
30971
|
};
|
|
30904
30972
|
}
|
|
30905
30973
|
|
|
30974
|
+
// ../../packages/adapter-core/src/adapter.ts
|
|
30975
|
+
var SENSITIVE_HEADERS = /* @__PURE__ */ new Set(["authorization", "cookie", "set-cookie", "x-api-key", "proxy-authorization"]);
|
|
30976
|
+
function redactHeaders(headers) {
|
|
30977
|
+
const redacted = {};
|
|
30978
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
30979
|
+
redacted[key] = SENSITIVE_HEADERS.has(key.toLowerCase()) ? "[REDACTED]" : value;
|
|
30980
|
+
}
|
|
30981
|
+
return redacted;
|
|
30982
|
+
}
|
|
30983
|
+
|
|
30906
30984
|
// ../../packages/adapter-core/src/session.ts
|
|
30907
30985
|
var AdapterSession = class {
|
|
30908
30986
|
constructor(target, factory) {
|
|
@@ -30958,6 +31036,7 @@ var AdapterResolver = class {
|
|
|
30958
31036
|
};
|
|
30959
31037
|
|
|
30960
31038
|
// ../../packages/execution-engine/src/engine/step-executor.ts
|
|
31039
|
+
var SNIPPET_LIMIT = 400;
|
|
30961
31040
|
async function executeStep(step, index, adapter, context, artifacts, options) {
|
|
30962
31041
|
const startedAt = Date.now();
|
|
30963
31042
|
const result = {
|
|
@@ -30992,10 +31071,15 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
|
|
|
30992
31071
|
await adapter.wait(locator, step.timeout);
|
|
30993
31072
|
break;
|
|
30994
31073
|
case "assert":
|
|
30995
|
-
|
|
31074
|
+
if (step.target === "api") {
|
|
31075
|
+
if (!adapter.assertResponse) throw new Error("\u5F53\u524D adapter \u4E0D\u652F\u6301 api \u65AD\u8A00");
|
|
31076
|
+
await adapter.assertResponse(context.resolve(step.expected ?? ""));
|
|
31077
|
+
} else {
|
|
31078
|
+
await adapter.assert(locator, context.resolve(step.expected ?? ""));
|
|
31079
|
+
}
|
|
30996
31080
|
break;
|
|
30997
31081
|
case "extract": {
|
|
30998
|
-
const value = await adapter.extract(locator);
|
|
31082
|
+
const value = step.target === "api" ? await extractApiResponse(adapter, context.resolve(step.value ?? "")) : await adapter.extract(locator);
|
|
30999
31083
|
if (step.variable) {
|
|
31000
31084
|
context.set(step.variable, value);
|
|
31001
31085
|
result.extracted = { [step.variable]: value };
|
|
@@ -31007,6 +31091,31 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
|
|
|
31007
31091
|
result.screenshot = await artifacts.saveStepScreenshot(`${options.screenshotName}.png`, data);
|
|
31008
31092
|
break;
|
|
31009
31093
|
}
|
|
31094
|
+
case "request": {
|
|
31095
|
+
if (!adapter.request) throw new Error("\u5F53\u524D adapter \u4E0D\u652F\u6301 api request");
|
|
31096
|
+
const method = step.method ?? "GET";
|
|
31097
|
+
const url2 = context.resolve(step.url ?? "");
|
|
31098
|
+
const headers = resolveHeaders(step.headers, context);
|
|
31099
|
+
const body = resolveBody(step.body, context);
|
|
31100
|
+
const response = await adapter.request({ method, url: url2, headers, body });
|
|
31101
|
+
result.http = {
|
|
31102
|
+
method: method.toUpperCase(),
|
|
31103
|
+
url: url2,
|
|
31104
|
+
status: response.status,
|
|
31105
|
+
...headers ? { requestHeaders: redactHeaders(headers) } : {},
|
|
31106
|
+
...body !== void 0 ? { requestBody: snippet(body) } : {},
|
|
31107
|
+
...response.bodyText ? { responseSnippet: snippet(response.bodyText) } : {}
|
|
31108
|
+
};
|
|
31109
|
+
if (step.expected !== void 0) {
|
|
31110
|
+
const expectedStatus = context.resolve(step.expected);
|
|
31111
|
+
if (String(response.status) !== expectedStatus) {
|
|
31112
|
+
throw new Error(
|
|
31113
|
+
`HTTP \u72B6\u6001\u7801 ${response.status} ${response.statusText},\u671F\u671B ${expectedStatus}:${snippet(response.bodyText, 200)}`
|
|
31114
|
+
);
|
|
31115
|
+
}
|
|
31116
|
+
}
|
|
31117
|
+
break;
|
|
31118
|
+
}
|
|
31010
31119
|
default: {
|
|
31011
31120
|
const exhaustive = step.action;
|
|
31012
31121
|
throw new Error(`\u4E0D\u652F\u6301\u7684 action:${String(exhaustive)}`);
|
|
@@ -31024,6 +31133,39 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
|
|
|
31024
31133
|
result.durationMs = Date.now() - startedAt;
|
|
31025
31134
|
return result;
|
|
31026
31135
|
}
|
|
31136
|
+
async function extractApiResponse(adapter, path) {
|
|
31137
|
+
if (!adapter.extractResponse) throw new Error("\u5F53\u524D adapter \u4E0D\u652F\u6301 api \u63D0\u53D6");
|
|
31138
|
+
if (!path) throw new Error("api \u7AEF extract \u9700\u8981\u63D0\u4F9B value(\u70B9\u53F7 JSON path)");
|
|
31139
|
+
return adapter.extractResponse(path);
|
|
31140
|
+
}
|
|
31141
|
+
function resolveHeaders(headers, context) {
|
|
31142
|
+
if (!headers) return void 0;
|
|
31143
|
+
const resolved = {};
|
|
31144
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
31145
|
+
resolved[key] = context.resolve(value);
|
|
31146
|
+
}
|
|
31147
|
+
return resolved;
|
|
31148
|
+
}
|
|
31149
|
+
function resolveBody(body, context) {
|
|
31150
|
+
if (body === void 0) return void 0;
|
|
31151
|
+
if (typeof body === "string") return context.resolve(body);
|
|
31152
|
+
return JSON.stringify(resolveDeep(body, context));
|
|
31153
|
+
}
|
|
31154
|
+
function resolveDeep(value, context) {
|
|
31155
|
+
if (typeof value === "string") return context.resolve(value);
|
|
31156
|
+
if (Array.isArray(value)) return value.map((item) => resolveDeep(item, context));
|
|
31157
|
+
if (value !== null && typeof value === "object") {
|
|
31158
|
+
const out = {};
|
|
31159
|
+
for (const [key, item] of Object.entries(value)) {
|
|
31160
|
+
out[key] = resolveDeep(item, context);
|
|
31161
|
+
}
|
|
31162
|
+
return out;
|
|
31163
|
+
}
|
|
31164
|
+
return value;
|
|
31165
|
+
}
|
|
31166
|
+
function snippet(text, limit = SNIPPET_LIMIT) {
|
|
31167
|
+
return text.length > limit ? `${text.slice(0, limit)}\u2026` : text;
|
|
31168
|
+
}
|
|
31027
31169
|
|
|
31028
31170
|
// ../../packages/execution-engine/src/engine/execution-engine.ts
|
|
31029
31171
|
var ExecutionEngine = class {
|
|
@@ -31031,6 +31173,9 @@ var ExecutionEngine = class {
|
|
|
31031
31173
|
const { runId, file: file2, artifacts, bus, resolver } = options;
|
|
31032
31174
|
const startedAt = /* @__PURE__ */ new Date();
|
|
31033
31175
|
const context = new ExecutionContext();
|
|
31176
|
+
if (data.accountRef && options.accounts?.[data.accountRef] !== void 0) {
|
|
31177
|
+
context.seedAccount(options.accounts[data.accountRef]);
|
|
31178
|
+
}
|
|
31034
31179
|
const steps = [];
|
|
31035
31180
|
const evidenceAdapters = /* @__PURE__ */ new Map();
|
|
31036
31181
|
let caseError;
|
|
@@ -31184,12 +31329,14 @@ var TestRunner = class {
|
|
|
31184
31329
|
customRunId;
|
|
31185
31330
|
extraSubscriber;
|
|
31186
31331
|
signal;
|
|
31332
|
+
accounts;
|
|
31187
31333
|
constructor(options) {
|
|
31188
31334
|
this.resolver = options.resolver;
|
|
31189
31335
|
this.runsRoot = resolve4(options.runsRoot ?? join13(process.cwd(), ".testpilot", "artifacts", "runs"));
|
|
31190
31336
|
this.customRunId = options.runId;
|
|
31191
31337
|
this.extraSubscriber = options.onEvent;
|
|
31192
31338
|
this.signal = options.signal;
|
|
31339
|
+
this.accounts = options.accounts;
|
|
31193
31340
|
}
|
|
31194
31341
|
async run(cases) {
|
|
31195
31342
|
const startedAt = /* @__PURE__ */ new Date();
|
|
@@ -31215,7 +31362,8 @@ var TestRunner = class {
|
|
|
31215
31362
|
file: item.file,
|
|
31216
31363
|
artifacts,
|
|
31217
31364
|
bus,
|
|
31218
|
-
resolver: this.resolver
|
|
31365
|
+
resolver: this.resolver,
|
|
31366
|
+
accounts: this.accounts
|
|
31219
31367
|
})
|
|
31220
31368
|
);
|
|
31221
31369
|
}
|
|
@@ -31612,6 +31760,132 @@ function wechatideAdapterFactory(input2 = {}) {
|
|
|
31612
31760
|
};
|
|
31613
31761
|
}
|
|
31614
31762
|
|
|
31763
|
+
// ../../packages/adapter-api/src/index.ts
|
|
31764
|
+
var ApiAdapter = class {
|
|
31765
|
+
constructor(options = {}) {
|
|
31766
|
+
this.options = options;
|
|
31767
|
+
}
|
|
31768
|
+
target = "api";
|
|
31769
|
+
last;
|
|
31770
|
+
async launch() {
|
|
31771
|
+
}
|
|
31772
|
+
/** 相对 url 按 baseUrl 解析;已带协议的绝对地址原样发送 */
|
|
31773
|
+
resolveUrl(url2) {
|
|
31774
|
+
if (/^https?:\/\//i.test(url2)) return url2;
|
|
31775
|
+
const base = this.options.baseUrl?.replace(/\/$/, "");
|
|
31776
|
+
if (!base) {
|
|
31777
|
+
throw new TestPilotError("CONFIG_MISSING", `\u76F8\u5BF9 url "${url2}" \u9700\u8981\u5728 testpilot.yaml \u914D\u7F6E api.baseUrl`);
|
|
31778
|
+
}
|
|
31779
|
+
return `${base}/${url2.replace(/^\//, "")}`;
|
|
31780
|
+
}
|
|
31781
|
+
async request(request) {
|
|
31782
|
+
const url2 = this.resolveUrl(request.url);
|
|
31783
|
+
const headers = { ...request.headers ?? {} };
|
|
31784
|
+
let body;
|
|
31785
|
+
if (request.body !== void 0) {
|
|
31786
|
+
if (typeof request.body === "string") {
|
|
31787
|
+
body = request.body;
|
|
31788
|
+
} else {
|
|
31789
|
+
body = JSON.stringify(request.body);
|
|
31790
|
+
headers["content-type"] ??= "application/json";
|
|
31791
|
+
}
|
|
31792
|
+
}
|
|
31793
|
+
let res;
|
|
31794
|
+
try {
|
|
31795
|
+
res = await fetch(url2, {
|
|
31796
|
+
method: request.method.toUpperCase(),
|
|
31797
|
+
headers,
|
|
31798
|
+
body,
|
|
31799
|
+
signal: AbortSignal.timeout(this.options.timeoutMs ?? 15e3)
|
|
31800
|
+
});
|
|
31801
|
+
} catch (err) {
|
|
31802
|
+
throw new Error(`API \u8BF7\u6C42\u5931\u8D25 ${request.method.toUpperCase()} ${url2}:${err instanceof Error ? err.message : String(err)}`);
|
|
31803
|
+
}
|
|
31804
|
+
const bodyText = await res.text();
|
|
31805
|
+
let json2;
|
|
31806
|
+
if (bodyText) {
|
|
31807
|
+
try {
|
|
31808
|
+
json2 = JSON.parse(bodyText);
|
|
31809
|
+
} catch {
|
|
31810
|
+
json2 = void 0;
|
|
31811
|
+
}
|
|
31812
|
+
}
|
|
31813
|
+
const responseHeaders = {};
|
|
31814
|
+
res.headers.forEach((value, key) => {
|
|
31815
|
+
responseHeaders[key] = value;
|
|
31816
|
+
});
|
|
31817
|
+
this.last = { status: res.status, statusText: res.statusText, headers: responseHeaders, bodyText, json: json2 };
|
|
31818
|
+
return this.last;
|
|
31819
|
+
}
|
|
31820
|
+
async assertResponse(expected) {
|
|
31821
|
+
if (!this.last) throw new Error("\u5C1A\u672A\u53D1\u8D77\u4EFB\u4F55 API \u8BF7\u6C42,\u65E0\u6CD5\u65AD\u8A00\u54CD\u5E94");
|
|
31822
|
+
if (!this.last.bodyText.includes(expected)) {
|
|
31823
|
+
const snippet2 = this.last.bodyText.slice(0, 200);
|
|
31824
|
+
throw new Error(`\u54CD\u5E94\u4F53\u4E0D\u5305\u542B "${expected}":HTTP ${this.last.status} ${snippet2}`);
|
|
31825
|
+
}
|
|
31826
|
+
}
|
|
31827
|
+
async extractResponse(path) {
|
|
31828
|
+
if (!this.last) throw new Error("\u5C1A\u672A\u53D1\u8D77\u4EFB\u4F55 API \u8BF7\u6C42,\u65E0\u6CD5\u63D0\u53D6\u54CD\u5E94\u5B57\u6BB5");
|
|
31829
|
+
let current = this.last.json;
|
|
31830
|
+
if (current === void 0) throw new Error("\u54CD\u5E94\u4F53\u4E0D\u662F JSON,\u65E0\u6CD5\u6309 path \u63D0\u53D6");
|
|
31831
|
+
for (const segment of path.split(".")) {
|
|
31832
|
+
if (current === null || current === void 0) {
|
|
31833
|
+
throw new Error(`JSON path "${path}" \u5728 "${segment}" \u5904\u4E2D\u65AD:\u503C\u4E3A\u7A7A`);
|
|
31834
|
+
}
|
|
31835
|
+
if (Array.isArray(current)) {
|
|
31836
|
+
const index = Number(segment);
|
|
31837
|
+
if (!Number.isInteger(index) || index < 0 || index >= current.length) {
|
|
31838
|
+
throw new Error(`JSON path "${path}":\u6570\u7EC4\u4E0B\u6807 "${segment}" \u8D8A\u754C`);
|
|
31839
|
+
}
|
|
31840
|
+
current = current[index];
|
|
31841
|
+
} else if (typeof current === "object") {
|
|
31842
|
+
current = current[segment];
|
|
31843
|
+
} else {
|
|
31844
|
+
throw new Error(`JSON path "${path}" \u5728 "${segment}" \u5904\u4E2D\u65AD:\u5DF2\u662F\u6807\u91CF\u503C`);
|
|
31845
|
+
}
|
|
31846
|
+
}
|
|
31847
|
+
if (current === null || current === void 0) throw new Error(`JSON path "${path}" \u7684\u503C\u4E3A\u7A7A`);
|
|
31848
|
+
return typeof current === "object" ? JSON.stringify(current) : String(current);
|
|
31849
|
+
}
|
|
31850
|
+
async close() {
|
|
31851
|
+
this.last = void 0;
|
|
31852
|
+
}
|
|
31853
|
+
// ---- UI 能力:api 端不支持 ----
|
|
31854
|
+
unsupported(action) {
|
|
31855
|
+
throw new TestPilotError("UNSUPPORTED_ACTION", `api \u7AEF\u4E0D\u652F\u6301 "${action}"(\u4EC5\u652F\u6301 request / assert / extract)`);
|
|
31856
|
+
}
|
|
31857
|
+
async navigate() {
|
|
31858
|
+
this.unsupported("navigate");
|
|
31859
|
+
}
|
|
31860
|
+
async click(_locator) {
|
|
31861
|
+
this.unsupported("click");
|
|
31862
|
+
}
|
|
31863
|
+
async input(_locator, _value) {
|
|
31864
|
+
this.unsupported("input");
|
|
31865
|
+
}
|
|
31866
|
+
async select(_locator, _value) {
|
|
31867
|
+
this.unsupported("select");
|
|
31868
|
+
}
|
|
31869
|
+
async wait(_locator, _timeoutMs) {
|
|
31870
|
+
this.unsupported("wait");
|
|
31871
|
+
}
|
|
31872
|
+
async assert(_locator, _expected) {
|
|
31873
|
+
this.unsupported("assert(locator)");
|
|
31874
|
+
}
|
|
31875
|
+
async extract(_locator) {
|
|
31876
|
+
this.unsupported("extract(locator)");
|
|
31877
|
+
}
|
|
31878
|
+
async screenshot() {
|
|
31879
|
+
this.unsupported("screenshot");
|
|
31880
|
+
}
|
|
31881
|
+
};
|
|
31882
|
+
function apiAdapterFactory(options = {}) {
|
|
31883
|
+
return {
|
|
31884
|
+
target: "api",
|
|
31885
|
+
create: async () => new ApiAdapter(options)
|
|
31886
|
+
};
|
|
31887
|
+
}
|
|
31888
|
+
|
|
31615
31889
|
// ../../packages/sdk/src/runs.ts
|
|
31616
31890
|
var RunError = class extends Error {
|
|
31617
31891
|
code;
|
|
@@ -31627,7 +31901,8 @@ function createDefaultAdapters(config2) {
|
|
|
31627
31901
|
wechatideAdapterFactory({
|
|
31628
31902
|
projectPath: config2.miniapp?.projectPath,
|
|
31629
31903
|
cliPath: config2.miniapp?.cliPath
|
|
31630
|
-
})
|
|
31904
|
+
}),
|
|
31905
|
+
apiAdapterFactory({ baseUrl: config2.api?.baseUrl, timeoutMs: config2.api?.timeoutMs })
|
|
31631
31906
|
];
|
|
31632
31907
|
}
|
|
31633
31908
|
async function runCases(options = {}) {
|
|
@@ -31652,17 +31927,23 @@ async function runCases(options = {}) {
|
|
|
31652
31927
|
const factories = options.adapters ?? createDefaultAdapters(config2);
|
|
31653
31928
|
const resolver = new AdapterResolver();
|
|
31654
31929
|
for (const factory of factories) resolver.register(factory);
|
|
31930
|
+
const missingAccounts = [
|
|
31931
|
+
...new Set(
|
|
31932
|
+
runnable.map((item) => item.case?.accountRef).filter((ref) => !!ref && options.accounts?.[ref] === void 0)
|
|
31933
|
+
)
|
|
31934
|
+
];
|
|
31655
31935
|
const runner = new TestRunner({
|
|
31656
31936
|
resolver,
|
|
31657
31937
|
runsRoot: join15(root, RUNS_DIR),
|
|
31658
31938
|
runId: options.runId,
|
|
31659
31939
|
onEvent: options.onEvent,
|
|
31660
|
-
signal: options.signal
|
|
31940
|
+
signal: options.signal,
|
|
31941
|
+
accounts: options.accounts
|
|
31661
31942
|
});
|
|
31662
31943
|
const summary = await runner.run(
|
|
31663
31944
|
runnable.map((item) => ({ data: item.case, file: item.file }))
|
|
31664
31945
|
);
|
|
31665
|
-
return { summary, invalid, tagFiltered };
|
|
31946
|
+
return { summary, invalid, tagFiltered, missingAccounts };
|
|
31666
31947
|
}
|
|
31667
31948
|
function runsRoot(root) {
|
|
31668
31949
|
return join15(resolveRoot(root), RUNS_DIR);
|
|
@@ -31956,11 +32237,18 @@ async function runDoctor(root) {
|
|
|
31956
32237
|
const ok = (label) => ({ status: "ok", label });
|
|
31957
32238
|
const warn = (label, hint) => ({ status: "warn", label, hint });
|
|
31958
32239
|
const error62 = (label, hint) => ({ status: "error", label, hint });
|
|
32240
|
+
const info = await detectProject(root);
|
|
32241
|
+
const runtimeItems = [];
|
|
31959
32242
|
const nodeMajor = Number(process.versions.node.split(".")[0]);
|
|
31960
|
-
push(
|
|
32243
|
+
runtimeItems.push(
|
|
31961
32244
|
nodeMajor >= 22 ? ok(`Node.js ${process.versions.node}`) : error62(`Node.js ${process.versions.node}`, "\u9700\u8981 Node.js >= 22")
|
|
31962
|
-
|
|
31963
|
-
|
|
32245
|
+
);
|
|
32246
|
+
if (info.packageManager !== "npx") runtimeItems.push(ok(`Package Manager ${info.packageManager}`));
|
|
32247
|
+
else runtimeItems.push(warn("Package Manager \u672A\u8BC6\u522B", "\u672A\u53D1\u73B0 pnpm/yarn/npm lockfile"));
|
|
32248
|
+
runtimeItems.push(
|
|
32249
|
+
info.git.commit ? ok("Git") : warn("Git \u672A\u68C0\u6D4B\u5230", "Case Metadata \u65E0\u6CD5\u540C\u6B65\u5230 Server")
|
|
32250
|
+
);
|
|
32251
|
+
push("Runtime", runtimeItems);
|
|
31964
32252
|
const projectItems = [
|
|
31965
32253
|
ok(`Project ${info.name}(${info.type})`),
|
|
31966
32254
|
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"),
|
|
@@ -31970,7 +32258,7 @@ async function runDoctor(root) {
|
|
|
31970
32258
|
push("Project", projectItems);
|
|
31971
32259
|
const config2 = await loadTestpilotConfig(root).catch(() => void 0);
|
|
31972
32260
|
const casesDir = config2?.casesDir ?? "tests/e2e/cases";
|
|
31973
|
-
const cases = await collectCases([casesDir], { root }).catch(() => []);
|
|
32261
|
+
const cases = await collectCases([join21(root, casesDir)], { root }).catch(() => []);
|
|
31974
32262
|
const invalidCount = cases.filter((item) => !item.valid).length;
|
|
31975
32263
|
const testDir = join21(root, "tests", "e2e");
|
|
31976
32264
|
push("Test", [
|
|
@@ -31991,15 +32279,26 @@ async function runDoctor(root) {
|
|
|
31991
32279
|
)
|
|
31992
32280
|
);
|
|
31993
32281
|
const refs = await collectEnvVarRefs(root);
|
|
31994
|
-
|
|
31995
|
-
|
|
31996
|
-
|
|
31997
|
-
|
|
31998
|
-
|
|
31999
|
-
|
|
32000
|
-
|
|
32282
|
+
const envItems = refs.map(
|
|
32283
|
+
(name) => process.env[name] !== void 0 ? ok(name) : error62(`${name} missing`, "\u5728\u8FD0\u884C\u73AF\u5883(.env / CI Secret)\u4E2D\u8BBE\u7F6E")
|
|
32284
|
+
);
|
|
32285
|
+
const serverConfigured = (process.env.TESTPILOT_SERVER_URL ?? config2?.server?.baseUrl) !== void 0;
|
|
32286
|
+
const accountRefs = [
|
|
32287
|
+
...new Set(cases.map((item) => item.case?.accountRef).filter((ref) => !!ref))
|
|
32288
|
+
];
|
|
32289
|
+
for (const ref of accountRefs) {
|
|
32290
|
+
const envName = `TESTPILOT_ACCOUNT_${ref.toUpperCase().replace(/-/g, "_")}`;
|
|
32291
|
+
if (process.env[envName] !== void 0) envItems.push(ok(`${envName}(\u8D26\u53F7 ${ref})`));
|
|
32292
|
+
else if (!serverConfigured)
|
|
32293
|
+
envItems.push(
|
|
32294
|
+
warn(
|
|
32295
|
+
`\u8D26\u53F7 ${ref} \u51ED\u636E\u7F3A\u5931`,
|
|
32296
|
+
`\u672C\u5730\u8FD0\u884C\u8BBE\u7F6E ${envName};\u6216\u914D\u7F6E Server \u540E\u6309\u73AF\u5883\u51ED\u636E\u89E3\u6790`
|
|
32297
|
+
)
|
|
32298
|
+
);
|
|
32001
32299
|
}
|
|
32002
|
-
|
|
32300
|
+
if (envItems.length > 0) push("Environment", envItems);
|
|
32301
|
+
const serverUrl = serverConfigured ? process.env.TESTPILOT_SERVER_URL ?? config2?.server?.baseUrl : void 0;
|
|
32003
32302
|
if (serverUrl) {
|
|
32004
32303
|
const link = await readProjectLink(root);
|
|
32005
32304
|
const serverItems = [];
|
|
@@ -32205,9 +32504,12 @@ async function installSkill(root, info, adapters) {
|
|
|
32205
32504
|
await cp(src, dest, { recursive: true });
|
|
32206
32505
|
core = "created";
|
|
32207
32506
|
}
|
|
32507
|
+
const config2 = await loadTestpilotConfig(root).catch(() => void 0);
|
|
32508
|
+
const defaultEnvironment = typeof config2?.environment?.default === "string" ? config2.environment.default : "\u672A\u8BBE\u7F6E";
|
|
32509
|
+
const defaultWorkspace = config2?.workspace?.default ?? "\u672A\u8BBE\u7F6E(\u53EF\u5728 yaml workspace.default \u914D\u7F6E)";
|
|
32208
32510
|
const refsDir = join23(dest, "references");
|
|
32209
32511
|
const files = [
|
|
32210
|
-
["project.md", () => renderProjectMd(info, adapters)],
|
|
32512
|
+
["project.md", () => renderProjectMd(info, adapters, defaultEnvironment, defaultWorkspace)],
|
|
32211
32513
|
["test-conventions.md", renderTestConventionsMd],
|
|
32212
32514
|
["adapters.md", () => renderAdaptersMd(adapters)]
|
|
32213
32515
|
];
|
|
@@ -32221,7 +32523,7 @@ async function installSkill(root, info, adapters) {
|
|
|
32221
32523
|
}
|
|
32222
32524
|
return { core, references, dest };
|
|
32223
32525
|
}
|
|
32224
|
-
function renderProjectMd(info, adapters) {
|
|
32526
|
+
function renderProjectMd(info, adapters, defaultEnvironment, defaultWorkspace) {
|
|
32225
32527
|
const available = adapters.filter((item) => item.available).map((item) => item.id);
|
|
32226
32528
|
return `# Project
|
|
32227
32529
|
|
|
@@ -32234,8 +32536,8 @@ function renderProjectMd(info, adapters) {
|
|
|
32234
32536
|
- Test directory: tests/e2e
|
|
32235
32537
|
- Case directory: tests/e2e/cases
|
|
32236
32538
|
- Available adapters: ${available.length > 0 ? available.join(", ") : "(none detected)"}
|
|
32237
|
-
- Default environment:
|
|
32238
|
-
- Default workspace:
|
|
32539
|
+
- Default environment: ${defaultEnvironment}
|
|
32540
|
+
- Default workspace: ${defaultWorkspace}
|
|
32239
32541
|
`;
|
|
32240
32542
|
}
|
|
32241
32543
|
function renderTestConventionsMd() {
|
|
@@ -32353,6 +32655,7 @@ async function runInit(root, options = {}) {
|
|
|
32353
32655
|
printDoctor(doctor);
|
|
32354
32656
|
console.log("\u4E0B\u4E00\u6B65:");
|
|
32355
32657
|
console.log(" 1. \u8BA9 AI \u9605\u8BFB .agents/skills/testpilot/SKILL.md(\u9879\u76EE\u4E0A\u4E0B\u6587\u89C1 references/)");
|
|
32658
|
+
console.log(" \u53EF\u9009:\u6309\u6240\u7528 Agent \u590D\u5236\u5230 .cursor/skills/\u3001.claude/skills/ \u7B49\u76EE\u5F55");
|
|
32356
32659
|
console.log(" 2. \u5728 tests/e2e/cases/ \u521B\u5EFA Case(\u9075\u5FAA references/test-conventions.md)");
|
|
32357
32660
|
console.log(" 3. npx csspilot validate && npx csspilot run");
|
|
32358
32661
|
return { project, adapters, config: config2, testStructure, skill, doctor };
|
|
@@ -32435,11 +32738,17 @@ function makeRunCommand() {
|
|
|
32435
32738
|
return new Command("run").description("\u6267\u884C\u6D4B\u8BD5\u7528\u4F8B(\u9ED8\u8BA4\u8FD0\u884C testpilot.yaml \u914D\u7F6E\u7684 Case)").argument("[paths...]", "\u7528\u4F8B\u6587\u4EF6\u6216\u76EE\u5F55,\u9ED8\u8BA4\u6309\u9879\u76EE\u914D\u7F6E").option("--tag <tag>", "\u6309\u6807\u7B7E\u8FC7\u6EE4\u7528\u4F8B").action(async (paths, options) => {
|
|
32436
32739
|
const client = new TestPilotClient();
|
|
32437
32740
|
try {
|
|
32438
|
-
const { summary, invalid, tagFiltered } = await client.runCases({
|
|
32741
|
+
const { summary, invalid, tagFiltered, missingAccounts } = await client.runCases({
|
|
32439
32742
|
paths,
|
|
32440
32743
|
tag: options.tag,
|
|
32744
|
+
accounts: collectAccountEnv(),
|
|
32441
32745
|
onEvent: consoleEventHandler
|
|
32442
32746
|
});
|
|
32747
|
+
if (missingAccounts.length > 0) {
|
|
32748
|
+
console.log(
|
|
32749
|
+
`\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)`
|
|
32750
|
+
);
|
|
32751
|
+
}
|
|
32443
32752
|
const notes = [
|
|
32444
32753
|
invalid.length > 0 ? `${invalid.length} \u4E2A\u6821\u9A8C\u5931\u8D25\u8DF3\u8FC7` : "",
|
|
32445
32754
|
tagFiltered > 0 ? `${tagFiltered} \u4E2A\u88AB --tag \u8FC7\u6EE4` : ""
|
|
@@ -32457,6 +32766,15 @@ function makeRunCommand() {
|
|
|
32457
32766
|
}
|
|
32458
32767
|
});
|
|
32459
32768
|
}
|
|
32769
|
+
function collectAccountEnv() {
|
|
32770
|
+
const accounts = {};
|
|
32771
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
32772
|
+
if (!key.startsWith("TESTPILOT_ACCOUNT_") || value === void 0) continue;
|
|
32773
|
+
const ref = key.slice("TESTPILOT_ACCOUNT_".length).toLowerCase().replace(/_/g, "-");
|
|
32774
|
+
if (ref) accounts[ref] = value;
|
|
32775
|
+
}
|
|
32776
|
+
return accounts;
|
|
32777
|
+
}
|
|
32460
32778
|
function consoleEventHandler(event) {
|
|
32461
32779
|
switch (event.type) {
|
|
32462
32780
|
case "run-started":
|
|
@@ -32585,7 +32903,7 @@ function makeValidateCommand() {
|
|
|
32585
32903
|
}
|
|
32586
32904
|
|
|
32587
32905
|
// src/cli.ts
|
|
32588
|
-
var CLI_VERSION = "0.
|
|
32906
|
+
var CLI_VERSION = "0.3.0";
|
|
32589
32907
|
async function createCli() {
|
|
32590
32908
|
const program2 = new Command();
|
|
32591
32909
|
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());
|