Add two-session scenario test demonstrating the full flow. Fix OperationalState to also skip reclassify when already classified. Session 1: GPN test → classify → critical reads → inventory GPN→GPV→SPV → ProvCode. Session 2: skip GPN + skip all (1 GPV only). All 811 tests pass. Build status: Success

This commit is contained in:
ATX Bot committed 2026-06-08 17:30:21 +02:00
1 parent ffbed94dcc
commit 25d69bd03f
2 files changed
+269 -2

No files matched your search

+4 -2
View File
@@ -457,8 +457,10 @@ export class OperationalState extends DeviceState {
async handle(context: DeviceContext): Promise<DeviceStateResult> {
const { device } = context
// Consolidated reclassification (Phase 11)
reclassifyDeviceFromCatalog(device)
// Consolidated reclassification — only if not already classified
if (device.gpnStatus !== 'supported' && device.getParameters().length === 0) {
reclassifyDeviceFromCatalog(device)
}
// Persist device with operational status
device.operationalStatus = 'operational'
@@ -0,0 +1,265 @@
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 ConfigurationModel
vi.mock('#models/configuration', () => ({
default: {
find: vi.fn().mockResolvedValue([{
identifier: 'BM632w-standard',
name: 'BM632w Standard',
version: '1.0.1',
priority: 1,
policy: 'maintain',
enabled: true,
criteria: { manufacturer: 'Huawei*', productClass: 'BM632w' },
inventory: [{ path: 'InternetGatewayDevice.ManagementServer.' }],
desiredParameters: [
{ name: 'InternetGatewayDevice.ManagementServer.PeriodicInformInterval', value: '300', type: 'unsignedInt' },
],
firmwareTarget: null,
schedule: null,
tags: [],
}]),
findOne: vi.fn().mockResolvedValue(null),
},
}))
// Mock FirmwareCatalogService
vi.mock('../../../../app/services/firmware_catalog_service.js', () => ({
default: class { async resolveUpgradePath() { return [] } },
FirmwareCatalogService: class { async resolveUpgradePath() { return [] } },
}))
// Mock schedule_window
vi.mock('../../../../app/utils/schedule_window.js', () => ({
isWithinSchedule: vi.fn().mockReturnValue(true),
}))
// Mock reclassify
vi.mock('../../../../app/services/parameter_writability.js', () => ({
reclassifyDeviceFromCatalog: vi.fn(),
}))
import { MainStateMachine } 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, DeviceProperty } from '../../../../app/domain/device/device_parameter.js'
import { CWMPTaskQueue } from '../../../../app/services/cwmp/task_queue.js'
import { ParameterType } from '../../../../app/domain/shared/types.js'
import { reclassifyDeviceFromCatalog } from '../../../../app/services/parameter_writability.js'
import { buildProvisioningCode } from '../../../../app/utils/provisioning_code.js'
import logger from '@adonisjs/core/services/logger'
/**
* Simulates two full Inform sessions from the same BM632w device.
*
* Session 1: Device is NEW (not in DB)
* → GPN test on Inform paths → classify → Critical reads → ConfigEval (GPN+GPV+SPV) → ProvCode → Done
*
* Session 2: Device is KNOWN (already classified, already compliant)
* → Skip GPN (already classified) → Critical reads → ConfigEval (skip) → Done
*/
describe('Two-session scenario: same device, first contact then reconnect', () => {
let device: Device
beforeEach(() => {
vi.clearAllMocks()
})
function makeNewDevice(): Device {
const d = new Device(new DeviceId('Huawei Technologies Co., Ltd.', '202BC1', 'BM632w', '000000'))
d.setDataModel('tr-098')
// Simulate Inform parameters (stored by handleInform before state machine starts)
d.updateFromValues([
{ Name: 'InternetGatewayDevice.DeviceInfo.HardwareVersion', Value: { _: '40501' } },
{ Name: 'InternetGatewayDevice.DeviceInfo.SoftwareVersion', Value: { _: 'V100R001' } },
{ Name: 'InternetGatewayDevice.ManagementServer.ConnectionRequestURL', Value: { _: 'http://172.20.0.5:37547/' } },
{ Name: 'InternetGatewayDevice.ManagementServer.PeriodicInformInterval', Value: { _: '60' } },
])
return d
}
it('SESSION 1: new device — full GPN + classify + critical reads + config eval + SPV + provCode', async () => {
device = makeNewDevice()
// gpnStatus = 'unknown', no parameters (only properties from Inform) → GPN test will run
expect(device.gpnStatus).toBe('unknown')
expect(device.getParameters().length).toBe(0) // all stored as properties initially
const taskQueue = new CWMPTaskQueue()
const sm = new MainStateMachine(device, 'session-1', taskQueue)
console.log('\n=== SESSION 1: New device (first contact) ===')
// Step 1: start → IdleState extracts Inform paths → sends GPN
const step1 = await sm.start()
console.log(`1. [${sm.getCurrentStateName()}] → GPN sent`)
expect(step1).toContain('GetParameterNames')
expect(sm.getCurrentStateName()).toBe('discovering-parameters')
// Step 2: First GPN response (DeviceInfo.) → still discovering (ManagementServer. pending)
await sm.handleResponse(true, {
ParameterList: {
ParameterInfoStruct: [
{ Name: 'InternetGatewayDevice.DeviceInfo.HardwareVersion', Writable: '0' },
{ Name: 'InternetGatewayDevice.DeviceInfo.SoftwareVersion', Writable: '0' },
],
},
})
console.log(`2. [${sm.getCurrentStateName()}] → First GPN response (DeviceInfo.), next GPN pending`)
expect(sm.getCurrentStateName()).toBe('discovering-parameters')
// Step 3: Second GPN response (ManagementServer.) → classify → transition to critical-reads → GPV
const step3 = await sm.handleResponse(true, {
ParameterList: {
ParameterInfoStruct: [
{ Name: 'InternetGatewayDevice.ManagementServer.ConnectionRequestURL', Writable: '0' },
{ Name: 'InternetGatewayDevice.ManagementServer.PeriodicInformInterval', Writable: '1' },
],
},
})
console.log(`3. [${sm.getCurrentStateName()}] → Second GPN response (ManagementServer.), GPV (critical reads) sent`)
expect(device.gpnStatus).toBe('supported')
expect(step3).toContain('GetParameterValues')
expect(sm.getCurrentStateName()).toBe('critical-reads')
// Step 4: Critical reads GPV response → update device → transition to config-eval
const step4 = await sm.handleResponse(true, {
ParameterList: {
ParameterValueStruct: [
{ Name: 'InternetGatewayDevice.DeviceInfo.HardwareVersion', Value: { _: '40501' } },
{ Name: 'InternetGatewayDevice.DeviceInfo.SoftwareVersion', Value: { _: 'V100R001' } },
{ Name: 'InternetGatewayDevice.DeviceInfo.ModelName', Value: { _: 'BM632w' } },
{ Name: 'InternetGatewayDevice.ManagementServer.ConnectionRequestURL', Value: { _: 'http://172.20.0.5:37547/' } },
],
},
})
console.log(`4. [${sm.getCurrentStateName()}] → Critical reads processed, ConfigEval started`)
expect(sm.getCurrentStateName()).toBe('configuration-evaluation')
// Step 5: ConfigEval sends inventory GPN
expect(step4).toContain('GetParameterNames')
console.log(`5. [${sm.getCurrentStateName()}] → Inventory GPN sent`)
// Step 6: Inventory GPN response → discover leaves → sends GPV
const step6 = await sm.handleResponse(true, {
ParameterList: {
ParameterInfoStruct: [
{ Name: 'InternetGatewayDevice.ManagementServer.PeriodicInformInterval', Writable: '1' },
{ Name: 'InternetGatewayDevice.ManagementServer.ConnectionRequestURL', Writable: '0' },
],
},
})
console.log(`6. [${sm.getCurrentStateName()}] → Inventory GPN response → sends GPV for leaves`)
expect(step6).toContain('GetParameterValues')
// Step 7: Inventory GPV response → update values → sends SPV for diff
const step7 = await sm.handleResponse(true, {
ParameterList: {
ParameterValueStruct: [
{ Name: 'InternetGatewayDevice.ManagementServer.PeriodicInformInterval', Value: { _: '60' } },
{ Name: 'InternetGatewayDevice.ManagementServer.ConnectionRequestURL', Value: { _: 'http://172.20.0.5:37547/' } },
],
},
})
console.log(`7. [${sm.getCurrentStateName()}] → GPV values saved → SPV sent (PeriodicInformInterval 60→300)`)
expect(step7).toContain('SetParameterValues')
expect(step7).toContain('300')
// Step 8: SPV response → transition to compliance-update → sends ProvisioningCode
const step8 = await sm.handleResponse(true, { Status: '0' })
console.log(`8. [${sm.getCurrentStateName()}] → SPV success → ProvisioningCode sent`)
expect(step8).toContain('SetParameterValues')
expect(step8).toContain('ProvisioningCode')
// Step 9: ProvCode response → executing-commands → operational → done
const step9 = await sm.handleResponse(true, { Status: '0' })
console.log(`9. [${sm.getCurrentStateName()}] → ProvCode done → session complete`)
expect(sm.isCompleted()).toBe(true)
expect(sm.getCurrentStateName()).toBe('operational')
console.log('\n✅ Session 1 complete: device classified + configured')
console.log(` gpnStatus: ${device.gpnStatus}`)
console.log(` parameters: ${device.getParameters().length}`)
console.log(` properties: ${device.getProperties().length}`)
})
it('SESSION 2: known device (already classified + compliant) — minimal session', async () => {
// Simulate a device loaded from DB after session 1
device = makeNewDevice()
device.setGpnStatus('supported')
// Simulate that organizeParameters was called in session 1: writable params are parameters
device.setParameter(new DeviceParameter(
'InternetGatewayDevice.ManagementServer.PeriodicInformInterval', '300', ParameterType.UNSIGNED_INT
))
// ProvisioningCode already set from session 1
const config = { identifier: 'BM632w-standard', version: '1.0.1', desiredParameters: [{ name: 'InternetGatewayDevice.ManagementServer.PeriodicInformInterval', value: '300', type: 'unsignedInt' }], firmwareTarget: null }
const provCode = buildProvisioningCode(config as any)
device.setProperty(new DeviceProperty('InternetGatewayDevice.DeviceInfo.ProvisioningCode', provCode, ParameterType.STRING))
expect(device.gpnStatus).toBe('supported')
expect(device.getParameters().length).toBeGreaterThan(0)
const taskQueue = new CWMPTaskQueue()
const sm = new MainStateMachine(device, 'session-2', taskQueue)
console.log('\n=== SESSION 2: Known device (already classified + compliant) ===')
// Step 1: start → IdleState sees gpnStatus=supported + has parameters → SKIP GPN → critical reads GPV
const step1 = await sm.start()
console.log(`1. [${sm.getCurrentStateName()}] → GPN SKIPPED (already classified) → GPV (critical reads) sent`)
expect(step1).toContain('GetParameterValues')
expect(step1).not.toContain('GetParameterNames')
expect(sm.getCurrentStateName()).toBe('critical-reads')
// Step 2: Critical reads GPV response → config eval
const step2 = await sm.handleResponse(true, {
ParameterList: {
ParameterValueStruct: [
{ Name: 'InternetGatewayDevice.DeviceInfo.HardwareVersion', Value: { _: '40501' } },
{ Name: 'InternetGatewayDevice.DeviceInfo.SoftwareVersion', Value: { _: 'V100R001' } },
{ Name: 'InternetGatewayDevice.DeviceInfo.ModelName', Value: { _: 'BM632w' } },
{ Name: 'InternetGatewayDevice.ManagementServer.ConnectionRequestURL', Value: { _: 'http://172.20.0.5:37547/' } },
],
},
})
console.log(`2. [${sm.getCurrentStateName()}] → Critical reads done → ConfigEval: already compliant, no SPV needed`)
// ConfigEval should find isAlreadyCompliant=true (value matches, provCode matches)
// → skips inventory, skips SPV, skips provCode → goes to operational
expect(sm.isCompleted()).toBe(true)
expect(sm.getCurrentStateName()).toBe('operational')
console.log(`3. [${sm.getCurrentStateName()}] → Session complete (no GPN, no SPV, no ProvCode)`)
// Verify reclassifyDeviceFromCatalog was NOT called (device already classified)
expect(vi.mocked(reclassifyDeviceFromCatalog)).not.toHaveBeenCalled()
console.log('\n✅ Session 2 complete: minimal session (GPV only)')
console.log(` Total RPCs: 1 (GetParameterValues for critical reads)`)
console.log(` Skipped: GPN test, inventory, SPV, ProvisioningCode`)
})
})