Files
adam-acs/tests/unit/services/cwmp/stale_session.spec.ts
T
Kevin L'HereecandContinue aad93911e4 feat: TR-069 CWMP state machine hardening + exchange viewer improvements
- TR-069 CWMP state machine hardening (14 phases): shared event utility,
  multi-event routing, GPN circuit breaker, IdleState GPN guard, expanded
  GPN fault codes, CriticalReadsState, ConfigEval GPN guard, task queue
  race condition fix, explicit error handling, terminal states
  (OperationalState/PartialOperationalState), consolidated reclassify,
  crash pattern detection, removed enqueueImmediate, state flow wiring
- Fix SOAP Fault not processed by state machine (soap-env:Fault detection)
- Fix GPN circuit breaker not persisting after session drops
- Fix CriticalReadsState GPV response parsing (xsi:type normalization)
- Fix CriticalReadsState not processing GPV response values
- Skip SPV when device is already compliant (ProvisioningCode check)
- Add Device.DeviceInfo.ModelName to critical reads GPV
- Fix TypeError on DeviceDetail parameters view (null guards)
- Log empty 204 responses in exchange trace UI
- TR-181 catalog: type corrections and new entries (+6640 lines)
- SequenceViewer: replace session-nav buttons with compact dropdown select
- SequenceViewer: fix CPE/ACS lifeline headers wrapping to 2 lines
- Add 20 unit tests for state machine fixes (SOAP Fault, GPN circuit
  breaker, CriticalReads, stale session, ConfigEval/ComplianceUpdate skips)

Generated with [Continue](https://continue.dev)

Co-Authored-By: Continue <noreply@continue.dev>
2026-06-08 12:49:00 +00:00

101 lines
3.4 KiB
TypeScript

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({}),
},
}))
import { StateMachineManager, MachineStatus } from '../../../../app/services/cwmp/state_machine_manager.js'
import { Device } from '../../../../app/domain/device/device.js'
import { DeviceId } from '../../../../app/domain/device/device_id.js'
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
}
describe('StateMachineManager — stale session replacement', () => {
let manager: StateMachineManager
beforeEach(() => {
vi.clearAllMocks()
// Reset singleton
;(StateMachineManager as any).instance = null
manager = StateMachineManager.getInstance()
})
it('replaces stale MainStateMachine when session differs', async () => {
const device = makeDevice()
// Start first machine on session-A
const machine1 = await manager.startStateMachine(device, 'session-A', { events: ['0 BOOTSTRAP'] })
expect(machine1.status).toBe(MachineStatus.RUNNING)
expect(machine1.session).toBe('session-A')
// Start second machine on session-B (different session, same device)
const machine2 = await manager.startStateMachine(device, 'session-B', { events: ['2 PERIODIC'] })
// Should be a NEW machine (not the old one rebinding)
expect(machine2.session).toBe('session-B')
// Old machine should be completed
expect(machine1.status).toBe(MachineStatus.COMPLETED)
})
it('does NOT replace machine when session is same', async () => {
const device = makeDevice()
// Start machine on session-A
const machine1 = await manager.startStateMachine(device, 'session-A', { events: ['0 BOOTSTRAP'] })
// Start again on SAME session
const machine2 = await manager.startStateMachine(device, 'session-A', { events: ['2 PERIODIC'] })
// Should return the same machine (rebinding)
expect(machine2.id).toBe(machine1.id)
expect(machine1.status).toBe(MachineStatus.RUNNING)
})
it('creates fresh machine after replacing stale one', async () => {
const device = makeDevice()
// Start first machine
await manager.startStateMachine(device, 'session-old', { events: ['0 BOOTSTRAP'] })
// Start new machine with different session
const newMachine = await manager.startStateMachine(device, 'session-new', { events: ['2 PERIODIC'] })
// Fresh machine should be running
expect(newMachine.status).toBe(MachineStatus.RUNNING)
expect(newMachine.session).toBe('session-new')
// Should be a MainStateMachine (PERIODIC event triggers MainStateMachine)
expect(newMachine.currentState).toBeDefined()
})
})