@robota-sdk/agent-plugin 3.0.0-beta.64

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 (82) hide show
  1. package/LICENSE +21 -0
  2. package/dist/node/index.cjs +1 -0
  3. package/dist/node/index.d.ts +1724 -0
  4. package/dist/node/index.d.ts.map +1 -0
  5. package/dist/node/index.js +2 -0
  6. package/dist/node/index.js.map +1 -0
  7. package/package.json +48 -0
  8. package/src/conversation-history/__tests__/conversation-history-plugin.test.ts +221 -0
  9. package/src/conversation-history/__tests__/history-storages.test.ts +115 -0
  10. package/src/conversation-history/conversation-history-helpers.ts +120 -0
  11. package/src/conversation-history/conversation-history-plugin.ts +294 -0
  12. package/src/conversation-history/index.ts +11 -0
  13. package/src/conversation-history/storages/database-storage.ts +96 -0
  14. package/src/conversation-history/storages/file-storage.ts +95 -0
  15. package/src/conversation-history/storages/index.ts +3 -0
  16. package/src/conversation-history/storages/memory-storage.ts +44 -0
  17. package/src/conversation-history/types.ts +64 -0
  18. package/src/error-handling/__tests__/error-handling-plugin.test.ts +201 -0
  19. package/src/error-handling/context-adapter.ts +48 -0
  20. package/src/error-handling/error-handling-helpers.ts +53 -0
  21. package/src/error-handling/error-handling-plugin.ts +293 -0
  22. package/src/error-handling/index.ts +9 -0
  23. package/src/error-handling/types.ts +82 -0
  24. package/src/execution-analytics/__tests__/execution-analytics-plugin.test.ts +224 -0
  25. package/src/execution-analytics/analytics-aggregation.ts +88 -0
  26. package/src/execution-analytics/execution-analytics-helpers.ts +83 -0
  27. package/src/execution-analytics/execution-analytics-plugin.ts +315 -0
  28. package/src/execution-analytics/index.ts +9 -0
  29. package/src/execution-analytics/types.ts +97 -0
  30. package/src/index.ts +8 -0
  31. package/src/limits/__tests__/limits-plugin.test.ts +712 -0
  32. package/src/limits/index.ts +9 -0
  33. package/src/limits/limits-helpers.ts +185 -0
  34. package/src/limits/limits-plugin.ts +196 -0
  35. package/src/limits/types.ts +73 -0
  36. package/src/limits/validation.ts +81 -0
  37. package/src/logging/__tests__/formatters.test.ts +48 -0
  38. package/src/logging/__tests__/logging-plugin.test.ts +464 -0
  39. package/src/logging/__tests__/logging-storages.test.ts +95 -0
  40. package/src/logging/formatters.ts +28 -0
  41. package/src/logging/index.ts +15 -0
  42. package/src/logging/logging-helpers.ts +223 -0
  43. package/src/logging/logging-plugin.ts +288 -0
  44. package/src/logging/storages/console-storage.ts +44 -0
  45. package/src/logging/storages/file-storage.ts +44 -0
  46. package/src/logging/storages/index.ts +4 -0
  47. package/src/logging/storages/remote-storage.ts +78 -0
  48. package/src/logging/storages/silent-storage.ts +18 -0
  49. package/src/logging/types.ts +106 -0
  50. package/src/performance/__tests__/memory-storage.test.ts +86 -0
  51. package/src/performance/__tests__/performance-plugin.test.ts +208 -0
  52. package/src/performance/__tests__/system-metrics-collector.test.ts +33 -0
  53. package/src/performance/collectors/system-metrics-collector.ts +69 -0
  54. package/src/performance/index.ts +12 -0
  55. package/src/performance/performance-helpers.ts +86 -0
  56. package/src/performance/performance-plugin.ts +274 -0
  57. package/src/performance/storages/index.ts +1 -0
  58. package/src/performance/storages/memory-storage.ts +88 -0
  59. package/src/performance/types.ts +160 -0
  60. package/src/usage/__tests__/aggregate-usage-stats.test.ts +136 -0
  61. package/src/usage/__tests__/memory-storage.test.ts +83 -0
  62. package/src/usage/__tests__/silent-storage.test.ts +44 -0
  63. package/src/usage/__tests__/usage-plugin-helpers.test.ts +155 -0
  64. package/src/usage/__tests__/usage-plugin.test.ts +358 -0
  65. package/src/usage/aggregate-usage-stats.ts +142 -0
  66. package/src/usage/index.ts +14 -0
  67. package/src/usage/storages/file-storage.ts +115 -0
  68. package/src/usage/storages/index.ts +4 -0
  69. package/src/usage/storages/memory-storage.ts +61 -0
  70. package/src/usage/storages/remote-storage.ts +143 -0
  71. package/src/usage/storages/silent-storage.ts +38 -0
  72. package/src/usage/types.ts +132 -0
  73. package/src/usage/usage-plugin-helpers.ts +116 -0
  74. package/src/usage/usage-plugin.ts +296 -0
  75. package/src/webhook/__tests__/webhook-plugin.test.ts +560 -0
  76. package/src/webhook/http-client.ts +141 -0
  77. package/src/webhook/index.ts +9 -0
  78. package/src/webhook/transformer.ts +209 -0
  79. package/src/webhook/types.ts +201 -0
  80. package/src/webhook/webhook-helpers.ts +60 -0
  81. package/src/webhook/webhook-plugin.ts +298 -0
  82. package/src/webhook/webhook-queue.ts +148 -0
@@ -0,0 +1,83 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { MemoryUsageStorage } from '../storages/memory-storage';
3
+ import type { IUsageStats } from '../types';
4
+
5
+ function makeEntry(overrides: Partial<IUsageStats> = {}): IUsageStats {
6
+ return {
7
+ provider: 'openai',
8
+ model: 'gpt-4',
9
+ tokensUsed: { input: 100, output: 50, total: 150 },
10
+ requestCount: 1,
11
+ duration: 500,
12
+ success: true,
13
+ timestamp: new Date(),
14
+ ...overrides,
15
+ };
16
+ }
17
+
18
+ describe('MemoryUsageStorage', () => {
19
+ let storage: MemoryUsageStorage;
20
+
21
+ beforeEach(() => {
22
+ storage = new MemoryUsageStorage(100);
23
+ });
24
+
25
+ it('saves and retrieves entries', async () => {
26
+ await storage.save(makeEntry());
27
+ const stats = await storage.getStats();
28
+ expect(stats).toHaveLength(1);
29
+ });
30
+
31
+ it('evicts oldest entries when maxEntries exceeded', async () => {
32
+ storage = new MemoryUsageStorage(3);
33
+ await storage.save(makeEntry({ provider: 'first' }));
34
+ await storage.save(makeEntry({ provider: 'second' }));
35
+ await storage.save(makeEntry({ provider: 'third' }));
36
+ await storage.save(makeEntry({ provider: 'fourth' }));
37
+ const stats = await storage.getStats();
38
+ expect(stats).toHaveLength(3);
39
+ // First entry should be evicted
40
+ expect(stats.some((s) => s.provider === 'first')).toBe(false);
41
+ expect(stats.some((s) => s.provider === 'fourth')).toBe(true);
42
+ });
43
+
44
+ it('filters by conversationId', async () => {
45
+ await storage.save(makeEntry({ conversationId: 'conv-1' }));
46
+ await storage.save(makeEntry({ conversationId: 'conv-2' }));
47
+ const stats = await storage.getStats('conv-1');
48
+ expect(stats).toHaveLength(1);
49
+ expect(stats[0].conversationId).toBe('conv-1');
50
+ });
51
+
52
+ it('filters by time range', async () => {
53
+ const old = new Date('2024-01-01');
54
+ const recent = new Date('2025-06-01');
55
+ await storage.save(makeEntry({ timestamp: old }));
56
+ await storage.save(makeEntry({ timestamp: recent }));
57
+ const stats = await storage.getStats(undefined, {
58
+ start: new Date('2025-01-01'),
59
+ end: new Date('2026-01-01'),
60
+ });
61
+ expect(stats).toHaveLength(1);
62
+ });
63
+
64
+ it('returns aggregated stats', async () => {
65
+ await storage.save(makeEntry({ success: true }));
66
+ await storage.save(makeEntry({ success: false }));
67
+ const aggregated = await storage.getAggregatedStats();
68
+ expect(aggregated.totalRequests).toBe(2);
69
+ expect(aggregated.successRate).toBe(0.5);
70
+ });
71
+
72
+ it('clears all entries', async () => {
73
+ await storage.save(makeEntry());
74
+ await storage.clear();
75
+ const stats = await storage.getStats();
76
+ expect(stats).toHaveLength(0);
77
+ });
78
+
79
+ it('flush and close are no-ops', async () => {
80
+ await expect(storage.flush()).resolves.toBeUndefined();
81
+ await expect(storage.close()).resolves.toBeUndefined();
82
+ });
83
+ });
@@ -0,0 +1,44 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { SilentUsageStorage } from '../storages/silent-storage';
3
+ import type { IUsageStats } from '../types';
4
+
5
+ describe('SilentUsageStorage', () => {
6
+ const storage = new SilentUsageStorage();
7
+
8
+ const entry: IUsageStats = {
9
+ provider: 'openai',
10
+ model: 'gpt-4',
11
+ tokensUsed: { input: 100, output: 50, total: 150 },
12
+ requestCount: 1,
13
+ duration: 500,
14
+ success: true,
15
+ timestamp: new Date(),
16
+ };
17
+
18
+ it('save is a no-op', async () => {
19
+ await expect(storage.save(entry)).resolves.toBeUndefined();
20
+ });
21
+
22
+ it('getStats returns empty array', async () => {
23
+ const stats = await storage.getStats();
24
+ expect(stats).toEqual([]);
25
+ });
26
+
27
+ it('getAggregatedStats returns zeroed aggregation', async () => {
28
+ const agg = await storage.getAggregatedStats();
29
+ expect(agg.totalRequests).toBe(0);
30
+ expect(agg.totalTokens).toBe(0);
31
+ });
32
+
33
+ it('clear is a no-op', async () => {
34
+ await expect(storage.clear()).resolves.toBeUndefined();
35
+ });
36
+
37
+ it('flush is a no-op', async () => {
38
+ await expect(storage.flush()).resolves.toBeUndefined();
39
+ });
40
+
41
+ it('close is a no-op', async () => {
42
+ await expect(storage.close()).resolves.toBeUndefined();
43
+ });
44
+ });
@@ -0,0 +1,155 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ resolvePluginOptions,
4
+ validateUsageOptions,
5
+ calculateCost,
6
+ isModuleSuccessEvent,
7
+ isModuleErrorEvent,
8
+ extractStringField,
9
+ resolveOperation,
10
+ } from '../usage-plugin-helpers';
11
+ import { ConfigurationError, EVENT_EMITTER_EVENTS } from '@robota-sdk/agent-core';
12
+
13
+ describe('usage-plugin-helpers', () => {
14
+ describe('resolvePluginOptions', () => {
15
+ it('returns defaults for minimal options', () => {
16
+ const resolved = resolvePluginOptions({ strategy: 'memory' });
17
+ expect(resolved.strategy).toBe('memory');
18
+ expect(resolved.enabled).toBe(true);
19
+ expect(resolved.trackCosts).toBe(true);
20
+ expect(resolved.maxEntries).toBe(10000);
21
+ expect(resolved.batchSize).toBe(50);
22
+ expect(resolved.flushInterval).toBe(60000);
23
+ expect(resolved.aggregateStats).toBe(true);
24
+ expect(resolved.aggregationInterval).toBe(300000);
25
+ });
26
+
27
+ it('preserves explicit costRates', () => {
28
+ const rates = { 'gpt-4': { input: 0.03, output: 0.06 } };
29
+ const resolved = resolvePluginOptions({ strategy: 'memory', costRates: rates });
30
+ expect(resolved.costRates).toEqual(rates);
31
+ });
32
+
33
+ it('omits costRates when not provided', () => {
34
+ const resolved = resolvePluginOptions({ strategy: 'memory' });
35
+ expect(resolved.costRates).toBeUndefined();
36
+ });
37
+ });
38
+
39
+ describe('calculateCost', () => {
40
+ const rates = { 'gpt-4': { input: 0.03, output: 0.06 } };
41
+
42
+ it('calculates cost for known model', () => {
43
+ const cost = calculateCost(rates, 'gpt-4', { input: 100, output: 50 });
44
+ expect(cost).toEqual({ input: 3, output: 3, total: 6 });
45
+ });
46
+
47
+ it('returns undefined for unknown model', () => {
48
+ expect(calculateCost(rates, 'unknown', { input: 100, output: 50 })).toBeUndefined();
49
+ });
50
+
51
+ it('returns undefined when no rates provided', () => {
52
+ expect(calculateCost(undefined, 'gpt-4', { input: 100, output: 50 })).toBeUndefined();
53
+ });
54
+ });
55
+
56
+ describe('validateUsageOptions', () => {
57
+ it('passes for valid memory options', () => {
58
+ expect(() => validateUsageOptions({ strategy: 'memory' })).not.toThrow();
59
+ });
60
+
61
+ it('passes for valid file options', () => {
62
+ expect(() =>
63
+ validateUsageOptions({ strategy: 'file', filePath: '/tmp/usage.json' }),
64
+ ).not.toThrow();
65
+ });
66
+
67
+ it('passes for valid remote options', () => {
68
+ expect(() =>
69
+ validateUsageOptions({ strategy: 'remote', remoteEndpoint: 'http://example.com' }),
70
+ ).not.toThrow();
71
+ });
72
+
73
+ it('throws for empty strategy', () => {
74
+ expect(() => validateUsageOptions({ strategy: '' as any })).toThrow(ConfigurationError);
75
+ });
76
+
77
+ it('throws for invalid strategy', () => {
78
+ expect(() => validateUsageOptions({ strategy: 'invalid' as any })).toThrow(
79
+ ConfigurationError,
80
+ );
81
+ });
82
+
83
+ it('throws for file strategy without filePath', () => {
84
+ expect(() => validateUsageOptions({ strategy: 'file' })).toThrow(ConfigurationError);
85
+ });
86
+
87
+ it('throws for remote strategy without endpoint', () => {
88
+ expect(() => validateUsageOptions({ strategy: 'remote' })).toThrow(ConfigurationError);
89
+ });
90
+ });
91
+
92
+ describe('isModuleSuccessEvent', () => {
93
+ it('returns true for module completion events', () => {
94
+ expect(isModuleSuccessEvent(EVENT_EMITTER_EVENTS.MODULE_INITIALIZE_COMPLETE)).toBe(true);
95
+ expect(isModuleSuccessEvent(EVENT_EMITTER_EVENTS.MODULE_EXECUTION_COMPLETE)).toBe(true);
96
+ expect(isModuleSuccessEvent(EVENT_EMITTER_EVENTS.MODULE_DISPOSE_COMPLETE)).toBe(true);
97
+ });
98
+
99
+ it('returns false for error events', () => {
100
+ expect(isModuleSuccessEvent(EVENT_EMITTER_EVENTS.MODULE_INITIALIZE_ERROR)).toBe(false);
101
+ });
102
+
103
+ it('returns false for unrelated events', () => {
104
+ expect(isModuleSuccessEvent('some.random.event' as any)).toBe(false);
105
+ });
106
+ });
107
+
108
+ describe('isModuleErrorEvent', () => {
109
+ it('returns true for module error events', () => {
110
+ expect(isModuleErrorEvent(EVENT_EMITTER_EVENTS.MODULE_INITIALIZE_ERROR)).toBe(true);
111
+ expect(isModuleErrorEvent(EVENT_EMITTER_EVENTS.MODULE_EXECUTION_ERROR)).toBe(true);
112
+ expect(isModuleErrorEvent(EVENT_EMITTER_EVENTS.MODULE_DISPOSE_ERROR)).toBe(true);
113
+ });
114
+
115
+ it('returns false for success events', () => {
116
+ expect(isModuleErrorEvent(EVENT_EMITTER_EVENTS.MODULE_EXECUTION_COMPLETE)).toBe(false);
117
+ });
118
+ });
119
+
120
+ describe('extractStringField', () => {
121
+ it('extracts string field from object', () => {
122
+ expect(extractStringField({ name: 'test' }, 'name')).toBe('test');
123
+ });
124
+
125
+ it('returns "unknown" for missing field', () => {
126
+ expect(extractStringField({ name: 'test' }, 'missing')).toBe('unknown');
127
+ });
128
+
129
+ it('returns "unknown" for non-string field', () => {
130
+ expect(extractStringField({ count: 42 }, 'count')).toBe('unknown');
131
+ });
132
+
133
+ it('returns "unknown" for null data', () => {
134
+ expect(extractStringField(null, 'field')).toBe('unknown');
135
+ });
136
+
137
+ it('returns "unknown" for undefined data', () => {
138
+ expect(extractStringField(undefined, 'field')).toBe('unknown');
139
+ });
140
+ });
141
+
142
+ describe('resolveOperation', () => {
143
+ it('returns "initialization" for initialize events', () => {
144
+ expect(resolveOperation('module.initialize.complete')).toBe('initialization');
145
+ });
146
+
147
+ it('returns "execution" for execution events', () => {
148
+ expect(resolveOperation('module.execution.complete')).toBe('execution');
149
+ });
150
+
151
+ it('returns "disposal" for other events', () => {
152
+ expect(resolveOperation('module.dispose.complete')).toBe('disposal');
153
+ });
154
+ });
155
+ });
@@ -0,0 +1,358 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { UsagePlugin } from '../usage-plugin';
3
+ import { ConfigurationError, PluginError } from '@robota-sdk/agent-core';
4
+
5
+ describe('UsagePlugin', () => {
6
+ let plugin: UsagePlugin;
7
+
8
+ afterEach(async () => {
9
+ if (plugin) {
10
+ await plugin.destroy();
11
+ }
12
+ });
13
+
14
+ describe('constructor', () => {
15
+ it('creates plugin with memory strategy', () => {
16
+ plugin = new UsagePlugin({ strategy: 'memory' });
17
+ expect(plugin.name).toBe('UsagePlugin');
18
+ expect(plugin.version).toBe('1.0.0');
19
+ });
20
+
21
+ it('creates plugin with silent strategy', () => {
22
+ plugin = new UsagePlugin({ strategy: 'silent' });
23
+ expect(plugin.name).toBe('UsagePlugin');
24
+ });
25
+
26
+ it('throws ConfigurationError for missing strategy', () => {
27
+ expect(() => new UsagePlugin({ strategy: '' as any })).toThrow(ConfigurationError);
28
+ });
29
+
30
+ it('throws ConfigurationError for invalid strategy', () => {
31
+ expect(() => new UsagePlugin({ strategy: 'invalid' as any })).toThrow(ConfigurationError);
32
+ });
33
+
34
+ it('throws ConfigurationError for file strategy without filePath', () => {
35
+ expect(() => new UsagePlugin({ strategy: 'file' })).toThrow(ConfigurationError);
36
+ });
37
+
38
+ it('throws ConfigurationError for remote strategy without endpoint', () => {
39
+ expect(() => new UsagePlugin({ strategy: 'remote' })).toThrow(ConfigurationError);
40
+ });
41
+
42
+ it('throws ConfigurationError for non-positive maxEntries', () => {
43
+ expect(() => new UsagePlugin({ strategy: 'memory', maxEntries: 0 })).toThrow(
44
+ ConfigurationError,
45
+ );
46
+ });
47
+
48
+ it('throws ConfigurationError for non-positive batchSize', () => {
49
+ expect(() => new UsagePlugin({ strategy: 'memory', batchSize: -1 })).toThrow(
50
+ ConfigurationError,
51
+ );
52
+ });
53
+
54
+ it('throws ConfigurationError for non-positive flushInterval', () => {
55
+ expect(() => new UsagePlugin({ strategy: 'memory', flushInterval: 0 })).toThrow(
56
+ ConfigurationError,
57
+ );
58
+ });
59
+
60
+ it('throws ConfigurationError for non-positive aggregationInterval', () => {
61
+ expect(() => new UsagePlugin({ strategy: 'memory', aggregationInterval: -5 })).toThrow(
62
+ ConfigurationError,
63
+ );
64
+ });
65
+ });
66
+
67
+ describe('recordUsage', () => {
68
+ beforeEach(() => {
69
+ plugin = new UsagePlugin({ strategy: 'memory', aggregateStats: false });
70
+ });
71
+
72
+ it('records a usage entry', async () => {
73
+ await plugin.recordUsage({
74
+ provider: 'openai',
75
+ model: 'gpt-4',
76
+ tokensUsed: { input: 100, output: 50, total: 150 },
77
+ requestCount: 1,
78
+ duration: 500,
79
+ success: true,
80
+ });
81
+ const stats = await plugin.getUsageStats();
82
+ expect(stats).toHaveLength(1);
83
+ expect(stats[0].provider).toBe('openai');
84
+ expect(stats[0].model).toBe('gpt-4');
85
+ expect(stats[0].tokensUsed.total).toBe(150);
86
+ });
87
+
88
+ it('calculates cost when trackCosts is enabled', async () => {
89
+ plugin = new UsagePlugin({
90
+ strategy: 'memory',
91
+ trackCosts: true,
92
+ costRates: { 'gpt-4': { input: 0.03, output: 0.06 } },
93
+ aggregateStats: false,
94
+ });
95
+
96
+ await plugin.recordUsage({
97
+ provider: 'openai',
98
+ model: 'gpt-4',
99
+ tokensUsed: { input: 100, output: 50, total: 150 },
100
+ requestCount: 1,
101
+ duration: 500,
102
+ success: true,
103
+ });
104
+
105
+ const stats = await plugin.getUsageStats();
106
+ expect(stats[0].cost).toBeDefined();
107
+ expect(stats[0].cost!.input).toBe(3);
108
+ expect(stats[0].cost!.output).toBe(3);
109
+ expect(stats[0].cost!.total).toBe(6);
110
+ });
111
+
112
+ it('does not calculate cost when no rate exists for model', async () => {
113
+ plugin = new UsagePlugin({
114
+ strategy: 'memory',
115
+ trackCosts: true,
116
+ costRates: { 'gpt-4': { input: 0.03, output: 0.06 } },
117
+ aggregateStats: false,
118
+ });
119
+
120
+ await plugin.recordUsage({
121
+ provider: 'openai',
122
+ model: 'gpt-3.5-turbo',
123
+ tokensUsed: { input: 100, output: 50, total: 150 },
124
+ requestCount: 1,
125
+ duration: 300,
126
+ success: true,
127
+ });
128
+
129
+ const stats = await plugin.getUsageStats();
130
+ expect(stats[0].cost).toBeUndefined();
131
+ });
132
+ });
133
+
134
+ describe('getUsageStats', () => {
135
+ beforeEach(async () => {
136
+ plugin = new UsagePlugin({ strategy: 'memory', aggregateStats: false });
137
+ await plugin.recordUsage({
138
+ provider: 'openai',
139
+ model: 'gpt-4',
140
+ tokensUsed: { input: 100, output: 50, total: 150 },
141
+ requestCount: 1,
142
+ duration: 500,
143
+ success: true,
144
+ conversationId: 'conv-1',
145
+ });
146
+ await plugin.recordUsage({
147
+ provider: 'anthropic',
148
+ model: 'claude-3',
149
+ tokensUsed: { input: 200, output: 100, total: 300 },
150
+ requestCount: 1,
151
+ duration: 800,
152
+ success: false,
153
+ conversationId: 'conv-2',
154
+ });
155
+ });
156
+
157
+ it('returns all stats when no filter', async () => {
158
+ const stats = await plugin.getUsageStats();
159
+ expect(stats).toHaveLength(2);
160
+ });
161
+
162
+ it('filters by conversationId', async () => {
163
+ const stats = await plugin.getUsageStats('conv-1');
164
+ expect(stats).toHaveLength(1);
165
+ expect(stats[0].provider).toBe('openai');
166
+ });
167
+ });
168
+
169
+ describe('getAggregatedStats', () => {
170
+ beforeEach(async () => {
171
+ plugin = new UsagePlugin({ strategy: 'memory', aggregateStats: false });
172
+ await plugin.recordUsage({
173
+ provider: 'openai',
174
+ model: 'gpt-4',
175
+ tokensUsed: { input: 100, output: 50, total: 150 },
176
+ requestCount: 1,
177
+ duration: 500,
178
+ success: true,
179
+ });
180
+ await plugin.recordUsage({
181
+ provider: 'openai',
182
+ model: 'gpt-4',
183
+ tokensUsed: { input: 200, output: 100, total: 300 },
184
+ requestCount: 1,
185
+ duration: 800,
186
+ success: false,
187
+ });
188
+ });
189
+
190
+ it('aggregates correctly', async () => {
191
+ const aggregated = await plugin.getAggregatedStats();
192
+ expect(aggregated.totalRequests).toBe(2);
193
+ expect(aggregated.totalTokens).toBe(450);
194
+ expect(aggregated.successRate).toBe(0.5);
195
+ });
196
+ });
197
+
198
+ describe('clearStats', () => {
199
+ it('clears all usage statistics', async () => {
200
+ plugin = new UsagePlugin({ strategy: 'memory', aggregateStats: false });
201
+ await plugin.recordUsage({
202
+ provider: 'openai',
203
+ model: 'gpt-4',
204
+ tokensUsed: { input: 100, output: 50, total: 150 },
205
+ requestCount: 1,
206
+ duration: 500,
207
+ success: true,
208
+ });
209
+ await plugin.clearStats();
210
+ const stats = await plugin.getUsageStats();
211
+ expect(stats).toHaveLength(0);
212
+ });
213
+ });
214
+
215
+ describe('flush', () => {
216
+ it('flushes without error for memory strategy', async () => {
217
+ plugin = new UsagePlugin({ strategy: 'memory', aggregateStats: false });
218
+ await expect(plugin.flush()).resolves.toBeUndefined();
219
+ });
220
+ });
221
+
222
+ describe('destroy', () => {
223
+ it('destroys the plugin without error', async () => {
224
+ plugin = new UsagePlugin({ strategy: 'memory', aggregateStats: false });
225
+ await expect(plugin.destroy()).resolves.toBeUndefined();
226
+ });
227
+ });
228
+
229
+ describe('silent strategy', () => {
230
+ beforeEach(() => {
231
+ plugin = new UsagePlugin({ strategy: 'silent', aggregateStats: false });
232
+ });
233
+
234
+ it('records and returns empty stats', async () => {
235
+ await plugin.recordUsage({
236
+ provider: 'openai',
237
+ model: 'gpt-4',
238
+ tokensUsed: { input: 100, output: 50, total: 150 },
239
+ requestCount: 1,
240
+ duration: 500,
241
+ success: true,
242
+ });
243
+ const stats = await plugin.getUsageStats();
244
+ expect(stats).toHaveLength(0);
245
+ });
246
+ });
247
+
248
+ describe('file strategy', () => {
249
+ it('creates plugin with file strategy and valid filePath', () => {
250
+ plugin = new UsagePlugin({
251
+ strategy: 'file',
252
+ filePath: './usage.json',
253
+ aggregateStats: false,
254
+ });
255
+ expect(plugin.name).toBe('UsagePlugin');
256
+ });
257
+
258
+ it('records usage with file strategy', async () => {
259
+ plugin = new UsagePlugin({
260
+ strategy: 'file',
261
+ filePath: './usage.json',
262
+ aggregateStats: false,
263
+ });
264
+ await expect(
265
+ plugin.recordUsage({
266
+ provider: 'openai',
267
+ model: 'gpt-4',
268
+ tokensUsed: { input: 100, output: 50, total: 150 },
269
+ requestCount: 1,
270
+ duration: 500,
271
+ success: true,
272
+ }),
273
+ ).resolves.toBeUndefined();
274
+ });
275
+ });
276
+
277
+ describe('remote strategy', () => {
278
+ it('creates plugin with remote strategy and valid endpoint', () => {
279
+ plugin = new UsagePlugin({
280
+ strategy: 'remote',
281
+ remoteEndpoint: 'https://api.example.com/usage',
282
+ aggregateStats: false,
283
+ });
284
+ expect(plugin.name).toBe('UsagePlugin');
285
+ });
286
+ });
287
+
288
+ describe('aggregateStats: true', () => {
289
+ it('creates plugin with aggregation enabled and runs timer callback', async () => {
290
+ plugin = new UsagePlugin({
291
+ strategy: 'memory',
292
+ aggregateStats: true,
293
+ aggregationInterval: 50,
294
+ });
295
+ expect(plugin).toBeDefined();
296
+ // allow timer to fire at least once
297
+ await new Promise((r) => setTimeout(r, 80));
298
+ });
299
+ });
300
+
301
+ describe('onModuleEvent', () => {
302
+ beforeEach(() => {
303
+ plugin = new UsagePlugin({ strategy: 'memory', aggregateStats: false });
304
+ });
305
+
306
+ it('records module success event', async () => {
307
+ await plugin.onModuleEvent('module.execution.complete', {
308
+ type: 'module.execution.complete',
309
+ timestamp: new Date(),
310
+ executionId: 'eid',
311
+ sessionId: 'sid',
312
+ data: { duration: 100, moduleName: 'testModule', moduleType: 'executor' },
313
+ });
314
+ const stats = await plugin.getUsageStats();
315
+ expect(stats).toHaveLength(1);
316
+ expect(stats[0].provider).toBe('module');
317
+ expect(stats[0].success).toBe(true);
318
+ });
319
+
320
+ it('records module error event', async () => {
321
+ await plugin.onModuleEvent('module.execution.error', {
322
+ type: 'module.execution.error',
323
+ timestamp: new Date(),
324
+ executionId: 'eid',
325
+ sessionId: 'sid',
326
+ error: new Error('test error'),
327
+ data: { duration: 50, moduleName: 'testModule', moduleType: 'executor' },
328
+ });
329
+ const stats = await plugin.getUsageStats();
330
+ expect(stats).toHaveLength(1);
331
+ expect(stats[0].success).toBe(false);
332
+ });
333
+
334
+ it('ignores non-module events', async () => {
335
+ await plugin.onModuleEvent('conversation.start', {
336
+ type: 'conversation.start',
337
+ timestamp: new Date(),
338
+ executionId: 'eid',
339
+ sessionId: 'sid',
340
+ data: {},
341
+ });
342
+ const stats = await plugin.getUsageStats();
343
+ expect(stats).toHaveLength(0);
344
+ });
345
+
346
+ it('ignores events with no duration', async () => {
347
+ await plugin.onModuleEvent('module.execution.complete', {
348
+ type: 'module.execution.complete',
349
+ timestamp: new Date(),
350
+ executionId: 'eid',
351
+ sessionId: 'sid',
352
+ data: { moduleName: 'testModule' },
353
+ });
354
+ const stats = await plugin.getUsageStats();
355
+ expect(stats).toHaveLength(0);
356
+ });
357
+ });
358
+ });