@principal-ai/control-tower-core 0.2.1 → 0.3.0
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/dist/client/BaseClient.d.ts +23 -0
- package/dist/client/BaseClient.d.ts.map +1 -1
- package/dist/client/BaseClient.js +60 -0
- package/dist/client/PresenceClient.d.ts +43 -8
- package/dist/client/PresenceClient.d.ts.map +1 -1
- package/dist/client/PresenceClient.js +72 -18
- package/dist/generated/client-connection-auth.types.d.ts +312 -0
- package/dist/generated/client-connection-auth.types.d.ts.map +1 -0
- package/dist/generated/client-connection-auth.types.js +11 -0
- package/dist/generated/control-tower-execution.types.d.ts +445 -0
- package/dist/generated/control-tower-execution.types.d.ts.map +1 -0
- package/dist/generated/control-tower-execution.types.js +11 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +6 -6
- package/dist/index.mjs +94 -7
- package/dist/index.mjs.map +6 -6
- package/dist/server/BaseServer.d.ts +18 -1
- package/dist/server/BaseServer.d.ts.map +1 -1
- package/dist/server/BaseServer.js +15 -1
- package/dist/server/ServerBuilder.d.ts +22 -1
- package/dist/server/ServerBuilder.d.ts.map +1 -1
- package/dist/server/ServerBuilder.js +24 -0
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/telemetry/EventValidationIntegration.d.ts +135 -0
- package/dist/telemetry/EventValidationIntegration.d.ts.map +1 -0
- package/dist/telemetry/EventValidationIntegration.js +253 -0
- package/dist/telemetry/EventValidationIntegration.test.d.ts +7 -0
- package/dist/telemetry/EventValidationIntegration.test.d.ts.map +1 -0
- package/dist/telemetry/EventValidationIntegration.test.js +322 -0
- package/dist/telemetry/TelemetryCapture.d.ts +268 -0
- package/dist/telemetry/TelemetryCapture.d.ts.map +1 -0
- package/dist/telemetry/TelemetryCapture.js +263 -0
- package/dist/telemetry/TelemetryCapture.test.d.ts +7 -0
- package/dist/telemetry/TelemetryCapture.test.d.ts.map +1 -0
- package/dist/telemetry/TelemetryCapture.test.js +396 -0
- package/dist/telemetry-example.d.ts +33 -0
- package/dist/telemetry-example.d.ts.map +1 -0
- package/dist/telemetry-example.js +124 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/presence.d.ts +69 -0
- package/dist/types/presence.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -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 @@
|
|
|
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
|
+
});
|