csspilot 0.3.2 → 0.4.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 +290 -139
- package/dist/index.js +290 -139
- package/dist/skills/testpilot/SKILL.md +10 -7
- package/dist/skills/testpilot/manifest.yaml +3 -2
- package/dist/skills/testpilot/workflows/create-case.md +10 -7
- package/dist/skills/testpilot/workflows/test-requirements-review.md +61 -0
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -30148,7 +30148,10 @@ async function loadTestpilotConfig(cwd = process.cwd()) {
|
|
|
30148
30148
|
baseUrl: parsed.api.baseUrl === void 0 ? void 0 : interpolateEnvRefs(parsed.api.baseUrl),
|
|
30149
30149
|
timeoutMs: parsed.api.timeoutMs
|
|
30150
30150
|
} : void 0,
|
|
30151
|
-
miniapp: parsed.miniapp
|
|
30151
|
+
miniapp: parsed.miniapp ? {
|
|
30152
|
+
projectPath: parsed.miniapp.projectPath === void 0 ? void 0 : interpolateEnvRefs(parsed.miniapp.projectPath),
|
|
30153
|
+
cliPath: parsed.miniapp.cliPath === void 0 ? void 0 : interpolateEnvRefs(parsed.miniapp.cliPath)
|
|
30154
|
+
} : void 0
|
|
30152
30155
|
};
|
|
30153
30156
|
}
|
|
30154
30157
|
|
|
@@ -30335,8 +30338,8 @@ function makeCiCommand() {
|
|
|
30335
30338
|
}
|
|
30336
30339
|
|
|
30337
30340
|
// src/commands/init/doctor.ts
|
|
30338
|
-
import { existsSync as
|
|
30339
|
-
import { join as
|
|
30341
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
30342
|
+
import { join as join22 } from "node:path";
|
|
30340
30343
|
|
|
30341
30344
|
// ../../packages/sdk/src/client.ts
|
|
30342
30345
|
import { isAbsolute as isAbsolute3, join as join17 } from "node:path";
|
|
@@ -32167,8 +32170,12 @@ async function collectEnvVarRefs(root) {
|
|
|
32167
32170
|
return [];
|
|
32168
32171
|
}
|
|
32169
32172
|
const names = /* @__PURE__ */ new Set();
|
|
32170
|
-
for (const
|
|
32171
|
-
|
|
32173
|
+
for (const line of text.split(/\r?\n/)) {
|
|
32174
|
+
const trimmed = line.trim();
|
|
32175
|
+
if (trimmed.startsWith("#")) continue;
|
|
32176
|
+
for (const match of trimmed.matchAll(ENV_REF_PATTERN)) {
|
|
32177
|
+
names.add(match[1]);
|
|
32178
|
+
}
|
|
32172
32179
|
}
|
|
32173
32180
|
return [...names].sort();
|
|
32174
32181
|
}
|
|
@@ -32260,6 +32267,144 @@ function adaptersFor(type) {
|
|
|
32260
32267
|
return ["playwright"];
|
|
32261
32268
|
}
|
|
32262
32269
|
|
|
32270
|
+
// src/commands/init/skill-installer.ts
|
|
32271
|
+
import { existsSync as existsSync6, readFileSync as readFileSync3 } from "node:fs";
|
|
32272
|
+
import { access as access2, cp, mkdir as mkdir11, writeFile as writeFile9 } from "node:fs/promises";
|
|
32273
|
+
import { dirname as dirname7, join as join21 } from "node:path";
|
|
32274
|
+
import { fileURLToPath } from "node:url";
|
|
32275
|
+
var CORE_FILES = ["SKILL.md", "manifest.yaml"];
|
|
32276
|
+
var CORE_DIRS = ["rules", "workflows"];
|
|
32277
|
+
async function exists3(path) {
|
|
32278
|
+
try {
|
|
32279
|
+
await access2(path);
|
|
32280
|
+
return true;
|
|
32281
|
+
} catch {
|
|
32282
|
+
return false;
|
|
32283
|
+
}
|
|
32284
|
+
}
|
|
32285
|
+
function readManifestVersion(dir) {
|
|
32286
|
+
const manifestPath = join21(dir, "manifest.yaml");
|
|
32287
|
+
if (!existsSync6(manifestPath)) return void 0;
|
|
32288
|
+
return /^version:\s*([^\s#]+)/m.exec(readFileSync3(manifestPath, "utf8"))?.[1];
|
|
32289
|
+
}
|
|
32290
|
+
function resolveSkillSourceDir() {
|
|
32291
|
+
const fromEnv = process.env.TESTPILOT_SKILL_DIR;
|
|
32292
|
+
if (fromEnv) return fromEnv;
|
|
32293
|
+
const bundled = fileURLToPath(new URL("./skills/testpilot", import.meta.url));
|
|
32294
|
+
if (existsSync6(bundled)) return bundled;
|
|
32295
|
+
const inRepo = fileURLToPath(new URL("../../../../../skills/testpilot", import.meta.url));
|
|
32296
|
+
return existsSync6(inRepo) ? inRepo : void 0;
|
|
32297
|
+
}
|
|
32298
|
+
async function copyCoreFiles(src, dest) {
|
|
32299
|
+
for (const name of CORE_FILES) {
|
|
32300
|
+
await cp(join21(src, name), join21(dest, name), { recursive: true, force: true });
|
|
32301
|
+
}
|
|
32302
|
+
for (const dir of CORE_DIRS) {
|
|
32303
|
+
if (existsSync6(join21(src, dir))) {
|
|
32304
|
+
await cp(join21(src, dir), join21(dest, dir), { recursive: true, force: true });
|
|
32305
|
+
}
|
|
32306
|
+
}
|
|
32307
|
+
}
|
|
32308
|
+
async function syncSkillFiles(root, options = {}) {
|
|
32309
|
+
const dest = join21(root, ".agents", "skills", "testpilot");
|
|
32310
|
+
const bundledVersion = readManifestVersion(resolveSkillSourceDir() ?? "");
|
|
32311
|
+
if (!await exists3(join21(dest, "manifest.yaml"))) {
|
|
32312
|
+
const src = resolveSkillSourceDir();
|
|
32313
|
+
if (!src) {
|
|
32314
|
+
throw new Error(
|
|
32315
|
+
"\u65E0\u6CD5\u5B9A\u4F4D TestPilot Skill \u6E90\u76EE\u5F55;\u53EF\u901A\u8FC7\u73AF\u5883\u53D8\u91CF TESTPILOT_SKILL_DIR \u6307\u5B9A skills/testpilot \u8DEF\u5F84"
|
|
32316
|
+
);
|
|
32317
|
+
}
|
|
32318
|
+
await mkdir11(dirname7(dest), { recursive: true });
|
|
32319
|
+
await cp(src, dest, { recursive: true });
|
|
32320
|
+
return { core: "created", bundledVersion, dest };
|
|
32321
|
+
}
|
|
32322
|
+
const installedVersion = readManifestVersion(dest);
|
|
32323
|
+
const bundled = resolveSkillSourceDir();
|
|
32324
|
+
if (!bundled) {
|
|
32325
|
+
return { core: "current", installedVersion, dest };
|
|
32326
|
+
}
|
|
32327
|
+
if (!options.force && installedVersion === bundledVersion) {
|
|
32328
|
+
return { core: "current", installedVersion, bundledVersion, dest };
|
|
32329
|
+
}
|
|
32330
|
+
await copyCoreFiles(bundled, dest);
|
|
32331
|
+
return {
|
|
32332
|
+
core: "updated",
|
|
32333
|
+
installedVersion,
|
|
32334
|
+
bundledVersion,
|
|
32335
|
+
dest
|
|
32336
|
+
};
|
|
32337
|
+
}
|
|
32338
|
+
async function installSkill(root, info, adapters) {
|
|
32339
|
+
const dest = join21(root, ".agents", "skills", "testpilot");
|
|
32340
|
+
const sync = await syncSkillFiles(root);
|
|
32341
|
+
const core = sync.core === "created" ? "created" : sync.core === "updated" ? "updated" : "reused";
|
|
32342
|
+
const coreVersion = sync.core === "updated" || sync.core === "created" ? { from: sync.installedVersion, to: sync.bundledVersion } : void 0;
|
|
32343
|
+
const config2 = await loadTestpilotConfig(root).catch(() => void 0);
|
|
32344
|
+
const defaultEnvironment = typeof config2?.environment?.default === "string" ? config2.environment.default : "\u672A\u8BBE\u7F6E";
|
|
32345
|
+
const defaultWorkspace = config2?.workspace?.default ?? "\u672A\u8BBE\u7F6E(\u53EF\u5728 yaml workspace.default \u914D\u7F6E)";
|
|
32346
|
+
const refsDir = join21(dest, "references");
|
|
32347
|
+
const files = [
|
|
32348
|
+
["project.md", () => renderProjectMd(info, adapters, defaultEnvironment, defaultWorkspace)],
|
|
32349
|
+
["test-conventions.md", renderTestConventionsMd],
|
|
32350
|
+
["adapters.md", () => renderAdaptersMd(adapters)]
|
|
32351
|
+
];
|
|
32352
|
+
let references = "reused";
|
|
32353
|
+
for (const [name, render] of files) {
|
|
32354
|
+
const file2 = join21(refsDir, name);
|
|
32355
|
+
if (await exists3(file2)) continue;
|
|
32356
|
+
await mkdir11(refsDir, { recursive: true });
|
|
32357
|
+
await writeFile9(file2, render(), "utf8");
|
|
32358
|
+
references = "created";
|
|
32359
|
+
}
|
|
32360
|
+
return coreVersion ? { core, coreVersion, references, dest } : { core, references, dest };
|
|
32361
|
+
}
|
|
32362
|
+
function renderProjectMd(info, adapters, defaultEnvironment, defaultWorkspace) {
|
|
32363
|
+
const available = adapters.filter((item) => item.available).map((item) => item.id);
|
|
32364
|
+
return `# Project
|
|
32365
|
+
|
|
32366
|
+
\u81EA\u52A8\u751F\u6210\u4E8E \`csspilot init\`,\u63CF\u8FF0\u672C\u9879\u76EE\u7684 TestPilot \u63A5\u5165\u4FE1\u606F;Agent \u4F18\u5148\u4ECE\u8FD9\u91CC\u4E86\u89E3\u9879\u76EE\u3002
|
|
32367
|
+
|
|
32368
|
+
- Project name: ${info.name}
|
|
32369
|
+
- Project type: ${info.type}
|
|
32370
|
+
- Language: ${info.language}
|
|
32371
|
+
- Package manager: ${info.packageManager}
|
|
32372
|
+
- Test directory: tests/e2e
|
|
32373
|
+
- Case directory: tests/e2e/cases
|
|
32374
|
+
- Available adapters: ${available.length > 0 ? available.join(", ") : "(none detected)"}
|
|
32375
|
+
- Default environment: ${defaultEnvironment}
|
|
32376
|
+
- Default workspace: ${defaultWorkspace}
|
|
32377
|
+
`;
|
|
32378
|
+
}
|
|
32379
|
+
function renderTestConventionsMd() {
|
|
32380
|
+
return `# Test Conventions
|
|
32381
|
+
|
|
32382
|
+
\u672C\u9879\u76EE E2E \u6D4B\u8BD5\u7EA6\u5B9A,Agent \u751F\u6210/\u4FEE\u6539 Case \u65F6\u5FC5\u987B\u9075\u5B88:
|
|
32383
|
+
|
|
32384
|
+
1. E2E Case \u4F4D\u4E8E tests/e2e/cases
|
|
32385
|
+
2. Case \u4F7F\u7528 TestPilot DSL
|
|
32386
|
+
3. \u4E0D\u5141\u8BB8\u76F4\u63A5\u8C03\u7528 Playwright
|
|
32387
|
+
4. \u4E0D\u5141\u8BB8\u76F4\u63A5\u8C03\u7528 WeChatIDE
|
|
32388
|
+
5. \u4E0D\u5141\u8BB8\u5728 Case \u4E2D\u5199\u660E\u6587\u5BC6\u7801(\u7528 accountRef \u5F15\u7528)
|
|
32389
|
+
6. \u4F18\u5148\u590D\u7528\u5DF2\u6709 Case
|
|
32390
|
+
7. \u65B0\u589E Case \u5FC5\u987B\u901A\u8FC7 csspilot validate
|
|
32391
|
+
8. Case ID \u5FC5\u987B\u552F\u4E00
|
|
32392
|
+
`;
|
|
32393
|
+
}
|
|
32394
|
+
function renderAdaptersMd(adapters) {
|
|
32395
|
+
const lines = adapters.map(
|
|
32396
|
+
(item) => `- ${item.id}: ${item.available ? "\u53EF\u7528" : "\u4E0D\u53EF\u7528"}${item.required ? "(\u9879\u76EE\u9700\u8981)" : "(\u53EF\u9009)"} \u2014 ${item.detail}`
|
|
32397
|
+
);
|
|
32398
|
+
return `# Adapters
|
|
32399
|
+
|
|
32400
|
+
\u6267\u884C\u7AEF\u68C0\u6D4B\u7ED3\u679C(\u4EE5 \`csspilot doctor\` \u6700\u65B0\u8F93\u51FA\u4E3A\u51C6):
|
|
32401
|
+
|
|
32402
|
+
${lines.join("\n")}
|
|
32403
|
+
|
|
32404
|
+
Agent \u6C38\u8FDC\u4E0D\u76F4\u63A5\u64CD\u4F5C Adapter;Case \u7ECF Execution Engine \u8C03\u5EA6\u5230\u5BF9\u5E94 Adapter \u6267\u884C\u3002
|
|
32405
|
+
`;
|
|
32406
|
+
}
|
|
32407
|
+
|
|
32263
32408
|
// src/commands/init/doctor.ts
|
|
32264
32409
|
async function runDoctor(root) {
|
|
32265
32410
|
const sections = [];
|
|
@@ -32284,25 +32429,37 @@ async function runDoctor(root) {
|
|
|
32284
32429
|
const projectItems = [
|
|
32285
32430
|
ok(`Project ${info.name}(${info.type})`),
|
|
32286
32431
|
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"),
|
|
32287
|
-
|
|
32288
|
-
|
|
32432
|
+
existsSync7(join22(root, ".testpilot", "project.json")) ? ok(".testpilot/project.json") : error62(".testpilot/project.json", "\u8FD0\u884C npx csspilot init \u751F\u6210"),
|
|
32433
|
+
existsSync7(join22(root, "testpilot.yaml")) ? ok("testpilot.yaml") : error62("testpilot.yaml", "\u8FD0\u884C npx csspilot init \u751F\u6210")
|
|
32289
32434
|
];
|
|
32290
32435
|
push("Project", projectItems);
|
|
32291
32436
|
const config2 = await loadTestpilotConfig(root).catch(() => void 0);
|
|
32292
32437
|
const casesDir = config2?.casesDir ?? "tests/e2e/cases";
|
|
32293
|
-
const cases = await collectCases([
|
|
32438
|
+
const cases = await collectCases([join22(root, casesDir)], { root }).catch(() => []);
|
|
32294
32439
|
const invalidCount = cases.filter((item) => !item.valid).length;
|
|
32295
|
-
const testDir =
|
|
32440
|
+
const testDir = join22(root, "tests", "e2e");
|
|
32296
32441
|
push("Test", [
|
|
32297
|
-
|
|
32298
|
-
|
|
32442
|
+
existsSync7(testDir) ? ok("tests/e2e") : error62("tests/e2e", "\u8FD0\u884C npx csspilot init \u521B\u5EFA"),
|
|
32443
|
+
existsSync7(join22(root, casesDir)) ? ok(`cases directory(${casesDir})`) : error62(`cases directory(${casesDir})`, "\u8FD0\u884C npx csspilot init \u521B\u5EFA"),
|
|
32299
32444
|
cases.length === 0 ? warn("Case validation \u8DF3\u8FC7(\u672A\u627E\u5230\u7528\u4F8B)", "\u5728 tests/e2e/cases \u521B\u5EFA Case") : invalidCount === 0 ? ok(`Case validation(${cases.length} \u4E2A\u7528\u4F8B\u5168\u90E8\u6709\u6548)`) : error62(`Case validation(${invalidCount}/${cases.length} \u4E2A\u7528\u4F8B\u6821\u9A8C\u5931\u8D25)`, "\u8FD0\u884C npx csspilot validate \u67E5\u770B\u8BE6\u60C5")
|
|
32300
32445
|
]);
|
|
32301
|
-
const skillDir =
|
|
32302
|
-
|
|
32303
|
-
|
|
32304
|
-
|
|
32305
|
-
|
|
32446
|
+
const skillDir = join22(root, ".agents", "skills", "testpilot");
|
|
32447
|
+
const installedSkillVersion = readManifestVersion(skillDir);
|
|
32448
|
+
const skillSource = resolveSkillSourceDir();
|
|
32449
|
+
const bundledSkillVersion = skillSource ? readManifestVersion(skillSource) : void 0;
|
|
32450
|
+
const skillItems = [
|
|
32451
|
+
installedSkillVersion ? ok(`TestPilot Skill(.agents/skills/testpilot,v${installedSkillVersion})`) : error62("TestPilot Skill", "\u8FD0\u884C npx csspilot init \u5B89\u88C5"),
|
|
32452
|
+
existsSync7(join22(skillDir, "references", "project.md")) ? ok("Project testing guide(references/)") : warn("Project testing guide \u7F3A\u5931", "references/ \u7531 init \u751F\u6210;\u53EF\u91CD\u65B0\u8FD0\u884C npx csspilot init")
|
|
32453
|
+
];
|
|
32454
|
+
if (installedSkillVersion && bundledSkillVersion && installedSkillVersion !== bundledSkillVersion) {
|
|
32455
|
+
skillItems.push(
|
|
32456
|
+
warn(
|
|
32457
|
+
`Skill \u53EF\u66F4\u65B0(v${installedSkillVersion} -> v${bundledSkillVersion})`,
|
|
32458
|
+
"\u8FD0\u884C npx csspilot update \u5E76\u63D0\u4EA4 .agents/skills/ \u53D8\u66F4"
|
|
32459
|
+
)
|
|
32460
|
+
);
|
|
32461
|
+
}
|
|
32462
|
+
push("Agent", skillItems);
|
|
32306
32463
|
const adapters = await detectAdapters(root, info.type);
|
|
32307
32464
|
push(
|
|
32308
32465
|
"Adapters",
|
|
@@ -32387,11 +32544,11 @@ function makeDoctorCommand() {
|
|
|
32387
32544
|
import { join as join25 } from "node:path";
|
|
32388
32545
|
|
|
32389
32546
|
// src/commands/init/config-initializer.ts
|
|
32390
|
-
import { access as
|
|
32391
|
-
import { basename as basename2, join as
|
|
32392
|
-
async function
|
|
32547
|
+
import { access as access3, mkdir as mkdir12, readFile as readFile8, writeFile as writeFile10 } from "node:fs/promises";
|
|
32548
|
+
import { basename as basename2, join as join23 } from "node:path";
|
|
32549
|
+
async function exists4(path) {
|
|
32393
32550
|
try {
|
|
32394
|
-
await
|
|
32551
|
+
await access3(path);
|
|
32395
32552
|
return true;
|
|
32396
32553
|
} catch {
|
|
32397
32554
|
return false;
|
|
@@ -32432,28 +32589,72 @@ ${adapters}
|
|
|
32432
32589
|
# server:
|
|
32433
32590
|
# baseUrl: http://127.0.0.1:3000
|
|
32434
32591
|
|
|
32435
|
-
# Web \u7AEF:navigate \u76F8\u5BF9 url \u7684\u57FA\u51C6\u5730\u5740
|
|
32592
|
+
# Web \u7AEF:navigate \u76F8\u5BF9 url \u7684\u57FA\u51C6\u5730\u5740(\u6BCF\u4EBA\u7684\u5730\u5740\u4E0D\u540C,\u503C\u653E .env)
|
|
32436
32593
|
# web:
|
|
32437
|
-
# baseUrl:
|
|
32594
|
+
# baseUrl: \${WEB_BASE_URL}
|
|
32438
32595
|
|
|
32439
|
-
# \u5C0F\u7A0B\u5E8F\u7AEF:\u586B\u5165\u5C0F\u7A0B\u5E8F\u9879\u76EE\u76EE\u5F55\u540E\u5373\u53EF\u6267\u884C target: miniapp \u7684\u6B65\u9AA4
|
|
32596
|
+
# \u5C0F\u7A0B\u5E8F\u7AEF:\u586B\u5165\u5C0F\u7A0B\u5E8F\u9879\u76EE\u76EE\u5F55\u540E\u5373\u53EF\u6267\u884C target: miniapp \u7684\u6B65\u9AA4(\u8DEF\u5F84\u6BCF\u4EBA\u4E0D\u540C,\u8D70 .env)
|
|
32440
32597
|
# miniapp:
|
|
32441
|
-
# projectPath:
|
|
32442
|
-
# cliPath:
|
|
32598
|
+
# projectPath: \${MINIAPP_PROJECT_PATH}
|
|
32599
|
+
# cliPath: \${MINIAPP_CLI_PATH}
|
|
32600
|
+
`;
|
|
32601
|
+
}
|
|
32602
|
+
function renderDotEnvExample() {
|
|
32603
|
+
return `# TestPilot \u73AF\u5883\u53D8\u91CF\u793A\u4F8B:\u590D\u5236\u4E3A .env \u5E76\u586B\u5165\u771F\u5B9E\u503C(.env \u542B\u654F\u611F\u4FE1\u606F,\u4E0D\u8981\u63D0\u4EA4\u5230 Git)
|
|
32604
|
+
# \u4F18\u5148\u7EA7:\u771F\u5B9E\u73AF\u5883\u53D8\u91CF > .env \u6587\u4EF6;testpilot.yaml \u4E2D\u7684 \${VAR} \u5F15\u7528\u8FD0\u884C\u65F6\u81EA\u52A8\u6CE8\u5165
|
|
32605
|
+
|
|
32606
|
+
# \u73AF\u5883\u914D\u7F6E(environment \u6BB5)
|
|
32607
|
+
TEST_BASE_URL=http://127.0.0.1:8080
|
|
32608
|
+
STAGING_BASE_URL=https://staging.example.com
|
|
32609
|
+
|
|
32610
|
+
# Web \u7AEF\u76F8\u5BF9 url \u7684\u57FA\u51C6(\u53D6\u6D88 testpilot.yaml \u4E2D web \u6BB5\u6CE8\u91CA\u540E\u751F\u6548)
|
|
32611
|
+
# WEB_BASE_URL=http://127.0.0.1:8080
|
|
32612
|
+
|
|
32613
|
+
# \u5C0F\u7A0B\u5E8F\u7AEF(\u53D6\u6D88 testpilot.yaml \u4E2D miniapp \u6BB5\u6CE8\u91CA\u540E\u751F\u6548)
|
|
32614
|
+
# MINIAPP_PROJECT_PATH=path/to/miniprogram
|
|
32615
|
+
# MINIAPP_CLI_PATH=C:/Program Files (x86)/Tencent/\u5FAE\u4FE1web\u5F00\u53D1\u8005\u5DE5\u5177/cli.bat
|
|
32616
|
+
|
|
32617
|
+
# TestPilot Server(Control Plane,\u53EF\u9009)
|
|
32618
|
+
# TESTPILOT_SERVER_URL=http://127.0.0.1:3000
|
|
32443
32619
|
`;
|
|
32444
32620
|
}
|
|
32621
|
+
async function ensureGitignoreEntry(root, entry, comment = "TestPilot \u672C\u5730\u73AF\u5883\u53D8\u91CF") {
|
|
32622
|
+
const gitignorePath = join23(root, ".gitignore");
|
|
32623
|
+
if (!await exists4(gitignorePath)) {
|
|
32624
|
+
await writeFile10(gitignorePath, `# ${comment}
|
|
32625
|
+
${entry}
|
|
32626
|
+
`, "utf8");
|
|
32627
|
+
return "created";
|
|
32628
|
+
}
|
|
32629
|
+
const content = await readFile8(gitignorePath, "utf8");
|
|
32630
|
+
if (content.split(/\r?\n/).some((item) => item.trim() === entry)) {
|
|
32631
|
+
return "exists";
|
|
32632
|
+
}
|
|
32633
|
+
const separator = content.endsWith("\n") || content === "" ? "" : "\n";
|
|
32634
|
+
await writeFile10(gitignorePath, `${content}${separator}# ${comment}
|
|
32635
|
+
${entry}
|
|
32636
|
+
`, "utf8");
|
|
32637
|
+
return "updated";
|
|
32638
|
+
}
|
|
32445
32639
|
async function initTestPilotConfig(root, info, adapters) {
|
|
32446
|
-
const yamlPath =
|
|
32640
|
+
const yamlPath = join23(root, "testpilot.yaml");
|
|
32447
32641
|
let yaml = "reused";
|
|
32448
|
-
if (!await
|
|
32449
|
-
await
|
|
32642
|
+
if (!await exists4(yamlPath)) {
|
|
32643
|
+
await writeFile10(yamlPath, renderTestpilotYaml(info), "utf8");
|
|
32450
32644
|
yaml = "created";
|
|
32451
32645
|
}
|
|
32452
|
-
const
|
|
32453
|
-
|
|
32454
|
-
|
|
32455
|
-
|
|
32456
|
-
|
|
32646
|
+
const envExamplePath = join23(root, ".env.example");
|
|
32647
|
+
let envExample = "reused";
|
|
32648
|
+
if (!await exists4(envExamplePath)) {
|
|
32649
|
+
await writeFile10(envExamplePath, renderDotEnvExample(), "utf8");
|
|
32650
|
+
envExample = "created";
|
|
32651
|
+
}
|
|
32652
|
+
const gitignore = await ensureGitignoreEntry(root, ".env");
|
|
32653
|
+
const runtimeDir = join23(root, ".testpilot", "artifacts");
|
|
32654
|
+
await mkdir12(runtimeDir, { recursive: true });
|
|
32655
|
+
const keep = join23(runtimeDir, ".gitkeep");
|
|
32656
|
+
if (!await exists4(keep)) {
|
|
32657
|
+
await writeFile10(keep, "", "utf8");
|
|
32457
32658
|
}
|
|
32458
32659
|
const existing = await readProjectLink(root);
|
|
32459
32660
|
const defaultAdapter = defaultAdapterFor(info.type);
|
|
@@ -32472,7 +32673,14 @@ async function initTestPilotConfig(root, info, adapters) {
|
|
|
32472
32673
|
}
|
|
32473
32674
|
if (link.serverUrl === void 0 && serverUrl) link.serverUrl = serverUrl;
|
|
32474
32675
|
const projectLinkFile = await writeProjectLink(link, root);
|
|
32475
|
-
return {
|
|
32676
|
+
return {
|
|
32677
|
+
yaml,
|
|
32678
|
+
runtimeDir: join23(root, ".testpilot"),
|
|
32679
|
+
projectLink: link,
|
|
32680
|
+
projectLinkFile,
|
|
32681
|
+
envExample,
|
|
32682
|
+
gitignore
|
|
32683
|
+
};
|
|
32476
32684
|
}
|
|
32477
32685
|
async function createProjectOnServer(root, info, _adapters, serverUrl) {
|
|
32478
32686
|
const git = info.git;
|
|
@@ -32501,106 +32709,6 @@ function randomId() {
|
|
|
32501
32709
|
return Math.random().toString(16).slice(2, 10);
|
|
32502
32710
|
}
|
|
32503
32711
|
|
|
32504
|
-
// src/commands/init/skill-installer.ts
|
|
32505
|
-
import { existsSync as existsSync7 } from "node:fs";
|
|
32506
|
-
import { access as access3, cp, mkdir as mkdir12, writeFile as writeFile10 } from "node:fs/promises";
|
|
32507
|
-
import { dirname as dirname7, join as join23 } from "node:path";
|
|
32508
|
-
import { fileURLToPath } from "node:url";
|
|
32509
|
-
async function exists4(path) {
|
|
32510
|
-
try {
|
|
32511
|
-
await access3(path);
|
|
32512
|
-
return true;
|
|
32513
|
-
} catch {
|
|
32514
|
-
return false;
|
|
32515
|
-
}
|
|
32516
|
-
}
|
|
32517
|
-
function resolveSkillSourceDir() {
|
|
32518
|
-
const fromEnv = process.env.TESTPILOT_SKILL_DIR;
|
|
32519
|
-
if (fromEnv) return fromEnv;
|
|
32520
|
-
const bundled = fileURLToPath(new URL("./skills/testpilot", import.meta.url));
|
|
32521
|
-
if (existsSync7(bundled)) return bundled;
|
|
32522
|
-
const inRepo = fileURLToPath(new URL("../../../../../skills/testpilot", import.meta.url));
|
|
32523
|
-
return existsSync7(inRepo) ? inRepo : void 0;
|
|
32524
|
-
}
|
|
32525
|
-
async function installSkill(root, info, adapters) {
|
|
32526
|
-
const dest = join23(root, ".agents", "skills", "testpilot");
|
|
32527
|
-
let core = "reused";
|
|
32528
|
-
if (!await exists4(join23(dest, "manifest.yaml"))) {
|
|
32529
|
-
const src = resolveSkillSourceDir();
|
|
32530
|
-
if (!src) {
|
|
32531
|
-
throw new Error(
|
|
32532
|
-
"\u65E0\u6CD5\u5B9A\u4F4D TestPilot Skill \u6E90\u76EE\u5F55;\u53EF\u901A\u8FC7\u73AF\u5883\u53D8\u91CF TESTPILOT_SKILL_DIR \u6307\u5B9A skills/testpilot \u8DEF\u5F84"
|
|
32533
|
-
);
|
|
32534
|
-
}
|
|
32535
|
-
await mkdir12(dirname7(dest), { recursive: true });
|
|
32536
|
-
await cp(src, dest, { recursive: true });
|
|
32537
|
-
core = "created";
|
|
32538
|
-
}
|
|
32539
|
-
const config2 = await loadTestpilotConfig(root).catch(() => void 0);
|
|
32540
|
-
const defaultEnvironment = typeof config2?.environment?.default === "string" ? config2.environment.default : "\u672A\u8BBE\u7F6E";
|
|
32541
|
-
const defaultWorkspace = config2?.workspace?.default ?? "\u672A\u8BBE\u7F6E(\u53EF\u5728 yaml workspace.default \u914D\u7F6E)";
|
|
32542
|
-
const refsDir = join23(dest, "references");
|
|
32543
|
-
const files = [
|
|
32544
|
-
["project.md", () => renderProjectMd(info, adapters, defaultEnvironment, defaultWorkspace)],
|
|
32545
|
-
["test-conventions.md", renderTestConventionsMd],
|
|
32546
|
-
["adapters.md", () => renderAdaptersMd(adapters)]
|
|
32547
|
-
];
|
|
32548
|
-
let references = "reused";
|
|
32549
|
-
for (const [name, render] of files) {
|
|
32550
|
-
const file2 = join23(refsDir, name);
|
|
32551
|
-
if (await exists4(file2)) continue;
|
|
32552
|
-
await mkdir12(refsDir, { recursive: true });
|
|
32553
|
-
await writeFile10(file2, render(), "utf8");
|
|
32554
|
-
references = "created";
|
|
32555
|
-
}
|
|
32556
|
-
return { core, references, dest };
|
|
32557
|
-
}
|
|
32558
|
-
function renderProjectMd(info, adapters, defaultEnvironment, defaultWorkspace) {
|
|
32559
|
-
const available = adapters.filter((item) => item.available).map((item) => item.id);
|
|
32560
|
-
return `# Project
|
|
32561
|
-
|
|
32562
|
-
\u81EA\u52A8\u751F\u6210\u4E8E \`csspilot init\`,\u63CF\u8FF0\u672C\u9879\u76EE\u7684 TestPilot \u63A5\u5165\u4FE1\u606F;Agent \u4F18\u5148\u4ECE\u8FD9\u91CC\u4E86\u89E3\u9879\u76EE\u3002
|
|
32563
|
-
|
|
32564
|
-
- Project name: ${info.name}
|
|
32565
|
-
- Project type: ${info.type}
|
|
32566
|
-
- Language: ${info.language}
|
|
32567
|
-
- Package manager: ${info.packageManager}
|
|
32568
|
-
- Test directory: tests/e2e
|
|
32569
|
-
- Case directory: tests/e2e/cases
|
|
32570
|
-
- Available adapters: ${available.length > 0 ? available.join(", ") : "(none detected)"}
|
|
32571
|
-
- Default environment: ${defaultEnvironment}
|
|
32572
|
-
- Default workspace: ${defaultWorkspace}
|
|
32573
|
-
`;
|
|
32574
|
-
}
|
|
32575
|
-
function renderTestConventionsMd() {
|
|
32576
|
-
return `# Test Conventions
|
|
32577
|
-
|
|
32578
|
-
\u672C\u9879\u76EE E2E \u6D4B\u8BD5\u7EA6\u5B9A,Agent \u751F\u6210/\u4FEE\u6539 Case \u65F6\u5FC5\u987B\u9075\u5B88:
|
|
32579
|
-
|
|
32580
|
-
1. E2E Case \u4F4D\u4E8E tests/e2e/cases
|
|
32581
|
-
2. Case \u4F7F\u7528 TestPilot DSL
|
|
32582
|
-
3. \u4E0D\u5141\u8BB8\u76F4\u63A5\u8C03\u7528 Playwright
|
|
32583
|
-
4. \u4E0D\u5141\u8BB8\u76F4\u63A5\u8C03\u7528 WeChatIDE
|
|
32584
|
-
5. \u4E0D\u5141\u8BB8\u5728 Case \u4E2D\u5199\u660E\u6587\u5BC6\u7801(\u7528 accountRef \u5F15\u7528)
|
|
32585
|
-
6. \u4F18\u5148\u590D\u7528\u5DF2\u6709 Case
|
|
32586
|
-
7. \u65B0\u589E Case \u5FC5\u987B\u901A\u8FC7 csspilot validate
|
|
32587
|
-
8. Case ID \u5FC5\u987B\u552F\u4E00
|
|
32588
|
-
`;
|
|
32589
|
-
}
|
|
32590
|
-
function renderAdaptersMd(adapters) {
|
|
32591
|
-
const lines = adapters.map(
|
|
32592
|
-
(item) => `- ${item.id}: ${item.available ? "\u53EF\u7528" : "\u4E0D\u53EF\u7528"}${item.required ? "(\u9879\u76EE\u9700\u8981)" : "(\u53EF\u9009)"} \u2014 ${item.detail}`
|
|
32593
|
-
);
|
|
32594
|
-
return `# Adapters
|
|
32595
|
-
|
|
32596
|
-
\u6267\u884C\u7AEF\u68C0\u6D4B\u7ED3\u679C(\u4EE5 \`csspilot doctor\` \u6700\u65B0\u8F93\u51FA\u4E3A\u51C6):
|
|
32597
|
-
|
|
32598
|
-
${lines.join("\n")}
|
|
32599
|
-
|
|
32600
|
-
Agent \u6C38\u8FDC\u4E0D\u76F4\u63A5\u64CD\u4F5C Adapter;Case \u7ECF Execution Engine \u8C03\u5EA6\u5230\u5BF9\u5E94 Adapter \u6267\u884C\u3002
|
|
32601
|
-
`;
|
|
32602
|
-
}
|
|
32603
|
-
|
|
32604
32712
|
// src/commands/init/test-structure.ts
|
|
32605
32713
|
import { existsSync as existsSync8, readdirSync as readdirSync2 } from "node:fs";
|
|
32606
32714
|
import { mkdir as mkdir13, writeFile as writeFile11 } from "node:fs/promises";
|
|
@@ -32658,6 +32766,12 @@ async function runInit(root, options = {}) {
|
|
|
32658
32766
|
console.log(`\u2713 Runtime \u76EE\u5F55 ${join25(".testpilot", "artifacts")}`);
|
|
32659
32767
|
const serverMode = config2.projectLink.serverUrl ? `Server:${config2.projectLink.serverUrl}` : "\u672C\u5730\u6A21\u5F0F";
|
|
32660
32768
|
console.log(`\u2713 \u9879\u76EE\u5173\u8054 ${config2.projectLink.projectId}(${serverMode})`);
|
|
32769
|
+
console.log(
|
|
32770
|
+
config2.envExample === "created" ? "\u2713 \u73AF\u5883\u53D8\u91CF\u793A\u4F8B .env.example(\u590D\u5236\u4E3A .env \u586B\u5165\u771F\u5B9E\u503C)" : "\u2713 \u73AF\u5883\u53D8\u91CF\u793A\u4F8B .env.example(\u5DF2\u5B58\u5728,\u4FDD\u7559)"
|
|
32771
|
+
);
|
|
32772
|
+
console.log(
|
|
32773
|
+
config2.gitignore === "exists" ? "\u2713 Git \u5FFD\u7565 .env(\u5DF2\u5728 .gitignore)" : `\u2713 Git \u5FFD\u7565 .env(\u5DF2\u5199\u5165 .gitignore)`
|
|
32774
|
+
);
|
|
32661
32775
|
const testStructure = await initTestStructure(root, project);
|
|
32662
32776
|
const reuseNote = project.existingE2eDirs.filter((dir) => dir !== "tests/e2e");
|
|
32663
32777
|
console.log(
|
|
@@ -32667,9 +32781,15 @@ async function runInit(root, options = {}) {
|
|
|
32667
32781
|
console.log(` \u26A0 \u68C0\u6D4B\u5230\u5176\u4ED6 E2E \u76EE\u5F55:${reuseNote.join(", ")}(\u4FDD\u7559\u4E0D\u52A8,\u672A\u5F3A\u5236\u8FC1\u79FB)`);
|
|
32668
32782
|
}
|
|
32669
32783
|
const skill = await installSkill(root, project, adapters);
|
|
32670
|
-
|
|
32671
|
-
|
|
32672
|
-
|
|
32784
|
+
if (skill.core === "updated") {
|
|
32785
|
+
console.log(
|
|
32786
|
+
`\u2713 Skill \u66F4\u65B0 .agents/skills/testpilot/(v${skill.coreVersion?.from ?? "?"} -> v${skill.coreVersion?.to ?? "?"})`
|
|
32787
|
+
);
|
|
32788
|
+
} else {
|
|
32789
|
+
console.log(
|
|
32790
|
+
skill.core === "created" ? `\u2713 Skill \u5B89\u88C5 ${join25(".agents", "skills", "testpilot")}` : "\u2713 Skill \u5B89\u88C5 .agents/skills/testpilot/(\u5DF2\u5B58\u5728,\u8DF3\u8FC7)"
|
|
32791
|
+
);
|
|
32792
|
+
}
|
|
32673
32793
|
console.log(
|
|
32674
32794
|
skill.references === "created" ? "\u2713 Agent \u4E0A\u4E0B\u6587 references/{project,test-conventions,adapters}.md" : "\u2713 Agent \u4E0A\u4E0B\u6587 references/(\u5DF2\u5B58\u5728,\u4FDD\u7559)"
|
|
32675
32795
|
);
|
|
@@ -32899,6 +33019,37 @@ function makeSyncMetadataCommand() {
|
|
|
32899
33019
|
});
|
|
32900
33020
|
}
|
|
32901
33021
|
|
|
33022
|
+
// src/commands/update.ts
|
|
33023
|
+
import { writeFile as writeFile12 } from "node:fs/promises";
|
|
33024
|
+
import { join as join27 } from "node:path";
|
|
33025
|
+
function makeUpdateCommand() {
|
|
33026
|
+
const update = new Command("update").description("\u540C\u6B65 TestPilot \u63A5\u5165\u7269\u5230\u5F53\u524D CLI \u7248\u672C(Skill / .env.example;\u4E0D\u89E6\u78B0 .env \u4E0E\u7528\u4F8B)").option("--force", "Skill \u7248\u672C\u4E00\u81F4\u65F6\u4E5F\u5F3A\u5236\u8986\u76D6\u672C\u5730\u4FEE\u6539", false).action(async (options) => {
|
|
33027
|
+
const root = process.cwd();
|
|
33028
|
+
const dest = join27(".agents", "skills", "testpilot");
|
|
33029
|
+
const skill = await syncSkillFiles(root, { force: options.force });
|
|
33030
|
+
switch (skill.core) {
|
|
33031
|
+
case "created":
|
|
33032
|
+
console.log(`\u2713 Skill \u5B89\u88C5 ${dest}(v${skill.bundledVersion ?? "?"})`);
|
|
33033
|
+
break;
|
|
33034
|
+
case "updated":
|
|
33035
|
+
console.log(
|
|
33036
|
+
`\u2713 Skill \u66F4\u65B0 ${dest} v${skill.installedVersion ?? "?"} -> v${skill.bundledVersion ?? "?"}`
|
|
33037
|
+
);
|
|
33038
|
+
break;
|
|
33039
|
+
default:
|
|
33040
|
+
console.log(`\u2713 Skill \u5DF2\u662F\u6700\u65B0 v${skill.bundledVersion ?? "?"}`);
|
|
33041
|
+
}
|
|
33042
|
+
await writeFile12(join27(root, ".env.example"), renderDotEnvExample(), "utf8");
|
|
33043
|
+
console.log("\u2713 .env.example \u5DF2\u540C\u6B65\u4E3A\u5F53\u524D\u7248\u672C\u7684\u793A\u4F8B(.env \u4E0D\u53D7\u5F71\u54CD)");
|
|
33044
|
+
const gitignore = await ensureGitignoreEntry(root, ".env");
|
|
33045
|
+
console.log(
|
|
33046
|
+
gitignore === "exists" ? "\u2713 .gitignore \u5DF2\u5FFD\u7565 .env" : "\u2713 .gitignore \u5DF2\u8FFD\u52A0\u5FFD\u7565 .env"
|
|
33047
|
+
);
|
|
33048
|
+
console.log("\u63D0\u793A:\u63D0\u4EA4 .agents/skills/ \u4E0E .env.example \u7684\u53D8\u66F4,\u56E2\u961F\u4E0E AI \u5373\u53EF\u4F7F\u7528\u6700\u65B0\u80FD\u529B");
|
|
33049
|
+
});
|
|
33050
|
+
return update;
|
|
33051
|
+
}
|
|
33052
|
+
|
|
32902
33053
|
// src/commands/validate.ts
|
|
32903
33054
|
function makeValidateCommand() {
|
|
32904
33055
|
return new Command("validate").description("\u6821\u9A8C\u7528\u4F8B DSL").argument("[paths...]", "\u7528\u4F8B\u6587\u4EF6\u6216\u76EE\u5F55,\u9ED8\u8BA4\u6309\u9879\u76EE\u914D\u7F6E").action(async (paths) => {
|
|
@@ -32935,10 +33086,10 @@ function makeValidateCommand() {
|
|
|
32935
33086
|
}
|
|
32936
33087
|
|
|
32937
33088
|
// src/cli.ts
|
|
32938
|
-
var CLI_VERSION = "0.
|
|
33089
|
+
var CLI_VERSION = "0.4.0";
|
|
32939
33090
|
async function createCli() {
|
|
32940
33091
|
const program2 = new Command();
|
|
32941
|
-
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());
|
|
33092
|
+
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(makeUpdateCommand()).addCommand(makeCiCommand());
|
|
32942
33093
|
return program2;
|
|
32943
33094
|
}
|
|
32944
33095
|
|
package/dist/index.js
CHANGED
|
@@ -10449,8 +10449,8 @@ function makeCiCommand() {
|
|
|
10449
10449
|
}
|
|
10450
10450
|
|
|
10451
10451
|
// src/commands/init/doctor.ts
|
|
10452
|
-
import { existsSync as
|
|
10453
|
-
import { join as
|
|
10452
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
10453
|
+
import { join as join22 } from "node:path";
|
|
10454
10454
|
|
|
10455
10455
|
// ../../packages/core/src/errors/index.ts
|
|
10456
10456
|
var TestPilotError = class extends Error {
|
|
@@ -30243,7 +30243,10 @@ async function loadTestpilotConfig(cwd = process.cwd()) {
|
|
|
30243
30243
|
baseUrl: parsed.api.baseUrl === void 0 ? void 0 : interpolateEnvRefs(parsed.api.baseUrl),
|
|
30244
30244
|
timeoutMs: parsed.api.timeoutMs
|
|
30245
30245
|
} : void 0,
|
|
30246
|
-
miniapp: parsed.miniapp
|
|
30246
|
+
miniapp: parsed.miniapp ? {
|
|
30247
|
+
projectPath: parsed.miniapp.projectPath === void 0 ? void 0 : interpolateEnvRefs(parsed.miniapp.projectPath),
|
|
30248
|
+
cliPath: parsed.miniapp.cliPath === void 0 ? void 0 : interpolateEnvRefs(parsed.miniapp.cliPath)
|
|
30249
|
+
} : void 0
|
|
30247
30250
|
};
|
|
30248
30251
|
}
|
|
30249
30252
|
|
|
@@ -32166,8 +32169,12 @@ async function collectEnvVarRefs(root) {
|
|
|
32166
32169
|
return [];
|
|
32167
32170
|
}
|
|
32168
32171
|
const names = /* @__PURE__ */ new Set();
|
|
32169
|
-
for (const
|
|
32170
|
-
|
|
32172
|
+
for (const line of text.split(/\r?\n/)) {
|
|
32173
|
+
const trimmed = line.trim();
|
|
32174
|
+
if (trimmed.startsWith("#")) continue;
|
|
32175
|
+
for (const match of trimmed.matchAll(ENV_REF_PATTERN)) {
|
|
32176
|
+
names.add(match[1]);
|
|
32177
|
+
}
|
|
32171
32178
|
}
|
|
32172
32179
|
return [...names].sort();
|
|
32173
32180
|
}
|
|
@@ -32259,6 +32266,144 @@ function adaptersFor(type) {
|
|
|
32259
32266
|
return ["playwright"];
|
|
32260
32267
|
}
|
|
32261
32268
|
|
|
32269
|
+
// src/commands/init/skill-installer.ts
|
|
32270
|
+
import { existsSync as existsSync6, readFileSync as readFileSync3 } from "node:fs";
|
|
32271
|
+
import { access as access2, cp, mkdir as mkdir11, writeFile as writeFile9 } from "node:fs/promises";
|
|
32272
|
+
import { dirname as dirname7, join as join21 } from "node:path";
|
|
32273
|
+
import { fileURLToPath } from "node:url";
|
|
32274
|
+
var CORE_FILES = ["SKILL.md", "manifest.yaml"];
|
|
32275
|
+
var CORE_DIRS = ["rules", "workflows"];
|
|
32276
|
+
async function exists3(path) {
|
|
32277
|
+
try {
|
|
32278
|
+
await access2(path);
|
|
32279
|
+
return true;
|
|
32280
|
+
} catch {
|
|
32281
|
+
return false;
|
|
32282
|
+
}
|
|
32283
|
+
}
|
|
32284
|
+
function readManifestVersion(dir) {
|
|
32285
|
+
const manifestPath = join21(dir, "manifest.yaml");
|
|
32286
|
+
if (!existsSync6(manifestPath)) return void 0;
|
|
32287
|
+
return /^version:\s*([^\s#]+)/m.exec(readFileSync3(manifestPath, "utf8"))?.[1];
|
|
32288
|
+
}
|
|
32289
|
+
function resolveSkillSourceDir() {
|
|
32290
|
+
const fromEnv = process.env.TESTPILOT_SKILL_DIR;
|
|
32291
|
+
if (fromEnv) return fromEnv;
|
|
32292
|
+
const bundled = fileURLToPath(new URL("./skills/testpilot", import.meta.url));
|
|
32293
|
+
if (existsSync6(bundled)) return bundled;
|
|
32294
|
+
const inRepo = fileURLToPath(new URL("../../../../../skills/testpilot", import.meta.url));
|
|
32295
|
+
return existsSync6(inRepo) ? inRepo : void 0;
|
|
32296
|
+
}
|
|
32297
|
+
async function copyCoreFiles(src, dest) {
|
|
32298
|
+
for (const name of CORE_FILES) {
|
|
32299
|
+
await cp(join21(src, name), join21(dest, name), { recursive: true, force: true });
|
|
32300
|
+
}
|
|
32301
|
+
for (const dir of CORE_DIRS) {
|
|
32302
|
+
if (existsSync6(join21(src, dir))) {
|
|
32303
|
+
await cp(join21(src, dir), join21(dest, dir), { recursive: true, force: true });
|
|
32304
|
+
}
|
|
32305
|
+
}
|
|
32306
|
+
}
|
|
32307
|
+
async function syncSkillFiles(root, options = {}) {
|
|
32308
|
+
const dest = join21(root, ".agents", "skills", "testpilot");
|
|
32309
|
+
const bundledVersion = readManifestVersion(resolveSkillSourceDir() ?? "");
|
|
32310
|
+
if (!await exists3(join21(dest, "manifest.yaml"))) {
|
|
32311
|
+
const src = resolveSkillSourceDir();
|
|
32312
|
+
if (!src) {
|
|
32313
|
+
throw new Error(
|
|
32314
|
+
"\u65E0\u6CD5\u5B9A\u4F4D TestPilot Skill \u6E90\u76EE\u5F55;\u53EF\u901A\u8FC7\u73AF\u5883\u53D8\u91CF TESTPILOT_SKILL_DIR \u6307\u5B9A skills/testpilot \u8DEF\u5F84"
|
|
32315
|
+
);
|
|
32316
|
+
}
|
|
32317
|
+
await mkdir11(dirname7(dest), { recursive: true });
|
|
32318
|
+
await cp(src, dest, { recursive: true });
|
|
32319
|
+
return { core: "created", bundledVersion, dest };
|
|
32320
|
+
}
|
|
32321
|
+
const installedVersion = readManifestVersion(dest);
|
|
32322
|
+
const bundled = resolveSkillSourceDir();
|
|
32323
|
+
if (!bundled) {
|
|
32324
|
+
return { core: "current", installedVersion, dest };
|
|
32325
|
+
}
|
|
32326
|
+
if (!options.force && installedVersion === bundledVersion) {
|
|
32327
|
+
return { core: "current", installedVersion, bundledVersion, dest };
|
|
32328
|
+
}
|
|
32329
|
+
await copyCoreFiles(bundled, dest);
|
|
32330
|
+
return {
|
|
32331
|
+
core: "updated",
|
|
32332
|
+
installedVersion,
|
|
32333
|
+
bundledVersion,
|
|
32334
|
+
dest
|
|
32335
|
+
};
|
|
32336
|
+
}
|
|
32337
|
+
async function installSkill(root, info, adapters) {
|
|
32338
|
+
const dest = join21(root, ".agents", "skills", "testpilot");
|
|
32339
|
+
const sync = await syncSkillFiles(root);
|
|
32340
|
+
const core = sync.core === "created" ? "created" : sync.core === "updated" ? "updated" : "reused";
|
|
32341
|
+
const coreVersion = sync.core === "updated" || sync.core === "created" ? { from: sync.installedVersion, to: sync.bundledVersion } : void 0;
|
|
32342
|
+
const config2 = await loadTestpilotConfig(root).catch(() => void 0);
|
|
32343
|
+
const defaultEnvironment = typeof config2?.environment?.default === "string" ? config2.environment.default : "\u672A\u8BBE\u7F6E";
|
|
32344
|
+
const defaultWorkspace = config2?.workspace?.default ?? "\u672A\u8BBE\u7F6E(\u53EF\u5728 yaml workspace.default \u914D\u7F6E)";
|
|
32345
|
+
const refsDir = join21(dest, "references");
|
|
32346
|
+
const files = [
|
|
32347
|
+
["project.md", () => renderProjectMd(info, adapters, defaultEnvironment, defaultWorkspace)],
|
|
32348
|
+
["test-conventions.md", renderTestConventionsMd],
|
|
32349
|
+
["adapters.md", () => renderAdaptersMd(adapters)]
|
|
32350
|
+
];
|
|
32351
|
+
let references = "reused";
|
|
32352
|
+
for (const [name, render] of files) {
|
|
32353
|
+
const file2 = join21(refsDir, name);
|
|
32354
|
+
if (await exists3(file2)) continue;
|
|
32355
|
+
await mkdir11(refsDir, { recursive: true });
|
|
32356
|
+
await writeFile9(file2, render(), "utf8");
|
|
32357
|
+
references = "created";
|
|
32358
|
+
}
|
|
32359
|
+
return coreVersion ? { core, coreVersion, references, dest } : { core, references, dest };
|
|
32360
|
+
}
|
|
32361
|
+
function renderProjectMd(info, adapters, defaultEnvironment, defaultWorkspace) {
|
|
32362
|
+
const available = adapters.filter((item) => item.available).map((item) => item.id);
|
|
32363
|
+
return `# Project
|
|
32364
|
+
|
|
32365
|
+
\u81EA\u52A8\u751F\u6210\u4E8E \`csspilot init\`,\u63CF\u8FF0\u672C\u9879\u76EE\u7684 TestPilot \u63A5\u5165\u4FE1\u606F;Agent \u4F18\u5148\u4ECE\u8FD9\u91CC\u4E86\u89E3\u9879\u76EE\u3002
|
|
32366
|
+
|
|
32367
|
+
- Project name: ${info.name}
|
|
32368
|
+
- Project type: ${info.type}
|
|
32369
|
+
- Language: ${info.language}
|
|
32370
|
+
- Package manager: ${info.packageManager}
|
|
32371
|
+
- Test directory: tests/e2e
|
|
32372
|
+
- Case directory: tests/e2e/cases
|
|
32373
|
+
- Available adapters: ${available.length > 0 ? available.join(", ") : "(none detected)"}
|
|
32374
|
+
- Default environment: ${defaultEnvironment}
|
|
32375
|
+
- Default workspace: ${defaultWorkspace}
|
|
32376
|
+
`;
|
|
32377
|
+
}
|
|
32378
|
+
function renderTestConventionsMd() {
|
|
32379
|
+
return `# Test Conventions
|
|
32380
|
+
|
|
32381
|
+
\u672C\u9879\u76EE E2E \u6D4B\u8BD5\u7EA6\u5B9A,Agent \u751F\u6210/\u4FEE\u6539 Case \u65F6\u5FC5\u987B\u9075\u5B88:
|
|
32382
|
+
|
|
32383
|
+
1. E2E Case \u4F4D\u4E8E tests/e2e/cases
|
|
32384
|
+
2. Case \u4F7F\u7528 TestPilot DSL
|
|
32385
|
+
3. \u4E0D\u5141\u8BB8\u76F4\u63A5\u8C03\u7528 Playwright
|
|
32386
|
+
4. \u4E0D\u5141\u8BB8\u76F4\u63A5\u8C03\u7528 WeChatIDE
|
|
32387
|
+
5. \u4E0D\u5141\u8BB8\u5728 Case \u4E2D\u5199\u660E\u6587\u5BC6\u7801(\u7528 accountRef \u5F15\u7528)
|
|
32388
|
+
6. \u4F18\u5148\u590D\u7528\u5DF2\u6709 Case
|
|
32389
|
+
7. \u65B0\u589E Case \u5FC5\u987B\u901A\u8FC7 csspilot validate
|
|
32390
|
+
8. Case ID \u5FC5\u987B\u552F\u4E00
|
|
32391
|
+
`;
|
|
32392
|
+
}
|
|
32393
|
+
function renderAdaptersMd(adapters) {
|
|
32394
|
+
const lines = adapters.map(
|
|
32395
|
+
(item) => `- ${item.id}: ${item.available ? "\u53EF\u7528" : "\u4E0D\u53EF\u7528"}${item.required ? "(\u9879\u76EE\u9700\u8981)" : "(\u53EF\u9009)"} \u2014 ${item.detail}`
|
|
32396
|
+
);
|
|
32397
|
+
return `# Adapters
|
|
32398
|
+
|
|
32399
|
+
\u6267\u884C\u7AEF\u68C0\u6D4B\u7ED3\u679C(\u4EE5 \`csspilot doctor\` \u6700\u65B0\u8F93\u51FA\u4E3A\u51C6):
|
|
32400
|
+
|
|
32401
|
+
${lines.join("\n")}
|
|
32402
|
+
|
|
32403
|
+
Agent \u6C38\u8FDC\u4E0D\u76F4\u63A5\u64CD\u4F5C Adapter;Case \u7ECF Execution Engine \u8C03\u5EA6\u5230\u5BF9\u5E94 Adapter \u6267\u884C\u3002
|
|
32404
|
+
`;
|
|
32405
|
+
}
|
|
32406
|
+
|
|
32262
32407
|
// src/commands/init/doctor.ts
|
|
32263
32408
|
async function runDoctor(root) {
|
|
32264
32409
|
const sections = [];
|
|
@@ -32283,25 +32428,37 @@ async function runDoctor(root) {
|
|
|
32283
32428
|
const projectItems = [
|
|
32284
32429
|
ok(`Project ${info.name}(${info.type})`),
|
|
32285
32430
|
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"),
|
|
32286
|
-
|
|
32287
|
-
|
|
32431
|
+
existsSync7(join22(root, ".testpilot", "project.json")) ? ok(".testpilot/project.json") : error62(".testpilot/project.json", "\u8FD0\u884C npx csspilot init \u751F\u6210"),
|
|
32432
|
+
existsSync7(join22(root, "testpilot.yaml")) ? ok("testpilot.yaml") : error62("testpilot.yaml", "\u8FD0\u884C npx csspilot init \u751F\u6210")
|
|
32288
32433
|
];
|
|
32289
32434
|
push("Project", projectItems);
|
|
32290
32435
|
const config2 = await loadTestpilotConfig(root).catch(() => void 0);
|
|
32291
32436
|
const casesDir = config2?.casesDir ?? "tests/e2e/cases";
|
|
32292
|
-
const cases = await collectCases([
|
|
32437
|
+
const cases = await collectCases([join22(root, casesDir)], { root }).catch(() => []);
|
|
32293
32438
|
const invalidCount = cases.filter((item) => !item.valid).length;
|
|
32294
|
-
const testDir =
|
|
32439
|
+
const testDir = join22(root, "tests", "e2e");
|
|
32295
32440
|
push("Test", [
|
|
32296
|
-
|
|
32297
|
-
|
|
32441
|
+
existsSync7(testDir) ? ok("tests/e2e") : error62("tests/e2e", "\u8FD0\u884C npx csspilot init \u521B\u5EFA"),
|
|
32442
|
+
existsSync7(join22(root, casesDir)) ? ok(`cases directory(${casesDir})`) : error62(`cases directory(${casesDir})`, "\u8FD0\u884C npx csspilot init \u521B\u5EFA"),
|
|
32298
32443
|
cases.length === 0 ? warn("Case validation \u8DF3\u8FC7(\u672A\u627E\u5230\u7528\u4F8B)", "\u5728 tests/e2e/cases \u521B\u5EFA Case") : invalidCount === 0 ? ok(`Case validation(${cases.length} \u4E2A\u7528\u4F8B\u5168\u90E8\u6709\u6548)`) : error62(`Case validation(${invalidCount}/${cases.length} \u4E2A\u7528\u4F8B\u6821\u9A8C\u5931\u8D25)`, "\u8FD0\u884C npx csspilot validate \u67E5\u770B\u8BE6\u60C5")
|
|
32299
32444
|
]);
|
|
32300
|
-
const skillDir =
|
|
32301
|
-
|
|
32302
|
-
|
|
32303
|
-
|
|
32304
|
-
|
|
32445
|
+
const skillDir = join22(root, ".agents", "skills", "testpilot");
|
|
32446
|
+
const installedSkillVersion = readManifestVersion(skillDir);
|
|
32447
|
+
const skillSource = resolveSkillSourceDir();
|
|
32448
|
+
const bundledSkillVersion = skillSource ? readManifestVersion(skillSource) : void 0;
|
|
32449
|
+
const skillItems = [
|
|
32450
|
+
installedSkillVersion ? ok(`TestPilot Skill(.agents/skills/testpilot,v${installedSkillVersion})`) : error62("TestPilot Skill", "\u8FD0\u884C npx csspilot init \u5B89\u88C5"),
|
|
32451
|
+
existsSync7(join22(skillDir, "references", "project.md")) ? ok("Project testing guide(references/)") : warn("Project testing guide \u7F3A\u5931", "references/ \u7531 init \u751F\u6210;\u53EF\u91CD\u65B0\u8FD0\u884C npx csspilot init")
|
|
32452
|
+
];
|
|
32453
|
+
if (installedSkillVersion && bundledSkillVersion && installedSkillVersion !== bundledSkillVersion) {
|
|
32454
|
+
skillItems.push(
|
|
32455
|
+
warn(
|
|
32456
|
+
`Skill \u53EF\u66F4\u65B0(v${installedSkillVersion} -> v${bundledSkillVersion})`,
|
|
32457
|
+
"\u8FD0\u884C npx csspilot update \u5E76\u63D0\u4EA4 .agents/skills/ \u53D8\u66F4"
|
|
32458
|
+
)
|
|
32459
|
+
);
|
|
32460
|
+
}
|
|
32461
|
+
push("Agent", skillItems);
|
|
32305
32462
|
const adapters = await detectAdapters(root, info.type);
|
|
32306
32463
|
push(
|
|
32307
32464
|
"Adapters",
|
|
@@ -32386,11 +32543,11 @@ function makeDoctorCommand() {
|
|
|
32386
32543
|
import { join as join25 } from "node:path";
|
|
32387
32544
|
|
|
32388
32545
|
// src/commands/init/config-initializer.ts
|
|
32389
|
-
import { access as
|
|
32390
|
-
import { basename as basename2, join as
|
|
32391
|
-
async function
|
|
32546
|
+
import { access as access3, mkdir as mkdir12, readFile as readFile8, writeFile as writeFile10 } from "node:fs/promises";
|
|
32547
|
+
import { basename as basename2, join as join23 } from "node:path";
|
|
32548
|
+
async function exists4(path) {
|
|
32392
32549
|
try {
|
|
32393
|
-
await
|
|
32550
|
+
await access3(path);
|
|
32394
32551
|
return true;
|
|
32395
32552
|
} catch {
|
|
32396
32553
|
return false;
|
|
@@ -32431,28 +32588,72 @@ ${adapters}
|
|
|
32431
32588
|
# server:
|
|
32432
32589
|
# baseUrl: http://127.0.0.1:3000
|
|
32433
32590
|
|
|
32434
|
-
# Web \u7AEF:navigate \u76F8\u5BF9 url \u7684\u57FA\u51C6\u5730\u5740
|
|
32591
|
+
# Web \u7AEF:navigate \u76F8\u5BF9 url \u7684\u57FA\u51C6\u5730\u5740(\u6BCF\u4EBA\u7684\u5730\u5740\u4E0D\u540C,\u503C\u653E .env)
|
|
32435
32592
|
# web:
|
|
32436
|
-
# baseUrl:
|
|
32593
|
+
# baseUrl: \${WEB_BASE_URL}
|
|
32437
32594
|
|
|
32438
|
-
# \u5C0F\u7A0B\u5E8F\u7AEF:\u586B\u5165\u5C0F\u7A0B\u5E8F\u9879\u76EE\u76EE\u5F55\u540E\u5373\u53EF\u6267\u884C target: miniapp \u7684\u6B65\u9AA4
|
|
32595
|
+
# \u5C0F\u7A0B\u5E8F\u7AEF:\u586B\u5165\u5C0F\u7A0B\u5E8F\u9879\u76EE\u76EE\u5F55\u540E\u5373\u53EF\u6267\u884C target: miniapp \u7684\u6B65\u9AA4(\u8DEF\u5F84\u6BCF\u4EBA\u4E0D\u540C,\u8D70 .env)
|
|
32439
32596
|
# miniapp:
|
|
32440
|
-
# projectPath:
|
|
32441
|
-
# cliPath:
|
|
32597
|
+
# projectPath: \${MINIAPP_PROJECT_PATH}
|
|
32598
|
+
# cliPath: \${MINIAPP_CLI_PATH}
|
|
32599
|
+
`;
|
|
32600
|
+
}
|
|
32601
|
+
function renderDotEnvExample() {
|
|
32602
|
+
return `# TestPilot \u73AF\u5883\u53D8\u91CF\u793A\u4F8B:\u590D\u5236\u4E3A .env \u5E76\u586B\u5165\u771F\u5B9E\u503C(.env \u542B\u654F\u611F\u4FE1\u606F,\u4E0D\u8981\u63D0\u4EA4\u5230 Git)
|
|
32603
|
+
# \u4F18\u5148\u7EA7:\u771F\u5B9E\u73AF\u5883\u53D8\u91CF > .env \u6587\u4EF6;testpilot.yaml \u4E2D\u7684 \${VAR} \u5F15\u7528\u8FD0\u884C\u65F6\u81EA\u52A8\u6CE8\u5165
|
|
32604
|
+
|
|
32605
|
+
# \u73AF\u5883\u914D\u7F6E(environment \u6BB5)
|
|
32606
|
+
TEST_BASE_URL=http://127.0.0.1:8080
|
|
32607
|
+
STAGING_BASE_URL=https://staging.example.com
|
|
32608
|
+
|
|
32609
|
+
# Web \u7AEF\u76F8\u5BF9 url \u7684\u57FA\u51C6(\u53D6\u6D88 testpilot.yaml \u4E2D web \u6BB5\u6CE8\u91CA\u540E\u751F\u6548)
|
|
32610
|
+
# WEB_BASE_URL=http://127.0.0.1:8080
|
|
32611
|
+
|
|
32612
|
+
# \u5C0F\u7A0B\u5E8F\u7AEF(\u53D6\u6D88 testpilot.yaml \u4E2D miniapp \u6BB5\u6CE8\u91CA\u540E\u751F\u6548)
|
|
32613
|
+
# MINIAPP_PROJECT_PATH=path/to/miniprogram
|
|
32614
|
+
# MINIAPP_CLI_PATH=C:/Program Files (x86)/Tencent/\u5FAE\u4FE1web\u5F00\u53D1\u8005\u5DE5\u5177/cli.bat
|
|
32615
|
+
|
|
32616
|
+
# TestPilot Server(Control Plane,\u53EF\u9009)
|
|
32617
|
+
# TESTPILOT_SERVER_URL=http://127.0.0.1:3000
|
|
32442
32618
|
`;
|
|
32443
32619
|
}
|
|
32620
|
+
async function ensureGitignoreEntry(root, entry, comment = "TestPilot \u672C\u5730\u73AF\u5883\u53D8\u91CF") {
|
|
32621
|
+
const gitignorePath = join23(root, ".gitignore");
|
|
32622
|
+
if (!await exists4(gitignorePath)) {
|
|
32623
|
+
await writeFile10(gitignorePath, `# ${comment}
|
|
32624
|
+
${entry}
|
|
32625
|
+
`, "utf8");
|
|
32626
|
+
return "created";
|
|
32627
|
+
}
|
|
32628
|
+
const content = await readFile8(gitignorePath, "utf8");
|
|
32629
|
+
if (content.split(/\r?\n/).some((item) => item.trim() === entry)) {
|
|
32630
|
+
return "exists";
|
|
32631
|
+
}
|
|
32632
|
+
const separator = content.endsWith("\n") || content === "" ? "" : "\n";
|
|
32633
|
+
await writeFile10(gitignorePath, `${content}${separator}# ${comment}
|
|
32634
|
+
${entry}
|
|
32635
|
+
`, "utf8");
|
|
32636
|
+
return "updated";
|
|
32637
|
+
}
|
|
32444
32638
|
async function initTestPilotConfig(root, info, adapters) {
|
|
32445
|
-
const yamlPath =
|
|
32639
|
+
const yamlPath = join23(root, "testpilot.yaml");
|
|
32446
32640
|
let yaml = "reused";
|
|
32447
|
-
if (!await
|
|
32448
|
-
await
|
|
32641
|
+
if (!await exists4(yamlPath)) {
|
|
32642
|
+
await writeFile10(yamlPath, renderTestpilotYaml(info), "utf8");
|
|
32449
32643
|
yaml = "created";
|
|
32450
32644
|
}
|
|
32451
|
-
const
|
|
32452
|
-
|
|
32453
|
-
|
|
32454
|
-
|
|
32455
|
-
|
|
32645
|
+
const envExamplePath = join23(root, ".env.example");
|
|
32646
|
+
let envExample = "reused";
|
|
32647
|
+
if (!await exists4(envExamplePath)) {
|
|
32648
|
+
await writeFile10(envExamplePath, renderDotEnvExample(), "utf8");
|
|
32649
|
+
envExample = "created";
|
|
32650
|
+
}
|
|
32651
|
+
const gitignore = await ensureGitignoreEntry(root, ".env");
|
|
32652
|
+
const runtimeDir = join23(root, ".testpilot", "artifacts");
|
|
32653
|
+
await mkdir12(runtimeDir, { recursive: true });
|
|
32654
|
+
const keep = join23(runtimeDir, ".gitkeep");
|
|
32655
|
+
if (!await exists4(keep)) {
|
|
32656
|
+
await writeFile10(keep, "", "utf8");
|
|
32456
32657
|
}
|
|
32457
32658
|
const existing = await readProjectLink(root);
|
|
32458
32659
|
const defaultAdapter = defaultAdapterFor(info.type);
|
|
@@ -32471,7 +32672,14 @@ async function initTestPilotConfig(root, info, adapters) {
|
|
|
32471
32672
|
}
|
|
32472
32673
|
if (link.serverUrl === void 0 && serverUrl) link.serverUrl = serverUrl;
|
|
32473
32674
|
const projectLinkFile = await writeProjectLink(link, root);
|
|
32474
|
-
return {
|
|
32675
|
+
return {
|
|
32676
|
+
yaml,
|
|
32677
|
+
runtimeDir: join23(root, ".testpilot"),
|
|
32678
|
+
projectLink: link,
|
|
32679
|
+
projectLinkFile,
|
|
32680
|
+
envExample,
|
|
32681
|
+
gitignore
|
|
32682
|
+
};
|
|
32475
32683
|
}
|
|
32476
32684
|
async function createProjectOnServer(root, info, _adapters, serverUrl) {
|
|
32477
32685
|
const git = info.git;
|
|
@@ -32500,106 +32708,6 @@ function randomId() {
|
|
|
32500
32708
|
return Math.random().toString(16).slice(2, 10);
|
|
32501
32709
|
}
|
|
32502
32710
|
|
|
32503
|
-
// src/commands/init/skill-installer.ts
|
|
32504
|
-
import { existsSync as existsSync7 } from "node:fs";
|
|
32505
|
-
import { access as access3, cp, mkdir as mkdir12, writeFile as writeFile10 } from "node:fs/promises";
|
|
32506
|
-
import { dirname as dirname7, join as join23 } from "node:path";
|
|
32507
|
-
import { fileURLToPath } from "node:url";
|
|
32508
|
-
async function exists4(path) {
|
|
32509
|
-
try {
|
|
32510
|
-
await access3(path);
|
|
32511
|
-
return true;
|
|
32512
|
-
} catch {
|
|
32513
|
-
return false;
|
|
32514
|
-
}
|
|
32515
|
-
}
|
|
32516
|
-
function resolveSkillSourceDir() {
|
|
32517
|
-
const fromEnv = process.env.TESTPILOT_SKILL_DIR;
|
|
32518
|
-
if (fromEnv) return fromEnv;
|
|
32519
|
-
const bundled = fileURLToPath(new URL("./skills/testpilot", import.meta.url));
|
|
32520
|
-
if (existsSync7(bundled)) return bundled;
|
|
32521
|
-
const inRepo = fileURLToPath(new URL("../../../../../skills/testpilot", import.meta.url));
|
|
32522
|
-
return existsSync7(inRepo) ? inRepo : void 0;
|
|
32523
|
-
}
|
|
32524
|
-
async function installSkill(root, info, adapters) {
|
|
32525
|
-
const dest = join23(root, ".agents", "skills", "testpilot");
|
|
32526
|
-
let core = "reused";
|
|
32527
|
-
if (!await exists4(join23(dest, "manifest.yaml"))) {
|
|
32528
|
-
const src = resolveSkillSourceDir();
|
|
32529
|
-
if (!src) {
|
|
32530
|
-
throw new Error(
|
|
32531
|
-
"\u65E0\u6CD5\u5B9A\u4F4D TestPilot Skill \u6E90\u76EE\u5F55;\u53EF\u901A\u8FC7\u73AF\u5883\u53D8\u91CF TESTPILOT_SKILL_DIR \u6307\u5B9A skills/testpilot \u8DEF\u5F84"
|
|
32532
|
-
);
|
|
32533
|
-
}
|
|
32534
|
-
await mkdir12(dirname7(dest), { recursive: true });
|
|
32535
|
-
await cp(src, dest, { recursive: true });
|
|
32536
|
-
core = "created";
|
|
32537
|
-
}
|
|
32538
|
-
const config2 = await loadTestpilotConfig(root).catch(() => void 0);
|
|
32539
|
-
const defaultEnvironment = typeof config2?.environment?.default === "string" ? config2.environment.default : "\u672A\u8BBE\u7F6E";
|
|
32540
|
-
const defaultWorkspace = config2?.workspace?.default ?? "\u672A\u8BBE\u7F6E(\u53EF\u5728 yaml workspace.default \u914D\u7F6E)";
|
|
32541
|
-
const refsDir = join23(dest, "references");
|
|
32542
|
-
const files = [
|
|
32543
|
-
["project.md", () => renderProjectMd(info, adapters, defaultEnvironment, defaultWorkspace)],
|
|
32544
|
-
["test-conventions.md", renderTestConventionsMd],
|
|
32545
|
-
["adapters.md", () => renderAdaptersMd(adapters)]
|
|
32546
|
-
];
|
|
32547
|
-
let references = "reused";
|
|
32548
|
-
for (const [name, render] of files) {
|
|
32549
|
-
const file2 = join23(refsDir, name);
|
|
32550
|
-
if (await exists4(file2)) continue;
|
|
32551
|
-
await mkdir12(refsDir, { recursive: true });
|
|
32552
|
-
await writeFile10(file2, render(), "utf8");
|
|
32553
|
-
references = "created";
|
|
32554
|
-
}
|
|
32555
|
-
return { core, references, dest };
|
|
32556
|
-
}
|
|
32557
|
-
function renderProjectMd(info, adapters, defaultEnvironment, defaultWorkspace) {
|
|
32558
|
-
const available = adapters.filter((item) => item.available).map((item) => item.id);
|
|
32559
|
-
return `# Project
|
|
32560
|
-
|
|
32561
|
-
\u81EA\u52A8\u751F\u6210\u4E8E \`csspilot init\`,\u63CF\u8FF0\u672C\u9879\u76EE\u7684 TestPilot \u63A5\u5165\u4FE1\u606F;Agent \u4F18\u5148\u4ECE\u8FD9\u91CC\u4E86\u89E3\u9879\u76EE\u3002
|
|
32562
|
-
|
|
32563
|
-
- Project name: ${info.name}
|
|
32564
|
-
- Project type: ${info.type}
|
|
32565
|
-
- Language: ${info.language}
|
|
32566
|
-
- Package manager: ${info.packageManager}
|
|
32567
|
-
- Test directory: tests/e2e
|
|
32568
|
-
- Case directory: tests/e2e/cases
|
|
32569
|
-
- Available adapters: ${available.length > 0 ? available.join(", ") : "(none detected)"}
|
|
32570
|
-
- Default environment: ${defaultEnvironment}
|
|
32571
|
-
- Default workspace: ${defaultWorkspace}
|
|
32572
|
-
`;
|
|
32573
|
-
}
|
|
32574
|
-
function renderTestConventionsMd() {
|
|
32575
|
-
return `# Test Conventions
|
|
32576
|
-
|
|
32577
|
-
\u672C\u9879\u76EE E2E \u6D4B\u8BD5\u7EA6\u5B9A,Agent \u751F\u6210/\u4FEE\u6539 Case \u65F6\u5FC5\u987B\u9075\u5B88:
|
|
32578
|
-
|
|
32579
|
-
1. E2E Case \u4F4D\u4E8E tests/e2e/cases
|
|
32580
|
-
2. Case \u4F7F\u7528 TestPilot DSL
|
|
32581
|
-
3. \u4E0D\u5141\u8BB8\u76F4\u63A5\u8C03\u7528 Playwright
|
|
32582
|
-
4. \u4E0D\u5141\u8BB8\u76F4\u63A5\u8C03\u7528 WeChatIDE
|
|
32583
|
-
5. \u4E0D\u5141\u8BB8\u5728 Case \u4E2D\u5199\u660E\u6587\u5BC6\u7801(\u7528 accountRef \u5F15\u7528)
|
|
32584
|
-
6. \u4F18\u5148\u590D\u7528\u5DF2\u6709 Case
|
|
32585
|
-
7. \u65B0\u589E Case \u5FC5\u987B\u901A\u8FC7 csspilot validate
|
|
32586
|
-
8. Case ID \u5FC5\u987B\u552F\u4E00
|
|
32587
|
-
`;
|
|
32588
|
-
}
|
|
32589
|
-
function renderAdaptersMd(adapters) {
|
|
32590
|
-
const lines = adapters.map(
|
|
32591
|
-
(item) => `- ${item.id}: ${item.available ? "\u53EF\u7528" : "\u4E0D\u53EF\u7528"}${item.required ? "(\u9879\u76EE\u9700\u8981)" : "(\u53EF\u9009)"} \u2014 ${item.detail}`
|
|
32592
|
-
);
|
|
32593
|
-
return `# Adapters
|
|
32594
|
-
|
|
32595
|
-
\u6267\u884C\u7AEF\u68C0\u6D4B\u7ED3\u679C(\u4EE5 \`csspilot doctor\` \u6700\u65B0\u8F93\u51FA\u4E3A\u51C6):
|
|
32596
|
-
|
|
32597
|
-
${lines.join("\n")}
|
|
32598
|
-
|
|
32599
|
-
Agent \u6C38\u8FDC\u4E0D\u76F4\u63A5\u64CD\u4F5C Adapter;Case \u7ECF Execution Engine \u8C03\u5EA6\u5230\u5BF9\u5E94 Adapter \u6267\u884C\u3002
|
|
32600
|
-
`;
|
|
32601
|
-
}
|
|
32602
|
-
|
|
32603
32711
|
// src/commands/init/test-structure.ts
|
|
32604
32712
|
import { existsSync as existsSync8, readdirSync as readdirSync2 } from "node:fs";
|
|
32605
32713
|
import { mkdir as mkdir13, writeFile as writeFile11 } from "node:fs/promises";
|
|
@@ -32657,6 +32765,12 @@ async function runInit(root, options = {}) {
|
|
|
32657
32765
|
console.log(`\u2713 Runtime \u76EE\u5F55 ${join25(".testpilot", "artifacts")}`);
|
|
32658
32766
|
const serverMode = config2.projectLink.serverUrl ? `Server:${config2.projectLink.serverUrl}` : "\u672C\u5730\u6A21\u5F0F";
|
|
32659
32767
|
console.log(`\u2713 \u9879\u76EE\u5173\u8054 ${config2.projectLink.projectId}(${serverMode})`);
|
|
32768
|
+
console.log(
|
|
32769
|
+
config2.envExample === "created" ? "\u2713 \u73AF\u5883\u53D8\u91CF\u793A\u4F8B .env.example(\u590D\u5236\u4E3A .env \u586B\u5165\u771F\u5B9E\u503C)" : "\u2713 \u73AF\u5883\u53D8\u91CF\u793A\u4F8B .env.example(\u5DF2\u5B58\u5728,\u4FDD\u7559)"
|
|
32770
|
+
);
|
|
32771
|
+
console.log(
|
|
32772
|
+
config2.gitignore === "exists" ? "\u2713 Git \u5FFD\u7565 .env(\u5DF2\u5728 .gitignore)" : `\u2713 Git \u5FFD\u7565 .env(\u5DF2\u5199\u5165 .gitignore)`
|
|
32773
|
+
);
|
|
32660
32774
|
const testStructure = await initTestStructure(root, project);
|
|
32661
32775
|
const reuseNote = project.existingE2eDirs.filter((dir) => dir !== "tests/e2e");
|
|
32662
32776
|
console.log(
|
|
@@ -32666,9 +32780,15 @@ async function runInit(root, options = {}) {
|
|
|
32666
32780
|
console.log(` \u26A0 \u68C0\u6D4B\u5230\u5176\u4ED6 E2E \u76EE\u5F55:${reuseNote.join(", ")}(\u4FDD\u7559\u4E0D\u52A8,\u672A\u5F3A\u5236\u8FC1\u79FB)`);
|
|
32667
32781
|
}
|
|
32668
32782
|
const skill = await installSkill(root, project, adapters);
|
|
32669
|
-
|
|
32670
|
-
|
|
32671
|
-
|
|
32783
|
+
if (skill.core === "updated") {
|
|
32784
|
+
console.log(
|
|
32785
|
+
`\u2713 Skill \u66F4\u65B0 .agents/skills/testpilot/(v${skill.coreVersion?.from ?? "?"} -> v${skill.coreVersion?.to ?? "?"})`
|
|
32786
|
+
);
|
|
32787
|
+
} else {
|
|
32788
|
+
console.log(
|
|
32789
|
+
skill.core === "created" ? `\u2713 Skill \u5B89\u88C5 ${join25(".agents", "skills", "testpilot")}` : "\u2713 Skill \u5B89\u88C5 .agents/skills/testpilot/(\u5DF2\u5B58\u5728,\u8DF3\u8FC7)"
|
|
32790
|
+
);
|
|
32791
|
+
}
|
|
32672
32792
|
console.log(
|
|
32673
32793
|
skill.references === "created" ? "\u2713 Agent \u4E0A\u4E0B\u6587 references/{project,test-conventions,adapters}.md" : "\u2713 Agent \u4E0A\u4E0B\u6587 references/(\u5DF2\u5B58\u5728,\u4FDD\u7559)"
|
|
32674
32794
|
);
|
|
@@ -32898,6 +33018,37 @@ function makeSyncMetadataCommand() {
|
|
|
32898
33018
|
});
|
|
32899
33019
|
}
|
|
32900
33020
|
|
|
33021
|
+
// src/commands/update.ts
|
|
33022
|
+
import { writeFile as writeFile12 } from "node:fs/promises";
|
|
33023
|
+
import { join as join27 } from "node:path";
|
|
33024
|
+
function makeUpdateCommand() {
|
|
33025
|
+
const update = new Command("update").description("\u540C\u6B65 TestPilot \u63A5\u5165\u7269\u5230\u5F53\u524D CLI \u7248\u672C(Skill / .env.example;\u4E0D\u89E6\u78B0 .env \u4E0E\u7528\u4F8B)").option("--force", "Skill \u7248\u672C\u4E00\u81F4\u65F6\u4E5F\u5F3A\u5236\u8986\u76D6\u672C\u5730\u4FEE\u6539", false).action(async (options) => {
|
|
33026
|
+
const root = process.cwd();
|
|
33027
|
+
const dest = join27(".agents", "skills", "testpilot");
|
|
33028
|
+
const skill = await syncSkillFiles(root, { force: options.force });
|
|
33029
|
+
switch (skill.core) {
|
|
33030
|
+
case "created":
|
|
33031
|
+
console.log(`\u2713 Skill \u5B89\u88C5 ${dest}(v${skill.bundledVersion ?? "?"})`);
|
|
33032
|
+
break;
|
|
33033
|
+
case "updated":
|
|
33034
|
+
console.log(
|
|
33035
|
+
`\u2713 Skill \u66F4\u65B0 ${dest} v${skill.installedVersion ?? "?"} -> v${skill.bundledVersion ?? "?"}`
|
|
33036
|
+
);
|
|
33037
|
+
break;
|
|
33038
|
+
default:
|
|
33039
|
+
console.log(`\u2713 Skill \u5DF2\u662F\u6700\u65B0 v${skill.bundledVersion ?? "?"}`);
|
|
33040
|
+
}
|
|
33041
|
+
await writeFile12(join27(root, ".env.example"), renderDotEnvExample(), "utf8");
|
|
33042
|
+
console.log("\u2713 .env.example \u5DF2\u540C\u6B65\u4E3A\u5F53\u524D\u7248\u672C\u7684\u793A\u4F8B(.env \u4E0D\u53D7\u5F71\u54CD)");
|
|
33043
|
+
const gitignore = await ensureGitignoreEntry(root, ".env");
|
|
33044
|
+
console.log(
|
|
33045
|
+
gitignore === "exists" ? "\u2713 .gitignore \u5DF2\u5FFD\u7565 .env" : "\u2713 .gitignore \u5DF2\u8FFD\u52A0\u5FFD\u7565 .env"
|
|
33046
|
+
);
|
|
33047
|
+
console.log("\u63D0\u793A:\u63D0\u4EA4 .agents/skills/ \u4E0E .env.example \u7684\u53D8\u66F4,\u56E2\u961F\u4E0E AI \u5373\u53EF\u4F7F\u7528\u6700\u65B0\u80FD\u529B");
|
|
33048
|
+
});
|
|
33049
|
+
return update;
|
|
33050
|
+
}
|
|
33051
|
+
|
|
32901
33052
|
// src/commands/validate.ts
|
|
32902
33053
|
function makeValidateCommand() {
|
|
32903
33054
|
return new Command("validate").description("\u6821\u9A8C\u7528\u4F8B DSL").argument("[paths...]", "\u7528\u4F8B\u6587\u4EF6\u6216\u76EE\u5F55,\u9ED8\u8BA4\u6309\u9879\u76EE\u914D\u7F6E").action(async (paths) => {
|
|
@@ -32934,10 +33085,10 @@ function makeValidateCommand() {
|
|
|
32934
33085
|
}
|
|
32935
33086
|
|
|
32936
33087
|
// src/cli.ts
|
|
32937
|
-
var CLI_VERSION = "0.
|
|
33088
|
+
var CLI_VERSION = "0.4.0";
|
|
32938
33089
|
async function createCli() {
|
|
32939
33090
|
const program2 = new Command();
|
|
32940
|
-
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());
|
|
33091
|
+
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(makeUpdateCommand()).addCommand(makeCiCommand());
|
|
32941
33092
|
return program2;
|
|
32942
33093
|
}
|
|
32943
33094
|
export {
|
|
@@ -12,6 +12,7 @@ TestPilot is a **tool, not a business test project**. Business cases always belo
|
|
|
12
12
|
|
|
13
13
|
Use TestPilot when the user wants to:
|
|
14
14
|
|
|
15
|
+
- review test requirements for testability
|
|
15
16
|
- create business test cases
|
|
16
17
|
- validate test cases
|
|
17
18
|
- run tests
|
|
@@ -20,12 +21,13 @@ Use TestPilot when the user wants to:
|
|
|
20
21
|
## Core workflow
|
|
21
22
|
|
|
22
23
|
1. Understand user requirement
|
|
23
|
-
2.
|
|
24
|
-
3.
|
|
25
|
-
4.
|
|
26
|
-
5.
|
|
27
|
-
6.
|
|
28
|
-
7.
|
|
24
|
+
2. Review requirements & produce test points (`workflows/test-requirements-review.md`)
|
|
25
|
+
3. Inspect project
|
|
26
|
+
4. Identify test target
|
|
27
|
+
5. Create TestPilot Case (`tests/e2e/cases/*.yaml`, linked to test point IDs)
|
|
28
|
+
6. Validate Case (`npx csspilot validate`)
|
|
29
|
+
7. Run Case (`npx csspilot run`)
|
|
30
|
+
8. Analyze result (`npx csspilot report`)
|
|
29
31
|
|
|
30
32
|
## Important rules
|
|
31
33
|
|
|
@@ -72,4 +74,5 @@ Read `references/` first (generated by `csspilot init` for this project):
|
|
|
72
74
|
- `rules/dsl.md` — case structure and the V0.1 action set
|
|
73
75
|
- `rules/locator.md` — locator forms and the no-invention rule
|
|
74
76
|
- `rules/assertion.md` — assertions and variable references
|
|
75
|
-
- `workflows
|
|
77
|
+
- `workflows/test-requirements-review.md` — requirements review, test point checklist
|
|
78
|
+
- `workflows/` — step-by-step workflows (review / create / validate / run / analyze)
|
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
# `npx csspilot init` 会把整个 skills/testpilot/ 安装到业务项目 .agents/skills/testpilot/
|
|
3
3
|
id: testpilot
|
|
4
4
|
name: TestPilot
|
|
5
|
-
version: 0.
|
|
5
|
+
version: 0.4.0
|
|
6
6
|
|
|
7
7
|
description: >
|
|
8
8
|
AI Native cross-platform testing skill.
|
|
9
9
|
|
|
10
10
|
capabilities:
|
|
11
|
+
- test-requirements-review
|
|
11
12
|
- create-case
|
|
12
13
|
- validate
|
|
13
14
|
- run
|
|
@@ -37,4 +38,4 @@ permissions:
|
|
|
37
38
|
- npx
|
|
38
39
|
- pnpm
|
|
39
40
|
- npm
|
|
40
|
-
-
|
|
41
|
+
- csspilot
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
# Workflow:创建测试 Case
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
输入:用户需求,或 `test-requirements-review` 产出的**测试点清单**(推荐先做需求评审)。
|
|
4
4
|
|
|
5
|
-
1.
|
|
6
|
-
2.
|
|
7
|
-
3.
|
|
8
|
-
4.
|
|
9
|
-
5. 按 DSL 规范生成 Case,写入 `tests/e2e/cases/<case-id>.yaml
|
|
5
|
+
1. 尚无测试点清单时,先执行 `workflows/test-requirements-review.md`;已有清单则直接使用。
|
|
6
|
+
2. 阅读本 Skill:`rules/dsl.md`、`rules/case-design.md`、`rules/locator.md`。
|
|
7
|
+
3. 分析项目结构:确定被测端(miniapp / web)与业务入口页面。
|
|
8
|
+
4. 针对测试点收集证据(读代码 / DOM / 现有测试),确定业务路径:从入口到业务结果页的完整步骤。
|
|
9
|
+
5. 按 DSL 规范生成 Case,写入 `tests/e2e/cases/<case-id>.yaml`:
|
|
10
|
+
- 一条 Case 覆盖一个测试点(或一组强关联测试点)
|
|
11
|
+
- Case 的 `description` 注明关联的测试点编号(如 `测试点: TP-001`),保持需求可溯源
|
|
12
|
+
- 按评审产出的平台执行信息声明 `accountRef` / `workspace`;发现 `.env` 缺少某平台的地址或凭据变量时,向用户索取后写入 `.env`,**不写进 Case**
|
|
10
13
|
6. 执行 `npx csspilot validate tests/e2e/cases/<case-id>.yaml`。
|
|
11
14
|
7. 校验失败则修复 Case 后重新 validate,直到通过。
|
|
12
15
|
|
|
13
|
-
禁止:跳过第
|
|
16
|
+
禁止:跳过第 3-4 步直接生成 Case(AI 不能凭空生成测试)。
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Workflow:测试需求评审(可测性评审 + 测试点分析)
|
|
2
|
+
|
|
3
|
+
输入:需求文档 / PRD / 用户的一句话(如"帮我测试用户下单")。
|
|
4
|
+
产出:**测试点清单** + **平台执行信息表** + 澄清问题 + 评审结论。测试点清单是 `create-case` 的正式输入。
|
|
5
|
+
|
|
6
|
+
## 步骤
|
|
7
|
+
|
|
8
|
+
1. **复述理解**:用自己的话列出需求覆盖的功能点与业务流程,向用户确认理解偏差。
|
|
9
|
+
2. **平台盘点**:识别需求涉及的执行端(miniapp / web / api)。跨端需求(如"小程序下单、Web 后台验证")必须逐平台澄清执行信息,见下方"多平台"一节。
|
|
10
|
+
3. **可测性检查清单**(逐项过):
|
|
11
|
+
- 验收标准是否明确:输入 / 操作步骤 / 预期结果是否可判定
|
|
12
|
+
- 边界与异常:空值、极值、重复提交、并发、权限、网络异常
|
|
13
|
+
- 数据准备可行性:测试账号、mock 数据、上下游依赖是否可用
|
|
14
|
+
- 平台与入口:miniapp / web 的页面路径与入口是否真实存在(不确定时先看项目代码)
|
|
15
|
+
- 回归范围:改动是否影响现有功能,需要哪些已有 Case 回归
|
|
16
|
+
4. **输出测试点清单**,每条包含:
|
|
17
|
+
|
|
18
|
+
| 字段 | 说明 |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| 编号 | `TP-001` 格式,Case 需回链 |
|
|
21
|
+
| 模块 | 所属功能模块 |
|
|
22
|
+
| 场景 | 一句话描述验证什么 |
|
|
23
|
+
| 类型 | 正向 / 边界 / 异常 |
|
|
24
|
+
| 优先级 | P0 / P1 / P2 |
|
|
25
|
+
| 平台 | miniapp / web / 双端 |
|
|
26
|
+
|
|
27
|
+
5. **澄清阻塞项**:需求缺失、矛盾、不可判定时,列出问题向用户提问。**不猜测**。
|
|
28
|
+
6. **给出评审结论**:
|
|
29
|
+
- 可测:测试点清单完整,直接进入 `create-case`
|
|
30
|
+
- 有条件可测:部分测试点依赖澄清,先对可执行部分建 Case
|
|
31
|
+
- 不可测:说明缺口,等待需求补充
|
|
32
|
+
|
|
33
|
+
## 多平台:平台执行信息澄清
|
|
34
|
+
|
|
35
|
+
需求涉及多个平台时,**每个平台都必须澄清以下三项**;缺任何一项,该平台相关测试点只能标记"有条件可测",不得开始写 Case:
|
|
36
|
+
|
|
37
|
+
| 平台 | 必须澄清 | 落地位置 |
|
|
38
|
+
| --- | --- | --- |
|
|
39
|
+
| web | 测试地址(baseUrl)+ 页面入口路由 + 登录角色 | `.env` 的 `WEB_BASE_URL` / `TEST_BASE_URL`;`accountRef` |
|
|
40
|
+
| miniapp | 小程序项目路径 + 起始页面路由 + 登录角色(连哪个环境后端) | `.env` 的 `MINIAPP_PROJECT_PATH`;`accountRef` |
|
|
41
|
+
| api | 服务 baseUrl + 鉴权方式(token / session)+ 账号角色 | `.env` 的 `API_BASE_URL`;`accountRef` |
|
|
42
|
+
|
|
43
|
+
规则:
|
|
44
|
+
|
|
45
|
+
- **登录角色**:每个平台确认用什么角色(普通用户 / 管理员 / 游客),映射为 `accountRef`(如 `admin` / `test-user`);真实凭据放 `TESTPILOT_ACCOUNT_<REF大写>` 环境变量或 Server Secrets,**禁止写进 Case**
|
|
46
|
+
- **测试地址**:一律指向测试环境,写入 `.env`(如 `WEB_BASE_URL`);**禁止把内网 / 生产地址写进 Case 或 testpilot.yaml**
|
|
47
|
+
- **使用 Workspace 的项目**:以上信息应登记为 Workspace 的绑定(项目 → 环境),Case 用 `workspace: <名称>` 声明,不逐条硬编码
|
|
48
|
+
|
|
49
|
+
### 澄清问题示例(多平台需求)
|
|
50
|
+
|
|
51
|
+
- Web 后台的测试地址是什么?订单查询页面的入口路由是?
|
|
52
|
+
- 小程序连接哪个环境的后端?小程序项目目录在本机哪里?
|
|
53
|
+
- 各平台分别用什么角色登录(普通用户 / 管理员)?该角色是否有完成流程所需的权限?
|
|
54
|
+
- 这些地址和账号是否已写入 `.env`?缺哪个我来补?
|
|
55
|
+
|
|
56
|
+
## 铁律
|
|
57
|
+
|
|
58
|
+
- 没有测试点清单,不要进入 `create-case`。
|
|
59
|
+
- 涉及多平台但执行信息(地址 / 入口 / 登录角色)不全时,结论只能是"有条件可测",先澄清再建 Case。
|
|
60
|
+
- 评审基于需求 + 项目代码/页面证据;两项都没有时先收集证据,不空谈。
|
|
61
|
+
- 每个测试点必须可验证:都能回答"怎么判定通过"。
|