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.
- package/.claude/.kickoff-hint.txt +3 -2
- package/.claude/CLAUDE.md +138 -0
- package/.claude/README.md +234 -43
- package/.claude/boris-optimizations.md +167 -0
- package/.claude/commands/todos.md +6 -41
- package/.claude/hooks/code-formatter.cjs +2 -7
- package/.claude/hooks/conversation-logger.cjs +222 -0
- package/.claude/hooks/multi-session.cjs +3 -9
- package/.claude/hooks/project-kickoff.cjs +198 -20
- package/.claude/hooks/rag-skill-loader.cjs +0 -7
- package/.claude/hooks/session-restore.cjs +0 -0
- package/.claude/hooks/session-save.cjs +0 -0
- package/.claude/hooks/thinking-silent.cjs +3 -9
- package/.claude/hooks/todo-manager.cjs +142 -269
- package/.claude/hooks/verify-work.cjs +4 -10
- package/.claude/rag/skill-index.json +128 -8
- package/.claude/settings.json +115 -0
- package/.claude/skills/123-skill/SKILL.md +61 -0
- package/.claude/skills/123-skill/examples/basic.md +3 -0
- package/.claude/skills/123-skill/metadata.yaml +30 -0
- package/.claude/skills/123-skill/templates/default.md +3 -0
- package/.claude/skills/SKILLS.md +145 -0
- package/.claude/skills/code-reviewer-123/SKILL.md +61 -0
- package/.claude/skills/code-reviewer-123/examples/basic.md +3 -0
- package/.claude/skills/code-reviewer-123/metadata.yaml +30 -0
- package/.claude/skills/code-reviewer-123/templates/default.md +3 -0
- package/.claude/skills/examples/README.md +47 -0
- package/.claude/skills/examples/basic-task.md +67 -0
- package/.claude/skills/examples/bug-fix-workflow.md +92 -0
- package/.claude/skills/examples/feature-development.md +81 -0
- package/.claude/skills/manus-kickoff/SKILL.md +128 -0
- package/.claude/skills/manus-kickoff/examples/basic.md +84 -0
- package/.claude/skills/manus-kickoff/metadata.yaml +33 -0
- package/.claude/skills/manus-kickoff/templates/PROJECT_KICKOFF.md +89 -0
- package/.claude/skills/manus-kickoff/templates/PROJECT_PROPOSAL.md +227 -0
- package/.claude/skills/manus-kickoff/templates/TASK_PLAN.md +121 -0
- package/.claude/skills/my-skill/SKILL.md +61 -0
- package/.claude/skills/my-skill/examples/basic.md +3 -0
- package/.claude/skills/my-skill/metadata.yaml +30 -0
- package/.claude/skills/my-skill/templates/default.md +3 -0
- package/.claude/skills/template/metadata.yaml +30 -0
- package/.claude/skills/test-skill-name/SKILL.md +61 -0
- package/.claude/skills/test-skill-name/examples/basic.md +3 -0
- package/.claude/skills/test-skill-name/metadata.yaml +30 -0
- package/.claude/skills/test-skill-name/templates/default.md +3 -0
- package/.claude/templates/PROJECT_KICKOFF.md +89 -0
- package/.claude/templates/PROJECT_PROPOSAL.md +227 -0
- package/.claude/templates/TASK_PLAN.md +121 -0
- package/.claude-plugin/marketplace.json +2 -2
- package/AGENTS.md +30 -6
- package/CHANGELOG.md +18 -0
- package/CLAUDE-template.md +114 -0
- package/README.md +16 -1
- package/config/official-skills.json +2 -2
- package/jest.config.js +3 -1
- package/lib/commands.js +1626 -1207
- package/lib/marketplace.js +1 -0
- package/package.json +1 -1
- package/project-paradigm.md +313 -0
- package/prompts/how-to-find.md +163 -0
- package/tests/commands.test.js +940 -17
- package/tests/config-schema.test.js +425 -0
- package/tests/marketplace.test.js +330 -214
- package/tests/sync-external.test.js +214 -0
- package/tests/update-registry.test.js +251 -0
- package/tests/utils.test.js +12 -8
- package/tests/web-search.test.js +392 -0
- package/thinkinglens-silent.md +138 -0
|
@@ -1,296 +1,351 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Marketplace 模块单元测试
|
|
2
|
+
* Marketplace 模块单元测试 - 扩展版
|
|
3
|
+
* 测试技能市场管理和 YAML 解析功能
|
|
3
4
|
*/
|
|
4
5
|
|
|
5
|
-
const
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
180
|
-
const yaml = `
|
|
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
|
-
|
|
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
|
|
188
|
-
const yaml =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
219
|
-
|
|
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
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
241
|
-
|
|
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
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
274
|
+
// ============================================================================
|
|
275
|
+
// 市场常量定义测试
|
|
276
|
+
// ============================================================================
|
|
250
277
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
257
|
-
|
|
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
|
-
|
|
260
|
-
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
298
|
+
// ============================================================================
|
|
299
|
+
// YAML 格式验证测试
|
|
300
|
+
// ============================================================================
|
|
267
301
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
280
|
-
|
|
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
|
-
|
|
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
|
});
|