From 110f5587a9cdf4a71b8050cb899ff639a2b24c47 Mon Sep 17 00:00:00 2001 From: hehoon <100522372+hehoon@users.noreply.github.com> Date: Sat, 10 Jan 2026 14:38:05 +0100 Subject: [PATCH 1/4] At server start fetch a list of detectors from Bookkeeping --- .../lib/services/BookkeepingService.js | 45 ++++++++- .../lib/services/BookkeepingService.test.js | 93 +++++++++++++++++++ QualityControl/test/setup/testSetupForBkp.js | 42 +++++++++ 3 files changed, 179 insertions(+), 1 deletion(-) diff --git a/QualityControl/lib/services/BookkeepingService.js b/QualityControl/lib/services/BookkeepingService.js index f5d9f6353..59820b7f1 100644 --- a/QualityControl/lib/services/BookkeepingService.js +++ b/QualityControl/lib/services/BookkeepingService.js @@ -20,9 +20,16 @@ import { wrapRunStatus } from '../dtos/BookkeepingDto.js'; const GET_BKP_DATABASE_STATUS_PATH = '/api/status/database'; const GET_RUN_TYPES_PATH = '/api/runTypes'; const GET_RUN_PATH = '/api/runs'; +const GET_DETECTORS_PATH = '/api/detectors'; const LOG_FACILITY = `${process.env.npm_config_log_label ?? 'qcg'}/bkp-service`; +/** + * @typedef {object} DetectorSummary + * @property {string} name - Human-readable detector name. + * @property {string} type - Detector type identifier. + */ + /** * BookkeepingService class to be used to retrieve data from Bookkeeping */ @@ -38,6 +45,9 @@ export class BookkeepingService { this._protocol = ''; this._logger = LogManager.getLogger(LOG_FACILITY); + + /** @type {Readonly} */ + this._detectors = Object.freeze([]); } /** @@ -77,7 +87,14 @@ export class BookkeepingService { return; } this.active = await this.simulateConnection(); - if (!this.active) { + if (this.active) { + try { + const detectorSummaries = await this._retrieveDetectorSummaries(); + this._detectors = Object.freeze(detectorSummaries.map((detector) => Object.freeze({ ...detector }))); + } catch (error) { + this._logger.errorMessage(`Failed to retrieve detectors: ${error?.message || error}`); + } + } else { this._logger.infoMessage(`Bookkeeping service will not be used. Reason: ${this.error}`); } } @@ -181,6 +198,32 @@ export class BookkeepingService { } } + /** + * Retrieves the information about the detectors from the Bookkeeping service. + * @private + * @returns {Promise} Array of detector summaries. + */ + async _retrieveDetectorSummaries() { + const { data } = await httpGetJson( + this._hostname, + this._port, + this._createPath(GET_DETECTORS_PATH), + { + protocol: this._protocol, + rejectUnauthorized: false, + }, + ); + return Array.isArray(data) ? data.map(({ name, type }) => ({ name, type })) : []; + } + + /** + * Returns a list of detector summaries. + * @returns {Readonly} An immutable array of detector summaries. + */ + get detectors() { + return this._detectors; + } + /** * Helper method to construct a URL path with the required authentication token. * Appends the service's token as a query parameter to the provided path. diff --git a/QualityControl/test/lib/services/BookkeepingService.test.js b/QualityControl/test/lib/services/BookkeepingService.test.js index 9b1c0de5e..a5c316858 100644 --- a/QualityControl/test/lib/services/BookkeepingService.test.js +++ b/QualityControl/test/lib/services/BookkeepingService.test.js @@ -44,6 +44,8 @@ export const bookkeepingServiceTestSuite = async () => { strictEqual(bookkeepingService._port, null); strictEqual(bookkeepingService._token, ''); strictEqual(bookkeepingService._protocol, ''); + deepStrictEqual(bookkeepingService._detectors, []); + ok(Object.isFrozen(bookkeepingService._detectors)); }); }); @@ -135,6 +137,22 @@ export const bookkeepingServiceTestSuite = async () => { ok(service.error.includes('Error trying to connect to Bookkeeping')); ok(service.error.includes('simulated failure')); }); + + test('should call _retrieveDetectorSummaries and set _detectors if active is true', async () => { + const DETECTOR_SUMMARIES = [ + { + name: 'Detector human-readable name', + type: 'Detector type identifier', + }, + ]; + + stub(service, 'simulateConnection').resolves(true); + simulateStub = stub(service, '_retrieveDetectorSummaries').resolves(DETECTOR_SUMMARIES); + await service.connect(); + ok(simulateStub.calledOnce); + deepStrictEqual(service._detectors, DETECTOR_SUMMARIES); + ok(Object.isFrozen(service._detectors)); + }); }); suite('simulateConnection', () => { let service = null; @@ -288,6 +306,81 @@ export const bookkeepingServiceTestSuite = async () => { ok(Object.values(RunStatus).includes(runStatus)); }); + suite('Retrieve detector summaries', () => { + const GET_DETECTORS_PATH = '/api/detectors'; + + let bkpService = null; + + beforeEach(() => { + bkpService = new BookkeepingService(VALID_CONFIG.bookkeeping); + bkpService.validateConfig(); // ensures internal fields like _hostname/_port/_token are set + bkpService.connect(); + }); + + afterEach(() => { + nock.cleanAll(); + }); + + test('should handle all detector types correctly', async () => { + const mockResponse = { + data: [ + { id: 1, name: 'ACO', type: 'PHYSICAL', createdAt: 1765468282000, updatedAt: 1765468282000 }, + { id: 2, name: 'EVS', type: 'AOT-EVENT', createdAt: 1765468282000, updatedAt: 1765468282000 }, + { id: 3, name: 'GLO', type: 'QC', createdAt: 1765468282000, updatedAt: 1765468282000 }, + { id: 4, name: 'MUD', type: 'MUON-GLO', createdAt: 1765468282000, updatedAt: 1765468282000 }, + { id: 5, name: 'VTX', type: 'AOT-GLO', createdAt: 1765468282000, updatedAt: 1765468282000 }, + { id: 6, name: 'TST', type: 'VIRTUAL', createdAt: 1765468282000, updatedAt: 1765468282000 }, + ], + }; + + nock(VALID_CONFIG.bookkeeping.url) + .get(GET_DETECTORS_PATH) + .query({ token: VALID_CONFIG.bookkeeping.token }) + .reply(200, mockResponse); + + const result = await bkpService._retrieveDetectorSummaries(); + + ok(Array.isArray(result)); + strictEqual(result.length, mockResponse.data.length); + + // Verify each detector is preserved + const detectorSummaries = result.map(({ name, type }) => ({ name, type })); + deepStrictEqual(result, detectorSummaries); + }); + + test('should return empty array when data is not an array', async () => { + const mockResponse = { + data: null, + }; + + nock(VALID_CONFIG.bookkeeping.url) + .get(GET_DETECTORS_PATH) + .query({ token: VALID_CONFIG.bookkeeping.token }) + .reply(200, mockResponse); + + const result = await bkpService._retrieveDetectorSummaries(); + + ok(Array.isArray(result)); + strictEqual(result.length, 0); + }); + + test('should return empty array when data is empty array', async () => { + const mockResponse = { + data: [], + }; + + nock(VALID_CONFIG.bookkeeping.url) + .get(GET_DETECTORS_PATH) + .query({ token: VALID_CONFIG.bookkeeping.token }) + .reply(200, mockResponse); + + const result = await bkpService._retrieveDetectorSummaries(); + + ok(Array.isArray(result)); + strictEqual(result.length, 0); + }); + }); + test('should return ONGOING status when timeO2End is not present', async () => { const mockResponse = { data: { timeO2End: undefined } }; diff --git a/QualityControl/test/setup/testSetupForBkp.js b/QualityControl/test/setup/testSetupForBkp.js index a9c4c8a2c..c1c061d12 100644 --- a/QualityControl/test/setup/testSetupForBkp.js +++ b/QualityControl/test/setup/testSetupForBkp.js @@ -42,6 +42,48 @@ export const initializeNockForBkp = () => { }, }, }); + nock(BKP_URL) + .persist() + .get(`/api/detectors${TOKEN_PATH}`) + .reply(200, { + data: [ + { + id: 17, + name: 'ACO', + type: 'PHYSICAL', + createdAt: 1765468282000, + updatedAt: 1765468282000, + }, + { + id: 1, + name: 'CPV', + type: 'PHYSICAL', + createdAt: 1765468282000, + updatedAt: 1765468282000, + }, + { + id: 23, + name: 'EVS', + type: 'AOT-EVENT', + createdAt: 1765468282000, + updatedAt: 1765468282000, + }, + { + id: 21, + name: 'GLO', + type: 'QC', + createdAt: 1765468282000, + updatedAt: 1765468282000, + }, + { + id: 15, + name: 'TST', + type: 'VIRTUAL', + createdAt: 1765468282000, + updatedAt: 1765468282000, + }, + ], + }); nock(BKP_URL) .persist() .get(`/api/runs/0${TOKEN_PATH}`) From 91fc672a0f408be6531f110abce3596d5d252623 Mon Sep 17 00:00:00 2001 From: hehoon <100522372+hehoon@users.noreply.github.com> Date: Sat, 10 Jan 2026 16:54:51 +0100 Subject: [PATCH 2/4] Move the list of detectors from the `BookkeepingService` to the `FilterService` --- .../common/library/typedef/DetectorSummary.js | 18 ++++++++++++ .../lib/controllers/FilterController.js | 7 ++--- .../lib/services/BookkeepingService.js | 29 ++----------------- QualityControl/lib/services/FilterService.js | 24 ++++++++++++++- .../lib/controllers/FiltersController.test.js | 20 +++++++++---- .../lib/services/BookkeepingService.test.js | 24 ++------------- .../test/lib/services/FilterService.test.js | 27 ++++++++++++++++- 7 files changed, 90 insertions(+), 59 deletions(-) create mode 100644 QualityControl/common/library/typedef/DetectorSummary.js diff --git a/QualityControl/common/library/typedef/DetectorSummary.js b/QualityControl/common/library/typedef/DetectorSummary.js new file mode 100644 index 000000000..481c887ea --- /dev/null +++ b/QualityControl/common/library/typedef/DetectorSummary.js @@ -0,0 +1,18 @@ +/** + * @license + * Copyright CERN and copyright holders of ALICE O2. This software is + * distributed under the terms of the GNU General Public License v3 (GPL + * Version 3), copied verbatim in the file "COPYING". + * + * See http://alice-o2.web.cern.ch/license for full licensing information. + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +/** + * @typedef {object} DetectorSummary + * @property {string} name - Human-readable detector name. + * @property {string} type - Detector type identifier. + */ diff --git a/QualityControl/lib/controllers/FilterController.js b/QualityControl/lib/controllers/FilterController.js index 0fcd51908..476da42d3 100644 --- a/QualityControl/lib/controllers/FilterController.js +++ b/QualityControl/lib/controllers/FilterController.js @@ -63,12 +63,11 @@ export class FilterController { */ async getFilterConfigurationHandler(req, res) { try { - let runTypes = []; - if (this._filterService) { - runTypes = await this._filterService.runTypes; - } + const runTypes = this._filterService?.runTypes ?? []; + const detectors = this._filterService?.detectors ?? []; res.status(200).json({ runTypes, + detectors, }); } catch (error) { res.status(503).json({ error: error.message || error }); diff --git a/QualityControl/lib/services/BookkeepingService.js b/QualityControl/lib/services/BookkeepingService.js index 59820b7f1..919e7877f 100644 --- a/QualityControl/lib/services/BookkeepingService.js +++ b/QualityControl/lib/services/BookkeepingService.js @@ -24,12 +24,6 @@ const GET_DETECTORS_PATH = '/api/detectors'; const LOG_FACILITY = `${process.env.npm_config_log_label ?? 'qcg'}/bkp-service`; -/** - * @typedef {object} DetectorSummary - * @property {string} name - Human-readable detector name. - * @property {string} type - Detector type identifier. - */ - /** * BookkeepingService class to be used to retrieve data from Bookkeeping */ @@ -45,9 +39,6 @@ export class BookkeepingService { this._protocol = ''; this._logger = LogManager.getLogger(LOG_FACILITY); - - /** @type {Readonly} */ - this._detectors = Object.freeze([]); } /** @@ -87,14 +78,7 @@ export class BookkeepingService { return; } this.active = await this.simulateConnection(); - if (this.active) { - try { - const detectorSummaries = await this._retrieveDetectorSummaries(); - this._detectors = Object.freeze(detectorSummaries.map((detector) => Object.freeze({ ...detector }))); - } catch (error) { - this._logger.errorMessage(`Failed to retrieve detectors: ${error?.message || error}`); - } - } else { + if (!this.active) { this._logger.infoMessage(`Bookkeeping service will not be used. Reason: ${this.error}`); } } @@ -200,10 +184,9 @@ export class BookkeepingService { /** * Retrieves the information about the detectors from the Bookkeeping service. - * @private * @returns {Promise} Array of detector summaries. */ - async _retrieveDetectorSummaries() { + async retrieveDetectorSummaries() { const { data } = await httpGetJson( this._hostname, this._port, @@ -216,14 +199,6 @@ export class BookkeepingService { return Array.isArray(data) ? data.map(({ name, type }) => ({ name, type })) : []; } - /** - * Returns a list of detector summaries. - * @returns {Readonly} An immutable array of detector summaries. - */ - get detectors() { - return this._detectors; - } - /** * Helper method to construct a URL path with the required authentication token. * Appends the service's token as a query parameter to the provided path. diff --git a/QualityControl/lib/services/FilterService.js b/QualityControl/lib/services/FilterService.js index d6da52c57..b964c90f6 100644 --- a/QualityControl/lib/services/FilterService.js +++ b/QualityControl/lib/services/FilterService.js @@ -31,7 +31,7 @@ export class FilterService { this._logger = LogManager.getLogger(LOG_FACILITY); this._bookkeepingService = bookkeepingService; this._runTypes = []; - this.initFilters(); + this._detectors = Object.freeze([]); this._runTypesRefreshInterval = config?.bookkeeping?.runTypesRefreshInterval ?? (config?.bookkeeping ? 24 * 60 * 60 * 1000 : -1); @@ -48,6 +48,7 @@ export class FilterService { async initFilters() { await this._bookkeepingService.connect(); await this.getRunTypes(); + await this._initializeDetectors(); } /** @@ -71,6 +72,27 @@ export class FilterService { } } + /** + * This method is used to retrieve the list of detectors from the bookkeeping service + * @returns {Promise} Resolves when the list of detectors is available + */ + async _initializeDetectors() { + try { + const detectorSummaries = await this._bookkeepingService.retrieveDetectorSummaries(); + this._detectors = Object.freeze(detectorSummaries.map((detector) => Object.freeze({ ...detector }))); + } catch (error) { + this._logger.errorMessage(`Failed to retrieve detectors: ${error?.message || error}`); + } + } + + /** + * Returns a list of detector summaries. + * @returns {Readonly} An immutable array of detector summaries. + */ + get detectors() { + return this._detectors; + } + /** * Returns the interval in milliseconds for how often the list of run types should be refreshed. * @returns {number} Interval in milliseconds for refreshing the list of run types. diff --git a/QualityControl/test/lib/controllers/FiltersController.test.js b/QualityControl/test/lib/controllers/FiltersController.test.js index a5cdd026e..eaf513114 100644 --- a/QualityControl/test/lib/controllers/FiltersController.test.js +++ b/QualityControl/test/lib/controllers/FiltersController.test.js @@ -38,10 +38,17 @@ export const filtersControllerTestSuite = async () => { }); suite('getFilterConfigurationHandler', async () => { - test('should successfully retrieve run types from Bookkeeping service', async () => { + test('should successfully retrieve run types and detectors from Bookkeeping service', async () => { const filterService = sinon.createStubInstance(FilterService); const mockedRunTypes = ['runType1', 'runType2']; + const mockedDetectors = [ + { + name: 'Detector human-readable name', + type: 'Detector type identifier', + }, + ]; sinon.stub(filterService, 'runTypes').get(() => mockedRunTypes); + sinon.stub(filterService, 'detectors').get(() => mockedDetectors); const res = { status: sinon.stub().returnsThis(), @@ -51,9 +58,12 @@ export const filtersControllerTestSuite = async () => { const filterController = new FilterController(filterService); await filterController.getFilterConfigurationHandler(req, res); ok(res.status.calledWith(200), 'Response status was not 200'); - ok(res.json.calledWith({ runTypes: mockedRunTypes }), 'Run types were not sent back'); + ok( + res.json.calledWith({ runTypes: mockedRunTypes, detectors: mockedDetectors }), + 'Response should include runTypes and detectors', + ); }); - test('should return an empty array if bookkeeping service is not defined', async () => { + test('should return an empty arrays if bookkeeping service is not defined', async () => { const bkpService = null; const res = { status: sinon.stub().returnsThis(), @@ -64,8 +74,8 @@ export const filtersControllerTestSuite = async () => { await filterController.getFilterConfigurationHandler(req, res); ok(res.status.calledWith(200), 'Response status was not 200'); ok( - res.json.calledWith({ runTypes: [] }), - 'Run types were not sent as an empty array', + res.json.calledWith({ runTypes: [], detectors: [] }), + 'runTypes and detectors were not sent as an empty array', ); }); }); diff --git a/QualityControl/test/lib/services/BookkeepingService.test.js b/QualityControl/test/lib/services/BookkeepingService.test.js index a5c316858..251c9a6a5 100644 --- a/QualityControl/test/lib/services/BookkeepingService.test.js +++ b/QualityControl/test/lib/services/BookkeepingService.test.js @@ -44,8 +44,6 @@ export const bookkeepingServiceTestSuite = async () => { strictEqual(bookkeepingService._port, null); strictEqual(bookkeepingService._token, ''); strictEqual(bookkeepingService._protocol, ''); - deepStrictEqual(bookkeepingService._detectors, []); - ok(Object.isFrozen(bookkeepingService._detectors)); }); }); @@ -137,22 +135,6 @@ export const bookkeepingServiceTestSuite = async () => { ok(service.error.includes('Error trying to connect to Bookkeeping')); ok(service.error.includes('simulated failure')); }); - - test('should call _retrieveDetectorSummaries and set _detectors if active is true', async () => { - const DETECTOR_SUMMARIES = [ - { - name: 'Detector human-readable name', - type: 'Detector type identifier', - }, - ]; - - stub(service, 'simulateConnection').resolves(true); - simulateStub = stub(service, '_retrieveDetectorSummaries').resolves(DETECTOR_SUMMARIES); - await service.connect(); - ok(simulateStub.calledOnce); - deepStrictEqual(service._detectors, DETECTOR_SUMMARIES); - ok(Object.isFrozen(service._detectors)); - }); }); suite('simulateConnection', () => { let service = null; @@ -338,7 +320,7 @@ export const bookkeepingServiceTestSuite = async () => { .query({ token: VALID_CONFIG.bookkeeping.token }) .reply(200, mockResponse); - const result = await bkpService._retrieveDetectorSummaries(); + const result = await bkpService.retrieveDetectorSummaries(); ok(Array.isArray(result)); strictEqual(result.length, mockResponse.data.length); @@ -358,7 +340,7 @@ export const bookkeepingServiceTestSuite = async () => { .query({ token: VALID_CONFIG.bookkeeping.token }) .reply(200, mockResponse); - const result = await bkpService._retrieveDetectorSummaries(); + const result = await bkpService.retrieveDetectorSummaries(); ok(Array.isArray(result)); strictEqual(result.length, 0); @@ -374,7 +356,7 @@ export const bookkeepingServiceTestSuite = async () => { .query({ token: VALID_CONFIG.bookkeeping.token }) .reply(200, mockResponse); - const result = await bkpService._retrieveDetectorSummaries(); + const result = await bkpService.retrieveDetectorSummaries(); ok(Array.isArray(result)); strictEqual(result.length, 0); diff --git a/QualityControl/test/lib/services/FilterService.test.js b/QualityControl/test/lib/services/FilterService.test.js index 248671f04..2f865e69d 100644 --- a/QualityControl/test/lib/services/FilterService.test.js +++ b/QualityControl/test/lib/services/FilterService.test.js @@ -12,7 +12,7 @@ * or submit itself to any jurisdiction. */ -import { deepStrictEqual } from 'node:assert'; +import { deepStrictEqual, ok } from 'node:assert'; import { suite, test, beforeEach, afterEach } from 'node:test'; import { FilterService } from '../../../lib/services/FilterService.js'; import { RunStatus } from '../../../common/library/runStatus.enum.js'; @@ -32,6 +32,7 @@ export const filterServiceTestSuite = async () => { connect: stub(), retrieveRunTypes: stub(), retrieveRunInformation: stub(), + retrieveDetectorSummaries: stub(), active: true, // assume the bookkeeping service is active by default }; filterService = new FilterService(bookkeepingServiceMock, configMock); @@ -63,6 +64,11 @@ export const filterServiceTestSuite = async () => { deepStrictEqual(filterServiceWithCustomConfig._runTypesRefreshInterval, 5000); }); + test('should init _detectors on instantiation', async () => { + deepStrictEqual(filterService._detectors, []); + ok(Object.isFrozen(filterService._detectors)); + }); + test('should init filters on instantiation', async () => { const initFiltersStub = stub(filterService, 'initFilters'); await filterService.initFilters(); @@ -71,7 +77,26 @@ export const filterServiceTestSuite = async () => { }); suite('initFilters', async () => { + test('should call _initializeDetectors', async () => { + const initializeDetectorsStub = stub(filterService, '_initializeDetectors'); + await filterService.initFilters(); + ok(initializeDetectorsStub.calledOnce); + }); + test('should set _detectors on _initializeDetectors call', async () => { + const DETECTOR_SUMMARIES = [ + { + name: 'Detector human-readable name', + type: 'Detector type identifier', + }, + ]; + + bookkeepingServiceMock.retrieveDetectorSummaries.resolves(DETECTOR_SUMMARIES); + await filterService._initializeDetectors(); + + deepStrictEqual(filterService._detectors, DETECTOR_SUMMARIES); + ok(Object.isFrozen(filterService._detectors)); + }); }); suite('getRunTypes', async () => { From aa9689c690e2246046f23fd6aa55c415f42ba62b Mon Sep 17 00:00:00 2001 From: hehoon <100522372+hehoon@users.noreply.github.com> Date: Wed, 14 Jan 2026 15:59:04 +0100 Subject: [PATCH 3/4] Move object data filtering logic out of BookkeepingService.js and into FilterService.js --- QualityControl/lib/services/BookkeepingService.js | 4 ++-- QualityControl/lib/services/FilterService.js | 2 +- QualityControl/test/lib/services/BookkeepingService.test.js | 5 ++--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/QualityControl/lib/services/BookkeepingService.js b/QualityControl/lib/services/BookkeepingService.js index 919e7877f..33da3dd6d 100644 --- a/QualityControl/lib/services/BookkeepingService.js +++ b/QualityControl/lib/services/BookkeepingService.js @@ -184,7 +184,7 @@ export class BookkeepingService { /** * Retrieves the information about the detectors from the Bookkeeping service. - * @returns {Promise} Array of detector summaries. + * @returns {Promise} Array of detector summaries. */ async retrieveDetectorSummaries() { const { data } = await httpGetJson( @@ -196,7 +196,7 @@ export class BookkeepingService { rejectUnauthorized: false, }, ); - return Array.isArray(data) ? data.map(({ name, type }) => ({ name, type })) : []; + return Array.isArray(data) ? data : []; } /** diff --git a/QualityControl/lib/services/FilterService.js b/QualityControl/lib/services/FilterService.js index b964c90f6..a8373894a 100644 --- a/QualityControl/lib/services/FilterService.js +++ b/QualityControl/lib/services/FilterService.js @@ -79,7 +79,7 @@ export class FilterService { async _initializeDetectors() { try { const detectorSummaries = await this._bookkeepingService.retrieveDetectorSummaries(); - this._detectors = Object.freeze(detectorSummaries.map((detector) => Object.freeze({ ...detector }))); + this._detectors = Object.freeze(detectorSummaries.map(({ name, type }) => Object.freeze({ name, type }))); } catch (error) { this._logger.errorMessage(`Failed to retrieve detectors: ${error?.message || error}`); } diff --git a/QualityControl/test/lib/services/BookkeepingService.test.js b/QualityControl/test/lib/services/BookkeepingService.test.js index 251c9a6a5..1502d896a 100644 --- a/QualityControl/test/lib/services/BookkeepingService.test.js +++ b/QualityControl/test/lib/services/BookkeepingService.test.js @@ -325,9 +325,8 @@ export const bookkeepingServiceTestSuite = async () => { ok(Array.isArray(result)); strictEqual(result.length, mockResponse.data.length); - // Verify each detector is preserved - const detectorSummaries = result.map(({ name, type }) => ({ name, type })); - deepStrictEqual(result, detectorSummaries); + // Verify detector data is preserved + deepStrictEqual(result, mockResponse.data); }); test('should return empty array when data is not an array', async () => { From 348b3aa20eb34204a306ea44edfcdf67817b1792 Mon Sep 17 00:00:00 2001 From: hehoon <100522372+hehoon@users.noreply.github.com> Date: Wed, 14 Jan 2026 16:04:01 +0100 Subject: [PATCH 4/4] Do not attempt to initialize detectors when bookkeeping is inactive --- QualityControl/lib/services/FilterService.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/QualityControl/lib/services/FilterService.js b/QualityControl/lib/services/FilterService.js index a8373894a..dd191ac34 100644 --- a/QualityControl/lib/services/FilterService.js +++ b/QualityControl/lib/services/FilterService.js @@ -78,6 +78,10 @@ export class FilterService { */ async _initializeDetectors() { try { + if (!this._bookkeepingService.active) { + return; + } + const detectorSummaries = await this._bookkeepingService.retrieveDetectorSummaries(); this._detectors = Object.freeze(detectorSummaries.map(({ name, type }) => Object.freeze({ name, type }))); } catch (error) {