@framers/agentos-ext-tool-multi-channel-post 0.1.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.
- package/LICENSE +98 -0
- package/dist/MultiChannelPostTool.d.ts +128 -0
- package/dist/MultiChannelPostTool.d.ts.map +1 -0
- package/dist/MultiChannelPostTool.js +366 -0
- package/dist/MultiChannelPostTool.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/manifest.json +35 -0
- package/package.json +67 -0
- package/src/MultiChannelPostTool.ts +491 -0
- package/src/index.ts +67 -0
- package/test/MultiChannelPostTool.spec.ts +528 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview Unit tests for MultiChannelPostTool.
|
|
4
|
+
*
|
|
5
|
+
* Tests cover: tool metadata, dry-run mode, multi-platform posting,
|
|
6
|
+
* per-platform content overrides, partial failures, content adaptation
|
|
7
|
+
* (character limits, hashtag placement), and missing executor handling.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
11
|
+
import {
|
|
12
|
+
MultiChannelPostTool,
|
|
13
|
+
type ToolExecutorFn,
|
|
14
|
+
type MultiChannelPostInput,
|
|
15
|
+
} from '../src/MultiChannelPostTool.js';
|
|
16
|
+
|
|
17
|
+
/* ------------------------------------------------------------------ */
|
|
18
|
+
/* Helpers */
|
|
19
|
+
/* ------------------------------------------------------------------ */
|
|
20
|
+
|
|
21
|
+
function createMockExecutor(
|
|
22
|
+
overrides?: Partial<Record<string, { success: boolean; data?: Record<string, unknown>; error?: string }>>,
|
|
23
|
+
): ToolExecutorFn {
|
|
24
|
+
return vi.fn(async (toolName: string, _args: Record<string, unknown>) => {
|
|
25
|
+
if (overrides?.[toolName]) return overrides[toolName]!;
|
|
26
|
+
return {
|
|
27
|
+
success: true,
|
|
28
|
+
data: { id: `post-${toolName}-001`, url: `https://${toolName}.example.com/post/001` },
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* ------------------------------------------------------------------ */
|
|
34
|
+
/* Tests */
|
|
35
|
+
/* ------------------------------------------------------------------ */
|
|
36
|
+
|
|
37
|
+
describe('MultiChannelPostTool', () => {
|
|
38
|
+
let tool: MultiChannelPostTool;
|
|
39
|
+
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
tool = new MultiChannelPostTool();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
/* ── Metadata ─────────────────────────────────────────────────── */
|
|
45
|
+
|
|
46
|
+
describe('metadata', () => {
|
|
47
|
+
it('should expose the correct id and name', () => {
|
|
48
|
+
expect(tool.id).toBe('multiChannelPost');
|
|
49
|
+
expect(tool.name).toBe('multiChannelPost');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should have a display name and description', () => {
|
|
53
|
+
expect(tool.displayName).toBe('Post to Multiple Platforms');
|
|
54
|
+
expect(tool.description).toContain('multiple social media platforms');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should declare the category as social', () => {
|
|
58
|
+
expect(tool.category).toBe('social');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should flag hasSideEffects as true', () => {
|
|
62
|
+
expect(tool.hasSideEffects).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should expose a valid inputSchema with required fields', () => {
|
|
66
|
+
expect(tool.inputSchema.type).toBe('object');
|
|
67
|
+
expect(tool.inputSchema.required).toEqual(['content', 'platforms']);
|
|
68
|
+
expect(tool.inputSchema.properties).toHaveProperty('content');
|
|
69
|
+
expect(tool.inputSchema.properties).toHaveProperty('platforms');
|
|
70
|
+
expect(tool.inputSchema.properties).toHaveProperty('adaptations');
|
|
71
|
+
expect(tool.inputSchema.properties).toHaveProperty('mediaUrls');
|
|
72
|
+
expect(tool.inputSchema.properties).toHaveProperty('platformConfigs');
|
|
73
|
+
expect(tool.inputSchema.properties).toHaveProperty('hashtags');
|
|
74
|
+
expect(tool.inputSchema.properties).toHaveProperty('dryRun');
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
/* ── Dry-run mode ─────────────────────────────────────────────── */
|
|
79
|
+
|
|
80
|
+
describe('dry-run mode', () => {
|
|
81
|
+
it('should return adapted content for each platform without posting', async () => {
|
|
82
|
+
const executor = createMockExecutor();
|
|
83
|
+
tool.setToolExecutor(executor);
|
|
84
|
+
|
|
85
|
+
const result = await tool.execute({
|
|
86
|
+
content: 'Hello world!',
|
|
87
|
+
platforms: ['twitter', 'linkedin'],
|
|
88
|
+
dryRun: true,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
expect(result.success).toBe(true);
|
|
92
|
+
expect(result.data!.totalPlatforms).toBe(2);
|
|
93
|
+
expect(result.data!.successful).toBe(2);
|
|
94
|
+
expect(result.data!.failed).toBe(0);
|
|
95
|
+
|
|
96
|
+
// The executor should NOT have been called at all
|
|
97
|
+
expect(executor).not.toHaveBeenCalled();
|
|
98
|
+
|
|
99
|
+
// Each result should contain the adapted content
|
|
100
|
+
for (const r of result.data!.results) {
|
|
101
|
+
expect(r.success).toBe(true);
|
|
102
|
+
expect(r.adaptedContent).toBeDefined();
|
|
103
|
+
expect(r.postId).toBeUndefined();
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should use per-platform overrides in dry-run mode', async () => {
|
|
108
|
+
const result = await tool.execute({
|
|
109
|
+
content: 'Default text',
|
|
110
|
+
platforms: ['twitter', 'linkedin'],
|
|
111
|
+
adaptations: { twitter: 'Custom tweet text' },
|
|
112
|
+
dryRun: true,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const twitterResult = result.data!.results.find((r) => r.platform === 'twitter')!;
|
|
116
|
+
const linkedinResult = result.data!.results.find((r) => r.platform === 'linkedin')!;
|
|
117
|
+
|
|
118
|
+
expect(twitterResult.adaptedContent).toBe('Custom tweet text');
|
|
119
|
+
expect(linkedinResult.adaptedContent).toBe('Default text');
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
/* ── Multi-platform posting ───────────────────────────────────── */
|
|
124
|
+
|
|
125
|
+
describe('posting to multiple platforms', () => {
|
|
126
|
+
it('should post to all specified platforms and aggregate results', async () => {
|
|
127
|
+
const executor = createMockExecutor();
|
|
128
|
+
tool.setToolExecutor(executor);
|
|
129
|
+
|
|
130
|
+
const result = await tool.execute({
|
|
131
|
+
content: 'Check out our new launch!',
|
|
132
|
+
platforms: ['twitter', 'linkedin', 'facebook'],
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
expect(result.success).toBe(true);
|
|
136
|
+
expect(result.data!.totalPlatforms).toBe(3);
|
|
137
|
+
expect(result.data!.successful).toBe(3);
|
|
138
|
+
expect(result.data!.failed).toBe(0);
|
|
139
|
+
expect(result.data!.results).toHaveLength(3);
|
|
140
|
+
|
|
141
|
+
// Verify correct tool names were called
|
|
142
|
+
expect(executor).toHaveBeenCalledTimes(3);
|
|
143
|
+
expect(executor).toHaveBeenCalledWith('twitterPost', expect.any(Object));
|
|
144
|
+
expect(executor).toHaveBeenCalledWith('linkedinPost', expect.any(Object));
|
|
145
|
+
expect(executor).toHaveBeenCalledWith('facebookPost', expect.any(Object));
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should pass media URLs in tool args', async () => {
|
|
149
|
+
const executor = createMockExecutor();
|
|
150
|
+
tool.setToolExecutor(executor);
|
|
151
|
+
|
|
152
|
+
await tool.execute({
|
|
153
|
+
content: 'Photo post',
|
|
154
|
+
platforms: ['twitter'],
|
|
155
|
+
mediaUrls: ['https://example.com/photo.jpg'],
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
expect(executor).toHaveBeenCalledWith('twitterPost', {
|
|
159
|
+
text: expect.any(String),
|
|
160
|
+
mediaPath: 'https://example.com/photo.jpg',
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should extract postId and url from executor results', async () => {
|
|
165
|
+
const executor = createMockExecutor({
|
|
166
|
+
twitterPost: {
|
|
167
|
+
success: true,
|
|
168
|
+
data: { id: 'tw-999', url: 'https://twitter.com/status/999' },
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
tool.setToolExecutor(executor);
|
|
172
|
+
|
|
173
|
+
const result = await tool.execute({
|
|
174
|
+
content: 'Hello',
|
|
175
|
+
platforms: ['twitter'],
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const tResult = result.data!.results[0]!;
|
|
179
|
+
expect(tResult.postId).toBe('tw-999');
|
|
180
|
+
expect(tResult.url).toBe('https://twitter.com/status/999');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should extract postId from data.postId when data.id is absent', async () => {
|
|
184
|
+
const executor = createMockExecutor({
|
|
185
|
+
linkedinPost: {
|
|
186
|
+
success: true,
|
|
187
|
+
data: { postId: 'li-555', url: 'https://linkedin.com/post/555' },
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
tool.setToolExecutor(executor);
|
|
191
|
+
|
|
192
|
+
const result = await tool.execute({
|
|
193
|
+
content: 'LinkedIn update',
|
|
194
|
+
platforms: ['linkedin'],
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
expect(result.data!.results[0]!.postId).toBe('li-555');
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
/* ── Per-platform content overrides ───────────────────────────── */
|
|
202
|
+
|
|
203
|
+
describe('per-platform content adaptations', () => {
|
|
204
|
+
it('should use explicit per-platform overrides when provided', async () => {
|
|
205
|
+
const executor = createMockExecutor();
|
|
206
|
+
tool.setToolExecutor(executor);
|
|
207
|
+
|
|
208
|
+
await tool.execute({
|
|
209
|
+
content: 'Default content',
|
|
210
|
+
platforms: ['twitter', 'instagram'],
|
|
211
|
+
mediaUrls: ['https://cdn.example.com/adapt-image.jpg'],
|
|
212
|
+
adaptations: {
|
|
213
|
+
twitter: 'A short tweet',
|
|
214
|
+
instagram: 'A longer Instagram caption with details',
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
expect(executor).toHaveBeenCalledWith('twitterPost', expect.objectContaining({ text: 'A short tweet' }));
|
|
219
|
+
expect(executor).toHaveBeenCalledWith('instagramPost', expect.objectContaining({ caption: 'A longer Instagram caption with details' }));
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
/* ── Content adaptation (auto) ────────────────────────────────── */
|
|
224
|
+
|
|
225
|
+
describe('automatic content adaptation', () => {
|
|
226
|
+
it('should truncate content that exceeds platform character limit', async () => {
|
|
227
|
+
const longContent = 'A'.repeat(300); // exceeds Twitter's 280 limit
|
|
228
|
+
const result = await tool.execute({
|
|
229
|
+
content: longContent,
|
|
230
|
+
platforms: ['twitter'],
|
|
231
|
+
dryRun: true,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const adapted = result.data!.results[0]!.adaptedContent!;
|
|
235
|
+
expect(adapted.length).toBeLessThanOrEqual(280);
|
|
236
|
+
expect(adapted.endsWith('...')).toBe(true);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should not truncate content within platform limits', async () => {
|
|
240
|
+
const shortContent = 'Short post';
|
|
241
|
+
const result = await tool.execute({
|
|
242
|
+
content: shortContent,
|
|
243
|
+
platforms: ['twitter'],
|
|
244
|
+
dryRun: true,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
expect(result.data!.results[0]!.adaptedContent).toBe('Short post');
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('should append hashtags inline for Twitter', async () => {
|
|
251
|
+
const result = await tool.execute({
|
|
252
|
+
content: 'Great day',
|
|
253
|
+
platforms: ['twitter'],
|
|
254
|
+
hashtags: ['awesome', '#blessed'],
|
|
255
|
+
dryRun: true,
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
const adapted = result.data!.results[0]!.adaptedContent!;
|
|
259
|
+
expect(adapted).toBe('Great day #awesome #blessed');
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('should append hashtags as footer for Instagram', async () => {
|
|
263
|
+
const result = await tool.execute({
|
|
264
|
+
content: 'Check this out',
|
|
265
|
+
platforms: ['instagram'],
|
|
266
|
+
hashtags: ['photography', 'nature'],
|
|
267
|
+
dryRun: true,
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
const adapted = result.data!.results[0]!.adaptedContent!;
|
|
271
|
+
expect(adapted).toBe('Check this out\n\n#photography #nature');
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('should not append hashtags for platforms with none style', async () => {
|
|
275
|
+
const result = await tool.execute({
|
|
276
|
+
content: 'Reddit post',
|
|
277
|
+
platforms: ['reddit'],
|
|
278
|
+
hashtags: ['test'],
|
|
279
|
+
dryRun: true,
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
const adapted = result.data!.results[0]!.adaptedContent!;
|
|
283
|
+
expect(adapted).toBe('Reddit post');
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
/* ── Partial failures ─────────────────────────────────────────── */
|
|
288
|
+
|
|
289
|
+
describe('partial failures', () => {
|
|
290
|
+
it('should handle a mix of successes and failures', async () => {
|
|
291
|
+
const executor = createMockExecutor({
|
|
292
|
+
twitterPost: { success: true, data: { id: 'tw-1' } },
|
|
293
|
+
linkedinPost: { success: false, error: 'LinkedIn API is down' },
|
|
294
|
+
facebookPost: { success: true, data: { id: 'fb-1' } },
|
|
295
|
+
});
|
|
296
|
+
tool.setToolExecutor(executor);
|
|
297
|
+
|
|
298
|
+
const result = await tool.execute({
|
|
299
|
+
content: 'Hello',
|
|
300
|
+
platforms: ['twitter', 'linkedin', 'facebook'],
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
expect(result.success).toBe(true); // at least one succeeded
|
|
304
|
+
expect(result.data!.successful).toBe(2);
|
|
305
|
+
expect(result.data!.failed).toBe(1);
|
|
306
|
+
|
|
307
|
+
const linkedinResult = result.data!.results.find((r) => r.platform === 'linkedin')!;
|
|
308
|
+
expect(linkedinResult.success).toBe(false);
|
|
309
|
+
expect(linkedinResult.error).toBe('LinkedIn API is down');
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('should report success=false when ALL platforms fail', async () => {
|
|
313
|
+
const executor = createMockExecutor({
|
|
314
|
+
twitterPost: { success: false, error: 'Fail 1' },
|
|
315
|
+
linkedinPost: { success: false, error: 'Fail 2' },
|
|
316
|
+
});
|
|
317
|
+
tool.setToolExecutor(executor);
|
|
318
|
+
|
|
319
|
+
const result = await tool.execute({
|
|
320
|
+
content: 'Hello',
|
|
321
|
+
platforms: ['twitter', 'linkedin'],
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
expect(result.success).toBe(false);
|
|
325
|
+
expect(result.data!.successful).toBe(0);
|
|
326
|
+
expect(result.data!.failed).toBe(2);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('should catch thrown errors from executor and report them gracefully', async () => {
|
|
330
|
+
const executor = vi.fn(async () => {
|
|
331
|
+
throw new Error('Network timeout');
|
|
332
|
+
});
|
|
333
|
+
tool.setToolExecutor(executor);
|
|
334
|
+
|
|
335
|
+
const result = await tool.execute({
|
|
336
|
+
content: 'Hello',
|
|
337
|
+
platforms: ['twitter'],
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
expect(result.success).toBe(false);
|
|
341
|
+
expect(result.data!.results[0]!.error).toBe('Network timeout');
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('should catch non-Error thrown values', async () => {
|
|
345
|
+
const executor = vi.fn(async () => {
|
|
346
|
+
throw 'string error'; // eslint-disable-line no-throw-literal
|
|
347
|
+
});
|
|
348
|
+
tool.setToolExecutor(executor);
|
|
349
|
+
|
|
350
|
+
const result = await tool.execute({
|
|
351
|
+
content: 'Hello',
|
|
352
|
+
platforms: ['twitter'],
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
expect(result.data!.results[0]!.error).toBe('string error');
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
/* ── Missing executor ─────────────────────────────────────────── */
|
|
360
|
+
|
|
361
|
+
describe('missing tool executor', () => {
|
|
362
|
+
it('should report per-platform errors when no executor is set', async () => {
|
|
363
|
+
const result = await tool.execute({
|
|
364
|
+
content: 'No executor available',
|
|
365
|
+
platforms: ['twitter', 'linkedin'],
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
expect(result.success).toBe(false);
|
|
369
|
+
expect(result.data!.failed).toBe(2);
|
|
370
|
+
for (const r of result.data!.results) {
|
|
371
|
+
expect(r.success).toBe(false);
|
|
372
|
+
expect(r.error).toContain('No tool executor available');
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
/* ── Tool name mapping ────────────────────────────────────────── */
|
|
378
|
+
|
|
379
|
+
describe('tool name mapping', () => {
|
|
380
|
+
it('should map known platforms to their expected tool names', async () => {
|
|
381
|
+
const executor = createMockExecutor();
|
|
382
|
+
tool.setToolExecutor(executor);
|
|
383
|
+
|
|
384
|
+
const platforms = ['twitter', 'instagram', 'reddit', 'youtube', 'bluesky', 'mastodon', 'threads'];
|
|
385
|
+
const expectedToolNames = [
|
|
386
|
+
'twitterPost', 'instagramPost', 'redditSubmitPost', 'youtubeUpload',
|
|
387
|
+
'blueskyPost', 'mastodonPost', 'threadsPost',
|
|
388
|
+
];
|
|
389
|
+
|
|
390
|
+
await tool.execute({
|
|
391
|
+
content: 'Test',
|
|
392
|
+
platforms,
|
|
393
|
+
mediaUrls: ['https://cdn.example.com/tool-map-media.mp4'],
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
for (const expected of expectedToolNames) {
|
|
397
|
+
expect(executor).toHaveBeenCalledWith(expected, expect.any(Object));
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it('should fallback to "<platform>Post" for unknown platforms', async () => {
|
|
402
|
+
const executor = createMockExecutor();
|
|
403
|
+
tool.setToolExecutor(executor);
|
|
404
|
+
|
|
405
|
+
await tool.execute({
|
|
406
|
+
content: 'Test',
|
|
407
|
+
platforms: ['myplatform'],
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
expect(executor).toHaveBeenCalledWith('myplatformPost', expect.any(Object));
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
/* ── Platform-specific arg building ───────────────────────────── */
|
|
415
|
+
|
|
416
|
+
describe('platform-specific tool arguments', () => {
|
|
417
|
+
it('should build twitter args with text field', async () => {
|
|
418
|
+
const executor = createMockExecutor();
|
|
419
|
+
tool.setToolExecutor(executor);
|
|
420
|
+
await tool.execute({ content: 'Tweet!', platforms: ['twitter'] });
|
|
421
|
+
expect(executor).toHaveBeenCalledWith('twitterPost', expect.objectContaining({ text: 'Tweet!' }));
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it('should build instagram args with caption + imageUrls', async () => {
|
|
425
|
+
const executor = createMockExecutor();
|
|
426
|
+
tool.setToolExecutor(executor);
|
|
427
|
+
await tool.execute({
|
|
428
|
+
content: 'IG post',
|
|
429
|
+
platforms: ['instagram'],
|
|
430
|
+
mediaUrls: ['https://cdn.example.com/photo.jpg'],
|
|
431
|
+
});
|
|
432
|
+
expect(executor).toHaveBeenCalledWith(
|
|
433
|
+
'instagramPost',
|
|
434
|
+
expect.objectContaining({
|
|
435
|
+
caption: 'IG post',
|
|
436
|
+
imageUrls: ['https://cdn.example.com/photo.jpg'],
|
|
437
|
+
}),
|
|
438
|
+
);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it('should build reddit args with required content/type fields', async () => {
|
|
442
|
+
const executor = createMockExecutor();
|
|
443
|
+
tool.setToolExecutor(executor);
|
|
444
|
+
await tool.execute({ content: 'Reddit thread', platforms: ['reddit'] });
|
|
445
|
+
expect(executor).toHaveBeenCalledWith(
|
|
446
|
+
'redditSubmitPost',
|
|
447
|
+
expect.objectContaining({
|
|
448
|
+
subreddit: 'self',
|
|
449
|
+
title: expect.any(String),
|
|
450
|
+
content: 'Reddit thread',
|
|
451
|
+
type: 'text',
|
|
452
|
+
}),
|
|
453
|
+
);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it('should build youtube args with required videoUrl', async () => {
|
|
457
|
+
const executor = createMockExecutor();
|
|
458
|
+
tool.setToolExecutor(executor);
|
|
459
|
+
await tool.execute({
|
|
460
|
+
content: 'Video description',
|
|
461
|
+
platforms: ['youtube'],
|
|
462
|
+
mediaUrls: ['https://cdn.example.com/video.mp4'],
|
|
463
|
+
});
|
|
464
|
+
expect(executor).toHaveBeenCalledWith(
|
|
465
|
+
'youtubeUpload',
|
|
466
|
+
expect.objectContaining({
|
|
467
|
+
videoUrl: 'https://cdn.example.com/video.mp4',
|
|
468
|
+
title: expect.any(String),
|
|
469
|
+
description: 'Video description',
|
|
470
|
+
}),
|
|
471
|
+
);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it('should fail youtube post when no video URL is provided', async () => {
|
|
475
|
+
const executor = createMockExecutor();
|
|
476
|
+
tool.setToolExecutor(executor);
|
|
477
|
+
|
|
478
|
+
const result = await tool.execute({
|
|
479
|
+
content: 'Video description',
|
|
480
|
+
platforms: ['youtube'],
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
expect(result.success).toBe(false);
|
|
484
|
+
expect(result.data!.results[0]!.error).toContain('youtube requires videoUrl');
|
|
485
|
+
expect(executor).not.toHaveBeenCalled();
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it('should allow platformConfigs overrides for pinterest', async () => {
|
|
489
|
+
const executor = createMockExecutor();
|
|
490
|
+
tool.setToolExecutor(executor);
|
|
491
|
+
|
|
492
|
+
await tool.execute({
|
|
493
|
+
content: 'Pin this',
|
|
494
|
+
platforms: ['pinterest'],
|
|
495
|
+
mediaUrls: ['https://cdn.example.com/image.jpg'],
|
|
496
|
+
platformConfigs: {
|
|
497
|
+
pinterest: {
|
|
498
|
+
boardId: 'board-123',
|
|
499
|
+
mediaType: 'image',
|
|
500
|
+
},
|
|
501
|
+
},
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
expect(executor).toHaveBeenCalledWith(
|
|
505
|
+
'pinterestPin',
|
|
506
|
+
expect.objectContaining({
|
|
507
|
+
boardId: 'board-123',
|
|
508
|
+
mediaType: 'image',
|
|
509
|
+
mediaUrl: 'https://cdn.example.com/image.jpg',
|
|
510
|
+
}),
|
|
511
|
+
);
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
it('should fail lemmy post when communityId is missing', async () => {
|
|
515
|
+
const executor = createMockExecutor();
|
|
516
|
+
tool.setToolExecutor(executor);
|
|
517
|
+
|
|
518
|
+
const result = await tool.execute({
|
|
519
|
+
content: 'Lemmy post',
|
|
520
|
+
platforms: ['lemmy'],
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
expect(result.success).toBe(false);
|
|
524
|
+
expect(result.data!.results[0]!.error).toContain('lemmy requires communityId');
|
|
525
|
+
expect(executor).not.toHaveBeenCalled();
|
|
526
|
+
});
|
|
527
|
+
});
|
|
528
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"lib": ["ES2022"],
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"noEmitOnError": false,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"declarationMap": true,
|
|
15
|
+
"sourceMap": true,
|
|
16
|
+
"resolveJsonModule": true,
|
|
17
|
+
"moduleResolution": "Bundler",
|
|
18
|
+
"types": ["node"]
|
|
19
|
+
},
|
|
20
|
+
"include": ["src/**/*"],
|
|
21
|
+
"exclude": ["node_modules", "dist", "test"]
|
|
22
|
+
}
|