@fluidframework/azure-end-to-end-tests 2.70.0-361248 → 2.70.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/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # @fluidframework/azure-end-to-end-tests
2
2
 
3
+ ## 2.70.0
4
+
5
+ Dependency updates only.
6
+
3
7
  ## 2.63.0
4
8
 
5
9
  Dependency updates only.
@@ -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,66 @@ 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
+ // Special case unexpected telemetry event
154
+ if (event.eventName.endsWith(":JoinResponseWhenAlone")) {
155
+ this.send({
156
+ event: "error",
157
+ error: `Unexpected ClientJoin response. Details: ${JSON.stringify(event.details)}`,
158
+ });
159
+ // Keep going
160
+ }
161
+ const interest = telemetryEventInterestLevel(event.eventName);
162
+ if (interest === "none") {
163
+ return;
164
+ }
165
+ this.log.push({
166
+ timestamp: Date.now(),
167
+ agentId: process_id,
168
+ eventCategory: "telemetry",
169
+ eventName: event.eventName,
170
+ details: typeof event.details === "string" ? event.details : JSON.stringify(event.details),
171
+ });
172
+ if (verbosity.includes("telem")) {
173
+ selectiveVerboseLog(event, logLevel);
174
+ }
175
+ },
176
+ };
177
+ this.onDisconnected = () => {
178
+ // Test state is a bit fragile and does not account for reconnections.
179
+ this.send({ event: "error", error: `${process_id}: Container disconnected` });
180
+ };
181
+ }
182
+ send(msg) {
183
+ this.log.push({
184
+ timestamp: Date.now(),
185
+ agentId: process_id,
186
+ eventCategory: "messageSent",
187
+ eventName: msg.event,
188
+ details: msg.event === "debugReportComplete" && msg.log
189
+ ? JSON.stringify({ logLength: msg.log.length })
190
+ : JSON.stringify(msg),
191
+ });
192
+ send(msg);
149
193
  }
150
194
  registerWorkspace(workspaceId, options) {
151
195
  if (!this.presence) {
152
- send({ event: "error", error: `${process_id} is not connected to presence` });
196
+ this.send({ event: "error", error: `${process_id} is not connected to presence` });
153
197
  return;
154
198
  }
155
199
  const { latest, latestMap } = options;
@@ -160,7 +204,7 @@ class MessageHandler {
160
204
  // TODO: AB#47518
161
205
  const latestState = workspace.states.latest;
162
206
  latestState.events.on("remoteUpdated", (update) => {
163
- send({
207
+ this.send({
164
208
  event: "latestValueUpdated",
165
209
  workspaceId,
166
210
  attendeeId: update.attendee.attendeeId,
@@ -168,7 +212,7 @@ class MessageHandler {
168
212
  });
169
213
  });
170
214
  for (const remote of latestState.getRemotes()) {
171
- send({
215
+ this.send({
172
216
  event: "latestValueUpdated",
173
217
  workspaceId,
174
218
  attendeeId: remote.attendee.attendeeId,
@@ -185,7 +229,7 @@ class MessageHandler {
185
229
  const latestMapState = workspace.states.latestMap;
186
230
  latestMapState.events.on("remoteUpdated", (update) => {
187
231
  for (const [key, valueWithMetadata] of update.items) {
188
- send({
232
+ this.send({
189
233
  event: "latestMapValueUpdated",
190
234
  workspaceId,
191
235
  attendeeId: update.attendee.attendeeId,
@@ -196,7 +240,7 @@ class MessageHandler {
196
240
  });
197
241
  for (const remote of latestMapState.getRemotes()) {
198
242
  for (const [key, valueWithMetadata] of remote.items) {
199
- send({
243
+ this.send({
200
244
  event: "latestMapValueUpdated",
201
245
  workspaceId,
202
246
  attendeeId: remote.attendee.attendeeId,
@@ -207,7 +251,7 @@ class MessageHandler {
207
251
  }
208
252
  }
209
253
  this.workspaces.set(workspaceId, workspace);
210
- send({
254
+ this.send({
211
255
  event: "workspaceRegistered",
212
256
  workspaceId,
213
257
  latest: latest ?? false,
@@ -216,15 +260,33 @@ class MessageHandler {
216
260
  }
217
261
  async onMessage(msg) {
218
262
  if (verbosity.includes("msgs")) {
263
+ this.log.push({
264
+ timestamp: Date.now(),
265
+ agentId: process_id,
266
+ eventCategory: "messageReceived",
267
+ eventName: msg.command,
268
+ });
219
269
  console.log(`[${process_id}] Received`, msg);
220
270
  }
271
+ if (msg.command === "ping") {
272
+ this.handlePing();
273
+ return;
274
+ }
275
+ if (msg.command === "connect") {
276
+ await this.handleConnect(msg);
277
+ return;
278
+ }
279
+ // All other message must wait if connect is in progress
280
+ if (this.msgQueue !== undefined) {
281
+ this.msgQueue.push(msg);
282
+ return;
283
+ }
284
+ this.processMessage(msg);
285
+ }
286
+ processMessage(msg) {
221
287
  switch (msg.command) {
222
- case "ping": {
223
- this.handlePing();
224
- break;
225
- }
226
- case "connect": {
227
- await this.handleConnect(msg);
288
+ case "debugReport": {
289
+ this.handleDebugReport(msg);
228
290
  break;
229
291
  }
230
292
  case "disconnectSelf": {
@@ -255,74 +317,125 @@ class MessageHandler {
255
317
  break;
256
318
  }
257
319
  default: {
258
- console.error(`${process_id}: Unknown command`);
259
- send({ event: "error", error: `${process_id} Unknown command` });
320
+ console.error(`${process_id}: Unknown command:`, msg);
321
+ this.send({
322
+ event: "error",
323
+ error: `${process_id} Unknown command: ${JSON.stringify(msg)}`,
324
+ });
260
325
  }
261
326
  }
262
327
  }
263
328
  handlePing() {
264
- send({ event: "ack" });
329
+ this.send({ event: "ack" });
265
330
  }
266
331
  async handleConnect(msg) {
267
332
  if (!msg.user) {
268
- send({ event: "error", error: `${process_id}: No azure user information given` });
333
+ this.send({ event: "error", error: `${process_id}: No azure user information given` });
269
334
  return;
270
335
  }
271
- if (isConnected(this.container)) {
272
- send({ event: "error", error: `${process_id}: Already connected to container` });
336
+ if (this.container) {
337
+ this.send({ event: "error", error: `${process_id}: Container already loaded` });
273
338
  return;
274
339
  }
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);
340
+ // Prevent reentrance. Queue messages until after connect is fully processed.
341
+ this.msgQueue = [];
342
+ try {
343
+ const { container, containerId, connected } = await getOrCreateContainer({
344
+ ...msg,
345
+ logger: this.logger,
346
+ onDisconnected: this.onDisconnected,
347
+ });
348
+ this.container = container;
349
+ const presence = getPresence(container);
350
+ this.presence = presence;
351
+ // wait for 'ConnectionState.Connected'
352
+ await connected;
353
+ // Acknowledge connection before sending current attendee information
354
+ this.send({
355
+ event: "connected",
356
+ containerId,
357
+ attendeeId: presence.attendees.getMyself().attendeeId,
358
+ });
359
+ // Send existing attendees excluding self to parent/orchestrator
360
+ const self = presence.attendees.getMyself();
361
+ for (const attendee of presence.attendees.getAttendees()) {
362
+ if (attendee !== self && attendee.getConnectionStatus() === "Connected") {
363
+ this.sendAttendeeConnected(attendee);
364
+ }
365
+ }
366
+ // Listen for presence events to notify parent/orchestrator when a new attendee joins or leaves the session.
367
+ presence.attendees.events.on("attendeeConnected", this.sendAttendeeConnected);
368
+ presence.attendees.events.on("attendeeDisconnected", this.sendAttendeeDisconnected);
369
+ }
370
+ finally {
371
+ // Process any queued messages received while connecting
372
+ for (const queuedMsg of this.msgQueue) {
373
+ this.processMessage(queuedMsg);
290
374
  }
375
+ this.msgQueue = undefined;
291
376
  }
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);
377
+ }
378
+ handleDebugReport(msg) {
379
+ if (msg.reportAttendees) {
380
+ if (this.presence) {
381
+ const attendees = this.presence.attendees.getAttendees();
382
+ let connectedCount = 0;
383
+ for (const attendee of attendees) {
384
+ if (attendee.getConnectionStatus() === "Connected") {
385
+ connectedCount++;
386
+ }
387
+ }
388
+ console.log(`[${process_id}] Report: ${attendees.size} attendees, ${connectedCount} connected`);
389
+ }
390
+ else {
391
+ this.send({ event: "error", error: `${process_id} is not connected to presence` });
392
+ }
393
+ }
394
+ const debugReport = {
395
+ event: "debugReportComplete",
396
+ };
397
+ if (msg.sendEventLog) {
398
+ debugReport.log = this.log;
399
+ }
400
+ this.send(debugReport);
295
401
  }
296
402
  handleDisconnectSelf() {
297
403
  if (!this.container) {
298
- send({ event: "error", error: `${process_id} is not connected to container` });
404
+ this.send({ event: "error", error: `${process_id} is not connected to container` });
299
405
  return;
300
406
  }
407
+ // There are no current scenarios where disconnect without presence is expected.
301
408
  if (!this.presence) {
302
- send({ event: "error", error: `${process_id} is not connected to presence` });
409
+ this.send({ event: "error", error: `${process_id} is not connected to presence` });
303
410
  return;
304
411
  }
412
+ // Disconnect event is treated as an error in normal handling.
413
+ // Remove listener as this disconnect is intentional.
414
+ this.container.off("disconnected", this.onDisconnected);
305
415
  this.container.disconnect();
306
- send({
416
+ this.send({
307
417
  event: "disconnectedSelf",
308
418
  attendeeId: this.presence.attendees.getMyself().attendeeId,
309
419
  });
310
420
  }
311
421
  handleSetLatestValue(msg) {
312
422
  if (!this.presence) {
313
- send({ event: "error", error: `${process_id} is not connected to presence` });
423
+ this.send({ event: "error", error: `${process_id} is not connected to presence` });
314
424
  return;
315
425
  }
316
426
  const workspace = this.workspaces.get(msg.workspaceId);
317
427
  if (!workspace) {
318
- send({ event: "error", error: `${process_id} workspace ${msg.workspaceId} not found` });
428
+ this.send({
429
+ event: "error",
430
+ error: `${process_id} workspace ${msg.workspaceId} not found`,
431
+ });
319
432
  return;
320
433
  }
321
434
  // Cast required due to optional keys in WorkspaceSchema
322
435
  // TODO: AB#47518
323
436
  const latestState = workspace.states.latest;
324
437
  if (!latestState) {
325
- send({
438
+ this.send({
326
439
  event: "error",
327
440
  error: `${process_id} latest state not registered for workspace ${msg.workspaceId}`,
328
441
  });
@@ -335,23 +448,26 @@ class MessageHandler {
335
448
  }
336
449
  handleSetLatestMapValue(msg) {
337
450
  if (!this.presence) {
338
- send({ event: "error", error: `${process_id} is not connected to presence` });
451
+ this.send({ event: "error", error: `${process_id} is not connected to presence` });
339
452
  return;
340
453
  }
341
454
  if (typeof msg.key !== "string") {
342
- send({ event: "error", error: `${process_id} invalid key type` });
455
+ this.send({ event: "error", error: `${process_id} invalid key type` });
343
456
  return;
344
457
  }
345
458
  const workspace = this.workspaces.get(msg.workspaceId);
346
459
  if (!workspace) {
347
- send({ event: "error", error: `${process_id} workspace ${msg.workspaceId} not found` });
460
+ this.send({
461
+ event: "error",
462
+ error: `${process_id} workspace ${msg.workspaceId} not found`,
463
+ });
348
464
  return;
349
465
  }
350
466
  // Cast required due to optional keys in WorkspaceSchema
351
467
  // TODO: AB#47518
352
468
  const latestMapState = workspace.states.latestMap;
353
469
  if (!latestMapState) {
354
- send({
470
+ this.send({
355
471
  event: "error",
356
472
  error: `${process_id} latestMap state not registered for workspace ${msg.workspaceId}`,
357
473
  });
@@ -364,19 +480,22 @@ class MessageHandler {
364
480
  }
365
481
  handleGetLatestValue(msg) {
366
482
  if (!this.presence) {
367
- send({ event: "error", error: `${process_id} is not connected to presence` });
483
+ this.send({ event: "error", error: `${process_id} is not connected to presence` });
368
484
  return;
369
485
  }
370
486
  const workspace = this.workspaces.get(msg.workspaceId);
371
487
  if (!workspace) {
372
- send({ event: "error", error: `${process_id} workspace ${msg.workspaceId} not found` });
488
+ this.send({
489
+ event: "error",
490
+ error: `${process_id} workspace ${msg.workspaceId} not found`,
491
+ });
373
492
  return;
374
493
  }
375
494
  // Cast required due to optional keys in WorkspaceSchema
376
495
  // TODO: AB#47518
377
496
  const latestState = workspace.states.latest;
378
497
  if (!latestState) {
379
- send({
498
+ this.send({
380
499
  event: "error",
381
500
  error: `${process_id} latest state not registered for workspace ${msg.workspaceId}`,
382
501
  });
@@ -391,7 +510,7 @@ class MessageHandler {
391
510
  else {
392
511
  value = latestState.local;
393
512
  }
394
- send({
513
+ this.send({
395
514
  event: "latestValueGetResponse",
396
515
  workspaceId: msg.workspaceId,
397
516
  attendeeId: msg.attendeeId,
@@ -400,23 +519,26 @@ class MessageHandler {
400
519
  }
401
520
  handleGetLatestMapValue(msg) {
402
521
  if (!this.presence) {
403
- send({ event: "error", error: `${process_id} is not connected to presence` });
522
+ this.send({ event: "error", error: `${process_id} is not connected to presence` });
404
523
  return;
405
524
  }
406
525
  if (typeof msg.key !== "string") {
407
- send({ event: "error", error: `${process_id} invalid key type` });
526
+ this.send({ event: "error", error: `${process_id} invalid key type` });
408
527
  return;
409
528
  }
410
529
  const workspace = this.workspaces.get(msg.workspaceId);
411
530
  if (!workspace) {
412
- send({ event: "error", error: `${process_id} workspace ${msg.workspaceId} not found` });
531
+ this.send({
532
+ event: "error",
533
+ error: `${process_id} workspace ${msg.workspaceId} not found`,
534
+ });
413
535
  return;
414
536
  }
415
537
  // Cast required due to optional keys in WorkspaceSchema
416
538
  // TODO: AB#47518
417
539
  const latestMapState = workspace.states.latestMap;
418
540
  if (!latestMapState) {
419
- send({
541
+ this.send({
420
542
  event: "error",
421
543
  error: `${process_id} latestMap state not registered for workspace ${msg.workspaceId}`,
422
544
  });
@@ -432,7 +554,7 @@ class MessageHandler {
432
554
  else {
433
555
  value = latestMapState.local.get(msg.key);
434
556
  }
435
- send({
557
+ this.send({
436
558
  event: "latestMapValueGetResponse",
437
559
  workspaceId: msg.workspaceId,
438
560
  attendeeId: msg.attendeeId,