matterbridge-roborock-vacuum-plugin
Version:
Matterbridge Roborock Vacuum Plugin
136 lines • 7.9 kB
JavaScript
import { RvcOperationalState, RvcRunMode } from 'matterbridge/matter/clusters';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { RunModeDisplayLabel, RunModeLabelInfo } from '../../../behaviors/roborock.vacuum/core/runModeConfig.js';
import { OperationStatusCode } from '../../../roborockCommunication/enums/index.js';
import { StatusChangeMessage } from '../../../roborockCommunication/models/index.js';
import { handleDeviceStatusSimpleUpdate, handleDeviceStatusUpdate, } from '../../../runtimes/handlers/deviceStateHandler.js';
import { asPartial, createMockConfigManager, createMockLogger } from '../../helpers/testUtils.js';
function createMockRobot() {
const cleaningMode = RunModeLabelInfo[RunModeDisplayLabel.Cleaning].mode;
return asPartial({
device: asPartial({ duid: 'test-duid', name: 'Test Robot' }),
dockStationStatus: undefined,
operationSessionStartMs: new Date('2026-07-05T12:00:00.000Z').getTime(),
operationPausedSinceMs: null,
operationPausedAccumMs: 0,
updateAttribute: vi.fn().mockResolvedValue(undefined),
triggerEvent: vi.fn().mockResolvedValue(true),
getAttribute: vi.fn((_clusterId, attribute) => {
if (attribute === 'currentMode') {
return cleaningMode;
}
if (attribute === 'operationalState') {
return RvcOperationalState.OperationalState.Running;
}
if (attribute === 'operationalError') {
return { errorStateId: RvcOperationalState.ErrorState.NoError };
}
return undefined;
}),
});
}
function createMockPlatform() {
return asPartial({
log: createMockLogger(),
configManager: createMockConfigManager({ includeDockStationStatus: false }),
});
}
describe('handleDeviceStatusUpdate operationCompletion integration', () => {
let robot;
let platform;
beforeEach(() => {
vi.clearAllMocks();
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-07-05T12:00:30.000Z'));
robot = createMockRobot();
platform = createMockPlatform();
});
it('should emit operationCompletion once when prior runMode was Cleaning and new status is Idle', async () => {
const message = new StatusChangeMessage('test-duid', OperationStatusCode.Idle, undefined, undefined, undefined, undefined, undefined, undefined);
await handleDeviceStatusUpdate(robot, message, platform);
expect(robot.triggerEvent).toHaveBeenCalledTimes(1);
expect(robot.triggerEvent).toHaveBeenCalledWith(RvcOperationalState, 'operationCompletion', expect.objectContaining({
completionErrorCode: RvcOperationalState.ErrorState.NoError,
totalOperationalTime: 30,
}), platform.log);
expect(robot.operationSessionStartMs).toBeNull();
});
});
describe('handleDeviceStatusSimpleUpdate with shared applyResolvedStateUpdates', () => {
let robot;
let platform;
beforeEach(() => {
vi.clearAllMocks();
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-07-05T12:00:30.000Z'));
robot = createMockRobot();
platform = createMockPlatform();
});
describe('basic path', () => {
it('should update currentMode and operationalState attributes for Idle status', async () => {
const message = { duid: 'test-duid', status: OperationStatusCode.Idle };
await handleDeviceStatusSimpleUpdate(robot, message, platform);
expect(robot.updateAttribute).toHaveBeenCalledTimes(2);
expect(robot.updateAttribute).toHaveBeenNthCalledWith(1, RvcRunMode.id, 'currentMode', expect.any(Number), platform.log);
expect(robot.updateAttribute).toHaveBeenNthCalledWith(2, RvcOperationalState.id, 'operationalState', RvcOperationalState.OperationalState.Docked, platform.log);
});
it('should update currentMode and operationalState attributes for Cleaning status', async () => {
const message = { duid: 'test-duid', status: OperationStatusCode.Cleaning };
await handleDeviceStatusSimpleUpdate(robot, message, platform);
expect(robot.updateAttribute).toHaveBeenCalledTimes(2);
expect(robot.updateAttribute).toHaveBeenNthCalledWith(1, RvcRunMode.id, 'currentMode', expect.any(Number), platform.log);
expect(robot.updateAttribute).toHaveBeenNthCalledWith(2, RvcOperationalState.id, 'operationalState', RvcOperationalState.OperationalState.Running, platform.log);
});
});
describe('operationalError path', () => {
it('should update operationalError attribute when status resolves to ChargingError', async () => {
const message = { duid: 'test-duid', status: OperationStatusCode.ChargingError };
await handleDeviceStatusSimpleUpdate(robot, message, platform);
expect(robot.updateAttribute).toHaveBeenCalledTimes(3);
expect(robot.updateAttribute).toHaveBeenNthCalledWith(1, RvcRunMode.id, 'currentMode', expect.any(Number), platform.log);
expect(robot.updateAttribute).toHaveBeenNthCalledWith(2, RvcOperationalState.id, 'operationalState', RvcOperationalState.OperationalState.Error, platform.log);
expect(robot.updateAttribute).toHaveBeenNthCalledWith(3, RvcOperationalState.id, 'operationalError', { errorStateId: RvcOperationalState.ErrorState.FailedToFindChargingDock }, platform.log);
});
});
describe('dock station error short-circuit', () => {
it('should call triggerDssError and not update attributes when dock station has error', async () => {
const mockDockStationStatus = asPartial({
hasError: vi.fn().mockReturnValue(true),
cleanFluidStatus: 0,
waterBoxFilterStatus: 0,
dustBagStatus: 0,
dirtyWaterBoxStatus: 0,
clearWaterBoxStatus: 0,
isUpdownWaterReady: 0,
});
robot.dockStationStatus = mockDockStationStatus;
const configWithDss = asPartial({
log: platform.log,
configManager: createMockConfigManager({ includeDockStationStatus: true }),
});
const message = { duid: 'test-duid', status: OperationStatusCode.Idle };
await handleDeviceStatusSimpleUpdate(robot, message, configWithDss);
expect(robot.updateAttribute).not.toHaveBeenCalled();
expect(robot.triggerEvent).not.toHaveBeenCalled();
});
});
describe('operation completion tracking', () => {
it('should track operation session timing and emit completion when transitioning from Cleaning to Idle', async () => {
const message = { duid: 'test-duid', status: OperationStatusCode.Idle };
await handleDeviceStatusSimpleUpdate(robot, message, platform);
expect(robot.triggerEvent).toHaveBeenCalledTimes(1);
expect(robot.triggerEvent).toHaveBeenCalledWith(RvcOperationalState, 'operationCompletion', expect.objectContaining({
completionErrorCode: RvcOperationalState.ErrorState.NoError,
totalOperationalTime: 30,
}), platform.log);
expect(robot.operationSessionStartMs).toBeNull();
});
it('should not emit operationCompletion when status remains Idle', async () => {
const message = { duid: 'test-duid', status: OperationStatusCode.Idle };
await handleDeviceStatusSimpleUpdate(robot, message, platform);
expect(robot.triggerEvent).toHaveBeenCalledTimes(1);
expect(robot.triggerEvent).toHaveBeenCalledWith(RvcOperationalState, 'operationCompletion', expect.anything(), platform.log);
});
});
});
//# sourceMappingURL=deviceStateHandler.test.js.map