Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 20 additions & 32 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

38 changes: 38 additions & 0 deletions src/models/RCS.Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,49 @@ export interface RemoteConfig {
AMTDomains: AMTDomain[]
}

/**
* Per-component activation outcome. Additive structured detail that rides alongside the
* legacy flat status strings in the WebSocket `message` field (see issue #2665). Older
* clients that do not understand `Components` simply ignore it.
*/
export type ComponentResultStatus = 'Success' | 'Failure' | 'NotApplicable'

/** Clean machine-readable mode enum for a component result. Prose belongs in Details. */
export type ComponentMode = 'ACM' | 'CCM' | 'LocalProfileSync'

export interface ComponentResult {
Result: ComponentResultStatus
Mode?: ComponentMode
/** Human-readable, sentence-case note (no trailing period). On failure this carries the reason. */
Details?: string
}

export interface ComponentResults {
Activation?: ComponentResult
WiredNetwork?: ComponentResult
WirelessNetwork?: ComponentResult
TLS?: ComponentResult
CIRAProxy?: ComponentResult
CIRAConnection?: ComponentResult
}

/**
* Schema version for the structured `Components` block (issue #2665). Bump when the set of
* reported components changes (e.g. when 802.1x reporting lands) so clients can reason about coverage.
*/
export const COMPONENTS_VERSION = 1

export interface Status {
Status?: string
Network?: string
CIRAConnection?: string
TLSConfiguration?: string
// Additive structured per-component results (issue #2665). Legacy flat fields above are
// always populated for backwards compatibility; the fields below carry the granular breakdown.
// Result is the overall roll-up; ComponentsVersion marks the Components schema version.
Result?: ComponentResultStatus
ComponentsVersion?: number
Components?: ComponentResults
}
export interface ClientObject {
ClientId: string
Expand Down
16 changes: 16 additions & 0 deletions src/stateMachines/activation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ vi.mock('./common.js', async () => {
invokeWsmanCall: invokeWsmanCallSpy,
invokeEnterpriseAssistantCall: vi.fn(),
processTLSTunnelResponse: vi.fn(),
recordComponentResult: actual.recordComponentResult,
deriveControlMode: actual.deriveControlMode,
finalizeComponentResults: actual.finalizeComponentResults,
HttpResponseError,
isDigestRealmValid,
coalesceMessage
Expand Down Expand Up @@ -828,6 +831,11 @@ describe('Activation State Machine', () => {
devices[context.clientId].status.Status = 'Admin control mode.'
activation.setActivationStatus({ context })
expect(devices[clientId].activationStatus).toBeTruthy()
expect(devices[clientId].status.Components?.Activation).toEqual({
Result: 'Success',
Mode: 'ACM',
Details: 'Admin control mode'
})
})
})

Expand Down Expand Up @@ -2081,8 +2089,13 @@ describe('Activation State Machine', () => {
}))

it('should send success message to device', () => {
context.status = 'success'
devices[clientId].status = { Status: 'Admin control mode.' }
activation.sendMessageToDevice({ context })
expect(sendSpy).toHaveBeenCalled()
expect(devices[clientId].status.Result).toEqual('Success')
expect(devices[clientId].status.ComponentsVersion).toEqual(1)
expect(devices[clientId].status.Components?.Activation?.Result).toEqual('Success')
})
it('should update Credentials', () => {
activation.updateCredentials({ context })
Expand All @@ -2092,6 +2105,8 @@ describe('Activation State Machine', () => {
it('should send error message to device', () => {
context.status = 'error'
context.message = null
// Device never reached an activated control mode, so Activation is recorded as a Failure.
devices[clientId].activationStatus = false
activation.sendMessageToDevice({ context })
expect(responseMessageSpy).toHaveBeenCalledWith(
context.clientId,
Expand All @@ -2100,6 +2115,7 @@ describe('Activation State Machine', () => {
'failed',
JSON.stringify(devices[clientId].status)
)
expect(devices[clientId].status.Components?.Activation?.Result).toEqual('Failure')
expect(sendSpy).toHaveBeenCalled()
})
})
Expand Down
24 changes: 23 additions & 1 deletion src/stateMachines/activation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,14 @@ import { DbCreatorFactory } from '../factories/DbCreatorFactory.js'
import { AMTUserName, GATEWAY_TIMEOUT_ERROR, asArray } from '../utils/constants.js'
import { CIRAConfiguration } from './ciraConfiguration.js'
import { TLS } from './tls.js'
import { type CommonContext, invokeWsmanCall, processTLSTunnelResponse } from './common.js'
import {
type CommonContext,
invokeWsmanCall,
processTLSTunnelResponse,
recordComponentResult,
deriveControlMode,
finalizeComponentResults
} from './common.js'
import ClientResponseMsg from '../utils/ClientResponseMsg.js'
import { Unconfiguration } from './unconfiguration.js'
import { type DeviceCredentials } from '../interfaces/ISecretManagerService.js'
Expand Down Expand Up @@ -139,7 +146,17 @@ export class Activation {
} else {
clientObj.status.Status = context.errorMessage !== '' ? context.errorMessage : 'Failed'
method = 'failed'
// Record the Activation component as a failure when the device never reached an
// activated control mode (issue #2665). If it activated but a later component failed,
// 'Set activation status' already recorded Activation as a success — don't overwrite it.
if (clientObj.activationStatus !== true) {
recordComponentResult(clientId, 'Activation', {
Result: 'Failure',
Details: clientObj.status.Status
})
}
Comment thread
madhavilosetty-intel marked this conversation as resolved.
}
finalizeComponentResults(clientId, status === 'success')
const responseMessage = ClientResponseMsg.get(clientId, null, status, method, JSON.stringify(clientObj.status))
this.logger.info(JSON.stringify(responseMessage, null, '\t'))
devices[clientId].ClientSocket?.send(JSON.stringify(responseMessage))
Expand Down Expand Up @@ -426,6 +443,11 @@ export class Activation {
const clientObj = devices[context.clientId]
this.logger.debug(`Device ${clientObj.uuid} activated in ${clientObj.status.Status}.`)
clientObj.activationStatus = true
recordComponentResult(context.clientId, 'Activation', {
Result: 'Success',
Mode: deriveControlMode(clientObj.status.Status),
Details: clientObj.status.Status
})
MqttProvider.publishEvent(
'success',
['Activator', 'execute'],
Expand Down
6 changes: 6 additions & 0 deletions src/stateMachines/ciraConfiguration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ vi.mock('./common.js', async () => {
invokeWsmanCall: invokeWsmanCallSpy,
invokeEnterpriseAssistantCall: vi.fn(),
processTLSTunnelResponse: vi.fn(),
recordComponentResult: actual.recordComponentResult,
HttpResponseError,
isDigestRealmValid,
coalesceMessage
Expand Down Expand Up @@ -183,6 +184,10 @@ describe('CIRA Configuration State Machine', () => {
if (state.matches('SUCCESS') || state.matches('FAILURE') || currentStateIndex === flowStates.length) {
const status = devices[clientId].status.CIRAConnection
expect(status).toEqual('Configured')
expect(devices[clientId].status.Components?.CIRAConnection).toEqual({
Result: 'Success',
Details: 'Configured'
})
resolve()
}
}
Expand Down Expand Up @@ -212,6 +217,7 @@ describe('CIRA Configuration State Machine', () => {
try {
if (state.matches('SUCCESS') || state.matches('FAILURE')) {
expect(state.matches('FAILURE')).toBeTruthy()
expect(devices[clientId].status.Components?.CIRAConnection?.Result).toEqual('Failure')
resolve()
}
} catch (err) {
Expand Down
7 changes: 6 additions & 1 deletion src/stateMachines/ciraConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { type DeviceCredentials } from '../interfaces/ISecretManagerService.js'
import { type AMTConfiguration } from '../models/index.js'
import { randomUUID } from 'node:crypto'
import { Error } from './error.js'
import { type CommonContext, invokeWsmanCall } from './common.js'
import { type CommonContext, invokeWsmanCall, recordComponentResult } from './common.js'

export interface CIRAConfigContext extends CommonContext {
status: 'success' | 'error' | 'wsman' | 'heartbeat_request'
Expand Down Expand Up @@ -315,6 +315,11 @@ export class CIRAConfiguration {
actions: {
'Update Configuration Status': ({ context, event }) => {
devices[context.clientId].status.CIRAConnection = context.statusMessage
const success = context.statusMessage === 'Configured'
recordComponentResult(context.clientId, 'CIRAConnection', {
Result: success ? 'Success' : 'Failure',
Details: context.statusMessage
})
Comment thread
madhavilosetty-intel marked this conversation as resolved.
},
'Reset Unauth Count': ({ context, event }) => {
devices[context.clientId].unauthCount = 0
Expand Down
Loading