@framers/agentos-ext-tool-social-analytics 0.1.0 → 0.1.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/manifest.json CHANGED
@@ -2,12 +2,12 @@
2
2
  "$schema": "https://agentos.sh/schemas/extension-manifest-v1.json",
3
3
  "id": "com.framers.tools.social-analytics",
4
4
  "name": "Cross-Platform Social Analytics Tool",
5
- "version": "0.1.0",
5
+ "version": "0.1.1",
6
6
  "description": "Aggregate engagement metrics (likes, comments, shares, impressions, engagement rate) across multiple social platforms in a single query.",
7
7
  "author": {
8
8
  "name": "Frame.dev",
9
9
  "email": "support@frame.dev",
10
- "url": "https://github.com/framersai"
10
+ "url": "https://github.com/framerslab"
11
11
  },
12
12
  "license": "MIT",
13
13
  "keywords": ["social", "analytics", "engagement", "cross-platform", "metrics"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@framers/agentos-ext-tool-social-analytics",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Cross-platform social analytics tool — aggregate engagement metrics from multiple platforms in a single query.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -37,30 +37,37 @@
37
37
  "license": "MIT",
38
38
  "repository": {
39
39
  "type": "git",
40
- "url": "https://github.com/framersai/agentos-extensions.git",
40
+ "url": "git+https://github.com/framerslab/agentos-extensions.git",
41
41
  "directory": "registry/curated/tools/social-analytics"
42
42
  },
43
43
  "publishConfig": {
44
44
  "access": "public"
45
45
  },
46
46
  "peerDependencies": {
47
- "@framers/agentos": "^0.5.2"
47
+ "@framers/agentos": ">=0.10.40"
48
48
  },
49
49
  "devDependencies": {
50
- "@framers/agentos": "^0.6.6",
51
50
  "@types/node": "^20.12.12",
52
- "@vitest/coverage-v8": "^1.6.0",
51
+ "@vitest/coverage-v8": "^3.2.7",
53
52
  "rimraf": "^5.0.7",
54
53
  "typescript": "^5.4.5",
55
- "vitest": "^1.6.0"
54
+ "vitest": "^3.2.7",
55
+ "@framers/agentos": "^0.10.40"
56
56
  },
57
+ "homepage": "https://github.com/framerslab/agentos-extensions/tree/master/registry/curated/tools/social-analytics#readme",
58
+ "bugs": {
59
+ "url": "https://github.com/framerslab/agentos-extensions/issues"
60
+ },
61
+ "files": [
62
+ "dist",
63
+ "manifest.json"
64
+ ],
57
65
  "scripts": {
58
66
  "build": "tsc",
59
67
  "dev": "tsc --watch",
60
- "test": "vitest run",
68
+ "test": "vitest run --passWithNoTests",
61
69
  "test:watch": "vitest",
62
70
  "test:coverage": "vitest run --coverage",
63
- "lint": "eslint src --ext .ts",
64
71
  "typecheck": "tsc --noEmit",
65
72
  "clean": "rimraf dist"
66
73
  }
@@ -1,193 +0,0 @@
1
- // @ts-nocheck
2
- /**
3
- * @fileoverview Cross-Platform Analytics Tool — aggregate engagement metrics from N platforms.
4
- *
5
- * This tool acts as an orchestrator that delegates to per-platform analytics tools
6
- * (twitterAnalytics, instagramAnalytics, etc.) and aggregates the results into a
7
- * unified view with per-platform breakdowns and overall totals.
8
- */
9
-
10
- /* ------------------------------------------------------------------ */
11
- /* Types */
12
- /* ------------------------------------------------------------------ */
13
-
14
- export interface CrossPlatformAnalyticsInput {
15
- /** Platforms to query analytics from. */
16
- platforms: string[];
17
- /** Platform-to-postId mapping (e.g., { twitter: "123", linkedin: "456" }). */
18
- postIds?: Record<string, string>;
19
- /** Time range for analytics (default: '7d'). */
20
- timeRange?: '24h' | '7d' | '30d' | '90d';
21
- }
22
-
23
- export interface PlatformMetrics {
24
- likes?: number;
25
- comments?: number;
26
- shares?: number;
27
- impressions?: number;
28
- engagementRate?: number;
29
- /** Platform-specific raw data fields. */
30
- [key: string]: unknown;
31
- }
32
-
33
- export interface PlatformAnalyticsResult {
34
- /** Metrics when successful, or an error descriptor on failure. */
35
- [key: string]: PlatformMetrics | { error: string };
36
- }
37
-
38
- export interface AnalyticsTotals {
39
- likes: number;
40
- comments: number;
41
- shares: number;
42
- impressions: number;
43
- /** Overall engagement rate as a percentage (0-100). */
44
- engagement: number;
45
- }
46
-
47
- export interface CrossPlatformAnalyticsOutput {
48
- platforms: PlatformAnalyticsResult;
49
- totals: AnalyticsTotals;
50
- timeRange: string;
51
- }
52
-
53
- /* ------------------------------------------------------------------ */
54
- /* Tool executor callback type */
55
- /* ------------------------------------------------------------------ */
56
-
57
- export type ToolExecutorFn = (
58
- toolName: string,
59
- args: Record<string, unknown>,
60
- ) => Promise<{ success: boolean; data?: Record<string, unknown>; error?: string }>;
61
-
62
- /* ------------------------------------------------------------------ */
63
- /* CrossPlatformAnalyticsTool */
64
- /* ------------------------------------------------------------------ */
65
-
66
- export class CrossPlatformAnalyticsTool {
67
- readonly id = 'crossPlatformAnalytics';
68
- readonly name = 'crossPlatformAnalytics';
69
- readonly displayName = 'Cross-Platform Analytics';
70
- readonly description =
71
- 'Aggregate engagement metrics across social platforms. Returns likes, comments, ' +
72
- 'shares, impressions, and engagement rate per platform and totals.';
73
- readonly category = 'social';
74
- readonly version = '0.1.0';
75
- readonly hasSideEffects = false;
76
-
77
- readonly inputSchema = {
78
- type: 'object' as const,
79
- properties: {
80
- platforms: {
81
- type: 'array',
82
- items: { type: 'string' },
83
- description: 'Platforms to query analytics from',
84
- },
85
- postIds: {
86
- type: 'object',
87
- additionalProperties: { type: 'string' },
88
- description: 'Platform-to-postId mapping (e.g., { twitter: "123", linkedin: "456" })',
89
- },
90
- timeRange: {
91
- type: 'string',
92
- enum: ['24h', '7d', '30d', '90d'],
93
- description: 'Time range for analytics (default: 7d)',
94
- },
95
- },
96
- required: ['platforms'],
97
- };
98
-
99
- /* -------------------------------------------------------------- */
100
- /* Tool executor — set by the orchestrator that loads this ext */
101
- /* -------------------------------------------------------------- */
102
-
103
- private toolExecutor?: ToolExecutorFn;
104
-
105
- setToolExecutor(executor: ToolExecutorFn): void {
106
- this.toolExecutor = executor;
107
- }
108
-
109
- /* -------------------------------------------------------------- */
110
- /* execute() */
111
- /* -------------------------------------------------------------- */
112
-
113
- async execute(
114
- args: CrossPlatformAnalyticsInput,
115
- ): Promise<{ success: boolean; data?: CrossPlatformAnalyticsOutput; error?: string }> {
116
- const platformResults: PlatformAnalyticsResult = {};
117
- const totals: AnalyticsTotals = {
118
- likes: 0,
119
- comments: 0,
120
- shares: 0,
121
- impressions: 0,
122
- engagement: 0,
123
- };
124
-
125
- for (const platform of args.platforms) {
126
- const analyticsToolName = `${platform}Analytics`;
127
- const postId = args.postIds?.[platform];
128
-
129
- try {
130
- if (this.toolExecutor && postId) {
131
- const result = await this.toolExecutor(analyticsToolName, {
132
- postId,
133
- timeRange: args.timeRange ?? '7d',
134
- });
135
-
136
- if (result.success && result.data) {
137
- const data = result.data;
138
- platformResults[platform] = data as PlatformMetrics;
139
-
140
- // Accumulate totals — normalize platform-specific field names.
141
- totals.likes += this.toNumber(data.likes ?? data.favouritesCount);
142
- totals.comments += this.toNumber(data.comments ?? data.repliesCount);
143
- totals.shares += this.toNumber(
144
- data.shares ?? data.retweets ?? data.reblogsCount ?? data.reposts,
145
- );
146
- totals.impressions += this.toNumber(data.impressions ?? data.views);
147
- } else {
148
- platformResults[platform] = { error: result.error ?? 'No data returned' };
149
- }
150
- } else {
151
- platformResults[platform] = {
152
- error: postId
153
- ? 'No tool executor available'
154
- : 'No post ID provided for this platform',
155
- };
156
- }
157
- } catch (err: unknown) {
158
- const message = err instanceof Error ? err.message : String(err);
159
- platformResults[platform] = { error: message };
160
- }
161
- }
162
-
163
- // Calculate overall engagement rate.
164
- if (totals.impressions > 0) {
165
- totals.engagement = Number(
166
- (((totals.likes + totals.comments + totals.shares) / totals.impressions) * 100).toFixed(2),
167
- );
168
- }
169
-
170
- return {
171
- success: true,
172
- data: {
173
- platforms: platformResults,
174
- totals,
175
- timeRange: args.timeRange ?? '7d',
176
- },
177
- };
178
- }
179
-
180
- /* -------------------------------------------------------------- */
181
- /* Helpers */
182
- /* -------------------------------------------------------------- */
183
-
184
- /** Safely coerce an unknown value to a number, defaulting to 0. */
185
- private toNumber(value: unknown): number {
186
- if (typeof value === 'number' && !Number.isNaN(value)) return value;
187
- if (typeof value === 'string') {
188
- const parsed = Number(value);
189
- return Number.isNaN(parsed) ? 0 : parsed;
190
- }
191
- return 0;
192
- }
193
- }
package/src/index.ts DELETED
@@ -1,70 +0,0 @@
1
- // @ts-nocheck
2
- /**
3
- * Cross-Platform Social Analytics Extension Pack — aggregate engagement metrics
4
- * from multiple social platforms in a single query.
5
- */
6
-
7
- import { CrossPlatformAnalyticsTool } from './CrossPlatformAnalyticsTool.js';
8
-
9
- /* ------------------------------------------------------------------ */
10
- /* Extension pack types */
11
- /* ------------------------------------------------------------------ */
12
-
13
- export interface ExtensionContext {
14
- options?: Record<string, unknown>;
15
- secrets?: Record<string, string>;
16
- getSecret?: (key: string) => string | undefined;
17
- logger?: { info: (msg: string) => void };
18
- }
19
-
20
- export interface ExtensionPack {
21
- name: string;
22
- version: string;
23
- descriptors: Array<{
24
- id: string;
25
- kind: string;
26
- priority?: number;
27
- payload: unknown;
28
- }>;
29
- onActivate?: () => Promise<void>;
30
- onDeactivate?: () => Promise<void>;
31
- }
32
-
33
- /* ------------------------------------------------------------------ */
34
- /* Factory */
35
- /* ------------------------------------------------------------------ */
36
-
37
- export function createExtensionPack(context: ExtensionContext): ExtensionPack {
38
- const tool = new CrossPlatformAnalyticsTool();
39
-
40
- return {
41
- name: '@framers/agentos-ext-tool-social-analytics',
42
- version: '0.1.0',
43
- descriptors: [
44
- {
45
- id: tool.name,
46
- kind: 'tool' as const,
47
- priority: 50,
48
- payload: tool,
49
- },
50
- ],
51
- onActivate: async () => context.logger?.info('Cross-Platform Analytics Extension activated'),
52
- onDeactivate: async () => context.logger?.info('Cross-Platform Analytics Extension deactivated'),
53
- };
54
- }
55
-
56
- /* ------------------------------------------------------------------ */
57
- /* Re-exports */
58
- /* ------------------------------------------------------------------ */
59
-
60
- export { CrossPlatformAnalyticsTool } from './CrossPlatformAnalyticsTool.js';
61
- export type {
62
- CrossPlatformAnalyticsInput,
63
- CrossPlatformAnalyticsOutput,
64
- PlatformMetrics,
65
- PlatformAnalyticsResult,
66
- AnalyticsTotals,
67
- ToolExecutorFn,
68
- } from './CrossPlatformAnalyticsTool.js';
69
-
70
- export default createExtensionPack;
@@ -1,455 +0,0 @@
1
- // @ts-nocheck
2
- /**
3
- * @fileoverview Unit tests for CrossPlatformAnalyticsTool.
4
- *
5
- * Tests cover: tool metadata, multi-platform metric aggregation,
6
- * field name normalization (likes/favouritesCount, shares/retweets/reblogsCount),
7
- * engagement rate calculation, missing post IDs, executor errors,
8
- * and missing executor handling.
9
- */
10
-
11
- import { describe, it, expect, vi, beforeEach } from 'vitest';
12
- import {
13
- CrossPlatformAnalyticsTool,
14
- type ToolExecutorFn,
15
- type CrossPlatformAnalyticsInput,
16
- } from '../src/CrossPlatformAnalyticsTool.js';
17
-
18
- /* ------------------------------------------------------------------ */
19
- /* Helpers */
20
- /* ------------------------------------------------------------------ */
21
-
22
- function createMockExecutor(
23
- platformData: Record<string, Record<string, unknown>>,
24
- ): ToolExecutorFn {
25
- return vi.fn(async (toolName: string, _args: Record<string, unknown>) => {
26
- // Extract platform name from tool name (e.g. "twitterAnalytics" -> "twitter")
27
- const platform = toolName.replace('Analytics', '');
28
- if (platformData[platform]) {
29
- return { success: true, data: platformData[platform] };
30
- }
31
- return { success: false, error: `No data configured for ${toolName}` };
32
- });
33
- }
34
-
35
- /* ------------------------------------------------------------------ */
36
- /* Tests */
37
- /* ------------------------------------------------------------------ */
38
-
39
- describe('CrossPlatformAnalyticsTool', () => {
40
- let tool: CrossPlatformAnalyticsTool;
41
-
42
- beforeEach(() => {
43
- tool = new CrossPlatformAnalyticsTool();
44
- });
45
-
46
- /* ── Metadata ─────────────────────────────────────────────────── */
47
-
48
- describe('metadata', () => {
49
- it('should expose the correct id and name', () => {
50
- expect(tool.id).toBe('crossPlatformAnalytics');
51
- expect(tool.name).toBe('crossPlatformAnalytics');
52
- });
53
-
54
- it('should declare hasSideEffects as false', () => {
55
- expect(tool.hasSideEffects).toBe(false);
56
- });
57
-
58
- it('should require platforms in the input schema', () => {
59
- expect(tool.inputSchema.required).toEqual(['platforms']);
60
- });
61
-
62
- it('should define timeRange as an enum with 4 options', () => {
63
- const timeRange = tool.inputSchema.properties.timeRange as { enum: string[] };
64
- expect(timeRange.enum).toEqual(['24h', '7d', '30d', '90d']);
65
- });
66
- });
67
-
68
- /* ── Aggregation across platforms ─────────────────────────────── */
69
-
70
- describe('metric aggregation', () => {
71
- it('should aggregate likes, comments, shares, and impressions from multiple platforms', async () => {
72
- const executor = createMockExecutor({
73
- twitter: { likes: 100, comments: 20, shares: 50, impressions: 5000 },
74
- linkedin: { likes: 200, comments: 30, shares: 80, impressions: 10000 },
75
- });
76
- tool.setToolExecutor(executor);
77
-
78
- const result = await tool.execute({
79
- platforms: ['twitter', 'linkedin'],
80
- postIds: { twitter: 'tw-1', linkedin: 'li-1' },
81
- });
82
-
83
- expect(result.success).toBe(true);
84
- const totals = result.data!.totals;
85
- expect(totals.likes).toBe(300);
86
- expect(totals.comments).toBe(50);
87
- expect(totals.shares).toBe(130);
88
- expect(totals.impressions).toBe(15000);
89
- });
90
-
91
- it('should pass timeRange to the per-platform executor', async () => {
92
- const executor = createMockExecutor({
93
- twitter: { likes: 10, comments: 2, shares: 5, impressions: 500 },
94
- });
95
- tool.setToolExecutor(executor);
96
-
97
- await tool.execute({
98
- platforms: ['twitter'],
99
- postIds: { twitter: 'tw-1' },
100
- timeRange: '30d',
101
- });
102
-
103
- expect(executor).toHaveBeenCalledWith('twitterAnalytics', {
104
- postId: 'tw-1',
105
- timeRange: '30d',
106
- });
107
- });
108
-
109
- it('should default timeRange to 7d', async () => {
110
- const executor = createMockExecutor({
111
- twitter: { likes: 10, comments: 2, shares: 5, impressions: 500 },
112
- });
113
- tool.setToolExecutor(executor);
114
-
115
- const result = await tool.execute({
116
- platforms: ['twitter'],
117
- postIds: { twitter: 'tw-1' },
118
- });
119
-
120
- expect(executor).toHaveBeenCalledWith('twitterAnalytics', {
121
- postId: 'tw-1',
122
- timeRange: '7d',
123
- });
124
-
125
- expect(result.data!.timeRange).toBe('7d');
126
- });
127
-
128
- it('should return the timeRange in the output', async () => {
129
- const executor = createMockExecutor({
130
- twitter: { likes: 0, comments: 0, shares: 0, impressions: 0 },
131
- });
132
- tool.setToolExecutor(executor);
133
-
134
- const result = await tool.execute({
135
- platforms: ['twitter'],
136
- postIds: { twitter: 'tw-1' },
137
- timeRange: '90d',
138
- });
139
-
140
- expect(result.data!.timeRange).toBe('90d');
141
- });
142
- });
143
-
144
- /* ── Field name normalization ─────────────────────────────────── */
145
-
146
- describe('field name normalization', () => {
147
- it('should normalize favouritesCount to likes', async () => {
148
- const executor = createMockExecutor({
149
- mastodon: { favouritesCount: 42, repliesCount: 5, reblogsCount: 12, impressions: 800 },
150
- });
151
- tool.setToolExecutor(executor);
152
-
153
- const result = await tool.execute({
154
- platforms: ['mastodon'],
155
- postIds: { mastodon: 'masto-1' },
156
- });
157
-
158
- expect(result.data!.totals.likes).toBe(42);
159
- expect(result.data!.totals.comments).toBe(5);
160
- expect(result.data!.totals.shares).toBe(12);
161
- });
162
-
163
- it('should normalize retweets to shares (Twitter)', async () => {
164
- const executor = createMockExecutor({
165
- twitter: { likes: 100, comments: 10, retweets: 30, impressions: 2000 },
166
- });
167
- tool.setToolExecutor(executor);
168
-
169
- const result = await tool.execute({
170
- platforms: ['twitter'],
171
- postIds: { twitter: 'tw-2' },
172
- });
173
-
174
- expect(result.data!.totals.shares).toBe(30);
175
- });
176
-
177
- it('should normalize reposts to shares (Bluesky)', async () => {
178
- const executor = createMockExecutor({
179
- bluesky: { likes: 50, comments: 3, reposts: 15, views: 1500 },
180
- });
181
- tool.setToolExecutor(executor);
182
-
183
- const result = await tool.execute({
184
- platforms: ['bluesky'],
185
- postIds: { bluesky: 'bsky-1' },
186
- });
187
-
188
- expect(result.data!.totals.shares).toBe(15);
189
- });
190
-
191
- it('should normalize views to impressions', async () => {
192
- const executor = createMockExecutor({
193
- bluesky: { likes: 50, comments: 3, reposts: 15, views: 1500 },
194
- });
195
- tool.setToolExecutor(executor);
196
-
197
- const result = await tool.execute({
198
- platforms: ['bluesky'],
199
- postIds: { bluesky: 'bsky-1' },
200
- });
201
-
202
- expect(result.data!.totals.impressions).toBe(1500);
203
- });
204
-
205
- it('should handle mixed normalized and standard field names across platforms', async () => {
206
- const executor = createMockExecutor({
207
- twitter: { likes: 100, comments: 10, retweets: 30, impressions: 2000 },
208
- mastodon: { favouritesCount: 50, repliesCount: 5, reblogsCount: 20, impressions: 1000 },
209
- });
210
- tool.setToolExecutor(executor);
211
-
212
- const result = await tool.execute({
213
- platforms: ['twitter', 'mastodon'],
214
- postIds: { twitter: 'tw-1', mastodon: 'masto-1' },
215
- });
216
-
217
- expect(result.data!.totals.likes).toBe(150);
218
- expect(result.data!.totals.comments).toBe(15);
219
- expect(result.data!.totals.shares).toBe(50);
220
- expect(result.data!.totals.impressions).toBe(3000);
221
- });
222
- });
223
-
224
- /* ── Engagement rate calculation ──────────────────────────────── */
225
-
226
- describe('engagement rate', () => {
227
- it('should calculate engagement rate as (likes+comments+shares)/impressions * 100', async () => {
228
- const executor = createMockExecutor({
229
- twitter: { likes: 100, comments: 20, shares: 30, impressions: 5000 },
230
- });
231
- tool.setToolExecutor(executor);
232
-
233
- const result = await tool.execute({
234
- platforms: ['twitter'],
235
- postIds: { twitter: 'tw-1' },
236
- });
237
-
238
- // (100 + 20 + 30) / 5000 * 100 = 3.00
239
- expect(result.data!.totals.engagement).toBe(3);
240
- });
241
-
242
- it('should return 0 engagement when impressions are 0', async () => {
243
- const executor = createMockExecutor({
244
- twitter: { likes: 5, comments: 1, shares: 0, impressions: 0 },
245
- });
246
- tool.setToolExecutor(executor);
247
-
248
- const result = await tool.execute({
249
- platforms: ['twitter'],
250
- postIds: { twitter: 'tw-1' },
251
- });
252
-
253
- expect(result.data!.totals.engagement).toBe(0);
254
- });
255
-
256
- it('should calculate engagement across all platforms combined', async () => {
257
- const executor = createMockExecutor({
258
- twitter: { likes: 50, comments: 10, shares: 20, impressions: 2000 },
259
- linkedin: { likes: 50, comments: 10, shares: 20, impressions: 3000 },
260
- });
261
- tool.setToolExecutor(executor);
262
-
263
- const result = await tool.execute({
264
- platforms: ['twitter', 'linkedin'],
265
- postIds: { twitter: 'tw-1', linkedin: 'li-1' },
266
- });
267
-
268
- // (100 + 20 + 40) / 5000 * 100 = 3.20
269
- expect(result.data!.totals.engagement).toBe(3.2);
270
- });
271
-
272
- it('should round engagement to 2 decimal places', async () => {
273
- const executor = createMockExecutor({
274
- twitter: { likes: 1, comments: 1, shares: 1, impressions: 7 },
275
- });
276
- tool.setToolExecutor(executor);
277
-
278
- const result = await tool.execute({
279
- platforms: ['twitter'],
280
- postIds: { twitter: 'tw-1' },
281
- });
282
-
283
- // (3 / 7) * 100 = 42.857... -> 42.86
284
- expect(result.data!.totals.engagement).toBe(42.86);
285
- });
286
- });
287
-
288
- /* ── Missing post IDs ─────────────────────────────────────────── */
289
-
290
- describe('missing post IDs', () => {
291
- it('should report error for a platform without a post ID', async () => {
292
- const executor = createMockExecutor({
293
- twitter: { likes: 100, comments: 20, shares: 30, impressions: 5000 },
294
- });
295
- tool.setToolExecutor(executor);
296
-
297
- const result = await tool.execute({
298
- platforms: ['twitter', 'linkedin'],
299
- postIds: { twitter: 'tw-1' }, // linkedin has no postId
300
- });
301
-
302
- expect(result.success).toBe(true);
303
- const linkedinResult = result.data!.platforms['linkedin'] as { error: string };
304
- expect(linkedinResult.error).toBe('No post ID provided for this platform');
305
- });
306
-
307
- it('should still aggregate metrics from platforms that have post IDs', async () => {
308
- const executor = createMockExecutor({
309
- twitter: { likes: 100, comments: 20, shares: 30, impressions: 5000 },
310
- });
311
- tool.setToolExecutor(executor);
312
-
313
- const result = await tool.execute({
314
- platforms: ['twitter', 'linkedin'],
315
- postIds: { twitter: 'tw-1' },
316
- });
317
-
318
- expect(result.data!.totals.likes).toBe(100);
319
- });
320
- });
321
-
322
- /* ── Executor errors ──────────────────────────────────────────── */
323
-
324
- describe('executor errors', () => {
325
- it('should handle executor returning success=false', async () => {
326
- const executor = vi.fn(async () => ({
327
- success: false,
328
- error: 'API rate limited',
329
- }));
330
- tool.setToolExecutor(executor);
331
-
332
- const result = await tool.execute({
333
- platforms: ['twitter'],
334
- postIds: { twitter: 'tw-1' },
335
- });
336
-
337
- expect(result.success).toBe(true); // tool-level always succeeds
338
- const twitterResult = result.data!.platforms['twitter'] as { error: string };
339
- expect(twitterResult.error).toBe('API rate limited');
340
- });
341
-
342
- it('should handle executor returning success=false with no error message', async () => {
343
- const executor = vi.fn(async () => ({
344
- success: false,
345
- }));
346
- tool.setToolExecutor(executor);
347
-
348
- const result = await tool.execute({
349
- platforms: ['twitter'],
350
- postIds: { twitter: 'tw-1' },
351
- });
352
-
353
- const twitterResult = result.data!.platforms['twitter'] as { error: string };
354
- expect(twitterResult.error).toBe('No data returned');
355
- });
356
-
357
- it('should handle thrown errors from executor gracefully', async () => {
358
- const executor = vi.fn(async () => {
359
- throw new Error('Connection refused');
360
- });
361
- tool.setToolExecutor(executor);
362
-
363
- const result = await tool.execute({
364
- platforms: ['twitter'],
365
- postIds: { twitter: 'tw-1' },
366
- });
367
-
368
- expect(result.success).toBe(true);
369
- const twitterResult = result.data!.platforms['twitter'] as { error: string };
370
- expect(twitterResult.error).toBe('Connection refused');
371
- });
372
-
373
- it('should handle non-Error thrown values', async () => {
374
- const executor = vi.fn(async () => {
375
- throw 42; // eslint-disable-line no-throw-literal
376
- });
377
- tool.setToolExecutor(executor);
378
-
379
- const result = await tool.execute({
380
- platforms: ['twitter'],
381
- postIds: { twitter: 'tw-1' },
382
- });
383
-
384
- const twitterResult = result.data!.platforms['twitter'] as { error: string };
385
- expect(twitterResult.error).toBe('42');
386
- });
387
- });
388
-
389
- /* ── No executor ──────────────────────────────────────────────── */
390
-
391
- describe('missing tool executor', () => {
392
- it('should report error when executor is not set and postId is present', async () => {
393
- const result = await tool.execute({
394
- platforms: ['twitter'],
395
- postIds: { twitter: 'tw-1' },
396
- });
397
-
398
- const twitterResult = result.data!.platforms['twitter'] as { error: string };
399
- expect(twitterResult.error).toBe('No tool executor available');
400
- });
401
- });
402
-
403
- /* ── toNumber helper (via output) ─────────────────────────────── */
404
-
405
- describe('toNumber coercion', () => {
406
- it('should coerce string numbers in platform metrics', async () => {
407
- const executor = createMockExecutor({
408
- twitter: { likes: '50', comments: '10', shares: '20', impressions: '2000' },
409
- });
410
- tool.setToolExecutor(executor);
411
-
412
- const result = await tool.execute({
413
- platforms: ['twitter'],
414
- postIds: { twitter: 'tw-1' },
415
- });
416
-
417
- expect(result.data!.totals.likes).toBe(50);
418
- expect(result.data!.totals.comments).toBe(10);
419
- expect(result.data!.totals.shares).toBe(20);
420
- expect(result.data!.totals.impressions).toBe(2000);
421
- });
422
-
423
- it('should treat undefined/null metrics as 0', async () => {
424
- const executor = createMockExecutor({
425
- twitter: { likes: undefined, comments: null, impressions: 1000 },
426
- });
427
- tool.setToolExecutor(executor);
428
-
429
- const result = await tool.execute({
430
- platforms: ['twitter'],
431
- postIds: { twitter: 'tw-1' },
432
- });
433
-
434
- expect(result.data!.totals.likes).toBe(0);
435
- expect(result.data!.totals.comments).toBe(0);
436
- expect(result.data!.totals.shares).toBe(0);
437
- });
438
-
439
- it('should treat NaN as 0', async () => {
440
- const executor = createMockExecutor({
441
- twitter: { likes: NaN, comments: 'not-a-number', shares: 5, impressions: 100 },
442
- });
443
- tool.setToolExecutor(executor);
444
-
445
- const result = await tool.execute({
446
- platforms: ['twitter'],
447
- postIds: { twitter: 'tw-1' },
448
- });
449
-
450
- expect(result.data!.totals.likes).toBe(0);
451
- expect(result.data!.totals.comments).toBe(0);
452
- expect(result.data!.totals.shares).toBe(5);
453
- });
454
- });
455
- });
package/tsconfig.json DELETED
@@ -1,22 +0,0 @@
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
- }