Skip to content

Conversation

@WillieHabi
Copy link
Contributor

@WillieHabi WillieHabi commented Jan 5, 2026

Description

Fixes AB#56686 -- self-attendee is announced via 'attendeeConnected' when the local client connects.

To test this, we added prepareDisconnectedPresence test helper which creates a presence instance in a disconnected state. This allows us to test and setup event listeners before initial connection occurs.

We also refactor usage of client2 as the local client in our unit testing to be more explicit. This is so when we do prepareConnectedPresence, the client is only added to the audience during the connect step.

List of test added:

- PresenceManager
    - self attendee
        - is announced via `attendeeConnected` upon initial connection
        - has status "Disconnected" when presence initializes while self not in Audience
        - 'has status "Disconnected" when runtime is connected but self is not yet in Audience
        - is announced via `attendeeConnected` when added to Audience while runtime is connected
        - has status "Connected" when announced via `attendeeConnected
        - is announced via `attendeeDisconnected` when local client disconnects
        - is announced via `attendeeConnected` when local client reconnects 

- self attendee
    - is announced via 'attendeeConnected' when local client connects
    - has status "Connected" when announced via 'attendeeConnected'
    - is announced via 'attendeeConnected' when local client reconnects
Copilot AI review requested due to automatic review settings January 5, 2026 22:14
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes issue AB#56686 by ensuring the self-attendee is announced via the attendeeConnected event when the local client connects or reconnects. Previously, this event was not being emitted for the self-attendee due to a TODO comment blocking the implementation.

Key changes:

  • Enabled the attendeeConnected event emission for self-attendee in systemWorkspace.ts
  • Added prepareDisconnectedPresence test helper to support testing connection events from a disconnected state
  • Added three comprehensive test cases to verify self-attendee connection announcement behavior

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
packages/framework/presence/src/systemWorkspace.ts Removed TODO comment and enabled event emission for self-attendee connection
packages/framework/presence/src/test/testUtils.ts Added new test helper function to create presence instances in disconnected state for testing connection events
packages/framework/presence/src/test/presenceManager.spec.ts Added test suite for self-attendee connection events and updated existing tests to exclude self-attendee from assertions

// Pass time (to mimic likely response)
clock.tick(broadcastJoinResponseDelaysMs.namedResponder + 20);

// Send a fake join response
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment on line 351 is missing contextual information that would help readers understand the purpose of the fake join response. The similar comment in prepareConnectedPresence (lines 227-230) includes a helpful explanation: "There are no other attendees in the session (not realistic) but this convinces the presence manager that it now has full knowledge, which enables it to respond to other's join requests accurately." Consider adding this explanation here as well for consistency and better code maintainability.

Suggested change
// Send a fake join response
// Send a fake join response. There are no other attendees in the session (not realistic) but
// this convinces the presence manager that it now has full knowledge, which enables it to
// respond to other's join requests accurately.

Copilot uses AI. Check for mistakes.
@WillieHabi WillieHabi requested a review from jason-ha January 5, 2026 23:19
@jason-ha
Copy link
Contributor

jason-ha commented Jan 6, 2026

runtime: MockEphemeralRuntime,

How is prepareConnectedPresence different from calling prepareDisconnectedPresence followed by using connectPresence that it returns?


Refers to: packages/framework/presence/src/test/testUtils.ts:141 in a11008f. [](commit_id = a11008f, deletion_comment = False)

Copy link
Contributor

@jason-ha jason-ha left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self-Attendee section should also have a test for attendeeDisconnected.

@WillieHabi
Copy link
Contributor Author

runtime: MockEphemeralRuntime,

How is prepareConnectedPresence different from calling prepareDisconnectedPresence followed by using connectPresence that it returns?

Refers to: packages/framework/presence/src/test/testUtils.ts:141 in a11008f. [](commit_id = a11008f, deletion_comment = False)

prepareConnectedPresence before would return instantiate presence but it wouldn't go through the full connected flow (runtime.connect). I changed this now so it does go through connect flow by using 'prepareDisconnectedPresence' + 'connect'.

@WillieHabi WillieHabi requested a review from jason-ha January 8, 2026 04:02
Copy link
Contributor

@jason-ha jason-ha left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't get through looking at everything, but here are some notes.
Also still missing a self attendeeDisconnected case.

Comment on lines 185 to 190
// Add to quorum as a write client (consistent with buildClientDataArray creating write clients)
const quorumSize = this.quorum.getMembers().size;
this.quorum.addMember(newClientId, {
client: newMember,
sequenceNumber: 10 * quorumSize,
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. I think there are a number of questions for this change:

  1. why is it needed / desired?
  2. assuming it is kept, where is the related removal?
  3. the timing does not appear to be correct. quorum is an op-based construct. This connect is should only go up to minimal connection status. Ops that change quorum would come later.
  4. There is no check to see that client is a write client and would appear in quorum.

Note that the Audience that Presence cares about is the Signal-only Audience.

Copy link
Contributor Author

@WillieHabi WillieHabi Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll ended up adding a syncWithQuorum method to MockEphemeralRuntime instead of doing this. I think that's better for testing purposes and more just correct, but here's the responses I wrote before and my thought process.

  1. When we connect/disconnect our local client in our testing, there's currently no mechanism to keep quorum in sync. This is desirable/needed for example when we test responding to ClientJoin -- since quorum is not synced with connect flow, the local client does not appear in quorum and cannot compute its join order. This only worked before because we used client2 as the designated local client and tested against it's hardcoded quorum sequenceNumber/join order. But if we had a test where we disconnect + reconnect with new clientID and try to test the same scenario it would all fall apart since quorum is not synced with the local connection flow.

  2. I saw we handle removal of clients from audience and quorum in runtime.removeMember(). But in runtime.disconnect() we don't handle removal of local client from either audience or quorum, which doesn't seem right since it's impossible for local client to disconnect and still be in quorum. I think maybe calling removeMember(this.clientId) in disconnect() would make sense here(?)

  3. Yeah this callout makes sense to me, the timing of audience and quorum removal at the same time is not right.

  4. We build the new client doing buildClientDataArray([clientId], 1 \* numWriteClients *\) so we always connect as write client.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that Quorum is a funny beast. When you join a document as a writer, it takes note of you being there. When you join later Quorum and traditional Audience will show add- and removeMember for everyone in the history (I think we don't see it too often because Summarization is aggressive and squashes the history.)
We largely do not require Quorum any longer. We do leverage it as fallback when other Join mechanism don't perform as intended. I hope that is very rare now. And things should be functional whether our local write client is in Quorum at time of demand or not. Ideally, we have coverage for both. What does our coverage look like for presenceDatastoreManager.ts lines 857 to 877 running just the Presence > protocol handling tests?

@WillieHabi WillieHabi requested a review from jason-ha January 9, 2026 17:32
Copy link
Contributor

@jason-ha jason-ha left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description is still stale

});

it("is announced via `attendeeConnected` when presence initializes while self is in Audience", () => {
it("is announced via `attendeeConnected` upon initial connection", () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"while self is in Audience" is important criteria/qualification.
What happens when runtime is connected but self isn't in Audience?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

renamed test to "is announced via attendeeConnected upon connect while self is in audience".

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

...What happens when runtime is connected but self isn't in Audience?

Copy link
Contributor Author

@WillieHabi WillieHabi Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apologies, I thought this was a rhetorical question -- completley forgot it was possible to be connected to service and not be present in signal audience. I added a test that confirms the self attendee has status Disconnected in this case.

I now am wondering if we're missing the case where self attendee is in audience when presence initializes but we are not connected to service. Is this possible? Or does transition to CatchingUp always beat membership in signalAudience?

edit: I now see in our usage of addClientConnectionId we trigger presence join based on audience addMember() events for self and we don't listen to "joined" events.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When disconnected, self should be removed from Audience. At least I think that is always supposed to be the case.

@WillieHabi WillieHabi requested a review from jason-ha January 14, 2026 05:38
Copy link
Contributor

@jason-ha jason-ha left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks pretty good - 75% nits

  • one test case request

logger,
));

runtime.addAudienceWriteClientsToQuorum();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this important here, but not something we see in other places?
(Please add comment)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah thanks for this callout, this is not needed anymore now that localClient defaults to being in audience/quorum.

);
});

it('has status "Disconnected" when presence initializes while self not in Audience', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be a case, but the implementation here has container fully disconnected. Fully disconnected trumps self not in Audience. At no time, AFAIK, should self be in Audience while disconnected.
The description for this logic could mention container disconnected container.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed to 'has status "Disconnected" when presence initializes while container is disconnected'

Copy link
Contributor

@jason-ha jason-ha left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good enough to approve :)
Be sure to update description with the test cases.
That said there looks like one more case of interest - could be added as a follow-up.

@WillieHabi WillieHabi requested a review from jason-ha January 15, 2026 23:50
Comment on lines +200 to +207
// Expect join signal when self is added to audience while connected
const expectedJoin = generateBasicClientJoin(clock.now, {
attendeeId: localAttendeeId,
clientConnectionId: initialLocalClientConnectionId,
updateProviders: ["client0", "client1", "client3"],
});
delete (expectedJoin as Partial<typeof expectedJoin>).clientId;
runtime.signalsExpected.push([expectedJoin]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like prepareExpectedClientJoin though that one figures out updateProviders on its own

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll get what I have so far in and address this one separately. Think I can maybe also pull out a prepareConnectedPresenceWithoutSelfInAudience for these two new tests as well.

@WillieHabi WillieHabi merged commit f41dea7 into microsoft:main Jan 16, 2026
30 checks passed
WillieHabi added a commit that referenced this pull request Jan 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants