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 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
- checkLocator(step, at, issues);
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
- for (const field of ["value", "expected"]) {
30497
- const raw = step[field];
30498
- if (!raw) continue;
30529
+ const refs = [];
30530
+ const collect = (field, raw) => {
30531
+ if (!raw) return;
30499
30532
  for (const match of raw.matchAll(VARIABLE_REF)) {
30500
- const name = match[1];
30501
- if (name && !declared.has(name)) {
30502
- issues.push({
30503
- path: `${at}.${field}`,
30504
- code: "semantic",
30505
- message: `\u5F15\u7528\u4E86\u672A\u5B9A\u4E49\u53D8\u91CF "\${${name}}":\u9700\u8981\u5148\u901A\u8FC7 extract \u6B65\u9AA4\u63D0\u53D6`
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
- /** 将字符串中的 ${name} 替换为已提取的变量值;未定义的引用保持原样(校验阶段已拦截) */
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
- await adapter.assert(locator, context.resolve(step.expected ?? ""));
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("Runtime", [
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
- const info = await detectProject(root);
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
- if (refs.length > 0) {
31995
- push(
31996
- "Environment",
31997
- refs.map(
31998
- (name) => process.env[name] !== void 0 ? ok(name) : error62(`${name} missing`, "\u5728\u8FD0\u884C\u73AF\u5883(.env / CI Secret)\u4E2D\u8BBE\u7F6E")
31999
- )
32000
- );
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
- const serverUrl = process.env.TESTPILOT_SERVER_URL ?? config2?.server?.baseUrl;
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: test(testpilot.yaml environment.default)
32238
- - Default workspace: \u672A\u8BBE\u7F6E(Case \u53EF\u6309\u9700\u58F0\u660E workspace \u5B57\u6BB5)
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.2.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());