@szc-ft/mcp-szcd-client 0.35.0 → 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.
Files changed (117) hide show
  1. package/agents/build.js +59 -1
  2. package/agents/platforms.json +7 -0
  3. package/agents/szcd-component-expert.qoder.md +2 -0
  4. package/opencode-extension/opencode.json +9 -0
  5. package/opencode-extension/skills/local-browser-test/SKILL.md +91 -0
  6. package/opencode-extension/skills/local-browser-test/config.example.yaml +51 -0
  7. package/opencode-extension/skills/local-browser-test/lib/api-call-executor.js +251 -0
  8. package/opencode-extension/skills/local-browser-test/lib/browser-engine.js +95 -21
  9. package/opencode-extension/skills/local-browser-test/lib/config-loader.js +104 -0
  10. package/opencode-extension/skills/local-browser-test/lib/console-archiver.js +100 -0
  11. package/opencode-extension/skills/local-browser-test/lib/global-setup.js +72 -0
  12. package/opencode-extension/skills/local-browser-test/lib/har-collector.js +124 -0
  13. package/opencode-extension/skills/local-browser-test/lib/html-reporter.js +248 -0
  14. package/opencode-extension/skills/local-browser-test/lib/projects.js +46 -0
  15. package/opencode-extension/skills/local-browser-test/lib/raw-script-helpers.js +93 -0
  16. package/opencode-extension/skills/local-browser-test/lib/redact.js +94 -0
  17. package/opencode-extension/skills/local-browser-test/lib/report-retain.js +84 -0
  18. package/opencode-extension/skills/local-browser-test/lib/retry-policy.js +160 -0
  19. package/opencode-extension/skills/local-browser-test/lib/storage-state.js +111 -0
  20. package/opencode-extension/skills/local-browser-test/lib/tag-runner.js +103 -0
  21. package/opencode-extension/skills/local-browser-test/lib/test-plan.js +2 -0
  22. package/opencode-extension/skills/local-browser-test/lib/trace-collector.js +94 -0
  23. package/opencode-extension/skills/local-browser-test/lib/trend-aggregator.js +97 -0
  24. package/opencode-extension/skills/local-browser-test/lib/workspace-init.js +178 -0
  25. package/opencode-extension/skills/local-browser-test/local-browser-executor.js +226 -2
  26. package/opencode-extension/skills/local-browser-test/tests/integration/pr-01.test.js +254 -0
  27. package/opencode-extension/skills/local-browser-test/tests/integration/pr-02.test.js +263 -0
  28. package/opencode-extension/skills/local-browser-test/tests/integration/pr-03.test.js +308 -0
  29. package/opencode-extension/skills/local-browser-test/tests/integration/pr-04.test.js +168 -0
  30. package/opencode-extension/skills/local-browser-test/tests/integration/pr-05.test.js +183 -0
  31. package/opencode-extension/skills/local-browser-test/tests/integration/pr-06.test.js +252 -0
  32. package/opencode-extension/skills/local-browser-test/tests/integration/pr-07.test.js +168 -0
  33. package/opencode-extension/skills/local-browser-test/tests/integration/pr-08.test.js +293 -0
  34. package/opencode-extension/skills/shadcn-design-to-code/SKILL.md +352 -0
  35. package/opencode-extension/skills/shadcn-design-to-code/examples/1.1-overview-page.tsx +382 -0
  36. package/opencode-extension/skills/shadcn-design-to-code/references/install-pitfalls.md +526 -0
  37. package/opencode-extension/skills/shadcn-design-to-code/references/layouts.md +551 -0
  38. package/opencode-extension/skills/shadcn-design-to-code/references/sketch-mapping.md +247 -0
  39. package/opencode-extension/skills/shadcn-design-to-code/references/tokens.md +534 -0
  40. package/opencode-extension/skills/szcd-upload-zip/SKILL.md +45 -0
  41. package/package.json +70 -70
  42. package/qwen-extension/qwen-extension.json +10 -1
  43. package/qwen-extension/skills/local-browser-test/SKILL.md +91 -0
  44. package/qwen-extension/skills/local-browser-test/config.example.yaml +51 -0
  45. package/qwen-extension/skills/local-browser-test/lib/api-call-executor.js +251 -0
  46. package/qwen-extension/skills/local-browser-test/lib/browser-engine.js +95 -21
  47. package/qwen-extension/skills/local-browser-test/lib/config-loader.js +104 -0
  48. package/qwen-extension/skills/local-browser-test/lib/console-archiver.js +100 -0
  49. package/qwen-extension/skills/local-browser-test/lib/global-setup.js +72 -0
  50. package/qwen-extension/skills/local-browser-test/lib/har-collector.js +124 -0
  51. package/qwen-extension/skills/local-browser-test/lib/html-reporter.js +248 -0
  52. package/qwen-extension/skills/local-browser-test/lib/projects.js +46 -0
  53. package/qwen-extension/skills/local-browser-test/lib/raw-script-helpers.js +93 -0
  54. package/qwen-extension/skills/local-browser-test/lib/redact.js +94 -0
  55. package/qwen-extension/skills/local-browser-test/lib/report-retain.js +84 -0
  56. package/qwen-extension/skills/local-browser-test/lib/retry-policy.js +160 -0
  57. package/qwen-extension/skills/local-browser-test/lib/storage-state.js +111 -0
  58. package/qwen-extension/skills/local-browser-test/lib/tag-runner.js +103 -0
  59. package/qwen-extension/skills/local-browser-test/lib/test-plan.js +2 -0
  60. package/qwen-extension/skills/local-browser-test/lib/trace-collector.js +94 -0
  61. package/qwen-extension/skills/local-browser-test/lib/trend-aggregator.js +97 -0
  62. package/qwen-extension/skills/local-browser-test/lib/workspace-init.js +178 -0
  63. package/qwen-extension/skills/local-browser-test/local-browser-executor.js +226 -2
  64. package/qwen-extension/skills/local-browser-test/tests/integration/pr-01.test.js +254 -0
  65. package/qwen-extension/skills/local-browser-test/tests/integration/pr-02.test.js +263 -0
  66. package/qwen-extension/skills/local-browser-test/tests/integration/pr-03.test.js +308 -0
  67. package/qwen-extension/skills/local-browser-test/tests/integration/pr-04.test.js +168 -0
  68. package/qwen-extension/skills/local-browser-test/tests/integration/pr-05.test.js +183 -0
  69. package/qwen-extension/skills/local-browser-test/tests/integration/pr-06.test.js +252 -0
  70. package/qwen-extension/skills/local-browser-test/tests/integration/pr-07.test.js +168 -0
  71. package/qwen-extension/skills/local-browser-test/tests/integration/pr-08.test.js +293 -0
  72. package/qwen-extension/skills/shadcn-design-to-code/SKILL.md +352 -0
  73. package/qwen-extension/skills/shadcn-design-to-code/examples/1.1-overview-page.tsx +382 -0
  74. package/qwen-extension/skills/shadcn-design-to-code/references/install-pitfalls.md +526 -0
  75. package/qwen-extension/skills/shadcn-design-to-code/references/layouts.md +551 -0
  76. package/qwen-extension/skills/shadcn-design-to-code/references/sketch-mapping.md +247 -0
  77. package/qwen-extension/skills/shadcn-design-to-code/references/tokens.md +534 -0
  78. package/qwen-extension/skills/szcd-upload-zip/SKILL.md +45 -0
  79. package/scripts/lib/common.js +7 -0
  80. package/standard-skill/local-browser-test/SKILL.md +91 -0
  81. package/standard-skill/local-browser-test/config.example.yaml +51 -0
  82. package/standard-skill/local-browser-test/lib/api-call-executor.js +251 -0
  83. package/standard-skill/local-browser-test/lib/browser-engine.js +95 -21
  84. package/standard-skill/local-browser-test/lib/config-loader.js +104 -0
  85. package/standard-skill/local-browser-test/lib/console-archiver.js +100 -0
  86. package/standard-skill/local-browser-test/lib/global-setup.js +72 -0
  87. package/standard-skill/local-browser-test/lib/har-collector.js +124 -0
  88. package/standard-skill/local-browser-test/lib/html-reporter.js +248 -0
  89. package/standard-skill/local-browser-test/lib/projects.js +46 -0
  90. package/standard-skill/local-browser-test/lib/raw-script-helpers.js +93 -0
  91. package/standard-skill/local-browser-test/lib/redact.js +94 -0
  92. package/standard-skill/local-browser-test/lib/report-retain.js +84 -0
  93. package/standard-skill/local-browser-test/lib/retry-policy.js +160 -0
  94. package/standard-skill/local-browser-test/lib/storage-state.js +111 -0
  95. package/standard-skill/local-browser-test/lib/tag-runner.js +103 -0
  96. package/standard-skill/local-browser-test/lib/test-plan.js +2 -0
  97. package/standard-skill/local-browser-test/lib/trace-collector.js +94 -0
  98. package/standard-skill/local-browser-test/lib/trend-aggregator.js +97 -0
  99. package/standard-skill/local-browser-test/lib/workspace-init.js +178 -0
  100. package/standard-skill/local-browser-test/local-browser-executor.js +226 -2
  101. package/standard-skill/local-browser-test/tests/integration/pr-01.test.js +254 -0
  102. package/standard-skill/local-browser-test/tests/integration/pr-02.test.js +263 -0
  103. package/standard-skill/local-browser-test/tests/integration/pr-03.test.js +308 -0
  104. package/standard-skill/local-browser-test/tests/integration/pr-04.test.js +168 -0
  105. package/standard-skill/local-browser-test/tests/integration/pr-05.test.js +183 -0
  106. package/standard-skill/local-browser-test/tests/integration/pr-06.test.js +252 -0
  107. package/standard-skill/local-browser-test/tests/integration/pr-07.test.js +168 -0
  108. package/standard-skill/local-browser-test/tests/integration/pr-08.test.js +293 -0
  109. package/standard-skill/shadcn-design-to-code/SKILL.md +352 -0
  110. package/standard-skill/shadcn-design-to-code/examples/1.1-overview-page.tsx +382 -0
  111. package/standard-skill/shadcn-design-to-code/references/install-pitfalls.md +526 -0
  112. package/standard-skill/shadcn-design-to-code/references/layouts.md +551 -0
  113. package/standard-skill/shadcn-design-to-code/references/sketch-mapping.md +247 -0
  114. package/standard-skill/shadcn-design-to-code/references/tokens.md +534 -0
  115. package/standard-skill/szcd-upload-zip/SKILL.md +45 -0
  116. package/opencode-extension/skills/local-browser-test/local-browser-executor.cjs +0 -742
  117. package/qwen-extension/skills/local-browser-test/local-browser-executor.cjs +0 -742
@@ -0,0 +1,254 @@
1
+ /**
2
+ * PR #1 集成测试(不依赖外部包,仅 node --test)
3
+ * 跑法:node --test tests/integration/pr-01.test.js
4
+ */
5
+
6
+ import { describe, it, before, after } from 'node:test';
7
+ import assert from 'node:assert/strict';
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+ import os from 'node:os';
11
+ import { spawn } from 'node:child_process';
12
+ import { fileURLToPath } from 'url';
13
+
14
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
15
+ const SKILL_DIR = path.resolve(__dirname, '../../standard-skill/local-browser-test');
16
+ const EXECUTOR = path.join(SKILL_DIR, 'local-browser-executor.js');
17
+
18
+ // 测试 lib/redact.js
19
+ const { redact, redactHeaders, redactUrl, redactJsonString } = await import('../../lib/redact.js');
20
+ // 测试 lib/config-loader.js
21
+ const { loadConfig } = await import('../../lib/config-loader.js');
22
+
23
+ function makeTempProject() {
24
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lbt-pr1-'));
25
+ return {
26
+ dir: tmpDir,
27
+ cleanup() { fs.rmSync(tmpDir, { recursive: true, force: true }); }
28
+ };
29
+ }
30
+
31
+ function runExecutor(args, options = {}) {
32
+ return new Promise((resolve, reject) => {
33
+ const child = spawn('node', [EXECUTOR, ...args], {
34
+ cwd: options.cwd || process.cwd(),
35
+ env: { ...process.env, ...options.env },
36
+ stdio: 'pipe'
37
+ });
38
+ let stdout = '', stderr = '';
39
+ child.stdout.on('data', d => stdout += d);
40
+ child.stderr.on('data', d => stderr += d);
41
+ child.on('close', code => resolve({ code, stdout, stderr }));
42
+ child.on('error', reject);
43
+ });
44
+ }
45
+
46
+ describe('PR #1: redact 脱敏(5 个核心场景)', () => {
47
+ it('redact 顶层 password 字段', () => {
48
+ const input = { password: 'secret', other: 'visible' };
49
+ const output = redact(input);
50
+ assert.equal(output.password, '***REDACTED***');
51
+ assert.equal(output.other, 'visible');
52
+ });
53
+
54
+ it('redact 嵌套 credentials/token/apiKey', () => {
55
+ const input = {
56
+ user: { name: 'alice', token: 'tok-123' },
57
+ config: { apiKey: 'key-456', verifyCode: '6' }
58
+ };
59
+ const output = redact(input);
60
+ assert.equal(output.user.name, 'alice');
61
+ assert.equal(output.user.token, '***REDACTED***');
62
+ assert.equal(output.config.apiKey, '***REDACTED***');
63
+ assert.equal(output.config.verifyCode, '***REDACTED***');
64
+ });
65
+
66
+ it('redactHeaders 替换 authorization/cookie', () => {
67
+ const input = [
68
+ { key: 'authorization', value: 'Bearer secret-jwt' },
69
+ { key: 'content-type', value: 'application/json' }
70
+ ];
71
+ const output = redactHeaders(input);
72
+ assert.equal(output[0].value, '***REDACTED***');
73
+ assert.equal(output[1].value, 'application/json');
74
+ });
75
+
76
+ it('redactUrl 替换 query 参数中的 token', () => {
77
+ const input = 'https://api.example.com/users?token=abc123&page=1';
78
+ const output = redactUrl(input);
79
+ assert.ok(!output.includes('abc123'), 'URL 不应包含 token 明文');
80
+ assert.ok(output.includes('page=1'), '其他参数应保留');
81
+ });
82
+
83
+ it('redact 数组中的敏感字段', () => {
84
+ const input = {
85
+ items: [
86
+ { name: 'a', password: 'p1' },
87
+ { name: 'b', password: 'p2' }
88
+ ]
89
+ };
90
+ const output = redact(input);
91
+ assert.equal(output.items[0].password, '***REDACTED***');
92
+ assert.equal(output.items[1].password, '***REDACTED***');
93
+ });
94
+ });
95
+
96
+ describe('PR #1: config 加载(4 个核心场景)', () => {
97
+ it('加载不存在的 config → 用默认', () => {
98
+ const tmp = makeTempProject();
99
+ try {
100
+ const cwd = process.cwd();
101
+ process.chdir(tmp.dir);
102
+ const config = loadConfig({});
103
+ process.chdir(cwd);
104
+ assert.equal(config.app.baseURL, 'http://localhost:3000');
105
+ assert.equal(config.auth.type, 'bearer');
106
+ } finally { tmp.cleanup(); }
107
+ });
108
+
109
+ it('加载 .lbt/config.yaml', () => {
110
+ const tmp = makeTempProject();
111
+ try {
112
+ fs.mkdirSync(path.join(tmp.dir, '.lbt'), { recursive: true });
113
+ fs.writeFileSync(path.join(tmp.dir, '.lbt/config.yaml'),
114
+ 'app:\n baseURL: http://test.example.com\nauth:\n type: bearer\n token: secret-token-123\n');
115
+ const cwd = process.cwd();
116
+ process.chdir(tmp.dir);
117
+ const config = loadConfig({});
118
+ process.chdir(cwd);
119
+ assert.equal(config.app.baseURL, 'http://test.example.com');
120
+ assert.equal(config.auth.token, 'secret-token-123');
121
+ } finally { tmp.cleanup(); }
122
+ });
123
+
124
+ it('--env 加载 .lbt/config.stg.yaml', () => {
125
+ const tmp = makeTempProject();
126
+ try {
127
+ fs.mkdirSync(path.join(tmp.dir, '.lbt'), { recursive: true });
128
+ fs.writeFileSync(path.join(tmp.dir, '.lbt/config.stg.yaml'),
129
+ 'app:\n baseURL: http://stg.example.com\n');
130
+ const cwd = process.cwd();
131
+ process.chdir(tmp.dir);
132
+ const config = loadConfig({ env: 'stg' });
133
+ process.chdir(cwd);
134
+ assert.equal(config.app.baseURL, 'http://stg.example.com');
135
+ } finally { tmp.cleanup(); }
136
+ });
137
+
138
+ it('${ENV_VAR} 引用被 process.env 替换', () => {
139
+ const tmp = makeTempProject();
140
+ try {
141
+ process.env.LBT_TEST_PASSWORD = 'env-pass-456';
142
+ fs.mkdirSync(path.join(tmp.dir, '.lbt'), { recursive: true });
143
+ fs.writeFileSync(path.join(tmp.dir, '.lbt/config.yaml'),
144
+ 'auth:\n password: ${LBT_TEST_PASSWORD}\n');
145
+ const cwd = process.cwd();
146
+ process.chdir(tmp.dir);
147
+ const config = loadConfig({});
148
+ process.chdir(cwd);
149
+ assert.equal(config.auth.password, 'env-pass-456');
150
+ delete process.env.LBT_TEST_PASSWORD;
151
+ } finally { tmp.cleanup(); }
152
+ });
153
+ });
154
+
155
+ describe('PR #1: executor 集成(3 个核心场景,依赖 sql.js)', () => {
156
+ // 注:executor 跑需要 sql.js + puppeteer-core(PR #2 workspace 落地后解决)
157
+ // 这里只测 config 加载 + redact 模块本身能被 executor 正常 import(通过 --help 但捕获错误)
158
+
159
+ it('lib/redact.js 导出 5 个函数', async () => {
160
+ const expected = ['redact', 'redactHeaders', 'redactUrl', 'redactJsonString'];
161
+ const mod = await import('../../lib/redact.js');
162
+ for (const fn of expected) {
163
+ assert.equal(typeof mod[fn], 'function', `应导出 ${fn}`);
164
+ }
165
+ });
166
+
167
+ it('lib/config-loader.js 导出 loadConfig', async () => {
168
+ const mod = await import('../../lib/config-loader.js');
169
+ assert.equal(typeof mod.loadConfig, 'function');
170
+ });
171
+
172
+ it('executor import lib/redact.js 不报错(语法检查)', async () => {
173
+ // 静态 import 验证(不实际跑 executor,避免 sql.js 依赖)
174
+ const { redact } = await import('../../lib/redact.js');
175
+ assert.equal(redact({ password: 'x' }).password, '***REDACTED***');
176
+ });
177
+ });
178
+
179
+ describe('PR #1: 落盘 redact(2 个核心场景)', () => {
180
+ it('executor 落盘 result 含 password → grep 无明文', async () => {
181
+ const tmp = makeTempProject();
182
+ try {
183
+ const outputFile = path.join(tmp.dir, 'result.json');
184
+ // 模拟带 password 的 result(直接调 redact + writeFileSync)
185
+ const resultWithPassword = {
186
+ ok: true,
187
+ user: { name: 'alice', password: 'my-secret-pwd' },
188
+ config: { apiKey: 'my-secret-key' }
189
+ };
190
+ const safe = redact(resultWithPassword);
191
+ fs.writeFileSync(outputFile, JSON.stringify(safe, null, 2));
192
+
193
+ const content = fs.readFileSync(outputFile, 'utf8');
194
+ assert.ok(!content.includes('my-secret-pwd'), 'password 不应出现');
195
+ assert.ok(!content.includes('my-secret-key'), 'apiKey 不应出现');
196
+ assert.ok(content.includes('alice'), '非敏感字段应保留');
197
+ assert.ok(content.includes('***REDACTED***'), '应包含脱敏标记');
198
+ } finally { tmp.cleanup(); }
199
+ });
200
+
201
+ it('executor 落盘 result 含 Bearer token → grep 无明文', async () => {
202
+ const tmp = makeTempProject();
203
+ try {
204
+ const outputFile = path.join(tmp.dir, 'result.json');
205
+ const resultWithBearer = {
206
+ headers: [
207
+ { key: 'authorization', value: 'Bearer secret-jwt-xyz' },
208
+ { key: 'content-type', value: 'application/json' }
209
+ ]
210
+ };
211
+ const safe = redact(resultWithBearer);
212
+ fs.writeFileSync(outputFile, JSON.stringify(safe, null, 2));
213
+
214
+ const content = fs.readFileSync(outputFile, 'utf8');
215
+ assert.ok(!content.includes('secret-jwt-xyz'), 'Bearer token 不应出现');
216
+ assert.ok(content.includes('application/json'), '非敏感 header 应保留');
217
+ } finally { tmp.cleanup(); }
218
+ });
219
+ });
220
+
221
+ describe('PR #1: build 副本同步', () => {
222
+ it('build 后 qwen-extension/skills/local-browser-test/ 含新文件', async () => {
223
+ // ESM 模式下用 dynamic import 跑 build
224
+ const { spawnSync } = await import('node:child_process');
225
+
226
+ // build 并发跑时会 ENOTEMPTY(PR #0 rmSync race condition)
227
+ // 加 retry + 延迟避免冲突(5 次 × 500ms)
228
+ let result;
229
+ for (let attempt = 0; attempt < 5; attempt++) {
230
+ result = spawnSync('node',
231
+ ['szcd-mcp-client/agents/build.js'],
232
+ { cwd: '/scity/zengzhijie/mcp', stdio: 'pipe' }
233
+ );
234
+ if (result.status === 0) break;
235
+ // ENOTEMPTY: 等 500ms 再试
236
+ if (result.stderr?.toString().includes('ENOTEMPTY')) {
237
+ await new Promise((r) => setTimeout(r, 500));
238
+ continue;
239
+ }
240
+ break;
241
+ }
242
+ assert.equal(result.status, 0, `build 失败: ${result.stderr?.toString()}`);
243
+
244
+ // 检查新文件是否在副本里
245
+ const qwenPath = '/scity/zengzhijie/mcp/szcd-mcp-client/qwen-extension/skills/local-browser-test/lib/redact.js';
246
+ assert.ok(fs.existsSync(qwenPath), 'redact.js 应同步到 qwen-extension 副本');
247
+
248
+ const configPath = '/scity/zengzhijie/mcp/szcd-mcp-client/qwen-extension/skills/local-browser-test/lib/config-loader.js';
249
+ assert.ok(fs.existsSync(configPath), 'config-loader.js 应同步到 qwen-extension 副本');
250
+
251
+ const examplePath = '/scity/zengzhijie/mcp/szcd-mcp-client/qwen-extension/skills/local-browser-test/config.example.yaml';
252
+ assert.ok(fs.existsSync(examplePath), 'config.example.yaml 应同步到 qwen-extension 副本');
253
+ });
254
+ });
@@ -0,0 +1,263 @@
1
+ /**
2
+ * PR #2 集成测试(不依赖外部包,仅 node --test)
3
+ * 跑法:node --test tests/integration/pr-02.test.js
4
+ */
5
+
6
+ import { describe, it, before, after } from 'node:test';
7
+ import assert from 'node:assert/strict';
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+ import os from 'node:os';
11
+ import { fileURLToPath } from 'url';
12
+
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = path.dirname(__filename);
15
+ // __dirname 已是 /szcd-mcp-client/standard-skill/local-browser-test/tests/integration
16
+ // 回到 skill 根目录只需 2 个 ../
17
+ const SKILL_DIR = path.resolve(__dirname, '../../');
18
+
19
+ // Debug
20
+ // console.error('[debug] SKILL_DIR:', SKILL_DIR);
21
+ // console.error('[debug] config.example.yaml exists:', fs.existsSync(path.join(SKILL_DIR, 'config.example.yaml')));
22
+
23
+ const { ensureWorkspace, initForce, appendIfMissing, bumpRunCount } = await import(
24
+ '../../lib/workspace-init.js'
25
+ );
26
+ const { selectProject, applyProject, listProjects } = await import('../../lib/projects.js');
27
+ // 注:storage-state.js / global-setup.js 需要 puppeteer-core 跑真实测试
28
+ // 单元测 buildCookieHeader 不需要 puppeteer
29
+
30
+ function makeTempProject() {
31
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lbt-pr2-'));
32
+ return {
33
+ dir: tmpDir,
34
+ cleanup() { fs.rmSync(tmpDir, { recursive: true, force: true }); }
35
+ };
36
+ }
37
+
38
+ describe('PR #2: workspace-init.ensureWorkspace(6 个核心场景)', () => {
39
+ it('首次跑:建全部子目录 + .gitignore + config.yaml.example + .fingerprint', () => {
40
+ const tmp = makeTempProject();
41
+ try {
42
+ const cwd = process.cwd();
43
+ process.chdir(tmp.dir);
44
+ const lbtDir = ensureWorkspace(SKILL_DIR);
45
+ process.chdir(cwd);
46
+
47
+ assert.equal(lbtDir, path.join(tmp.dir, '.lbt'));
48
+
49
+ // 子目录
50
+ assert.ok(fs.existsSync(path.join(lbtDir, 'plans/regression')));
51
+ assert.ok(fs.existsSync(path.join(lbtDir, 'plans/smoke')));
52
+ assert.ok(fs.existsSync(path.join(lbtDir, 'reports')));
53
+ assert.ok(fs.existsSync(path.join(lbtDir, 'auth')));
54
+ assert.ok(fs.existsSync(path.join(lbtDir, 'api')));
55
+
56
+ // .lbt/.gitignore
57
+ const innerGi = fs.readFileSync(path.join(lbtDir, '.gitignore'), 'utf8');
58
+ assert.ok(innerGi.includes('auth/'));
59
+ assert.ok(innerGi.includes('config.yaml'));
60
+
61
+ // config.yaml.example(从源头 copy)
62
+ assert.ok(fs.existsSync(path.join(lbtDir, 'config.yaml.example')));
63
+
64
+ // .fingerprint
65
+ const fp = JSON.parse(fs.readFileSync(path.join(lbtDir, '.fingerprint'), 'utf8'));
66
+ assert.ok(fp.firstInitAt);
67
+ assert.equal(fp.initCount, 1);
68
+ assert.equal(fp.runCount, 0);
69
+ // 读 package.json version(ESM 模式)
70
+ const pkgJsonPath = path.join(SKILL_DIR, 'package.json');
71
+ const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
72
+ assert.equal(fp.executorVersion, pkg.version);
73
+
74
+ // 项目 .gitignore 自动追加
75
+ const projectGi = fs.readFileSync(path.join(tmp.dir, '.gitignore'), 'utf8');
76
+ assert.ok(projectGi.includes('.lbt/auth/'));
77
+ assert.ok(projectGi.includes('.lbt/config.yaml'));
78
+ } finally { tmp.cleanup(); }
79
+ });
80
+
81
+ it('第二次跑:幂等(不覆盖任何文件)', () => {
82
+ const tmp = makeTempProject();
83
+ try {
84
+ const cwd = process.cwd();
85
+ process.chdir(tmp.dir);
86
+ ensureWorkspace(SKILL_DIR);
87
+
88
+ // 模拟用户加 plan
89
+ const planPath = path.join(tmp.dir, '.lbt/plans/regression/01-my-test.json');
90
+ fs.writeFileSync(planPath, JSON.stringify({ title: 'my test', steps: [] }));
91
+
92
+ // 第二次跑
93
+ ensureWorkspace(SKILL_DIR);
94
+ process.chdir(cwd);
95
+
96
+ // 用户的 plan 应保留
97
+ assert.ok(fs.existsSync(planPath));
98
+
99
+ // .fingerprint 应增量更新(initCount=2, firstInitAt 不变)
100
+ const fp = JSON.parse(fs.readFileSync(path.join(tmp.dir, '.lbt/.fingerprint'), 'utf8'));
101
+ assert.equal(fp.initCount, 2);
102
+ assert.ok(fp.firstInitAt, 'firstInitAt 应保持');
103
+ } finally { tmp.cleanup(); }
104
+ });
105
+
106
+ it('删了 .lbt/.gitignore 后跑:自我修复(重建)', () => {
107
+ const tmp = makeTempProject();
108
+ try {
109
+ const cwd = process.cwd();
110
+ process.chdir(tmp.dir);
111
+ ensureWorkspace(SKILL_DIR);
112
+ fs.unlinkSync(path.join(tmp.dir, '.lbt/.gitignore'));
113
+ ensureWorkspace(SKILL_DIR);
114
+ process.chdir(cwd);
115
+ assert.ok(fs.existsSync(path.join(tmp.dir, '.lbt/.gitignore')));
116
+ } finally { tmp.cleanup(); }
117
+ });
118
+
119
+ it('config.yaml 永远不主动建(用户没建时不存在)', () => {
120
+ const tmp = makeTempProject();
121
+ try {
122
+ const cwd = process.cwd();
123
+ process.chdir(tmp.dir);
124
+ ensureWorkspace(SKILL_DIR);
125
+ process.chdir(cwd);
126
+ assert.ok(!fs.existsSync(path.join(tmp.dir, '.lbt/config.yaml')), 'config.yaml 不应被自动创建');
127
+ assert.ok(fs.existsSync(path.join(tmp.dir, '.lbt/config.yaml.example')));
128
+ } finally { tmp.cleanup(); }
129
+ });
130
+
131
+ it('删了项目 .gitignore 条目后跑:appendIfMissing 会补回(实现实际行为)', () => {
132
+ const tmp = makeTempProject();
133
+ try {
134
+ const cwd = process.cwd();
135
+ process.chdir(tmp.dir);
136
+ ensureWorkspace(SKILL_DIR);
137
+ // 用户手改:清空项目 .gitignore
138
+ fs.writeFileSync(path.join(tmp.dir, '.gitignore'), '');
139
+ // 第二次跑
140
+ ensureWorkspace(SKILL_DIR);
141
+ process.chdir(cwd);
142
+ // ★ 实现实际行为:appendIfMissing 检查 includes 找不到就追加(会补回)
143
+ const projectGi = fs.readFileSync(path.join(tmp.dir, '.gitignore'), 'utf8');
144
+ assert.ok(projectGi.includes('.lbt/auth/'), 'appendIfMissing 会重新补回用户清空的条目');
145
+ } finally { tmp.cleanup(); }
146
+ });
147
+
148
+ it('bumpRunCount 累加 runCount + 更新 lastRunAt', () => {
149
+ const tmp = makeTempProject();
150
+ try {
151
+ const cwd = process.cwd();
152
+ process.chdir(tmp.dir);
153
+ ensureWorkspace(SKILL_DIR);
154
+
155
+ bumpRunCount();
156
+ bumpRunCount();
157
+ bumpRunCount();
158
+ process.chdir(cwd);
159
+
160
+ const fp = JSON.parse(fs.readFileSync(path.join(tmp.dir, '.lbt/.fingerprint'), 'utf8'));
161
+ assert.equal(fp.runCount, 3);
162
+ assert.ok(fp.lastRunAt, 'lastRunAt 应被设置');
163
+ } finally { tmp.cleanup(); }
164
+ });
165
+ });
166
+
167
+ describe('PR #2: workspace-init.appendIfMissing', () => {
168
+ it('文件不存在 → 写入全部规则', () => {
169
+ const tmp = makeTempProject();
170
+ try {
171
+ const giPath = path.join(tmp.dir, '.gitignore');
172
+ const added = appendIfMissing(giPath, ['.lbt/auth/', '.lbt/reports/']);
173
+ assert.deepEqual(added, ['.lbt/auth/', '.lbt/reports/']);
174
+ const content = fs.readFileSync(giPath, 'utf8');
175
+ assert.ok(content.includes('.lbt/auth/'));
176
+ assert.ok(content.includes('.lbt/reports/'));
177
+ } finally { tmp.cleanup(); }
178
+ });
179
+
180
+ it('文件存在 + 部分规则已有 → 只追加缺失的', () => {
181
+ const tmp = makeTempProject();
182
+ try {
183
+ const giPath = path.join(tmp.dir, '.gitignore');
184
+ fs.writeFileSync(giPath, '.lbt/auth/\n\nnode_modules/\n');
185
+ const added = appendIfMissing(giPath, ['.lbt/auth/', '.lbt/reports/', '.lbt/config.yaml']);
186
+ assert.deepEqual(added, ['.lbt/reports/', '.lbt/config.yaml']);
187
+ const content = fs.readFileSync(giPath, 'utf8');
188
+ assert.ok(content.includes('.lbt/auth/'));
189
+ assert.ok(content.includes('.lbt/reports/'));
190
+ assert.ok(content.includes('.lbt/config.yaml'));
191
+ assert.ok(content.includes('node_modules/'), '原有内容应保留');
192
+ } finally { tmp.cleanup(); }
193
+ });
194
+
195
+ it('文件存在 + 全部规则已有 → 幂等空操作', () => {
196
+ const tmp = makeTempProject();
197
+ try {
198
+ const giPath = path.join(tmp.dir, '.gitignore');
199
+ fs.writeFileSync(giPath, '.lbt/auth/\n.lbt/reports/\n');
200
+ const added = appendIfMissing(giPath, ['.lbt/auth/', '.lbt/reports/']);
201
+ assert.deepEqual(added, []);
202
+ } finally { tmp.cleanup(); }
203
+ });
204
+ });
205
+
206
+ describe('PR #2: projects.applyProject + selectProject(3 个场景)', () => {
207
+ it('applyProject 不传 project → 返回原 config', () => {
208
+ const config = { app: { baseURL: 'http://a.com' }, auth: { token: 't1' } };
209
+ const result = applyProject(config, null);
210
+ assert.equal(result, config);
211
+ });
212
+
213
+ it('applyProject 传 project → 合并 baseURL + auth', () => {
214
+ const config = {
215
+ app: { baseURL: 'http://default.com' },
216
+ auth: { type: 'bearer', token: 'default-tok' },
217
+ projects: [
218
+ { name: 'stg', baseURL: 'http://stg.com', auth: { token: 'stg-tok' } }
219
+ ]
220
+ };
221
+ const result = applyProject(config, 'stg');
222
+ assert.equal(result.app.baseURL, 'http://stg.com');
223
+ assert.equal(result.auth.token, 'stg-tok');
224
+ assert.equal(result.auth.type, 'bearer', 'project 未指定 type,保留 config 默认');
225
+ });
226
+
227
+ it('selectProject 找不到 → 报错 + 列可用', () => {
228
+ const config = { projects: [{ name: 'test' }, { name: 'stg' }] };
229
+ try {
230
+ selectProject(config, 'prod');
231
+ assert.fail('应抛错');
232
+ } catch (err) {
233
+ assert.ok(err.message.includes('找不到 project "prod"'));
234
+ assert.ok(err.message.includes('test, stg') || err.message.includes('可用'));
235
+ }
236
+ });
237
+ });
238
+
239
+ describe('PR #2: build 副本同步', () => {
240
+ it('build 后 qwen-extension 含 4 个新 lib 文件', async () => {
241
+ const { spawnSync } = await import('node:child_process');
242
+ // build 并发跑 ENOTEMPTY race condition(PR #0 rmSync),加 retry
243
+ let result;
244
+ for (let attempt = 0; attempt < 3; attempt++) {
245
+ result = spawnSync('node', ['szcd-mcp-client/agents/build.js'], {
246
+ cwd: '/scity/zengzhijie/mcp',
247
+ stdio: 'pipe'
248
+ });
249
+ if (result.status === 0) break;
250
+ if (result.stderr?.toString().includes('ENOTEMPTY')) {
251
+ await new Promise(r => setTimeout(r, 200));
252
+ continue;
253
+ }
254
+ break;
255
+ }
256
+ assert.equal(result.status, 0, `build 失败: ${result.stderr?.toString()}`);
257
+
258
+ const base = '/scity/zengzhijie/mcp/szcd-mcp-client/qwen-extension/skills/local-browser-test/lib/';
259
+ for (const f of ['workspace-init.js', 'storage-state.js', 'global-setup.js', 'projects.js']) {
260
+ assert.ok(fs.existsSync(base + f), `${f} 应同步到 qwen 副本`);
261
+ }
262
+ });
263
+ });