fix
This commit is contained in:
1 parent
08c620ee68
commit
dae82eeff2
5 files changed
+114
-11
No files matched your search
@@ -131,7 +131,20 @@ export class ConfigurationEvaluationState {
|
||||
const response = await context.services.deviceAction.getParameterValues(
|
||||
context.device,
|
||||
leaves,
|
||||
() => Promise.resolve('')
|
||||
async (result) => {
|
||||
if (result.status === 'success' && result.data) {
|
||||
const list = result.data?.ParameterList?.ParameterValueStruct
|
||||
if (Array.isArray(list) && list.length > 0) {
|
||||
const normalized = list.map((pv: any) => ({
|
||||
...pv,
|
||||
Name: typeof pv?.Name === 'string' ? pv.Name : pv?.Name?._ || pv?.Name,
|
||||
}))
|
||||
context.device.updateFromValues(normalized)
|
||||
logger.debug({ identifier: context.device.identifier, count: normalized.length }, '[ConfigEval] Inventory GPV values saved to device')
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
)
|
||||
return { response }
|
||||
}
|
||||
|
||||
@@ -343,8 +343,13 @@ export class StateMachineManager extends EventEmitter {
|
||||
return ''
|
||||
}
|
||||
|
||||
// If state machine produced a response, send it
|
||||
// If state machine produced a response, send it.
|
||||
// Also track the task ID that was just created (by addTask inside a state handler)
|
||||
// so that handleCWMPResponse can execute its callback when the CPE replies.
|
||||
if (response && response !== '') {
|
||||
if (!machineInstance.lastTaskId) {
|
||||
machineInstance.lastTaskId = this.taskQueue.getLastCreatedTaskId(machineInstance.deviceId)
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
@@ -409,6 +414,12 @@ export class StateMachineManager extends EventEmitter {
|
||||
return ''
|
||||
}
|
||||
|
||||
// If the state machine produced a new request (e.g. next inventory GPN, next SPV),
|
||||
// track the task ID so the next handleCWMPResponse can fire its callback.
|
||||
if (response && response !== '' && !machineInstance.lastTaskId) {
|
||||
machineInstance.lastTaskId = this.taskQueue.getLastCreatedTaskId(machineInstance.deviceId)
|
||||
}
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
logger.error(`Error handling CWMP response: ${error}`)
|
||||
|
||||
@@ -86,6 +86,8 @@ export class CWMPTaskQueue extends EventEmitter {
|
||||
private tasks: Map<string, CWMPTask> = new Map()
|
||||
private deviceQueues: Map<string, string[]> = new Map()
|
||||
private taskCallbacks: Map<string, (result: TaskResult) => Promise<string>> = new Map()
|
||||
/** Tracks the most recently created task per device (set by addTask) */
|
||||
private lastCreatedTaskIdByDevice: Map<string, string> = new Map()
|
||||
|
||||
/**
|
||||
* Adds a task with callback (legacy-compatible API used by DeviceActionExecutor)
|
||||
@@ -108,6 +110,9 @@ export class CWMPTaskQueue extends EventEmitter {
|
||||
cwmpTask.status = TaskStatus.SENT
|
||||
}
|
||||
|
||||
// Track the most recent task created for this device so callers can retrieve its ID
|
||||
this.lastCreatedTaskIdByDevice.set(JSON.stringify(deviceId), cwmpTask.id)
|
||||
|
||||
// Generate CWMP envelope
|
||||
const builder = CWMPTaskQueue.task_map[taskType]
|
||||
if (typeof builder !== 'function') {
|
||||
@@ -217,20 +222,28 @@ export class CWMPTaskQueue extends EventEmitter {
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ID of the most recently created task for a device (via addTask).
|
||||
* Used by the state machine manager to correlate responses with in-flight tasks
|
||||
* that were created directly by state handlers (not served from the external queue).
|
||||
*/
|
||||
getLastCreatedTaskId(deviceId: DeviceId): string | undefined {
|
||||
return this.lastCreatedTaskIdByDevice.get(JSON.stringify(deviceId))
|
||||
}
|
||||
|
||||
clearPendingTasksForDevice(deviceId: DeviceId): void {
|
||||
const deviceKey = JSON.stringify(deviceId)
|
||||
const queue = this.deviceQueues.get(deviceKey) || []
|
||||
const keep: string[] = []
|
||||
// Clear ALL tasks (PENDING and SENT) — after session completion, no task
|
||||
// from the previous session should bleed into the next one. Tasks that were
|
||||
// already SENT (created by state-machine states via addTask()) are ghost
|
||||
// entries that would otherwise be served to the next session.
|
||||
for (const taskId of queue) {
|
||||
const t = this.tasks.get(taskId)
|
||||
if (t && t.status === TaskStatus.SENT) {
|
||||
keep.push(taskId)
|
||||
continue
|
||||
}
|
||||
this.tasks.delete(taskId)
|
||||
this.taskCallbacks.delete(taskId)
|
||||
}
|
||||
this.deviceQueues.set(deviceKey, keep)
|
||||
this.deviceQueues.set(deviceKey, [])
|
||||
this.lastCreatedTaskIdByDevice.delete(deviceKey)
|
||||
}
|
||||
|
||||
private generateTaskId(deviceId: DeviceId, type: string): string {
|
||||
|
||||
@@ -502,6 +502,69 @@ describe('FirmwareUpgradeState — download callback', () => {
|
||||
})
|
||||
|
||||
|
||||
describe('ConfigurationEvaluationState — inventory phase 2 GPV saves values on device', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('GPV callback in phase 2 calls updateFromValues so parameters are persisted', async () => {
|
||||
const config = makeConfig({
|
||||
desiredParameters: [{ name: 'Device.X', value: '1', type: 'string' }],
|
||||
inventory: [{ path: 'Device.ManagementServer.' }],
|
||||
})
|
||||
vi.mocked(ConfigurationModel.find).mockResolvedValue([config])
|
||||
|
||||
const device = makeDevice()
|
||||
device.setGpnStatus('supported')
|
||||
const context = makeContext(device)
|
||||
|
||||
const state = new ConfigurationEvaluationState()
|
||||
|
||||
// Step 1: first handle → sends GPN for inventory
|
||||
await state.handle(context as any)
|
||||
|
||||
// Step 2: execute GPN callback with two leaf names
|
||||
const taskCallbacks = (context.taskQueue as any).taskCallbacks as Map<string, any>
|
||||
const gpnTaskId = Array.from(taskCallbacks.keys())[0]
|
||||
await context.taskQueue.executeTaskCallback(gpnTaskId!, ResultStatus.Success, {
|
||||
ParameterList: {
|
||||
ParameterInfoStruct: [
|
||||
{ Name: 'Device.ManagementServer.PeriodicInformInterval', Writable: '1' },
|
||||
{ Name: 'Device.ManagementServer.ConnectionRequestURL', Writable: '0' },
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
// Step 3: second handle → sends GPV for discovered leaves (phase 2)
|
||||
const result2 = await state.handle(context as any)
|
||||
expect(result2.response).toContain('GetParameterValues')
|
||||
|
||||
// Before GPV callback — device has no value for PeriodicInformInterval
|
||||
const before = device.getParameters().find((p) => p.name === 'Device.ManagementServer.PeriodicInformInterval')
|
||||
expect(before?.value ?? null).toBeNull()
|
||||
|
||||
// Step 4: execute GPV callback with actual values
|
||||
const gpvTaskId = Array.from(taskCallbacks.keys()).pop()
|
||||
await context.taskQueue.executeTaskCallback(gpvTaskId!, ResultStatus.Success, {
|
||||
ParameterList: {
|
||||
ParameterValueStruct: [
|
||||
{ Name: 'Device.ManagementServer.PeriodicInformInterval', Value: { _: '3600', $: { 'xsi:type': 'xsd:unsignedInt' } } },
|
||||
{ Name: 'Device.ManagementServer.ConnectionRequestURL', Value: { _: 'http://cpe/cr', $: { 'xsi:type': 'xsd:string' } } },
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
// After GPV callback — device must have the values saved
|
||||
const after = device.getParameters().find((p) => p.name === 'Device.ManagementServer.PeriodicInformInterval')
|
||||
expect(after).toBeDefined()
|
||||
expect(after!.value).toBe('3600')
|
||||
|
||||
const afterURL = device.getParameters().find((p) => p.name === 'Device.ManagementServer.ConnectionRequestURL')
|
||||
expect(afterURL).toBeDefined()
|
||||
expect(afterURL!.value).toBe('http://cpe/cr')
|
||||
})
|
||||
})
|
||||
|
||||
describe('ConfigurationEvaluationState — firmware upgrade transition (lines 154-158)', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
@@ -204,13 +204,16 @@ describe('CWMPTaskQueue — branch coverage', () => {
|
||||
const tq = new CWMPTaskQueue()
|
||||
const deviceId = new DeviceId('X', 'Y', 'Z', 'S')
|
||||
|
||||
tq.addCWMPTask(deviceId, 'get_parameter_values', ['a']) // PENDING
|
||||
const pendingTask = tq.addCWMPTask(deviceId, 'get_parameter_values', ['a']) // PENDING
|
||||
const { taskId: sentId } = tq.addTask(deviceId, 'set_parameter_values', {}, async () => '') // SENT
|
||||
|
||||
tq.clearPendingTasksForDevice(deviceId)
|
||||
|
||||
// clearPendingTasksForDevice clears ALL tasks (PENDING and SENT) to avoid
|
||||
// ghost entries bleeding into the next session
|
||||
const tasks = (tq as any).tasks as Map<string, any>
|
||||
expect(tasks.has(sentId)).toBe(true)
|
||||
expect(tasks.has(pendingTask.id)).toBe(false)
|
||||
expect(tasks.has(sentId)).toBe(false)
|
||||
})
|
||||
|
||||
it('registerCallback adds callback for existing task', () => {
|
||||
|
||||
Reference in new issue
Block a user