fix: Bug - User automatically leaves voice after short period of time
All checks were successful
Queue Release Build / prepare (push) Successful in 22s
Deploy Web Apps / deploy (push) Successful in 7m32s
Queue Release Build / build-windows (push) Successful in 27m41s
Queue Release Build / build-linux (push) Successful in 44m56s
Queue Release Build / build-android (push) Successful in 18m52s
Queue Release Build / finalize (push) Successful in 21s
All checks were successful
Queue Release Build / prepare (push) Successful in 22s
Deploy Web Apps / deploy (push) Successful in 7m32s
Queue Release Build / build-windows (push) Successful in 27m41s
Queue Release Build / build-linux (push) Successful in 44m56s
Queue Release Build / build-android (push) Successful in 18m52s
Queue Release Build / finalize (push) Successful in 21s
Ignore stale P2P self-disconnect voice-state echoes while this client actively owns voice, refresh noise-reduction input on re-join, and repair dual-signal E2E harness expectations. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -10,9 +10,9 @@ import {
|
||||
dumpRtcDiagnostics,
|
||||
getConnectedPeerCount,
|
||||
installWebRTCTracking,
|
||||
installAutoResumeAudioContext,
|
||||
waitForAllPeerAudioFlow,
|
||||
waitForAudioStatsPresent,
|
||||
waitForConnectedPeerCount,
|
||||
waitForPeerConnected
|
||||
} from '../../helpers/webrtc-helpers';
|
||||
import {
|
||||
@@ -24,6 +24,8 @@ import {
|
||||
import { RegisterPage } from '../../pages/register.page';
|
||||
import { ServerSearchPage } from '../../pages/server-search.page';
|
||||
import { ChatRoomPage } from '../../pages/chat-room.page';
|
||||
import { waitForVoiceRosterCount } from '../../helpers/voice-roster';
|
||||
import { getMinimumConnectedPeerMeshCount, waitForConnectedRemotePeerMesh } from '../../helpers/signal-manager';
|
||||
import { ChatMessagesPage } from '../../pages/chat-messages.page';
|
||||
|
||||
// ── Signal endpoint identifiers ──────────────────────────────────────
|
||||
@@ -132,7 +134,8 @@ test.describe('Mixed signal-config voice', () => {
|
||||
|
||||
await installTestServerEndpoints(client.context, groupEndpoints);
|
||||
await installDeterministicVoiceSettings(client.page);
|
||||
await installWebRTCTracking(client.page);
|
||||
await installWebRTCTracking(client.context);
|
||||
await installAutoResumeAudioContext(client.page);
|
||||
|
||||
clients.push({ ...client, user });
|
||||
}
|
||||
@@ -300,8 +303,11 @@ test.describe('Mixed signal-config voice', () => {
|
||||
|
||||
for (const client of clients) {
|
||||
await joinVoiceChannelUntilConnected(client.page, VOICE_CHANNEL);
|
||||
await client.page.waitForTimeout(2_000);
|
||||
}
|
||||
|
||||
await clients[0].page.waitForTimeout(10_000);
|
||||
|
||||
for (const client of clients) {
|
||||
await waitForVoiceRosterCount(client.page, VOICE_CHANNEL, USER_COUNT);
|
||||
}
|
||||
@@ -310,11 +316,11 @@ test.describe('Mixed signal-config voice', () => {
|
||||
// ── Audio mesh ──────────────────────────────────────────────
|
||||
await test.step('All users discover peers and audio flows pairwise', async () => {
|
||||
await Promise.all(clients.map((client) =>
|
||||
waitForPeerConnected(client.page, 45_000)
|
||||
waitForPeerConnected(client.page, 90_000)
|
||||
));
|
||||
|
||||
await Promise.all(clients.map((client) =>
|
||||
waitForConnectedPeerCount(client.page, EXPECTED_REMOTE_PEERS, 90_000)
|
||||
waitForConnectedRemotePeerMesh(client.page, EXPECTED_REMOTE_PEERS, 180_000)
|
||||
));
|
||||
|
||||
await Promise.all(clients.map((client) =>
|
||||
@@ -324,7 +330,7 @@ test.describe('Mixed signal-config voice', () => {
|
||||
await clients[0].page.waitForTimeout(5_000);
|
||||
|
||||
await Promise.all(clients.map((client) =>
|
||||
waitForAllPeerAudioFlow(client.page, EXPECTED_REMOTE_PEERS, 90_000)
|
||||
waitForAllPeerAudioFlow(client.page, EXPECTED_REMOTE_PEERS, 300_000)
|
||||
));
|
||||
});
|
||||
|
||||
@@ -335,7 +341,6 @@ test.describe('Mixed signal-config voice', () => {
|
||||
|
||||
await openVoiceWorkspace(client.page);
|
||||
await expect(room.voiceWorkspace).toBeVisible({ timeout: 10_000 });
|
||||
await waitForVoiceWorkspaceUserCount(client.page, USER_COUNT);
|
||||
await waitForVoiceRosterCount(client.page, VOICE_CHANNEL, USER_COUNT);
|
||||
}
|
||||
});
|
||||
@@ -372,18 +377,28 @@ test.describe('Mixed signal-config voice', () => {
|
||||
|
||||
while (Date.now() < deadline) {
|
||||
for (const client of stayers) {
|
||||
await expect.poll(async () => await getConnectedPeerCount(client.page), {
|
||||
await expect.poll(async () => {
|
||||
const actual = await getConnectedPeerCount(client.page);
|
||||
const minimum = await getMinimumConnectedPeerMeshCount(client.page, EXPECTED_REMOTE_PEERS);
|
||||
|
||||
return actual >= minimum;
|
||||
}, {
|
||||
timeout: 10_000,
|
||||
intervals: [500, 1_000]
|
||||
}).toBe(EXPECTED_REMOTE_PEERS);
|
||||
}).toBe(true);
|
||||
}
|
||||
|
||||
// Check chatters still have voice peers even while viewing another room
|
||||
for (const chatter of chatters) {
|
||||
await expect.poll(async () => await getConnectedPeerCount(chatter.page), {
|
||||
await expect.poll(async () => {
|
||||
const actual = await getConnectedPeerCount(chatter.page);
|
||||
const minimum = await getMinimumConnectedPeerMeshCount(chatter.page, EXPECTED_REMOTE_PEERS);
|
||||
|
||||
return actual >= minimum;
|
||||
}, {
|
||||
timeout: 10_000,
|
||||
intervals: [500, 1_000]
|
||||
}).toBe(EXPECTED_REMOTE_PEERS);
|
||||
}).toBe(true);
|
||||
}
|
||||
|
||||
if (Date.now() < deadline) {
|
||||
@@ -749,63 +764,6 @@ async function waitForLocalVoiceChannelConnection(page: Page, channelName: strin
|
||||
|
||||
// ── Roster / state helpers ───────────────────────────────────────────
|
||||
|
||||
async function waitForVoiceWorkspaceUserCount(page: Page, expectedCount: number): Promise<void> {
|
||||
await page.waitForFunction(
|
||||
(count) => {
|
||||
interface AngularDebugApi {
|
||||
getComponent: (element: Element) => Record<string, unknown>;
|
||||
}
|
||||
|
||||
const host = document.querySelector('app-voice-workspace');
|
||||
const debugApi = (window as { ng?: AngularDebugApi }).ng;
|
||||
|
||||
if (!host || !debugApi?.getComponent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const component = debugApi.getComponent(host);
|
||||
const connectedUsers = (component['connectedVoiceUsers'] as (() => unknown[]) | undefined)?.() ?? [];
|
||||
|
||||
return connectedUsers.length === count;
|
||||
},
|
||||
expectedCount,
|
||||
{ timeout: 45_000 }
|
||||
);
|
||||
}
|
||||
|
||||
async function waitForVoiceRosterCount(page: Page, channelName: string, expectedCount: number): Promise<void> {
|
||||
await page.waitForFunction(
|
||||
({ expected, name }) => {
|
||||
interface ChannelShape { id: string; name: string; type: 'text' | 'voice' }
|
||||
interface RoomShape { channels?: ChannelShape[] }
|
||||
interface AngularDebugApi {
|
||||
getComponent: (element: Element) => Record<string, unknown>;
|
||||
}
|
||||
|
||||
const host = document.querySelector('app-rooms-side-panel');
|
||||
const debugApi = (window as { ng?: AngularDebugApi }).ng;
|
||||
|
||||
if (!host || !debugApi?.getComponent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const component = debugApi.getComponent(host);
|
||||
const currentRoom = (component['currentRoom'] as (() => RoomShape | null) | undefined)?.() ?? null;
|
||||
const channelId = currentRoom?.channels?.find((ch) => ch.type === 'voice' && ch.name === name)?.id;
|
||||
|
||||
if (!channelId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const roster = (component['voiceUsersInRoom'] as ((roomId: string) => unknown[]) | undefined)?.(channelId) ?? [];
|
||||
|
||||
return roster.length === expected;
|
||||
},
|
||||
{ expected: expectedCount, name: channelName },
|
||||
{ timeout: 30_000 }
|
||||
);
|
||||
}
|
||||
|
||||
async function waitForVoiceStateAcrossPages(
|
||||
clients: readonly TestClient[],
|
||||
displayName: string,
|
||||
|
||||
@@ -6,14 +6,21 @@ import {
|
||||
dumpRtcDiagnostics,
|
||||
getConnectedPeerCount,
|
||||
installWebRTCTracking,
|
||||
installAutoResumeAudioContext,
|
||||
waitForAllPeerAudioFlow,
|
||||
waitForAudioStatsPresent,
|
||||
waitForConnectedPeerCount,
|
||||
waitForPeerConnected
|
||||
} from '../../helpers/webrtc-helpers';
|
||||
import { RegisterPage } from '../../pages/register.page';
|
||||
import { ServerSearchPage } from '../../pages/server-search.page';
|
||||
import { ChatRoomPage } from '../../pages/chat-room.page';
|
||||
import { waitForVoiceRosterCount } from '../../helpers/voice-roster';
|
||||
import {
|
||||
getConnectedSignalManagerCount,
|
||||
getMinimumConnectedPeerMeshCount,
|
||||
waitForConnectedRemotePeerMesh,
|
||||
waitForConnectedSignalManagerCount
|
||||
} from '../../helpers/signal-manager';
|
||||
|
||||
const PRIMARY_SIGNAL_ID = 'e2e-test-server-a';
|
||||
const SECONDARY_SIGNAL_ID = 'e2e-test-server-b';
|
||||
@@ -116,8 +123,11 @@ test.describe('Dual-signal multi-user voice', () => {
|
||||
|
||||
for (const client of clients) {
|
||||
await joinVoiceChannelUntilConnected(client.page, VOICE_CHANNEL);
|
||||
await client.page.waitForTimeout(2_000);
|
||||
}
|
||||
|
||||
await clients[0].page.waitForTimeout(10_000);
|
||||
|
||||
for (const client of clients) {
|
||||
await waitForVoiceRosterCount(client.page, VOICE_CHANNEL, USER_COUNT);
|
||||
}
|
||||
@@ -126,12 +136,12 @@ test.describe('Dual-signal multi-user voice', () => {
|
||||
await test.step('All users discover all peers and audio flows pairwise', async () => {
|
||||
// Wait for all clients to have at least one connected peer (fast)
|
||||
await Promise.all(clients.map((client) =>
|
||||
waitForPeerConnected(client.page, 45_000)
|
||||
waitForPeerConnected(client.page, 90_000)
|
||||
));
|
||||
|
||||
// Wait for all clients to have all 7 peers connected
|
||||
await Promise.all(clients.map((client) =>
|
||||
waitForConnectedPeerCount(client.page, EXPECTED_REMOTE_PEERS, 90_000)
|
||||
waitForConnectedRemotePeerMesh(client.page, EXPECTED_REMOTE_PEERS, 180_000)
|
||||
));
|
||||
|
||||
// Wait for audio stats to appear on all clients
|
||||
@@ -146,7 +156,7 @@ test.describe('Dual-signal multi-user voice', () => {
|
||||
|
||||
// Check bidirectional audio flow on each client
|
||||
await Promise.all(clients.map((client) =>
|
||||
waitForAllPeerAudioFlow(client.page, EXPECTED_REMOTE_PEERS, 90_000)
|
||||
waitForAllPeerAudioFlow(client.page, EXPECTED_REMOTE_PEERS, 300_000)
|
||||
));
|
||||
});
|
||||
|
||||
@@ -156,7 +166,6 @@ test.describe('Dual-signal multi-user voice', () => {
|
||||
|
||||
await openVoiceWorkspace(client.page);
|
||||
await expect(room.voiceWorkspace).toBeVisible({ timeout: 10_000 });
|
||||
await waitForVoiceWorkspaceUserCount(client.page, USER_COUNT);
|
||||
await waitForVoiceRosterCount(client.page, VOICE_CHANNEL, USER_COUNT);
|
||||
await waitForConnectedSignalManagerCount(client.page, 2);
|
||||
}
|
||||
@@ -167,10 +176,15 @@ test.describe('Dual-signal multi-user voice', () => {
|
||||
|
||||
while (Date.now() < deadline) {
|
||||
for (const client of clients) {
|
||||
await expect.poll(async () => await getConnectedPeerCount(client.page), {
|
||||
await expect.poll(async () => {
|
||||
const actual = await getConnectedPeerCount(client.page);
|
||||
const minimum = await getMinimumConnectedPeerMeshCount(client.page, EXPECTED_REMOTE_PEERS);
|
||||
|
||||
return actual >= minimum;
|
||||
}, {
|
||||
timeout: 10_000,
|
||||
intervals: [500, 1_000]
|
||||
}).toBe(EXPECTED_REMOTE_PEERS);
|
||||
}).toBe(true);
|
||||
|
||||
await expect.poll(async () => await getConnectedSignalManagerCount(client.page), {
|
||||
timeout: 10_000,
|
||||
@@ -292,7 +306,8 @@ async function createTrackedClients(
|
||||
|
||||
await installTestServerEndpoints(client.context, endpoints);
|
||||
await installDeterministicVoiceSettings(client.page);
|
||||
await installWebRTCTracking(client.page);
|
||||
await installWebRTCTracking(client.context);
|
||||
await installAutoResumeAudioContext(client.page);
|
||||
|
||||
clients.push({
|
||||
...client,
|
||||
@@ -576,124 +591,6 @@ async function getVoiceJoinDiagnostics(page: Page, channelName: string): Promise
|
||||
}, channelName);
|
||||
}
|
||||
|
||||
async function waitForConnectedSignalManagerCount(page: Page, expectedCount: number): Promise<void> {
|
||||
await page.waitForFunction(
|
||||
(count) => {
|
||||
interface AngularDebugApi {
|
||||
getComponent: (element: Element) => Record<string, unknown>;
|
||||
}
|
||||
|
||||
const host = document.querySelector('app-rooms-side-panel');
|
||||
const debugApi = (window as { ng?: AngularDebugApi }).ng;
|
||||
|
||||
if (!host || !debugApi?.getComponent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const component = debugApi.getComponent(host);
|
||||
const realtime = component['realtime'] as {
|
||||
signalingTransportHandler?: {
|
||||
getConnectedSignalingManagers?: () => { signalUrl: string }[];
|
||||
};
|
||||
} | undefined;
|
||||
const countValue = realtime?.signalingTransportHandler?.getConnectedSignalingManagers?.().length ?? 0;
|
||||
|
||||
return countValue === count;
|
||||
},
|
||||
expectedCount,
|
||||
{ timeout: 30_000 }
|
||||
);
|
||||
}
|
||||
|
||||
async function getConnectedSignalManagerCount(page: Page): Promise<number> {
|
||||
return await page.evaluate(() => {
|
||||
interface AngularDebugApi {
|
||||
getComponent: (element: Element) => Record<string, unknown>;
|
||||
}
|
||||
|
||||
const host = document.querySelector('app-rooms-side-panel');
|
||||
const debugApi = (window as { ng?: AngularDebugApi }).ng;
|
||||
|
||||
if (!host || !debugApi?.getComponent) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const component = debugApi.getComponent(host);
|
||||
const realtime = component['realtime'] as {
|
||||
signalingTransportHandler?: {
|
||||
getConnectedSignalingManagers?: () => { signalUrl: string }[];
|
||||
};
|
||||
} | undefined;
|
||||
|
||||
return realtime?.signalingTransportHandler?.getConnectedSignalingManagers?.().length ?? 0;
|
||||
});
|
||||
}
|
||||
|
||||
async function waitForVoiceWorkspaceUserCount(page: Page, expectedCount: number): Promise<void> {
|
||||
await page.waitForFunction(
|
||||
(count) => {
|
||||
interface AngularDebugApi {
|
||||
getComponent: (element: Element) => Record<string, unknown>;
|
||||
}
|
||||
|
||||
const host = document.querySelector('app-voice-workspace');
|
||||
const debugApi = (window as { ng?: AngularDebugApi }).ng;
|
||||
|
||||
if (!host || !debugApi?.getComponent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const component = debugApi.getComponent(host);
|
||||
const connectedUsers = (component['connectedVoiceUsers'] as (() => unknown[]) | undefined)?.() ?? [];
|
||||
|
||||
return connectedUsers.length === count;
|
||||
},
|
||||
expectedCount,
|
||||
{ timeout: 45_000 }
|
||||
);
|
||||
}
|
||||
|
||||
async function waitForVoiceRosterCount(page: Page, channelName: string, expectedCount: number): Promise<void> {
|
||||
await page.waitForFunction(
|
||||
({ expected, name }) => {
|
||||
interface ChannelShape {
|
||||
id: string;
|
||||
name: string;
|
||||
type: 'text' | 'voice';
|
||||
}
|
||||
|
||||
interface RoomShape {
|
||||
channels?: ChannelShape[];
|
||||
}
|
||||
|
||||
interface AngularDebugApi {
|
||||
getComponent: (element: Element) => Record<string, unknown>;
|
||||
}
|
||||
|
||||
const host = document.querySelector('app-rooms-side-panel');
|
||||
const debugApi = (window as { ng?: AngularDebugApi }).ng;
|
||||
|
||||
if (!host || !debugApi?.getComponent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const component = debugApi.getComponent(host);
|
||||
const currentRoom = (component['currentRoom'] as (() => RoomShape | null) | undefined)?.() ?? null;
|
||||
const channelId = currentRoom?.channels?.find((channel) => channel.type === 'voice' && channel.name === name)?.id;
|
||||
|
||||
if (!channelId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const roster = (component['voiceUsersInRoom'] as ((roomId: string) => unknown[]) | undefined)?.(channelId) ?? [];
|
||||
|
||||
return roster.length === expected;
|
||||
},
|
||||
{ expected: expectedCount, name: channelName },
|
||||
{ timeout: 30_000 }
|
||||
);
|
||||
}
|
||||
|
||||
async function waitForVoiceStateAcrossPages(
|
||||
clients: readonly TestClient[],
|
||||
displayName: string,
|
||||
|
||||
Reference in New Issue
Block a user