backend: enhance stale recording evaluation logic and add comprehensive tests

This commit is contained in:
Carlos Santos 2025-08-14 17:50:21 +02:00
parent 3da1f0e041
commit 41e39bc3bd
2 changed files with 575 additions and 3 deletions

View File

@ -862,6 +862,7 @@ export class RecordingService {
*/
protected async evaluateAndAbortStaleRecording(egressInfo: EgressInfo): Promise<boolean> {
const recordingId = RecordingHelper.extractRecordingIdFromEgress(egressInfo);
const { roomId } = RecordingHelper.extractInfoFromRecordingId(recordingId);
const updatedAt = RecordingHelper.extractUpdatedDate(egressInfo);
const staleAfterMs = ms(INTERNAL_CONFIG.RECORDING_STALE_AFTER);
@ -878,17 +879,29 @@ export class RecordingService {
return false;
}
const lkRoomExists = await this.livekitService.roomExists(roomId);
const ageIsStale = updatedAt < Date.now() - staleAfterMs;
let isRecordingStale = false;
if (ageIsStale) {
if (!lkRoomExists) {
isRecordingStale = true; // There is no room and updated before stale time -> stale
} else {
const lkRoom = await this.livekitService.getRoom(roomId);
isRecordingStale = lkRoom.numPublishers === 0; // No publishers in the room and updated before stale time -> stale
}
}
// Check if recording has not been updated recently
const isStale = updatedAt < Date.now() - staleAfterMs;
this.logger.debug(`Recording ${recordingId} last updated at ${new Date(updatedAt).toISOString()}`);
if (!isStale) {
if (!isRecordingStale) {
this.logger.debug(`Recording ${recordingId} is still fresh`);
return false;
}
this.logger.warn(`Recording ${recordingId} is stale, aborting...`);
this.logger.warn(`Room ${roomId} does not exist and recording ${recordingId} is stale, aborting...`);
// Abort the recording
const { egressId } = RecordingHelper.extractInfoFromRecordingId(recordingId);

View File

@ -0,0 +1,559 @@
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, jest } from '@jest/globals';
import { EgressInfo, EgressStatus, Room } from 'livekit-server-sdk';
import ms from 'ms';
import { container } from '../../../../src/config/index.js';
import INTERNAL_CONFIG from '../../../../src/config/internal-config.js';
import { MeetLock } from '../../../../src/helpers/index.js';
import {
LiveKitService,
LoggerService,
MeetStorageService,
MutexService,
RecordingService
} from '../../../../src/services/index.js';
import { MeetRecordingInfo, MeetRecordingStatus } from '../../../../src/typings/ce/index.js';
import { startTestServer } from '../../../helpers/request-helpers.js';
describe('Recording Cleanup Tests', () => {
let recordingService: RecordingService;
let mutexService: MutexService;
let livekitService: LiveKitService;
let storageService: MeetStorageService;
const getRecordingLock = (roomId: string) => MeetLock.getRecordingActiveLock(roomId);
const testRooms = {
recentLock: 'room-recent-lock',
withPublishers: 'room-with-publishers',
withoutPublishersWithRecording: 'room-without-publishers-with-recording',
withoutPublishersNoRecording: 'room-without-publishers-no-recording',
nonExistentWithRecording: 'room-non-existent-with-recording',
nonExistentNoRecording: 'room-non-existent-no-recording',
staleRecording: 'room-stale-recording',
freshRecording: 'room-fresh-recording',
abortedRecording: 'room-aborted-recording',
noUpdatedAt: 'room-no-updated-at'
};
beforeAll(() => {
startTestServer();
recordingService = container.get(RecordingService);
mutexService = container.get(MutexService);
livekitService = container.get(LiveKitService);
storageService = container.get(MeetStorageService);
// Mute logs for the test
const logger = container.get(LoggerService);
jest.spyOn(logger, 'debug').mockImplementation(() => {});
jest.spyOn(logger, 'verbose').mockImplementation(() => {});
jest.spyOn(logger, 'info').mockImplementation(() => {});
jest.spyOn(logger, 'warn').mockImplementation(() => {});
jest.spyOn(logger, 'error').mockImplementation(() => {});
});
beforeEach(async () => {
// Clean up any existing locks before each test
for (const roomId of Object.values(testRooms)) {
try {
await mutexService.release(getRecordingLock(roomId));
} catch (e) {
// Ignore errors if the lock does not exist
}
}
// Setup spies
jest.spyOn(mutexService, 'getLocksByPrefix');
jest.spyOn(mutexService, 'lockExists');
jest.spyOn(mutexService, 'getLockCreatedAt');
jest.spyOn(mutexService, 'release');
jest.spyOn(livekitService, 'roomExists');
jest.spyOn(livekitService, 'getRoom');
jest.spyOn(livekitService, 'getInProgressRecordingsEgress');
jest.spyOn(livekitService, 'stopEgress');
jest.spyOn(storageService, 'getRecordingMetadata');
jest.spyOn(storageService, 'saveRecordingMetadata');
jest.spyOn(recordingService as never, 'performRecordingLocksGarbageCollection');
jest.spyOn(recordingService as never, 'evaluateAndReleaseOrphanedLock');
jest.spyOn(recordingService as never, 'performStaleRecordingsCleanup');
jest.spyOn(recordingService as never, 'evaluateAndAbortStaleRecording');
jest.spyOn(recordingService, 'getRecording');
jest.spyOn(recordingService as never, 'updateRecordingStatus');
jest.clearAllMocks();
// Do not set up global mocks here to improve test isolation
});
afterEach(async () => {
// Clean up all spies and its invocations
jest.clearAllMocks();
jest.restoreAllMocks();
// Explicitly restore the mock behavior for getLockCreatedAt
if (mutexService.getLockCreatedAt && jest.isMockFunction(mutexService.getLockCreatedAt)) {
(mutexService.getLockCreatedAt as jest.Mock).mockReset();
}
});
afterAll(async () => {
// Clean up all test locks
for (const roomId of Object.values(testRooms)) {
try {
await mutexService.release(getRecordingLock(roomId));
} catch (e) {
// Ignore errors if the lock does not exist
}
}
// Restore all mocks
jest.restoreAllMocks();
});
/**
* Creates a mock EgressInfo object for testing
*/
function createMockEgressInfo(
roomId: string,
egressId: string,
status: EgressStatus,
updatedAt?: number
): EgressInfo {
const uid = '1234567890';
return {
egressId,
roomId,
roomName: roomId,
status,
updatedAt: updatedAt ? BigInt(updatedAt * 1_000_000) : undefined, // Convert to nanoseconds
startedAt: BigInt(Date.now() * 1_000_000),
endedAt: status === EgressStatus.EGRESS_COMPLETE ? BigInt(Date.now() * 1_000_000) : undefined,
fileResults: [
{
filename: `${roomId}--${uid}.mp4`,
size: BigInt(1024 * 1024), // 1MB
duration: BigInt(60 * 1_000_000_000) // 60 seconds in nanoseconds
}
],
streamResults: [],
request: {
case: 'roomComposite'
}
} as unknown as EgressInfo;
}
/**
* Creates a mock MeetRecordingInfo object for testing
*/
function createMockRecordingInfo(
recordingId: string,
roomId: string,
status: MeetRecordingStatus
): MeetRecordingInfo {
return {
recordingId,
roomId,
roomName: roomId,
status,
startDate: Date.now() - ms('10m'),
filename: `${recordingId}.mp4`
};
}
describe('Perform Stale Recordings Cleanup', () => {
it('should not process any recordings when there are no in-progress recordings', async () => {
// Simulate empty response from LiveKit
(livekitService.getInProgressRecordingsEgress as jest.Mock).mockResolvedValueOnce([] as never);
// Execute the stale recordings cleanup
await recordingService['performStaleRecordingsCleanup']();
// Verify that we checked for recordings but didn't attempt to process any
expect(livekitService.getInProgressRecordingsEgress).toHaveBeenCalled();
expect((recordingService as never)['evaluateAndAbortStaleRecording']).not.toHaveBeenCalled();
});
it('should gracefully handle errors during in-progress recordings retrieval', async () => {
// Simulate LiveKit service failure
(livekitService.getInProgressRecordingsEgress as jest.Mock).mockRejectedValueOnce(
new Error('Failed to retrieve recordings') as never
);
// Execute the stale recordings cleanup - should not throw
await recordingService['performStaleRecordingsCleanup']();
// Verify the error was handled properly without further processing
expect(livekitService.getInProgressRecordingsEgress).toHaveBeenCalled();
expect((recordingService as never)['evaluateAndAbortStaleRecording']).not.toHaveBeenCalled();
});
it('should process each in-progress recording to detect and abort stale ones', async () => {
const roomIds = [testRooms.staleRecording, testRooms.freshRecording, testRooms.abortedRecording];
const mockEgressInfos = roomIds.map((roomId) =>
createMockEgressInfo(roomId, `EG_${roomId}`, EgressStatus.EGRESS_ACTIVE)
);
// Simulate existing in-progress recordings in the system
(livekitService.getInProgressRecordingsEgress as jest.Mock).mockResolvedValueOnce(mockEgressInfos as never);
// Execute the stale recordings cleanup
await recordingService['performStaleRecordingsCleanup']();
// Verify that each recording was processed individually
expect((recordingService as never)['evaluateAndAbortStaleRecording']).toHaveBeenCalledTimes(3);
mockEgressInfos.forEach((egressInfo) => {
expect((recordingService as never)['evaluateAndAbortStaleRecording']).toHaveBeenCalledWith(egressInfo);
});
});
});
describe('Evaluate and Abort Stale Recording', () => {
it('should skip processing if the recording is already aborted', async () => {
const roomId = testRooms.abortedRecording;
const egressId = `EG_${roomId}`;
const egressInfo = createMockEgressInfo(roomId, egressId, EgressStatus.EGRESS_ACTIVE);
// Mock recording as already aborted
const mockRecordingInfo = createMockRecordingInfo(
`${roomId}--${egressId}--1234567890`,
roomId,
MeetRecordingStatus.ABORTED
);
(recordingService.getRecording as jest.Mock).mockResolvedValueOnce(mockRecordingInfo as never);
// Execute evaluateAndAbortStaleRecording
const result = await recordingService['evaluateAndAbortStaleRecording'](egressInfo);
// Verify that the method returned true (already aborted)
expect(result).toBe(true);
expect(recordingService.getRecording).toHaveBeenCalled();
expect(livekitService.roomExists).not.toHaveBeenCalled();
expect(recordingService['updateRecordingStatus']).not.toHaveBeenCalled();
expect(livekitService.stopEgress).not.toHaveBeenCalled();
});
it('should skip processing if the recording has no updatedAt timestamp', async () => {
const roomId = testRooms.noUpdatedAt;
const egressId = `EG_${roomId}`;
const egressInfo = createMockEgressInfo(roomId, egressId, EgressStatus.EGRESS_ACTIVE); // No updatedAt
// Mock recording as active
const mockRecordingInfo = createMockRecordingInfo(
`${roomId}--${egressId}--1234567890`,
roomId,
MeetRecordingStatus.ACTIVE
);
(recordingService.getRecording as jest.Mock).mockResolvedValueOnce(mockRecordingInfo as never);
// Execute evaluateAndAbortStaleRecording
const result = await recordingService['evaluateAndAbortStaleRecording'](egressInfo);
// Verify that the method returned false (kept as fresh)
expect(result).toBe(false);
expect(recordingService.getRecording).toHaveBeenCalled();
expect(livekitService.roomExists).not.toHaveBeenCalled();
expect(recordingService['updateRecordingStatus']).not.toHaveBeenCalled();
expect(livekitService.stopEgress).not.toHaveBeenCalled();
});
it('should keep recording as fresh if it has been updated recently', async () => {
const roomId = testRooms.freshRecording;
const egressId = `EG_${roomId}`;
const recentUpdateTime = Date.now() - ms('1m'); // 1 minute ago (fresh)
const egressInfo = createMockEgressInfo(roomId, egressId, EgressStatus.EGRESS_ACTIVE, recentUpdateTime);
// Mock recording as active
const mockRecordingInfo = createMockRecordingInfo(
`${roomId}--${egressId}--1234567890`,
roomId,
MeetRecordingStatus.ACTIVE
);
(recordingService.getRecording as jest.Mock).mockResolvedValueOnce(mockRecordingInfo as never);
(livekitService.roomExists as jest.Mock).mockResolvedValueOnce(true as never);
// Execute evaluateAndAbortStaleRecording
const result = await recordingService['evaluateAndAbortStaleRecording'](egressInfo);
// Verify that the method returned false (still fresh)
expect(result).toBe(false);
expect(recordingService.getRecording).toHaveBeenCalled();
expect(livekitService.roomExists).toHaveBeenCalledWith(roomId);
expect(livekitService.stopEgress).not.toHaveBeenCalled();
expect(recordingService['updateRecordingStatus']).not.toHaveBeenCalled();
});
it('should abort recording if room does not exist and recording updated time is stale', async () => {
const roomId = testRooms.staleRecording;
const egressId = `EG_${roomId}`;
const recordingId = `${roomId}--${egressId}--1234567890`;
const staleUpdateTime = Date.now() - ms('10m'); // 10 minutes ago (stale)
const egressInfo = createMockEgressInfo(roomId, egressId, EgressStatus.EGRESS_ACTIVE, staleUpdateTime);
// Mock recording as active
const mockRecordingInfo = createMockRecordingInfo(recordingId, roomId, MeetRecordingStatus.ACTIVE);
(recordingService.getRecording as jest.Mock).mockResolvedValueOnce(mockRecordingInfo as never);
(livekitService.roomExists as jest.Mock).mockResolvedValueOnce(false as never);
(livekitService.stopEgress as jest.Mock).mockResolvedValueOnce({} as never);
((recordingService as never)['updateRecordingStatus'] as jest.Mock).mockResolvedValueOnce(
undefined as never
);
// Execute evaluateAndAbortStaleRecording
const result = await recordingService['evaluateAndAbortStaleRecording'](egressInfo);
// Verify that the recording was aborted
expect(result).toBe(true);
expect(recordingService.getRecording).toHaveBeenCalledWith(recordingId);
expect(livekitService.roomExists).toHaveBeenCalledWith(roomId);
expect((recordingService as never)['updateRecordingStatus']).toHaveBeenCalledWith(
recordingId,
MeetRecordingStatus.ABORTED
);
expect(livekitService.stopEgress).toHaveBeenCalledWith(egressId);
});
it('should abort recording if room exists with no publishers and updated time is stale', async () => {
const roomId = testRooms.staleRecording;
const egressId = `EG_${roomId}`;
const recordingId = `${roomId}--${egressId}--1234567890`;
const staleUpdateTime = Date.now() - ms('10m'); // 10 minutes ago (stale)
const egressInfo = createMockEgressInfo(roomId, egressId, EgressStatus.EGRESS_ACTIVE, staleUpdateTime);
// Mock recording as active
const mockRecordingInfo = createMockRecordingInfo(recordingId, roomId, MeetRecordingStatus.ACTIVE);
(recordingService.getRecording as jest.Mock).mockResolvedValue(mockRecordingInfo as never);
(livekitService.roomExists as jest.Mock).mockResolvedValue(true as never); // Room exists
(livekitService.getRoom as jest.Mock).mockResolvedValue({
numParticipants: 0,
numPublishers: 0
} as Room as never);
(livekitService.stopEgress as jest.Mock).mockResolvedValue({} as never);
((recordingService as never)['updateRecordingStatus'] as jest.Mock).mockResolvedValueOnce(
undefined as never
);
// Execute evaluateAndAbortStaleRecording
const result = await recordingService['evaluateAndAbortStaleRecording'](egressInfo);
// Verify that the recording was aborted
expect(result).toBe(true);
expect(recordingService.getRecording).toHaveBeenCalledWith(recordingId);
expect(livekitService.roomExists).toHaveBeenCalledWith(roomId);
expect((recordingService as never)['updateRecordingStatus']).toHaveBeenCalledWith(
recordingId,
MeetRecordingStatus.ABORTED
);
expect(livekitService.stopEgress).toHaveBeenCalledWith(egressId);
});
it('should keep recording if it has been updated recently regardless room existence', async () => {
const roomId = testRooms.staleRecording;
const egressId = `EG_${roomId}`;
const recordingId = `${roomId}--${egressId}--1234567890`;
const egressInfo = createMockEgressInfo(roomId, egressId, EgressStatus.EGRESS_ACTIVE, Date.now());
// Mock recording as active
const mockRecordingInfo = createMockRecordingInfo(recordingId, roomId, MeetRecordingStatus.ACTIVE);
(recordingService.getRecording as jest.Mock).mockResolvedValue(mockRecordingInfo as never);
(livekitService.roomExists as jest.Mock).mockResolvedValueOnce(true as never); // Room exists
// Execute evaluateAndAbortStaleRecording
let result = await recordingService['evaluateAndAbortStaleRecording'](egressInfo);
// Verify that the recording was kept fresh (not aborted)
expect(result).toBe(false);
expect(recordingService.getRecording).toHaveBeenCalledWith(recordingId);
expect(livekitService.roomExists).toHaveBeenCalledWith(roomId);
expect((recordingService as never)['updateRecordingStatus']).not.toHaveBeenCalled();
expect(livekitService.stopEgress).not.toHaveBeenCalled();
(livekitService.roomExists as jest.Mock).mockResolvedValueOnce(false as never); // Room exists
// Execute evaluateAndAbortStaleRecording
result = await recordingService['evaluateAndAbortStaleRecording'](egressInfo);
// Verify that the recording was kept fresh (not aborted)
expect(result).toBe(false);
expect(recordingService.getRecording).toHaveBeenCalledWith(recordingId);
expect(livekitService.roomExists).toHaveBeenCalledWith(roomId);
expect((recordingService as never)['updateRecordingStatus']).not.toHaveBeenCalled();
expect(livekitService.stopEgress).not.toHaveBeenCalled();
});
it('should keep recording if room exists with publishers even when updated time is stale', async () => {
const roomId = testRooms.staleRecording;
const egressId = `EG_${roomId}`;
const recordingId = `${roomId}--${egressId}--1234567890`;
const staleUpdateTime = Date.now() - ms('10m'); // 10 minutes ago (stale)
const egressInfo = createMockEgressInfo(roomId, egressId, EgressStatus.EGRESS_ACTIVE, staleUpdateTime);
// Mock recording as active
const mockRecordingInfo = createMockRecordingInfo(recordingId, roomId, MeetRecordingStatus.ACTIVE);
(recordingService.getRecording as jest.Mock).mockResolvedValueOnce(mockRecordingInfo as never);
(livekitService.roomExists as jest.Mock).mockResolvedValueOnce(true as never);
(livekitService.getRoom as jest.Mock).mockResolvedValueOnce({
numParticipants: 1,
numPublishers: 1
} as Room as never);
// Execute evaluateAndAbortStaleRecording
const result = await recordingService['evaluateAndAbortStaleRecording'](egressInfo);
// Verify that the recording was kept fresh (not aborted)
expect(result).toBe(false);
expect(recordingService.getRecording).toHaveBeenCalledWith(recordingId);
expect(livekitService.roomExists).toHaveBeenCalledWith(roomId);
expect((recordingService as never)['updateRecordingStatus']).not.toHaveBeenCalled();
expect(livekitService.stopEgress).not.toHaveBeenCalled();
});
it('should keep recording if room exists with no publishers but updated time is recent', async () => {
const roomId = testRooms.staleRecording;
const egressId = `EG_${roomId}`;
const recordingId = `${roomId}--${egressId}--1234567890`;
const recentUpdateTime = Date.now() - ms('1m'); // 1 minute ago (recent)
const egressInfo = createMockEgressInfo(roomId, egressId, EgressStatus.EGRESS_ACTIVE, recentUpdateTime);
// Mock recording as active
const mockRecordingInfo = createMockRecordingInfo(recordingId, roomId, MeetRecordingStatus.ACTIVE);
(recordingService.getRecording as jest.Mock).mockResolvedValueOnce(mockRecordingInfo as never);
(livekitService.roomExists as jest.Mock).mockResolvedValueOnce(true as never);
(livekitService.getRoom as jest.Mock).mockResolvedValueOnce({
numParticipants: 1,
numPublishers: 0
} as Room as never);
// Execute evaluateAndAbortStaleRecording
const result = await recordingService['evaluateAndAbortStaleRecording'](egressInfo);
// Verify that the recording was kept fresh (not aborted)
expect(result).toBe(false);
expect(recordingService.getRecording).toHaveBeenCalledWith(recordingId);
expect(livekitService.roomExists).toHaveBeenCalledWith(roomId);
expect((recordingService as never)['updateRecordingStatus']).not.toHaveBeenCalled();
expect(livekitService.stopEgress).not.toHaveBeenCalled();
});
it('should keep recording if room exists with publishers and updated time is recent', async () => {
const roomId = testRooms.staleRecording;
const egressId = `EG_${roomId}`;
const recordingId = `${roomId}--${egressId}--1234567890`;
const recentUpdateTime = Date.now() - ms('1m'); // 1 minute ago (recent)
const egressInfo = createMockEgressInfo(roomId, egressId, EgressStatus.EGRESS_ACTIVE, recentUpdateTime);
// Mock recording as active
const mockRecordingInfo = createMockRecordingInfo(recordingId, roomId, MeetRecordingStatus.ACTIVE);
(recordingService.getRecording as jest.Mock).mockResolvedValueOnce(mockRecordingInfo as never);
(livekitService.roomExists as jest.Mock).mockResolvedValueOnce(true as never);
(livekitService.getRoom as jest.Mock).mockResolvedValueOnce({
numParticipants: 1,
numPublishers: 1
} as Room as never);
// Execute evaluateAndAbortStaleRecording
const result = await recordingService['evaluateAndAbortStaleRecording'](egressInfo);
// Verify that the recording was kept fresh (not aborted)
expect(result).toBe(false);
expect(recordingService.getRecording).toHaveBeenCalledWith(recordingId);
expect(livekitService.roomExists).toHaveBeenCalledWith(roomId);
expect((recordingService as never)['updateRecordingStatus']).not.toHaveBeenCalled();
expect(livekitService.stopEgress).not.toHaveBeenCalled();
});
it('should handle edge case when updatedAt is exactly on the staleAfterMs threshold', async () => {
const roomId = testRooms.staleRecording;
const egressId = `EG_${roomId}`;
const recordingId = `${roomId}--${egressId}--1234567890`;
const staleUpdateTime = Date.now() - ms(INTERNAL_CONFIG.RECORDING_STALE_AFTER);
const egressInfo = createMockEgressInfo(roomId, egressId, EgressStatus.EGRESS_ACTIVE, staleUpdateTime);
// Mock recording as active
const mockRecordingInfo = createMockRecordingInfo(recordingId, roomId, MeetRecordingStatus.ACTIVE);
(recordingService.getRecording as jest.Mock).mockResolvedValueOnce(mockRecordingInfo as never);
(livekitService.roomExists as jest.Mock).mockResolvedValueOnce(false as never);
(livekitService.stopEgress as jest.Mock).mockResolvedValueOnce({} as never);
((recordingService as never)['updateRecordingStatus'] as jest.Mock).mockResolvedValueOnce(
undefined as never
);
// Execute evaluateAndAbortStaleRecording
const result = await recordingService['evaluateAndAbortStaleRecording'](egressInfo);
// Verify that the recording was aborted
expect(result).toBe(true);
expect(recordingService.getRecording).toHaveBeenCalledWith(recordingId);
expect(livekitService.roomExists).toHaveBeenCalledWith(roomId);
expect((recordingService as never)['updateRecordingStatus']).toHaveBeenCalledWith(
recordingId,
MeetRecordingStatus.ABORTED
);
expect(livekitService.stopEgress).toHaveBeenCalledWith(egressId);
});
it('should handle errors during recording processing and rethrow them', async () => {
const roomId = testRooms.staleRecording;
const egressId = `EG_${roomId}`;
const recordingId = `${roomId}--${egressId}--1234567890`;
const egressInfo = createMockEgressInfo(roomId, egressId, EgressStatus.EGRESS_ACTIVE);
// Mock error during recording retrieval
(recordingService.getRecording as jest.Mock).mockRejectedValueOnce(
new Error('Recording not found in storage') as never
);
// Execute evaluateAndAbortStaleRecording and expect error to propagate
await expect(recordingService['evaluateAndAbortStaleRecording'](egressInfo)).rejects.toThrow(
'Recording not found in storage'
);
expect(recordingService.getRecording).toHaveBeenCalledWith(recordingId);
});
it('should handle errors during recording abort and rethrow them', async () => {
const roomId = testRooms.staleRecording;
const egressId = `EG_${roomId}`;
const recordingId = `${roomId}--${egressId}--1234567890`;
const staleUpdateTime = Date.now() - ms('10m'); // 10 minutes ago (stale)
const egressInfo = createMockEgressInfo(roomId, egressId, EgressStatus.EGRESS_ACTIVE, staleUpdateTime);
// Mock recording as active
const mockRecordingInfo = createMockRecordingInfo(recordingId, roomId, MeetRecordingStatus.ACTIVE);
(recordingService.getRecording as jest.Mock).mockResolvedValueOnce(mockRecordingInfo as never);
(livekitService.roomExists as jest.Mock).mockResolvedValueOnce(false as never);
(livekitService.stopEgress as jest.Mock).mockRejectedValueOnce(new Error('Failed to stop egress') as never);
// Execute evaluateAndAbortStaleRecording and expect error to propagate
await expect(recordingService['evaluateAndAbortStaleRecording'](egressInfo)).rejects.toThrow(
'Failed to stop egress'
);
expect(recordingService.getRecording).toHaveBeenCalledWith(recordingId);
expect(livekitService.roomExists).toHaveBeenCalledWith(roomId);
expect(livekitService.stopEgress).toHaveBeenCalledWith(egressId);
});
it('should handle case where updatedAt is in the future due to clock skew', async () => {
const roomId = testRooms.staleRecording;
const egressId = `EG_${roomId}`;
const recordingId = `${roomId}--${egressId}--1234567890`;
const staleUpdateTime = Date.now() + ms('10m'); // 10 minutes in the future (not stale)
const egressInfo = createMockEgressInfo(roomId, egressId, EgressStatus.EGRESS_ACTIVE, staleUpdateTime);
// Mock recording as active
const mockRecordingInfo = createMockRecordingInfo(recordingId, roomId, MeetRecordingStatus.ACTIVE);
(recordingService.getRecording as jest.Mock).mockResolvedValueOnce(mockRecordingInfo as never);
(livekitService.roomExists as jest.Mock).mockResolvedValueOnce(true as never);
(livekitService.getRoom as jest.Mock).mockResolvedValueOnce({
numParticipants: 0,
numPublishers: 0
} as Room as never);
// Execute evaluateAndAbortStaleRecording and expect it to resolve to false
await expect(recordingService['evaluateAndAbortStaleRecording'](egressInfo)).resolves.toBe(false);
expect(recordingService.getRecording).toHaveBeenCalledWith(recordingId);
expect(livekitService.roomExists).toHaveBeenCalledWith(roomId);
expect(livekitService.getRoom).not.toBeCalled();
});
});
});