From 59988a79d50fe1d02ec58f64422d2a59a7cf915b Mon Sep 17 00:00:00 2001 From: "Claude Sonnet 5.5" Date: Thu, 22 Oct 2026 12:39:40 +0200 Subject: [PATCH 1/7] [O2B-1623] Add the missing license headers and documentation to the CTP trigger counters files The router, the controller, the service, the model, the migration renaming the table and the three tests of the CTP trigger counters had no license header, unlike nearly all the other files of their layers. The HTTP and gRPC controllers handlers are now documented instead of disabling the JSDoc rules, and the service documentation of `className` is fixed (it described the run number). --- ...rigger-counters-to-ctp-trigger-counters.js | 13 ++++++++++++ lib/database/models/ctpTriggerCounters.js | 13 ++++++++++++ .../ctpTriggerCounters.controller.js | 20 ++++++++++++++++++- .../gRPC/GRPCCtpTriggerCountersController.js | 16 ++++++++++++++- .../routers/ctpTriggerCounters.router.js | 13 ++++++++++++ .../CtpTriggerCountersService.js | 15 +++++++++++++- test/api/ctpTriggerCounters.test.js | 13 ++++++++++++ .../CtpTriggerCountersService.test.js | 13 ++++++++++++ .../services/ctpTriggerCounters/index.js | 13 ++++++++++++ 9 files changed, 126 insertions(+), 3 deletions(-) diff --git a/lib/database/migrations/v1/20250211085415-rename-trigger-counters-to-ctp-trigger-counters.js b/lib/database/migrations/v1/20250211085415-rename-trigger-counters-to-ctp-trigger-counters.js index 37da4d0c6a..2931e3049f 100644 --- a/lib/database/migrations/v1/20250211085415-rename-trigger-counters-to-ctp-trigger-counters.js +++ b/lib/database/migrations/v1/20250211085415-rename-trigger-counters-to-ctp-trigger-counters.js @@ -1,3 +1,16 @@ +/** + * @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. + */ + 'use strict'; /** @type {import('sequelize-cli').Migration} */ diff --git a/lib/database/models/ctpTriggerCounters.js b/lib/database/models/ctpTriggerCounters.js index 4e8234a752..e2c8e5d903 100644 --- a/lib/database/models/ctpTriggerCounters.js +++ b/lib/database/models/ctpTriggerCounters.js @@ -1,3 +1,16 @@ +/** + * @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. + */ + const Sequelize = require('sequelize'); module.exports = (sequelize) => { diff --git a/lib/server/controllers/ctpTriggerCounters.controller.js b/lib/server/controllers/ctpTriggerCounters.controller.js index 9f290dbdd9..f549c926ab 100644 --- a/lib/server/controllers/ctpTriggerCounters.controller.js +++ b/lib/server/controllers/ctpTriggerCounters.controller.js @@ -1,12 +1,30 @@ +/** + * @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. + */ + const { dtoValidator } = require('../utilities'); const { DtoFactory } = require('../../domain/dtos/DtoFactory'); const Joi = require('joi'); const { updateExpressResponseFromNativeError } = require('../express/updateExpressResponseFromNativeError'); const { ctpTriggerCountersService } = require('../services/ctpTriggerCounters/CtpTriggerCountersService'); -// eslint-disable-next-line jsdoc/require-param /** * Return all the trigger counters for a given run + * + * @param {Object} request The *request* object represents the HTTP request and has properties for the request query + * string, parameters, body, HTTP headers, and so on. + * @param {Object} response The *response* object represents the HTTP response that an Express app sends when it gets an + * HTTP request. + * @returns {Promise} resolves once the response has been sent */ const getAllCtpTriggerCountersForRunHandler = async (request, response) => { const requestDto = await dtoValidator(DtoFactory.paramsOnly({ diff --git a/lib/server/controllers/gRPC/GRPCCtpTriggerCountersController.js b/lib/server/controllers/gRPC/GRPCCtpTriggerCountersController.js index 0dab70caee..b026846da7 100644 --- a/lib/server/controllers/gRPC/GRPCCtpTriggerCountersController.js +++ b/lib/server/controllers/gRPC/GRPCCtpTriggerCountersController.js @@ -24,7 +24,21 @@ class GRPCCtpTriggerCountersController { this.ctpTriggerCountersService = ctpTriggerCountersService; } - // eslint-disable-next-line jsdoc/require-jsdoc + /** + * Create or update the counters of a trigger class of a run + * + * @param {object} request the gRPC request + * @param {number} request.runNumber the run number of the run for which counters are created/updated + * @param {string} request.className the name of the trigger class for which counters are created/updated + * @param {string|number} [request.timestamp] the timestamp of the measure (UNIX timestamp in milliseconds) + * @param {number} request.lmb LM level counter before vetoes + * @param {number} request.lma LM level counter after vetoes + * @param {number} request.l0b L0 level counter before vetoes + * @param {number} request.l0a L0 level counter after vetoes + * @param {number} request.l1b L1 level counter before vetoes + * @param {number} request.l1a L1 level counter after vetoes + * @return {Promise} resolves with an empty response once the counters have been created or updated + */ async CreateOrUpdateForRun({ runNumber, className, timestamp, lmb, lma, l0b, l0a, l1b, l1a }) { await this.ctpTriggerCountersService.createOrUpdatePerRun( { runNumber, className }, diff --git a/lib/server/routers/ctpTriggerCounters.router.js b/lib/server/routers/ctpTriggerCounters.router.js index 73908d3a9f..1ebf2e627e 100644 --- a/lib/server/routers/ctpTriggerCounters.router.js +++ b/lib/server/routers/ctpTriggerCounters.router.js @@ -1,3 +1,16 @@ +/** + * @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. + */ + const { getAllCtpTriggerCountersForRunHandler } = require('../controllers/ctpTriggerCounters.controller'); exports.ctpTriggerCountersRouter = { diff --git a/lib/server/services/ctpTriggerCounters/CtpTriggerCountersService.js b/lib/server/services/ctpTriggerCounters/CtpTriggerCountersService.js index db88e8719f..a68143782b 100644 --- a/lib/server/services/ctpTriggerCounters/CtpTriggerCountersService.js +++ b/lib/server/services/ctpTriggerCounters/CtpTriggerCountersService.js @@ -1,3 +1,16 @@ +/** + * @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. + */ + const { CtpTriggerCountersRepository } = require('../../../database/repositories'); const { ctpTriggerCountersAdapter } = require('../../../database/adapters'); const { getRunOrFail } = require('../run/getRunOrFail.js'); @@ -21,7 +34,7 @@ class CtpTriggerCountersService { * * @param {object} criteria the criteria identifying the counters to update * @param {number} criteria.runNumber the run number of run for which trigger counters are created/updated - * @param {string} criteria.className the run number of run for which trigger counters are created/updated + * @param {string} criteria.className the name of the trigger class for which counters are created/updated * @param {Pick} counters the actual counters data * @return {Promise} resolves once the counters have been created */ diff --git a/test/api/ctpTriggerCounters.test.js b/test/api/ctpTriggerCounters.test.js index 2d127c6be2..0768f92862 100644 --- a/test/api/ctpTriggerCounters.test.js +++ b/test/api/ctpTriggerCounters.test.js @@ -1,3 +1,16 @@ +/** + * @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. + */ + const { resetDatabaseContent } = require('../utilities/resetDatabaseContent'); const request = require('supertest'); const { server } = require('../../lib/application'); diff --git a/test/lib/server/services/ctpTriggerCounters/CtpTriggerCountersService.test.js b/test/lib/server/services/ctpTriggerCounters/CtpTriggerCountersService.test.js index fed65c6813..c5bcaf41b2 100644 --- a/test/lib/server/services/ctpTriggerCounters/CtpTriggerCountersService.test.js +++ b/test/lib/server/services/ctpTriggerCounters/CtpTriggerCountersService.test.js @@ -1,3 +1,16 @@ +/** + * @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. + */ + const { ctpTriggerCountersService } = require('../../../../../lib/server/services/ctpTriggerCounters/CtpTriggerCountersService'); const { expect } = require('chai'); const { resetDatabaseContent } = require('../../../../utilities/resetDatabaseContent.js'); diff --git a/test/lib/server/services/ctpTriggerCounters/index.js b/test/lib/server/services/ctpTriggerCounters/index.js index 0c11f8c347..b20ac83102 100644 --- a/test/lib/server/services/ctpTriggerCounters/index.js +++ b/test/lib/server/services/ctpTriggerCounters/index.js @@ -1,3 +1,16 @@ +/** + * @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. + */ + const { resetDatabaseContent } = require('../../../../utilities/resetDatabaseContent'); const CtpTriggerCountersServiceTest = require('./CtpTriggerCountersService.test'); From 944a69502f68f5c18fbe96b1c3901687d714348d Mon Sep 17 00:00:00 2001 From: "Claude Sonnet 5.5" Date: Thu, 22 Oct 2026 12:47:21 +0200 Subject: [PATCH 2/7] [O2B-1623] Build the CTP trigger counters service on small functions `CtpTriggerCountersService` used the repository directly. It now relies on two small functions, `getAllCtpTriggerCounters(qbConfiguration)` returning models and `createOrUpdateCtpTriggerCounters(criteria, counters)`, and keeps the adapter, the run existence check and the public API (`getPerRun`, `createOrUpdatePerRun`), as it is done for run types, detectors and tags. Both small functions have their own tests. --- .../CtpTriggerCountersService.js | 18 ++----- .../createOrUpdateCtpTriggerCounters.js | 37 +++++++++++++ .../getAllCtpTriggerCounters.js | 30 +++++++++++ .../createOrUpdateCtpTriggerCounters.test.js | 52 +++++++++++++++++++ .../getAllCtpTriggerCounters.test.js | 32 ++++++++++++ .../services/ctpTriggerCounters/index.js | 5 ++ 6 files changed, 161 insertions(+), 13 deletions(-) create mode 100644 lib/server/services/ctpTriggerCounters/createOrUpdateCtpTriggerCounters.js create mode 100644 lib/server/services/ctpTriggerCounters/getAllCtpTriggerCounters.js create mode 100644 test/lib/server/services/ctpTriggerCounters/createOrUpdateCtpTriggerCounters.test.js create mode 100644 test/lib/server/services/ctpTriggerCounters/getAllCtpTriggerCounters.test.js diff --git a/lib/server/services/ctpTriggerCounters/CtpTriggerCountersService.js b/lib/server/services/ctpTriggerCounters/CtpTriggerCountersService.js index a68143782b..bec6912801 100644 --- a/lib/server/services/ctpTriggerCounters/CtpTriggerCountersService.js +++ b/lib/server/services/ctpTriggerCounters/CtpTriggerCountersService.js @@ -11,9 +11,10 @@ * or submit itself to any jurisdiction. */ -const { CtpTriggerCountersRepository } = require('../../../database/repositories'); const { ctpTriggerCountersAdapter } = require('../../../database/adapters'); const { getRunOrFail } = require('../run/getRunOrFail.js'); +const { getAllCtpTriggerCounters } = require('./getAllCtpTriggerCounters.js'); +const { createOrUpdateCtpTriggerCounters } = require('./createOrUpdateCtpTriggerCounters.js'); /** * Service related to trigger counters @@ -26,7 +27,8 @@ class CtpTriggerCountersService { * @return {Promise} the trigger counters for the run */ async getPerRun(runNumber) { - return (await CtpTriggerCountersRepository.findAll({ where: { runNumber } })).map(ctpTriggerCountersAdapter.toEntity); + return (await getAllCtpTriggerCounters((queryBuilder) => queryBuilder.where('runNumber').is(runNumber))) + .map(ctpTriggerCountersAdapter.toEntity); } /** @@ -41,17 +43,7 @@ class CtpTriggerCountersService { async createOrUpdatePerRun({ runNumber, className }, counters) { // Check that run exists const run = await getRunOrFail({ runNumber }); - await CtpTriggerCountersRepository.upsert({ - runNumber: run.runNumber, - className, - timestamp: counters.timestamp, - lmb: counters.lmb, - lma: counters.lma, - l0b: counters.l0b, - l0a: counters.l0a, - l1b: counters.l1b, - l1a: counters.l1a, - }); + await createOrUpdateCtpTriggerCounters({ runNumber: run.runNumber, className }, counters); } } diff --git a/lib/server/services/ctpTriggerCounters/createOrUpdateCtpTriggerCounters.js b/lib/server/services/ctpTriggerCounters/createOrUpdateCtpTriggerCounters.js new file mode 100644 index 0000000000..75209d70b1 --- /dev/null +++ b/lib/server/services/ctpTriggerCounters/createOrUpdateCtpTriggerCounters.js @@ -0,0 +1,37 @@ +/** + * @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. + */ + +const { CtpTriggerCountersRepository } = require('../../../database/repositories/index.js'); + +/** + * Create the counters of a trigger class of a run, or update them if they already exist + * + * @param {object} criteria the criteria identifying the counters to create or update + * @param {number} criteria.runNumber the run number of the run for which counters are created or updated + * @param {string} criteria.className the name of the trigger class for which counters are created or updated + * @param {Pick} counters the actual counters data + * @return {Promise} resolves once the counters have been created or updated + */ +exports.createOrUpdateCtpTriggerCounters = async ({ runNumber, className }, counters) => { + await CtpTriggerCountersRepository.upsert({ + runNumber, + className, + timestamp: counters.timestamp, + lmb: counters.lmb, + lma: counters.lma, + l0b: counters.l0b, + l0a: counters.l0a, + l1b: counters.l1b, + l1a: counters.l1a, + }); +}; diff --git a/lib/server/services/ctpTriggerCounters/getAllCtpTriggerCounters.js b/lib/server/services/ctpTriggerCounters/getAllCtpTriggerCounters.js new file mode 100644 index 0000000000..8cdd9f9302 --- /dev/null +++ b/lib/server/services/ctpTriggerCounters/getAllCtpTriggerCounters.js @@ -0,0 +1,30 @@ +/** + * @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. + */ + +const { CtpTriggerCountersRepository } = require('../../../database/repositories/index.js'); +const { utilities: { QueryBuilder } } = require('../../../database'); + +/** + * Return the list of all the CTP trigger counters + * + * @param {Function|null} [qbConfiguration] function called with the counters query builder as parameter to add specific configuration to the + * query + * @return {Promise} Promise resolving with the list of counters + */ +exports.getAllCtpTriggerCounters = (qbConfiguration = null) => { + const queryBuilder = new QueryBuilder(); + if (qbConfiguration) { + qbConfiguration(queryBuilder); + } + return CtpTriggerCountersRepository.findAll(queryBuilder); +}; diff --git a/test/lib/server/services/ctpTriggerCounters/createOrUpdateCtpTriggerCounters.test.js b/test/lib/server/services/ctpTriggerCounters/createOrUpdateCtpTriggerCounters.test.js new file mode 100644 index 0000000000..99a11ff524 --- /dev/null +++ b/test/lib/server/services/ctpTriggerCounters/createOrUpdateCtpTriggerCounters.test.js @@ -0,0 +1,52 @@ +/** + * @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. + */ + +const { createOrUpdateCtpTriggerCounters } = require('../../../../../lib/server/services/ctpTriggerCounters/createOrUpdateCtpTriggerCounters.js'); +const { getAllCtpTriggerCounters } = require('../../../../../lib/server/services/ctpTriggerCounters/getAllCtpTriggerCounters.js'); +const { expect } = require('chai'); + +/** + * Return the counters of a given trigger class of a given run + * + * @param {number} runNumber the run number + * @param {string} className the name of the trigger class + * @return {Promise} the counters found + */ +const getCounters = (runNumber, className) => getAllCtpTriggerCounters((queryBuilder) => { + queryBuilder.where('runNumber').is(runNumber); + queryBuilder.where('className').is(className); +}); + +module.exports = () => { + const criteria = { runNumber: 2, className: 'CREATE-OR-UPDATE-CLASS' }; + + it('should successfully create the counters of a trigger class of a run', async () => { + expect(await getCounters(criteria.runNumber, criteria.className)).to.eql([]); + + await createOrUpdateCtpTriggerCounters(criteria, { timestamp: 1717425912000, lmb: 1, lma: 2, l0b: 3, l0a: 4, l1b: 5, l1a: 6 }); + + const [counters, ...others] = await getCounters(criteria.runNumber, criteria.className); + expect(others).to.lengthOf(0); + expect([counters.lmb, counters.lma, counters.l0b, counters.l0a, counters.l1b, counters.l1a]).to.eql([1, 2, 3, 4, 5, 6]); + expect(new Date(counters.timestamp).getTime()).to.equal(1717425912000); + }); + + it('should successfully update the counters instead of creating new ones when they already exist', async () => { + await createOrUpdateCtpTriggerCounters(criteria, { timestamp: 1717425913000, lmb: 11, lma: 12, l0b: 13, l0a: 14, l1b: 15, l1a: 16 }); + + const [counters, ...others] = await getCounters(criteria.runNumber, criteria.className); + expect(others).to.lengthOf(0); + expect([counters.lmb, counters.lma, counters.l0b, counters.l0a, counters.l1b, counters.l1a]).to.eql([11, 12, 13, 14, 15, 16]); + expect(new Date(counters.timestamp).getTime()).to.equal(1717425913000); + }); +}; diff --git a/test/lib/server/services/ctpTriggerCounters/getAllCtpTriggerCounters.test.js b/test/lib/server/services/ctpTriggerCounters/getAllCtpTriggerCounters.test.js new file mode 100644 index 0000000000..a4afe880c8 --- /dev/null +++ b/test/lib/server/services/ctpTriggerCounters/getAllCtpTriggerCounters.test.js @@ -0,0 +1,32 @@ +/** + * @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. + */ + +const { getAllCtpTriggerCounters } = require('../../../../../lib/server/services/ctpTriggerCounters/getAllCtpTriggerCounters.js'); +const { expect } = require('chai'); + +module.exports = () => { + it('should successfully return all the trigger counters', async () => { + const counters = await getAllCtpTriggerCounters(); + + expect(counters.length).to.be.greaterThan(1); + expect(counters.map(({ className }) => className)).to.include.members(['FIRST-CLASS-NAME', 'SECOND-CLASS-NAME']); + }); + + it('should apply the given query builder configuration', async () => { + const countersOfRun1 = await getAllCtpTriggerCounters((queryBuilder) => queryBuilder.where('runNumber').is(1)); + expect(countersOfRun1.map(({ className }) => className)).to.have.members(['FIRST-CLASS-NAME', 'SECOND-CLASS-NAME']); + expect(countersOfRun1.every(({ runNumber }) => runNumber === 1)).to.be.true; + + expect(await getAllCtpTriggerCounters((queryBuilder) => queryBuilder.where('runNumber').is(2))).to.eql([]); + }); +}; diff --git a/test/lib/server/services/ctpTriggerCounters/index.js b/test/lib/server/services/ctpTriggerCounters/index.js index b20ac83102..e014f767fd 100644 --- a/test/lib/server/services/ctpTriggerCounters/index.js +++ b/test/lib/server/services/ctpTriggerCounters/index.js @@ -13,9 +13,14 @@ const { resetDatabaseContent } = require('../../../../utilities/resetDatabaseContent'); const CtpTriggerCountersServiceTest = require('./CtpTriggerCountersService.test'); +const getAllCtpTriggerCountersTest = require('./getAllCtpTriggerCounters.test.js'); +const createOrUpdateCtpTriggerCountersTest = require('./createOrUpdateCtpTriggerCounters.test.js'); module.exports = () => { before(resetDatabaseContent); + after(resetDatabaseContent); describe('CtpTriggerCountersService', CtpTriggerCountersServiceTest); + describe('getAllCtpTriggerCounters', getAllCtpTriggerCountersTest); + describe('createOrUpdateCtpTriggerCounters', createOrUpdateCtpTriggerCountersTest); }; From 7212f1ffe3aa7b58be5497804a5eeb80c9ffcd0f Mon Sep 17 00:00:00 2001 From: "Claude Sonnet 5.5" Date: Thu, 22 Oct 2026 13:39:49 +0200 Subject: [PATCH 3/7] [O2B-1623] Validate the run number of the CTP trigger counters like the other run endpoints `GET /api/ctp-trigger-counters/:runNumber` accepted any number (`0`, negative numbers, decimals) and answered with an empty list. The run number is now validated with `EntityIdDto` like in `GetRunDto`, so that invalid values are rejected with a 400. An API test checks the four kinds of invalid run numbers. --- .../controllers/ctpTriggerCounters.controller.js | 4 ++-- test/api/ctpTriggerCounters.test.js | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/lib/server/controllers/ctpTriggerCounters.controller.js b/lib/server/controllers/ctpTriggerCounters.controller.js index f549c926ab..97a59c29a7 100644 --- a/lib/server/controllers/ctpTriggerCounters.controller.js +++ b/lib/server/controllers/ctpTriggerCounters.controller.js @@ -13,7 +13,7 @@ const { dtoValidator } = require('../utilities'); const { DtoFactory } = require('../../domain/dtos/DtoFactory'); -const Joi = require('joi'); +const EntityIdDto = require('../../domain/dtos/EntityIdDto.js'); const { updateExpressResponseFromNativeError } = require('../express/updateExpressResponseFromNativeError'); const { ctpTriggerCountersService } = require('../services/ctpTriggerCounters/CtpTriggerCountersService'); @@ -28,7 +28,7 @@ const { ctpTriggerCountersService } = require('../services/ctpTriggerCounters/Ct */ const getAllCtpTriggerCountersForRunHandler = async (request, response) => { const requestDto = await dtoValidator(DtoFactory.paramsOnly({ - runNumber: Joi.number().required(), + runNumber: EntityIdDto.required(), }), request, response); if (requestDto) { diff --git a/test/api/ctpTriggerCounters.test.js b/test/api/ctpTriggerCounters.test.js index 0768f92862..ad630c4db9 100644 --- a/test/api/ctpTriggerCounters.test.js +++ b/test/api/ctpTriggerCounters.test.js @@ -37,5 +37,20 @@ module.exports = () => { expect(response.body.data.map(({ id }) => id)).to.eql([]); } }); + + it('Should return 400 if the run number is not a positive integer', async () => { + for (const [runNumber, message] of [ + ['abc', '"params.runNumber" must be a number'], + ['-1', '"params.runNumber" must be a positive number'], + ['0', '"params.runNumber" must be a positive number'], + ['0.5', '"params.runNumber" must be an integer'], + ]) { + const response = await request(server).get(`/api/ctp-trigger-counters/${runNumber}`); + + expect(response.status, `run number ${runNumber}`).to.equal(400); + const error = response.body.errors.find(({ source: { pointer } }) => pointer === '/data/attributes/params/runNumber'); + expect(error.detail, `run number ${runNumber}`).to.equal(message); + } + }); }); }; From 91d577266c32cb1fc163e854a66086beee42fe0a Mon Sep 17 00:00:00 2001 From: "Claude Sonnet 5.5" Date: Thu, 22 Oct 2026 13:50:26 +0200 Subject: [PATCH 4/7] [O2B-1623] Answer 404 for the CTP trigger counters of a run that does not exist `GET /api/ctp-trigger-counters/:runNumber` answered `200` with an empty list for a run that does not exist, unlike the other endpoints reading data of a run (`/api/runs/:runNumber/logs`, `/api/runs/:runNumber/flps`), which hides a wrong run number. The counters are fetched first and the run is only checked when none is found, to tell a run without counters (empty list) from a missing run (`NotFoundError`, so a 404): counters can only exist for an existing run, as `ctp_trigger_counters.run_number` is a foreign key, so the common case still costs a single query. --- .../CtpTriggerCountersService.js | 15 +++++++++++++-- test/api/ctpTriggerCounters.test.js | 9 +++++++++ .../CtpTriggerCountersService.test.js | 8 ++++++++ 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/lib/server/services/ctpTriggerCounters/CtpTriggerCountersService.js b/lib/server/services/ctpTriggerCounters/CtpTriggerCountersService.js index bec6912801..a592dece0a 100644 --- a/lib/server/services/ctpTriggerCounters/CtpTriggerCountersService.js +++ b/lib/server/services/ctpTriggerCounters/CtpTriggerCountersService.js @@ -24,11 +24,22 @@ class CtpTriggerCountersService { * Return the list of trigger counters linked to a given run * * @param {number} runNumber the run number of the run for which counters must be fetched - * @return {Promise} the trigger counters for the run + * @return {Promise} the trigger counters for the run (empty if the run has none) + * @throws {NotFoundError} if the run does not exist */ async getPerRun(runNumber) { - return (await getAllCtpTriggerCounters((queryBuilder) => queryBuilder.where('runNumber').is(runNumber))) + const counters = (await getAllCtpTriggerCounters((queryBuilder) => queryBuilder.where('runNumber').is(runNumber))) .map(ctpTriggerCountersAdapter.toEntity); + + if (counters.length === 0) { + /* + * Counters can only exist for an existing run (foreign key), so the run only needs to be checked to tell a run without counters + * from a missing run + */ + await getRunOrFail({ runNumber }); + } + + return counters; } /** diff --git a/test/api/ctpTriggerCounters.test.js b/test/api/ctpTriggerCounters.test.js index ad630c4db9..286ef90012 100644 --- a/test/api/ctpTriggerCounters.test.js +++ b/test/api/ctpTriggerCounters.test.js @@ -38,6 +38,15 @@ module.exports = () => { } }); + it('Should return 404 if the run does not exist', async () => { + const response = await request(server).get('/api/ctp-trigger-counters/999999999'); + + expect(response.status).to.equal(404); + expect(response.body.errors).to.eql([ + { status: 404, title: 'Not found', detail: 'Run with this run number (999999999) could not be found' }, + ]); + }); + it('Should return 400 if the run number is not a positive integer', async () => { for (const [runNumber, message] of [ ['abc', '"params.runNumber" must be a number'], diff --git a/test/lib/server/services/ctpTriggerCounters/CtpTriggerCountersService.test.js b/test/lib/server/services/ctpTriggerCounters/CtpTriggerCountersService.test.js index c5bcaf41b2..43bdd96014 100644 --- a/test/lib/server/services/ctpTriggerCounters/CtpTriggerCountersService.test.js +++ b/test/lib/server/services/ctpTriggerCounters/CtpTriggerCountersService.test.js @@ -16,6 +16,7 @@ const { expect } = require('chai'); const { resetDatabaseContent } = require('../../../../utilities/resetDatabaseContent.js'); const assert = require('assert'); const { BadParameterError } = require('../../../../../lib/server/errors/BadParameterError.js'); +const { NotFoundError } = require('../../../../../lib/server/errors/NotFoundError.js'); /** * Simplify a trigger counter to extract properties interesting in testing (remove createdAt and updatedAt) @@ -69,6 +70,13 @@ module.exports = () => { expect(await ctpTriggerCountersService.getPerRun(2)).to.eql([]); }); + it('Should throw a not found error when fetching the counters of a non-existing run', async () => { + await assert.rejects( + () => ctpTriggerCountersService.getPerRun(999), + new NotFoundError('Run with this run number (999) could not be found'), + ); + }); + it('Should successfully insert new counters for a given run', async () => { const now = new Date(); const counters = { From 6a35b9ae4bb85874963a0ad6e6e94ed342bd43ec Mon Sep 17 00:00:00 2001 From: "Claude Sonnet 5.5" Date: Thu, 22 Oct 2026 12:15:51 +0200 Subject: [PATCH 5/7] [O2B-1623] Do not send createdAt and updatedAt of CTP trigger counters Nothing uses the creation and update dates of the CTP trigger counters: the run details page only reads the class name and the counters (`lmb`, `lma`, `l0b`, `l0a`, `l1b`, `l1a`), the service test already removes the dates before comparing, and `CtpTriggerCountersAdapter.toEntity` is only used by `CtpTriggerCountersService.getPerRun`. The adapter now stops returning them, the `CtpTriggerCounters` typedef is updated and the API test checks the returned keys. --- lib/database/adapters/CtpTriggerCountersAdapter.js | 4 +--- lib/domain/entities/CtpTriggerCounters.js | 2 -- test/api/ctpTriggerCounters.test.js | 3 +++ .../ctpTriggerCounters/CtpTriggerCountersService.test.js | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/database/adapters/CtpTriggerCountersAdapter.js b/lib/database/adapters/CtpTriggerCountersAdapter.js index 6bf576f953..c775de6caf 100644 --- a/lib/database/adapters/CtpTriggerCountersAdapter.js +++ b/lib/database/adapters/CtpTriggerCountersAdapter.js @@ -29,7 +29,7 @@ class CtpTriggerCountersAdapter { * @param {SequelizeCtpTriggerCounters} databaseObject Object to convert. * @returns {CtpTriggerCounters} Converted entity object. */ - toEntity({ id, timestamp, runNumber, className, lmb, lma, l0b, l0a, l1b, l1a, createdAt, updatedAt }) { + toEntity({ id, timestamp, runNumber, className, lmb, lma, l0b, l0a, l1b, l1a, createdAt }) { return { id, timestamp: timestamp ? new Date(timestamp).getTime() : createdAt, @@ -41,8 +41,6 @@ class CtpTriggerCountersAdapter { l0a, l1b, l1a, - createdAt: createdAt ? new Date(createdAt).getTime() : createdAt, - updatedAt: updatedAt ? new Date(updatedAt).getTime() : updatedAt, }; } diff --git a/lib/domain/entities/CtpTriggerCounters.js b/lib/domain/entities/CtpTriggerCounters.js index e66af5ad79..71808a0ddf 100644 --- a/lib/domain/entities/CtpTriggerCounters.js +++ b/lib/domain/entities/CtpTriggerCounters.js @@ -24,6 +24,4 @@ * @property {number} l0a * @property {number} l1b * @property {number} l1a - * @property {number} createdAt - * @property {number} updatedAt */ diff --git a/test/api/ctpTriggerCounters.test.js b/test/api/ctpTriggerCounters.test.js index 286ef90012..aedf0f42a8 100644 --- a/test/api/ctpTriggerCounters.test.js +++ b/test/api/ctpTriggerCounters.test.js @@ -25,6 +25,9 @@ module.exports = () => { const response = await request(server).get('/api/ctp-trigger-counters/1'); expect(response.status).to.equal(200); + response.body.data.forEach((counters) => expect(Object.keys(counters)).to.have.members( + ['id', 'timestamp', 'runNumber', 'className', 'lmb', 'lma', 'l0b', 'l0a', 'l1b', 'l1a'], + )); expect(response.body.data.map(({ id, runNumber, className }) => ({ id, runNumber, className }))).to.deep.eql([ { id: 1, runNumber: 1, className: 'FIRST-CLASS-NAME' }, { id: 2, runNumber: 1, className: 'SECOND-CLASS-NAME' }, diff --git a/test/lib/server/services/ctpTriggerCounters/CtpTriggerCountersService.test.js b/test/lib/server/services/ctpTriggerCounters/CtpTriggerCountersService.test.js index 43bdd96014..1359058779 100644 --- a/test/lib/server/services/ctpTriggerCounters/CtpTriggerCountersService.test.js +++ b/test/lib/server/services/ctpTriggerCounters/CtpTriggerCountersService.test.js @@ -19,7 +19,7 @@ const { BadParameterError } = require('../../../../../lib/server/errors/BadParam const { NotFoundError } = require('../../../../../lib/server/errors/NotFoundError.js'); /** - * Simplify a trigger counter to extract properties interesting in testing (remove createdAt and updatedAt) + * Simplify a trigger counter to extract properties interesting in testing * * @param {CtpTriggerCounters} counters the counters to simplify * @return {object} simplified trigger counters From f73891b14e8c1d103d3837e92313624266ea7556 Mon Sep 17 00:00:00 2001 From: "Claude Sonnet 5.5" Date: Tue, 20 Oct 2026 14:58:52 +0200 Subject: [PATCH 6/7] [O2B-1623] Document the ctp-trigger-counters endpoint in the HTTP API specification --- lib/server/api-docs/openapi.yaml | 56 ++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/lib/server/api-docs/openapi.yaml b/lib/server/api-docs/openapi.yaml index 700d80fd25..2069aec8fa 100644 --- a/lib/server/api-docs/openapi.yaml +++ b/lib/server/api-docs/openapi.yaml @@ -45,6 +45,8 @@ tags: description: Detectors of the ALICE experiment. - name: Run types description: Types of runs (e.g. calibration kinds). + - name: CTP trigger counters + description: Counters of the central trigger processor, per run and trigger class. paths: /tags: @@ -359,6 +361,39 @@ paths: errors: - { status: 404, title: Not found, detail: Run type with this id (999999999) could not be found } + /ctp-trigger-counters/{runNumber}: + get: + tags: [CTP trigger counters] + operationId: listCtpTriggerCountersOfRun + summary: List the CTP trigger counters of a run + description: | + Returns the trigger counters of every trigger class recorded for the run. Not paginated. + A run without counters gives `200` with an empty list. A run that does not exist gives `404`. + parameters: + - name: runNumber + in: path + required: true + description: Run number. + schema: { type: integer, minimum: 1, examples: [106] } + responses: + '200': + description: The counters, one entry per trigger class. + content: + application/json: + schema: { $ref: '#/components/schemas/CtpTriggerCountersListResponse' } + example: + data: + - { id: 1, runNumber: 106, className: minbias, timestamp: 1704067200000, lmb: 10, lma: 9, l0b: 8, l0a: 7, l1b: 6, l1a: 5 } + '400': { $ref: '#/components/responses/ValidationError' } + '404': + description: No run has this run number. + content: + application/json: + schema: { $ref: '#/components/schemas/ErrorResponse' } + example: + errors: + - { status: 404, title: Not found, detail: Run with this run number (999999999) could not be found } + components: securitySchemes: jwtQuery: @@ -540,3 +575,24 @@ components: data: type: array items: { $ref: '#/components/schemas/RunType' } + CtpTriggerCounters: + type: object + description: Counters of one CTP trigger class of a run (LM, L0 and L1 levels, before and after vetoes). + properties: + id: { type: integer } + runNumber: { type: integer } + className: { type: string, description: Name of the trigger class. } + timestamp: { type: integer, description: Epoch milliseconds of the measurement. } + lmb: { type: integer, description: LM level counter before vetoes. } + lma: { type: integer, description: LM level counter after vetoes. } + l0b: { type: integer, description: L0 level counter before vetoes. } + l0a: { type: integer, description: L0 level counter after vetoes. } + l1b: { type: integer, description: L1 level counter before vetoes. } + l1a: { type: integer, description: L1 level counter after vetoes. } + CtpTriggerCountersListResponse: + type: object + required: [data] + properties: + data: + type: array + items: { $ref: '#/components/schemas/CtpTriggerCounters' } From 260724de556a78aaf5fa93e1ee2bf95f220f5492 Mon Sep 17 00:00:00 2001 From: "Claude Sonnet 5.5" Date: Thu, 22 Oct 2026 13:50:37 +0200 Subject: [PATCH 7/7] [O2B-1623] Regenerate the HTTP API documentation with the ctp-trigger-counters endpoint --- lib/server/api-docs/API.md | 25 ++++++++++++++++++++++++ lib/server/api-docs/index.html | 35 ++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/lib/server/api-docs/API.md b/lib/server/api-docs/API.md index 6a9bccd0a2..0b9bdd80fe 100644 --- a/lib/server/api-docs/API.md +++ b/lib/server/api-docs/API.md @@ -325,3 +325,28 @@ Responses: ```bash curl -g "$BOOKKEEPING_URL/api/runTypes/1?token=$TOKEN" ``` + +## CTP trigger counters + +Counters of the central trigger processor, per run and trigger class. + +### `GET /ctp-trigger-counters/{runNumber}` - List the CTP trigger counters of a run + +Operation id: `listCtpTriggerCountersOfRun` + +Returns the trigger counters of every trigger class recorded for the run. Not paginated. +A run without counters gives `200` with an empty list. A run that does not exist gives `404`. + +| Parameter | In | Type | Required | Notes | +| --- | --- | --- | --- | --- | +| `runNumber` | path | integer | yes | Run number. | + +Responses: + +- `200`: The counters, one entry per trigger class. +- `400`: The request failed validation. HTTP status is 400 although each error's `status` field is `"422"`. `source.pointer` has the form `/data/attributes//`. +- `404`: No run has this run number. + +```bash +curl -g "$BOOKKEEPING_URL/api/ctp-trigger-counters/106?token=$TOKEN" +``` diff --git a/lib/server/api-docs/index.html b/lib/server/api-docs/index.html index 591258d62b..6f3163ee7f 100644 --- a/lib/server/api-docs/index.html +++ b/lib/server/api-docs/index.html @@ -535,6 +535,41 @@

Run types

curl -g "$BOOKKEEPING_URL/api/runTypes/1?token=$TOKEN"
 
+

CTP trigger counters

+

Counters of the central trigger processor, per run and trigger class.

+
+

GET /ctp-trigger-counters/{runNumber} - List the CTP trigger counters of a run

+ +

Operation id: listCtpTriggerCountersOfRun

+

Returns the trigger counters of every trigger class recorded for the run. Not paginated. +A run without counters gives 200 with an empty list. A run that does not exist gives 404.

+ + + + + + + + + + + + + + + + + +
ParameterInTypeRequiredNotes
runNumberpathintegeryesRun number.
+

Responses:

+
    +
  • 200: The counters, one entry per trigger class.
  • +
  • 400: The request failed validation. HTTP status is 400 although each error's status field is "422". source.pointer has the form /data/attributes/<query|params|body>/<field>.
  • +
  • 404: No run has this run number.
  • +
+
curl -g "$BOOKKEEPING_URL/api/ctp-trigger-counters/106?token=$TOKEN"
+
+