foliko 2.0.21 → 2.0.22

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.
@@ -0,0 +1,247 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import os from 'os';
5
+
6
+ const { SubAgentManagerPlugin } = require('../../plugins/core/sub-agent/index.js');
7
+
8
+ function makePlugin() {
9
+ const p = new SubAgentManagerPlugin({});
10
+ p.install({ getCwd: () => process.cwd() });
11
+ return p;
12
+ }
13
+
14
+ describe('_parseMarkdownConfig 解析 agent .md 文件', () => {
15
+ let tempDir;
16
+ beforeEach(() => {
17
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-parse-'));
18
+ });
19
+ afterEach(() => {
20
+ fs.rmSync(tempDir, { recursive: true, force: true });
21
+ });
22
+
23
+ function writeAgent(name, content) {
24
+ const filePath = path.join(tempDir, `${name}.md`);
25
+ fs.writeFileSync(filePath, content, 'utf-8');
26
+ return filePath;
27
+ }
28
+
29
+ it('★ 关键 bug:frontmatter + body 里有 JSON 示例 → 应该用 frontmatter,不用示例', () => {
30
+ // 真实 bug 场景:news-pipeline-agent.md 有 frontmatter,但 body 里有 ```json 示例
31
+ const content = `---
32
+ name: news-pipeline-agent
33
+ description: 热点新闻视频自动化生产专家
34
+ tools: Read, Write, Edit
35
+ model: inherit
36
+ ---
37
+
38
+ # 热点新闻视频自动化专家
39
+
40
+ 执行流程:
41
+
42
+ \`\`\`json
43
+ {
44
+ "news_list": [
45
+ { "title": "新闻", "summary": "摘要" }
46
+ ]
47
+ }
48
+ \`\`\`
49
+ `;
50
+ const filePath = writeAgent('news-pipeline-agent', content);
51
+ const p = makePlugin();
52
+ const config = p._parseMarkdownConfig(filePath, 'news-pipeline-agent');
53
+ expect(config.name).toBe('news-pipeline-agent');
54
+ expect(config.description).toContain('热点');
55
+ expect(config.tools).toBe('Read, Write, Edit');
56
+ expect(config.model).toBe('inherit');
57
+ });
58
+
59
+ it('★ 关键 bug:只有 body JSON(无 frontmatter)→ 应该用 JSON 但必须有 name', () => {
60
+ const content = `---
61
+ description: 没 name 的 frontmatter
62
+ ---
63
+
64
+ \`\`\`json
65
+ {
66
+ "news_list": [{"title": "x"}]
67
+ }
68
+ \`\`\`
69
+ `;
70
+ const filePath = writeAgent('agent-a', content);
71
+ const p = makePlugin();
72
+ const config = p._parseMarkdownConfig(filePath, 'agent-a');
73
+ // frontmatter 没 name,回退到 JSON,但 JSON 也没 name
74
+ // 最终只有 defaultName,没有 description(因为 JSON 也没 name 被丢弃)
75
+ expect(config.name).toBe('agent-a');
76
+ });
77
+
78
+ it('纯 frontmatter:标准情况', () => {
79
+ const content = `---
80
+ name: simple-agent
81
+ description: 简单
82
+ tools: a, b, c
83
+ ---`;
84
+ const filePath = writeAgent('simple', content);
85
+ const p = makePlugin();
86
+ const config = p._parseMarkdownConfig(filePath, 'simple');
87
+ expect(config.name).toBe('simple-agent');
88
+ expect(config.tools).toBe('a, b, c');
89
+ });
90
+
91
+ it('纯 body JSON(有 name):应该用 JSON', () => {
92
+ const content = `# 没有 frontmatter
93
+
94
+ \`\`\`json
95
+ {
96
+ "name": "json-agent",
97
+ "description": "JSON 配置",
98
+ "tools": ["a", "b"]
99
+ }
100
+ \`\`\`
101
+ `;
102
+ const filePath = writeAgent('json-agent', content);
103
+ const p = makePlugin();
104
+ const config = p._parseMarkdownConfig(filePath, 'json-agent');
105
+ expect(config.name).toBe('json-agent');
106
+ expect(config.tools).toEqual(['a', 'b']);
107
+ });
108
+
109
+ it('★ 关键 bug:frontmatter 在中间(不在开头)→ 不应被识别为 frontmatter', () => {
110
+ const content = `# 标题
111
+
112
+ \`\`\`
113
+ ---
114
+ name: fake-name
115
+ ---
116
+ \`\`\`
117
+ `;
118
+ const filePath = writeAgent('edge', content);
119
+ const p = makePlugin();
120
+ const config = p._parseMarkdownConfig(filePath, 'edge');
121
+ // frontmatter 在代码块内,不应被识别
122
+ // body 没有 name 的 JSON,应使用 defaultName
123
+ expect(config.name).toBe('edge');
124
+ });
125
+
126
+ it('★ 真实复现:news-pipeline-agent.md', () => {
127
+ // 复制实际文件测
128
+ const realFile = 'D:/date/20260624/video/.foliko/agents/news-pipeline-agent.md';
129
+ if (!fs.existsSync(realFile)) return; // 跳过(如果文件不存在)
130
+ const p = makePlugin();
131
+ const config = p._parseMarkdownConfig(realFile, 'news-pipeline-agent');
132
+ expect(config.name).toBe('news-pipeline-agent');
133
+ expect(config.tools).toBeTruthy();
134
+ expect(typeof config.tools).toBe('string'); // 逗号分隔的字符串
135
+ });
136
+
137
+ it('★ 新功能:markdown body 提取为 systemPrompt', () => {
138
+ const content = `---
139
+ name: body-test
140
+ description: 测试 body 提取
141
+ ---
142
+
143
+ # 标题行(应被去除)
144
+
145
+ 你是一个**测试 agent**。
146
+
147
+ ## 章节 1
148
+ 内容 1
149
+
150
+ ## 章节 2
151
+ 内容 2
152
+ `;
153
+ const filePath = writeAgent('body-test', content);
154
+ const p = makePlugin();
155
+ const config = p._parseMarkdownConfig(filePath, 'body-test');
156
+ expect(config.name).toBe('body-test');
157
+ expect(config.systemPrompt).toBeTruthy();
158
+ expect(config.systemPrompt).toContain('测试 agent');
159
+ expect(config.systemPrompt).toContain('章节 1');
160
+ expect(config.systemPrompt).toContain('章节 2');
161
+ // 第一个 # 标题行应被去除
162
+ expect(config.systemPrompt).not.toContain('标题行(应被去除)');
163
+ });
164
+
165
+ it('★ 新功能:太短的 body 不作为 systemPrompt', () => {
166
+ const content = `---
167
+ name: short-body
168
+ ---
169
+
170
+ hi
171
+ `;
172
+ const filePath = writeAgent('short-body', content);
173
+ const p = makePlugin();
174
+ const config = p._parseMarkdownConfig(filePath, 'short-body');
175
+ // body 只有 "hi",长度 < 20,不应作为 systemPrompt
176
+ expect(config.systemPrompt).toBeUndefined();
177
+ });
178
+
179
+ it('★ 新功能:skills 字符串(YAML),由 SubAgentPlugin 构造时分割', () => {
180
+ const content = `---
181
+ name: skill-test
182
+ skills: a, b, c
183
+ ---`;
184
+ const filePath = writeAgent('skill-test', content);
185
+ const p = makePlugin();
186
+ const config = p._parseMarkdownConfig(filePath, 'skill-test');
187
+ // _parseYamlLike 把逗号分隔的字符串保留为字符串(不会切分)
188
+ // SubAgentPlugin 构造函数负责字符串→数组转换
189
+ expect(config.skills).toBe('a, b, c');
190
+
191
+ // 验证 SubAgentPlugin 的转换
192
+ const { SubAgentPlugin } = require('../../plugins/core/sub-agent/index.js');
193
+ const sub = new SubAgentPlugin(config);
194
+ expect(sub.skills).toEqual(['a', 'b', 'c']);
195
+ });
196
+
197
+ it('★ 新功能:skills 数组形式(来自 .json 配置)保留', () => {
198
+ const content = `---
199
+ name: skill-arr
200
+ skills: [a, b]
201
+ ---`;
202
+ const filePath = writeAgent('skill-arr', content);
203
+ const p = makePlugin();
204
+ const config = p._parseMarkdownConfig(filePath, 'skill-arr');
205
+ // _parseYamlLike 把 '[a, b]' 解析成 ['a', 'b']
206
+ expect(config.skills).toEqual(['a', 'b']);
207
+ });
208
+ });
209
+
210
+ describe('_loadAgentsFromDir 集成', () => {
211
+ let tempDir;
212
+ beforeEach(() => {
213
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-load-'));
214
+ fs.mkdirSync(path.join(tempDir, '.foliko', 'agents'), { recursive: true });
215
+ });
216
+ afterEach(() => {
217
+ fs.rmSync(tempDir, { recursive: true, force: true });
218
+ });
219
+
220
+ it('正确加载带 body JSON 示例的 agent', () => {
221
+ const filePath = path.join(tempDir, '.foliko', 'agents', 'pip-a.md');
222
+ fs.writeFileSync(filePath, `---
223
+ name: pip-a
224
+ description: pipeline A
225
+ tools: read, write
226
+ ---
227
+
228
+ # 标题
229
+
230
+ 执行流程里有一段示例代码:
231
+
232
+ \`\`\`json
233
+ { "step": "example", "data": 123 }
234
+ \`\`\`
235
+ `, 'utf-8');
236
+
237
+ const p = new SubAgentManagerPlugin({});
238
+ p.install({ getCwd: () => tempDir });
239
+ p._loadAgentsFromDir();
240
+
241
+ expect(p.agents).toHaveLength(1);
242
+ expect(p.agents[0].name).toBe('pip-a');
243
+ expect(p.agents[0].description).toBe('pipeline A');
244
+ expect(p.agents[0].tools).toBe('read, write');
245
+ expect(p.agents[0]._fromDir).toBe(true);
246
+ });
247
+ });
@@ -0,0 +1,197 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ const { SubAgentPlugin, SubAgentManagerPlugin } = require('../../plugins/core/sub-agent/index.js');
3
+
4
+ // 测试 SubAgentPlugin 接受 systemPrompt + skills 字段
5
+
6
+ function makeFw(opts = {}) {
7
+ const fakeSkills = opts.skills || new Map();
8
+ return {
9
+ getCwd: () => process.cwd(),
10
+ pluginManager: {
11
+ get: (name) => {
12
+ if (name === 'skill-manager') {
13
+ return {
14
+ getAllSkills: () => Array.from(fakeSkills.values()),
15
+ getSkill: (n) => fakeSkills.get(n),
16
+ hasSkill: (n) => fakeSkills.has(n),
17
+ getSkillDetails: (n) => {
18
+ const s = fakeSkills.get(n);
19
+ return s ? { name: s.name, commands: s._commands } : null;
20
+ },
21
+ };
22
+ }
23
+ if (name === 'extension-executor') {
24
+ return {
25
+ getExtensionTools: (pluginName) => {
26
+ if (pluginName.startsWith('skill:')) {
27
+ const skillName = pluginName.slice(6);
28
+ const s = fakeSkills.get(skillName);
29
+ if (!s) return [];
30
+ return s._commands.map(c => ({ name: c.name, description: c.description }));
31
+ }
32
+ return [];
33
+ },
34
+ };
35
+ }
36
+ return null;
37
+ },
38
+ },
39
+ };
40
+ }
41
+
42
+ describe('SubAgentPlugin 接受 systemPrompt 字段', () => {
43
+ it('★ 直接传 systemPrompt 时,_buildBaseSystemPrompt 优先用', () => {
44
+ const p = new SubAgentPlugin({
45
+ name: 'a',
46
+ systemPrompt: '你是专门做 X 的 agent\n\n具体规则...',
47
+ });
48
+ p.install({ getCwd: () => process.cwd() });
49
+ const prompt = p._buildBaseSystemPrompt();
50
+ expect(prompt).toContain('你是专门做 X 的 agent');
51
+ expect(prompt).toContain('具体规则');
52
+ });
53
+
54
+ it('无 systemPrompt 时回退到 SubAgentConfigManager', () => {
55
+ const p = new SubAgentPlugin({ name: 'no-prompt' });
56
+ p.install({
57
+ getCwd: () => process.cwd(),
58
+ _subAgentConfigManager: {
59
+ get: (name) => name === 'no-prompt' ? {
60
+ getSystemPrompt: () => 'from-config-manager',
61
+ } : null,
62
+ },
63
+ });
64
+ expect(p._buildBaseSystemPrompt()).toBe('from-config-manager');
65
+ });
66
+ });
67
+
68
+ describe('SubAgentPlugin 接受 skills 字段', () => {
69
+ it('★ 数组形式:保持原样', () => {
70
+ const p = new SubAgentPlugin({ name: 'a', skills: ['x', 'y'] });
71
+ expect(p.skills).toEqual(['x', 'y']);
72
+ });
73
+
74
+ it('★ 字符串形式:按逗号分割', () => {
75
+ const p = new SubAgentPlugin({ name: 'a', skills: 'x, y, z' });
76
+ expect(p.skills).toEqual(['x', 'y', 'z']);
77
+ });
78
+
79
+ it('缺省时为空数组', () => {
80
+ const p = new SubAgentPlugin({ name: 'a' });
81
+ expect(p.skills).toEqual([]);
82
+ });
83
+
84
+ it('★ _buildSkillsSection 从 skill-manager 查询命令并注入', () => {
85
+ const skills = new Map([
86
+ ['creator', {
87
+ name: 'creator',
88
+ _commands: [
89
+ { name: 'addCover', description: '加封面' },
90
+ { name: 'addSubtitle', description: '加字幕' },
91
+ ],
92
+ }],
93
+ ]);
94
+ const p = new SubAgentPlugin({ name: 'a', skills: ['creator'] });
95
+ p.install(makeFw({ skills }));
96
+ const section = p._buildSkillsSection();
97
+ expect(section).toContain('## 🛠️ 可用技能');
98
+ expect(section).toContain('### creator');
99
+ expect(section).toContain('**addCover**: 加封面');
100
+ expect(section).toContain('**addSubtitle**: 加字幕');
101
+ expect(section).toContain('ext_call({plugin: "skill:creator"');
102
+ });
103
+
104
+ it('★ _buildSkillsSection:空 skills 时不返回 section', () => {
105
+ const p = new SubAgentPlugin({ name: 'a', skills: ['nonexistent'] });
106
+ p.install(makeFw({ skills: new Map() }));
107
+ // 现在的实现:空 commands 直接跳过(让 _getFullSystemPrompt 决定是否包含)
108
+ const section = p._buildSkillsSection();
109
+ // 没有可用命令时返回空字符串(避免显示 "(该技能尚未注册)" 等无意义内容)
110
+ expect(section).toBe('');
111
+ });
112
+
113
+ it('★ _buildSkillsSection:skills 为空时返回空', () => {
114
+ const p = new SubAgentPlugin({ name: 'a', skills: [] });
115
+ p.install(makeFw());
116
+ expect(p._buildSkillsSection()).toBe('');
117
+ });
118
+
119
+ it('★ _getFullSystemPrompt 叠加 systemPrompt + skills section', () => {
120
+ const skills = new Map([
121
+ ['hot-news', {
122
+ name: 'hot-news',
123
+ _commands: [{ name: 'fetch', description: '抓取' }],
124
+ }],
125
+ ]);
126
+ const p = new SubAgentPlugin({
127
+ name: 'news',
128
+ description: '新闻专家',
129
+ systemPrompt: '你是一个新闻 agent。\n\n## 规则\n做新闻。',
130
+ skills: ['hot-news'],
131
+ });
132
+ p.install(makeFw({ skills }));
133
+ const prompt = p._getFullSystemPrompt();
134
+ expect(prompt).toContain('你是一个新闻 agent');
135
+ expect(prompt).toContain('## 规则');
136
+ expect(prompt).toContain('## 🛠️ 可用技能');
137
+ expect(prompt).toContain('**fetch**: 抓取');
138
+ });
139
+ });
140
+
141
+ describe('SubAgentManagerPlugin 集成', () => {
142
+ it('★ 端到端:news-pipeline-agent.md + skills 自动注入', () => {
143
+ const skills = new Map([
144
+ ['hot-news-video', { name: 'hot-news-video', _commands: [{ name: 'fetch', description: '抓热点' }] }],
145
+ ['baidu-video-publisher', { name: 'baidu-video-publisher', _commands: [{ name: 'publish', description: '发百度' }] }],
146
+ ['creator', { name: 'creator', _commands: [{ name: 'addCover', description: '封面' }] }],
147
+ ]);
148
+ const fs = require('fs');
149
+ const os = require('os');
150
+ const path = require('path');
151
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sub-int-'));
152
+ fs.writeFileSync(path.join(tempDir, 'news-pipeline.md'), `---
153
+ name: news-pipeline
154
+ description: 视频自动化
155
+ tools: Read, Write, Edit, Bash, fetch, ext_call, subagent_call
156
+ skills: hot-news-video, baidu-video-publisher, creator
157
+ model: inherit
158
+ ---
159
+
160
+ # 标题
161
+
162
+ 你是一个**视频自动化** agent。
163
+
164
+ ## 📋 流程
165
+ 1. 抓新闻
166
+ 2. 做视频
167
+ 3. 发布
168
+ `, 'utf-8');
169
+
170
+ const mgr = new SubAgentManagerPlugin({});
171
+ mgr.install({ ...makeFw({ skills }), getCwd: () => tempDir });
172
+ mgr.config.agentsDir = tempDir; // override
173
+ mgr._loadAgentsFromDir();
174
+
175
+ expect(mgr.agents).toHaveLength(1);
176
+ const cfg = mgr.agents[0];
177
+ expect(cfg.name).toBe('news-pipeline');
178
+ expect(cfg.systemPrompt).toContain('视频自动化');
179
+ // _parseMarkdownConfig 返回字符串,SubAgentPlugin 构造时分割
180
+ expect(cfg.skills).toBe('hot-news-video, baidu-video-publisher, creator');
181
+
182
+ // 模拟创建 SubAgentPlugin
183
+ const subPlugin = new SubAgentPlugin(cfg);
184
+ subPlugin.install({ ...makeFw({ skills }), getCwd: () => tempDir });
185
+ const fullPrompt = subPlugin._getFullSystemPrompt();
186
+ expect(fullPrompt).toContain('视频自动化');
187
+ expect(fullPrompt).toContain('## 🛠️ 可用技能');
188
+ expect(fullPrompt).toContain('### hot-news-video');
189
+ expect(fullPrompt).toContain('### baidu-video-publisher');
190
+ expect(fullPrompt).toContain('### creator');
191
+ expect(fullPrompt).toContain('**fetch**: 抓热点');
192
+ expect(fullPrompt).toContain('**publish**: 发百度');
193
+ expect(fullPrompt).toContain('**addCover**: 封面');
194
+
195
+ fs.rmSync(tempDir, { recursive: true, force: true });
196
+ });
197
+ });