@principal-ai/principal-view-core 0.5.6

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 (204) hide show
  1. package/README.md +126 -0
  2. package/dist/ConfigurationLoader.d.ts +76 -0
  3. package/dist/ConfigurationLoader.d.ts.map +1 -0
  4. package/dist/ConfigurationLoader.js +144 -0
  5. package/dist/ConfigurationLoader.js.map +1 -0
  6. package/dist/ConfigurationValidator.d.ts +31 -0
  7. package/dist/ConfigurationValidator.d.ts.map +1 -0
  8. package/dist/ConfigurationValidator.js +242 -0
  9. package/dist/ConfigurationValidator.js.map +1 -0
  10. package/dist/EventProcessor.d.ts +49 -0
  11. package/dist/EventProcessor.d.ts.map +1 -0
  12. package/dist/EventProcessor.js +215 -0
  13. package/dist/EventProcessor.js.map +1 -0
  14. package/dist/EventRecorderService.d.ts +305 -0
  15. package/dist/EventRecorderService.d.ts.map +1 -0
  16. package/dist/EventRecorderService.js +463 -0
  17. package/dist/EventRecorderService.js.map +1 -0
  18. package/dist/LibraryLoader.d.ts +63 -0
  19. package/dist/LibraryLoader.d.ts.map +1 -0
  20. package/dist/LibraryLoader.js +188 -0
  21. package/dist/LibraryLoader.js.map +1 -0
  22. package/dist/PathBasedEventProcessor.d.ts +90 -0
  23. package/dist/PathBasedEventProcessor.d.ts.map +1 -0
  24. package/dist/PathBasedEventProcessor.js +239 -0
  25. package/dist/PathBasedEventProcessor.js.map +1 -0
  26. package/dist/SessionManager.d.ts +194 -0
  27. package/dist/SessionManager.d.ts.map +1 -0
  28. package/dist/SessionManager.js +299 -0
  29. package/dist/SessionManager.js.map +1 -0
  30. package/dist/ValidationEngine.d.ts +31 -0
  31. package/dist/ValidationEngine.d.ts.map +1 -0
  32. package/dist/ValidationEngine.js +158 -0
  33. package/dist/ValidationEngine.js.map +1 -0
  34. package/dist/helpers/GraphInstrumentationHelper.d.ts +93 -0
  35. package/dist/helpers/GraphInstrumentationHelper.d.ts.map +1 -0
  36. package/dist/helpers/GraphInstrumentationHelper.js +248 -0
  37. package/dist/helpers/GraphInstrumentationHelper.js.map +1 -0
  38. package/dist/index.d.ts +33 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.js +34 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/rules/config.d.ts +57 -0
  43. package/dist/rules/config.d.ts.map +1 -0
  44. package/dist/rules/config.js +382 -0
  45. package/dist/rules/config.js.map +1 -0
  46. package/dist/rules/engine.d.ts +70 -0
  47. package/dist/rules/engine.d.ts.map +1 -0
  48. package/dist/rules/engine.js +252 -0
  49. package/dist/rules/engine.js.map +1 -0
  50. package/dist/rules/implementations/connection-type-references.d.ts +7 -0
  51. package/dist/rules/implementations/connection-type-references.d.ts.map +1 -0
  52. package/dist/rules/implementations/connection-type-references.js +104 -0
  53. package/dist/rules/implementations/connection-type-references.js.map +1 -0
  54. package/dist/rules/implementations/dead-end-states.d.ts +17 -0
  55. package/dist/rules/implementations/dead-end-states.d.ts.map +1 -0
  56. package/dist/rules/implementations/dead-end-states.js +72 -0
  57. package/dist/rules/implementations/dead-end-states.js.map +1 -0
  58. package/dist/rules/implementations/index.d.ts +24 -0
  59. package/dist/rules/implementations/index.d.ts.map +1 -0
  60. package/dist/rules/implementations/index.js +62 -0
  61. package/dist/rules/implementations/index.js.map +1 -0
  62. package/dist/rules/implementations/library-node-type-match.d.ts +17 -0
  63. package/dist/rules/implementations/library-node-type-match.d.ts.map +1 -0
  64. package/dist/rules/implementations/library-node-type-match.js +123 -0
  65. package/dist/rules/implementations/library-node-type-match.js.map +1 -0
  66. package/dist/rules/implementations/minimum-node-sources.d.ts +22 -0
  67. package/dist/rules/implementations/minimum-node-sources.d.ts.map +1 -0
  68. package/dist/rules/implementations/minimum-node-sources.js +54 -0
  69. package/dist/rules/implementations/minimum-node-sources.js.map +1 -0
  70. package/dist/rules/implementations/no-unknown-fields.d.ts +7 -0
  71. package/dist/rules/implementations/no-unknown-fields.d.ts.map +1 -0
  72. package/dist/rules/implementations/no-unknown-fields.js +211 -0
  73. package/dist/rules/implementations/no-unknown-fields.js.map +1 -0
  74. package/dist/rules/implementations/orphaned-edge-types.d.ts +7 -0
  75. package/dist/rules/implementations/orphaned-edge-types.d.ts.map +1 -0
  76. package/dist/rules/implementations/orphaned-edge-types.js +47 -0
  77. package/dist/rules/implementations/orphaned-edge-types.js.map +1 -0
  78. package/dist/rules/implementations/orphaned-node-types.d.ts +7 -0
  79. package/dist/rules/implementations/orphaned-node-types.d.ts.map +1 -0
  80. package/dist/rules/implementations/orphaned-node-types.js +50 -0
  81. package/dist/rules/implementations/orphaned-node-types.js.map +1 -0
  82. package/dist/rules/implementations/required-metadata.d.ts +7 -0
  83. package/dist/rules/implementations/required-metadata.d.ts.map +1 -0
  84. package/dist/rules/implementations/required-metadata.js +57 -0
  85. package/dist/rules/implementations/required-metadata.js.map +1 -0
  86. package/dist/rules/implementations/state-transition-references.d.ts +7 -0
  87. package/dist/rules/implementations/state-transition-references.d.ts.map +1 -0
  88. package/dist/rules/implementations/state-transition-references.js +135 -0
  89. package/dist/rules/implementations/state-transition-references.js.map +1 -0
  90. package/dist/rules/implementations/unreachable-states.d.ts +7 -0
  91. package/dist/rules/implementations/unreachable-states.d.ts.map +1 -0
  92. package/dist/rules/implementations/unreachable-states.js +80 -0
  93. package/dist/rules/implementations/unreachable-states.js.map +1 -0
  94. package/dist/rules/implementations/valid-action-patterns.d.ts +17 -0
  95. package/dist/rules/implementations/valid-action-patterns.d.ts.map +1 -0
  96. package/dist/rules/implementations/valid-action-patterns.js +109 -0
  97. package/dist/rules/implementations/valid-action-patterns.js.map +1 -0
  98. package/dist/rules/implementations/valid-color-format.d.ts +7 -0
  99. package/dist/rules/implementations/valid-color-format.d.ts.map +1 -0
  100. package/dist/rules/implementations/valid-color-format.js +91 -0
  101. package/dist/rules/implementations/valid-color-format.js.map +1 -0
  102. package/dist/rules/implementations/valid-edge-types.d.ts +7 -0
  103. package/dist/rules/implementations/valid-edge-types.d.ts.map +1 -0
  104. package/dist/rules/implementations/valid-edge-types.js +244 -0
  105. package/dist/rules/implementations/valid-edge-types.js.map +1 -0
  106. package/dist/rules/implementations/valid-node-types.d.ts +7 -0
  107. package/dist/rules/implementations/valid-node-types.d.ts.map +1 -0
  108. package/dist/rules/implementations/valid-node-types.js +175 -0
  109. package/dist/rules/implementations/valid-node-types.js.map +1 -0
  110. package/dist/rules/index.d.ts +28 -0
  111. package/dist/rules/index.d.ts.map +1 -0
  112. package/dist/rules/index.js +45 -0
  113. package/dist/rules/index.js.map +1 -0
  114. package/dist/rules/types.d.ts +309 -0
  115. package/dist/rules/types.d.ts.map +1 -0
  116. package/dist/rules/types.js +35 -0
  117. package/dist/rules/types.js.map +1 -0
  118. package/dist/types/canvas.d.ts +409 -0
  119. package/dist/types/canvas.d.ts.map +1 -0
  120. package/dist/types/canvas.js +70 -0
  121. package/dist/types/canvas.js.map +1 -0
  122. package/dist/types/index.d.ts +311 -0
  123. package/dist/types/index.d.ts.map +1 -0
  124. package/dist/types/index.js +13 -0
  125. package/dist/types/index.js.map +1 -0
  126. package/dist/types/library.d.ts +185 -0
  127. package/dist/types/library.d.ts.map +1 -0
  128. package/dist/types/library.js +15 -0
  129. package/dist/types/library.js.map +1 -0
  130. package/dist/types/path-based-config.d.ts +230 -0
  131. package/dist/types/path-based-config.d.ts.map +1 -0
  132. package/dist/types/path-based-config.js +9 -0
  133. package/dist/types/path-based-config.js.map +1 -0
  134. package/dist/utils/CanvasConverter.d.ts +118 -0
  135. package/dist/utils/CanvasConverter.d.ts.map +1 -0
  136. package/dist/utils/CanvasConverter.js +315 -0
  137. package/dist/utils/CanvasConverter.js.map +1 -0
  138. package/dist/utils/GraphConverter.d.ts +18 -0
  139. package/dist/utils/GraphConverter.d.ts.map +1 -0
  140. package/dist/utils/GraphConverter.js +61 -0
  141. package/dist/utils/GraphConverter.js.map +1 -0
  142. package/dist/utils/LibraryConverter.d.ts +113 -0
  143. package/dist/utils/LibraryConverter.d.ts.map +1 -0
  144. package/dist/utils/LibraryConverter.js +166 -0
  145. package/dist/utils/LibraryConverter.js.map +1 -0
  146. package/dist/utils/PathMatcher.d.ts +55 -0
  147. package/dist/utils/PathMatcher.d.ts.map +1 -0
  148. package/dist/utils/PathMatcher.js +172 -0
  149. package/dist/utils/PathMatcher.js.map +1 -0
  150. package/dist/utils/YamlParser.d.ts +36 -0
  151. package/dist/utils/YamlParser.d.ts.map +1 -0
  152. package/dist/utils/YamlParser.js +63 -0
  153. package/dist/utils/YamlParser.js.map +1 -0
  154. package/package.json +47 -0
  155. package/src/ConfigurationLoader.test.ts +490 -0
  156. package/src/ConfigurationLoader.ts +185 -0
  157. package/src/ConfigurationValidator.test.ts +200 -0
  158. package/src/ConfigurationValidator.ts +283 -0
  159. package/src/EventProcessor.test.ts +405 -0
  160. package/src/EventProcessor.ts +250 -0
  161. package/src/EventRecorderService.test.ts +541 -0
  162. package/src/EventRecorderService.ts +744 -0
  163. package/src/LibraryLoader.ts +215 -0
  164. package/src/PathBasedEventProcessor.test.ts +567 -0
  165. package/src/PathBasedEventProcessor.ts +332 -0
  166. package/src/SessionManager.test.ts +424 -0
  167. package/src/SessionManager.ts +470 -0
  168. package/src/ValidationEngine.test.ts +371 -0
  169. package/src/ValidationEngine.ts +196 -0
  170. package/src/helpers/GraphInstrumentationHelper.test.ts +340 -0
  171. package/src/helpers/GraphInstrumentationHelper.ts +326 -0
  172. package/src/index.ts +85 -0
  173. package/src/rules/config.test.ts +278 -0
  174. package/src/rules/config.ts +459 -0
  175. package/src/rules/engine.test.ts +332 -0
  176. package/src/rules/engine.ts +318 -0
  177. package/src/rules/implementations/connection-type-references.ts +117 -0
  178. package/src/rules/implementations/dead-end-states.ts +101 -0
  179. package/src/rules/implementations/index.ts +73 -0
  180. package/src/rules/implementations/library-node-type-match.ts +148 -0
  181. package/src/rules/implementations/minimum-node-sources.ts +82 -0
  182. package/src/rules/implementations/no-unknown-fields.ts +342 -0
  183. package/src/rules/implementations/orphaned-edge-types.ts +55 -0
  184. package/src/rules/implementations/orphaned-node-types.ts +58 -0
  185. package/src/rules/implementations/required-metadata.ts +64 -0
  186. package/src/rules/implementations/state-transition-references.ts +151 -0
  187. package/src/rules/implementations/unreachable-states.ts +94 -0
  188. package/src/rules/implementations/valid-action-patterns.ts +136 -0
  189. package/src/rules/implementations/valid-color-format.ts +140 -0
  190. package/src/rules/implementations/valid-edge-types.ts +258 -0
  191. package/src/rules/implementations/valid-node-types.ts +189 -0
  192. package/src/rules/index.ts +95 -0
  193. package/src/rules/types.ts +426 -0
  194. package/src/types/canvas.ts +496 -0
  195. package/src/types/index.ts +382 -0
  196. package/src/types/library.ts +233 -0
  197. package/src/types/path-based-config.ts +281 -0
  198. package/src/utils/CanvasConverter.ts +431 -0
  199. package/src/utils/GraphConverter.test.ts +195 -0
  200. package/src/utils/GraphConverter.ts +71 -0
  201. package/src/utils/LibraryConverter.ts +245 -0
  202. package/src/utils/PathMatcher.test.ts +148 -0
  203. package/src/utils/PathMatcher.ts +183 -0
  204. package/src/utils/YamlParser.ts +75 -0
@@ -0,0 +1,424 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
2
+ import { SessionManager } from './SessionManager';
3
+ import type { GraphEvent } from './types';
4
+
5
+ describe('SessionManager', () => {
6
+ let manager: SessionManager;
7
+
8
+ beforeEach(() => {
9
+ manager = new SessionManager({ autoCleanup: false });
10
+ });
11
+
12
+ afterEach(() => {
13
+ manager.dispose();
14
+ });
15
+
16
+ // Helper to create a mock event
17
+ const createMockEvent = (id: string): GraphEvent => ({
18
+ id,
19
+ type: 'test_event',
20
+ timestamp: Date.now(),
21
+ category: 'state',
22
+ operation: 'update',
23
+ payload: { nodeId: 'test-node', newState: 'active' },
24
+ });
25
+
26
+ describe('createSession', () => {
27
+ it('should create a new session with generated ID', () => {
28
+ const session = manager.createSession({ name: 'Test Session' });
29
+
30
+ expect(session.id).toMatch(/^session-/);
31
+ expect(session.name).toBe('Test Session');
32
+ expect(session.status).toBe('recording');
33
+ expect(session.events).toEqual([]);
34
+ expect(session.startedAt).toBeLessThanOrEqual(Date.now());
35
+ });
36
+
37
+ it('should create a session with custom ID', () => {
38
+ const session = manager.createSession({
39
+ name: 'Test Session',
40
+ id: 'custom-id',
41
+ });
42
+
43
+ expect(session.id).toBe('custom-id');
44
+ });
45
+
46
+ it('should create a session with metadata', () => {
47
+ const session = manager.createSession({
48
+ name: 'Test Session',
49
+ metadata: {
50
+ testFile: 'test/foo.test.ts',
51
+ testName: 'should do something',
52
+ tags: ['unit', 'fast'],
53
+ },
54
+ });
55
+
56
+ expect(session.metadata.testFile).toBe('test/foo.test.ts');
57
+ expect(session.metadata.testName).toBe('should do something');
58
+ expect(session.metadata.tags).toEqual(['unit', 'fast']);
59
+ });
60
+
61
+ it('should throw if session ID already exists', () => {
62
+ manager.createSession({ name: 'First', id: 'duplicate' });
63
+
64
+ expect(() => {
65
+ manager.createSession({ name: 'Second', id: 'duplicate' });
66
+ }).toThrow('already exists');
67
+ });
68
+
69
+ it('should set the created session as active', () => {
70
+ const session = manager.createSession({ name: 'Test' });
71
+ const active = manager.getActiveSession();
72
+
73
+ expect(active).toBeDefined();
74
+ expect(active?.id).toBe(session.id);
75
+ });
76
+ });
77
+
78
+ describe('getSession', () => {
79
+ it('should return session by ID', () => {
80
+ const created = manager.createSession({ name: 'Test', id: 'my-id' });
81
+ const fetched = manager.getSession('my-id');
82
+
83
+ expect(fetched).toEqual(created);
84
+ });
85
+
86
+ it('should return undefined for non-existent ID', () => {
87
+ const fetched = manager.getSession('non-existent');
88
+ expect(fetched).toBeUndefined();
89
+ });
90
+ });
91
+
92
+ describe('listSessions', () => {
93
+ it('should return empty array when no sessions', () => {
94
+ const sessions = manager.listSessions();
95
+ expect(sessions).toEqual([]);
96
+ });
97
+
98
+ it('should return sessions sorted by start time (newest first)', async () => {
99
+ manager.createSession({ name: 'First' });
100
+ await new Promise(resolve => setTimeout(resolve, 5));
101
+ manager.createSession({ name: 'Second' });
102
+ await new Promise(resolve => setTimeout(resolve, 5));
103
+ manager.createSession({ name: 'Third' });
104
+
105
+ const sessions = manager.listSessions();
106
+
107
+ expect(sessions).toHaveLength(3);
108
+ expect(sessions[0].name).toBe('Third');
109
+ expect(sessions[1].name).toBe('Second');
110
+ expect(sessions[2].name).toBe('First');
111
+ });
112
+ });
113
+
114
+ describe('addEvent', () => {
115
+ it('should add event to session', () => {
116
+ const session = manager.createSession({ name: 'Test' });
117
+ const event = createMockEvent('evt-1');
118
+
119
+ manager.addEvent(session.id, event);
120
+
121
+ const updated = manager.getSession(session.id);
122
+ expect(updated?.events).toHaveLength(1);
123
+ expect(updated?.events[0]).toEqual(event);
124
+ });
125
+
126
+ it('should add multiple events in order', () => {
127
+ const session = manager.createSession({ name: 'Test' });
128
+
129
+ manager.addEvent(session.id, createMockEvent('evt-1'));
130
+ manager.addEvent(session.id, createMockEvent('evt-2'));
131
+ manager.addEvent(session.id, createMockEvent('evt-3'));
132
+
133
+ const updated = manager.getSession(session.id);
134
+ expect(updated?.events).toHaveLength(3);
135
+ expect(updated?.events[0].id).toBe('evt-1');
136
+ expect(updated?.events[1].id).toBe('evt-2');
137
+ expect(updated?.events[2].id).toBe('evt-3');
138
+ });
139
+
140
+ it('should throw for non-existent session', () => {
141
+ expect(() => {
142
+ manager.addEvent('non-existent', createMockEvent('evt-1'));
143
+ }).toThrow('not found');
144
+ });
145
+
146
+ it('should throw for completed session', () => {
147
+ const session = manager.createSession({ name: 'Test' });
148
+ manager.endSession(session.id);
149
+
150
+ expect(() => {
151
+ manager.addEvent(session.id, createMockEvent('evt-1'));
152
+ }).toThrow('not recording');
153
+ });
154
+ });
155
+
156
+ describe('addEventToActive', () => {
157
+ it('should add event to active session', () => {
158
+ manager.createSession({ name: 'Test' });
159
+ const event = createMockEvent('evt-1');
160
+
161
+ manager.addEventToActive(event);
162
+
163
+ const active = manager.getActiveSession();
164
+ expect(active?.events).toHaveLength(1);
165
+ });
166
+
167
+ it('should throw when no active session', () => {
168
+ expect(() => {
169
+ manager.addEventToActive(createMockEvent('evt-1'));
170
+ }).toThrow('No active session');
171
+ });
172
+ });
173
+
174
+ describe('endSession', () => {
175
+ it('should mark session as completed', () => {
176
+ const session = manager.createSession({ name: 'Test' });
177
+ manager.endSession(session.id);
178
+
179
+ const updated = manager.getSession(session.id);
180
+ expect(updated?.status).toBe('completed');
181
+ expect(updated?.endedAt).toBeDefined();
182
+ });
183
+
184
+ it('should set result and duration', () => {
185
+ const session = manager.createSession({ name: 'Test' });
186
+ manager.endSession(session.id, { result: 'pass' });
187
+
188
+ const updated = manager.getSession(session.id);
189
+ expect(updated?.metadata.result).toBe('pass');
190
+ expect(updated?.metadata.duration).toBeGreaterThanOrEqual(0);
191
+ });
192
+
193
+ it('should set error message on failure', () => {
194
+ const session = manager.createSession({ name: 'Test' });
195
+ manager.endSession(session.id, { result: 'fail', error: 'Something went wrong' });
196
+
197
+ const updated = manager.getSession(session.id);
198
+ expect(updated?.metadata.result).toBe('fail');
199
+ expect(updated?.metadata.error).toBe('Something went wrong');
200
+ });
201
+
202
+ it('should clear active session', () => {
203
+ const session = manager.createSession({ name: 'Test' });
204
+ expect(manager.getActiveSession()).toBeDefined();
205
+
206
+ manager.endSession(session.id);
207
+ expect(manager.getActiveSession()).toBeUndefined();
208
+ });
209
+
210
+ it('should throw for non-existent session', () => {
211
+ expect(() => {
212
+ manager.endSession('non-existent');
213
+ }).toThrow('not found');
214
+ });
215
+
216
+ it('should throw for already completed session', () => {
217
+ const session = manager.createSession({ name: 'Test' });
218
+ manager.endSession(session.id);
219
+
220
+ expect(() => {
221
+ manager.endSession(session.id);
222
+ }).toThrow('not recording');
223
+ });
224
+ });
225
+
226
+ describe('errorSession', () => {
227
+ it('should mark session as error with message', () => {
228
+ const session = manager.createSession({ name: 'Test' });
229
+ manager.errorSession(session.id, 'Connection failed');
230
+
231
+ const updated = manager.getSession(session.id);
232
+ expect(updated?.status).toBe('error');
233
+ expect(updated?.metadata.error).toBe('Connection failed');
234
+ expect(updated?.endedAt).toBeDefined();
235
+ });
236
+ });
237
+
238
+ describe('deleteSession', () => {
239
+ it('should delete session', () => {
240
+ const session = manager.createSession({ name: 'Test' });
241
+ expect(manager.getSession(session.id)).toBeDefined();
242
+
243
+ const deleted = manager.deleteSession(session.id);
244
+ expect(deleted).toBe(true);
245
+ expect(manager.getSession(session.id)).toBeUndefined();
246
+ });
247
+
248
+ it('should return false for non-existent session', () => {
249
+ const deleted = manager.deleteSession('non-existent');
250
+ expect(deleted).toBe(false);
251
+ });
252
+
253
+ it('should clear active session if deleted', () => {
254
+ const session = manager.createSession({ name: 'Test' });
255
+ expect(manager.getActiveSession()).toBeDefined();
256
+
257
+ manager.deleteSession(session.id);
258
+ expect(manager.getActiveSession()).toBeUndefined();
259
+ });
260
+ });
261
+
262
+ describe('clearSessions', () => {
263
+ it('should clear all sessions', () => {
264
+ manager.createSession({ name: 'First' });
265
+ manager.createSession({ name: 'Second' });
266
+ manager.createSession({ name: 'Third' });
267
+
268
+ expect(manager.listSessions()).toHaveLength(3);
269
+
270
+ manager.clearSessions();
271
+
272
+ expect(manager.listSessions()).toHaveLength(0);
273
+ expect(manager.getActiveSession()).toBeUndefined();
274
+ });
275
+ });
276
+
277
+ describe('exportSession / importSession', () => {
278
+ it('should export session to JSON', () => {
279
+ const session = manager.createSession({ name: 'Test' });
280
+ manager.addEvent(session.id, createMockEvent('evt-1'));
281
+ manager.endSession(session.id, { result: 'pass' });
282
+
283
+ const json = manager.exportSession(session.id);
284
+ const parsed = JSON.parse(json);
285
+
286
+ expect(parsed.name).toBe('Test');
287
+ expect(parsed.events).toHaveLength(1);
288
+ expect(parsed.status).toBe('completed');
289
+ });
290
+
291
+ it('should import session from JSON', () => {
292
+ const original = manager.createSession({ name: 'Original' });
293
+ manager.addEvent(original.id, createMockEvent('evt-1'));
294
+ manager.endSession(original.id);
295
+
296
+ const json = manager.exportSession(original.id);
297
+ manager.clearSessions();
298
+
299
+ const imported = manager.importSession(json);
300
+
301
+ expect(imported.name).toBe('Original');
302
+ expect(imported.events).toHaveLength(1);
303
+ expect(imported.status).toBe('completed');
304
+ });
305
+
306
+ it('should generate new ID if duplicate on import', () => {
307
+ const original = manager.createSession({ name: 'Test', id: 'my-id' });
308
+ manager.endSession(original.id);
309
+ const json = manager.exportSession(original.id);
310
+
311
+ const imported = manager.importSession(json);
312
+
313
+ expect(imported.id).not.toBe('my-id');
314
+ expect(manager.listSessions()).toHaveLength(2);
315
+ });
316
+
317
+ it('should throw for invalid JSON format', () => {
318
+ expect(() => {
319
+ manager.importSession('{}');
320
+ }).toThrow('Invalid session format');
321
+ });
322
+ });
323
+
324
+ describe('onSessionChange', () => {
325
+ it('should call listener immediately with current sessions', () => {
326
+ manager.createSession({ name: 'Existing' });
327
+
328
+ let received: any[] = [];
329
+ manager.onSessionChange((sessions) => {
330
+ received = sessions;
331
+ });
332
+
333
+ expect(received).toHaveLength(1);
334
+ expect(received[0].name).toBe('Existing');
335
+ });
336
+
337
+ it('should call listener on session create', () => {
338
+ let callCount = 0;
339
+ manager.onSessionChange(() => {
340
+ callCount++;
341
+ });
342
+
343
+ manager.createSession({ name: 'New' });
344
+
345
+ expect(callCount).toBe(2); // Initial + create
346
+ });
347
+
348
+ it('should call listener on event add', () => {
349
+ const session = manager.createSession({ name: 'Test' });
350
+
351
+ let callCount = 0;
352
+ manager.onSessionChange(() => {
353
+ callCount++;
354
+ });
355
+
356
+ manager.addEvent(session.id, createMockEvent('evt-1'));
357
+
358
+ expect(callCount).toBe(2); // Initial + add
359
+ });
360
+
361
+ it('should unsubscribe when returned function is called', () => {
362
+ let callCount = 0;
363
+ const unsubscribe = manager.onSessionChange(() => {
364
+ callCount++;
365
+ });
366
+
367
+ expect(callCount).toBe(1); // Initial
368
+
369
+ unsubscribe();
370
+ manager.createSession({ name: 'New' });
371
+
372
+ expect(callCount).toBe(1); // No additional calls
373
+ });
374
+ });
375
+
376
+ describe('getStats', () => {
377
+ it('should return correct stats', () => {
378
+ const session1 = manager.createSession({ name: 'First' });
379
+ manager.addEvent(session1.id, createMockEvent('evt-1'));
380
+ manager.addEvent(session1.id, createMockEvent('evt-2'));
381
+ manager.endSession(session1.id);
382
+
383
+ const session2 = manager.createSession({ name: 'Second' });
384
+ manager.addEvent(session2.id, createMockEvent('evt-3'));
385
+
386
+ const stats = manager.getStats();
387
+
388
+ expect(stats.totalSessions).toBe(2);
389
+ expect(stats.activeSessions).toBe(1);
390
+ expect(stats.totalEvents).toBe(3);
391
+ expect(stats.oldestSession).toBe(session1.startedAt);
392
+ expect(stats.newestSession).toBe(session2.startedAt);
393
+ });
394
+
395
+ it('should return null timestamps when no sessions', () => {
396
+ const stats = manager.getStats();
397
+
398
+ expect(stats.totalSessions).toBe(0);
399
+ expect(stats.oldestSession).toBeNull();
400
+ expect(stats.newestSession).toBeNull();
401
+ });
402
+ });
403
+
404
+ describe('max events limit', () => {
405
+ it('should not add events beyond limit', () => {
406
+ const manager = new SessionManager({
407
+ maxEventsPerSession: 3,
408
+ autoCleanup: false,
409
+ });
410
+
411
+ const session = manager.createSession({ name: 'Test' });
412
+
413
+ manager.addEvent(session.id, createMockEvent('evt-1'));
414
+ manager.addEvent(session.id, createMockEvent('evt-2'));
415
+ manager.addEvent(session.id, createMockEvent('evt-3'));
416
+ manager.addEvent(session.id, createMockEvent('evt-4')); // Should be ignored
417
+
418
+ const updated = manager.getSession(session.id);
419
+ expect(updated?.events).toHaveLength(3);
420
+
421
+ manager.dispose();
422
+ });
423
+ });
424
+ });