diff --git a/lib/database/adapters/GaqSummaryAdapter.js b/lib/database/adapters/GaqSummaryAdapter.js new file mode 100644 index 0000000000..53b6baf61a --- /dev/null +++ b/lib/database/adapters/GaqSummaryAdapter.js @@ -0,0 +1,62 @@ +/** + * @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. + */ + +/** + * GaqSummaryAdapter + */ +class GaqSummaryAdapter { + /** + * Constructor + */ + constructor() { + this.toEntity = this.toEntity.bind(this); + } + + /** + * Converts the given database object to an entity object. + * + * @param {SequelizeGaqSummary} databaseObject Object to convert. + * @returns {GaqSummary} Converted entity object. + */ + toEntity(databaseObject) { + const { + dataPassId, + runNumber, + badRunCoverage, + explicitlyNotBadRunCoverage, + mcReproducibleCoverage, + missingVerificationsCount, + undefinedQualityPeriodsCount, + notComputable, + invalidatedAt, + createdAt, + updatedAt, + } = databaseObject; + + return { + dataPassId, + runNumber, + badRunCoverage, + explicitlyNotBadRunCoverage, + mcReproducibleCoverage, + missingVerificationsCount, + undefinedQualityPeriodsCount, + notComputable, + invalidatedAt, + createdAt, + updatedAt, + }; + } +} + +module.exports = { GaqSummaryAdapter }; diff --git a/lib/database/adapters/index.js b/lib/database/adapters/index.js index 5ff6404b3d..9c858e1bc2 100644 --- a/lib/database/adapters/index.js +++ b/lib/database/adapters/index.js @@ -27,6 +27,7 @@ const EorReasonAdapter = require('./EorReasonAdapter'); const FlpRoleAdapter = require('./FlpRoleAdapter'); const { HostAdapter } = require('./HostAdapter.js'); const { GaqDetectorAdapter } = require('./GaqDetectorAdapter.js'); +const { GaqSummaryAdapter } = require('./GaqSummaryAdapter.js'); const { LhcFillAdapter } = require('./LhcFillAdapter.js'); const { LhcFillStatisticsAdapter } = require('./LhcFillStatisticsAdapter.js'); const LhcPeriodAdapter = require('./LhcPeriodAdapter'); @@ -63,6 +64,7 @@ const environmentHistoryItemAdapter = new EnvironmentHistoryItemAdapter(); const eorReasonAdapter = new EorReasonAdapter(); const flpRoleAdapter = new FlpRoleAdapter(); const gaqDetectorAdapter = new GaqDetectorAdapter(); +const gaqSummaryAdapter = new GaqSummaryAdapter(); const hostAdapter = new HostAdapter(); const lhcFillAdapter = new LhcFillAdapter(); const lhcFillStatisticsAdapter = new LhcFillStatisticsAdapter(); @@ -159,6 +161,7 @@ module.exports = { eorReasonAdapter, flpRoleAdapter, gaqDetectorAdapter, + gaqSummaryAdapter, hostAdapter, lhcFillAdapter, lhcFillStatisticsAdapter, diff --git a/lib/database/migrations/v1/20260223120000-create-gaq-summary-tables.js b/lib/database/migrations/v1/20260223120000-create-gaq-summary-tables.js new file mode 100644 index 0000000000..639db07c5c --- /dev/null +++ b/lib/database/migrations/v1/20260223120000-create-gaq-summary-tables.js @@ -0,0 +1,71 @@ +'use strict'; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + up: async (queryInterface, Sequelize) => queryInterface.sequelize.transaction(async (transaction) => { + await queryInterface.createTable('gaq_summaries', { + data_pass_id: { + type: Sequelize.INTEGER, + primaryKey: true, + allowNull: false, + references: { + model: 'data_passes', + key: 'id', + }, + }, + run_number: { + type: Sequelize.INTEGER, + primaryKey: true, + allowNull: false, + references: { + model: 'runs', + key: 'run_number', + }, + }, + bad_run_coverage: { + type: Sequelize.FLOAT, + }, + explicitly_not_bad_run_coverage: { + type: Sequelize.FLOAT, + }, + mc_reproducible_coverage: { + type: Sequelize.FLOAT, + }, + missing_verifications_count: { + type: Sequelize.INTEGER, + }, + undefined_quality_periods_count: { + type: Sequelize.INTEGER, + }, + not_computable: { + type: Sequelize.BOOLEAN, + allowNull: false, + defaultValue: false, + }, + invalidated_at: { + type: Sequelize.DATE(3), + allowNull: true, + defaultValue: null, + }, + created_at: { + type: Sequelize.DATE(3), + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP(3)'), + }, + updated_at: { + type: Sequelize.DATE(3), + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)'), + }, + }, { transaction }); + + await queryInterface.addIndex('gaq_summaries', { + name: 'gaq_summaries_invalidated_at_idx', + fields: ['invalidated_at'], + }, { transaction }); + }), + + down: async (queryInterface) => queryInterface.sequelize.transaction(async (transaction) => { + await queryInterface.dropTable('gaq_summaries', { transaction }); + }), +}; diff --git a/lib/database/models/gaqSummary.js b/lib/database/models/gaqSummary.js new file mode 100644 index 0000000000..ca2a1f69b8 --- /dev/null +++ b/lib/database/models/gaqSummary.js @@ -0,0 +1,59 @@ +/** + * @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. + */ + +module.exports = (sequelize) => { + const Sequelize = require('sequelize'); + + const GaqSummary = sequelize.define('GaqSummary', { + dataPassId: { + type: Sequelize.INTEGER, + primaryKey: true, + }, + runNumber: { + type: Sequelize.INTEGER, + primaryKey: true, + }, + badRunCoverage: { + type: Sequelize.FLOAT, + }, + explicitlyNotBadRunCoverage: { + type: Sequelize.FLOAT, + }, + mcReproducibleCoverage: { + type: Sequelize.FLOAT, + }, + missingVerificationsCount: { + type: Sequelize.INTEGER, + }, + undefinedQualityPeriodsCount: { + type: Sequelize.INTEGER, + }, + notComputable: { + type: Sequelize.BOOLEAN, + allowNull: false, + defaultValue: false, + }, + invalidatedAt: { + type: Sequelize.DATE(3), + }, + }, { tableName: 'gaq_summaries' }); + + GaqSummary.removeAttribute('id'); + + GaqSummary.associate = (models) => { + GaqSummary.belongsTo(models.Run, { foreignKey: 'runNumber', as: 'run' }); + GaqSummary.belongsTo(models.DataPass, { foreignKey: 'dataPassId', as: 'dataPass' }); + }; + + return GaqSummary; +}; diff --git a/lib/database/models/index.js b/lib/database/models/index.js index 87d793fac3..2549209c5b 100644 --- a/lib/database/models/index.js +++ b/lib/database/models/index.js @@ -27,6 +27,7 @@ const EorReason = require('./eorreason'); const EpnRoleSession = require('./epnrolesession'); const FlpRole = require('./flprole'); const GaqDetector = require('./gaqDetector.js'); +const GaqSummary = require('./gaqSummary.js'); const Host = require('./host.js'); const LhcFill = require('./lhcFill'); const LhcFillStatistics = require('./lhcFillStatistics.js'); @@ -66,6 +67,7 @@ module.exports = (sequelize) => { EpnRoleSessionkey: EpnRoleSession(sequelize), FlpRole: FlpRole(sequelize), GaqDetector: GaqDetector(sequelize), + GaqSummary: GaqSummary(sequelize), Host: Host(sequelize), LhcFill: LhcFill(sequelize), LhcFillStatistics: LhcFillStatistics(sequelize), diff --git a/lib/database/repositories/GaqSummaryRepository.js b/lib/database/repositories/GaqSummaryRepository.js new file mode 100644 index 0000000000..81463d31c3 --- /dev/null +++ b/lib/database/repositories/GaqSummaryRepository.js @@ -0,0 +1,55 @@ +/** + * @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 { + models: { + GaqSummary, + }, +} = require('..'); +const Repository = require('./Repository'); + +/** + * GaqSummary repository + */ +class GaqSummaryRepository extends Repository { + /** + * Creates a new `GaqSummaryRepository` instance. + */ + constructor() { + super(GaqSummary); + } + + /** + * Mark the summary for a given (dataPassId, runNumber) as invalidated, creating the row if it does not yet exist + * + * @param {number} dataPassId data pass id + * @param {number} runNumber run number + * @return {Promise} resolves once the summary is invalidated + */ + async invalidate(dataPassId, runNumber) { + await this.upsert({ dataPassId, runNumber, invalidatedAt: new Date() }); + } + + /** + * Mark a list of summaries as invalidated in parallel + * + * @param {{ dataPassId: number, runNumber: number }[]} pairs the (dataPassId, runNumber) pairs to invalidate + * @return {Promise} resolves once all summaries are invalidated + */ + async invalidateMany(pairs) { + const invalidatedAt = new Date(); + await Promise.all(pairs.map(({ dataPassId, runNumber }) => this.upsert({ dataPassId, runNumber, invalidatedAt }))); + } +} + +module.exports = new GaqSummaryRepository(); diff --git a/lib/database/repositories/index.js b/lib/database/repositories/index.js index 0c79279752..4be7600145 100644 --- a/lib/database/repositories/index.js +++ b/lib/database/repositories/index.js @@ -27,6 +27,7 @@ const EnvironmentRepository = require('./EnvironmentRepository'); const EorReasonRepository = require('./EorReasonRepository'); const FlpRoleRepository = require('./FlpRoleRepository'); const GaqDetectorRepository = require('./GaqDetectorRepository.js'); +const GaqSummaryRepository = require('./GaqSummaryRepository.js'); const HostRepository = require('./HostRepository.js'); const LhcFillRepository = require('./LhcFillRepository'); const LhcFillStatisticsRepository = require('./LhcFillStatisticsRepository.js'); @@ -70,6 +71,7 @@ module.exports = { EorReasonRepository, FlpRoleRepository, GaqDetectorRepository, + GaqSummaryRepository, HostRepository, LhcFillRepository, LhcFillStatisticsRepository, diff --git a/lib/server/services/gaq/GaqDetectorsService.js b/lib/server/services/gaq/GaqDetectorsService.js index 1ac2a0dc7e..4f6d466503 100644 --- a/lib/server/services/gaq/GaqDetectorsService.js +++ b/lib/server/services/gaq/GaqDetectorsService.js @@ -12,7 +12,7 @@ */ const { gaqDetectorAdapter, detectorAdapter } = require('../../../database/adapters'); -const { GaqDetectorRepository, RunRepository, DetectorRepository } = require('../../../database/repositories'); +const { GaqDetectorRepository, RunRepository, DetectorRepository, GaqSummaryRepository } = require('../../../database/repositories'); const { BadParameterError } = require('../../errors/BadParameterError'); const { dataSource } = require('../../../database/DataSource.js'); const { Op } = require('sequelize'); @@ -57,6 +57,9 @@ class GaqDetectorService { .flatMap((runNumber) => detectorIds .map((detectorId) => ({ dataPassId, runNumber, detectorId }))); const createdEntries = await GaqDetectorRepository.insertAll(gaqEntries); + + await GaqSummaryRepository.invalidateMany(runNumbers.map((runNumber) => ({ dataPassId, runNumber }))); + return createdEntries.map(gaqDetectorAdapter.toEntity); }); } @@ -101,6 +104,9 @@ class GaqDetectorService { .flatMap(({ runNumber, detectors }) => detectors .map(({ id: detectorId }) => ({ dataPassId, runNumber, detectorId }))); const createdEntries = await GaqDetectorRepository.insertAll(gaqEntries); + + await GaqSummaryRepository.invalidateMany(runNumbers.map((runNumber) => ({ dataPassId, runNumber }))); + return createdEntries.map(gaqDetectorAdapter.toEntity); }, { transaction }); } diff --git a/lib/server/services/qualityControlFlag/QcFlagService.js b/lib/server/services/qualityControlFlag/QcFlagService.js index 3d34e1446b..b6a647290a 100644 --- a/lib/server/services/qualityControlFlag/QcFlagService.js +++ b/lib/server/services/qualityControlFlag/QcFlagService.js @@ -20,6 +20,7 @@ const { RunRepository, QcFlagVerificationRepository, QcFlagEffectivePeriodRepository, + GaqSummaryRepository, }, } = require('../../../database/index.js'); const { dataSource } = require('../../../database/DataSource.js'); @@ -209,6 +210,14 @@ class QcFlagService { ], }); + /** + * Invalidate GAQ summary for the dataPass and runNumber of the created flag. + * Skip when `verify` is true: verifyFlag() above already invalidated the same (dataPassId, runNumber). + */ + if (dataPass && !verify) { + await GaqSummaryRepository.invalidate(dataPass.id, runNumber); + } + createdFlags.push(qcFlagAdapter.toEntity(createdFlag)); } catch (error) { this._logger.warnMessage(`Failed to create QC flag with properties: ${JSON.stringify(qcFlag)}. Error: ${error}`); @@ -284,6 +293,12 @@ class QcFlagService { { const { id, from, to, origin, createdById, runNumber, dplDetectorId, flagTypeId, createdAt } = qcFlag; + + if (dataPassId) { + // Invalidate GAQ summary for the dataPass and runNumber of the deleted flag + await GaqSummaryRepository.invalidate(dataPassId, runNumber); + } + const qcFlagPropertiesToLog = { id, from, @@ -338,8 +353,8 @@ class QcFlagService { await QcFlagEffectivePeriodRepository.removeAll({ where: { id: { [Op.in]: effectivePeriodIds } } }); // Sequelize update requires a where and can't work only using association - const qcFlagIds = (await QcFlagRepository.findAll({ - attributes: ['id'], + const qcFlagIdsToRunNumbers = (await QcFlagRepository.findAll({ + attributes: ['id', 'runNumber'], include: { association: 'dataPasses', attributes: [], @@ -348,14 +363,21 @@ class QcFlagService { id: dataPassId, }, }, + where: { deleted: false }, raw: true, - })).map(({ id }) => id); + })).map(({ id, runNumber }) => ({ id, runNumber })); + + const qcFlagIds = qcFlagIdsToRunNumbers.map(({ id }) => id); + const runNumbers = new Set(qcFlagIdsToRunNumbers.map(({ runNumber }) => runNumber)); await QcFlagRepository.updateAll( { deleted: true }, { where: { id: qcFlagIds } }, ); + // Invalidate GAQ summary for the dataPass and all runNumbers of the deleted flags + await GaqSummaryRepository.invalidateMany(Array.from(runNumbers, (runNumber) => ({ dataPassId, runNumber }))); + return qcFlagIds.length; }); } @@ -389,7 +411,18 @@ class QcFlagService { createdById: user.id, }); - return await this.getOneOrFail(flagId); + const updatedQcFlag = await this.getOneOrFail(flagId); + + // Get data pass id for the flag (if exists) to invalidate only related GAQ summaries + const dataPassQcFlag = await DataPassQcFlagRepository.findOne({ where: { qualityControlFlagId: flagId } }); + const dataPassId = dataPassQcFlag?.dataPassId; + + // Invalidate GAQ summary if it's the first verification + if (dataPassId && (!qcFlag.verifications || qcFlag.verifications.length === 0)) { + await GaqSummaryRepository.invalidate(dataPassId, updatedQcFlag.runNumber); + } + + return updatedQcFlag; }, { transaction }); } diff --git a/lib/server/services/run/updateRun.js b/lib/server/services/run/updateRun.js index 2e8b50048c..f4b9f0e69a 100644 --- a/lib/server/services/run/updateRun.js +++ b/lib/server/services/run/updateRun.js @@ -12,6 +12,8 @@ */ const RunRepository = require('../../../database/repositories/RunRepository.js'); +const GaqSummaryRepository = require('../../../database/repositories/GaqSummaryRepository.js'); +const DataPassRunRepository = require('../../../database/repositories/DataPassRunRepository.js'); const { getRunOrFail } = require('./getRunOrFail.js'); const { utilities: { TransactionHelper } } = require('../../../database'); const { checkLhcFill } = require('../../../usecases/lhcFill/checkLhcFill.js'); @@ -35,6 +37,20 @@ const { logSpecificRunTag } = require('./logEntriesCreation/logSpecificRunTag.js */ const TAGS_TO_LOG = ['Not for physics']; +/** + * Run patch fields whose values feed the qc_time_start / qc_time_end virtual columns + * + * @type {string[]} + */ +const QC_TIME_SOURCE_FIELDS = [ + 'firstTfTimestamp', + 'lastTfTimestamp', + 'timeTrgStart', + 'timeTrgEnd', + 'timeO2Start', + 'timeO2End', +]; + /** * Update the given run * @@ -82,7 +98,12 @@ exports.updateRun = async (identifier, payload, transaction) => { } // Store the run quality to create a log if it changed - const previousRun = { runQuality: runModel?.runQuality, calibrationStatus: runModel?.calibrationStatus }; + const previousRun = { + runQuality: runModel?.runQuality, + calibrationStatus: runModel?.calibrationStatus, + qcTimeStart: runModel?.qcTimeStart, + qcTimeEnd: runModel?.qcTimeEnd, + }; runPatch.definition = runPatch.definition ?? getRunDefinition({ ...runModel.dataValues, @@ -204,6 +225,20 @@ exports.updateRun = async (identifier, payload, transaction) => { } } + // Only check for invalidation when the patch could have changed them + if (QC_TIME_SOURCE_FIELDS.some((field) => field in runPatch)) { + // Reload because qcTimeStart/qcTimeEnd are virtual columns not in the patch and stay stale after update + await runModel.reload(); + if (previousRun.qcTimeStart?.getTime() !== runModel.qcTimeStart?.getTime() + || previousRun.qcTimeEnd?.getTime() !== runModel.qcTimeEnd?.getTime()) { + const dataPassRuns = await DataPassRunRepository.findAll({ + attributes: ['dataPassId'], + where: { runNumber: runModel.runNumber }, + }); + await GaqSummaryRepository.invalidateMany(dataPassRuns.map(({ dataPassId }) => ({ dataPassId, runNumber: runModel.runNumber }))); + } + } + return runModel; }, { transaction }); }; diff --git a/test/api/qcFlags.test.js b/test/api/qcFlags.test.js index ece5d54c54..24e843d98e 100644 --- a/test/api/qcFlags.test.js +++ b/test/api/qcFlags.test.js @@ -1110,7 +1110,7 @@ module.exports = () => { .delete(`/api/qcFlags/perDataPass?dataPassId=${dataPassId}&token=${BkpRoles.DPG_ASYNC_QC_ADMIN}`); expect(response.status).to.be.equal(200); - expect(response.body.data.deletedCount).to.equal(11); // 9 from seeders, 2 created in POST requests previously in this test + expect(response.body.data.deletedCount).to.equal(10); // 9 from seeders, 2 created in POST requests previously in this test, 1 already soft-deleted }); }); diff --git a/test/lib/server/services/gaq/GaqSummary.test.js b/test/lib/server/services/gaq/GaqSummary.test.js new file mode 100644 index 0000000000..c681b62b93 --- /dev/null +++ b/test/lib/server/services/gaq/GaqSummary.test.js @@ -0,0 +1,140 @@ +/** + * @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 { expect } = require('chai'); +const { resetDatabaseContent } = require('../../../../utilities/resetDatabaseContent.js'); +const { repositories: { GaqSummaryRepository } } = require('../../../../../lib/database'); +const { qcFlagService } = require('../../../../../lib/server/services/qualityControlFlag/QcFlagService.js'); +const { gaqDetectorService } = require('../../../../../lib/server/services/gaq/GaqDetectorsService.js'); +const { updateRun } = require('../../../../../lib/server/services/run/updateRun.js'); + +module.exports = () => { + before(async () => { + await resetDatabaseContent(); + }); + + const relations = { user: { roles: ['admin'], externalUserId: 1 } }; + const dataPassId = 4; // LHC22a_apass2 + const runNumber = 56; + + /** + * Check whether an invalidation entry exists for a given data pass and run + * + * @param {number} expectedDataPassId + * @param {number} expectedRunNumber + * @param {boolean} toBeNull + * + * @return {Promise} + */ + const expectInvalidation = async (expectedDataPassId, expectedRunNumber, toBeNull = false) => { + const summary = await GaqSummaryRepository.findOne({ + where: { dataPassId: expectedDataPassId, runNumber: expectedRunNumber }, + }); + if (toBeNull) { + expect(summary?.invalidatedAt, `Expected no invalidation for dataPassId=${expectedDataPassId} runNumber=${expectedRunNumber}`).to.be.null; + } else { + expect(summary?.invalidatedAt, `Expected invalidation for dataPassId=${expectedDataPassId} runNumber=${expectedRunNumber}`).to.not.be.null; + } + }; + + describe("GAQ Summary Invalidation", async () => { + // Resetting the invalidated column between each case + afterEach(async () => { + await GaqSummaryRepository.updateAll({ invalidatedAt: null }, { where: {} }); + }); + + it('should invalidate GAQ summary when a QC flag is created for a data pass', async () => { + await qcFlagService.create( + [{ from: null, to: null, flagTypeId: 3 }], + { runNumber, detectorIdentifier: { detectorId: 7 }, dataPassIdentifier: { id: dataPassId } }, + relations, + ); + + await expectInvalidation(dataPassId, runNumber); + }); + + it('should invalidate GAQ summary when a QC flag is verified for the first time for a data pass', async () => { + const flagId = 8; // Seeded flag in data pass 4, run 100, with no verifications + + await qcFlagService.verifyFlag({ flagId }, relations); + + await expectInvalidation(dataPassId, 100); + + // Clear invalidation + await GaqSummaryRepository.updateAll({ invalidatedAt: null }, { where: { dataPassId, runNumber: 100 } }); + + // Verify again to check that no new invalidation is created when the flag is already verified + await qcFlagService.verifyFlag({ flagId }, relations); + await expectInvalidation(dataPassId, 100, true); + }); + + it('should invalidate GAQ summary when a QC flag is deleted for a data pass', async () => { + const flagId = 9; // Seeded flag in data pass 4, run 105, with no verifications (so deletion is allowed) + await qcFlagService.delete(flagId); + + await expectInvalidation(dataPassId, 105); + }); + + it('should invalidate GAQ summary for all runs when all QC flags are deleted for a data pass', async () => { + await qcFlagService.deleteAllForDataPass(dataPassId); + + await expectInvalidation(dataPassId, 100); + }); + + it('should invalidate GAQ summary when GAQ detectors are explicitly set for a data pass and run', async () => { + const gaqDataPassId = 3; // LHC22a_apass1 (has run 56 and detectors set up in GaqDetectorService tests) + const detectorIds = [4, 7]; + + await gaqDetectorService.setGaqDetectors(gaqDataPassId, [runNumber], detectorIds); + + await expectInvalidation(gaqDataPassId, runNumber); + }); + + it('should invalidate GAQ summary when default GAQ detectors are used for a data pass and run', async () => { + const gaqDataPassId = 3; + + await gaqDetectorService.useDefaultGaqDetectors(gaqDataPassId, [runNumber]); + + await expectInvalidation(gaqDataPassId, runNumber); + }); + + it('should invalidate GAQ summary when the QC time of a run changes', async () => { + await updateRun( + { runNumber }, + { runPatch: { timeTrgStart: new Date('2019-08-08 20:30:00') } }, + ); + + await expectInvalidation(dataPassId, runNumber); + }); + + it('should NOT invalidate GAQ summary when updateRun patch does not touch QC time source fields', async () => { + await updateRun( + { runNumber }, + { runPatch: { definition: 'PHYSICS' } }, + ); + + await expectInvalidation(dataPassId, runNumber, true); + }); + + it('should NOT invalidate GAQ summary when a QC time source field is patched but qcTimeStart/qcTimeEnd do not change', async () => { + // Run 56 has a non-null time_trg_start, so qc_time_start resolves to time_trg_start regardless of time_o2_start. + // Patching only time_o2_start moves a source field but does not change the virtual qcTimeStart / qcTimeEnd, so no invalidation should fire. + await updateRun( + { runNumber }, + { runPatch: { timeO2Start: new Date('2019-08-08 18:00:00') } }, + ); + + await expectInvalidation(dataPassId, runNumber, true); + }); + }); +}; diff --git a/test/lib/server/services/gaq/index.js b/test/lib/server/services/gaq/index.js index 985f80fe84..93e10b2ac6 100644 --- a/test/lib/server/services/gaq/index.js +++ b/test/lib/server/services/gaq/index.js @@ -12,7 +12,9 @@ */ const GaqDetectorServiceSuite = require('./GaqDetectorService.test.js'); +const GaqSummarySuite = require('./GaqSummary.test.js'); module.exports = () => { describe('GaqDetectorService', GaqDetectorServiceSuite); + describe('GaqSummary', GaqSummarySuite); }; diff --git a/test/lib/server/services/index.js b/test/lib/server/services/index.js index 01df33fcaf..38aabbf6dd 100644 --- a/test/lib/server/services/index.js +++ b/test/lib/server/services/index.js @@ -32,7 +32,7 @@ const UserSuite = require('./user/index.js'); const SimulationPassesSuite = require('./simulationPasses/index.js'); const QcFlagsSuite = require('./qualityControlFlag/index.js'); const CtpTriggerCountersSuite = require('./ctpTriggerCounters/index.js'); -const GaqDetectorSuite = require('./gaq'); +const GaqSuite = require('./gaq'); module.exports = () => { before(resetDatabaseContent); @@ -46,7 +46,7 @@ module.exports = () => { describe('Environment history item', EnvironmentHistoryItemSuite); describe('EOS report', EosReportSuite); describe('Flp role', FlpRoleSuite); - describe('GaqDetector', GaqDetectorSuite); + describe('GAQ', GaqSuite); describe('LHC fill suite', LhcFillSuite); describe('Logs', LogSuite); describe('RunType', RunTypeSuite);