@fluidframework/azure-end-to-end-tests 2.70.0-361248 → 2.70.0-361788

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.
@@ -24,28 +24,42 @@ const endPoint = process.env.azure__fluid__relay__service__endpoint;
24
24
  if (useAzure && endPoint === undefined) {
25
25
  throw new Error("Azure Fluid Relay service endpoint is missing");
26
26
  }
27
+ const containerSchema = {
28
+ initialObjects: {
29
+ // A DataObject is added as otherwise fluid-static complains "Container cannot be initialized without any DataTypes"
30
+ _unused: TestDataObject,
31
+ },
32
+ };
33
+ function telemetryEventInterestLevel(eventName) {
34
+ if (eventName.includes(":Signal") || eventName.includes(":Join")) {
35
+ return "details";
36
+ }
37
+ else if (eventName.includes(":Container:") || eventName.includes(":Presence:")) {
38
+ return "basic";
39
+ }
40
+ return "none";
41
+ }
27
42
  function selectiveVerboseLog(event, logLevel) {
28
- if (event.eventName.includes(":Signal") || event.eventName.includes(":Join")) {
29
- console.log(`[${process_id}] [${logLevel ?? LogLevel.default}]`, {
30
- eventName: event.eventName,
31
- details: event.details,
32
- containerConnectionState: event.containerConnectionState,
33
- });
43
+ const interest = telemetryEventInterestLevel(event.eventName);
44
+ if (interest === "none") {
45
+ return;
34
46
  }
35
- else if (event.eventName.includes(":Container:") ||
36
- event.eventName.includes(":Presence:")) {
37
- console.log(`[${process_id}] [${logLevel ?? LogLevel.default}]`, {
38
- eventName: event.eventName,
39
- containerConnectionState: event.containerConnectionState,
40
- });
47
+ const content = {
48
+ eventName: event.eventName,
49
+ containerConnectionState: event.containerConnectionState,
50
+ };
51
+ if (interest === "details") {
52
+ content.details = event.details;
41
53
  }
54
+ console.log(`[${process_id}] [${logLevel ?? LogLevel.default}]`, content);
42
55
  }
43
56
  /**
44
- * Get or create a Fluid container with Presence in initialObjects.
57
+ * Get or create a Fluid container.
45
58
  */
46
- const getOrCreatePresenceContainer = async (id, user, scopes, createScopes) => {
59
+ const getOrCreateContainer = async (params) => {
47
60
  let container;
48
- let containerId;
61
+ let { containerId } = params;
62
+ const { logger, onDisconnected, user, scopes, createScopes } = params;
49
63
  const connectionProps = useAzure
50
64
  ? {
51
65
  tenantId,
@@ -60,40 +74,30 @@ const getOrCreatePresenceContainer = async (id, user, scopes, createScopes) => {
60
74
  };
61
75
  const client = new AzureClient({
62
76
  connection: connectionProps,
63
- logger: {
64
- send: verbosity.includes("telem") ? selectiveVerboseLog : () => { },
65
- },
77
+ logger,
66
78
  });
67
- const schema = {
68
- initialObjects: {
69
- // A DataObject is added as otherwise fluid-static complains "Container cannot be initialized without any DataTypes"
70
- _unused: TestDataObject,
71
- },
72
- };
73
79
  let services;
74
- if (id === undefined) {
75
- ({ container, services } = await client.createContainer(schema, "2"));
80
+ if (containerId === undefined) {
81
+ ({ container, services } = await client.createContainer(containerSchema, "2"));
76
82
  containerId = await container.attach();
77
83
  }
78
84
  else {
79
- containerId = id;
80
- ({ container, services } = await client.getContainer(containerId, schema, "2"));
85
+ ({ container, services } = await client.getContainer(containerId, containerSchema, "2"));
81
86
  }
82
- // wait for 'ConnectionState.Connected' so we return with client connected to container
83
- if (container.connectionState !== ConnectionState.Connected) {
84
- await timeoutPromise((resolve) => container.once("connected", () => resolve()), {
87
+ container.on("disconnected", onDisconnected);
88
+ const connected = container.connectionState === ConnectionState.Connected
89
+ ? Promise.resolve()
90
+ : timeoutPromise((resolve) => container.once("connected", () => resolve()), {
85
91
  durationMs: connectTimeoutMs,
86
92
  errorMsg: "container connect() timeout",
87
93
  });
88
- }
89
94
  assert.strictEqual(container.attachState, AttachState.Attached, "Container is not attached after attach is called");
90
- const presence = getPresence(container);
91
95
  return {
92
96
  client,
93
97
  container,
94
- presence,
95
98
  services,
96
99
  containerId,
100
+ connected,
97
101
  };
98
102
  };
99
103
  function createSendFunction() {
@@ -110,21 +114,6 @@ function createSendFunction() {
110
114
  throw new Error("process.send is not defined");
111
115
  }
112
116
  const send = createSendFunction();
113
- function sendAttendeeConnected(attendee) {
114
- send({
115
- event: "attendeeConnected",
116
- attendeeId: attendee.attendeeId,
117
- });
118
- }
119
- function sendAttendeeDisconnected(attendee) {
120
- send({
121
- event: "attendeeDisconnected",
122
- attendeeId: attendee.attendeeId,
123
- });
124
- }
125
- function isConnected(container) {
126
- return container !== undefined && container.connectionState === ConnectionState.Connected;
127
- }
128
117
  function isStringOrNumberRecord(value) {
129
118
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
130
119
  return false;
@@ -145,11 +134,58 @@ function isStringOrNumberRecord(value) {
145
134
  const WorkspaceSchema = {};
146
135
  class MessageHandler {
147
136
  constructor() {
137
+ this.log = [];
148
138
  this.workspaces = new Map();
139
+ this.sendAttendeeConnected = (attendee) => {
140
+ this.send({
141
+ event: "attendeeConnected",
142
+ attendeeId: attendee.attendeeId,
143
+ });
144
+ };
145
+ this.sendAttendeeDisconnected = (attendee) => {
146
+ this.send({
147
+ event: "attendeeDisconnected",
148
+ attendeeId: attendee.attendeeId,
149
+ });
150
+ };
151
+ this.logger = {
152
+ send: (event, logLevel) => {
153
+ const interest = telemetryEventInterestLevel(event.eventName);
154
+ if (interest === "none") {
155
+ return;
156
+ }
157
+ this.log.push({
158
+ timestamp: Date.now(),
159
+ agentId: process_id,
160
+ eventCategory: "telemetry",
161
+ eventName: event.eventName,
162
+ details: typeof event.details === "string" ? event.details : JSON.stringify(event.details),
163
+ });
164
+ if (verbosity.includes("telem")) {
165
+ selectiveVerboseLog(event, logLevel);
166
+ }
167
+ },
168
+ };
169
+ this.onDisconnected = () => {
170
+ // Test state is a bit fragile and does not account for reconnections.
171
+ this.send({ event: "error", error: `${process_id}: Container disconnected` });
172
+ };
173
+ }
174
+ send(msg) {
175
+ this.log.push({
176
+ timestamp: Date.now(),
177
+ agentId: process_id,
178
+ eventCategory: "messageSent",
179
+ eventName: msg.event,
180
+ details: msg.event === "debugReportComplete" && msg.log
181
+ ? JSON.stringify({ logLength: msg.log.length })
182
+ : JSON.stringify(msg),
183
+ });
184
+ send(msg);
149
185
  }
150
186
  registerWorkspace(workspaceId, options) {
151
187
  if (!this.presence) {
152
- send({ event: "error", error: `${process_id} is not connected to presence` });
188
+ this.send({ event: "error", error: `${process_id} is not connected to presence` });
153
189
  return;
154
190
  }
155
191
  const { latest, latestMap } = options;
@@ -160,7 +196,7 @@ class MessageHandler {
160
196
  // TODO: AB#47518
161
197
  const latestState = workspace.states.latest;
162
198
  latestState.events.on("remoteUpdated", (update) => {
163
- send({
199
+ this.send({
164
200
  event: "latestValueUpdated",
165
201
  workspaceId,
166
202
  attendeeId: update.attendee.attendeeId,
@@ -168,7 +204,7 @@ class MessageHandler {
168
204
  });
169
205
  });
170
206
  for (const remote of latestState.getRemotes()) {
171
- send({
207
+ this.send({
172
208
  event: "latestValueUpdated",
173
209
  workspaceId,
174
210
  attendeeId: remote.attendee.attendeeId,
@@ -185,7 +221,7 @@ class MessageHandler {
185
221
  const latestMapState = workspace.states.latestMap;
186
222
  latestMapState.events.on("remoteUpdated", (update) => {
187
223
  for (const [key, valueWithMetadata] of update.items) {
188
- send({
224
+ this.send({
189
225
  event: "latestMapValueUpdated",
190
226
  workspaceId,
191
227
  attendeeId: update.attendee.attendeeId,
@@ -196,7 +232,7 @@ class MessageHandler {
196
232
  });
197
233
  for (const remote of latestMapState.getRemotes()) {
198
234
  for (const [key, valueWithMetadata] of remote.items) {
199
- send({
235
+ this.send({
200
236
  event: "latestMapValueUpdated",
201
237
  workspaceId,
202
238
  attendeeId: remote.attendee.attendeeId,
@@ -207,7 +243,7 @@ class MessageHandler {
207
243
  }
208
244
  }
209
245
  this.workspaces.set(workspaceId, workspace);
210
- send({
246
+ this.send({
211
247
  event: "workspaceRegistered",
212
248
  workspaceId,
213
249
  latest: latest ?? false,
@@ -216,15 +252,33 @@ class MessageHandler {
216
252
  }
217
253
  async onMessage(msg) {
218
254
  if (verbosity.includes("msgs")) {
255
+ this.log.push({
256
+ timestamp: Date.now(),
257
+ agentId: process_id,
258
+ eventCategory: "messageReceived",
259
+ eventName: msg.command,
260
+ });
219
261
  console.log(`[${process_id}] Received`, msg);
220
262
  }
263
+ if (msg.command === "ping") {
264
+ this.handlePing();
265
+ return;
266
+ }
267
+ if (msg.command === "connect") {
268
+ await this.handleConnect(msg);
269
+ return;
270
+ }
271
+ // All other message must wait if connect is in progress
272
+ if (this.msgQueue !== undefined) {
273
+ this.msgQueue.push(msg);
274
+ return;
275
+ }
276
+ this.processMessage(msg);
277
+ }
278
+ processMessage(msg) {
221
279
  switch (msg.command) {
222
- case "ping": {
223
- this.handlePing();
224
- break;
225
- }
226
- case "connect": {
227
- await this.handleConnect(msg);
280
+ case "debugReport": {
281
+ this.handleDebugReport(msg);
228
282
  break;
229
283
  }
230
284
  case "disconnectSelf": {
@@ -255,74 +309,125 @@ class MessageHandler {
255
309
  break;
256
310
  }
257
311
  default: {
258
- console.error(`${process_id}: Unknown command`);
259
- send({ event: "error", error: `${process_id} Unknown command` });
312
+ console.error(`${process_id}: Unknown command:`, msg);
313
+ this.send({
314
+ event: "error",
315
+ error: `${process_id} Unknown command: ${JSON.stringify(msg)}`,
316
+ });
260
317
  }
261
318
  }
262
319
  }
263
320
  handlePing() {
264
- send({ event: "ack" });
321
+ this.send({ event: "ack" });
265
322
  }
266
323
  async handleConnect(msg) {
267
324
  if (!msg.user) {
268
- send({ event: "error", error: `${process_id}: No azure user information given` });
325
+ this.send({ event: "error", error: `${process_id}: No azure user information given` });
269
326
  return;
270
327
  }
271
- if (isConnected(this.container)) {
272
- send({ event: "error", error: `${process_id}: Already connected to container` });
328
+ if (this.container) {
329
+ this.send({ event: "error", error: `${process_id}: Container already loaded` });
273
330
  return;
274
331
  }
275
- const { container, presence, containerId } = await getOrCreatePresenceContainer(msg.containerId, msg.user, msg.scopes, msg.createScopes);
276
- this.container = container;
277
- this.presence = presence;
278
- this.containerId = containerId;
279
- // Acknowledge connection before sending current attendee information
280
- send({
281
- event: "connected",
282
- containerId,
283
- attendeeId: presence.attendees.getMyself().attendeeId,
284
- });
285
- // Send existing attendees excluding self to parent/orchestrator
286
- const self = presence.attendees.getMyself();
287
- for (const attendee of presence.attendees.getAttendees()) {
288
- if (attendee !== self) {
289
- sendAttendeeConnected(attendee);
332
+ // Prevent reentrance. Queue messages until after connect is fully processed.
333
+ this.msgQueue = [];
334
+ try {
335
+ const { container, containerId, connected } = await getOrCreateContainer({
336
+ ...msg,
337
+ logger: this.logger,
338
+ onDisconnected: this.onDisconnected,
339
+ });
340
+ this.container = container;
341
+ const presence = getPresence(container);
342
+ this.presence = presence;
343
+ // wait for 'ConnectionState.Connected'
344
+ await connected;
345
+ // Acknowledge connection before sending current attendee information
346
+ this.send({
347
+ event: "connected",
348
+ containerId,
349
+ attendeeId: presence.attendees.getMyself().attendeeId,
350
+ });
351
+ // Send existing attendees excluding self to parent/orchestrator
352
+ const self = presence.attendees.getMyself();
353
+ for (const attendee of presence.attendees.getAttendees()) {
354
+ if (attendee !== self && attendee.getConnectionStatus() === "Connected") {
355
+ this.sendAttendeeConnected(attendee);
356
+ }
357
+ }
358
+ // Listen for presence events to notify parent/orchestrator when a new attendee joins or leaves the session.
359
+ presence.attendees.events.on("attendeeConnected", this.sendAttendeeConnected);
360
+ presence.attendees.events.on("attendeeDisconnected", this.sendAttendeeDisconnected);
361
+ }
362
+ finally {
363
+ // Process any queued messages received while connecting
364
+ for (const queuedMsg of this.msgQueue) {
365
+ this.processMessage(queuedMsg);
366
+ }
367
+ this.msgQueue = undefined;
368
+ }
369
+ }
370
+ handleDebugReport(msg) {
371
+ if (msg.reportAttendees) {
372
+ if (this.presence) {
373
+ const attendees = this.presence.attendees.getAttendees();
374
+ let connectedCount = 0;
375
+ for (const attendee of attendees) {
376
+ if (attendee.getConnectionStatus() === "Connected") {
377
+ connectedCount++;
378
+ }
379
+ }
380
+ console.log(`[${process_id}] Report: ${attendees.size} attendees, ${connectedCount} connected`);
381
+ }
382
+ else {
383
+ this.send({ event: "error", error: `${process_id} is not connected to presence` });
290
384
  }
291
385
  }
292
- // Listen for presence events to notify parent/orchestrator when a new attendee joins or leaves the session.
293
- presence.attendees.events.on("attendeeConnected", sendAttendeeConnected);
294
- presence.attendees.events.on("attendeeDisconnected", sendAttendeeDisconnected);
386
+ const debugReport = {
387
+ event: "debugReportComplete",
388
+ };
389
+ if (msg.sendEventLog) {
390
+ debugReport.log = this.log;
391
+ }
392
+ this.send(debugReport);
295
393
  }
296
394
  handleDisconnectSelf() {
297
395
  if (!this.container) {
298
- send({ event: "error", error: `${process_id} is not connected to container` });
396
+ this.send({ event: "error", error: `${process_id} is not connected to container` });
299
397
  return;
300
398
  }
399
+ // There are no current scenarios where disconnect without presence is expected.
301
400
  if (!this.presence) {
302
- send({ event: "error", error: `${process_id} is not connected to presence` });
401
+ this.send({ event: "error", error: `${process_id} is not connected to presence` });
303
402
  return;
304
403
  }
404
+ // Disconnect event is treated as an error in normal handling.
405
+ // Remove listener as this disconnect is intentional.
406
+ this.container.off("disconnected", this.onDisconnected);
305
407
  this.container.disconnect();
306
- send({
408
+ this.send({
307
409
  event: "disconnectedSelf",
308
410
  attendeeId: this.presence.attendees.getMyself().attendeeId,
309
411
  });
310
412
  }
311
413
  handleSetLatestValue(msg) {
312
414
  if (!this.presence) {
313
- send({ event: "error", error: `${process_id} is not connected to presence` });
415
+ this.send({ event: "error", error: `${process_id} is not connected to presence` });
314
416
  return;
315
417
  }
316
418
  const workspace = this.workspaces.get(msg.workspaceId);
317
419
  if (!workspace) {
318
- send({ event: "error", error: `${process_id} workspace ${msg.workspaceId} not found` });
420
+ this.send({
421
+ event: "error",
422
+ error: `${process_id} workspace ${msg.workspaceId} not found`,
423
+ });
319
424
  return;
320
425
  }
321
426
  // Cast required due to optional keys in WorkspaceSchema
322
427
  // TODO: AB#47518
323
428
  const latestState = workspace.states.latest;
324
429
  if (!latestState) {
325
- send({
430
+ this.send({
326
431
  event: "error",
327
432
  error: `${process_id} latest state not registered for workspace ${msg.workspaceId}`,
328
433
  });
@@ -335,23 +440,26 @@ class MessageHandler {
335
440
  }
336
441
  handleSetLatestMapValue(msg) {
337
442
  if (!this.presence) {
338
- send({ event: "error", error: `${process_id} is not connected to presence` });
443
+ this.send({ event: "error", error: `${process_id} is not connected to presence` });
339
444
  return;
340
445
  }
341
446
  if (typeof msg.key !== "string") {
342
- send({ event: "error", error: `${process_id} invalid key type` });
447
+ this.send({ event: "error", error: `${process_id} invalid key type` });
343
448
  return;
344
449
  }
345
450
  const workspace = this.workspaces.get(msg.workspaceId);
346
451
  if (!workspace) {
347
- send({ event: "error", error: `${process_id} workspace ${msg.workspaceId} not found` });
452
+ this.send({
453
+ event: "error",
454
+ error: `${process_id} workspace ${msg.workspaceId} not found`,
455
+ });
348
456
  return;
349
457
  }
350
458
  // Cast required due to optional keys in WorkspaceSchema
351
459
  // TODO: AB#47518
352
460
  const latestMapState = workspace.states.latestMap;
353
461
  if (!latestMapState) {
354
- send({
462
+ this.send({
355
463
  event: "error",
356
464
  error: `${process_id} latestMap state not registered for workspace ${msg.workspaceId}`,
357
465
  });
@@ -364,19 +472,22 @@ class MessageHandler {
364
472
  }
365
473
  handleGetLatestValue(msg) {
366
474
  if (!this.presence) {
367
- send({ event: "error", error: `${process_id} is not connected to presence` });
475
+ this.send({ event: "error", error: `${process_id} is not connected to presence` });
368
476
  return;
369
477
  }
370
478
  const workspace = this.workspaces.get(msg.workspaceId);
371
479
  if (!workspace) {
372
- send({ event: "error", error: `${process_id} workspace ${msg.workspaceId} not found` });
480
+ this.send({
481
+ event: "error",
482
+ error: `${process_id} workspace ${msg.workspaceId} not found`,
483
+ });
373
484
  return;
374
485
  }
375
486
  // Cast required due to optional keys in WorkspaceSchema
376
487
  // TODO: AB#47518
377
488
  const latestState = workspace.states.latest;
378
489
  if (!latestState) {
379
- send({
490
+ this.send({
380
491
  event: "error",
381
492
  error: `${process_id} latest state not registered for workspace ${msg.workspaceId}`,
382
493
  });
@@ -391,7 +502,7 @@ class MessageHandler {
391
502
  else {
392
503
  value = latestState.local;
393
504
  }
394
- send({
505
+ this.send({
395
506
  event: "latestValueGetResponse",
396
507
  workspaceId: msg.workspaceId,
397
508
  attendeeId: msg.attendeeId,
@@ -400,23 +511,26 @@ class MessageHandler {
400
511
  }
401
512
  handleGetLatestMapValue(msg) {
402
513
  if (!this.presence) {
403
- send({ event: "error", error: `${process_id} is not connected to presence` });
514
+ this.send({ event: "error", error: `${process_id} is not connected to presence` });
404
515
  return;
405
516
  }
406
517
  if (typeof msg.key !== "string") {
407
- send({ event: "error", error: `${process_id} invalid key type` });
518
+ this.send({ event: "error", error: `${process_id} invalid key type` });
408
519
  return;
409
520
  }
410
521
  const workspace = this.workspaces.get(msg.workspaceId);
411
522
  if (!workspace) {
412
- send({ event: "error", error: `${process_id} workspace ${msg.workspaceId} not found` });
523
+ this.send({
524
+ event: "error",
525
+ error: `${process_id} workspace ${msg.workspaceId} not found`,
526
+ });
413
527
  return;
414
528
  }
415
529
  // Cast required due to optional keys in WorkspaceSchema
416
530
  // TODO: AB#47518
417
531
  const latestMapState = workspace.states.latestMap;
418
532
  if (!latestMapState) {
419
- send({
533
+ this.send({
420
534
  event: "error",
421
535
  error: `${process_id} latestMap state not registered for workspace ${msg.workspaceId}`,
422
536
  });
@@ -432,7 +546,7 @@ class MessageHandler {
432
546
  else {
433
547
  value = latestMapState.local.get(msg.key);
434
548
  }
435
- send({
549
+ this.send({
436
550
  event: "latestMapValueGetResponse",
437
551
  workspaceId: msg.workspaceId,
438
552
  attendeeId: msg.attendeeId,