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/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
- checkLocator(step, at, issues);
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
- for (const field of ["value", "expected"]) {
30496
- const raw = step[field];
30497
- if (!raw) continue;
30528
+ const refs = [];
30529
+ const collect = (field, raw) => {
30530
+ if (!raw) return;
30498
30531
  for (const match of raw.matchAll(VARIABLE_REF)) {
30499
- const name = match[1];
30500
- if (name && !declared.has(name)) {
30501
- issues.push({
30502
- path: `${at}.${field}`,
30503
- code: "semantic",
30504
- message: `\u5F15\u7528\u4E86\u672A\u5B9A\u4E49\u53D8\u91CF "\${${name}}":\u9700\u8981\u5148\u901A\u8FC7 extract \u6B65\u9AA4\u63D0\u53D6`
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
- /** 将字符串中的 ${name} 替换为已提取的变量值;未定义的引用保持原样(校验阶段已拦截) */
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
- await adapter.assert(locator, context.resolve(step.expected ?? ""));
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("Runtime", [
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
- const info = await detectProject(root);
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
- if (refs.length > 0) {
31994
- push(
31995
- "Environment",
31996
- refs.map(
31997
- (name) => process.env[name] !== void 0 ? ok(name) : error62(`${name} missing`, "\u5728\u8FD0\u884C\u73AF\u5883(.env / CI Secret)\u4E2D\u8BBE\u7F6E")
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
- const serverUrl = process.env.TESTPILOT_SERVER_URL ?? config2?.server?.baseUrl;
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 = [];
@@ -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: test(testpilot.yaml environment.default)
32237
- - Default workspace: \u672A\u8BBE\u7F6E(Case \u53EF\u6309\u9700\u58F0\u660E workspace \u5B57\u6BB5)
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.2.0";
32905
+ var CLI_VERSION = "0.3.0";
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());
@@ -47,6 +47,7 @@ npx csspilot run # run cases (file paths / --tag)
47
47
  npx csspilot report # generate JSON + HTML report
48
48
  npx csspilot doctor # health check (config / cases / adapters / env / server)
49
49
  npx csspilot ci init # generate TestPilot CI workflow template
50
+ npx csspilot sync-metadata # sync case metadata to TestPilot Server (CI, after git push)
50
51
  ```
51
52
 
52
53
  ## Project context