From 78fd0ea41f558a08bd1e75e8c2b81621ec1eba93 Mon Sep 17 00:00:00 2001 From: hehoon <100522372+hehoon@users.noreply.github.com> Date: Sun, 30 Nov 2025 16:00:49 +0100 Subject: [PATCH 01/15] Add a unique error message for objects in ccdb not found due to filters --- .../lib/services/ccdb/CcdbService.js | 19 +++++++++--- .../test/lib/services/CcdbService.test.js | 30 +++++++++++++++++++ 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/QualityControl/lib/services/ccdb/CcdbService.js b/QualityControl/lib/services/ccdb/CcdbService.js index 2b2fa3a7c..fe3519ec5 100644 --- a/QualityControl/lib/services/ccdb/CcdbService.js +++ b/QualityControl/lib/services/ccdb/CcdbService.js @@ -176,7 +176,7 @@ export class CcdbService { * ``` * @param {CcdbObjectIdentification} partialIdentification - fields such as path, validFrom, etc. * @returns {Promise.} - returns object full identification - * @throws {Error} + * @throws {Error} throws if the object cannot be found */ async getObjectIdentification(partialIdentification) { const headers = { @@ -195,7 +195,10 @@ export class CcdbService { [CCDB_RESPONSE_BODY_KEYS.ID]: qcObject[CCDB_RESPONSE_BODY_KEYS.ID], }; } else { - throw new Error(`Object: ${url} could not be found`); + const baseMessage = `Object at '${url}' could not be found.`; + const hasFilters = partialIdentification?.filters && Object.keys(partialIdentification.filters).length > 0; + const finalMessage = hasFilters ? `${baseMessage} It was likely excluded by the applied filters.` : baseMessage; + throw new Error(finalMessage); } } @@ -226,7 +229,12 @@ export class CcdbService { .split(', ') .filter((location) => !location.startsWith('alien') && !location.startsWith('file')); if (!location) { - throw new Error(`No location provided by CCDB for object with path: ${path}`); + const baseMessage = `No location provided by CCDB for object with path: ${path}`; + const hasFilters = identification?.filters && Object.keys(identification.filters).length > 0; + const finalMessage = hasFilters + ? `${baseMessage}. It was likely excluded by the applied filters.` + : baseMessage; + throw new Error(finalMessage); } return { ...headers, @@ -234,7 +242,10 @@ export class CcdbService { path, }; } else { - throw new Error(`Unable to retrieve object: ${path} due to status: ${status}`); + const baseMessage = `Unable to retrieve object: ${path} due to status: ${status}`; + const hasFilters = identification?.filters && Object.keys(identification.filters).length > 0; + const finalMessage = hasFilters ? `${baseMessage}. It was likely excluded by the applied filters.` : baseMessage; + throw new Error(finalMessage); } } diff --git a/QualityControl/test/lib/services/CcdbService.test.js b/QualityControl/test/lib/services/CcdbService.test.js index f01725f3f..4bbb667ab 100644 --- a/QualityControl/test/lib/services/CcdbService.test.js +++ b/QualityControl/test/lib/services/CcdbService.test.js @@ -471,5 +471,35 @@ export const ccdbServiceTestSuite = async () => { strictEqual(ccdb._buildCcdbUrlPath(identification), '/qc/TPC/object/12322222/123332323/123-ffg/RunNumber=123456/PartName=Pass'); }); }); + + suite('CcdbService getObjectIdentification() Error Messages', () => { + let ccdb = null; + + before(() => { + ccdb = new CcdbService(ccdbConfig); + }); + + test('should throw error if object not found with filters applied', async () => { + nock('http://ccdb-local:8083') + .get(/.*/) + .reply(200, { objects: [] }); + + await rejects( + async () => ccdb.getObjectIdentification({ path: 'qc-test', filters: { RunNumber: 123456 } }), + /It was likely excluded by the applied filters/i, + ); + }); + + test('should throw error if object not found without filters', async () => { + nock('http://ccdb-local:8083') + .get(/.*/) + .reply(200, { objects: [] }); + + await rejects( + async () => ccdb.getObjectIdentification({ path: 'qc-test' }), + /Object at '\/latest\/qc-test' could not be found\.$/i, + ); + }); + }); }); }; From bff89b017e122b05c171891975e348fb74a2a0a8 Mon Sep 17 00:00:00 2001 From: hehoon <100522372+hehoon@users.noreply.github.com> Date: Sun, 30 Nov 2025 16:25:11 +0100 Subject: [PATCH 02/15] Add a failure callback to draw.js that passes the error as its first parameter --- QualityControl/public/common/object/draw.js | 61 ++++++++++++------- QualityControl/public/layout/view/page.js | 4 +- .../layout/view/panels/objectTreeSidebar.js | 8 ++- QualityControl/public/object/QCObject.js | 5 +- .../public/object/objectTreePage.js | 4 +- 5 files changed, 54 insertions(+), 28 deletions(-) diff --git a/QualityControl/public/common/object/draw.js b/QualityControl/public/common/object/draw.js index e622cb427..b51acdcfa 100644 --- a/QualityControl/public/common/object/draw.js +++ b/QualityControl/public/common/object/draw.js @@ -26,21 +26,21 @@ import { keyedTimerDebouncer, pointerId } from '../utils.js'; * - `Loading`: returns a loading placeholder. * - `Failure`: returns an error box with the error message. * - `Success`: draws the object using `drawObject`. - * @param {QCObject} qcObjectModel - the QCObject model - * @param {string} objectName - the name of the QC object to draw + * @param {RemoteData} remoteData - the RemoteData object containing {qcObject, info, timestamps} * @param {object} options - optional options of presentation * @param {string[]} drawingOptions - optional drawing options to be used + * @param {(Error) => void} failFn - optional function to execute upon drawing failure * @returns {vnode} output virtual-dom, a single div with JSROOT attached to it */ -export const draw = (qcObjectModel, objectName, options = {}, drawingOptions = []) => - qcObjectModel.objects[objectName]?.match({ +export const draw = (remoteData, options = {}, drawingOptions = [], failFn = () => {}) => + remoteData?.match({ NotAsked: () => null, Loading: () => h('.flex-column.items-center.justify-center', [h('.animate-slow-appearance', 'Loading')]), Failure: (error) => h('.error-box.danger.flex-column.justify-center.f6.text-center', {}, [ h('span.error-icon', { title: 'Error' }, iconWarning()), h('span', error), ]), - Success: (data) => drawObject(data, options, drawingOptions, qcObjectModel), + Success: (data) => drawObject(data, options, drawingOptions, failFn), }); /** @@ -50,11 +50,11 @@ export const draw = (qcObjectModel, objectName, options = {}, drawingOptions = [ * @param {JSON} object - {qcObject, info, timestamps} * @param {object} options - optional options of presentation * @param {string[]} drawingOptions - optional drawing options to be used - * @param {QCObject} qcObjectModel - the QCObject model, used to invalidate (failure) RemoteData + * @param {(Error) => void} failFn - optional function to execute upon drawing failure * @returns {vnode} output virtual-dom, a single div with JSROOT attached to it */ -export const drawObject = (object, options = {}, drawingOptions = [], qcObjectModel = undefined) => { - const { qcObject, name, etag } = object; +export const drawObject = (object, options = {}, drawingOptions = [], failFn = () => {}) => { + const { qcObject, etag } = object; const { root } = qcObject; if (isObjectOfTypeChecker(root)) { return checkersPanel(root); @@ -79,18 +79,18 @@ export const drawObject = (object, options = {}, drawingOptions = [], qcObjectMo oncreate: (vnode) => { // Setup resize function vnode.dom.onresize = () => { - redrawOnSizeUpdate(vnode.dom, root, drawingOptions); + redrawOnSizeUpdate(vnode.dom, root, drawingOptions, failFn); }; // Resize on window size change window.addEventListener('resize', vnode.dom.onresize); - drawOnCreate(vnode.dom, root, drawingOptions, qcObjectModel, name); + drawOnCreate(vnode.dom, root, drawingOptions, failFn); }, onupdate: (vnode) => { const isRedrawn = redrawOnDataUpdate(vnode.dom, root, drawingOptions); if (!isRedrawn) { - redrawOnSizeUpdate(vnode.dom, root, drawingOptions); + redrawOnSizeUpdate(vnode.dom, root, drawingOptions, failFn); } }, onremove: (vnode) => { @@ -114,23 +114,27 @@ export const drawObject = (object, options = {}, drawingOptions = [], qcObjectMo * @param {HTMLElement} dom - the div containing jsroot plot * @param {object} root - root object in JSON representation * @param {string[]} drawingOptions - list of options to be used for drawing object - * @param {QCObject} qcObjectModel - the QCObject model - * @param {string} objectName - the name of the QC object to draw + * @param {(Error) => void} failFn - function to execute upon drawing failure * @throws {EvalError} If CSP disallows 'unsafe-eval'. * This is typically called when the drawing is incomplete or malformed. * @returns {undefined} */ -const drawOnCreate = async (dom, root, drawingOptions, qcObjectModel, objectName) => { +const drawOnCreate = async (dom, root, drawingOptions, failFn) => { const finalDrawingOptions = generateDrawingOptionString(root, drawingOptions); JSROOT.draw(dom, root, finalDrawingOptions).then((painter) => { if (painter === null) { // eslint-disable-next-line no-console console.error('null painter in JSROOT'); + if (typeof failFn === 'function') { + failFn(new Error('null painter in JSROOT')); + } } }).catch((error) => { // eslint-disable-next-line no-console console.error(error); - qcObjectModel?.invalidObject(objectName); + if (typeof failFn === 'function') { + failFn(error); + } }); dom.dataset.fingerprintRedraw = fingerprintResize(dom.clientWidth, dom.clientHeight); dom.dataset.fingerprintData = fingerprintData(root, drawingOptions); @@ -156,11 +160,12 @@ const drawOnCreate = async (dom, root, drawingOptions, qcObjectModel, objectName * @param {Model} model - Root model of the application * @param {HTMLElement} dom - Element containing the JSROOT plot * @param {TabObject} tabObject - Object describing the graph to redraw inside `dom` + * @param {(Error) => void} failFn - Function to execute upon drawing failure * @returns {undefined} */ const redrawOnSizeUpdate = keyedTimerDebouncer( (_, dom) => dom, - (dom, root, drawingOptions) => { + (dom, root, drawingOptions, failFn) => { let previousFingerprint = dom.dataset.fingerprintResize; const intervalId = setInterval(() => { @@ -175,7 +180,7 @@ const redrawOnSizeUpdate = keyedTimerDebouncer( // Size stable across intervals (safe to redraw) if (dom.dataset.fingerprintResize !== currentFingerprint) { - redraw(dom, root, drawingOptions); + redraw(dom, root, drawingOptions, failFn); } clearInterval(intervalId); @@ -187,10 +192,10 @@ const redrawOnSizeUpdate = keyedTimerDebouncer( }, 50); }, 200, - (dom, root, drawingOptions) => { + (dom, root, drawingOptions, failFn) => { const resizeFingerprint = fingerprintResize(dom.clientWidth, dom.clientHeight); if (dom.dataset.fingerprintResize !== resizeFingerprint) { - redraw(dom, root, drawingOptions); + redraw(dom, root, drawingOptions, failFn); } }, ); @@ -202,12 +207,13 @@ const redrawOnSizeUpdate = keyedTimerDebouncer( * @param {HTMLElement} dom - Target element containing the JSROOT graph. * @param {object} root - JSROOT-compatible data object to be rendered. * @param {string[]} drawingOptions - Initial or user-provided drawing options. + * @param {(Error) => void} failFn - Function to execute upon drawing failure * @returns {boolean} whether the JSROOT plot was redrawn */ -const redrawOnDataUpdate = (dom, root, drawingOptions) => { +const redrawOnDataUpdate = (dom, root, drawingOptions, failFn) => { const dataFingerprint = fingerprintData(root, drawingOptions); if (dom.dataset.fingerprintData !== dataFingerprint) { - redraw(dom, root, drawingOptions); + redraw(dom, root, drawingOptions, failFn); return true; } return false; @@ -218,14 +224,23 @@ const redrawOnDataUpdate = (dom, root, drawingOptions) => { * @param {HTMLElement} dom - Target element containing the JSROOT graph. * @param {object} root - JSROOT-compatible data object to be rendered. * @param {string[]} drawingOptions - Initial or user-provided drawing options. + * @param {(Error) => void} failFn - Function to execute upon drawing failure * @returns {undefined} */ -const redraw = (dom, root, drawingOptions) => { +const redraw = (dom, root, drawingOptions, failFn) => { // A bug exists in JSROOT where the cursor gets stuck on `wait` when redrawing multiple objects simultaneously. // We save the current cursor state here and revert back to it after redrawing is complete. const currentCursor = document.body.style.cursor; const finalDrawingOptions = generateDrawingOptionString(root, drawingOptions); - JSROOT.redraw(dom, root, finalDrawingOptions); + try { + JSROOT.redraw(dom, root, finalDrawingOptions); + } catch (error) { + // eslint-disable-next-line no-console + console.error(error); + if (typeof failFn === 'function') { + failFn(error); + } + } document.body.style.cursor = currentCursor; }; diff --git a/QualityControl/public/layout/view/page.js b/QualityControl/public/layout/view/page.js index 4e72784f3..24710c746 100644 --- a/QualityControl/public/layout/view/page.js +++ b/QualityControl/public/layout/view/page.js @@ -207,7 +207,9 @@ const drawComponent = (model, tabObject) => { display: 'flex', 'flex-direction': 'column', }, - }, draw(model.object, tabObject.name)), + }, draw(model.object.objects[tabObject.name], {}, [], (error) => { + model.object.invalidObject(tabObject.name, error.message); + })), objectInfoResizePanel(model, tabObject), displayTimestamp && minimalObjectInfo(runNumber, lastModified), ]); diff --git a/QualityControl/public/layout/view/panels/objectTreeSidebar.js b/QualityControl/public/layout/view/panels/objectTreeSidebar.js index 28f2f7033..ba9b06236 100644 --- a/QualityControl/public/layout/view/panels/objectTreeSidebar.js +++ b/QualityControl/public/layout/view/panels/objectTreeSidebar.js @@ -193,7 +193,13 @@ const leafRow = (model, sideTree, level) => { const objectPreview = (model) => { const isSelected = model.object.selected; if (isSelected) { - return isSelected && h('.bg-white', { style: 'height: 20em' }, draw(model.object, model.object.selected.name)); + return isSelected && h( + '.bg-white', + { style: 'height: 20em' }, + draw(model.object.objects[model.object.selected.name], {}, [], (error) => { + model.object.invalidObject(model.object.selected.name, error.message); + }), + ); } return null; }; diff --git a/QualityControl/public/object/QCObject.js b/QualityControl/public/object/QCObject.js index c84b55195..1e12276c1 100644 --- a/QualityControl/public/object/QCObject.js +++ b/QualityControl/public/object/QCObject.js @@ -343,10 +343,11 @@ export default class QCObject extends BaseViewModel { /** * Indicate that the object loaded is wrong. Used after trying to print it with jsroot * @param {string} name - name of the object + * @param {string} reason - the reason for invalidating the object * @returns {undefined} */ - invalidObject(name) { - this.objects[name] = RemoteData.failure('JSROOT was unable to draw this object'); + invalidObject(name, reason) { + this.objects[name] = RemoteData.failure(reason || 'JSROOT was unable to draw this object'); this.notify(); } diff --git a/QualityControl/public/object/objectTreePage.js b/QualityControl/public/object/objectTreePage.js index aa0476f3a..5fd8c76d5 100644 --- a/QualityControl/public/object/objectTreePage.js +++ b/QualityControl/public/object/objectTreePage.js @@ -121,7 +121,9 @@ const drawPlot = (model, object) => { iconCircleX(), ), ]), - h('', { style: 'height:77%;' }, draw(model.object, name, { stat: true })), + h('', { style: 'height:77%;' }, draw(model.object.objects[name], { stat: true }, [], (error) => { + model.object.invalidObject(name, error.message); + })), h('.scroll-y', {}, [ h('.w-100.flex-row', { style: 'justify-content: center' }, h('.w-80', timestampSelectForm(model))), qcObjectInfoPanel(object, { 'font-size': '.875rem;' }, defaultRowAttributes(model.notification)), From d8a2579c0e5820fdf5aac5cf24333f7a72e49e16 Mon Sep 17 00:00:00 2001 From: hehoon <100522372+hehoon@users.noreply.github.com> Date: Sun, 30 Nov 2025 16:25:53 +0100 Subject: [PATCH 03/15] Display JSROOT drawing errors for the object view page --- .../public/pages/objectView/ObjectViewModel.js | 9 +++++++++ QualityControl/public/pages/objectView/ObjectViewPage.js | 4 +++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/QualityControl/public/pages/objectView/ObjectViewModel.js b/QualityControl/public/pages/objectView/ObjectViewModel.js index 088b3cf2f..c80ae469f 100644 --- a/QualityControl/public/pages/objectView/ObjectViewModel.js +++ b/QualityControl/public/pages/objectView/ObjectViewModel.js @@ -195,4 +195,13 @@ export default class ObjectViewModel extends BaseViewModel { async triggerFilter() { await this.init(this.model.router.params); } + + /** + * Should be called when a failure occurs when drawing a JSROOT plot + * @param {string} message - the failure message to display + */ + drawingFailureOccurred(message) { + this.selected = RemoteData.failure(message || 'Failed to draw JSROOT plot'); + this.notify(); + } } diff --git a/QualityControl/public/pages/objectView/ObjectViewPage.js b/QualityControl/public/pages/objectView/ObjectViewPage.js index ec9fba3bc..b296c716c 100644 --- a/QualityControl/public/pages/objectView/ObjectViewPage.js +++ b/QualityControl/public/pages/objectView/ObjectViewPage.js @@ -82,7 +82,9 @@ const objectPlotAndInfo = (objectViewModel) => h('.flex-grow', { // Key change forces redraw when toggling info panel key: isObjectInfoVisible ? 'objectPlotWithoutInfoPanel' : 'objectPlotWithInfoPanel', - }, drawObject(qcObject, {}, drawingOptions)), + }, drawObject(qcObject, {}, drawingOptions, (error) => { + objectViewModel.drawingFailureOccurred(error.message); + })), isObjectInfoVisible && h('.scroll-y.w-30', { key: 'objectInfoPanel', }, [ From 5032a428806f3889ce0f8bdaea9c8b9c33646686 Mon Sep 17 00:00:00 2001 From: hehoon <100522372+hehoon@users.noreply.github.com> Date: Sun, 30 Nov 2025 16:26:53 +0100 Subject: [PATCH 04/15] Get any errors from the backend and display them on the frontend --- .../public/services/QCObject.service.js | 20 +++++++++++-------- .../object-view-from-object-tree.test.js | 4 ++-- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/QualityControl/public/services/QCObject.service.js b/QualityControl/public/services/QCObject.service.js index d4ef8e353..31283c8e3 100644 --- a/QualityControl/public/services/QCObject.service.js +++ b/QualityControl/public/services/QCObject.service.js @@ -91,16 +91,18 @@ export default class QCObjectService { that.notify(); return RemoteData.success(result); } else { - this.objectsLoadedMap[objectName] = RemoteData.failure(`404: Object "${objectName}" could not be found.`); + const failure = RemoteData.failure(result.message || `Object "${objectName}" could not be found.`); + this.objectsLoadedMap[objectName] = failure; that.notify(); - return RemoteData.failure(`404: Object "${objectName}" could not be found.`); + return failure; } } catch (error) { // eslint-disable-next-line no-console console.error(error); - this.objectsLoadedMap[objectName] = RemoteData.failure(`404: Object "${objectName}" could not be loaded.`); + const failure = RemoteData.failure(error.message || `Object "${objectName}" could not be loaded.`); + this.objectsLoadedMap[objectName] = failure; that.notify(); - return RemoteData.failure(`Object '${objectName}' could not be loaded`); + return failure; } } @@ -131,16 +133,18 @@ export default class QCObjectService { that.notify(); return RemoteData.success(result); } else { - this.objectsLoadedMap[objectId] = RemoteData.failure(`404: Object with ID: "${objectId}" could not be found.`); + const failure = RemoteData.failure(result.message || `Object with ID "${objectId}" could not be found.`); + this.objectsLoadedMap[objectId] = failure; that.notify(); - return RemoteData.failure(`404: Object with ID:"${objectId}" could not be found.`); + return failure; } } catch (error) { // eslint-disable-next-line no-console console.error(error); - this.objectsLoadedMap[objectId] = RemoteData.failure(`404: Object with ID: "${objectId}" could not be loaded.`); + const failure = RemoteData.failure(error.message || `Object with ID "${objectId}" could not be loaded.`); + this.objectsLoadedMap[objectId] = failure; that.notify(); - return RemoteData.failure(`Object with ID:"${objectId}" could not be loaded`); + return failure; } } diff --git a/QualityControl/test/public/pages/object-view-from-object-tree.test.js b/QualityControl/test/public/pages/object-view-from-object-tree.test.js index 9a73b9540..c4633141a 100644 --- a/QualityControl/test/public/pages/object-view-from-object-tree.test.js +++ b/QualityControl/test/public/pages/object-view-from-object-tree.test.js @@ -11,7 +11,7 @@ * or submit itself to any jurisdiction. */ -import { strictEqual } from 'node:assert'; +import { match, strictEqual } from 'node:assert'; const OBJECT_VIEW_PAGE_PARAM = '?page=objectView'; export const objectViewFromObjectTreeTests = async (url, page, timeout = 5000, testParent) => { @@ -63,7 +63,7 @@ export const objectViewFromObjectTreeTests = async (url, page, timeout = 5000, t (element) => document.querySelector(element).textContent, errorMessageElement, ); - strictEqual(message, `404: Object "${objectName}" could not be found.`); + match(message, /^Request to server failed/i); }, ); From d9ecd2a330c71096005f63c7abd03a4a20b58f31 Mon Sep 17 00:00:00 2001 From: hehoon <100522372+hehoon@users.noreply.github.com> Date: Mon, 1 Dec 2025 13:58:27 +0100 Subject: [PATCH 05/15] Add test to verify error display when JSROOT object fetch fails due to network failure --- .../object-view-from-layout-show.test.js | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/QualityControl/test/public/pages/object-view-from-layout-show.test.js b/QualityControl/test/public/pages/object-view-from-layout-show.test.js index fd32ebda1..5c48aa867 100644 --- a/QualityControl/test/public/pages/object-view-from-layout-show.test.js +++ b/QualityControl/test/public/pages/object-view-from-layout-show.test.js @@ -273,4 +273,41 @@ export const objectViewFromLayoutShowTests = async (url, page, timeout = 5000, t strictEqual(redrawn, true, 'JSRoot drawing was not redrawn on visibility toggle'); }, ); + + await testParent.test( + 'should display an error when the JSROOT object fails to fetch due to a network failure', + { timeout: 10000 }, + async () => { + // Enable request interception for this test + await page.setRequestInterception(true); + + // Define request handler scoped to this test + const requestHandler = (interceptedRequest) => { + const url = interceptedRequest.url(); + + // Abort only the API request for JSRoot object + if (url.includes('/api/object')) { + interceptedRequest.abort('failed'); // simulates network failure + } else { + interceptedRequest.continue(); + } + }; + + try { + // Attach the handler + page.on('request', requestHandler); + + await page.reload({ waitUntil: 'networkidle0' }); + await delay(1000); + + const errorText = await page.evaluate(() => document.querySelector('#Error .f3').innerText); + + strictEqual(errorText, 'Connection to server failed, please try again'); + } finally { + // Cleanup: remove listener and disable interception + page.off('request', requestHandler); + await page.setRequestInterception(false); + } + }, + ); }; From dfbb9250f6e245bb9d054c06248dd5fba542c5be Mon Sep 17 00:00:00 2001 From: hehoon <100522372+hehoon@users.noreply.github.com> Date: Mon, 1 Dec 2025 15:30:00 +0100 Subject: [PATCH 06/15] Add _buildFilterErrorMessage and catch errors from httpGetJson --- .../lib/services/ccdb/CcdbService.js | 86 +++++++++++++------ 1 file changed, 61 insertions(+), 25 deletions(-) diff --git a/QualityControl/lib/services/ccdb/CcdbService.js b/QualityControl/lib/services/ccdb/CcdbService.js index fe3519ec5..b6f567285 100644 --- a/QualityControl/lib/services/ccdb/CcdbService.js +++ b/QualityControl/lib/services/ccdb/CcdbService.js @@ -185,21 +185,32 @@ export class CcdbService { }; const url = `/latest${this._buildCcdbUrlPath(partialIdentification)}`; - const result = await httpGetJson(this._hostname, this._port, url, { headers }); - if (result?.objects?.length > 0) { - const [qcObject] = result.objects; - return { - [CCDB_RESPONSE_BODY_KEYS.PATH]: qcObject[CCDB_RESPONSE_BODY_KEYS.PATH], - [CCDB_RESPONSE_BODY_KEYS.VALID_FROM]: qcObject[CCDB_RESPONSE_BODY_KEYS.VALID_FROM], - [CCDB_RESPONSE_BODY_KEYS.VALID_UNTIL]: qcObject[CCDB_RESPONSE_BODY_KEYS.VALID_UNTIL], - [CCDB_RESPONSE_BODY_KEYS.ID]: qcObject[CCDB_RESPONSE_BODY_KEYS.ID], - }; - } else { - const baseMessage = `Object at '${url}' could not be found.`; - const hasFilters = partialIdentification?.filters && Object.keys(partialIdentification.filters).length > 0; - const finalMessage = hasFilters ? `${baseMessage} It was likely excluded by the applied filters.` : baseMessage; - throw new Error(finalMessage); + let result = null; + try { + result = await httpGetJson(this._hostname, this._port, url, { headers }); + } catch { + const errorMessage = this._buildFilterErrorMessage( + `Object at url '${url}' and path '${PATH}' could not be found.`, + partialIdentification.filters, + ); + throw new Error(errorMessage); } + + if (!result?.objects?.length) { + const errorMessage = this._buildFilterErrorMessage( + `Object at url '${url}' and path '${PATH}' could not be found.`, + partialIdentification.filters, + ); + throw new Error(errorMessage); + } + + const [qcObject] = result.objects; + return { + [CCDB_RESPONSE_BODY_KEYS.PATH]: qcObject[CCDB_RESPONSE_BODY_KEYS.PATH], + [CCDB_RESPONSE_BODY_KEYS.VALID_FROM]: qcObject[CCDB_RESPONSE_BODY_KEYS.VALID_FROM], + [CCDB_RESPONSE_BODY_KEYS.VALID_UNTIL]: qcObject[CCDB_RESPONSE_BODY_KEYS.VALID_UNTIL], + [CCDB_RESPONSE_BODY_KEYS.ID]: qcObject[CCDB_RESPONSE_BODY_KEYS.ID], + }; } /** @@ -218,7 +229,7 @@ export class CcdbService { * @throws {Error} */ async getObjectDetails(identification) { - const { path = '', validFrom = undefined } = identification ?? {}; + const { path = '', filters, validFrom = undefined } = identification ?? {}; if (!path || !validFrom) { throw new Error('Missing mandatory parameters: path & validFrom'); } @@ -229,12 +240,11 @@ export class CcdbService { .split(', ') .filter((location) => !location.startsWith('alien') && !location.startsWith('file')); if (!location) { - const baseMessage = `No location provided by CCDB for object with path: ${path}`; - const hasFilters = identification?.filters && Object.keys(identification.filters).length > 0; - const finalMessage = hasFilters - ? `${baseMessage}. It was likely excluded by the applied filters.` - : baseMessage; - throw new Error(finalMessage); + const errorMessage = this._buildFilterErrorMessage( + `No location provided by CCDB for object with path: ${path}`, + filters, + ); + throw new Error(errorMessage); } return { ...headers, @@ -242,10 +252,11 @@ export class CcdbService { path, }; } else { - const baseMessage = `Unable to retrieve object: ${path} due to status: ${status}`; - const hasFilters = identification?.filters && Object.keys(identification.filters).length > 0; - const finalMessage = hasFilters ? `${baseMessage}. It was likely excluded by the applied filters.` : baseMessage; - throw new Error(finalMessage); + const errorMessage = this._buildFilterErrorMessage( + `Unable to retrieve object: ${path} due to status: ${status}`, + filters, + ); + throw new Error(errorMessage); } } @@ -353,4 +364,29 @@ export class CcdbService { } return url; } + + /** + * Builds a detailed error message for CCDB objects that may have been filtered out. + * This method appends a filter-specific hint to a base error message when + * the `filters` object contains one or more keys. It ensures proper punctuation + * and provides a clear explanation for why the object might not have been found. + * @param {string} baseMessage - The initial error message describing the failure. + * @param {object} [filters] - Optional object representing filters applied when searching for the object. + * @returns {string} - The final, human-readable error message including filter hints if applicable. + */ + _buildFilterErrorMessage(baseMessage, filters) { + // Only append filter-specific hint if filters object exists and has keys + if (filters && Object.keys(filters).length > 0) { + // Ensure the base message ends with proper punctuation. + // If it does NOT end with any Unicode punctuation, append a period. + if (/\p{P}$/u.test(baseMessage)) { + baseMessage += '.'; + } + + // Append a clear, descriptive filter hint. + baseMessage += 'It was likely excluded by the applied filters.'; + } + + return baseMessage; + } } From d43cee7f95700f8eec420be76868047d0625b3ba Mon Sep 17 00:00:00 2001 From: hehoon <100522372+hehoon@users.noreply.github.com> Date: Mon, 1 Dec 2025 15:30:11 +0100 Subject: [PATCH 07/15] Refactor tests and replaced overly broad .get(/.*/) with something more precise --- .../test/api/objects/api-get-object.test.js | 6 +++++- .../test/lib/services/CcdbService.test.js | 14 ++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/QualityControl/test/api/objects/api-get-object.test.js b/QualityControl/test/api/objects/api-get-object.test.js index 1c6107531..2686e1f56 100644 --- a/QualityControl/test/api/objects/api-get-object.test.js +++ b/QualityControl/test/api/objects/api-get-object.test.js @@ -48,7 +48,11 @@ export const apiGetObjectsTests = () => { test('should return 500 if service fails to retrieve object', async () => { const url = `${URL_ADDRESS}/api/object?token=${OWNER_TEST_TOKEN}&path=invalid/path`; - await testResult(url, 500, { message: 'Non-2xx status code: 501', status: 500, title: 'Unknown Error' }); + await testResult(url, 500, { + message: 'Object at url \'/latest/invalid/path\' and path \'path\' could not be found.', + status: 500, + title: 'Unknown Error', + }); }); }); diff --git a/QualityControl/test/lib/services/CcdbService.test.js b/QualityControl/test/lib/services/CcdbService.test.js index 4bbb667ab..ae5f509b7 100644 --- a/QualityControl/test/lib/services/CcdbService.test.js +++ b/QualityControl/test/lib/services/CcdbService.test.js @@ -474,30 +474,32 @@ export const ccdbServiceTestSuite = async () => { suite('CcdbService getObjectIdentification() Error Messages', () => { let ccdb = null; + let CCDB_URL_HEALTH_POINT = ''; before(() => { ccdb = new CcdbService(ccdbConfig); + CCDB_URL_HEALTH_POINT = `/monitor/${CCDB_MONITOR}/.*/${CCDB_VERSION_KEY}`; }); test('should throw error if object not found with filters applied', async () => { nock('http://ccdb-local:8083') - .get(/.*/) + .get(CCDB_URL_HEALTH_POINT) .reply(200, { objects: [] }); await rejects( - async () => ccdb.getObjectIdentification({ path: 'qc-test', filters: { RunNumber: 123456 } }), - /It was likely excluded by the applied filters/i, + async () => ccdb.getObjectIdentification({ path: PATH, id: ID, filters: { RunNumber: 123456 } }), + `Object at url '${ID}' and path ${PATH} could not be found. It was likely excluded by the applied filters.`, ); }); test('should throw error if object not found without filters', async () => { nock('http://ccdb-local:8083') - .get(/.*/) + .get(CCDB_URL_HEALTH_POINT) .reply(200, { objects: [] }); await rejects( - async () => ccdb.getObjectIdentification({ path: 'qc-test' }), - /Object at '\/latest\/qc-test' could not be found\.$/i, + async () => ccdb.getObjectIdentification({ path: PATH, id: ID }), + `Object at url '${ID}' and path ${PATH} could not be found.`, ); }); }); From cf14ff73137f8c3a824d5ce2036c27f6452fd36c Mon Sep 17 00:00:00 2001 From: hehoon <100522372+hehoon@users.noreply.github.com> Date: Mon, 1 Dec 2025 15:43:58 +0100 Subject: [PATCH 08/15] Fix `_buildFilterErrorMessage` to correctly append a period when message lacks punctuation. In addition, only add a whitespace to the end of the message if it is lacking one --- QualityControl/lib/services/ccdb/CcdbService.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/QualityControl/lib/services/ccdb/CcdbService.js b/QualityControl/lib/services/ccdb/CcdbService.js index b6f567285..84a8fb449 100644 --- a/QualityControl/lib/services/ccdb/CcdbService.js +++ b/QualityControl/lib/services/ccdb/CcdbService.js @@ -379,9 +379,12 @@ export class CcdbService { if (filters && Object.keys(filters).length > 0) { // Ensure the base message ends with proper punctuation. // If it does NOT end with any Unicode punctuation, append a period. - if (/\p{P}$/u.test(baseMessage)) { + if (!/\p{P}$/u.test(baseMessage)) { baseMessage += '.'; } + if (!baseMessage.endsWith(' ')) { + baseMessage += ' '; + } // Append a clear, descriptive filter hint. baseMessage += 'It was likely excluded by the applied filters.'; From 90d277c79ebede72ceb165971e3774a758c2c51a Mon Sep 17 00:00:00 2001 From: hehoon <100522372+hehoon@users.noreply.github.com> Date: Mon, 1 Dec 2025 15:44:56 +0100 Subject: [PATCH 09/15] Update tests to pass an Error object to rejects instead of a string message --- QualityControl/test/lib/services/CcdbService.test.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/QualityControl/test/lib/services/CcdbService.test.js b/QualityControl/test/lib/services/CcdbService.test.js index ae5f509b7..aeec745d6 100644 --- a/QualityControl/test/lib/services/CcdbService.test.js +++ b/QualityControl/test/lib/services/CcdbService.test.js @@ -486,9 +486,13 @@ export const ccdbServiceTestSuite = async () => { .get(CCDB_URL_HEALTH_POINT) .reply(200, { objects: [] }); + const filters = { RunNumber: 123456 }; + const filterString = Object.entries(filters).map(([key, value]) => `${key}=${value}`).join('/'); + await rejects( - async () => ccdb.getObjectIdentification({ path: PATH, id: ID, filters: { RunNumber: 123456 } }), - `Object at url '${ID}' and path ${PATH} could not be found. It was likely excluded by the applied filters.`, + async () => ccdb.getObjectIdentification({ path: PATH, id: ID, filters }), + // eslint-disable-next-line @stylistic/js/max-len + new Error(`Object at url '/latest/path/${ID}/${filterString}' and path '${PATH}' could not be found. It was likely excluded by the applied filters.`), ); }); @@ -499,7 +503,7 @@ export const ccdbServiceTestSuite = async () => { await rejects( async () => ccdb.getObjectIdentification({ path: PATH, id: ID }), - `Object at url '${ID}' and path ${PATH} could not be found.`, + new Error(`Object at url '/latest/path/${ID}' and path '${PATH}' could not be found.`), ); }); }); From 1a0551301fd88c44ec61ebe0f018351a430e2a81 Mon Sep 17 00:00:00 2001 From: hehoon <100522372+hehoon@users.noreply.github.com> Date: Mon, 1 Dec 2025 15:56:45 +0100 Subject: [PATCH 10/15] Display the actual path in the error message instead of the word 'PATH' --- QualityControl/lib/services/ccdb/CcdbService.js | 4 ++-- QualityControl/test/api/objects/api-get-object.test.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/QualityControl/lib/services/ccdb/CcdbService.js b/QualityControl/lib/services/ccdb/CcdbService.js index 84a8fb449..2cd30255b 100644 --- a/QualityControl/lib/services/ccdb/CcdbService.js +++ b/QualityControl/lib/services/ccdb/CcdbService.js @@ -190,7 +190,7 @@ export class CcdbService { result = await httpGetJson(this._hostname, this._port, url, { headers }); } catch { const errorMessage = this._buildFilterErrorMessage( - `Object at url '${url}' and path '${PATH}' could not be found.`, + `Object at url '${url}' and path '${partialIdentification.path}' could not be found.`, partialIdentification.filters, ); throw new Error(errorMessage); @@ -198,7 +198,7 @@ export class CcdbService { if (!result?.objects?.length) { const errorMessage = this._buildFilterErrorMessage( - `Object at url '${url}' and path '${PATH}' could not be found.`, + `Object at url '${url}' and path '${partialIdentification.path}' could not be found.`, partialIdentification.filters, ); throw new Error(errorMessage); diff --git a/QualityControl/test/api/objects/api-get-object.test.js b/QualityControl/test/api/objects/api-get-object.test.js index 2686e1f56..5454214ac 100644 --- a/QualityControl/test/api/objects/api-get-object.test.js +++ b/QualityControl/test/api/objects/api-get-object.test.js @@ -49,7 +49,7 @@ export const apiGetObjectsTests = () => { test('should return 500 if service fails to retrieve object', async () => { const url = `${URL_ADDRESS}/api/object?token=${OWNER_TEST_TOKEN}&path=invalid/path`; await testResult(url, 500, { - message: 'Object at url \'/latest/invalid/path\' and path \'path\' could not be found.', + message: 'Object at url \'/latest/invalid/path\' and path \'invalid/path\' could not be found.', status: 500, title: 'Unknown Error', }); From c165ee753085893b69afcc96ebf31458685e66c8 Mon Sep 17 00:00:00 2001 From: hehoon <100522372+hehoon@users.noreply.github.com> Date: Tue, 2 Dec 2025 14:57:09 +0100 Subject: [PATCH 11/15] Add tests to check for error messages on network or backend failure --- .../object-view-from-layout-show.test.js | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/QualityControl/test/public/pages/object-view-from-layout-show.test.js b/QualityControl/test/public/pages/object-view-from-layout-show.test.js index eae8410cc..07b65f280 100644 --- a/QualityControl/test/public/pages/object-view-from-layout-show.test.js +++ b/QualityControl/test/public/pages/object-view-from-layout-show.test.js @@ -408,4 +408,86 @@ export const objectViewFromLayoutShowTests = async (url, page, timeout = 5000, t ); }, ); + + await testParent.test( + 'should display an error when the JSROOT object fails to fetch due to a network failure', + { timeout }, + async () => { + const requestHandler = (interceptedRequest) => { + const url = interceptedRequest.url(); + + if (url.includes('/api/object')) { + interceptedRequest.abort('failed'); // simulates network failure + } else { + interceptedRequest.continue(); + } + }; + + try { + // Enable interception and attach the handler + await page.setRequestInterception(true); + page.on('request', requestHandler); + + await page.reload({ waitUntil: 'networkidle0' }); + await delay(100); + + const errorText = await page.evaluate(() => document.querySelector('#Error .f3')?.innerText); + + strictEqual(errorText, 'Connection to server failed, please try again'); + } catch (error) { + // Test failed + strictEqual(1, 0, error.message); + } finally { + // Cleanup: remove listener and disable interception + page.off('request', requestHandler); + await page.setRequestInterception(false); + } + }, + ); + + await testParent.test( + 'should display an error when the JSROOT object fails to fetch due to a backend failure', + { timeout }, + async () => { + const requestHandler = (interceptedRequest) => { + const url = interceptedRequest.url(); + + if (url.includes('/api/object')) { + // Respond with a backend error + interceptedRequest.respond({ + status: 500, + contentType: 'application/json', + body: JSON.stringify({ + message: 'JSROOT failed to open file \'url\'', + }), + }); + } else { + interceptedRequest.continue(); + } + }; + + try { + // Enable interception and attach the handler + await page.setRequestInterception(true); + page.on('request', requestHandler); + + await page.reload({ waitUntil: 'networkidle0' }); + await delay(100); + + const errorText = await page.evaluate(() => document.querySelector('#Error .f3')?.innerText); + + strictEqual( + errorText, + 'Request to server failed (500 Internal Server Error): JSROOT failed to open file \'url\'', + ); + } catch (error) { + // Test failed + strictEqual(1, 0, error.message); + } finally { + // Cleanup: remove listener and disable interception + page.off('request', requestHandler); + await page.setRequestInterception(false); + } + }, + ); }; From cea8895171a951b99015110abd83fdd06418eb2c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Dec 2025 11:07:19 +0100 Subject: [PATCH 12/15] Bump actions/checkout from 5 to 6 (#3205) Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 2 +- .github/workflows/configuration.yml | 6 +++--- .github/workflows/control.yml | 4 ++-- .github/workflows/framework.yml | 12 ++++++------ .github/workflows/infologger.yml | 4 ++-- .github/workflows/proto-sync.yml | 2 +- .github/workflows/qc.yml | 6 +++--- .github/workflows/release.yml | 4 ++-- .github/workflows/tokenization-grpc-wrapper.yml | 2 +- .github/workflows/tokenization.yml | 8 ++++---- 10 files changed, 25 insertions(+), 25 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 0a738da89..e8b6313c3 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Initialize CodeQL uses: github/codeql-action/init@v4 diff --git a/.github/workflows/configuration.yml b/.github/workflows/configuration.yml index e7867102c..fb08c19e5 100644 --- a/.github/workflows/configuration.yml +++ b/.github/workflows/configuration.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 6 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Setup node uses: actions/setup-node@v6 with: @@ -27,7 +27,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 6 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Setup node uses: actions/setup-node@v6 with: @@ -40,7 +40,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Setup node uses: actions/setup-node@v6 with: diff --git a/.github/workflows/control.yml b/.github/workflows/control.yml index be8f34e23..818aab3fa 100644 --- a/.github/workflows/control.yml +++ b/.github/workflows/control.yml @@ -15,7 +15,7 @@ jobs: runs-on: macOS-latest timeout-minutes: 6 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Setup node uses: actions/setup-node@v6 with: @@ -27,7 +27,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 6 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Setup node uses: actions/setup-node@v6 with: diff --git a/.github/workflows/framework.yml b/.github/workflows/framework.yml index 9eb3164bd..893c069e3 100644 --- a/.github/workflows/framework.yml +++ b/.github/workflows/framework.yml @@ -15,7 +15,7 @@ jobs: runs-on: macOS-latest timeout-minutes: 6 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Setup node uses: actions/setup-node@v6 with: @@ -28,7 +28,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 6 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: node-version: '22.x' @@ -46,7 +46,7 @@ jobs: runs-on: windows-latest timeout-minutes: 10 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: node-version: '22.x' @@ -62,7 +62,7 @@ jobs: steps: - name: Run Control tests by using `@aliceo2/web-ui` from local `../Framework` run : (echo "Run Control tests by using @aliceo2/web-ui from local ../Framework";) - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Setup node uses: actions/setup-node@v6 with: @@ -84,7 +84,7 @@ jobs: # steps: # - name: Run QualityControl tests by using `@aliceo2/web-ui` from local `../Framework` # run : (echo "Run QualityControl tests by using @aliceo2/web-ui from local ../Framework";) - # - uses: actions/checkout@v5 + # - uses: actions/checkout@v6 # - name: Setup node # uses: actions/setup-node@v6 # with: @@ -105,7 +105,7 @@ jobs: steps: - name: Run InfoLogger tests by using `@aliceo2/web-ui` from local `../Framework` run : (echo "Run InfoLogger tests by using @aliceo2/web-ui from local ../Framework";) - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Setup node uses: actions/setup-node@v6 with: diff --git a/.github/workflows/infologger.yml b/.github/workflows/infologger.yml index 47b991e0f..db0b95edc 100644 --- a/.github/workflows/infologger.yml +++ b/.github/workflows/infologger.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 6 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Setup node uses: actions/setup-node@v6 with: @@ -26,7 +26,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 6 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: node-version: '22.x' diff --git a/.github/workflows/proto-sync.yml b/.github/workflows/proto-sync.yml index b1f71b5df..37df9b7ed 100644 --- a/.github/workflows/proto-sync.yml +++ b/.github/workflows/proto-sync.yml @@ -12,7 +12,7 @@ jobs: env: GH_TOKEN: ${{ github.token }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: token: ${{ secrets.GITHUB_TOKEN }} - name: Copy new proto file(s) diff --git a/.github/workflows/qc.yml b/.github/workflows/qc.yml index 99c1f7e3d..6e485cd53 100644 --- a/.github/workflows/qc.yml +++ b/.github/workflows/qc.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 6 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Setup node uses: actions/setup-node@v6 with: @@ -28,7 +28,7 @@ jobs: timeout-minutes: 6 needs: lint-check steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Setup node uses: actions/setup-node@v6 with: @@ -40,7 +40,7 @@ jobs: timeout-minutes: 6 needs: lint-check steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Setup node uses: actions/setup-node@v6 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c70761824..25c1127b8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,7 +11,7 @@ jobs: VERSION: ${{ steps.set-project.outputs.version }} TAG: ${{ steps.set-project.outputs.tag }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: node-version: '22.x' @@ -51,7 +51,7 @@ jobs: outputs: ASSET_URL: ${{ steps.upload.outputs.asset_url }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: node-version: '22.x' diff --git a/.github/workflows/tokenization-grpc-wrapper.yml b/.github/workflows/tokenization-grpc-wrapper.yml index 3a8d6d18f..ef6474ab3 100644 --- a/.github/workflows/tokenization-grpc-wrapper.yml +++ b/.github/workflows/tokenization-grpc-wrapper.yml @@ -20,7 +20,7 @@ jobs: working-directory: Tokenization/backend/wrapper steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Setup Node uses: actions/setup-node@v6 with: diff --git a/.github/workflows/tokenization.yml b/.github/workflows/tokenization.yml index 8731d3687..20f0126f0 100644 --- a/.github/workflows/tokenization.yml +++ b/.github/workflows/tokenization.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 6 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Setup node uses: actions/setup-node@v5 with: @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 6 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Setup node uses: actions/setup-node@v5 with: @@ -40,7 +40,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 6 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Setup node uses: actions/setup-node@v5 with: @@ -53,7 +53,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 6 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Setup node uses: actions/setup-node@v5 with: From 844fbb2a75021df050bada33accd971b507d29b9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Dec 2025 11:13:39 +0100 Subject: [PATCH 13/15] Bump actions/setup-node from 5 to 6 (#3206) Bumps [actions/setup-node](https://github.com/actions/setup-node) from 5 to 6. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/setup-node dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/tokenization.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tokenization.yml b/.github/workflows/tokenization.yml index 20f0126f0..72cfd65a4 100644 --- a/.github/workflows/tokenization.yml +++ b/.github/workflows/tokenization.yml @@ -16,7 +16,7 @@ jobs: steps: - uses: actions/checkout@v6 - name: Setup node - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version: '22.x' - run: (cd Tokenization/backend/central-system; npm i) @@ -27,7 +27,7 @@ jobs: steps: - uses: actions/checkout@v6 - name: Setup node - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version: '22.x' - run: (cd Tokenization/webapp; npm i ) @@ -42,7 +42,7 @@ jobs: steps: - uses: actions/checkout@v6 - name: Setup node - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version: '22.x' - run: (cd Tokenization/webapp; npm run docker:test) @@ -55,7 +55,7 @@ jobs: steps: - uses: actions/checkout@v6 - name: Setup node - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version: '22.x' - run: (cd Tokenization/backend/central-system; npm i; npm run test) From 6cc344bf7e9e69a73a0fc837439b8127463c5d23 Mon Sep 17 00:00:00 2001 From: hehoon <100522372+hehoon@users.noreply.github.com> Date: Wed, 3 Dec 2025 15:18:57 +0100 Subject: [PATCH 14/15] Use NotFoundError when the object is not found. Throw an error when the backend cannot retrieve the object due to a bad connection --- .../lib/services/ccdb/CcdbService.js | 18 ++++++++---------- .../test/api/objects/api-get-object.test.js | 2 +- .../test/lib/services/CcdbService.test.js | 12 +++++------- 3 files changed, 14 insertions(+), 18 deletions(-) diff --git a/QualityControl/lib/services/ccdb/CcdbService.js b/QualityControl/lib/services/ccdb/CcdbService.js index 2cd30255b..5b6097760 100644 --- a/QualityControl/lib/services/ccdb/CcdbService.js +++ b/QualityControl/lib/services/ccdb/CcdbService.js @@ -12,7 +12,7 @@ * or submit itself to any jurisdiction. */ -import { FailedDependencyError, LogManager } from '@aliceo2/web-ui'; +import { FailedDependencyError, LogManager, NotFoundError } from '@aliceo2/web-ui'; import { httpHeadJson, httpGetJson } from '../../utils/httpRequests.js'; import { CCDB_MONITOR, CCDB_VERSION_KEY, CCDB_RESPONSE_BODY_KEYS, CCDB_FILTER_FIELDS, CCDB_RESPONSE_HEADER_KEYS, @@ -176,7 +176,8 @@ export class CcdbService { * ``` * @param {CcdbObjectIdentification} partialIdentification - fields such as path, validFrom, etc. * @returns {Promise.} - returns object full identification - * @throws {Error} throws if the object cannot be found + * @throws {Error} throws if the object cannot be fetched + * @throws {NotFoundError} throws if the object cannot be found */ async getObjectIdentification(partialIdentification) { const headers = { @@ -189,11 +190,7 @@ export class CcdbService { try { result = await httpGetJson(this._hostname, this._port, url, { headers }); } catch { - const errorMessage = this._buildFilterErrorMessage( - `Object at url '${url}' and path '${partialIdentification.path}' could not be found.`, - partialIdentification.filters, - ); - throw new Error(errorMessage); + throw new Error(`Failed to fetch object at url '${url}' and path '${partialIdentification.path}'.`); } if (!result?.objects?.length) { @@ -201,7 +198,7 @@ export class CcdbService { `Object at url '${url}' and path '${partialIdentification.path}' could not be found.`, partialIdentification.filters, ); - throw new Error(errorMessage); + throw new NotFoundError(errorMessage); } const [qcObject] = result.objects; @@ -265,7 +262,8 @@ export class CcdbService { * The minimum required parameter to provide is the `path` * @param {CcdbObjectIdentification} identification - attributes by which the object should be queried * @returns {Promise.} - object details for a given timestamp - * @throws {Error} + * @throws {Error} Thrown when an error occurs whilst fetching the object + * @throws {NotFoundError} Thrown when the object cannot be found */ async getObjectLatestVersionInfo(identification) { const { path } = identification ?? {}; @@ -279,7 +277,7 @@ export class CcdbService { const url = `/latest${this._buildCcdbUrlPath(identification)}`; const { objects } = await httpGetJson(this._hostname, this._port, url, { headers: timestampHeaders }); if (objects?.length <= 0) { - throw new Error(`No object found for: ${path}`); + throw new NotFoundError(`No object found for: ${path}`); } return objects[0]; } catch { diff --git a/QualityControl/test/api/objects/api-get-object.test.js b/QualityControl/test/api/objects/api-get-object.test.js index 5454214ac..30d7ed82c 100644 --- a/QualityControl/test/api/objects/api-get-object.test.js +++ b/QualityControl/test/api/objects/api-get-object.test.js @@ -49,7 +49,7 @@ export const apiGetObjectsTests = () => { test('should return 500 if service fails to retrieve object', async () => { const url = `${URL_ADDRESS}/api/object?token=${OWNER_TEST_TOKEN}&path=invalid/path`; await testResult(url, 500, { - message: 'Object at url \'/latest/invalid/path\' and path \'invalid/path\' could not be found.', + message: 'Failed to fetch object at url \'/latest/invalid/path\' and path \'invalid/path\'.', status: 500, title: 'Unknown Error', }); diff --git a/QualityControl/test/lib/services/CcdbService.test.js b/QualityControl/test/lib/services/CcdbService.test.js index aeec745d6..52139a431 100644 --- a/QualityControl/test/lib/services/CcdbService.test.js +++ b/QualityControl/test/lib/services/CcdbService.test.js @@ -474,21 +474,19 @@ export const ccdbServiceTestSuite = async () => { suite('CcdbService getObjectIdentification() Error Messages', () => { let ccdb = null; - let CCDB_URL_HEALTH_POINT = ''; before(() => { ccdb = new CcdbService(ccdbConfig); - CCDB_URL_HEALTH_POINT = `/monitor/${CCDB_MONITOR}/.*/${CCDB_VERSION_KEY}`; }); test('should throw error if object not found with filters applied', async () => { - nock('http://ccdb-local:8083') - .get(CCDB_URL_HEALTH_POINT) - .reply(200, { objects: [] }); - const filters = { RunNumber: 123456 }; const filterString = Object.entries(filters).map(([key, value]) => `${key}=${value}`).join('/'); + nock('http://ccdb-local:8083') + .get(`/latest/path/${ID}/${filterString}`) + .reply(200, { objects: [] }); + await rejects( async () => ccdb.getObjectIdentification({ path: PATH, id: ID, filters }), // eslint-disable-next-line @stylistic/js/max-len @@ -498,7 +496,7 @@ export const ccdbServiceTestSuite = async () => { test('should throw error if object not found without filters', async () => { nock('http://ccdb-local:8083') - .get(CCDB_URL_HEALTH_POINT) + .get(`/latest/path/${ID}`) .reply(200, { objects: [] }); await rejects( From 5422ab8e2a98c5491ec90490a2f08221e3f8cb08 Mon Sep 17 00:00:00 2001 From: hehoon <100522372+hehoon@users.noreply.github.com> Date: Wed, 3 Dec 2025 15:32:45 +0100 Subject: [PATCH 15/15] Do not display the error message prefix (from Loader) in JSROOT plotting errors --- QualityControl/public/services/QCObject.service.js | 2 +- .../test/public/pages/object-view-from-object-tree.test.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/QualityControl/public/services/QCObject.service.js b/QualityControl/public/services/QCObject.service.js index 31283c8e3..15d953238 100644 --- a/QualityControl/public/services/QCObject.service.js +++ b/QualityControl/public/services/QCObject.service.js @@ -79,7 +79,7 @@ export default class QCObjectService { ? { RunNumber: this.filterModel.runNumber } : this.filterModel.filterMap; const url = this._buildURL(`/api/object?path=${objectName}`, id, validFrom, filters); - const { result, ok } = await this.model.loader.get(url); + const { result, ok } = await this.model.loader.get(url, {}, true); if (ok) { result.qcObject = { root: JSROOT.parse(result.root), diff --git a/QualityControl/test/public/pages/object-view-from-object-tree.test.js b/QualityControl/test/public/pages/object-view-from-object-tree.test.js index c4633141a..b9a70e26e 100644 --- a/QualityControl/test/public/pages/object-view-from-object-tree.test.js +++ b/QualityControl/test/public/pages/object-view-from-object-tree.test.js @@ -11,7 +11,7 @@ * or submit itself to any jurisdiction. */ -import { match, strictEqual } from 'node:assert'; +import { strictEqual } from 'node:assert'; const OBJECT_VIEW_PAGE_PARAM = '?page=objectView'; export const objectViewFromObjectTreeTests = async (url, page, timeout = 5000, testParent) => { @@ -63,7 +63,7 @@ export const objectViewFromObjectTreeTests = async (url, page, timeout = 5000, t (element) => document.querySelector(element).textContent, errorMessageElement, ); - match(message, /^Request to server failed/i); + strictEqual(message, 'Failed to fetch object at url \'/latest/NOT_FOUND_OBJECT\' and path \'NOT_FOUND_OBJECT\'.'); }, );