diff --git a/QualityControl/common/library/enums/transition.enum.js b/QualityControl/common/library/enums/transition.enum.js index fe9b41897..ae12c4449 100644 --- a/QualityControl/common/library/enums/transition.enum.js +++ b/QualityControl/common/library/enums/transition.enum.js @@ -18,6 +18,20 @@ * @readonly */ export const Transition = Object.freeze({ + NULL: 'NULL', // custom QCG value for no transition START_ACTIVITY: 'START_ACTIVITY', STOP_ACTIVITY: 'STOP_ACTIVITY', }); + +/** + * Enumeration for different statuses of a transitions as per: + * @link https://github.com/AliceO2Group/Control/blob/master/common/protos/events.proto#L35 + */ +export const TransitionStatus = Object.freeze({ + NULL: 'NULL', + STARTED: 'STARTED', + ONGOING: 'ONGOING', + DONE_OK: 'DONE_OK', + DONE_ERROR: 'DONE_ERROR', + DONE_TIMEOUT: 'DONE_TIMEOUT', +}); diff --git a/QualityControl/lib/services/RunModeService.js b/QualityControl/lib/services/RunModeService.js index 1294289b2..4bceb6e6e 100644 --- a/QualityControl/lib/services/RunModeService.js +++ b/QualityControl/lib/services/RunModeService.js @@ -14,7 +14,7 @@ import { LogManager, WebSocketMessage } from '@aliceo2/web-ui'; import { EmitterKeys } from '../../common/library/enums/emitterKeys.enum.js'; -import { Transition } from '../../common/library/enums/transition.enum.js'; +import { Transition, TransitionStatus } from '../../common/library/enums/transition.enum.js'; import { RunStatus } from '../../common/library/runStatus.enum.js'; import { parseObjects } from '../../common/library/qcObject/utils.js'; import QCObjectDto from '../dtos/QCObjectDto.js'; @@ -138,10 +138,11 @@ export class RunModeService { * @param {object} runEvent - Object containing runNumber and transition type. * @param {number} runEvent.runNumber - The run number associated with the event. * @param {string} runEvent.transition - The transition type (e.g., 'START_ACTIVITY', 'STOP_ACTIVITY'). + * @param {string} runEvent.transitionStatus - The status of the transition (e.g., 'DONE_OK'). * @returns {Promise} */ - async _onRunTrackEvent({ runNumber, transition }) { - if (transition === Transition.START_ACTIVITY) { + async _onRunTrackEvent({ runNumber, transition = Transition.NULL, transitionStatus = TransitionStatus.NULL }) { + if (transition === Transition.START_ACTIVITY && transitionStatus === TransitionStatus.DONE_OK) { await this._initializeRunData(runNumber); const wsMessage = new WebSocketMessage(); diff --git a/QualityControl/lib/services/external/AliEcsSynchronizer.js b/QualityControl/lib/services/external/AliEcsSynchronizer.js index ed59a8ffd..1bd0391bf 100644 --- a/QualityControl/lib/services/external/AliEcsSynchronizer.js +++ b/QualityControl/lib/services/external/AliEcsSynchronizer.js @@ -18,6 +18,13 @@ import { ServiceStatus } from '../../../common/library/enums/Status/serviceStatu const LOG_FACILITY = `${process.env.npm_config_log_label ?? 'qcg'}/ecs-synchronizer`; const RUN_TOPICS = ['aliecs.run']; +/** + * @type {RunEvent} + * @property {number} runNumber - The run number associated with the event. + * @property {Transition} transition - The type of transition (e.g., START_ACTIVITY, END_ACTIVITY). + * @property {TransitionStatus} transitionStatus - The status of the transition (e.g., DONE_OK, DONE_ERROR). + */ + /** * Service for processing events sent via Kafka from AliECS with proto objects */ @@ -73,6 +80,9 @@ export class AliEcsSynchronizer { * @returns {void} */ async _onRunMessage(eventMessage) { + /** + * @param {RunEvent} - eventMessage - message received on run topic + */ const { runEvent, timestamp } = eventMessage; if (!runEvent) { this._logger.warnMessage('Received run message on run topic without runEvent field'); @@ -82,9 +92,10 @@ export class AliEcsSynchronizer { } else if (!runEvent.transition) { this._logger.warnMessage('Received run message on run topic without runEvent.transition field'); } else { - const { runNumber, transition } = runEvent; + const { runNumber, transition, transitionStatus } = runEvent; this._eventEmitter.emit(EmitterKeys.RUN_TRACK, { runNumber, + transitionStatus, transition, timestamp: timestamp.toNumber(), }); diff --git a/QualityControl/public/common/notifications/model/NotificationRunStartModel.js b/QualityControl/public/common/notifications/model/NotificationRunStartModel.js index 37e9ad8eb..d5e2a0776 100644 --- a/QualityControl/public/common/notifications/model/NotificationRunStartModel.js +++ b/QualityControl/public/common/notifications/model/NotificationRunStartModel.js @@ -34,7 +34,7 @@ export default class NotificationRunStartModel extends Observable { this.model.ws.addListener('command', (message) => { if (message.command === `${EmitterKeys.RUN_TRACK}:${Transition.START_ACTIVITY}`) { - this._handleWSRunTrack.bind(this, message.payload); + this._handleWSRunTrack(message.payload); } }); } @@ -82,21 +82,14 @@ export default class NotificationRunStartModel extends Observable { } showNativeBrowserNotification({ - title: `RUN ${runNumber ?? 'unknown'} has started`, + title: `RUN ${runNumber ?? 'unknown'} has started. Click here to enter RunMode`, onclick: () => { - // On notification click we always navigate to the `objectTree` page. - // Additionally, we view the run using the given `runNumber`. this.model.router.go(`?page=objectTree&RunNumber=${runNumber}`); - // If RunMode is not activated, we should enable it const { isRunModeActivated } = this.model.filterModel; if (!isRunModeActivated) { this.model.filterModel.activateRunsMode(this.model.filterModel.getPageTargetModel()); } - - // We select the given `runNumber` in RunMode. - // We do not have to set the parameter in the URL, as this is already achieved on navigation. - this.model.filterModel.setFilterValue('RunNumber', runNumber?.toString()); }, }); } diff --git a/QualityControl/test/lib/services/RunModeService.test.js b/QualityControl/test/lib/services/RunModeService.test.js index c28f81341..d28fa9baf 100644 --- a/QualityControl/test/lib/services/RunModeService.test.js +++ b/QualityControl/test/lib/services/RunModeService.test.js @@ -19,7 +19,7 @@ import sinon from 'sinon'; import { RunModeService } from '../../../lib/services/RunModeService.js'; import { RunStatus } from '../../../common/library/runStatus.enum.js'; import { EmitterKeys } from '../../../common/library/enums/emitterKeys.enum.js'; -import { Transition } from '../../../common/library/enums/transition.enum.js'; +import { Transition, TransitionStatus } from '../../../common/library/enums/transition.enum.js'; import { delayAndCheck } from '../../testUtils/delay.js'; import { WebSocketMessage } from '@aliceo2/web-ui'; @@ -152,7 +152,7 @@ export const runModeServiceTestSuite = async () => { suite('_onRunTrackEvent - test suite', () => { test('should correctly parse event to RUN_TRACK and update ongoing runs map', async () => { - const runEvent = { runNumber: 1234, transition: 'START_ACTIVITY' }; + const runEvent = { runNumber: 1234, transition: 'START_ACTIVITY', transitionStatus: TransitionStatus.DONE_OK }; runModeService._dataService.getObjectsLatestVersionList = sinon.stub().resolves([{ path: '/path/from/event' }]); await runModeService._onRunTrackEvent(runEvent); @@ -164,7 +164,9 @@ export const runModeServiceTestSuite = async () => { }); test('should listen to events on RUN_TRACK and update ongoing runs map', async () => { - const runEvent = { runNumber: 1234, transition: Transition.START_ACTIVITY }; + const runEvent = { + runNumber: 1234, transition: Transition.START_ACTIVITY, transitionStatus: TransitionStatus.DONE_OK, + }; runModeService._dataService.getObjectsLatestVersionList = sinon.stub().resolves([{ path: '/path/from/event' }]); eventEmitter.emit(EmitterKeys.RUN_TRACK, runEvent); @@ -177,7 +179,9 @@ export const runModeServiceTestSuite = async () => { test('should listen to events on RUN_TRACK and broadcast to websocket', async () => { const runNumber = 1234; - const runEvent = { runNumber, transition: Transition.START_ACTIVITY }; + const runEvent = { + runNumber, transition: Transition.START_ACTIVITY, transitionStatus: TransitionStatus.DONE_OK, + }; runModeService._dataService.getObjectsLatestVersionList = sinon.stub().resolves([{ path: '/path/from/event' }]); eventEmitter.emit(EmitterKeys.RUN_TRACK, runEvent); @@ -194,7 +198,9 @@ export const runModeServiceTestSuite = async () => { }); test('should remove run from ongoing runs map on STOP_ACTIVITY event', async () => { - const runEventStop = { runNumber: 5678, transition: Transition.STOP_ACTIVITY }; + const runEventStop = { + runNumber: 5678, transition: Transition.STOP_ACTIVITY, transitionStatus: TransitionStatus.DONE_OK, + }; runModeService._ongoingRuns.set(runEventStop.runNumber, [{ path: '/some/path' }]); eventEmitter.emit(EmitterKeys.RUN_TRACK, runEventStop); diff --git a/QualityControl/test/lib/services/external/AliEcsSynchronizer.test.js b/QualityControl/test/lib/services/external/AliEcsSynchronizer.test.js index 630925b67..a04ed782d 100644 --- a/QualityControl/test/lib/services/external/AliEcsSynchronizer.test.js +++ b/QualityControl/test/lib/services/external/AliEcsSynchronizer.test.js @@ -16,7 +16,7 @@ import { ok, deepStrictEqual } from 'node:assert'; import { test, beforeEach, afterEach } from 'node:test'; import { stub, restore } from 'sinon'; import { AliEcsSynchronizer } from '../../../../lib/services/external/AliEcsSynchronizer.js'; -import { Transition } from '../../../../common/library/enums/transition.enum.js'; +import { Transition, TransitionStatus } from '../../../../common/library/enums/transition.enum.js'; import { EmitterKeys } from '../../../../common/library/enums/emitterKeys.enum.js'; export const aliecsSynchronizerTestSuite = async () => { @@ -50,13 +50,14 @@ export const aliecsSynchronizerTestSuite = async () => { test('should emit a run track event when a valid run message is received', () => { const runNumber = 123; const transition = Transition.START_ACTIVITY; + const transitionStatus = TransitionStatus.DONE_OK; const fixedTimestamp = Date.now(); const timestamp = { toNumber: () => fixedTimestamp }; - aliecsSynchronizer._onRunMessage({ runEvent: { runNumber, transition }, timestamp }); + aliecsSynchronizer._onRunMessage({ runEvent: { runNumber, transition, transitionStatus }, timestamp }); ok(eventEmitterMock.emit.called); deepStrictEqual(eventEmitterMock.emit.firstCall.args[0], EmitterKeys.RUN_TRACK); deepStrictEqual(eventEmitterMock.emit.firstCall.args[1], { - runNumber, transition, timestamp: timestamp.toNumber(), + runNumber, transition, transitionStatus, timestamp: timestamp.toNumber(), }); }); }; diff --git a/QualityControl/test/public/components/profileHeader.test.js b/QualityControl/test/public/components/profileHeader.test.js index e269b9e94..d9fe1abea 100644 --- a/QualityControl/test/public/components/profileHeader.test.js +++ b/QualityControl/test/public/components/profileHeader.test.js @@ -19,6 +19,7 @@ import { getLocalStorageAsJson } from '../../testUtils/localStorage.js'; import { IntegratedServices } from '../../../common/library/enums/Status/integratedServices.enum.js'; import { ServiceStatus } from '../../../common/library/enums/Status/serviceStatus.enum.js'; import { integratedServiceInterceptor } from '../../testUtils/interceptors/integratedServiceInterceptor.js'; +import { ONGOING_RUN_NUMBER } from '../../setup/mockKafkaEvents.js'; /** * Performs a series of automated tests on the layoutList page using Puppeteer. @@ -199,8 +200,6 @@ export const profileHeaderTests = async (url, page, timeout = 1000, testParent) }); await testParent.test('should enable RunMode when browser notification is clicked', { timeout }, async () => { - const RUN_NUMBER = 1234; - /* * Kafka must be enabled for the browser notification feature to function correctly. * We intercept the request and return a SUCCESS state of the kafka service. @@ -261,6 +260,7 @@ export const profileHeaderTests = async (url, page, timeout = 1000, testParent) window.Notification = MockNotification; }); + const RUN_NUMBER = ONGOING_RUN_NUMBER; // Trigger native browser notification by simulating websocket message await page.evaluate( (wsMessage) => window.model.notificationRunStartModel._handleWSRunTrack(wsMessage), diff --git a/QualityControl/test/public/features/runMode.test.js b/QualityControl/test/public/features/runMode.test.js index 6bc71744e..9d8bada41 100644 --- a/QualityControl/test/public/features/runMode.test.js +++ b/QualityControl/test/public/features/runMode.test.js @@ -12,16 +12,17 @@ */ /* eslint-disable @stylistic/js/max-len */ -import { strictEqual, ok } from 'node:assert'; +import { strictEqual, ok, deepStrictEqual } from 'node:assert'; import { delay } from '../../testUtils/delay.js'; import { IntegratedServices } from '../../../common/library/enums/Status/integratedServices.enum.js'; import { ServiceStatus } from '../../../common/library/enums/Status/serviceStatus.enum.js'; import { integratedServiceInterceptor } from '../../testUtils/interceptors/integratedServiceInterceptor.js'; +import { ONGOING_RUN_NUMBER } from '../../setup/mockKafkaEvents.js'; // If using nock for HTTP mocking (uncomment if available) // import nock from 'nock'; export const runModeTests = async (url, page, timeout = 5000, testParent) => { - const mockedTestRunNumber = 500001; + const mockedTestRunNumber = ONGOING_RUN_NUMBER; let countOngoingRunsCalls = 0; let countRunStatusCalls = 0; let expectCountRunStatusCalls = 0; @@ -185,10 +186,7 @@ export const runModeTests = async (url, page, timeout = 5000, testParent) => { .filter((value) => value !== ''); }); - ok(availableOptions.length > 0, 'Should have ongoing runs available in selector'); - ['500001', '500002', '500003'].forEach((run) => { - ok(availableOptions.includes(run), `Should include mock run ${run}`); - }); + deepStrictEqual(availableOptions, ['500001', '500002', '500003'], 'Ongoing runs selector should have correct options'); }); await testParent.test('should automatically select first run and update URL', { timeout }, async () => { diff --git a/QualityControl/test/setup/mockKafkaEvents.js b/QualityControl/test/setup/mockKafkaEvents.js index f656bbaf0..9a979f7ad 100644 --- a/QualityControl/test/setup/mockKafkaEvents.js +++ b/QualityControl/test/setup/mockKafkaEvents.js @@ -13,26 +13,25 @@ */ import { EmitterKeys } from '../../common/library/enums/emitterKeys.enum.js'; -import { Transition } from '../../common/library/enums/transition.enum.js'; +import { Transition, TransitionStatus } from '../../common/library/enums/transition.enum.js'; + +export const ONGOING_RUN_NUMBER = 500001; +export const ONGOING_RUNS_LIST = [ONGOING_RUN_NUMBER, 500002, 500003]; /** * Mock Kafka events for testing purposes * @param {EventEmitter} eventEmitter - Event emitter to emit mock events */ export const setupMockKafkaEvents = (eventEmitter) => { - // Simulate some ongoing runs being started - const mockOngoingRuns = ['500001', '500002', '500003']; - // Emit START_ACTIVITY events for mock runs after a short delay setTimeout(() => { - mockOngoingRuns.forEach((runNumber) => { + ONGOING_RUNS_LIST.forEach((runNumber) => { eventEmitter.emit(EmitterKeys.RUN_TRACK, { runNumber: parseInt(runNumber, 10), transition: Transition.START_ACTIVITY, + transitionStatus: TransitionStatus.DONE_OK, timestamp: Date.now(), }); }); - }, 100); - - return mockOngoingRuns; + }, 200); }; diff --git a/QualityControl/test/setup/testSetupForBkp.js b/QualityControl/test/setup/testSetupForBkp.js index e6caee2ee..41fa94835 100644 --- a/QualityControl/test/setup/testSetupForBkp.js +++ b/QualityControl/test/setup/testSetupForBkp.js @@ -16,6 +16,7 @@ import nock from 'nock'; import { config } from '../config.js'; import { BKP_MOCK_DATA } from './seeders/bkp-mock-data.js'; import { GET_BKP_GUI_STATUS_PATH } from '../../lib/services/BookkeepingService.js'; +import { ONGOING_RUN_NUMBER } from './mockKafkaEvents.js'; const BKP_URL = `${config.bookkeeping.url}`; const TOKEN_PATH = `?token=${config.bookkeeping.token}`; @@ -160,25 +161,43 @@ export const initializeNockForBkp = () => { }, }); nock(BKP_URL) - .get(`/api/runs/500001${TOKEN_PATH}`) + .get(`/api/runs/${ONGOING_RUN_NUMBER}${TOKEN_PATH}`) .reply(200, { data: { timeO2End: null, }, }) - .get(`/api/runs/500001${TOKEN_PATH}`) + .get(`/api/runs/${ONGOING_RUN_NUMBER}${TOKEN_PATH}`) .reply(200, { data: { timeO2End: null, }, }) - .get(`/api/runs/500001${TOKEN_PATH}`) + .get(`/api/runs/${ONGOING_RUN_NUMBER}${TOKEN_PATH}`) + .reply(200, { + data: { + timeO2End: null, + }, + }) + .get(`/api/runs/${ONGOING_RUN_NUMBER}${TOKEN_PATH}`) + .reply(200, { + data: { + timeO2End: null, + }, + }) + .get(`/api/runs/${ONGOING_RUN_NUMBER}${TOKEN_PATH}`) + .reply(200, { + data: { + timeO2End: null, + }, + }) + .get(`/api/runs/${ONGOING_RUN_NUMBER}${TOKEN_PATH}`) .reply(200, { data: { timeO2End: '2023-12-01T10:30:00Z', }, }) - .get(`/api/runs/500001${TOKEN_PATH}`) + .get(`/api/runs/${ONGOING_RUN_NUMBER}${TOKEN_PATH}`) .reply(200, { data: { timeO2End: '2023-12-01T10:30:00Z',