@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.
- package/LICENSE +21 -0
- package/dist/node/index.cjs +1 -0
- package/dist/node/index.d.ts +1724 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +2 -0
- package/dist/node/index.js.map +1 -0
- package/package.json +48 -0
- package/src/conversation-history/__tests__/conversation-history-plugin.test.ts +221 -0
- package/src/conversation-history/__tests__/history-storages.test.ts +115 -0
- package/src/conversation-history/conversation-history-helpers.ts +120 -0
- package/src/conversation-history/conversation-history-plugin.ts +294 -0
- package/src/conversation-history/index.ts +11 -0
- package/src/conversation-history/storages/database-storage.ts +96 -0
- package/src/conversation-history/storages/file-storage.ts +95 -0
- package/src/conversation-history/storages/index.ts +3 -0
- package/src/conversation-history/storages/memory-storage.ts +44 -0
- package/src/conversation-history/types.ts +64 -0
- package/src/error-handling/__tests__/error-handling-plugin.test.ts +201 -0
- package/src/error-handling/context-adapter.ts +48 -0
- package/src/error-handling/error-handling-helpers.ts +53 -0
- package/src/error-handling/error-handling-plugin.ts +293 -0
- package/src/error-handling/index.ts +9 -0
- package/src/error-handling/types.ts +82 -0
- package/src/execution-analytics/__tests__/execution-analytics-plugin.test.ts +224 -0
- package/src/execution-analytics/analytics-aggregation.ts +88 -0
- package/src/execution-analytics/execution-analytics-helpers.ts +83 -0
- package/src/execution-analytics/execution-analytics-plugin.ts +315 -0
- package/src/execution-analytics/index.ts +9 -0
- package/src/execution-analytics/types.ts +97 -0
- package/src/index.ts +8 -0
- package/src/limits/__tests__/limits-plugin.test.ts +712 -0
- package/src/limits/index.ts +9 -0
- package/src/limits/limits-helpers.ts +185 -0
- package/src/limits/limits-plugin.ts +196 -0
- package/src/limits/types.ts +73 -0
- package/src/limits/validation.ts +81 -0
- package/src/logging/__tests__/formatters.test.ts +48 -0
- package/src/logging/__tests__/logging-plugin.test.ts +464 -0
- package/src/logging/__tests__/logging-storages.test.ts +95 -0
- package/src/logging/formatters.ts +28 -0
- package/src/logging/index.ts +15 -0
- package/src/logging/logging-helpers.ts +223 -0
- package/src/logging/logging-plugin.ts +288 -0
- package/src/logging/storages/console-storage.ts +44 -0
- package/src/logging/storages/file-storage.ts +44 -0
- package/src/logging/storages/index.ts +4 -0
- package/src/logging/storages/remote-storage.ts +78 -0
- package/src/logging/storages/silent-storage.ts +18 -0
- package/src/logging/types.ts +106 -0
- package/src/performance/__tests__/memory-storage.test.ts +86 -0
- package/src/performance/__tests__/performance-plugin.test.ts +208 -0
- package/src/performance/__tests__/system-metrics-collector.test.ts +33 -0
- package/src/performance/collectors/system-metrics-collector.ts +69 -0
- package/src/performance/index.ts +12 -0
- package/src/performance/performance-helpers.ts +86 -0
- package/src/performance/performance-plugin.ts +274 -0
- package/src/performance/storages/index.ts +1 -0
- package/src/performance/storages/memory-storage.ts +88 -0
- package/src/performance/types.ts +160 -0
- package/src/usage/__tests__/aggregate-usage-stats.test.ts +136 -0
- package/src/usage/__tests__/memory-storage.test.ts +83 -0
- package/src/usage/__tests__/silent-storage.test.ts +44 -0
- package/src/usage/__tests__/usage-plugin-helpers.test.ts +155 -0
- package/src/usage/__tests__/usage-plugin.test.ts +358 -0
- package/src/usage/aggregate-usage-stats.ts +142 -0
- package/src/usage/index.ts +14 -0
- package/src/usage/storages/file-storage.ts +115 -0
- package/src/usage/storages/index.ts +4 -0
- package/src/usage/storages/memory-storage.ts +61 -0
- package/src/usage/storages/remote-storage.ts +143 -0
- package/src/usage/storages/silent-storage.ts +38 -0
- package/src/usage/types.ts +132 -0
- package/src/usage/usage-plugin-helpers.ts +116 -0
- package/src/usage/usage-plugin.ts +296 -0
- package/src/webhook/__tests__/webhook-plugin.test.ts +560 -0
- package/src/webhook/http-client.ts +141 -0
- package/src/webhook/index.ts +9 -0
- package/src/webhook/transformer.ts +209 -0
- package/src/webhook/types.ts +201 -0
- package/src/webhook/webhook-helpers.ts +60 -0
- package/src/webhook/webhook-plugin.ts +298 -0
- package/src/webhook/webhook-queue.ts +148 -0
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
2
|
+
import { ConfigurationError, PluginError } from '@robota-sdk/agent-core';
|
|
3
|
+
|
|
4
|
+
// Mock logger before importing LoggingPlugin
|
|
5
|
+
vi.mock('@robota-sdk/agent-core', async (importOriginal) => {
|
|
6
|
+
const actual = await importOriginal<typeof import('@robota-sdk/agent-core')>();
|
|
7
|
+
return {
|
|
8
|
+
...actual,
|
|
9
|
+
createLogger: vi.fn().mockReturnValue({
|
|
10
|
+
debug: vi.fn(),
|
|
11
|
+
info: vi.fn(),
|
|
12
|
+
warn: vi.fn(),
|
|
13
|
+
error: vi.fn(),
|
|
14
|
+
isDebugEnabled: vi.fn().mockReturnValue(false),
|
|
15
|
+
setLevel: vi.fn(),
|
|
16
|
+
getLevel: vi.fn().mockReturnValue('warn'),
|
|
17
|
+
}),
|
|
18
|
+
SilentLogger: {
|
|
19
|
+
debug: vi.fn(),
|
|
20
|
+
info: vi.fn(),
|
|
21
|
+
warn: vi.fn(),
|
|
22
|
+
error: vi.fn(),
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
import { LoggingPlugin } from '../logging-plugin';
|
|
28
|
+
import type { ILogEntry, ILogStorage, ILogFormatter } from '../types';
|
|
29
|
+
import { EVENT_EMITTER_EVENTS } from '@robota-sdk/agent-core';
|
|
30
|
+
import type { IEventEmitterEventData } from '@robota-sdk/agent-core';
|
|
31
|
+
|
|
32
|
+
// Spy storage that records all writes
|
|
33
|
+
class SpyStorage implements ILogStorage {
|
|
34
|
+
entries: ILogEntry[] = [];
|
|
35
|
+
flushCount = 0;
|
|
36
|
+
closeCount = 0;
|
|
37
|
+
|
|
38
|
+
async write(entry: ILogEntry): Promise<void> {
|
|
39
|
+
this.entries.push(entry);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async flush(): Promise<void> {
|
|
43
|
+
this.flushCount++;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async close(): Promise<void> {
|
|
47
|
+
this.closeCount++;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Failing storage that throws on write
|
|
52
|
+
class FailingStorage implements ILogStorage {
|
|
53
|
+
async write(): Promise<void> {
|
|
54
|
+
throw new Error('Storage write failed');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async flush(): Promise<void> {
|
|
58
|
+
throw new Error('Storage flush failed');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async close(): Promise<void> {}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Inject a spy storage into a LoggingPlugin instance.
|
|
66
|
+
* Returns the spy so tests can inspect written entries.
|
|
67
|
+
*/
|
|
68
|
+
function injectSpyStorage(plugin: LoggingPlugin): SpyStorage {
|
|
69
|
+
const spy = new SpyStorage();
|
|
70
|
+
// Access private field for test injection — acceptable in test scope
|
|
71
|
+
(plugin as unknown as { storage: ILogStorage }).storage = spy;
|
|
72
|
+
return spy;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
describe('LoggingPlugin', () => {
|
|
76
|
+
afterEach(() => {
|
|
77
|
+
vi.clearAllMocks();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// ----------------------------------------------------------------
|
|
81
|
+
// Construction and validation
|
|
82
|
+
// ----------------------------------------------------------------
|
|
83
|
+
describe('construction', () => {
|
|
84
|
+
it('should create with silent strategy', () => {
|
|
85
|
+
const plugin = new LoggingPlugin({ strategy: 'silent' });
|
|
86
|
+
expect(plugin.name).toBe('LoggingPlugin');
|
|
87
|
+
expect(plugin.version).toBe('1.0.0');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should create with console strategy', () => {
|
|
91
|
+
const plugin = new LoggingPlugin({ strategy: 'console' });
|
|
92
|
+
expect(plugin.name).toBe('LoggingPlugin');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should throw on missing strategy', () => {
|
|
96
|
+
expect(() => new LoggingPlugin({ strategy: '' as 'silent' })).toThrow(ConfigurationError);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should throw on invalid strategy', () => {
|
|
100
|
+
expect(() => new LoggingPlugin({ strategy: 'redis' as 'silent' })).toThrow(
|
|
101
|
+
ConfigurationError,
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should throw on invalid log level', () => {
|
|
106
|
+
expect(() => new LoggingPlugin({ strategy: 'silent', level: 'trace' as 'info' })).toThrow(
|
|
107
|
+
ConfigurationError,
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should throw when file strategy has no filePath', () => {
|
|
112
|
+
expect(() => new LoggingPlugin({ strategy: 'file', filePath: '' })).toThrow(
|
|
113
|
+
ConfigurationError,
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should throw when remote strategy has no remoteEndpoint', () => {
|
|
118
|
+
expect(() => new LoggingPlugin({ strategy: 'remote', remoteEndpoint: '' })).toThrow(
|
|
119
|
+
ConfigurationError,
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should throw on non-positive maxLogs', () => {
|
|
124
|
+
expect(() => new LoggingPlugin({ strategy: 'silent', maxLogs: 0 })).toThrow(
|
|
125
|
+
ConfigurationError,
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should throw on non-positive batchSize', () => {
|
|
130
|
+
expect(() => new LoggingPlugin({ strategy: 'silent', batchSize: -1 })).toThrow(
|
|
131
|
+
ConfigurationError,
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should throw on non-positive flushInterval', () => {
|
|
136
|
+
expect(() => new LoggingPlugin({ strategy: 'silent', flushInterval: 0 })).toThrow(
|
|
137
|
+
ConfigurationError,
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// ----------------------------------------------------------------
|
|
143
|
+
// Log level filtering
|
|
144
|
+
// ----------------------------------------------------------------
|
|
145
|
+
describe('log level filtering', () => {
|
|
146
|
+
it('should write entries at or above the configured level', async () => {
|
|
147
|
+
const plugin = new LoggingPlugin({ strategy: 'silent', level: 'warn' });
|
|
148
|
+
const spy = injectSpyStorage(plugin);
|
|
149
|
+
|
|
150
|
+
await plugin.log('error', 'Error msg');
|
|
151
|
+
await plugin.log('warn', 'Warn msg');
|
|
152
|
+
|
|
153
|
+
expect(spy.entries).toHaveLength(2);
|
|
154
|
+
expect(spy.entries[0].level).toBe('error');
|
|
155
|
+
expect(spy.entries[1].level).toBe('warn');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should not write entries below the configured level', async () => {
|
|
159
|
+
const plugin = new LoggingPlugin({ strategy: 'silent', level: 'error' });
|
|
160
|
+
const spy = injectSpyStorage(plugin);
|
|
161
|
+
|
|
162
|
+
await plugin.info('Info msg');
|
|
163
|
+
await plugin.debug('Debug msg');
|
|
164
|
+
await plugin.warn('Warn msg');
|
|
165
|
+
|
|
166
|
+
expect(spy.entries).toHaveLength(0);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should write all levels when set to debug', async () => {
|
|
170
|
+
const plugin = new LoggingPlugin({ strategy: 'silent', level: 'debug' });
|
|
171
|
+
const spy = injectSpyStorage(plugin);
|
|
172
|
+
|
|
173
|
+
await plugin.debug('d');
|
|
174
|
+
await plugin.info('i');
|
|
175
|
+
await plugin.warn('w');
|
|
176
|
+
await plugin.error('e');
|
|
177
|
+
|
|
178
|
+
expect(spy.entries).toHaveLength(4);
|
|
179
|
+
expect(spy.entries.map((e) => e.level)).toEqual(['debug', 'info', 'warn', 'error']);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// ----------------------------------------------------------------
|
|
184
|
+
// Log entry structure
|
|
185
|
+
// ----------------------------------------------------------------
|
|
186
|
+
describe('log entry structure', () => {
|
|
187
|
+
it('should write entry with timestamp, level, and message', async () => {
|
|
188
|
+
const plugin = new LoggingPlugin({ strategy: 'silent', level: 'info' });
|
|
189
|
+
const spy = injectSpyStorage(plugin);
|
|
190
|
+
|
|
191
|
+
await plugin.info('Test message');
|
|
192
|
+
|
|
193
|
+
expect(spy.entries).toHaveLength(1);
|
|
194
|
+
const entry = spy.entries[0];
|
|
195
|
+
expect(entry.level).toBe('info');
|
|
196
|
+
expect(entry.message).toBe('Test message');
|
|
197
|
+
expect(entry.timestamp).toBeInstanceOf(Date);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('should include context when provided', async () => {
|
|
201
|
+
const plugin = new LoggingPlugin({ strategy: 'silent', level: 'info' });
|
|
202
|
+
const spy = injectSpyStorage(plugin);
|
|
203
|
+
|
|
204
|
+
await plugin.info('With context', { operation: 'test', duration: 100 });
|
|
205
|
+
|
|
206
|
+
const entry = spy.entries[0];
|
|
207
|
+
expect(entry.context).toEqual({ operation: 'test', duration: 100 });
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('should include metadata when provided', async () => {
|
|
211
|
+
const plugin = new LoggingPlugin({ strategy: 'silent', level: 'info' });
|
|
212
|
+
const spy = injectSpyStorage(plugin);
|
|
213
|
+
|
|
214
|
+
await plugin.log('info', 'With metadata', undefined, { executionId: 'exec-1' });
|
|
215
|
+
|
|
216
|
+
const entry = spy.entries[0];
|
|
217
|
+
expect(entry.metadata).toEqual({ executionId: 'exec-1' });
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('should include error details when includeStackTrace is true', async () => {
|
|
221
|
+
const plugin = new LoggingPlugin({
|
|
222
|
+
strategy: 'silent',
|
|
223
|
+
level: 'error',
|
|
224
|
+
includeStackTrace: true,
|
|
225
|
+
});
|
|
226
|
+
const spy = injectSpyStorage(plugin);
|
|
227
|
+
|
|
228
|
+
const err = new Error('Stack trace test');
|
|
229
|
+
await plugin.error('Error occurred', err);
|
|
230
|
+
|
|
231
|
+
const entry = spy.entries[0];
|
|
232
|
+
expect(entry.context?.errorMessage).toBe('Stack trace test');
|
|
233
|
+
expect(entry.context?.errorStack).toBeDefined();
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// ----------------------------------------------------------------
|
|
238
|
+
// Convenience methods
|
|
239
|
+
// ----------------------------------------------------------------
|
|
240
|
+
describe('convenience methods', () => {
|
|
241
|
+
it('should log execution start with truncated user input', async () => {
|
|
242
|
+
const plugin = new LoggingPlugin({ strategy: 'silent', level: 'info' });
|
|
243
|
+
const spy = injectSpyStorage(plugin);
|
|
244
|
+
|
|
245
|
+
const longInput = 'x'.repeat(200);
|
|
246
|
+
await plugin.logExecutionStart('exec-1', longInput);
|
|
247
|
+
|
|
248
|
+
expect(spy.entries).toHaveLength(1);
|
|
249
|
+
const entry = spy.entries[0];
|
|
250
|
+
expect(entry.message).toBe('Execution started');
|
|
251
|
+
expect((entry.context?.userInput as string).length).toBeLessThanOrEqual(100);
|
|
252
|
+
expect(entry.metadata?.executionId).toBe('exec-1');
|
|
253
|
+
expect(entry.metadata?.operation).toBe('execution_start');
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('should log execution complete with duration', async () => {
|
|
257
|
+
const plugin = new LoggingPlugin({ strategy: 'silent', level: 'info' });
|
|
258
|
+
const spy = injectSpyStorage(plugin);
|
|
259
|
+
|
|
260
|
+
await plugin.logExecutionComplete('exec-1', 150);
|
|
261
|
+
|
|
262
|
+
const entry = spy.entries[0];
|
|
263
|
+
expect(entry.message).toBe('Execution completed');
|
|
264
|
+
expect(entry.context?.duration).toBe(150);
|
|
265
|
+
expect(entry.metadata?.executionId).toBe('exec-1');
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('should log successful tool execution at info level', async () => {
|
|
269
|
+
const plugin = new LoggingPlugin({ strategy: 'silent', level: 'info' });
|
|
270
|
+
const spy = injectSpyStorage(plugin);
|
|
271
|
+
|
|
272
|
+
await plugin.logToolExecution('search-tool', 'exec-1', 50, true);
|
|
273
|
+
|
|
274
|
+
const entry = spy.entries[0];
|
|
275
|
+
expect(entry.level).toBe('info');
|
|
276
|
+
expect(entry.message).toBe('Tool executed successfully');
|
|
277
|
+
expect(entry.context?.toolName).toBe('search-tool');
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('should log failed tool execution at error level', async () => {
|
|
281
|
+
const plugin = new LoggingPlugin({ strategy: 'silent', level: 'info' });
|
|
282
|
+
const spy = injectSpyStorage(plugin);
|
|
283
|
+
|
|
284
|
+
await plugin.logToolExecution('search-tool', 'exec-1', 50, false);
|
|
285
|
+
|
|
286
|
+
const entry = spy.entries[0];
|
|
287
|
+
expect(entry.level).toBe('error');
|
|
288
|
+
expect(entry.message).toBe('Tool execution failed');
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// ----------------------------------------------------------------
|
|
293
|
+
// Formatter integration
|
|
294
|
+
// ----------------------------------------------------------------
|
|
295
|
+
describe('formatter', () => {
|
|
296
|
+
it('should accept a custom formatter', () => {
|
|
297
|
+
const formatter: ILogFormatter = {
|
|
298
|
+
format(entry: ILogEntry): string {
|
|
299
|
+
return `[${entry.level}] ${entry.message}`;
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const plugin = new LoggingPlugin({
|
|
304
|
+
strategy: 'console',
|
|
305
|
+
formatter,
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
expect(plugin.name).toBe('LoggingPlugin');
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// ----------------------------------------------------------------
|
|
313
|
+
// Module event handling
|
|
314
|
+
// ----------------------------------------------------------------
|
|
315
|
+
describe('onModuleEvent', () => {
|
|
316
|
+
function createEventData(
|
|
317
|
+
overrides: Partial<IEventEmitterEventData> = {},
|
|
318
|
+
): IEventEmitterEventData {
|
|
319
|
+
return {
|
|
320
|
+
type: EVENT_EMITTER_EVENTS.MODULE_INITIALIZE_START,
|
|
321
|
+
timestamp: new Date(),
|
|
322
|
+
executionId: 'exec-1',
|
|
323
|
+
data: {
|
|
324
|
+
moduleName: 'TestModule',
|
|
325
|
+
moduleType: 'processor',
|
|
326
|
+
},
|
|
327
|
+
...overrides,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
it('should write info entry for MODULE_INITIALIZE_START', async () => {
|
|
332
|
+
const plugin = new LoggingPlugin({ strategy: 'silent', level: 'info' });
|
|
333
|
+
const spy = injectSpyStorage(plugin);
|
|
334
|
+
|
|
335
|
+
await plugin.onModuleEvent(EVENT_EMITTER_EVENTS.MODULE_INITIALIZE_START, createEventData());
|
|
336
|
+
|
|
337
|
+
expect(spy.entries).toHaveLength(1);
|
|
338
|
+
expect(spy.entries[0].level).toBe('info');
|
|
339
|
+
expect(spy.entries[0].message).toContain('initialization started');
|
|
340
|
+
expect(spy.entries[0].context?.moduleName).toBe('TestModule');
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('should write info entry for MODULE_INITIALIZE_COMPLETE with duration', async () => {
|
|
344
|
+
const plugin = new LoggingPlugin({ strategy: 'silent', level: 'info' });
|
|
345
|
+
const spy = injectSpyStorage(plugin);
|
|
346
|
+
|
|
347
|
+
await plugin.onModuleEvent(
|
|
348
|
+
EVENT_EMITTER_EVENTS.MODULE_INITIALIZE_COMPLETE,
|
|
349
|
+
createEventData({
|
|
350
|
+
type: EVENT_EMITTER_EVENTS.MODULE_INITIALIZE_COMPLETE,
|
|
351
|
+
data: { moduleName: 'TestModule', moduleType: 'processor', duration: 50 },
|
|
352
|
+
}),
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
expect(spy.entries).toHaveLength(1);
|
|
356
|
+
expect(spy.entries[0].message).toContain('initialization completed');
|
|
357
|
+
expect(spy.entries[0].context?.duration).toBe(50);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('should write error entry for MODULE_INITIALIZE_ERROR', async () => {
|
|
361
|
+
const plugin = new LoggingPlugin({ strategy: 'silent', level: 'info' });
|
|
362
|
+
const spy = injectSpyStorage(plugin);
|
|
363
|
+
|
|
364
|
+
await plugin.onModuleEvent(
|
|
365
|
+
EVENT_EMITTER_EVENTS.MODULE_INITIALIZE_ERROR,
|
|
366
|
+
createEventData({
|
|
367
|
+
type: EVENT_EMITTER_EVENTS.MODULE_INITIALIZE_ERROR,
|
|
368
|
+
error: new Error('Init failed'),
|
|
369
|
+
}),
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
expect(spy.entries).toHaveLength(1);
|
|
373
|
+
expect(spy.entries[0].level).toBe('error');
|
|
374
|
+
expect(spy.entries[0].message).toContain('initialization failed');
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it('should write debug entry for MODULE_EXECUTION_START', async () => {
|
|
378
|
+
const plugin = new LoggingPlugin({ strategy: 'silent', level: 'debug' });
|
|
379
|
+
const spy = injectSpyStorage(plugin);
|
|
380
|
+
|
|
381
|
+
await plugin.onModuleEvent(
|
|
382
|
+
EVENT_EMITTER_EVENTS.MODULE_EXECUTION_START,
|
|
383
|
+
createEventData({ type: EVENT_EMITTER_EVENTS.MODULE_EXECUTION_START }),
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
expect(spy.entries).toHaveLength(1);
|
|
387
|
+
expect(spy.entries[0].level).toBe('debug');
|
|
388
|
+
expect(spy.entries[0].message).toContain('execution started');
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it('should write error entry for MODULE_EXECUTION_ERROR', async () => {
|
|
392
|
+
const plugin = new LoggingPlugin({ strategy: 'silent', level: 'info' });
|
|
393
|
+
const spy = injectSpyStorage(plugin);
|
|
394
|
+
|
|
395
|
+
await plugin.onModuleEvent(
|
|
396
|
+
EVENT_EMITTER_EVENTS.MODULE_EXECUTION_ERROR,
|
|
397
|
+
createEventData({
|
|
398
|
+
type: EVENT_EMITTER_EVENTS.MODULE_EXECUTION_ERROR,
|
|
399
|
+
error: new Error('Execution failed'),
|
|
400
|
+
}),
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
expect(spy.entries).toHaveLength(1);
|
|
404
|
+
expect(spy.entries[0].level).toBe('error');
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it('should use unknown for missing module data', async () => {
|
|
408
|
+
const plugin = new LoggingPlugin({ strategy: 'silent', level: 'info' });
|
|
409
|
+
const spy = injectSpyStorage(plugin);
|
|
410
|
+
|
|
411
|
+
await plugin.onModuleEvent(EVENT_EMITTER_EVENTS.MODULE_INITIALIZE_START, {
|
|
412
|
+
type: EVENT_EMITTER_EVENTS.MODULE_INITIALIZE_START,
|
|
413
|
+
timestamp: new Date(),
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
expect(spy.entries).toHaveLength(1);
|
|
417
|
+
expect(spy.entries[0].context?.moduleName).toBe('unknown');
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// ----------------------------------------------------------------
|
|
422
|
+
// Flush and destroy
|
|
423
|
+
// ----------------------------------------------------------------
|
|
424
|
+
describe('flush and destroy', () => {
|
|
425
|
+
it('should delegate flush to storage', async () => {
|
|
426
|
+
const plugin = new LoggingPlugin({ strategy: 'silent' });
|
|
427
|
+
const spy = injectSpyStorage(plugin);
|
|
428
|
+
|
|
429
|
+
await plugin.flush();
|
|
430
|
+
|
|
431
|
+
expect(spy.flushCount).toBe(1);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it('should delegate close to storage on destroy', async () => {
|
|
435
|
+
const plugin = new LoggingPlugin({ strategy: 'silent' });
|
|
436
|
+
const spy = injectSpyStorage(plugin);
|
|
437
|
+
|
|
438
|
+
await plugin.destroy();
|
|
439
|
+
|
|
440
|
+
expect(spy.closeCount).toBe(1);
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
// ----------------------------------------------------------------
|
|
445
|
+
// Error resilience
|
|
446
|
+
// ----------------------------------------------------------------
|
|
447
|
+
describe('error resilience', () => {
|
|
448
|
+
it('should not throw when storage write fails', async () => {
|
|
449
|
+
const plugin = new LoggingPlugin({ strategy: 'silent', level: 'info' });
|
|
450
|
+
const failing = new FailingStorage();
|
|
451
|
+
(plugin as unknown as { storage: ILogStorage }).storage = failing;
|
|
452
|
+
|
|
453
|
+
await expect(plugin.info('This should not throw')).resolves.not.toThrow();
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it('should throw PluginError when flush fails', async () => {
|
|
457
|
+
const plugin = new LoggingPlugin({ strategy: 'silent' });
|
|
458
|
+
const failing = new FailingStorage();
|
|
459
|
+
(plugin as unknown as { storage: ILogStorage }).storage = failing;
|
|
460
|
+
|
|
461
|
+
await expect(plugin.flush()).rejects.toThrow(PluginError);
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
2
|
+
import { ConsoleLogStorage } from '../storages/console-storage';
|
|
3
|
+
import { SilentLogStorage } from '../storages/silent-storage';
|
|
4
|
+
import { RemoteLogStorage } from '../storages/remote-storage';
|
|
5
|
+
import type { ILogEntry } from '../types';
|
|
6
|
+
|
|
7
|
+
const entry: ILogEntry = {
|
|
8
|
+
timestamp: new Date(),
|
|
9
|
+
level: 'info',
|
|
10
|
+
message: 'Test log',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
describe('ConsoleLogStorage', () => {
|
|
14
|
+
it('writes debug log via logger', async () => {
|
|
15
|
+
const logger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn(), log: vi.fn() };
|
|
16
|
+
const storage = new ConsoleLogStorage(undefined, logger);
|
|
17
|
+
await storage.write({ ...entry, level: 'debug' });
|
|
18
|
+
expect(logger.debug).toHaveBeenCalled();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('writes info log via logger', async () => {
|
|
22
|
+
const logger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn(), log: vi.fn() };
|
|
23
|
+
const storage = new ConsoleLogStorage(undefined, logger);
|
|
24
|
+
await storage.write({ ...entry, level: 'info' });
|
|
25
|
+
expect(logger.info).toHaveBeenCalled();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('writes warn log via logger', async () => {
|
|
29
|
+
const logger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn(), log: vi.fn() };
|
|
30
|
+
const storage = new ConsoleLogStorage(undefined, logger);
|
|
31
|
+
await storage.write({ ...entry, level: 'warn' });
|
|
32
|
+
expect(logger.warn).toHaveBeenCalled();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('writes error log via logger', async () => {
|
|
36
|
+
const logger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn(), log: vi.fn() };
|
|
37
|
+
const storage = new ConsoleLogStorage(undefined, logger);
|
|
38
|
+
await storage.write({ ...entry, level: 'error' });
|
|
39
|
+
expect(logger.error).toHaveBeenCalled();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('flush is a no-op', async () => {
|
|
43
|
+
const storage = new ConsoleLogStorage();
|
|
44
|
+
await expect(storage.flush()).resolves.toBeUndefined();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('close is a no-op', async () => {
|
|
48
|
+
const storage = new ConsoleLogStorage();
|
|
49
|
+
await expect(storage.close()).resolves.toBeUndefined();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('SilentLogStorage', () => {
|
|
54
|
+
const storage = new SilentLogStorage();
|
|
55
|
+
|
|
56
|
+
it('write is a no-op', async () => {
|
|
57
|
+
await expect(storage.write(entry)).resolves.toBeUndefined();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('flush is a no-op', async () => {
|
|
61
|
+
await expect(storage.flush()).resolves.toBeUndefined();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('close is a no-op', async () => {
|
|
65
|
+
await expect(storage.close()).resolves.toBeUndefined();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('RemoteLogStorage', () => {
|
|
70
|
+
let storage: RemoteLogStorage;
|
|
71
|
+
|
|
72
|
+
afterEach(async () => {
|
|
73
|
+
if (storage) await storage.close();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('batches logs and flushes when batch size reached', async () => {
|
|
77
|
+
storage = new RemoteLogStorage('http://example.com/logs');
|
|
78
|
+
// Write entries below batch size (10)
|
|
79
|
+
for (let i = 0; i < 5; i++) {
|
|
80
|
+
await storage.write({ ...entry, message: `log-${i}` });
|
|
81
|
+
}
|
|
82
|
+
// No error thrown
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('flushes on close', async () => {
|
|
86
|
+
storage = new RemoteLogStorage('http://example.com/logs');
|
|
87
|
+
await storage.write(entry);
|
|
88
|
+
await expect(storage.close()).resolves.toBeUndefined();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('flush with no pending logs is no-op', async () => {
|
|
92
|
+
storage = new RemoteLogStorage('http://example.com/logs');
|
|
93
|
+
await expect(storage.flush()).resolves.toBeUndefined();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ILogEntry, ILogFormatter } from './types';
|
|
2
|
+
|
|
3
|
+
const LEVEL_PAD_WIDTH = 5;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Default console formatter
|
|
7
|
+
*/
|
|
8
|
+
export class ConsoleLogFormatter implements ILogFormatter {
|
|
9
|
+
format(entry: ILogEntry): string {
|
|
10
|
+
const timestamp = entry.timestamp.toISOString();
|
|
11
|
+
const level = entry.level.toUpperCase().padStart(LEVEL_PAD_WIDTH);
|
|
12
|
+
const contextStr = entry.context ? ` | ${JSON.stringify(entry.context)}` : '';
|
|
13
|
+
const metadataStr = entry.metadata ? ` | ${JSON.stringify(entry.metadata)}` : '';
|
|
14
|
+
return `[${timestamp}] ${level} | ${entry.message}${contextStr}${metadataStr}`;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* JSON formatter for file/remote logging
|
|
20
|
+
*/
|
|
21
|
+
export class JsonLogFormatter implements ILogFormatter {
|
|
22
|
+
format(entry: ILogEntry): string {
|
|
23
|
+
return JSON.stringify({
|
|
24
|
+
...entry,
|
|
25
|
+
timestamp: entry.timestamp.toISOString(),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { LoggingPlugin } from './logging-plugin';
|
|
2
|
+
export { ConsoleLogStorage } from './storages/console-storage';
|
|
3
|
+
export { FileLogStorage } from './storages/file-storage';
|
|
4
|
+
export { RemoteLogStorage } from './storages/remote-storage';
|
|
5
|
+
export { SilentLogStorage } from './storages/silent-storage';
|
|
6
|
+
export { ConsoleLogFormatter, JsonLogFormatter } from './formatters';
|
|
7
|
+
export type {
|
|
8
|
+
TLoggingStrategy,
|
|
9
|
+
TLogLevel,
|
|
10
|
+
ILogEntry,
|
|
11
|
+
ILoggingPluginOptions,
|
|
12
|
+
ILoggingPluginStats,
|
|
13
|
+
ILogFormatter,
|
|
14
|
+
ILogStorage,
|
|
15
|
+
} from './types';
|