@principal-ai/control-tower-core 0.2.1 → 0.2.2

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 (31) hide show
  1. package/dist/generated/client-connection-auth.types.d.ts +312 -0
  2. package/dist/generated/client-connection-auth.types.d.ts.map +1 -0
  3. package/dist/generated/client-connection-auth.types.js +11 -0
  4. package/dist/generated/control-tower-execution.types.d.ts +445 -0
  5. package/dist/generated/control-tower-execution.types.d.ts.map +1 -0
  6. package/dist/generated/control-tower-execution.types.js +11 -0
  7. package/dist/index.js.map +3 -3
  8. package/dist/index.mjs +39 -6
  9. package/dist/index.mjs.map +3 -3
  10. package/dist/server/BaseServer.d.ts +22 -2
  11. package/dist/server/BaseServer.d.ts.map +1 -1
  12. package/dist/server/BaseServer.js +63 -8
  13. package/dist/telemetry/EventValidationIntegration.d.ts +135 -0
  14. package/dist/telemetry/EventValidationIntegration.d.ts.map +1 -0
  15. package/dist/telemetry/EventValidationIntegration.js +253 -0
  16. package/dist/telemetry/EventValidationIntegration.test.d.ts +7 -0
  17. package/dist/telemetry/EventValidationIntegration.test.d.ts.map +1 -0
  18. package/dist/telemetry/EventValidationIntegration.test.js +322 -0
  19. package/dist/telemetry/TelemetryCapture.d.ts +268 -0
  20. package/dist/telemetry/TelemetryCapture.d.ts.map +1 -0
  21. package/dist/telemetry/TelemetryCapture.js +263 -0
  22. package/dist/telemetry/TelemetryCapture.test.d.ts +7 -0
  23. package/dist/telemetry/TelemetryCapture.test.d.ts.map +1 -0
  24. package/dist/telemetry/TelemetryCapture.test.js +396 -0
  25. package/dist/telemetry-example.d.ts +33 -0
  26. package/dist/telemetry-example.d.ts.map +1 -0
  27. package/dist/telemetry-example.js +124 -0
  28. package/package.json +1 -1
  29. package/dist/adapters/websocket/WebSocketTransportAdapter.d.ts +0 -60
  30. package/dist/adapters/websocket/WebSocketTransportAdapter.d.ts.map +0 -1
  31. package/dist/adapters/websocket/WebSocketTransportAdapter.js +0 -386
@@ -0,0 +1,263 @@
1
+ "use strict";
2
+ /**
3
+ * Telemetry Capture and Replay System
4
+ *
5
+ * Allows capturing telemetry events during test execution and replaying them later.
6
+ * This enables:
7
+ * - Saving telemetry as test artifacts
8
+ * - Loading telemetry for visualization
9
+ * - Not depending on live emission during tests
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.TelemetryPlayer = exports.TelemetryCapture = void 0;
13
+ exports.saveTelemetryArtifact = saveTelemetryArtifact;
14
+ exports.loadTelemetryArtifact = loadTelemetryArtifact;
15
+ /**
16
+ * Telemetry Capture
17
+ *
18
+ * Captures telemetry events in memory and exports them as artifacts.
19
+ */
20
+ class TelemetryCapture {
21
+ constructor(options = {}) {
22
+ this.events = [];
23
+ this.sequenceCounter = 0;
24
+ this.startTime = Date.now();
25
+ this.metadata = {
26
+ sessionId: options.sessionId || `session-${this.startTime}-${Math.random().toString(36).slice(2)}`,
27
+ startTime: this.startTime,
28
+ testName: options.testName,
29
+ testCategory: options.testCategory,
30
+ tags: options.tags,
31
+ };
32
+ }
33
+ /**
34
+ * Create an event handler for ControlTowerTelemetry
35
+ */
36
+ createEventHandler() {
37
+ return (nodeId, eventName, attributes) => {
38
+ this.captureEvent(nodeId, eventName, attributes);
39
+ };
40
+ }
41
+ /**
42
+ * Capture a telemetry event
43
+ */
44
+ captureEvent(nodeId, eventName, attributes) {
45
+ const event = {
46
+ timestamp: Date.now(),
47
+ nodeId,
48
+ eventName,
49
+ attributes: { ...attributes }, // Clone to avoid mutation
50
+ sequenceNumber: this.sequenceCounter++,
51
+ };
52
+ this.events.push(event);
53
+ }
54
+ /**
55
+ * Stop capturing and finalize metadata
56
+ */
57
+ stop() {
58
+ this.metadata.endTime = Date.now();
59
+ }
60
+ /**
61
+ * Export as artifact in OpenTelemetry spans format
62
+ */
63
+ export() {
64
+ if (!this.metadata.endTime) {
65
+ this.stop();
66
+ }
67
+ const duration = this.metadata.endTime - this.metadata.startTime;
68
+ // Create a single span containing all events
69
+ const span = {
70
+ id: this.metadata.sessionId,
71
+ name: this.metadata.testName || 'test-execution',
72
+ startTime: this.metadata.startTime,
73
+ endTime: this.metadata.endTime,
74
+ duration,
75
+ status: 'OK',
76
+ attributes: {
77
+ 'test.category': this.metadata.testCategory,
78
+ 'test.name': this.metadata.testName,
79
+ ...this.metadata.tags,
80
+ },
81
+ events: this.events.map(e => ({
82
+ time: e.timestamp,
83
+ name: e.eventName,
84
+ attributes: {
85
+ 'node.id': e.nodeId,
86
+ ...e.attributes,
87
+ },
88
+ })),
89
+ };
90
+ return {
91
+ metadata: {
92
+ canvasName: this.metadata.tags?.canvas,
93
+ exportedAt: new Date(this.metadata.endTime).toISOString(),
94
+ source: this.metadata.testName,
95
+ framework: 'bun:test',
96
+ status: 'OK',
97
+ testCategory: this.metadata.testCategory,
98
+ tags: this.metadata.tags,
99
+ },
100
+ spans: [span],
101
+ };
102
+ }
103
+ /**
104
+ * Export as JSON string
105
+ */
106
+ exportJSON(pretty = true) {
107
+ return JSON.stringify(this.export(), null, pretty ? 2 : 0);
108
+ }
109
+ /**
110
+ * Get events captured so far
111
+ */
112
+ getEvents() {
113
+ return this.events;
114
+ }
115
+ /**
116
+ * Get event count
117
+ */
118
+ getEventCount() {
119
+ return this.events.length;
120
+ }
121
+ /**
122
+ * Clear all captured events
123
+ */
124
+ clear() {
125
+ this.events = [];
126
+ this.sequenceCounter = 0;
127
+ }
128
+ }
129
+ exports.TelemetryCapture = TelemetryCapture;
130
+ /**
131
+ * Telemetry Player
132
+ *
133
+ * Replays captured telemetry events.
134
+ */
135
+ class TelemetryPlayer {
136
+ constructor(artifact) {
137
+ this.currentIndex = 0;
138
+ this.artifact = artifact;
139
+ }
140
+ /**
141
+ * Load artifact from JSON string
142
+ */
143
+ static fromJSON(json) {
144
+ const artifact = JSON.parse(json);
145
+ return new TelemetryPlayer(artifact);
146
+ }
147
+ /**
148
+ * Load artifact from file
149
+ */
150
+ static fromFile(path) {
151
+ const fs = require('fs');
152
+ const json = fs.readFileSync(path, 'utf-8');
153
+ return TelemetryPlayer.fromJSON(json);
154
+ }
155
+ /**
156
+ * Get artifact metadata
157
+ */
158
+ getMetadata() {
159
+ return this.artifact.metadata;
160
+ }
161
+ /**
162
+ * Get all spans
163
+ */
164
+ getSpans() {
165
+ return this.artifact.spans;
166
+ }
167
+ /**
168
+ * Get all events from all spans
169
+ */
170
+ getAllEvents() {
171
+ return this.artifact.spans.flatMap(span => span.events);
172
+ }
173
+ /**
174
+ * Get summary statistics
175
+ */
176
+ getSummary() {
177
+ const allEvents = this.getAllEvents();
178
+ return {
179
+ totalSpans: this.artifact.spans.length,
180
+ totalEvents: allEvents.length,
181
+ status: this.artifact.metadata?.status || 'OK',
182
+ };
183
+ }
184
+ /**
185
+ * Get events for a specific node
186
+ */
187
+ getEventsByNode(nodeId) {
188
+ return this.getAllEvents().filter(e => e.attributes?.['node.id'] === nodeId);
189
+ }
190
+ /**
191
+ * Get events by name
192
+ */
193
+ getEventsByName(eventName) {
194
+ return this.getAllEvents().filter(e => e.name === eventName);
195
+ }
196
+ /**
197
+ * Get events in time range
198
+ */
199
+ getEventsInRange(startTime, endTime) {
200
+ return this.getAllEvents().filter(e => e.time >= startTime && e.time <= endTime);
201
+ }
202
+ /**
203
+ * Replay events with callback
204
+ */
205
+ replay(callback) {
206
+ for (const event of this.getAllEvents()) {
207
+ callback(event);
208
+ }
209
+ }
210
+ /**
211
+ * Replay events with timing (respects original timing)
212
+ */
213
+ async replayWithTiming(callback) {
214
+ const allEvents = this.getAllEvents();
215
+ if (allEvents.length === 0)
216
+ return;
217
+ const baseTime = allEvents[0].time;
218
+ for (const event of allEvents) {
219
+ const delay = event.time - baseTime;
220
+ await new Promise(resolve => setTimeout(resolve, delay));
221
+ await callback(event);
222
+ }
223
+ }
224
+ /**
225
+ * Get next event (for step-through debugging)
226
+ */
227
+ next() {
228
+ const allEvents = this.getAllEvents();
229
+ if (this.currentIndex >= allEvents.length) {
230
+ return null;
231
+ }
232
+ return allEvents[this.currentIndex++];
233
+ }
234
+ /**
235
+ * Reset player to beginning
236
+ */
237
+ reset() {
238
+ this.currentIndex = 0;
239
+ }
240
+ /**
241
+ * Check if more events available
242
+ */
243
+ hasNext() {
244
+ return this.currentIndex < this.getAllEvents().length;
245
+ }
246
+ }
247
+ exports.TelemetryPlayer = TelemetryPlayer;
248
+ /**
249
+ * Helper to save artifact to file
250
+ */
251
+ function saveTelemetryArtifact(artifact, path, pretty = true) {
252
+ const fs = require('fs');
253
+ const json = JSON.stringify(artifact, null, pretty ? 2 : 0);
254
+ fs.writeFileSync(path, json, 'utf-8');
255
+ }
256
+ /**
257
+ * Helper to load artifact from file
258
+ */
259
+ function loadTelemetryArtifact(path) {
260
+ const fs = require('fs');
261
+ const json = fs.readFileSync(path, 'utf-8');
262
+ return JSON.parse(json);
263
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Telemetry Capture Tests
3
+ *
4
+ * Demonstrates how to capture and replay telemetry for narrative tests.
5
+ */
6
+ export {};
7
+ //# sourceMappingURL=TelemetryCapture.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TelemetryCapture.test.d.ts","sourceRoot":"","sources":["../../src/telemetry/TelemetryCapture.test.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
@@ -0,0 +1,396 @@
1
+ "use strict";
2
+ /**
3
+ * Telemetry Capture Tests
4
+ *
5
+ * Demonstrates how to capture and replay telemetry for narrative tests.
6
+ */
7
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
8
+ if (k2 === undefined) k2 = k;
9
+ var desc = Object.getOwnPropertyDescriptor(m, k);
10
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
11
+ desc = { enumerable: true, get: function() { return m[k]; } };
12
+ }
13
+ Object.defineProperty(o, k2, desc);
14
+ }) : (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ o[k2] = m[k];
17
+ }));
18
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
19
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
20
+ }) : function(o, v) {
21
+ o["default"] = v;
22
+ });
23
+ var __importStar = (this && this.__importStar) || (function () {
24
+ var ownKeys = function(o) {
25
+ ownKeys = Object.getOwnPropertyNames || function (o) {
26
+ var ar = [];
27
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
28
+ return ar;
29
+ };
30
+ return ownKeys(o);
31
+ };
32
+ return function (mod) {
33
+ if (mod && mod.__esModule) return mod;
34
+ var result = {};
35
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
36
+ __setModuleDefault(result, mod);
37
+ return result;
38
+ };
39
+ })();
40
+ Object.defineProperty(exports, "__esModule", { value: true });
41
+ const bun_test_1 = require("bun:test");
42
+ const TelemetryCapture_1 = require("./TelemetryCapture");
43
+ const EventValidationIntegration_1 = require("./EventValidationIntegration");
44
+ const fs = __importStar(require("fs"));
45
+ const path = __importStar(require("path"));
46
+ const os = __importStar(require("os"));
47
+ (0, bun_test_1.describe)('TelemetryCapture', () => {
48
+ let capture;
49
+ let telemetry;
50
+ (0, bun_test_1.beforeEach)(() => {
51
+ // Create capture for a narrative test
52
+ capture = new TelemetryCapture_1.TelemetryCapture({
53
+ testName: 'Client Connection Flow',
54
+ testCategory: 'Integration',
55
+ tags: { feature: 'client-lifecycle', version: '1.0.0' },
56
+ });
57
+ // Create telemetry with capture handler
58
+ const canvas = (0, EventValidationIntegration_1.loadControlTowerCanvas)();
59
+ telemetry = new EventValidationIntegration_1.ControlTowerTelemetry(canvas, {
60
+ strict: false,
61
+ onEvent: capture.createEventHandler(),
62
+ });
63
+ });
64
+ (0, bun_test_1.describe)('Event Capture', () => {
65
+ (0, bun_test_1.test)('should capture events with metadata', () => {
66
+ // Emit some events
67
+ telemetry.clientLifecycle('client.connected', {
68
+ 'client.id': 'client-123',
69
+ 'transport.type': 'websocket',
70
+ 'connection.time': Date.now(),
71
+ });
72
+ telemetry.clientLifecycle('client.authenticated', {
73
+ 'client.id': 'client-123',
74
+ 'user.id': 'user-456',
75
+ 'auth.method': 'jwt',
76
+ });
77
+ // Verify capture
78
+ const events = capture.getEvents();
79
+ (0, bun_test_1.expect)(events).toHaveLength(2);
80
+ (0, bun_test_1.expect)(events[0].nodeId).toBe('client-lifecycle');
81
+ (0, bun_test_1.expect)(events[0].eventName).toBe('client.connected');
82
+ (0, bun_test_1.expect)(events[0].sequenceNumber).toBe(0);
83
+ (0, bun_test_1.expect)(events[1].sequenceNumber).toBe(1);
84
+ });
85
+ (0, bun_test_1.test)('should include timestamps', () => {
86
+ const before = Date.now();
87
+ telemetry.messageHandler('message.received', {
88
+ 'client.id': 'client-123',
89
+ 'message.type': 'test',
90
+ 'message.size': 100,
91
+ });
92
+ const after = Date.now();
93
+ const events = capture.getEvents();
94
+ (0, bun_test_1.expect)(events[0].timestamp).toBeGreaterThanOrEqual(before);
95
+ (0, bun_test_1.expect)(events[0].timestamp).toBeLessThanOrEqual(after);
96
+ });
97
+ (0, bun_test_1.test)('should clone attributes to prevent mutation', () => {
98
+ const attrs = {
99
+ 'client.id': 'client-123',
100
+ 'transport.type': 'websocket',
101
+ 'connection.time': Date.now(),
102
+ };
103
+ telemetry.clientLifecycle('client.connected', attrs);
104
+ // Mutate original
105
+ attrs['transport.type'] = 'webrtc';
106
+ // Verify captured version is unchanged
107
+ const events = capture.getEvents();
108
+ (0, bun_test_1.expect)(events[0].attributes['transport.type']).toBe('websocket');
109
+ });
110
+ });
111
+ (0, bun_test_1.describe)('Artifact Export', () => {
112
+ (0, bun_test_1.test)('should export complete artifact', () => {
113
+ // Simulate a narrative test flow
114
+ telemetry.clientLifecycle('client.connected', {
115
+ 'client.id': 'client-123',
116
+ 'transport.type': 'websocket',
117
+ 'connection.time': Date.now(),
118
+ });
119
+ telemetry.roomManager('room.created', {
120
+ 'room.id': 'room-001',
121
+ 'creator.id': 'client-123',
122
+ 'config.max_events': 1000,
123
+ });
124
+ telemetry.roomManager('room.client_joined', {
125
+ 'room.id': 'room-001',
126
+ 'client.id': 'client-123',
127
+ 'room.client_count': 1,
128
+ });
129
+ capture.stop();
130
+ const artifact = capture.export();
131
+ // Verify artifact structure
132
+ (0, bun_test_1.expect)(artifact.version).toBe('1.0.0');
133
+ (0, bun_test_1.expect)(artifact.metadata.testName).toBe('Client Connection Flow');
134
+ (0, bun_test_1.expect)(artifact.metadata.testCategory).toBe('Integration');
135
+ (0, bun_test_1.expect)(artifact.metadata.tags).toEqual({
136
+ feature: 'client-lifecycle',
137
+ version: '1.0.0',
138
+ });
139
+ (0, bun_test_1.expect)(artifact.events).toHaveLength(3);
140
+ (0, bun_test_1.expect)(artifact.summary.totalEvents).toBe(3);
141
+ (0, bun_test_1.expect)(artifact.summary.eventsByNode['client-lifecycle']).toBe(1);
142
+ (0, bun_test_1.expect)(artifact.summary.eventsByNode['room-manager']).toBe(2);
143
+ });
144
+ (0, bun_test_1.test)('should calculate summary statistics', () => {
145
+ // Multiple events from different nodes
146
+ telemetry.messageHandler('message.received', {
147
+ 'client.id': 'client-123',
148
+ 'message.type': 'test',
149
+ });
150
+ telemetry.messageHandler('message.received', {
151
+ 'client.id': 'client-456',
152
+ 'message.type': 'test',
153
+ });
154
+ telemetry.clientLifecycle('client.connected', {
155
+ 'client.id': 'client-123',
156
+ 'transport.type': 'websocket',
157
+ 'connection.time': Date.now(),
158
+ });
159
+ const artifact = capture.export();
160
+ (0, bun_test_1.expect)(artifact.summary.totalEvents).toBe(3);
161
+ (0, bun_test_1.expect)(artifact.summary.eventsByNode['message-handler']).toBe(2);
162
+ (0, bun_test_1.expect)(artifact.summary.eventsByNode['client-lifecycle']).toBe(1);
163
+ (0, bun_test_1.expect)(artifact.summary.eventsByName['message.received']).toBe(2);
164
+ (0, bun_test_1.expect)(artifact.summary.eventsByName['client.connected']).toBe(1);
165
+ });
166
+ (0, bun_test_1.test)('should export as JSON', () => {
167
+ telemetry.clientLifecycle('client.connected', {
168
+ 'client.id': 'client-123',
169
+ 'transport.type': 'websocket',
170
+ 'connection.time': Date.now(),
171
+ });
172
+ const json = capture.exportJSON();
173
+ const parsed = JSON.parse(json);
174
+ (0, bun_test_1.expect)(parsed.version).toBe('1.0.0');
175
+ (0, bun_test_1.expect)(parsed.events).toHaveLength(1);
176
+ });
177
+ });
178
+ (0, bun_test_1.describe)('File Persistence', () => {
179
+ let tempDir;
180
+ let artifactPath;
181
+ (0, bun_test_1.beforeEach)(() => {
182
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'telemetry-test-'));
183
+ artifactPath = path.join(tempDir, 'test-artifact.json');
184
+ });
185
+ (0, bun_test_1.test)('should save and load artifacts', () => {
186
+ // Capture events
187
+ telemetry.clientLifecycle('client.connected', {
188
+ 'client.id': 'client-123',
189
+ 'transport.type': 'websocket',
190
+ 'connection.time': Date.now(),
191
+ });
192
+ telemetry.clientLifecycle('client.authenticated', {
193
+ 'client.id': 'client-123',
194
+ 'user.id': 'user-456',
195
+ 'auth.method': 'jwt',
196
+ });
197
+ // Save to file
198
+ const artifact = capture.export();
199
+ (0, TelemetryCapture_1.saveTelemetryArtifact)(artifact, artifactPath);
200
+ // Verify file exists
201
+ (0, bun_test_1.expect)(fs.existsSync(artifactPath)).toBe(true);
202
+ // Load from file
203
+ const loaded = (0, TelemetryCapture_1.loadTelemetryArtifact)(artifactPath);
204
+ (0, bun_test_1.expect)(loaded.version).toBe(artifact.version);
205
+ (0, bun_test_1.expect)(loaded.events).toHaveLength(2);
206
+ (0, bun_test_1.expect)(loaded.metadata.testName).toBe('Client Connection Flow');
207
+ });
208
+ });
209
+ });
210
+ (0, bun_test_1.describe)('TelemetryPlayer', () => {
211
+ let artifact;
212
+ (0, bun_test_1.beforeEach)(() => {
213
+ // Create a sample artifact
214
+ const capture = new TelemetryCapture_1.TelemetryCapture({
215
+ testName: 'Sample Test',
216
+ testCategory: 'Integration',
217
+ });
218
+ const canvas = (0, EventValidationIntegration_1.loadControlTowerCanvas)();
219
+ const telemetry = new EventValidationIntegration_1.ControlTowerTelemetry(canvas, {
220
+ onEvent: capture.createEventHandler(),
221
+ });
222
+ // Simulate events
223
+ telemetry.clientLifecycle('client.connected', {
224
+ 'client.id': 'client-123',
225
+ 'transport.type': 'websocket',
226
+ 'connection.time': Date.now(),
227
+ });
228
+ telemetry.roomManager('room.created', {
229
+ 'room.id': 'room-001',
230
+ 'creator.id': 'client-123',
231
+ });
232
+ telemetry.roomManager('room.client_joined', {
233
+ 'room.id': 'room-001',
234
+ 'client.id': 'client-123',
235
+ 'room.client_count': 1,
236
+ });
237
+ artifact = capture.export();
238
+ });
239
+ (0, bun_test_1.describe)('Playback', () => {
240
+ (0, bun_test_1.test)('should load artifact and provide metadata', () => {
241
+ const player = new TelemetryCapture_1.TelemetryPlayer(artifact);
242
+ const metadata = player.getMetadata();
243
+ (0, bun_test_1.expect)(metadata.testName).toBe('Sample Test');
244
+ (0, bun_test_1.expect)(metadata.testCategory).toBe('Integration');
245
+ });
246
+ (0, bun_test_1.test)('should provide all events', () => {
247
+ const player = new TelemetryCapture_1.TelemetryPlayer(artifact);
248
+ const events = player.getAllEvents();
249
+ (0, bun_test_1.expect)(events).toHaveLength(3);
250
+ (0, bun_test_1.expect)(events[0].eventName).toBe('client.connected');
251
+ });
252
+ (0, bun_test_1.test)('should filter events by node', () => {
253
+ const player = new TelemetryCapture_1.TelemetryPlayer(artifact);
254
+ const roomEvents = player.getEventsByNode('room-manager');
255
+ (0, bun_test_1.expect)(roomEvents).toHaveLength(2);
256
+ (0, bun_test_1.expect)(roomEvents[0].eventName).toBe('room.created');
257
+ (0, bun_test_1.expect)(roomEvents[1].eventName).toBe('room.client_joined');
258
+ });
259
+ (0, bun_test_1.test)('should filter events by name', () => {
260
+ const player = new TelemetryCapture_1.TelemetryPlayer(artifact);
261
+ const connectedEvents = player.getEventsByName('client.connected');
262
+ (0, bun_test_1.expect)(connectedEvents).toHaveLength(1);
263
+ (0, bun_test_1.expect)(connectedEvents[0].nodeId).toBe('client-lifecycle');
264
+ });
265
+ (0, bun_test_1.test)('should replay events with callback', () => {
266
+ const player = new TelemetryCapture_1.TelemetryPlayer(artifact);
267
+ const replayed = [];
268
+ player.replay(event => {
269
+ replayed.push(event.eventName);
270
+ });
271
+ (0, bun_test_1.expect)(replayed).toEqual([
272
+ 'client.connected',
273
+ 'room.created',
274
+ 'room.client_joined',
275
+ ]);
276
+ });
277
+ (0, bun_test_1.test)('should support step-through playback', () => {
278
+ const player = new TelemetryCapture_1.TelemetryPlayer(artifact);
279
+ (0, bun_test_1.expect)(player.hasNext()).toBe(true);
280
+ const event1 = player.next();
281
+ (0, bun_test_1.expect)(event1?.eventName).toBe('client.connected');
282
+ const event2 = player.next();
283
+ (0, bun_test_1.expect)(event2?.eventName).toBe('room.created');
284
+ const event3 = player.next();
285
+ (0, bun_test_1.expect)(event3?.eventName).toBe('room.client_joined');
286
+ (0, bun_test_1.expect)(player.hasNext()).toBe(false);
287
+ (0, bun_test_1.expect)(player.next()).toBeNull();
288
+ });
289
+ (0, bun_test_1.test)('should reset playback', () => {
290
+ const player = new TelemetryCapture_1.TelemetryPlayer(artifact);
291
+ player.next();
292
+ player.next();
293
+ player.reset();
294
+ const event = player.next();
295
+ (0, bun_test_1.expect)(event?.eventName).toBe('client.connected');
296
+ });
297
+ });
298
+ (0, bun_test_1.describe)('File Loading', () => {
299
+ (0, bun_test_1.test)('should load from JSON string', () => {
300
+ const json = JSON.stringify(artifact);
301
+ const player = TelemetryCapture_1.TelemetryPlayer.fromJSON(json);
302
+ (0, bun_test_1.expect)(player.getAllEvents()).toHaveLength(3);
303
+ });
304
+ (0, bun_test_1.test)('should load from file', () => {
305
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'telemetry-test-'));
306
+ const artifactPath = path.join(tempDir, 'artifact.json');
307
+ (0, TelemetryCapture_1.saveTelemetryArtifact)(artifact, artifactPath);
308
+ const player = TelemetryCapture_1.TelemetryPlayer.fromFile(artifactPath);
309
+ (0, bun_test_1.expect)(player.getAllEvents()).toHaveLength(3);
310
+ (0, bun_test_1.expect)(player.getMetadata().testName).toBe('Sample Test');
311
+ });
312
+ });
313
+ });
314
+ (0, bun_test_1.describe)('Narrative Test Example', () => {
315
+ (0, bun_test_1.test)('should capture complete client connection flow', () => {
316
+ // This demonstrates a narrative test that captures telemetry
317
+ const capture = new TelemetryCapture_1.TelemetryCapture({
318
+ testName: 'Complete Client Connection Flow',
319
+ testCategory: 'Narrative Tests',
320
+ tags: {
321
+ feature: 'client-lifecycle',
322
+ scenario: 'happy-path',
323
+ },
324
+ });
325
+ const canvas = (0, EventValidationIntegration_1.loadControlTowerCanvas)();
326
+ const telemetry = new EventValidationIntegration_1.ControlTowerTelemetry(canvas, {
327
+ strict: false,
328
+ onEvent: capture.createEventHandler(),
329
+ });
330
+ // Simulate complete flow
331
+ const clientId = 'client-abc123';
332
+ const userId = 'user-xyz789';
333
+ const roomId = 'room-collab-001';
334
+ // 1. Client connects
335
+ telemetry.clientLifecycle('client.connected', {
336
+ 'client.id': clientId,
337
+ 'transport.type': 'websocket',
338
+ 'connection.time': Date.now(),
339
+ });
340
+ // 2. Client authenticates
341
+ telemetry.clientLifecycle('client.authenticated', {
342
+ 'client.id': clientId,
343
+ 'user.id': userId,
344
+ 'auth.method': 'jwt',
345
+ });
346
+ // 3. Create room
347
+ telemetry.roomManager('room.created', {
348
+ 'room.id': roomId,
349
+ 'creator.id': clientId,
350
+ 'config.max_events': 1000,
351
+ });
352
+ // 4. Client joins room
353
+ telemetry.roomManager('room.client_joined', {
354
+ 'room.id': roomId,
355
+ 'client.id': clientId,
356
+ 'room.client_count': 1,
357
+ });
358
+ // 5. Broadcast event
359
+ telemetry.eventBroadcaster('event.broadcast', {
360
+ 'room.id': roomId,
361
+ 'event.type': 'file_change',
362
+ 'recipient.count': 1,
363
+ });
364
+ // 6. Acquire lock
365
+ telemetry.lockManager('lock.acquired', {
366
+ 'lock.id': 'file:/src/index.ts',
367
+ 'client.id': clientId,
368
+ 'lock.type': 'file',
369
+ 'queue.wait_time': 50,
370
+ });
371
+ // 7. Update presence
372
+ telemetry.presenceManager('presence.status_changed', {
373
+ 'user.id': userId,
374
+ 'status.from': 'offline',
375
+ 'status.to': 'online',
376
+ 'device.count': 1,
377
+ });
378
+ // Export artifact
379
+ capture.stop();
380
+ const artifact = capture.export();
381
+ // Verify narrative flow
382
+ (0, bun_test_1.expect)(artifact.events).toHaveLength(7);
383
+ (0, bun_test_1.expect)(artifact.metadata.testName).toBe('Complete Client Connection Flow');
384
+ (0, bun_test_1.expect)(artifact.summary.eventsByNode['client-lifecycle']).toBe(2);
385
+ (0, bun_test_1.expect)(artifact.summary.eventsByNode['room-manager']).toBe(2);
386
+ (0, bun_test_1.expect)(artifact.summary.eventsByNode['event-broadcaster']).toBe(1);
387
+ (0, bun_test_1.expect)(artifact.summary.eventsByNode['lock-manager']).toBe(1);
388
+ (0, bun_test_1.expect)(artifact.summary.eventsByNode['presence-manager']).toBe(1);
389
+ // This artifact can now be:
390
+ // 1. Saved to test/fixtures/narratives/client-connection-flow.json
391
+ // 2. Loaded in Storybook for visualization
392
+ // 3. Used for regression testing
393
+ // 4. Analyzed for performance metrics
394
+ console.log('Artifact summary:', artifact.summary);
395
+ });
396
+ });