import { describe, it, expect, vi, beforeEach } from 'vitest' // Mock logger vi.mock('@adonisjs/core/services/logger', () => ({ default: { info: vi.fn(), debug: vi.fn(), warn: vi.fn(), error: vi.fn() }, })) // Mock DeviceModel vi.mock('../../../../app/models/device.js', () => ({ default: { createOrUpdate: vi.fn().mockResolvedValue({}), findByDeviceId: vi.fn().mockResolvedValue(null), updateOne: vi.fn().mockResolvedValue({}), }, })) // Mock CommandModel vi.mock('../../../../app/models/command.js', () => ({ default: { getCollection: vi.fn().mockReturnValue({ find: vi.fn().mockReturnValue({ sort: vi.fn().mockReturnValue({ toArray: vi.fn().mockResolvedValue([]) }) }), countDocuments: vi.fn().mockResolvedValue(0), }), updateCommand: vi.fn().mockResolvedValue({}), }, })) // Mock reclassify vi.mock('../../../../app/services/parameter_writability.js', () => ({ reclassifyDeviceFromCatalog: vi.fn(), })) import { ExecutingCommandsState } from '../../../../app/services/cwmp/main_state_machine.js' import { Device } from '../../../../app/domain/device/device.js' import { DeviceId } from '../../../../app/domain/device/device_id.js' import { DeviceParameter } from '../../../../app/domain/device/device_parameter.js' import { CWMPTaskQueue } from '../../../../app/services/cwmp/task_queue.js' import { DeviceActionExecutor } from '../../../../app/services/cwmp/device_action_executor.js' import { CommandStatus, CommandType } from '../../../../app/domain/commands/command.js' import { ModeType } from '../../../../app/domain/shared/mode.js' import { ParameterType } from '../../../../app/domain/shared/types.js' import CommandModel from '../../../../app/models/command.js' import logger from '@adonisjs/core/services/logger' function makeDevice(): Device { const device = new Device(new DeviceId('Sagemcom', 'F8B7FF', 'Livebox 4', 'LB4-TEST-001')) device.setDataModel('tr-181') device.setGpnStatus('unsupported') return device } function makeContext(device: Device) { const taskQueue = new CWMPTaskQueue() return { device, session: 'test-session', taskQueue, message: undefined as any, services: { deviceAction: new DeviceActionExecutor(taskQueue) }, } } // Helper to inject mock commands into the ExecutingCommandsState flow function setupCommandMock(commands: any[]) { const collection = CommandModel.getCollection() let callCount = 0 vi.mocked(collection.find).mockImplementation(() => ({ sort: () => ({ toArray: () => { const result = callCount === 0 ? commands : [] callCount++ return Promise.resolve(result) } }) } as any)) } describe('ExecutingCommandsState — direct testing', () => { beforeEach(() => { vi.clearAllMocks() }) it('returns isComplete when no commands and includeNI=false', async () => { setupCommandMock([]) const device = makeDevice() const context = makeContext(device) const state = new ExecutingCommandsState(false) const result = await state.handle(context) expect(result.isComplete).toBe(true) }) it('transitions to OperationalState when no commands and includeNI=true', async () => { setupCommandMock([]) const device = makeDevice() const context = makeContext(device) const state = new ExecutingCommandsState(true) const result = await state.handle(context) expect(result.nextState).toBeDefined() expect((result.nextState as any).name).toBe('operational') }) it('sends GET_PARAMETER_VALUES command', async () => { const cmd = { _id: 'c1', action: CommandType.GET_PARAMETER_VALUES, parameters: ['Device.Info.SW'], status: CommandStatus.PENDING, mode: ModeType.I, priority: 1 } setupCommandMock([cmd]) const device = makeDevice() const context = makeContext(device) const state = new ExecutingCommandsState(true) const result = await state.handle(context) expect(result.response).toContain('GetParameterValues') expect(result.response).toContain('Device.Info.SW') }) it('sends SET_PARAMETER_VALUES command', async () => { const cmd = { _id: 'c2', action: CommandType.SET_PARAMETER_VALUES, parameters: [{ name: 'Device.X', value: '42', type: 'int' }], status: CommandStatus.PENDING, mode: ModeType.I, priority: 1 } setupCommandMock([cmd]) const device = makeDevice() const context = makeContext(device) const state = new ExecutingCommandsState(true) const result = await state.handle(context) expect(result.response).toContain('SetParameterValues') expect(result.response).toContain('Device.X') }) it('sends REBOOT command', async () => { const cmd = { _id: 'c3', action: CommandType.REBOOT, parameters: { commandKey: 'reboot-1' }, status: CommandStatus.PENDING, mode: ModeType.I, priority: 1 } setupCommandMock([cmd]) const device = makeDevice() const context = makeContext(device) const state = new ExecutingCommandsState(true) const result = await state.handle(context) expect(result.response).toContain('Reboot') }) it('sends DOWNLOAD command', async () => { const cmd = { _id: 'c4', action: CommandType.DOWNLOAD, parameters: { commandKey: 'fw', fileType: '1 Firmware', url: 'http://x.bin' }, status: CommandStatus.PENDING, mode: ModeType.I, priority: 1 } setupCommandMock([cmd]) const device = makeDevice() const context = makeContext(device) const state = new ExecutingCommandsState(true) const result = await state.handle(context) expect(result.response).toContain('Download') }) it('sends FACTORY_RESET command', async () => { const cmd = { _id: 'c5', action: CommandType.FACTORY_RESET, parameters: {}, status: CommandStatus.PENDING, mode: ModeType.I, priority: 1 } setupCommandMock([cmd]) const device = makeDevice() const context = makeContext(device) const state = new ExecutingCommandsState(true) const result = await state.handle(context) expect(result.response).toContain('FactoryReset') }) it('sends SCHEDULE_INFORM command', async () => { const cmd = { _id: 'c6', action: CommandType.SCHEDULE_INFORM, parameters: { delayInSeconds: 600 }, status: CommandStatus.PENDING, mode: ModeType.I, priority: 1 } setupCommandMock([cmd]) const device = makeDevice() const context = makeContext(device) const state = new ExecutingCommandsState(true) const result = await state.handle(context) expect(result.response).toContain('ScheduleInform') expect(result.response).toContain('600') }) it('sends REFRESH command (GPV on known params)', async () => { const cmd = { _id: 'c7', action: CommandType.REFRESH, parameters: {}, status: CommandStatus.PENDING, mode: ModeType.I, priority: 1 } setupCommandMock([cmd]) const device = makeDevice() device.setParameter(new DeviceParameter('Device.Info.SW', '5.0', ParameterType.STRING)) const context = makeContext(device) const state = new ExecutingCommandsState(true) const result = await state.handle(context) expect(result.response).toContain('GetParameterValues') expect(result.response).toContain('Device.Info.SW') }) it('handles unknown command type by marking as FAILED', async () => { const cmd = { _id: 'c8', action: 'INVALID_TYPE', parameters: {}, status: CommandStatus.PENDING, mode: ModeType.I, priority: 1 } setupCommandMock([cmd]) const device = makeDevice() const context = makeContext(device) const state = new ExecutingCommandsState(true) await state.handle(context) expect(vi.mocked(CommandModel.updateCommand)).toHaveBeenCalledWith( 'c8', expect.objectContaining({ status: CommandStatus.FAILED }) ) }) it('marks command COMPLETED on success response and does SPV optimistic update', async () => { const cmd = { _id: 'c9', action: CommandType.SET_PARAMETER_VALUES, parameters: [{ name: 'Device.X.Val', value: 'new' }], status: CommandStatus.PENDING, mode: ModeType.I, priority: 1 } setupCommandMock([cmd]) const device = makeDevice() const context = makeContext(device) const state = new ExecutingCommandsState(true) // First handle: loads commands, sends SPV await state.handle(context) // Second handle: receives success response context.message = { success: true, data: { Status: '0' } } const result = await state.handle(context) expect(vi.mocked(CommandModel.updateCommand)).toHaveBeenCalledWith('c9', expect.objectContaining({ status: CommandStatus.COMPLETED })) // Optimistic update const prop = device.getProperty('Device.X.Val') expect(prop).not.toBeNull() expect(prop!.value).toBe('new') // No more commands → transitions to operational expect(result.nextState).toBeDefined() expect((result.nextState as any).name).toBe('operational') }) it('marks command FAILED on fault response', async () => { const cmd = { _id: 'c10', action: CommandType.GET_PARAMETER_VALUES, parameters: ['Device.X'], status: CommandStatus.PENDING, mode: ModeType.I, priority: 1 } setupCommandMock([cmd]) const device = makeDevice() const context = makeContext(device) const state = new ExecutingCommandsState(true) // First handle: loads commands, sends GPV await state.handle(context) // Second handle: receives fault context.message = { success: false, data: { FaultCode: 9001 } } await state.handle(context) expect(vi.mocked(CommandModel.updateCommand)).toHaveBeenCalledWith('c10', expect.objectContaining({ status: CommandStatus.FAILED })) }) }) // GPN success flow is tested in main_state_machine_extra.spec.ts and gpn_circuit_breaker.spec.ts describe('ExecutingCommandsState — branch coverage', () => { beforeEach(() => { vi.clearAllMocks() }) it('handles multiple commands in queue sequentially', async () => { const cmd1 = { _id: 'c1', action: CommandType.GET_PARAMETER_VALUES, parameters: ['Device.A'], status: CommandStatus.PENDING, mode: ModeType.I, priority: 1 } const cmd2 = { _id: 'c2', action: CommandType.REBOOT, parameters: { commandKey: 'r' }, status: CommandStatus.PENDING, mode: ModeType.I, priority: 2 } setupCommandMock([cmd1, cmd2]) const device = makeDevice() const context = makeContext(device) const state = new ExecutingCommandsState(true) const r1 = await state.handle(context) expect(r1.response).toContain('GetParameterValues') context.message = { success: true, data: {} } const r2 = await state.handle(context) expect(r2.response).toContain('Reboot') context.message = { success: true, data: {} } const r3 = await state.handle(context) expect(r3.nextState).toBeDefined() expect((r3.nextState as any).name).toBe('operational') }) it('GET_PARAMETER_NAMES command with string params', async () => { setupCommandMock([{ _id: 'c3', action: CommandType.GET_PARAMETER_NAMES, parameters: 'Device.ManagementServer.', status: CommandStatus.PENDING, mode: ModeType.I, priority: 1 }]) const result = await new ExecutingCommandsState(true).handle(makeContext(makeDevice())) expect(result.response).toContain('GetParameterNames') }) it('GET_PARAMETER_NAMES command with object params', async () => { setupCommandMock([{ _id: 'c4', action: CommandType.GET_PARAMETER_NAMES, parameters: { path: 'Device.Info.' }, status: CommandStatus.PENDING, mode: ModeType.I, priority: 1 }]) const result = await new ExecutingCommandsState(true).handle(makeContext(makeDevice())) expect(result.response).toContain('GetParameterNames') }) it('SET_PARAMETER_VALUES with parameterValues property format', async () => { setupCommandMock([{ _id: 'c5', action: CommandType.SET_PARAMETER_VALUES, parameters: { parameterValues: [{ name: 'Device.X', value: '1' }] }, status: CommandStatus.PENDING, mode: ModeType.I, priority: 1 }]) const result = await new ExecutingCommandsState(true).handle(makeContext(makeDevice())) expect(result.response).toContain('SetParameterValues') }) it('SET_PARAMETER_ATTRIBUTES command', async () => { setupCommandMock([{ _id: 'c6', action: CommandType.SET_PARAMETER_ATTRIBUTES, parameters: [{ name: 'Device.X', notificationChange: true, notification: 2, accessListChange: false, accessList: [] }], status: CommandStatus.PENDING, mode: ModeType.I, priority: 1 }]) const result = await new ExecutingCommandsState(true).handle(makeContext(makeDevice())) expect(result.response).toContain('SetParameterAttributes') }) it('UPLOAD command', async () => { setupCommandMock([{ _id: 'c7', action: CommandType.UPLOAD, parameters: { commandKey: 'u1', fileType: '2', url: 'http://u.bin' }, status: CommandStatus.PENDING, mode: ModeType.I, priority: 1 }]) const result = await new ExecutingCommandsState(true).handle(makeContext(makeDevice())) expect(result.response).toContain('Upload') }) it('CommandModel.updateCommand error is caught', async () => { setupCommandMock([{ _id: 'c8', action: CommandType.GET_PARAMETER_VALUES, parameters: ['Device.X'], status: CommandStatus.PENDING, mode: ModeType.I, priority: 1 }]) const device = makeDevice() const context = makeContext(device) const state = new ExecutingCommandsState(true) await state.handle(context) vi.mocked(CommandModel.updateCommand).mockRejectedValueOnce(new Error('DB error')) context.message = { success: true, data: {} } await state.handle(context) expect(vi.mocked(logger.error)).toHaveBeenCalled() }) it('returns empty response for includeNI=false when all commands done', async () => { setupCommandMock([{ _id: 'c9', action: CommandType.REBOOT, parameters: {}, status: CommandStatus.PENDING, mode: ModeType.I, priority: 1 }]) const context = makeContext(makeDevice()) const state = new ExecutingCommandsState(false) await state.handle(context) context.message = { success: true, data: {} } const result = await state.handle(context) expect(result.response).toBe('') expect(result.nextState).toBeUndefined() }) })