@szc-ft/mcp-szcd-client 0.34.2 → 0.38.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/agents/build.js +59 -1
- package/agents/platforms.json +7 -0
- package/agents/szcd-component-expert.qoder.md +2 -0
- package/opencode-extension/opencode.json +9 -0
- package/opencode-extension/skills/local-browser-test/SKILL.md +91 -0
- package/opencode-extension/skills/local-browser-test/config.example.yaml +51 -0
- package/opencode-extension/skills/local-browser-test/lib/api-call-executor.js +251 -0
- package/opencode-extension/skills/local-browser-test/lib/browser-engine.js +95 -21
- package/opencode-extension/skills/local-browser-test/lib/config-loader.js +104 -0
- package/opencode-extension/skills/local-browser-test/lib/console-archiver.js +100 -0
- package/opencode-extension/skills/local-browser-test/lib/global-setup.js +72 -0
- package/opencode-extension/skills/local-browser-test/lib/har-collector.js +124 -0
- package/opencode-extension/skills/local-browser-test/lib/html-reporter.js +248 -0
- package/opencode-extension/skills/local-browser-test/lib/projects.js +46 -0
- package/opencode-extension/skills/local-browser-test/lib/raw-script-helpers.js +93 -0
- package/opencode-extension/skills/local-browser-test/lib/redact.js +94 -0
- package/opencode-extension/skills/local-browser-test/lib/report-retain.js +84 -0
- package/opencode-extension/skills/local-browser-test/lib/retry-policy.js +160 -0
- package/opencode-extension/skills/local-browser-test/lib/storage-state.js +111 -0
- package/opencode-extension/skills/local-browser-test/lib/tag-runner.js +103 -0
- package/opencode-extension/skills/local-browser-test/lib/test-plan.js +2 -0
- package/opencode-extension/skills/local-browser-test/lib/trace-collector.js +94 -0
- package/opencode-extension/skills/local-browser-test/lib/trend-aggregator.js +97 -0
- package/opencode-extension/skills/local-browser-test/lib/workspace-init.js +178 -0
- package/opencode-extension/skills/local-browser-test/local-browser-executor.js +226 -2
- package/opencode-extension/skills/local-browser-test/tests/integration/pr-01.test.js +254 -0
- package/opencode-extension/skills/local-browser-test/tests/integration/pr-02.test.js +263 -0
- package/opencode-extension/skills/local-browser-test/tests/integration/pr-03.test.js +308 -0
- package/opencode-extension/skills/local-browser-test/tests/integration/pr-04.test.js +168 -0
- package/opencode-extension/skills/local-browser-test/tests/integration/pr-05.test.js +183 -0
- package/opencode-extension/skills/local-browser-test/tests/integration/pr-06.test.js +252 -0
- package/opencode-extension/skills/local-browser-test/tests/integration/pr-07.test.js +168 -0
- package/opencode-extension/skills/local-browser-test/tests/integration/pr-08.test.js +293 -0
- package/opencode-extension/skills/shadcn-design-to-code/SKILL.md +352 -0
- package/opencode-extension/skills/shadcn-design-to-code/examples/1.1-overview-page.tsx +382 -0
- package/opencode-extension/skills/shadcn-design-to-code/references/install-pitfalls.md +526 -0
- package/opencode-extension/skills/shadcn-design-to-code/references/layouts.md +551 -0
- package/opencode-extension/skills/shadcn-design-to-code/references/sketch-mapping.md +247 -0
- package/opencode-extension/skills/shadcn-design-to-code/references/tokens.md +534 -0
- package/opencode-extension/skills/szcd-upload-zip/SKILL.md +45 -0
- package/package.json +70 -70
- package/qwen-extension/qwen-extension.json +10 -1
- package/qwen-extension/skills/local-browser-test/SKILL.md +91 -0
- package/qwen-extension/skills/local-browser-test/config.example.yaml +51 -0
- package/qwen-extension/skills/local-browser-test/lib/api-call-executor.js +251 -0
- package/qwen-extension/skills/local-browser-test/lib/browser-engine.js +95 -21
- package/qwen-extension/skills/local-browser-test/lib/config-loader.js +104 -0
- package/qwen-extension/skills/local-browser-test/lib/console-archiver.js +100 -0
- package/qwen-extension/skills/local-browser-test/lib/global-setup.js +72 -0
- package/qwen-extension/skills/local-browser-test/lib/har-collector.js +124 -0
- package/qwen-extension/skills/local-browser-test/lib/html-reporter.js +248 -0
- package/qwen-extension/skills/local-browser-test/lib/projects.js +46 -0
- package/qwen-extension/skills/local-browser-test/lib/raw-script-helpers.js +93 -0
- package/qwen-extension/skills/local-browser-test/lib/redact.js +94 -0
- package/qwen-extension/skills/local-browser-test/lib/report-retain.js +84 -0
- package/qwen-extension/skills/local-browser-test/lib/retry-policy.js +160 -0
- package/qwen-extension/skills/local-browser-test/lib/storage-state.js +111 -0
- package/qwen-extension/skills/local-browser-test/lib/tag-runner.js +103 -0
- package/qwen-extension/skills/local-browser-test/lib/test-plan.js +2 -0
- package/qwen-extension/skills/local-browser-test/lib/trace-collector.js +94 -0
- package/qwen-extension/skills/local-browser-test/lib/trend-aggregator.js +97 -0
- package/qwen-extension/skills/local-browser-test/lib/workspace-init.js +178 -0
- package/qwen-extension/skills/local-browser-test/local-browser-executor.js +226 -2
- package/qwen-extension/skills/local-browser-test/tests/integration/pr-01.test.js +254 -0
- package/qwen-extension/skills/local-browser-test/tests/integration/pr-02.test.js +263 -0
- package/qwen-extension/skills/local-browser-test/tests/integration/pr-03.test.js +308 -0
- package/qwen-extension/skills/local-browser-test/tests/integration/pr-04.test.js +168 -0
- package/qwen-extension/skills/local-browser-test/tests/integration/pr-05.test.js +183 -0
- package/qwen-extension/skills/local-browser-test/tests/integration/pr-06.test.js +252 -0
- package/qwen-extension/skills/local-browser-test/tests/integration/pr-07.test.js +168 -0
- package/qwen-extension/skills/local-browser-test/tests/integration/pr-08.test.js +293 -0
- package/qwen-extension/skills/shadcn-design-to-code/SKILL.md +352 -0
- package/qwen-extension/skills/shadcn-design-to-code/examples/1.1-overview-page.tsx +382 -0
- package/qwen-extension/skills/shadcn-design-to-code/references/install-pitfalls.md +526 -0
- package/qwen-extension/skills/shadcn-design-to-code/references/layouts.md +551 -0
- package/qwen-extension/skills/shadcn-design-to-code/references/sketch-mapping.md +247 -0
- package/qwen-extension/skills/shadcn-design-to-code/references/tokens.md +534 -0
- package/qwen-extension/skills/szcd-upload-zip/SKILL.md +45 -0
- package/scripts/lib/common.js +8 -1
- package/standard-skill/local-browser-test/SKILL.md +91 -0
- package/standard-skill/local-browser-test/config.example.yaml +51 -0
- package/standard-skill/local-browser-test/lib/api-call-executor.js +251 -0
- package/standard-skill/local-browser-test/lib/browser-engine.js +95 -21
- package/standard-skill/local-browser-test/lib/config-loader.js +104 -0
- package/standard-skill/local-browser-test/lib/console-archiver.js +100 -0
- package/standard-skill/local-browser-test/lib/global-setup.js +72 -0
- package/standard-skill/local-browser-test/lib/har-collector.js +124 -0
- package/standard-skill/local-browser-test/lib/html-reporter.js +248 -0
- package/standard-skill/local-browser-test/lib/projects.js +46 -0
- package/standard-skill/local-browser-test/lib/raw-script-helpers.js +93 -0
- package/standard-skill/local-browser-test/lib/redact.js +94 -0
- package/standard-skill/local-browser-test/lib/report-retain.js +84 -0
- package/standard-skill/local-browser-test/lib/retry-policy.js +160 -0
- package/standard-skill/local-browser-test/lib/storage-state.js +111 -0
- package/standard-skill/local-browser-test/lib/tag-runner.js +103 -0
- package/standard-skill/local-browser-test/lib/test-plan.js +2 -0
- package/standard-skill/local-browser-test/lib/trace-collector.js +94 -0
- package/standard-skill/local-browser-test/lib/trend-aggregator.js +97 -0
- package/standard-skill/local-browser-test/lib/workspace-init.js +178 -0
- package/standard-skill/local-browser-test/local-browser-executor.js +226 -2
- package/standard-skill/local-browser-test/tests/integration/pr-01.test.js +254 -0
- package/standard-skill/local-browser-test/tests/integration/pr-02.test.js +263 -0
- package/standard-skill/local-browser-test/tests/integration/pr-03.test.js +308 -0
- package/standard-skill/local-browser-test/tests/integration/pr-04.test.js +168 -0
- package/standard-skill/local-browser-test/tests/integration/pr-05.test.js +183 -0
- package/standard-skill/local-browser-test/tests/integration/pr-06.test.js +252 -0
- package/standard-skill/local-browser-test/tests/integration/pr-07.test.js +168 -0
- package/standard-skill/local-browser-test/tests/integration/pr-08.test.js +293 -0
- package/standard-skill/shadcn-design-to-code/SKILL.md +352 -0
- package/standard-skill/shadcn-design-to-code/examples/1.1-overview-page.tsx +382 -0
- package/standard-skill/shadcn-design-to-code/references/install-pitfalls.md +526 -0
- package/standard-skill/shadcn-design-to-code/references/layouts.md +551 -0
- package/standard-skill/shadcn-design-to-code/references/sketch-mapping.md +247 -0
- package/standard-skill/shadcn-design-to-code/references/tokens.md +534 -0
- package/standard-skill/szcd-upload-zip/SKILL.md +45 -0
- package/opencode-extension/skills/local-browser-test/local-browser-executor.cjs +0 -742
- package/qwen-extension/skills/local-browser-test/local-browser-executor.cjs +0 -742
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* trend.json 按日聚合(v4 关键能力)
|
|
3
|
+
*
|
|
4
|
+
* 设计:
|
|
5
|
+
* - schemaVersion: 1
|
|
6
|
+
* - byDay[YYYY-MM-DD]: { runs, total, passed, failed, flaky, skipped,
|
|
7
|
+
* byStrategy: {api, ui, newman, browser},
|
|
8
|
+
* byTag: {}, byProject: {}, byBranch: {} }
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
|
|
13
|
+
const DEFAULTS = {
|
|
14
|
+
schemaVersion: 1,
|
|
15
|
+
updatedAt: null,
|
|
16
|
+
byDay: {}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 把一次 run 追加到 trend.json
|
|
21
|
+
* @param {object} result
|
|
22
|
+
* @param {string} trendPath
|
|
23
|
+
*/
|
|
24
|
+
export function appendToTrend(result, trendPath) {
|
|
25
|
+
const trend = fs.existsSync(trendPath)
|
|
26
|
+
? JSON.parse(fs.readFileSync(trendPath, "utf8"))
|
|
27
|
+
: structuredClone(DEFAULTS);
|
|
28
|
+
|
|
29
|
+
const day = (result.startedAt ?? new Date().toISOString()).slice(0, 10);
|
|
30
|
+
if (!trend.byDay[day]) {
|
|
31
|
+
trend.byDay[day] = {
|
|
32
|
+
runs: 0, total: 0, passed: 0, failed: 0, flaky: 0, skipped: 0,
|
|
33
|
+
byStrategy: { api: 0, ui: 0, newman: 0, browser: 0 },
|
|
34
|
+
byTag: {},
|
|
35
|
+
byProject: {},
|
|
36
|
+
byBranch: {}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const d = trend.byDay[day];
|
|
41
|
+
d.runs += 1;
|
|
42
|
+
d.total += result.stats?.total ?? 0;
|
|
43
|
+
d.passed += result.stats?.passed ?? 0;
|
|
44
|
+
d.failed += result.stats?.failed ?? 0;
|
|
45
|
+
d.flaky += result.stats?.flaky ?? 0;
|
|
46
|
+
d.skipped += result.stats?.skipped ?? 0;
|
|
47
|
+
|
|
48
|
+
// ★ v4 新增:按 strategy 分桶
|
|
49
|
+
const strategy = result.strategy ?? "newman";
|
|
50
|
+
d.byStrategy[strategy] = (d.byStrategy[strategy] ?? 0) + (result.stats?.total ?? 0);
|
|
51
|
+
|
|
52
|
+
// 按 tag 分桶
|
|
53
|
+
if (result.tag) {
|
|
54
|
+
if (!d.byTag[result.tag]) d.byTag[result.tag] = { runs: 0, passed: 0, failed: 0 };
|
|
55
|
+
const t = d.byTag[result.tag];
|
|
56
|
+
t.runs += 1;
|
|
57
|
+
t.passed += result.stats?.passed ?? 0;
|
|
58
|
+
t.failed += result.stats?.failed ?? 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 按 project 分桶
|
|
62
|
+
if (result.project) {
|
|
63
|
+
if (!d.byProject[result.project]) d.byProject[result.project] = { runs: 0, passed: 0, failed: 0 };
|
|
64
|
+
const p = d.byProject[result.project];
|
|
65
|
+
p.runs += 1;
|
|
66
|
+
p.passed += result.stats?.passed ?? 0;
|
|
67
|
+
p.failed += result.stats?.failed ?? 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 按 branch 分桶
|
|
71
|
+
if (result.branch) {
|
|
72
|
+
if (!d.byBranch[result.branch]) d.byBranch[result.branch] = { runs: 0, passed: 0, failed: 0 };
|
|
73
|
+
const b = d.byBranch[result.branch];
|
|
74
|
+
b.runs += 1;
|
|
75
|
+
b.passed += result.stats?.passed ?? 0;
|
|
76
|
+
b.failed += result.stats?.failed ?? 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
trend.updatedAt = new Date().toISOString();
|
|
80
|
+
fs.writeFileSync(trendPath, JSON.stringify(trend, null, 2));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 裁剪 trend.json:保留最近 N 天
|
|
85
|
+
* @param {string} trendPath
|
|
86
|
+
* @param {number} days
|
|
87
|
+
*/
|
|
88
|
+
export function trimTrendToDays(trendPath, days) {
|
|
89
|
+
if (!fs.existsSync(trendPath)) return;
|
|
90
|
+
const trend = JSON.parse(fs.readFileSync(trendPath, "utf8"));
|
|
91
|
+
const cutoff = new Date(Date.now() - days * 24 * 3600 * 1000).toISOString().slice(0, 10);
|
|
92
|
+
for (const day of Object.keys(trend.byDay)) {
|
|
93
|
+
if (day < cutoff) delete trend.byDay[day];
|
|
94
|
+
}
|
|
95
|
+
trend.updatedAt = new Date().toISOString();
|
|
96
|
+
fs.writeFileSync(trendPath, JSON.stringify(trend, null, 2));
|
|
97
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* .lbt/ workspace 自动初始化(v3 多次调用安全)
|
|
3
|
+
*
|
|
4
|
+
* 关键设计:
|
|
5
|
+
* - 缺啥补啥,绝不覆盖任何已有文件
|
|
6
|
+
* - 双层 .gitignore(.lbt/.gitignore + 项目级 .gitignore 追加)
|
|
7
|
+
* - .fingerprint 走 read → merge → write 增量更新
|
|
8
|
+
* - --init-force 显式重置(需 yes 二次确认)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import readline from "node:readline";
|
|
14
|
+
|
|
15
|
+
// ESM-friendly 方式读 package.json(避免 createRequire 在 lib 内 resolve 失败)
|
|
16
|
+
const PKG_URL = new URL("../package.json", import.meta.url);
|
|
17
|
+
const PKG = JSON.parse(fs.readFileSync(PKG_URL, "utf8"));
|
|
18
|
+
|
|
19
|
+
const INNER_GITIGNORE_CONTENT =
|
|
20
|
+
"auth/\n" +
|
|
21
|
+
"*.log\n" +
|
|
22
|
+
"config.yaml\n" +
|
|
23
|
+
"reports/\n";
|
|
24
|
+
|
|
25
|
+
const PROJECT_GITIGNORE_RULES = [
|
|
26
|
+
".lbt/auth/",
|
|
27
|
+
".lbt/config.yaml",
|
|
28
|
+
".lbt/reports/",
|
|
29
|
+
".lbt/*.log"
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 幂等追加到 .gitignore(不破坏用户已有内容)
|
|
34
|
+
* @param {string} projectGi 项目级 .gitignore 路径
|
|
35
|
+
* @param {string[]} rules 要追加的规则
|
|
36
|
+
* @returns {string[]} 实际新增的规则(已存在的跳过)
|
|
37
|
+
*/
|
|
38
|
+
export function appendIfMissing(projectGi, rules) {
|
|
39
|
+
if (!fs.existsSync(projectGi)) {
|
|
40
|
+
fs.writeFileSync(projectGi, rules.join("\n") + "\n");
|
|
41
|
+
return rules;
|
|
42
|
+
}
|
|
43
|
+
const content = fs.readFileSync(projectGi, "utf8");
|
|
44
|
+
const missing = rules.filter((r) => !content.includes(r));
|
|
45
|
+
if (missing.length > 0) {
|
|
46
|
+
const header = "\n# szcd local-browser-test workspace\n";
|
|
47
|
+
fs.appendFileSync(projectGi, header + missing.join("\n") + "\n");
|
|
48
|
+
return missing;
|
|
49
|
+
}
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* ensureWorkspace: 缺啥补啥,绝不覆盖任何已有文件
|
|
55
|
+
* - 子目录:mkdir recursive(已存在则 no-op)
|
|
56
|
+
* - .lbt/.gitignore:已存在则跳过
|
|
57
|
+
* - config.yaml.example:已存在则跳过(用户可能改过)
|
|
58
|
+
* - config.yaml:永远不主动建(避免覆盖用户配置)
|
|
59
|
+
* - 项目 .gitignore:appendIfMissing
|
|
60
|
+
* - .fingerprint:read → merge → write(保留 firstInitAt / initCount)
|
|
61
|
+
*
|
|
62
|
+
* @param {string} SKILL_DIR skill 目录绝对路径
|
|
63
|
+
* @param {string} [lbtDirOverride] 可选,cwd 外的 .lbt/ 路径
|
|
64
|
+
* @returns {string} .lbt 实际路径
|
|
65
|
+
*/
|
|
66
|
+
export function ensureWorkspace(SKILL_DIR, lbtDirOverride = null) {
|
|
67
|
+
const lbtDir = lbtDirOverride || path.join(process.cwd(), ".lbt");
|
|
68
|
+
const isNew = !fs.existsSync(lbtDir);
|
|
69
|
+
const actions = [];
|
|
70
|
+
|
|
71
|
+
// 1. 子目录(idempotent:mkdir recursive 已存在则 no-op)
|
|
72
|
+
for (const sub of ["plans/regression", "plans/smoke", "reports", "auth", "api"]) {
|
|
73
|
+
fs.mkdirSync(path.join(lbtDir, sub), { recursive: true });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 2. .lbt/.gitignore(内部自带,双层防御)
|
|
77
|
+
const innerGi = path.join(lbtDir, ".gitignore");
|
|
78
|
+
if (!fs.existsSync(innerGi)) {
|
|
79
|
+
fs.writeFileSync(innerGi, INNER_GITIGNORE_CONTENT);
|
|
80
|
+
actions.push("+ 创建 .lbt/.gitignore");
|
|
81
|
+
} else {
|
|
82
|
+
actions.push("· .lbt/.gitignore 已存在,跳过");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 3. config.yaml.example(已存在则跳过,用户可能改过)
|
|
86
|
+
const examplePath = path.join(lbtDir, "config.yaml.example");
|
|
87
|
+
if (!fs.existsSync(examplePath)) {
|
|
88
|
+
const sourceExample = path.join(SKILL_DIR, "config.example.yaml");
|
|
89
|
+
if (fs.existsSync(sourceExample)) {
|
|
90
|
+
fs.copyFileSync(sourceExample, examplePath);
|
|
91
|
+
actions.push("+ 创建 config.yaml.example");
|
|
92
|
+
}
|
|
93
|
+
} else {
|
|
94
|
+
actions.push("· config.yaml.example 已存在,跳过");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 4. config.yaml — 永远不主动建(避免覆盖用户配置)
|
|
98
|
+
|
|
99
|
+
// 5. 项目级 .gitignore(幂等追加)
|
|
100
|
+
const appended = appendIfMissing(path.join(process.cwd(), ".gitignore"), PROJECT_GITIGNORE_RULES);
|
|
101
|
+
if (appended.length > 0) {
|
|
102
|
+
actions.push(`+ 项目 .gitignore 追加: ${appended.join(", ")}`);
|
|
103
|
+
} else {
|
|
104
|
+
actions.push("· 项目 .gitignore 已包含 .lbt/ 规则,跳过");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 6. .fingerprint(read → merge → write 保留 firstInitAt)
|
|
108
|
+
const fpPath = path.join(lbtDir, ".fingerprint");
|
|
109
|
+
const existing = fs.existsSync(fpPath)
|
|
110
|
+
? JSON.parse(fs.readFileSync(fpPath, "utf8"))
|
|
111
|
+
: {};
|
|
112
|
+
const now = new Date().toISOString();
|
|
113
|
+
const merged = {
|
|
114
|
+
...existing,
|
|
115
|
+
skillDir: SKILL_DIR,
|
|
116
|
+
executorVersion: PKG.version,
|
|
117
|
+
lastInitAt: now,
|
|
118
|
+
firstInitAt: existing.firstInitAt ?? now,
|
|
119
|
+
initCount: (existing.initCount ?? 0) + 1,
|
|
120
|
+
runCount: existing.runCount ?? 0, // 由 bumpRunCount 时 +1
|
|
121
|
+
};
|
|
122
|
+
fs.writeFileSync(fpPath, JSON.stringify(merged, null, 2));
|
|
123
|
+
|
|
124
|
+
// 7. 打印动作摘要
|
|
125
|
+
console.log(`${isNew ? "✓" : "↻"} workspace ${lbtDir}:`);
|
|
126
|
+
for (const a of actions) console.log(` ${a}`);
|
|
127
|
+
if (isNew) {
|
|
128
|
+
console.log(` 提示:cp .lbt/config.yaml.example .lbt/config.yaml 并填值`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return lbtDir;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* initForce: 显式强制重置(需 --init-force 显式开启 + yes 二次确认)
|
|
136
|
+
* @param {string} SKILL_DIR
|
|
137
|
+
* @param {string} [lbtDirOverride]
|
|
138
|
+
* @returns {Promise<string>} .lbt 实际路径
|
|
139
|
+
*/
|
|
140
|
+
export async function initForce(SKILL_DIR, lbtDirOverride = null) {
|
|
141
|
+
const lbtDir = lbtDirOverride || path.join(process.cwd(), ".lbt");
|
|
142
|
+
if (fs.existsSync(lbtDir)) {
|
|
143
|
+
console.log(`⚠ 将清空 .lbt/ 全部内容(含 plans / reports / auth / config)`);
|
|
144
|
+
console.log(` 如有重要 plan 请先备份到 .lbt/plans/ 之外的位置`);
|
|
145
|
+
const answer = await prompt("\n继续清空?(yes/no): ");
|
|
146
|
+
if (answer !== "yes") {
|
|
147
|
+
console.log("已取消,workspace 保持原样");
|
|
148
|
+
return lbtDir;
|
|
149
|
+
}
|
|
150
|
+
fs.rmSync(lbtDir, { recursive: true, force: true });
|
|
151
|
+
console.log("✓ 已清空 .lbt/");
|
|
152
|
+
}
|
|
153
|
+
return ensureWorkspace(SKILL_DIR, lbtDirOverride);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function prompt(question) {
|
|
157
|
+
return new Promise((resolve) => {
|
|
158
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
159
|
+
rl.question(question, (ans) => {
|
|
160
|
+
rl.close();
|
|
161
|
+
resolve(ans.trim().toLowerCase());
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* 跑完一次 run 后,更新 .fingerprint 的 runCount 和 lastRunAt
|
|
168
|
+
* @param {string} [lbtDirOverride]
|
|
169
|
+
*/
|
|
170
|
+
export function bumpRunCount(lbtDirOverride = null) {
|
|
171
|
+
const lbtDir = lbtDirOverride || path.join(process.cwd(), ".lbt");
|
|
172
|
+
const fpPath = path.join(lbtDir, ".fingerprint");
|
|
173
|
+
if (!fs.existsSync(fpPath)) return;
|
|
174
|
+
const fp = JSON.parse(fs.readFileSync(fpPath, "utf8"));
|
|
175
|
+
fp.runCount = (fp.runCount ?? 0) + 1;
|
|
176
|
+
fp.lastRunAt = new Date().toISOString();
|
|
177
|
+
fs.writeFileSync(fpPath, JSON.stringify(fp, null, 2));
|
|
178
|
+
}
|
|
@@ -31,6 +31,18 @@ import fs from "node:fs";
|
|
|
31
31
|
import path from "node:path";
|
|
32
32
|
import { fileURLToPath } from "node:url";
|
|
33
33
|
import { BrowserEngine } from "./lib/browser-engine.js";
|
|
34
|
+
// PR #1: 配置加载 + 脱敏
|
|
35
|
+
import { loadConfig } from "./lib/config-loader.js";
|
|
36
|
+
import { redact } from "./lib/redact.js";
|
|
37
|
+
// PR #2: workspace + multi-env + storageState + globalSetup + projects
|
|
38
|
+
import { ensureWorkspace, initForce, bumpRunCount } from "./lib/workspace-init.js";
|
|
39
|
+
import { applyStorageState, buildCookieHeader } from "./lib/storage-state.js";
|
|
40
|
+
import { globalSetupTakeover, globalSetupFillForm } from "./lib/global-setup.js";
|
|
41
|
+
import { applyProject, listProjects } from "./lib/projects.js";
|
|
42
|
+
// PR #3: v4 Playwright 风格 HTML 报告
|
|
43
|
+
import { generateRunHtml, appendRunCardToIndex, initIndexHtml } from "./lib/html-reporter.js";
|
|
44
|
+
import { appendToTrend } from "./lib/trend-aggregator.js";
|
|
45
|
+
import { applyRetain, archiveFailedRun } from "./lib/report-retain.js";
|
|
34
46
|
|
|
35
47
|
const __filename = fileURLToPath(import.meta.url);
|
|
36
48
|
const __dirname = path.dirname(__filename);
|
|
@@ -52,6 +64,71 @@ function parseArgs() {
|
|
|
52
64
|
case "--plan-file":
|
|
53
65
|
parsed.planFile = args[++i];
|
|
54
66
|
break;
|
|
67
|
+
// ★ PR #1 v3: 配置外置
|
|
68
|
+
case "--config":
|
|
69
|
+
parsed.config = args[++i];
|
|
70
|
+
break;
|
|
71
|
+
case "--env":
|
|
72
|
+
parsed.env = args[++i];
|
|
73
|
+
break;
|
|
74
|
+
// ★ PR #2 v3: workspace + multi-env + storageState + globalSetup
|
|
75
|
+
case "--workspace":
|
|
76
|
+
parsed.workspace = args[++i];
|
|
77
|
+
break;
|
|
78
|
+
case "--project":
|
|
79
|
+
parsed.project = args[++i];
|
|
80
|
+
break;
|
|
81
|
+
case "--storage-state":
|
|
82
|
+
parsed.storageState = args[++i];
|
|
83
|
+
break;
|
|
84
|
+
case "--global-setup":
|
|
85
|
+
parsed.globalSetup = true;
|
|
86
|
+
break;
|
|
87
|
+
case "--init-force":
|
|
88
|
+
parsed.initForce = true;
|
|
89
|
+
break;
|
|
90
|
+
// ★ PR #5 v3: 失败定位资料(trace / video / HAR / console)
|
|
91
|
+
case "--screenshot":
|
|
92
|
+
parsed.screenshotPolicy = args[++i] || 'always';
|
|
93
|
+
break;
|
|
94
|
+
case "--video":
|
|
95
|
+
parsed.videoPolicy = args[++i] || 'off';
|
|
96
|
+
break;
|
|
97
|
+
case "--trace":
|
|
98
|
+
parsed.tracePolicy = args[++i] || 'off';
|
|
99
|
+
break;
|
|
100
|
+
case "--record-har":
|
|
101
|
+
parsed.recordHar = true;
|
|
102
|
+
break;
|
|
103
|
+
case "--ci":
|
|
104
|
+
parsed.ci = true;
|
|
105
|
+
// --ci 一键切 CI 模式
|
|
106
|
+
if (!parsed.screenshotPolicy) parsed.screenshotPolicy = 'only-on-failure';
|
|
107
|
+
if (!parsed.videoPolicy) parsed.videoPolicy = 'retain-on-failure';
|
|
108
|
+
if (!parsed.tracePolicy) parsed.tracePolicy = 'retain-on-failure';
|
|
109
|
+
// PR #6 v3: --ci 默认 retries=2
|
|
110
|
+
if (parsed.retries === undefined) parsed.retries = 2;
|
|
111
|
+
break;
|
|
112
|
+
// ★ PR #6 v3: 重试策略(用例级 + 写操作禁重试 + flaky)
|
|
113
|
+
case "--retries":
|
|
114
|
+
parsed.retries = parseInt(args[++i], 10) || 0;
|
|
115
|
+
break;
|
|
116
|
+
case "--flaky-threshold":
|
|
117
|
+
parsed.flakyThreshold = parseInt(args[++i], 10) || 5;
|
|
118
|
+
break;
|
|
119
|
+
case "--no-retry-actions":
|
|
120
|
+
parsed.noRetryActions = true;
|
|
121
|
+
break;
|
|
122
|
+
// ★ PR #7 v3: 用例标签过滤
|
|
123
|
+
case "--tags":
|
|
124
|
+
parsed.tags = args[++i] || "";
|
|
125
|
+
break;
|
|
126
|
+
case "--exclude-tags":
|
|
127
|
+
parsed.excludeTags = args[++i] || "";
|
|
128
|
+
break;
|
|
129
|
+
case "--tag-mode":
|
|
130
|
+
parsed.tagMode = args[++i] || "any";
|
|
131
|
+
break;
|
|
55
132
|
case "--output":
|
|
56
133
|
parsed.output = args[++i];
|
|
57
134
|
break;
|
|
@@ -694,7 +771,8 @@ async function executeAction(args) {
|
|
|
694
771
|
if (args.testReport) {
|
|
695
772
|
const dir = path.dirname(args.testReport);
|
|
696
773
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
697
|
-
|
|
774
|
+
// ★ PR #1 v3: 落盘前 redact
|
|
775
|
+
fs.writeFileSync(args.testReport, JSON.stringify(redact(result), null, 2), "utf8");
|
|
698
776
|
process.stderr.write(`测试报告已写入: ${args.testReport}\n`);
|
|
699
777
|
}
|
|
700
778
|
|
|
@@ -894,6 +972,91 @@ async function executeAction(args) {
|
|
|
894
972
|
async function main() {
|
|
895
973
|
const args = parseArgs();
|
|
896
974
|
|
|
975
|
+
// ★ PR #1 v3: 加载配置(必须在 args 解析后立即做)
|
|
976
|
+
let config = null;
|
|
977
|
+
try {
|
|
978
|
+
config = loadConfig(args);
|
|
979
|
+
// 注入到 args,供后续 action 使用
|
|
980
|
+
args.config = config;
|
|
981
|
+
// 兼容 baseURL 字段
|
|
982
|
+
if (!args.baseUrl && config.app?.baseURL) {
|
|
983
|
+
args.baseUrl = config.app.baseURL;
|
|
984
|
+
}
|
|
985
|
+
if (config.auth?.type) args.authType = config.auth.type;
|
|
986
|
+
} catch (err) {
|
|
987
|
+
// 配置加载失败:若用户显式传 --config/--env 则报错,否则用默认
|
|
988
|
+
if (args.config || args.env) {
|
|
989
|
+
process.stderr.write(`❌ 配置加载失败: ${err.message}\n`);
|
|
990
|
+
process.exit(2);
|
|
991
|
+
}
|
|
992
|
+
// 隐式加载 + 没指定 → 用默认(不报错)
|
|
993
|
+
process.stderr.write(`ℹ️ 使用默认配置(${err.message})\n`);
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// ★ PR #2 v3: 加载配置后立即 apply project(多环境)
|
|
997
|
+
if (config && args.project) {
|
|
998
|
+
try {
|
|
999
|
+
const updated = applyProject(config, args.project);
|
|
1000
|
+
args.config = updated;
|
|
1001
|
+
if (updated.app?.baseURL && !args.baseUrl) args.baseUrl = updated.app.baseURL;
|
|
1002
|
+
if (updated.auth?.token) args.authToken = updated.auth.token;
|
|
1003
|
+
if (updated.auth?.type) args.authType = updated.auth.type;
|
|
1004
|
+
} catch (err) {
|
|
1005
|
+
process.stderr.write(`❌ ${err.message}\n`);
|
|
1006
|
+
process.exit(2);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
// ★ PR #2 v3: workspace 初始化(仅显式 --workspace 或 --init-force 时)
|
|
1011
|
+
if (args.workspace || args.initForce) {
|
|
1012
|
+
const SKILL_DIR = __dirname;
|
|
1013
|
+
if (args.initForce) {
|
|
1014
|
+
await initForce(SKILL_DIR, args.workspace || null);
|
|
1015
|
+
} else {
|
|
1016
|
+
ensureWorkspace(SKILL_DIR, args.workspace || null);
|
|
1017
|
+
}
|
|
1018
|
+
args.workspace = args.workspace || path.join(process.cwd(), ".lbt");
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// ★ PR #2 v3: globalSetup 模式(不走 testCase,单独导出 storageState)
|
|
1022
|
+
if (args.globalSetup) {
|
|
1023
|
+
try {
|
|
1024
|
+
// 启动浏览器
|
|
1025
|
+
const engine = new BrowserEngine();
|
|
1026
|
+
const initResult = await engine.initialize({ cdpUrl: args.cdpUrl });
|
|
1027
|
+
if (initResult?.error) {
|
|
1028
|
+
process.stderr.write(`❌ 浏览器初始化失败: ${initResult.error}\n`);
|
|
1029
|
+
process.exit(2);
|
|
1030
|
+
}
|
|
1031
|
+
const page = await engine.newPage();
|
|
1032
|
+
|
|
1033
|
+
const auth = args.config?.auth ?? {};
|
|
1034
|
+
const outputPath = args.storageState || path.join(
|
|
1035
|
+
args.workspace || path.join(process.cwd(), ".lbt"),
|
|
1036
|
+
"auth",
|
|
1037
|
+
`${args.project || "default"}.json`
|
|
1038
|
+
);
|
|
1039
|
+
|
|
1040
|
+
if (auth.username && auth.password) {
|
|
1041
|
+
// 模式 2:自动填表
|
|
1042
|
+
await globalSetupFillForm(page, {
|
|
1043
|
+
loginUrl: (args.baseUrl || "http://localhost:3000") + "/login",
|
|
1044
|
+
username: auth.username,
|
|
1045
|
+
password: auth.password,
|
|
1046
|
+
verifyCode: auth.verifyCode
|
|
1047
|
+
}, outputPath);
|
|
1048
|
+
} else {
|
|
1049
|
+
// 模式 1:接管浏览器(用户在浏览器里已登录)
|
|
1050
|
+
await globalSetupTakeover(page, outputPath);
|
|
1051
|
+
}
|
|
1052
|
+
await engine.close();
|
|
1053
|
+
process.exit(0);
|
|
1054
|
+
} catch (err) {
|
|
1055
|
+
process.stderr.write(`❌ globalSetup 失败: ${err.message}\n`);
|
|
1056
|
+
process.exit(2);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
897
1060
|
if (args.action === "assert-api" || args.action === "assertApi") {
|
|
898
1061
|
try {
|
|
899
1062
|
const results = executeAssertApiAction(args);
|
|
@@ -939,6 +1102,9 @@ async function main() {
|
|
|
939
1102
|
// 输出结果
|
|
940
1103
|
outputResult(results, args.output);
|
|
941
1104
|
|
|
1105
|
+
// ★ PR #3 v3: 生成 v4 报告(仅 workspace 模式启用)
|
|
1106
|
+
v4ReportIfEnabled(results, args);
|
|
1107
|
+
|
|
942
1108
|
// 输出摘要
|
|
943
1109
|
process.stderr.write(`\n执行完成:\n`);
|
|
944
1110
|
process.stderr.write(` 模式: ${results.mode || "failed"}\n`);
|
|
@@ -960,7 +1126,9 @@ async function main() {
|
|
|
960
1126
|
}
|
|
961
1127
|
|
|
962
1128
|
function outputResult(results, outputPath) {
|
|
963
|
-
|
|
1129
|
+
// ★ PR #1 v3: 落盘前必走 redact(password / verifyCode / credentials / token 等)
|
|
1130
|
+
const safe = redact(results);
|
|
1131
|
+
const json = JSON.stringify(safe, null, 2);
|
|
964
1132
|
|
|
965
1133
|
if (outputPath) {
|
|
966
1134
|
const dir = path.dirname(outputPath);
|
|
@@ -974,6 +1142,62 @@ function outputResult(results, outputPath) {
|
|
|
974
1142
|
}
|
|
975
1143
|
}
|
|
976
1144
|
|
|
1145
|
+
/**
|
|
1146
|
+
* ★ PR #3 v3: 生成 v4 Playwright 风格 HTML 报告
|
|
1147
|
+
* - 仅在 args.workspace 存在时启用
|
|
1148
|
+
* - 生成 runs/<id>/index.html + trend.json + 入口 index.html + retain
|
|
1149
|
+
* - 失败归档到 runs-failed/
|
|
1150
|
+
*/
|
|
1151
|
+
function v4ReportIfEnabled(results, args) {
|
|
1152
|
+
if (!args.workspace) return; // 仅 workspace 模式启用
|
|
1153
|
+
try {
|
|
1154
|
+
const reportsDir = path.join(args.workspace, "reports");
|
|
1155
|
+
const runId = `${(results.startedAt ?? new Date().toISOString()).replace(/[:.]/g, "-")}-pid${process.pid}`;
|
|
1156
|
+
const runDir = path.join(reportsDir, "runs", runId);
|
|
1157
|
+
fs.mkdirSync(runDir, { recursive: true });
|
|
1158
|
+
|
|
1159
|
+
// 1. 补 runId
|
|
1160
|
+
results.runId = runId;
|
|
1161
|
+
|
|
1162
|
+
// 2. 写 result.json(redact 后)
|
|
1163
|
+
fs.writeFileSync(
|
|
1164
|
+
path.join(runDir, "result.json"),
|
|
1165
|
+
JSON.stringify(redact(results), null, 2)
|
|
1166
|
+
);
|
|
1167
|
+
|
|
1168
|
+
// 3. 生成单 run HTML
|
|
1169
|
+
generateRunHtml(results, runDir);
|
|
1170
|
+
|
|
1171
|
+
// 4. 初始化入口 + 追加 run card(仅首次)
|
|
1172
|
+
initIndexHtml(reportsDir);
|
|
1173
|
+
appendRunCardToIndex(results, path.join(reportsDir, "index.html"));
|
|
1174
|
+
|
|
1175
|
+
// 5. 写 trend.json
|
|
1176
|
+
appendToTrend(results, path.join(reportsDir, "trend.json"));
|
|
1177
|
+
|
|
1178
|
+
// 6. 失败归档
|
|
1179
|
+
if ((results.stats?.failed ?? 0) > 0) {
|
|
1180
|
+
archiveFailedRun(runId, runDir, path.join(reportsDir, "runs-failed"));
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// 7. retain 策略
|
|
1184
|
+
const reportCfg = args.config?.report ?? {};
|
|
1185
|
+
const retainResult = applyRetain(reportsDir, {
|
|
1186
|
+
retainRuns: reportCfg.retainRuns ?? 30,
|
|
1187
|
+
retainRunsByDay: reportCfg.retainDays ?? 7,
|
|
1188
|
+
retainFailedForever: reportCfg.retainFailedForever ?? true
|
|
1189
|
+
});
|
|
1190
|
+
|
|
1191
|
+
// 8. bumpRunCount
|
|
1192
|
+
bumpRunCount(args.workspace);
|
|
1193
|
+
|
|
1194
|
+
process.stderr.write(`✓ v4 报告: ${reportsDir}/runs/${runId}/index.html\n`);
|
|
1195
|
+
process.stderr.write(` (保留 ${retainResult.kept} / 移除 ${retainResult.removed} / 总 ${retainResult.total})\n`);
|
|
1196
|
+
} catch (err) {
|
|
1197
|
+
process.stderr.write(`⚠️ v4 报告生成失败: ${err.message}\n`);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
977
1201
|
main().catch((err) => {
|
|
978
1202
|
process.stderr.write(`Fatal error: ${err.message}\n`);
|
|
979
1203
|
process.exit(2);
|