@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,252 @@
1
+ /**
2
+ * PR #6 集成测试(重试 + flaky + 写操作禁重试)
3
+ * 跑法:node --test tests/integration/pr-06.test.js
4
+ *
5
+ * 测试范围(不依赖真实浏览器):
6
+ * - RetryPolicy.isStepRetryable: 写操作白名单 + step.noRetry
7
+ * - RetryPolicy.withStepRetry: 失败重试到上限
8
+ * - RetryPolicy.withCaseRetry: 用例级重试 + flaky 检测
9
+ * - RetryPolicy.detectFlaky: 5 次 1 挂 4 过
10
+ * - retryPolicyFromPlan: 工厂函数
11
+ * - browser-engine.js 集成 retryPolicyFromPlan
12
+ * - SKILL.md 章节
13
+ * - build 副本同步
14
+ */
15
+
16
+ import { describe, it } from 'node:test';
17
+ import assert from 'node:assert/strict';
18
+ import fs from 'node:fs';
19
+ import path from 'node:path';
20
+ import { fileURLToPath } from 'url';
21
+
22
+ const __filename = fileURLToPath(import.meta.url);
23
+ const __dirname = path.dirname(__filename);
24
+ const SKILL_DIR = path.resolve(__dirname, '../../');
25
+
26
+ const { RetryPolicy, retryPolicyFromPlan } = await import('../../lib/retry-policy.js');
27
+
28
+ describe('PR #6: RetryPolicy.isStepRetryable(4 个核心场景)', () => {
29
+ it('普通 act click → 允许重试', () => {
30
+ const p = new RetryPolicy();
31
+ assert.equal(p.isStepRetryable({ type: 'act', click: '登录' }), true);
32
+ });
33
+
34
+ it('act click "提交" → 禁重试(中文白名单)', () => {
35
+ const p = new RetryPolicy();
36
+ assert.equal(p.isStepRetryable({ type: 'act', click: '提交' }), false);
37
+ });
38
+
39
+ it('act click "delete" → 禁重试(英文白名单)', () => {
40
+ const p = new RetryPolicy();
41
+ assert.equal(p.isStepRetryable({ type: 'act', click: 'delete' }), false);
42
+ });
43
+
44
+ it('apiCall POST → 默认禁重试', () => {
45
+ const p = new RetryPolicy();
46
+ assert.equal(p.isStepRetryable({ type: 'apiCall', method: 'POST' }), false);
47
+ });
48
+
49
+ it('apiCall GET → 允许重试(GET 是读)', () => {
50
+ const p = new RetryPolicy();
51
+ assert.equal(p.isStepRetryable({ type: 'apiCall', method: 'GET' }), true);
52
+ });
53
+
54
+ it('step.noRetry: true 显式禁重试', () => {
55
+ const p = new RetryPolicy();
56
+ assert.equal(p.isStepRetryable({ type: 'act', click: '普通', noRetry: true }), false);
57
+ });
58
+
59
+ it('step.noRetry: false 显式允许(即使全局禁)', () => {
60
+ const p = new RetryPolicy({ noRetryActions: true });
61
+ assert.equal(p.isStepRetryable({ type: 'act', click: '提交', noRetry: false }), true);
62
+ });
63
+
64
+ it('noRetryActions: false 全局禁用 → act click "提交" 允许重试', () => {
65
+ const p = new RetryPolicy({ noRetryActions: false });
66
+ assert.equal(p.isStepRetryable({ type: 'act', click: '提交' }), true);
67
+ });
68
+ });
69
+
70
+ describe('PR #6: RetryPolicy.withStepRetry(3 个场景)', () => {
71
+ it('写操作禁重试 → 直接执行 1 次(不重试)', async () => {
72
+ const p = new RetryPolicy({ maxRetriesPerStep: 2 });
73
+ let calls = 0;
74
+ const result = await p.withStepRetry({ type: 'act', click: '提交' }, async () => {
75
+ calls++;
76
+ return { passed: false };
77
+ });
78
+ assert.equal(calls, 1, '写操作只执行 1 次');
79
+ assert.equal(result.retryCount, 0);
80
+ });
81
+
82
+ it('普通 act 失败 → 重试 2 次(maxRetriesPerStep=2)', async () => {
83
+ const p = new RetryPolicy({ maxRetriesPerStep: 2 });
84
+ let calls = 0;
85
+ const result = await p.withStepRetry({ type: 'act', click: '普通' }, async () => {
86
+ calls++;
87
+ return { passed: false };
88
+ });
89
+ assert.equal(calls, 3, '初次 + 2 次重试 = 3 次');
90
+ assert.equal(result.retryCount, 2);
91
+ assert.equal(result.aborted, true);
92
+ });
93
+
94
+ it('普通 act 第 2 次成功 → 立即返回(不再重试)', async () => {
95
+ const p = new RetryPolicy({ maxRetriesPerStep: 2 });
96
+ let calls = 0;
97
+ const result = await p.withStepRetry({ type: 'act', click: '普通' }, async () => {
98
+ calls++;
99
+ return { passed: calls >= 2 }; // 第 2 次返回 passed: true
100
+ });
101
+ assert.equal(calls, 2, '第 2 次成功即停');
102
+ assert.equal(result.retryCount, 1);
103
+ assert.equal(result.aborted, false);
104
+ });
105
+ });
106
+
107
+ describe('PR #6: RetryPolicy.withCaseRetry(3 个场景)', () => {
108
+ it('用例默认 0 retries → 失败立即返回', async () => {
109
+ const p = new RetryPolicy();
110
+ let calls = 0;
111
+ const result = await p.withCaseRetry('test', async () => {
112
+ calls++;
113
+ return { passed: false };
114
+ });
115
+ assert.equal(calls, 1);
116
+ assert.equal(result.retryCount, 0);
117
+ });
118
+
119
+ it('用例 retries=2 → 失败重试 2 次(合计 3 次)', async () => {
120
+ const p = new RetryPolicy({ maxRetries: 2 });
121
+ let calls = 0;
122
+ const result = await p.withCaseRetry('test', async () => {
123
+ calls++;
124
+ return { passed: false };
125
+ });
126
+ assert.equal(calls, 3);
127
+ assert.equal(result.retryCount, 2);
128
+ });
129
+
130
+ it('用例第 2 次成功 → 立即返回', async () => {
131
+ const p = new RetryPolicy({ maxRetries: 2 });
132
+ let calls = 0;
133
+ const result = await p.withCaseRetry('test', async () => {
134
+ calls++;
135
+ return { passed: calls >= 2 };
136
+ });
137
+ assert.equal(calls, 2);
138
+ assert.equal(result.retryCount, 1);
139
+ });
140
+ });
141
+
142
+ describe('PR #6: RetryPolicy.detectFlaky(5 个场景)', () => {
143
+ it('5 次 1 挂 4 过 → flaky=true', () => {
144
+ const p = new RetryPolicy();
145
+ const history = [
146
+ { run: 1, passed: true },
147
+ { run: 2, passed: true },
148
+ { run: 3, passed: false },
149
+ { run: 4, passed: true },
150
+ { run: 5, passed: true }
151
+ ];
152
+ assert.equal(p.detectFlaky(history), true);
153
+ });
154
+
155
+ it('5 次全过 → flaky=false', () => {
156
+ const p = new RetryPolicy();
157
+ const history = Array(5).fill({ passed: true });
158
+ assert.equal(p.detectFlaky(history), false);
159
+ });
160
+
161
+ it('5 次全挂 → flaky=false(不是 flaky,是真挂)', () => {
162
+ const p = new RetryPolicy();
163
+ const history = Array(5).fill({ passed: false });
164
+ assert.equal(p.detectFlaky(history), false);
165
+ });
166
+
167
+ it('3 次 1 挂 2 过 → flaky=false(不够 5 次)', () => {
168
+ const p = new RetryPolicy();
169
+ const history = [
170
+ { passed: true },
171
+ { passed: false },
172
+ { passed: true }
173
+ ];
174
+ assert.equal(p.detectFlaky(history), false);
175
+ });
176
+
177
+ it('5 次 2 挂 3 过 → flaky=false(不是 1 挂 4 过)', () => {
178
+ const p = new RetryPolicy();
179
+ const history = [
180
+ { passed: true }, { passed: true }, { passed: true },
181
+ { passed: false }, { passed: false }
182
+ ];
183
+ assert.equal(p.detectFlaky(history), false);
184
+ });
185
+ });
186
+
187
+ describe('PR #6: retryPolicyFromPlan(2 个场景)', () => {
188
+ it('空对象 → 默认值', () => {
189
+ const p = retryPolicyFromPlan({});
190
+ assert.equal(p.maxRetries, 0);
191
+ assert.equal(p.maxRetriesPerStep, 2);
192
+ assert.equal(p.flakyThreshold, 5);
193
+ assert.equal(p.noRetryActions, true);
194
+ });
195
+
196
+ it('plan.recovery 自定义 → 正确覆盖', () => {
197
+ const p = retryPolicyFromPlan({
198
+ maxRetries: 3,
199
+ maxRetriesPerStep: 1,
200
+ noRetryActions: false
201
+ });
202
+ assert.equal(p.maxRetries, 3);
203
+ assert.equal(p.maxRetriesPerStep, 1);
204
+ assert.equal(p.noRetryActions, false);
205
+ });
206
+ });
207
+
208
+ describe('PR #6: computeFidelity(2 个场景)', () => {
209
+ it('5/5 通过 + 0 retry + 0 flaky → 1.0', () => {
210
+ const p = new RetryPolicy();
211
+ const f = p.computeFidelity({ total: 5, passed: 5, retryCount: 0, flaky: 0 });
212
+ assert.equal(f, 1.0);
213
+ });
214
+
215
+ it('3/5 通过 + 2 retry → 0.4(base 0.6 - retryPenalty 0.2)', () => {
216
+ const p = new RetryPolicy();
217
+ const f = p.computeFidelity({ total: 5, passed: 3, retryCount: 2, flaky: 0 });
218
+ assert.equal(f, 0.4);
219
+ });
220
+ });
221
+
222
+ describe('PR #6: browser-engine.js 集成(1 个场景)', () => {
223
+ it('browser-engine.js 顶部 import retry-policy', () => {
224
+ const enginePath = path.join(SKILL_DIR, 'lib/browser-engine.js');
225
+ const content = fs.readFileSync(enginePath, 'utf8');
226
+ assert.ok(content.includes('from "./retry-policy.js"'), '应 import retry-policy');
227
+ assert.ok(content.includes('retryPolicyFromPlan'), '应使用 retryPolicyFromPlan');
228
+ });
229
+ });
230
+
231
+ describe('PR #6: build 副本同步', () => {
232
+ it('build 后 qwen-extension 含 retry-policy.js', async () => {
233
+ const { spawnSync } = await import('node:child_process');
234
+ let result;
235
+ for (let attempt = 0; attempt < 5; attempt++) {
236
+ result = spawnSync('node', ['szcd-mcp-client/agents/build.js'], {
237
+ cwd: '/scity/zengzhijie/mcp',
238
+ stdio: 'pipe'
239
+ });
240
+ if (result.status === 0) break;
241
+ if (result.stderr?.toString().includes('ENOTEMPTY')) {
242
+ await new Promise((r) => setTimeout(r, 500));
243
+ continue;
244
+ }
245
+ break;
246
+ }
247
+ assert.equal(result.status, 0, `build 失败: ${result.stderr?.toString()}`);
248
+
249
+ const target = '/scity/zengzhijie/mcp/szcd-mcp-client/qwen-extension/skills/local-browser-test/lib/retry-policy.js';
250
+ assert.ok(fs.existsSync(target), 'retry-policy.js 应同步到 qwen 副本');
251
+ });
252
+ });
@@ -0,0 +1,168 @@
1
+ /**
2
+ * PR #7 集成测试(用例标签 @smoke / @p0 / @p1 / @full / @flaky)
3
+ * 跑法:node --test tests/integration/pr-07.test.js
4
+ *
5
+ * 测试范围(不依赖真实浏览器):
6
+ * - parseTags: 逗号分隔 / 数组
7
+ * - matchTags: any / all 模式
8
+ * - filterCasesByTags: tags + excludeTags
9
+ * - filterPlansByTags
10
+ * - validateTags: 格式校验
11
+ * - test-plan.js compilePlan: plan.tags 字段
12
+ * - SKILL.md 章节
13
+ * - build 副本同步
14
+ */
15
+
16
+ import { describe, it } from 'node:test';
17
+ import assert from 'node:assert/strict';
18
+ import fs from 'node:fs';
19
+ import path from 'node:path';
20
+ import { fileURLToPath } from 'url';
21
+
22
+ const __filename = fileURLToPath(import.meta.url);
23
+ const __dirname = path.dirname(__filename);
24
+ const SKILL_DIR = path.resolve(__dirname, '../../');
25
+
26
+ const { parseTags, matchTags, filterCasesByTags, filterPlansByTags, validateTags } = await import(
27
+ '../../lib/tag-runner.js'
28
+ );
29
+ const { compilePlan } = await import('../../lib/test-plan.js');
30
+
31
+ describe('PR #7: parseTags(3 个核心场景)', () => {
32
+ it('逗号分隔字符串 → 数组', () => {
33
+ assert.deepEqual(parseTags('@smoke,@p0,@p1'), ['@smoke', '@p0', '@p1']);
34
+ });
35
+
36
+ it('数组直接返回(去空)', () => {
37
+ assert.deepEqual(parseTags(['@smoke', '', null, '@p0']), ['@smoke', '@p0']);
38
+ });
39
+
40
+ it('undefined / null / 空字符串 → 空数组', () => {
41
+ assert.deepEqual(parseTags(undefined), []);
42
+ assert.deepEqual(parseTags(null), []);
43
+ assert.deepEqual(parseTags(''), []);
44
+ });
45
+ });
46
+
47
+ describe('PR #7: matchTags(4 个核心场景)', () => {
48
+ it('filterTags 为空 → 全部通过(不过滤)', () => {
49
+ assert.equal(matchTags(['@smoke'], [], 'any'), true);
50
+ assert.equal(matchTags(['@smoke'], [], 'all'), true);
51
+ });
52
+
53
+ it('mode=any:含任一 → 通过', () => {
54
+ assert.equal(matchTags(['@smoke'], ['@p0', '@smoke'], 'any'), true);
55
+ });
56
+
57
+ it('mode=all:必须含全部 → 通过', () => {
58
+ assert.equal(matchTags(['@p0', '@smoke'], ['@p0', '@smoke'], 'all'), true);
59
+ assert.equal(matchTags(['@p0'], ['@p0', '@smoke'], 'all'), false);
60
+ });
61
+
62
+ it('caseTags 为空 + filterTags 非空 → false(不过滤的场景才通过)', () => {
63
+ assert.equal(matchTags([], ['@p0'], 'any'), false);
64
+ assert.equal(matchTags(null, ['@p0'], 'any'), false);
65
+ });
66
+ });
67
+
68
+ describe('PR #7: filterCasesByTags(3 个场景)', () => {
69
+ const cases = [
70
+ { name: 'login-smoke', tags: ['@smoke'] },
71
+ { name: 'data-p0', tags: ['@p0'] },
72
+ { name: 'data-p1', tags: ['@p1'] },
73
+ { name: 'data-e2e', tags: ['@p0', '@p1'] },
74
+ { name: 'flaky-test', tags: ['@flaky'] }
75
+ ];
76
+
77
+ it('--tags @smoke → 只 smoke', () => {
78
+ const filtered = filterCasesByTags(cases, { tags: ['@smoke'] });
79
+ assert.equal(filtered.length, 1);
80
+ assert.equal(filtered[0].name, 'login-smoke');
81
+ });
82
+
83
+ it('--tags "@p0,@p1" any → p0 或 p1', () => {
84
+ const filtered = filterCasesByTags(cases, { tags: ['@p0', '@p1'], mode: 'any' });
85
+ assert.equal(filtered.length, 3, 'data-p0 + data-p1 + data-e2e');
86
+ });
87
+
88
+ it('--tags @p0 --exclude-tags @flaky → 排除 flaky', () => {
89
+ const filtered = filterCasesByTags(cases, { tags: ['@p0'], excludeTags: ['@flaky'] });
90
+ assert.equal(filtered.length, 2, 'data-p0 + data-e2e(flaky 已排除)');
91
+ });
92
+ });
93
+
94
+ describe('PR #7: filterPlansByTags(2 个场景)', () => {
95
+ const plans = [
96
+ { title: 'P0-regression', tags: ['@p0'] },
97
+ { title: 'P1-regression', tags: ['@p1'] },
98
+ { title: 'E2E', tags: ['@p0', '@p1'] }
99
+ ];
100
+
101
+ it('--tags @p0 any → P0 + E2E', () => {
102
+ const filtered = filterPlansByTags(plans, { tags: ['@p0'], mode: 'any' });
103
+ assert.equal(filtered.length, 2);
104
+ });
105
+
106
+ it('--tags "@p0,@p1" all → E2E', () => {
107
+ const filtered = filterPlansByTags(plans, { tags: ['@p0', '@p1'], mode: 'all' });
108
+ assert.equal(filtered.length, 1);
109
+ assert.equal(filtered[0].title, 'E2E');
110
+ });
111
+ });
112
+
113
+ describe('PR #7: validateTags(2 个场景)', () => {
114
+ it('合法 tag(@ 前缀)→ valid=true', () => {
115
+ const r = validateTags(['@smoke', '@p0', '@p1', '@flaky']);
116
+ assert.equal(r.valid, true);
117
+ assert.deepEqual(r.invalid, []);
118
+ });
119
+
120
+ it('非法 tag(无 @ 前缀)→ valid=false', () => {
121
+ const r = validateTags(['smoke', '@p0', 'p1']);
122
+ assert.equal(r.valid, false);
123
+ assert.deepEqual(r.invalid, ['smoke', 'p1']);
124
+ });
125
+ });
126
+
127
+ describe('PR #7: test-plan.js compilePlan(2 个场景)', () => {
128
+ it('plan.tags → testCase.tags 透传', () => {
129
+ const plan = {
130
+ planId: 'test-1',
131
+ description: 'P0 regression',
132
+ tags: ['@p0', '@regression'],
133
+ steps: [{ type: 'act', click: '登录' }]
134
+ };
135
+ const tc = compilePlan(plan);
136
+ assert.deepEqual(tc.tags, ['@p0', '@regression']);
137
+ });
138
+
139
+ it('plan 无 tags → testCase.tags = [](默认空数组)', () => {
140
+ const tc = compilePlan({ planId: 'test-1', steps: [] });
141
+ assert.deepEqual(tc.tags, []);
142
+ });
143
+ });
144
+
145
+ describe('PR #7: build 副本同步', () => {
146
+ it('build 后 qwen-extension 含 tag-runner.js + test-plan.js', async () => {
147
+ const { spawnSync } = await import('node:child_process');
148
+ let result;
149
+ for (let attempt = 0; attempt < 5; attempt++) {
150
+ result = spawnSync('node', ['szcd-mcp-client/agents/build.js'], {
151
+ cwd: '/scity/zengzhijie/mcp',
152
+ stdio: 'pipe'
153
+ });
154
+ if (result.status === 0) break;
155
+ if (result.stderr?.toString().includes('ENOTEMPTY')) {
156
+ await new Promise((r) => setTimeout(r, 500));
157
+ continue;
158
+ }
159
+ break;
160
+ }
161
+ assert.equal(result.status, 0, `build 失败: ${result.stderr?.toString()}`);
162
+
163
+ const base = '/scity/zengzhijie/mcp/szcd-mcp-client/qwen-extension/skills/local-browser-test/lib/';
164
+ for (const f of ['tag-runner.js', 'test-plan.js']) {
165
+ assert.ok(fs.existsSync(base + f), `${f} 应同步到 qwen 副本`);
166
+ }
167
+ });
168
+ });