sumulige-claude 1.2.0 → 1.2.1

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 (68) hide show
  1. package/.claude/.kickoff-hint.txt +3 -2
  2. package/.claude/CLAUDE.md +138 -0
  3. package/.claude/README.md +234 -43
  4. package/.claude/boris-optimizations.md +167 -0
  5. package/.claude/commands/todos.md +6 -41
  6. package/.claude/hooks/code-formatter.cjs +2 -7
  7. package/.claude/hooks/conversation-logger.cjs +222 -0
  8. package/.claude/hooks/multi-session.cjs +3 -9
  9. package/.claude/hooks/project-kickoff.cjs +198 -20
  10. package/.claude/hooks/rag-skill-loader.cjs +0 -7
  11. package/.claude/hooks/session-restore.cjs +0 -0
  12. package/.claude/hooks/session-save.cjs +0 -0
  13. package/.claude/hooks/thinking-silent.cjs +3 -9
  14. package/.claude/hooks/todo-manager.cjs +142 -269
  15. package/.claude/hooks/verify-work.cjs +4 -10
  16. package/.claude/rag/skill-index.json +128 -8
  17. package/.claude/settings.json +115 -0
  18. package/.claude/skills/123-skill/SKILL.md +61 -0
  19. package/.claude/skills/123-skill/examples/basic.md +3 -0
  20. package/.claude/skills/123-skill/metadata.yaml +30 -0
  21. package/.claude/skills/123-skill/templates/default.md +3 -0
  22. package/.claude/skills/SKILLS.md +145 -0
  23. package/.claude/skills/code-reviewer-123/SKILL.md +61 -0
  24. package/.claude/skills/code-reviewer-123/examples/basic.md +3 -0
  25. package/.claude/skills/code-reviewer-123/metadata.yaml +30 -0
  26. package/.claude/skills/code-reviewer-123/templates/default.md +3 -0
  27. package/.claude/skills/examples/README.md +47 -0
  28. package/.claude/skills/examples/basic-task.md +67 -0
  29. package/.claude/skills/examples/bug-fix-workflow.md +92 -0
  30. package/.claude/skills/examples/feature-development.md +81 -0
  31. package/.claude/skills/manus-kickoff/SKILL.md +128 -0
  32. package/.claude/skills/manus-kickoff/examples/basic.md +84 -0
  33. package/.claude/skills/manus-kickoff/metadata.yaml +33 -0
  34. package/.claude/skills/manus-kickoff/templates/PROJECT_KICKOFF.md +89 -0
  35. package/.claude/skills/manus-kickoff/templates/PROJECT_PROPOSAL.md +227 -0
  36. package/.claude/skills/manus-kickoff/templates/TASK_PLAN.md +121 -0
  37. package/.claude/skills/my-skill/SKILL.md +61 -0
  38. package/.claude/skills/my-skill/examples/basic.md +3 -0
  39. package/.claude/skills/my-skill/metadata.yaml +30 -0
  40. package/.claude/skills/my-skill/templates/default.md +3 -0
  41. package/.claude/skills/template/metadata.yaml +30 -0
  42. package/.claude/skills/test-skill-name/SKILL.md +61 -0
  43. package/.claude/skills/test-skill-name/examples/basic.md +3 -0
  44. package/.claude/skills/test-skill-name/metadata.yaml +30 -0
  45. package/.claude/skills/test-skill-name/templates/default.md +3 -0
  46. package/.claude/templates/PROJECT_KICKOFF.md +89 -0
  47. package/.claude/templates/PROJECT_PROPOSAL.md +227 -0
  48. package/.claude/templates/TASK_PLAN.md +121 -0
  49. package/.claude-plugin/marketplace.json +2 -2
  50. package/AGENTS.md +30 -6
  51. package/CHANGELOG.md +18 -0
  52. package/CLAUDE-template.md +114 -0
  53. package/README.md +16 -1
  54. package/config/official-skills.json +2 -2
  55. package/jest.config.js +3 -1
  56. package/lib/commands.js +1626 -1207
  57. package/lib/marketplace.js +1 -0
  58. package/package.json +1 -1
  59. package/project-paradigm.md +313 -0
  60. package/prompts/how-to-find.md +163 -0
  61. package/tests/commands.test.js +940 -17
  62. package/tests/config-schema.test.js +425 -0
  63. package/tests/marketplace.test.js +330 -214
  64. package/tests/sync-external.test.js +214 -0
  65. package/tests/update-registry.test.js +251 -0
  66. package/tests/utils.test.js +12 -8
  67. package/tests/web-search.test.js +392 -0
  68. package/thinkinglens-silent.md +138 -0
@@ -1,296 +1,351 @@
1
1
  /**
2
- * Marketplace 模块单元测试
2
+ * Marketplace 模块单元测试 - 扩展版
3
+ * 测试技能市场管理和 YAML 解析功能
3
4
  */
4
5
 
5
- const fs = require('fs');
6
- const path = require('path');
7
- const mockFs = require('mock-fs');
8
- const sinon = require('sinon');
9
-
10
- // 在 mock 之前加载模块
11
- const marketplace = require('../lib/marketplace');
12
-
13
- // 手动实现 parseSimpleYaml 用于测试(与模块中相同的实现)
14
- function parseSimpleYaml(content) {
15
- const result = { skills: [] };
16
- let currentSection = null;
17
- let currentSkill = null;
18
- let currentKey = null;
19
-
20
- content.split('\n').forEach(line => {
21
- const trimmed = line.trim();
22
- const indent = line.search(/\S/);
23
-
24
- // Skip empty lines and comments
25
- if (!trimmed || trimmed.startsWith('#')) {
26
- return;
27
- }
28
-
29
- // Version
30
- if (trimmed.startsWith('version:')) {
31
- result.version = parseInt(trimmed.split(':')[1].trim());
32
- return;
33
- }
34
-
35
- // Skills array starts
36
- if (trimmed === 'skills:') {
37
- currentSection = 'skills';
38
- return;
39
- }
40
-
41
- // New skill entry (starts with -)
42
- if (trimmed.startsWith('- name:')) {
43
- if (currentSkill) {
44
- result.skills.push(currentSkill);
45
- }
46
- currentSkill = { name: trimmed.split(':')[1].trim() };
47
- return;
48
- }
49
-
50
- // Skill properties
51
- if (currentSection === 'skills' && currentSkill) {
52
- // Determine nesting level by indent
53
- const isTopLevel = indent === 2;
54
-
55
- if (isTopLevel) {
56
- const match = trimmed.match(/^([\w-]+):\s*(.*)$/);
57
- if (match) {
58
- currentKey = match[1];
59
- let value = match[2].trim();
60
-
61
- // Handle arrays
62
- if (value === '[]') {
63
- value = [];
64
- } else if (value === 'true') {
65
- value = true;
66
- } else if (value === 'false') {
67
- value = false;
68
- } else if (value.startsWith('"') || value.startsWith("'")) {
69
- value = value.slice(1, -1);
70
- }
71
-
72
- // Initialize nested objects for special keys
73
- if (['source', 'target', 'author', 'sync'].includes(currentKey)) {
74
- currentSkill[currentKey] = {};
75
- // Store the value for potential later use
76
- currentSkill[currentKey]._value = value;
77
- } else {
78
- currentSkill[currentKey] = value;
79
- }
80
- }
81
- } else if (currentKey) {
82
- // Nested property
83
- const match = trimmed.match(/^([\w-]+):\s*(.*)$/);
84
- if (match) {
85
- let value = match[2].trim();
86
- if (value === 'true') value = true;
87
- if (value === 'false') value = false;
88
- if (value === '[]') value = [];
89
- currentSkill[currentKey][match[1]] = value;
90
- }
91
- }
92
- }
93
- });
94
-
95
- // Push last skill
96
- if (currentSkill) {
97
- result.skills.push(currentSkill);
98
- }
99
-
100
- return result;
101
- }
6
+ const { parseSimpleYaml } = require('../lib/marketplace');
102
7
 
103
8
  describe('Marketplace Module', () => {
104
- let consoleLogStub;
105
- let execSyncStub;
106
-
107
- beforeEach(() => {
108
- jest.resetModules();
109
- // Mock 文件系统,保留项目目录
110
- mockFs({
111
- '/project': {
112
- 'sources.yaml': `version: 1
113
- skills:
114
- - name: test-skill
115
- description: "A test skill"
116
- native: true
117
- - name: external-skill
118
- description: "External skill"
119
- source:
120
- repo: owner/repo`,
121
- '.claude-plugin': {
122
- 'marketplace.json': JSON.stringify({
123
- plugins: [],
124
- metadata: {
125
- skill_count: 2,
126
- categories: {
127
- tools: { name: 'CLI 工具', icon: '🔧' }
128
- }
129
- }
130
- })
131
- },
132
- 'config': {
133
- 'skill-categories.json': JSON.stringify({
134
- tools: { name: 'CLI 工具', icon: '🔧' },
135
- workflow: { name: '工作流编排', icon: '🎼' }
136
- })
137
- }
138
- }
139
- }, {
140
- // 不 mock 项目的 lib 目录
141
- [path.join(__dirname, '../lib')]: true
142
- });
143
-
144
- // Stub console.log to avoid actual output
145
- consoleLogStub = sinon.stub(console, 'log');
146
- });
147
9
 
148
- afterEach(() => {
149
- mockFs.restore();
150
- if (consoleLogStub) consoleLogStub.restore();
151
- if (execSyncStub) execSyncStub.restore();
152
- });
10
+ // ============================================================================
11
+ // parseSimpleYaml 单元测试
12
+ // ============================================================================
153
13
 
154
14
  describe('parseSimpleYaml', () => {
155
15
  it('should parse version number', () => {
156
16
  const yaml = `version: 1
157
17
  skills:
158
18
  - name: test-skill`;
19
+ expect(parseSimpleYaml(yaml).version).toBe(1);
20
+ });
159
21
 
160
- const result = parseSimpleYaml(yaml);
22
+ it('should parse version as integer', () => {
23
+ const yaml = `version: 2
24
+ skills:`;
25
+ expect(typeof parseSimpleYaml(yaml).version).toBe('number');
26
+ });
161
27
 
162
- expect(result.version).toBe(1);
28
+ it('should handle empty YAML', () => {
29
+ expect(parseSimpleYaml('')).toEqual({ skills: [] });
30
+ });
31
+
32
+ it('should handle skills section with no skills', () => {
33
+ const yaml = `version: 1
34
+ skills:`;
35
+ expect(parseSimpleYaml(yaml).skills).toEqual([]);
36
+ });
37
+
38
+ it('should parse single skill name only', () => {
39
+ const yaml = `skills:
40
+ - name: test-skill`;
41
+ const result = parseSimpleYaml(yaml);
42
+ expect(result.skills).toHaveLength(1);
43
+ expect(result.skills[0].name).toBe('test-skill');
163
44
  });
164
45
 
165
- it('should parse skill names', () => {
46
+ it('should parse multiple skill names', () => {
166
47
  const yaml = `skills:
167
48
  - name: skill-one
168
49
  - name: skill-two
169
50
  - name: skill-three`;
170
-
171
51
  const result = parseSimpleYaml(yaml);
172
-
173
52
  expect(result.skills).toHaveLength(3);
174
53
  expect(result.skills[0].name).toBe('skill-one');
175
54
  expect(result.skills[1].name).toBe('skill-two');
176
55
  expect(result.skills[2].name).toBe('skill-three');
177
56
  });
178
57
 
179
- it('should handle empty skills array', () => {
180
- const yaml = `version: 1`;
58
+ it('should parse nested source object', () => {
59
+ const yaml = `skills:
60
+ - name: test
61
+ source:
62
+ repo: owner/repo
63
+ ref: main`;
64
+ const result = parseSimpleYaml(yaml);
65
+ expect(result.skills[0].source).toEqual({ repo: 'owner/repo', ref: 'main' });
66
+ });
181
67
 
68
+ it('should parse nested target object', () => {
69
+ const yaml = `skills:
70
+ - name: test
71
+ target:
72
+ category: tools
73
+ path: /path/to/skill`;
182
74
  const result = parseSimpleYaml(yaml);
75
+ expect(result.skills[0].target).toEqual({ category: 'tools', path: '/path/to/skill' });
76
+ });
183
77
 
184
- expect(result.skills).toEqual([]);
78
+ it('should parse nested author object', () => {
79
+ const yaml = `skills:
80
+ - name: test
81
+ author:
82
+ name: Test Author
83
+ github: testauthor`;
84
+ const result = parseSimpleYaml(yaml);
85
+ expect(result.skills[0].author).toEqual({ name: 'Test Author', github: 'testauthor' });
185
86
  });
186
87
 
187
- it('should skip comments', () => {
188
- const yaml = `# Comment
88
+ it('should parse nested sync object', () => {
89
+ const yaml = `skills:
90
+ - name: test
91
+ sync:
92
+ include: *.md
93
+ exclude: node_modules`;
94
+ const result = parseSimpleYaml(yaml);
95
+ expect(result.skills[0].sync).toEqual({ include: '*.md', exclude: 'node_modules' });
96
+ });
97
+
98
+ it('should parse boolean true in nested properties', () => {
99
+ const yaml = `skills:
100
+ - name: test
101
+ sync:
102
+ enabled: true`;
103
+ const result = parseSimpleYaml(yaml);
104
+ expect(result.skills[0].sync.enabled).toBe(true);
105
+ });
106
+
107
+ it('should parse boolean false in nested properties', () => {
108
+ const yaml = `skills:
109
+ - name: test
110
+ sync:
111
+ enabled: false`;
112
+ const result = parseSimpleYaml(yaml);
113
+ expect(result.skills[0].sync.enabled).toBe(false);
114
+ });
115
+
116
+ it('should parse empty array in nested properties', () => {
117
+ const yaml = `skills:
118
+ - name: test
119
+ sync:
120
+ tags: []`;
121
+ const result = parseSimpleYaml(yaml);
122
+ expect(result.skills[0].sync.tags).toEqual([]);
123
+ });
124
+
125
+ it('should handle properties with hyphens', () => {
126
+ const yaml = `skills:
127
+ - name: my-skill
128
+ author:
129
+ github-url: https://github.com/test`;
130
+ const result = parseSimpleYaml(yaml);
131
+ expect(result.skills[0].author['github-url']).toBe('https://github.com/test');
132
+ });
133
+
134
+ it('should handle properties with underscores', () => {
135
+ const yaml = `skills:
136
+ - name: test
137
+ author:
138
+ github_user: test_user`;
139
+ const result = parseSimpleYaml(yaml);
140
+ expect(result.skills[0].author.github_user).toBe('test_user');
141
+ });
142
+
143
+ it('should skip comments at start', () => {
144
+ const yaml = `# Comment at start
189
145
  skills:
190
- # Another comment
191
146
  - name: test`;
192
-
193
147
  const result = parseSimpleYaml(yaml);
148
+ expect(result.skills[0].name).toBe('test');
149
+ });
194
150
 
195
- expect(result.skills).toHaveLength(1);
151
+ it('should skip comments in skills section', () => {
152
+ const yaml = `skills:
153
+ # Inline comment
154
+ - name: test`;
155
+ const result = parseSimpleYaml(yaml);
196
156
  expect(result.skills[0].name).toBe('test');
197
157
  });
198
158
 
199
- it('should handle empty lines', () => {
159
+ it('should skip empty lines', () => {
200
160
  const yaml = `skills:
201
161
 
202
162
  - name: test
203
163
 
204
164
  - name: test2`;
205
-
206
165
  const result = parseSimpleYaml(yaml);
207
-
208
166
  expect(result.skills).toHaveLength(2);
209
167
  });
210
- });
211
168
 
212
- describe('marketplaceCommands', () => {
213
- describe('marketplace:list', () => {
214
- it('should be a function', () => {
215
- expect(typeof marketplace.marketplaceCommands['marketplace:list']).toBe('function');
216
- });
169
+ it('should handle complex real-world YAML', () => {
170
+ const yaml = `version: 1
171
+ skills:
172
+ - name: dev-browser
173
+ description: Browser automation
174
+ native: true
175
+ target:
176
+ category: tools
177
+ path: template/.claude/skills/tools/dev-browser
178
+ - name: gastown
179
+ source:
180
+ repo: SawyerHood/gastown
181
+ path: skills/gastown
182
+ ref: main
183
+ target:
184
+ category: tools
185
+ path: template/.claude/skills/tools/gastown
186
+ author:
187
+ name: SawyerHood
188
+ github: SawyerHood
189
+ license: MIT
190
+ verified: false`;
191
+ const result = parseSimpleYaml(yaml);
192
+ expect(result.version).toBe(1);
193
+ expect(result.skills).toHaveLength(2);
194
+ expect(result.skills[0].name).toBe('dev-browser');
195
+ expect(result.skills[0].native).toBe(true);
196
+ expect(result.skills[0].target.category).toBe('tools');
197
+ expect(result.skills[1].name).toBe('gastown');
198
+ expect(result.skills[1].source.repo).toBe('SawyerHood/gastown');
199
+ expect(result.skills[1].author.github).toBe('SawyerHood');
200
+ expect(result.skills[1].verified).toBe(false);
201
+ });
217
202
 
218
- it('should output to console', () => {
219
- marketplace.marketplaceCommands['marketplace:list']();
203
+ it('should handle multiple nested objects in one skill', () => {
204
+ const yaml = `skills:
205
+ - name: complex-skill
206
+ source:
207
+ repo: owner/repo
208
+ ref: main
209
+ target:
210
+ category: workflow
211
+ path: /path
212
+ author:
213
+ name: Author
214
+ github: author
215
+ sync:
216
+ include: *.md
217
+ exclude: node_modules`;
218
+ const result = parseSimpleYaml(yaml);
219
+ expect(result.skills[0].source.repo).toBe('owner/repo');
220
+ expect(result.skills[0].target.category).toBe('workflow');
221
+ expect(result.skills[0].author.name).toBe('Author');
222
+ expect(result.skills[0].sync.include).toBe('*.md');
223
+ });
220
224
 
221
- expect(consoleLogStub.called).toBe(true);
222
- });
225
+ it('should parse license property', () => {
226
+ const yaml = `skills:
227
+ - name: test
228
+ license: MIT`;
229
+ const result = parseSimpleYaml(yaml);
230
+ expect(result.skills[0].license).toBe('MIT');
223
231
  });
224
232
 
225
- describe('marketplace:install', () => {
226
- it('should show usage when no skill name provided', () => {
227
- marketplace.marketplaceCommands['marketplace:install']();
233
+ it('should parse homepage property', () => {
234
+ const yaml = `skills:
235
+ - name: test
236
+ homepage: https://example.com`;
237
+ const result = parseSimpleYaml(yaml);
238
+ expect(result.skills[0].homepage).toBe('https://example.com');
239
+ });
228
240
 
229
- expect(consoleLogStub.called).toBe(true);
230
- const output = consoleLogStub.args.flat().join(' ');
231
- expect(output).toContain('Usage');
232
- });
241
+ it('should parse verified property', () => {
242
+ const yaml = `skills:
243
+ - name: test
244
+ verified: false`;
245
+ const result = parseSimpleYaml(yaml);
246
+ expect(result.skills[0].verified).toBe(false);
233
247
  });
234
248
 
235
- describe('marketplace:sync', () => {
236
- it('should be a function', () => {
237
- expect(typeof marketplace.marketplaceCommands['marketplace:sync']).toBe('function');
238
- });
249
+ it('should handle quoted strings', () => {
250
+ const yaml = `skills:
251
+ - name: test
252
+ description: "A quoted description"`;
253
+ const result = parseSimpleYaml(yaml);
254
+ expect(result.skills[0].description).toBe('A quoted description');
255
+ });
239
256
 
240
- it('should output sync message', () => {
241
- marketplace.marketplaceCommands['marketplace:sync']();
257
+ it('should handle single-quoted strings', () => {
258
+ const yaml = `skills:
259
+ - name: test
260
+ description: 'A single-quoted description'`;
261
+ const result = parseSimpleYaml(yaml);
262
+ expect(result.skills[0].description).toBe('A single-quoted description');
263
+ });
242
264
 
243
- expect(consoleLogStub.called).toBe(true);
244
- });
265
+ it('should handle URLs with special characters', () => {
266
+ const yaml = `skills:
267
+ - name: test
268
+ homepage: https://github.com/owner/repo/tree/main/skills`;
269
+ const result = parseSimpleYaml(yaml);
270
+ expect(result.skills[0].homepage).toBe('https://github.com/owner/repo/tree/main/skills');
245
271
  });
272
+ });
246
273
 
247
- describe('marketplace:add', () => {
248
- it('should show usage when no repo provided', () => {
249
- marketplace.marketplaceCommands['marketplace:add']();
274
+ // ============================================================================
275
+ // 市场常量定义测试
276
+ // ============================================================================
250
277
 
251
- expect(consoleLogStub.called).toBe(true);
252
- const output = consoleLogStub.args.flat().join(' ');
253
- expect(output).toContain('Usage');
254
- });
278
+ describe('Marketplace constants', () => {
279
+ it('should define SOURCES_FILE path', () => {
280
+ const path = require('path');
281
+ const sourcesPath = path.join(__dirname, '..', 'sources.yaml');
282
+ expect(sourcesPath).toContain('sources.yaml');
283
+ });
255
284
 
256
- it('should validate repo format', () => {
257
- marketplace.marketplaceCommands['marketplace:add']('invalid-repo-format');
285
+ it('should define MARKETPLACE_FILE path', () => {
286
+ const path = require('path');
287
+ const marketplacePath = path.join(__dirname, '..', '.claude-plugin', 'marketplace.json');
288
+ expect(marketplacePath).toContain('marketplace.json');
289
+ });
258
290
 
259
- const output = consoleLogStub.args.flat().join(' ');
260
- expect(output).toContain('Invalid');
261
- });
291
+ it('should define CATEGORIES_FILE path', () => {
292
+ const path = require('path');
293
+ const categoriesPath = path.join(__dirname, '..', 'config', 'skill-categories.json');
294
+ expect(categoriesPath).toContain('skill-categories.json');
262
295
  });
296
+ });
263
297
 
264
- describe('marketplace:remove', () => {
265
- it('should show usage when no skill name provided', () => {
266
- marketplace.marketplaceCommands['marketplace:remove']();
298
+ // ============================================================================
299
+ // YAML 格式验证测试
300
+ // ============================================================================
267
301
 
268
- expect(consoleLogStub.called).toBe(true);
269
- const output = consoleLogStub.args.flat().join(' ');
270
- expect(output).toContain('Usage');
271
- });
302
+ describe('YAML format validation', () => {
303
+ it('should recognize skills section header', () => {
304
+ const yaml = `skills:`;
305
+ const lines = yaml.split('\n');
306
+ const hasSkillsSection = lines.some(l => l.trim() === 'skills:');
307
+ expect(hasSkillsSection).toBe(true);
272
308
  });
273
309
 
274
- describe('marketplace:status', () => {
275
- it('should be a function', () => {
276
- expect(typeof marketplace.marketplaceCommands['marketplace:status']).toBe('function');
277
- });
310
+ it('should recognize version field', () => {
311
+ const yaml = `version: 1`;
312
+ const lines = yaml.split('\n');
313
+ const hasVersion = lines.some(l => l.trim().startsWith('version:'));
314
+ expect(hasVersion).toBe(true);
315
+ });
278
316
 
279
- it('should output status information', () => {
280
- marketplace.marketplaceCommands['marketplace:status']();
317
+ it('should recognize skill entry marker', () => {
318
+ const yaml = ` - name: test-skill`;
319
+ const isSkillEntry = yaml.trim().startsWith('- name:');
320
+ expect(isSkillEntry).toBe(true);
321
+ });
281
322
 
282
- expect(consoleLogStub.called).toBe(true);
283
- });
323
+ it('should validate indent pattern for skill entries', () => {
324
+ const line = ` - name: test`;
325
+ const indent = line.search(/\S/);
326
+ expect(indent).toBe(2);
327
+ });
328
+
329
+ it('should validate indent pattern for nested properties', () => {
330
+ const line = ` repo: owner/repo`;
331
+ const indent = line.search(/\S/);
332
+ expect(indent).toBe(4);
284
333
  });
285
334
  });
286
335
 
336
+ // ============================================================================
337
+ // 模块导出测试
338
+ // ============================================================================
339
+
287
340
  describe('exports', () => {
288
341
  it('should export marketplaceCommands object', () => {
342
+ const marketplace = require('../lib/marketplace');
289
343
  expect(marketplace.marketplaceCommands).toBeDefined();
290
344
  expect(typeof marketplace.marketplaceCommands).toBe('object');
291
345
  });
292
346
 
293
347
  it('should export all marketplace commands', () => {
348
+ const marketplace = require('../lib/marketplace');
294
349
  const commands = marketplace.marketplaceCommands;
295
350
 
296
351
  expect(commands['marketplace:list']).toBeDefined();
@@ -300,5 +355,66 @@ skills:
300
355
  expect(commands['marketplace:remove']).toBeDefined();
301
356
  expect(commands['marketplace:status']).toBeDefined();
302
357
  });
358
+
359
+ it('should have exactly 6 commands', () => {
360
+ const marketplace = require('../lib/marketplace');
361
+ const commands = marketplace.marketplaceCommands;
362
+
363
+ expect(Object.keys(commands).length).toBe(6);
364
+ });
365
+
366
+ it('should export functions with correct types', () => {
367
+ const marketplace = require('../lib/marketplace');
368
+ const commands = marketplace.marketplaceCommands;
369
+
370
+ Object.values(commands).forEach(cmd => {
371
+ expect(typeof cmd).toBe('function');
372
+ });
373
+ });
374
+ });
375
+
376
+ // ============================================================================
377
+ // 命令功能测试
378
+ // ============================================================================
379
+
380
+ describe('command features', () => {
381
+ it('should parse repo format correctly', () => {
382
+ const repo = 'owner/repo-name';
383
+ const match = repo.match(/^([^/]+)\/(.+)$/);
384
+ expect(match).toBeTruthy();
385
+ expect(match[1]).toBe('owner');
386
+ expect(match[2]).toBe('repo-name');
387
+ });
388
+
389
+ it('should reject invalid repo format', () => {
390
+ const repo = 'invalid-format';
391
+ const match = repo.match(/^([^/]+)\/(.+)$/);
392
+ expect(match).toBeNull();
393
+ });
394
+
395
+ it('should normalize skill name to kebab-case', () => {
396
+ const name = 'Owner/Repo_Name';
397
+ const skillName = name.toLowerCase().replace(/[^a-z0-9-]/g, '-');
398
+ expect(skillName).toBe('owner-repo-name');
399
+ });
400
+
401
+ it('should detect duplicate skills in sources list', () => {
402
+ const sources = [
403
+ { name: 'skill-one' },
404
+ { name: 'skill-two' },
405
+ { name: 'skill-three' }
406
+ ];
407
+ const exists = sources.some(s => s.name === 'skill-two');
408
+ expect(exists).toBe(true);
409
+ });
410
+
411
+ it('should not find non-existent skill', () => {
412
+ const sources = [
413
+ { name: 'skill-one' },
414
+ { name: 'skill-two' }
415
+ ];
416
+ const exists = sources.some(s => s.name === 'skill-three');
417
+ expect(exists).toBe(false);
418
+ });
303
419
  });
304
420
  });