From 75d9f3c94d82c5f8cf2fade58dffb1ab65c4c2eb Mon Sep 17 00:00:00 2001 From: Gary Mirams Date: Tue, 26 May 2026 00:51:17 +0100 Subject: [PATCH 01/13] Avoid 'undefined' being passed to ApPredict executable --- server.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/server.js b/server.js index f89552c..42c36c4 100644 --- a/server.js +++ b/server.js @@ -436,7 +436,13 @@ function call_invoke(appredict_input, config) { } - var args = `--pacing-freq ${appredict_input.pacingFrequency} --pacing-max-time ${appredict_input.pacingMaxTime} `; + var args = ''; + if (typeof appredict_input.pacingFrequency !== 'undefined') { + args += `--pacing-freq ${appredict_input.pacingFrequency} `; + } + if (typeof appredict_input.pacingMaxTime !== 'undefined') { + args += `--pacing-max-time ${appredict_input.pacingMaxTime} `; + } var spreads_detected = false; @@ -1059,7 +1065,7 @@ const server = http.createServer((request, response) => { break; default: return_obj = { - 'error': 'Operation' + operation + 'is invalid. Valid data query options are: "STOP", "voltage_traces", "voltage_results", "progress_status", "q_net", "pkpd_results", "messages" and "received"' + 'error': 'Operation "' + operation + '"is invalid. Valid data query options are: "STOP", "voltage_traces", "voltage_results", "progress_status", "q_net", "pkpd_results", "messages" and "received"' } break; } From 27f1051f4233d1971a4c8271c2cc53ff14347d50 Mon Sep 17 00:00:00 2001 From: Gary Mirams Date: Tue, 26 May 2026 01:01:03 +0100 Subject: [PATCH 02/13] Couple more options available and handy --- server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server.js b/server.js index 42c36c4..78430ca 100644 --- a/server.js +++ b/server.js @@ -1065,7 +1065,7 @@ const server = http.createServer((request, response) => { break; default: return_obj = { - 'error': 'Operation "' + operation + '"is invalid. Valid data query options are: "STOP", "voltage_traces", "voltage_results", "progress_status", "q_net", "pkpd_results", "messages" and "received"' + 'error': 'Operation "' + operation + '"is invalid. Valid data query options are: "STDERR", "STDOUT", "STOP", "voltage_traces", "voltage_results", "progress_status", "q_net", "pkpd_results", "messages" and "received"' } break; } From c9ba5e931580387a42a189ad49c7369d2d2d25df Mon Sep 17 00:00:00 2001 From: Gary Mirams Date: Tue, 26 May 2026 01:02:22 +0100 Subject: [PATCH 03/13] Update copyright --- LICENSE | 2 +- server.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/LICENSE b/LICENSE index f25a3a0..87413c6 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,7 @@ BSD 3-Clause License Unless otherwise noted, all files within this distribution are -Copyright (c) 2013-2021 University of Oxford, 2016-2021 University College London, and 2016-2024 University of Nottingham. +Copyright (c) 2013-2021 University of Oxford, 2016-2021 University College London, and 2016-2026 University of Nottingham. All rights reserved. University of Oxford means the Chancellor, Masters and Scholars of the diff --git a/server.js b/server.js index 78430ca..081b03a 100644 --- a/server.js +++ b/server.js @@ -32,7 +32,7 @@ const RUNME_SCRIPT = './run_me.sh'; /**************************** OPTIONS help texts ***************************/ -const HELP_APPREDICT = `Copyright (c) 2005-2024, University of Oxford. +const HELP_APPREDICT = `Copyright (c) 2005-2026, University of Oxford. All rights reserved. University of Oxford means the Chancellor, Masters and Scholars of the From 3653fafda26b3f52c20907c838b802a84d0080a5 Mon Sep 17 00:00:00 2001 From: Gary Mirams Date: Tue, 26 May 2026 01:12:09 +0100 Subject: [PATCH 04/13] correct bracket use --- server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server.js b/server.js index 081b03a..8525e93 100644 --- a/server.js +++ b/server.js @@ -1065,7 +1065,7 @@ const server = http.createServer((request, response) => { break; default: return_obj = { - 'error': 'Operation "' + operation + '"is invalid. Valid data query options are: "STDERR", "STDOUT", "STOP", "voltage_traces", "voltage_results", "progress_status", "q_net", "pkpd_results", "messages" and "received"' + 'error': 'Operation ' + operation + 'is invalid. Valid data query options are: "STDERR", "STDOUT", "STOP", "voltage_traces", "voltage_results", "progress_status", "q_net", "pkpd_results", "messages" and "received"' } break; } From 669f4f3c0f09b5772cad9ccac091163ec3b4e9b8 Mon Sep 17 00:00:00 2001 From: Gary Mirams Date: Tue, 26 May 2026 01:20:54 +0100 Subject: [PATCH 05/13] pacingFrequency is mandatory --- server.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/server.js b/server.js index 8525e93..0e19a21 100644 --- a/server.js +++ b/server.js @@ -436,10 +436,7 @@ function call_invoke(appredict_input, config) { } - var args = ''; - if (typeof appredict_input.pacingFrequency !== 'undefined') { - args += `--pacing-freq ${appredict_input.pacingFrequency} `; - } + var args = `--pacing-freq ${appredict_input.pacingFrequency} `; if (typeof appredict_input.pacingMaxTime !== 'undefined') { args += `--pacing-max-time ${appredict_input.pacingMaxTime} `; } From 0812b760b56715435aa527c0547a46f1cf3ff2f4 Mon Sep 17 00:00:00 2001 From: Gary Mirams Date: Tue, 26 May 2026 01:32:33 +0100 Subject: [PATCH 06/13] Apply suggestion from @mirams --- server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server.js b/server.js index 0e19a21..7184d86 100644 --- a/server.js +++ b/server.js @@ -1062,7 +1062,7 @@ const server = http.createServer((request, response) => { break; default: return_obj = { - 'error': 'Operation ' + operation + 'is invalid. Valid data query options are: "STDERR", "STDOUT", "STOP", "voltage_traces", "voltage_results", "progress_status", "q_net", "pkpd_results", "messages" and "received"' + 'error': 'Operation ' + operation + ' is invalid. Valid data query options are: "STDERR", "STDOUT", "STOP", "voltage_traces", "voltage_results", "progress_status", "q_net", "pkpd_results", "messages" and "received"' } break; } From ff507ec6334fe8a483204f5ade728c7bf5986523 Mon Sep 17 00:00:00 2001 From: Gary Mirams Date: Tue, 26 May 2026 01:40:46 +0100 Subject: [PATCH 07/13] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- server.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server.js b/server.js index 7184d86..cb5252d 100644 --- a/server.js +++ b/server.js @@ -437,7 +437,9 @@ function call_invoke(appredict_input, config) { var args = `--pacing-freq ${appredict_input.pacingFrequency} `; - if (typeof appredict_input.pacingMaxTime !== 'undefined') { + if (has_data(appredict_input.pacingMaxTime) && + !isNaN(parseFloat(appredict_input.pacingMaxTime)) && + isFinite(appredict_input.pacingMaxTime)) { args += `--pacing-max-time ${appredict_input.pacingMaxTime} `; } From daf6eccc3dc72c5a40c4582abbc6de69b362ff33 Mon Sep 17 00:00:00 2001 From: Gary Mirams Date: Tue, 26 May 2026 01:47:50 +0100 Subject: [PATCH 08/13] Remove checks done a couple of lines above --- server.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/server.js b/server.js index cb5252d..9b7c871 100644 --- a/server.js +++ b/server.js @@ -437,9 +437,7 @@ function call_invoke(appredict_input, config) { var args = `--pacing-freq ${appredict_input.pacingFrequency} `; - if (has_data(appredict_input.pacingMaxTime) && - !isNaN(parseFloat(appredict_input.pacingMaxTime)) && - isFinite(appredict_input.pacingMaxTime)) { + if (has_data(appredict_input.pacingMaxTime)) { args += `--pacing-max-time ${appredict_input.pacingMaxTime} `; } From e0e38be9c07ff0714fb797f286f0abda1d76f875 Mon Sep 17 00:00:00 2001 From: Kwabena Amponsah Date: Tue, 26 May 2026 15:36:10 +0100 Subject: [PATCH 09/13] Add test for voltage_results --- .github/workflows/docker-test.yml | 43 ++++++++ package.json | 4 + test/simulation.test.js | 163 ++++++++++++++++++++++++++++++ 3 files changed, 210 insertions(+) create mode 100644 .github/workflows/docker-test.yml create mode 100644 test/simulation.test.js diff --git a/.github/workflows/docker-test.yml b/.github/workflows/docker-test.yml new file mode 100644 index 0000000..268d9f5 --- /dev/null +++ b/.github/workflows/docker-test.yml @@ -0,0 +1,43 @@ +name: docker-test + +on: + workflow_dispatch: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +jobs: + build-and-test: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: 20 + + - name: Build Docker image + run: docker build -t ap-nimbus-app-manager:test . + + - name: Run app-manager container + run: docker run -d --name app-manager-ci -p 8080:8080 ap-nimbus-app-manager:test + + - name: Run simulation test from host + env: + BASE_URL: http://localhost:8080 + run: npm run test:simulation + + - name: Show container logs on failure + if: failure() + run: docker logs app-manager-ci + + - name: Stop and remove container + if: always() + run: docker rm -f app-manager-ci diff --git a/package.json b/package.json index 03cc164..2894b37 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,10 @@ { "name": "app-manager", "version": "2.0.0", + "scripts": { + "test": "node --test", + "test:simulation": "node --test test/simulation.test.js" + }, "dependencies": { "chokidar": "^3.6.0", "tiny-json-http": "^7.5.1", diff --git a/test/simulation.test.js b/test/simulation.test.js new file mode 100644 index 0000000..7eacaf3 --- /dev/null +++ b/test/simulation.test.js @@ -0,0 +1,163 @@ +"use strict"; + +const assert = require("node:assert/strict"); +const test = require("node:test"); + +const BASE_URL = process.env.BASE_URL || "http://localhost:8080"; + +const REQUEST_PAYLOAD = { + modelId: 1, + pacingFrequency: 0.5, + plasmaPoints: [0, 3, 10, 30, 100] +}; + +const EXPECTED_VOLTAGE_RESULTS = [ + { + c: "Concentration(uM)", + uv: "UpstrokeVelocity(mV/ms)", + pv: "PeakVm(mV)", + a50: "APD50(ms)", + a90: "APD90(ms)", + da90: ["delta_APD90(%)"] + }, + { + c: "0", + uv: "350.596", + pv: "47.8243", + a50: "184.455", + a90: "216.892", + da90: ["0"] + }, + { + c: "0.001", + uv: "351.022", + pv: "47.8407", + a50: "184.7", + a90: "217.155", + da90: ["0.121071"] + } +]; + +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +function assertAlmostEqual(actual, expected, tolerance, label) { + const actualNumber = Number.parseFloat(actual); + const expectedNumber = Number.parseFloat(expected); + assert.ok(Number.isFinite(actualNumber), `${label} actual value is not numeric: ${actual}`); + assert.ok(Number.isFinite(expectedNumber), `${label} expected value is not numeric: ${expected}`); + const difference = Math.abs(actualNumber - expectedNumber); + assert.ok( + difference <= tolerance, + `${label} differs by ${difference}, tolerance is ${tolerance}. actual=${actualNumber}, expected=${expectedNumber}` + ); +} + +async function requestJson(requestUrl, options, context) { + const response = await fetch(requestUrl, options); + const raw = await response.text(); + if (!response.ok) { + throw new Error(`${context} failed with HTTP ${response.status}: ${raw}`); + } + + try { + return JSON.parse(raw); + } catch (error) { + throw new Error(`${context} did not return valid JSON: ${raw}`); + } +} + +async function waitForServerReady(timeoutMs) { + const start = Date.now(); + while (Date.now() - start < timeoutMs) { + try { + const payload = await requestJson(`${BASE_URL}/`, { method: "GET" }, "Server readiness check"); + if (payload && (payload.error || payload.success)) { + return; + } + } catch (error) { + // Ignore until timeout while server starts. + } + await sleep(500); + } + throw new Error("Timed out waiting for app-manager to become ready on port 8080."); +} + +async function pollVoltageResults(simulationId, timeoutMs) { + const start = Date.now(); + let lastPayload; + + while (Date.now() - start < timeoutMs) { + const payload = await requestJson( + `${BASE_URL}/api/collection/${simulationId}/voltage_results`, + { method: "GET" }, + "voltage_results query" + ); + lastPayload = payload; + + if (payload && Array.isArray(payload.success) && payload.success.length >= EXPECTED_VOLTAGE_RESULTS.length) { + return payload; + } + + await sleep(1000); + } + + throw new Error( + `Timed out waiting for voltage_results for simulation ${simulationId}. Last payload: ${JSON.stringify(lastPayload)}` + ); +} + +test("posts inline request payload and validates voltage_results", { timeout: 120000 }, async () => { + + await waitForServerReady(30000); + + const requestBody = JSON.stringify(REQUEST_PAYLOAD); + const postPayload = await requestJson( + `${BASE_URL}/`, + { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: requestBody + }, + "Simulation start" + ); + assert.ok(postPayload.success, `Expected success object in POST response. Got: ${JSON.stringify(postPayload)}`); + assert.ok(postPayload.success.id, `Expected simulation id in POST response. Got: ${JSON.stringify(postPayload)}`); + + const simulationId = postPayload.success.id; + + const voltageResultsPayload = await pollVoltageResults(simulationId, 60000); + const actualVoltageResults = voltageResultsPayload.success; + assert.ok(Array.isArray(actualVoltageResults), "Expected an array in voltage_results response"); + + assert.equal( + actualVoltageResults.length, + EXPECTED_VOLTAGE_RESULTS.length, + "actual and expected voltage_results lengths should match" + ); + + assert.deepStrictEqual( + actualVoltageResults[0], + EXPECTED_VOLTAGE_RESULTS[0], + "voltage_results header row does not match expected values" + ); + + for (let i = 1; i < EXPECTED_VOLTAGE_RESULTS.length; i += 1) { + const actualRow = actualVoltageResults[i]; + const expectedRow = EXPECTED_VOLTAGE_RESULTS[i]; + + assert.equal(actualRow.c, expectedRow.c, `row ${i} c`); + assertAlmostEqual(actualRow.uv, expectedRow.uv, 5, `row ${i} uv`); + assertAlmostEqual(actualRow.pv, expectedRow.pv, 1, `row ${i} pv`); + assertAlmostEqual(actualRow.a50, expectedRow.a50, 5, `row ${i} a50`); + assertAlmostEqual(actualRow.a90, expectedRow.a90, 5, `row ${i} a90`); + + assert.equal(actualRow.da90.length, expectedRow.da90.length, `row ${i} da90 length should match`); + for (let j = 0; j < expectedRow.da90.length; j += 1) { + assertAlmostEqual(actualRow.da90[j], expectedRow.da90[j], 0.5, `row ${i} da90[${j}]`); + } + } +}); From 61a1682e1eb6e3f3f3c796029d6de48fbde870cb Mon Sep 17 00:00:00 2001 From: Kwabena Amponsah Date: Wed, 27 May 2026 11:44:43 +0100 Subject: [PATCH 10/13] Add test for STDOUT --- test/simulation.test.js | 152 +++++++++++++++++++++++++++------------- 1 file changed, 105 insertions(+), 47 deletions(-) diff --git a/test/simulation.test.js b/test/simulation.test.js index 7eacaf3..65d519e 100644 --- a/test/simulation.test.js +++ b/test/simulation.test.js @@ -5,43 +5,24 @@ const test = require("node:test"); const BASE_URL = process.env.BASE_URL || "http://localhost:8080"; -const REQUEST_PAYLOAD = { - modelId: 1, - pacingFrequency: 0.5, - plasmaPoints: [0, 3, 10, 30, 100] -}; - -const EXPECTED_VOLTAGE_RESULTS = [ - { - c: "Concentration(uM)", - uv: "UpstrokeVelocity(mV/ms)", - pv: "PeakVm(mV)", - a50: "APD50(ms)", - a90: "APD90(ms)", - da90: ["delta_APD90(%)"] - }, - { - c: "0", - uv: "350.596", - pv: "47.8243", - a50: "184.455", - a90: "216.892", - da90: ["0"] - }, - { - c: "0.001", - uv: "351.022", - pv: "47.8407", - a50: "184.7", - a90: "217.155", - da90: ["0.121071"] - } -]; - +/** + * Waits for the given number of milliseconds. + * + * @param {number} ms Milliseconds to wait before resolving. + * @returns {Promise} A promise that resolves after the delay. + */ function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } +/** + * Asserts that two numeric values are within a given tolerance. + * + * @param {string|number} actual The measured value from the API response. + * @param {string|number} expected The expected reference value. + * @param {number} tolerance The maximum allowed absolute difference. + * @param {string} label A label used to identify assertion failures. + */ function assertAlmostEqual(actual, expected, tolerance, label) { const actualNumber = Number.parseFloat(actual); const expectedNumber = Number.parseFloat(expected); @@ -54,6 +35,14 @@ function assertAlmostEqual(actual, expected, tolerance, label) { ); } +/** + * Fetches JSON from the API and throws with context on HTTP or parse failures. + * + * @param {string} requestUrl The URL to request. + * @param {RequestInit} options Fetch options for the request. + * @param {string} context A short description used in error messages. + * @returns {Promise} The parsed JSON payload. + */ async function requestJson(requestUrl, options, context) { const response = await fetch(requestUrl, options); const raw = await response.text(); @@ -68,6 +57,12 @@ async function requestJson(requestUrl, options, context) { } } +/** + * Polls the root endpoint until the app-manager server is ready to accept requests. + * + * @param {number} timeoutMs Maximum time to wait before failing. + * @returns {Promise} Resolves once the service responds with a valid payload. + */ async function waitForServerReady(timeoutMs) { const start = Date.now(); while (Date.now() - start < timeoutMs) { @@ -84,19 +79,28 @@ async function waitForServerReady(timeoutMs) { throw new Error("Timed out waiting for app-manager to become ready on port 8080."); } -async function pollVoltageResults(simulationId, timeoutMs) { +/** + * Polls a simulation result endpoint until the payload satisfies the readiness check. + * + * @param {string} simulationId The simulation identifier returned from the POST request. + * @param {string} operation The collection operation to query, such as `STDOUT` or `voltage_results`. + * @param {(payload: any) => boolean} isReady Predicate that determines when the payload is usable. + * @param {number} timeoutMs Maximum time to wait before failing. + * @returns {Promise} The first payload that satisfies `isReady`. + */ +async function pollResults(simulationId, operation, isReady, timeoutMs) { const start = Date.now(); let lastPayload; while (Date.now() - start < timeoutMs) { const payload = await requestJson( - `${BASE_URL}/api/collection/${simulationId}/voltage_results`, + `${BASE_URL}/api/collection/${simulationId}/${operation}`, { method: "GET" }, - "voltage_results query" + `${operation} query` ); lastPayload = payload; - if (payload && Array.isArray(payload.success) && payload.success.length >= EXPECTED_VOLTAGE_RESULTS.length) { + if (isReady(payload)) { return payload; } @@ -104,14 +108,50 @@ async function pollVoltageResults(simulationId, timeoutMs) { } throw new Error( - `Timed out waiting for voltage_results for simulation ${simulationId}. Last payload: ${JSON.stringify(lastPayload)}` + `Timed out waiting for ${operation} for simulation ${simulationId}. Last payload: ${JSON.stringify(lastPayload)}` ); } -test("posts inline request payload and validates voltage_results", { timeout: 120000 }, async () => { +// Test the simulation endpoint by posting a request and validating the output. +test("posts request payload and validates output", { timeout: 120000 }, async () => { await waitForServerReady(30000); + const REQUEST_PAYLOAD = { + modelId: 1, + pacingFrequency: 0.5, + plasmaPoints: [0, 3, 10, 30, 100] + }; + + const EXPECTED_STDOUT = /^ApPredict args : --pacing-freq 0\.5 --plasma-concs 0 3 10 30 100 --model 1$/m; + + const EXPECTED_VOLTAGE_RESULTS = [ + { + c: "Concentration(uM)", + uv: "UpstrokeVelocity(mV/ms)", + pv: "PeakVm(mV)", + a50: "APD50(ms)", + a90: "APD90(ms)", + da90: ["delta_APD90(%)"] + }, + { + c: "0", + uv: "350.596", + pv: "47.8243", + a50: "184.455", + a90: "216.892", + da90: ["0"] + }, + { + c: "0.001", + uv: "351.022", + pv: "47.8407", + a50: "184.7", + a90: "217.155", + da90: ["0.121071"] + } + ]; + const requestBody = JSON.stringify(REQUEST_PAYLOAD); const postPayload = await requestJson( `${BASE_URL}/`, @@ -129,7 +169,25 @@ test("posts inline request payload and validates voltage_results", { timeout: 12 const simulationId = postPayload.success.id; - const voltageResultsPayload = await pollVoltageResults(simulationId, 60000); + const voltageResultsPayload = await pollResults( + simulationId, + "voltage_results", + (p) => p && Array.isArray(p.success) && p.success.length >= EXPECTED_VOLTAGE_RESULTS.length, + 60000 + ); + + const stdoutPayload = await pollResults( + simulationId, + "STDOUT", + (p) => p && p.success && p.content, + 30000 + ); + + assert.ok( + EXPECTED_STDOUT.test(stdoutPayload.content), + `Expected ApPredict args string not found in STDOUT. Got: ${stdoutPayload.content}` + ); + const actualVoltageResults = voltageResultsPayload.success; assert.ok(Array.isArray(actualVoltageResults), "Expected an array in voltage_results response"); @@ -149,15 +207,15 @@ test("posts inline request payload and validates voltage_results", { timeout: 12 const actualRow = actualVoltageResults[i]; const expectedRow = EXPECTED_VOLTAGE_RESULTS[i]; - assert.equal(actualRow.c, expectedRow.c, `row ${i} c`); - assertAlmostEqual(actualRow.uv, expectedRow.uv, 5, `row ${i} uv`); - assertAlmostEqual(actualRow.pv, expectedRow.pv, 1, `row ${i} pv`); - assertAlmostEqual(actualRow.a50, expectedRow.a50, 5, `row ${i} a50`); - assertAlmostEqual(actualRow.a90, expectedRow.a90, 5, `row ${i} a90`); + assert.equal(actualRow.c, expectedRow.c, `voltage_results ${i} c`); + assertAlmostEqual(actualRow.uv, expectedRow.uv, 5, `voltage_results ${i} uv`); + assertAlmostEqual(actualRow.pv, expectedRow.pv, 1, `voltage_results ${i} pv`); + assertAlmostEqual(actualRow.a50, expectedRow.a50, 5, `voltage_results ${i} a50`); + assertAlmostEqual(actualRow.a90, expectedRow.a90, 5, `voltage_results ${i} a90`); - assert.equal(actualRow.da90.length, expectedRow.da90.length, `row ${i} da90 length should match`); + assert.equal(actualRow.da90.length, expectedRow.da90.length, `voltage_results ${i} da90 length should match`); for (let j = 0; j < expectedRow.da90.length; j += 1) { - assertAlmostEqual(actualRow.da90[j], expectedRow.da90[j], 0.5, `row ${i} da90[${j}]`); + assertAlmostEqual(actualRow.da90[j], expectedRow.da90[j], 0.2, `voltage_results ${i} da90[${j}]`); } } }); From cfda812d4497cd7dc7f31b62012ef280e6d459fb Mon Sep 17 00:00:00 2001 From: Kwabena Amponsah Date: Wed, 27 May 2026 12:10:49 +0100 Subject: [PATCH 11/13] Log test simulation results --- test/simulation.test.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/simulation.test.js b/test/simulation.test.js index 65d519e..3b1da2d 100644 --- a/test/simulation.test.js +++ b/test/simulation.test.js @@ -183,6 +183,9 @@ test("posts request payload and validates output", { timeout: 120000 }, async () 30000 ); + console.log("Full STDOUT:\n", stdoutPayload.content, "\n\n"); + console.log("Full voltage_results:\n", JSON.stringify(voltageResultsPayload.success, null, 2), "\n\n"); + assert.ok( EXPECTED_STDOUT.test(stdoutPayload.content), `Expected ApPredict args string not found in STDOUT. Got: ${stdoutPayload.content}` From c31941c94e4673c6c8cdc4a6f2aa89c3404274f3 Mon Sep 17 00:00:00 2001 From: Kwabena Amponsah Date: Wed, 27 May 2026 13:06:51 +0100 Subject: [PATCH 12/13] Increase voltage_results wait time --- test/simulation.test.js | 234 +++++++++++++++++++++++----------------- 1 file changed, 135 insertions(+), 99 deletions(-) diff --git a/test/simulation.test.js b/test/simulation.test.js index 3b1da2d..3de49e6 100644 --- a/test/simulation.test.js +++ b/test/simulation.test.js @@ -31,7 +31,7 @@ function assertAlmostEqual(actual, expected, tolerance, label) { const difference = Math.abs(actualNumber - expectedNumber); assert.ok( difference <= tolerance, - `${label} differs by ${difference}, tolerance is ${tolerance}. actual=${actualNumber}, expected=${expectedNumber}` + `${label} differs by ${difference}, tolerance is ${tolerance}. actual=${actualNumber}, expected=${expectedNumber}`, ); } @@ -96,7 +96,7 @@ async function pollResults(simulationId, operation, isReady, timeoutMs) { const payload = await requestJson( `${BASE_URL}/api/collection/${simulationId}/${operation}`, { method: "GET" }, - `${operation} query` + `${operation} query`, ); lastPayload = payload; @@ -108,117 +108,153 @@ async function pollResults(simulationId, operation, isReady, timeoutMs) { } throw new Error( - `Timed out waiting for ${operation} for simulation ${simulationId}. Last payload: ${JSON.stringify(lastPayload)}` + `Timed out waiting for ${operation} for simulation ${simulationId}. Last payload: ${JSON.stringify(lastPayload)}`, ); } // Test the simulation endpoint by posting a request and validating the output. -test("posts request payload and validates output", { timeout: 120000 }, async () => { - - await waitForServerReady(30000); - - const REQUEST_PAYLOAD = { - modelId: 1, - pacingFrequency: 0.5, - plasmaPoints: [0, 3, 10, 30, 100] - }; - - const EXPECTED_STDOUT = /^ApPredict args : --pacing-freq 0\.5 --plasma-concs 0 3 10 30 100 --model 1$/m; - - const EXPECTED_VOLTAGE_RESULTS = [ - { - c: "Concentration(uM)", - uv: "UpstrokeVelocity(mV/ms)", - pv: "PeakVm(mV)", - a50: "APD50(ms)", - a90: "APD90(ms)", - da90: ["delta_APD90(%)"] - }, - { - c: "0", - uv: "350.596", - pv: "47.8243", - a50: "184.455", - a90: "216.892", - da90: ["0"] - }, - { - c: "0.001", - uv: "351.022", - pv: "47.8407", - a50: "184.7", - a90: "217.155", - da90: ["0.121071"] - } - ]; - - const requestBody = JSON.stringify(REQUEST_PAYLOAD); - const postPayload = await requestJson( - `${BASE_URL}/`, - { - method: "POST", - headers: { - "Content-Type": "application/json" +test( + "posts request payload and validates output", + { timeout: 600000 }, // 10m + async () => { + + await waitForServerReady(30000); // 30s + + const REQUEST_PAYLOAD = { + modelId: 1, + pacingFrequency: 0.5, + plasmaPoints: [0, 3, 10, 30, 100], + }; + + const EXPECTED_STDOUT = /^ApPredict args : --pacing-freq 0\.5 --plasma-concs 0 3 10 30 100 --model 1$/m; + + const EXPECTED_VOLTAGE_RESULTS = [ + { + c: "Concentration(uM)", + uv: "UpstrokeVelocity(mV/ms)", + pv: "PeakVm(mV)", + a50: "APD50(ms)", + a90: "APD90(ms)", + da90: ["delta_APD90(%)"], }, - body: requestBody - }, - "Simulation start" - ); - assert.ok(postPayload.success, `Expected success object in POST response. Got: ${JSON.stringify(postPayload)}`); - assert.ok(postPayload.success.id, `Expected simulation id in POST response. Got: ${JSON.stringify(postPayload)}`); + { + c: "0", + uv: "353.889", + pv: "47.9938", + a50: "186.989", + a90: "219.604", + da90: ["0"], + }, + { + c: "0.001", + uv: "353.89", + pv: "47.9938", + a50: "186.989", + a90: "219.604", + da90: ["-5.91879e-05"], + }, + { + c: "3", + uv: "353.891", + pv: "47.9939", + a50: "186.989", + a90: "219.604", + da90: ["7.21773e-05"], + }, + { + c: "10", + uv: "353.89", + pv: "47.9938", + a50: "186.989", + a90: "219.604", + da90: ["-6.13187e-05"], + }, + { + c: "30", + uv: "353.889", + pv: "47.9938", + a50: "186.989", + a90: "219.604", + da90: ["2.5213e-05"], + }, + { + c: "100", + uv: "353.89", + pv: "47.9938", + a50: "186.989", + a90: "219.604", + da90: ["-3.50691e-05"], + }, + ]; + + const requestBody = JSON.stringify(REQUEST_PAYLOAD); + const postPayload = await requestJson( + `${BASE_URL}/`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: requestBody, + }, + "Simulation start", + ); + assert.ok(postPayload.success, `Expected success object in POST response. Got: ${JSON.stringify(postPayload)}`); + assert.ok(postPayload.success.id, `Expected simulation id in POST response. Got: ${JSON.stringify(postPayload)}`); - const simulationId = postPayload.success.id; + const simulationId = postPayload.success.id; - const voltageResultsPayload = await pollResults( - simulationId, - "voltage_results", - (p) => p && Array.isArray(p.success) && p.success.length >= EXPECTED_VOLTAGE_RESULTS.length, - 60000 - ); + const voltageResultsPayload = await pollResults( + simulationId, + "voltage_results", + (p) => p && Array.isArray(p.success) && p.success.length >= EXPECTED_VOLTAGE_RESULTS.length, + 300000, // 5m + ); - const stdoutPayload = await pollResults( - simulationId, - "STDOUT", - (p) => p && p.success && p.content, - 30000 - ); + const stdoutPayload = await pollResults( + simulationId, + "STDOUT", + (p) => p && p.success && p.content, + 30000, // 30s + ); - console.log("Full STDOUT:\n", stdoutPayload.content, "\n\n"); - console.log("Full voltage_results:\n", JSON.stringify(voltageResultsPayload.success, null, 2), "\n\n"); + console.log("Full STDOUT:\n", stdoutPayload.content, "\n\n"); + console.log("Full voltage_results:\n", JSON.stringify(voltageResultsPayload.success, null, 2), "\n\n"); - assert.ok( - EXPECTED_STDOUT.test(stdoutPayload.content), - `Expected ApPredict args string not found in STDOUT. Got: ${stdoutPayload.content}` - ); + assert.ok( + EXPECTED_STDOUT.test(stdoutPayload.content), + `Expected ApPredict args string not found in STDOUT. Got: ${stdoutPayload.content}`, + ); - const actualVoltageResults = voltageResultsPayload.success; - assert.ok(Array.isArray(actualVoltageResults), "Expected an array in voltage_results response"); + const actualVoltageResults = voltageResultsPayload.success; + assert.ok(Array.isArray(actualVoltageResults), "Expected an array in voltage_results response"); - assert.equal( - actualVoltageResults.length, - EXPECTED_VOLTAGE_RESULTS.length, - "actual and expected voltage_results lengths should match" - ); + assert.equal( + actualVoltageResults.length, + EXPECTED_VOLTAGE_RESULTS.length, + "actual and expected voltage_results lengths should match", + ); - assert.deepStrictEqual( - actualVoltageResults[0], - EXPECTED_VOLTAGE_RESULTS[0], - "voltage_results header row does not match expected values" - ); + assert.deepStrictEqual( + actualVoltageResults[0], + EXPECTED_VOLTAGE_RESULTS[0], + "voltage_results header row does not match expected values", + ); - for (let i = 1; i < EXPECTED_VOLTAGE_RESULTS.length; i += 1) { - const actualRow = actualVoltageResults[i]; - const expectedRow = EXPECTED_VOLTAGE_RESULTS[i]; + for (let i = 1; i < EXPECTED_VOLTAGE_RESULTS.length; i += 1) { + const actualRow = actualVoltageResults[i]; + const expectedRow = EXPECTED_VOLTAGE_RESULTS[i]; - assert.equal(actualRow.c, expectedRow.c, `voltage_results ${i} c`); - assertAlmostEqual(actualRow.uv, expectedRow.uv, 5, `voltage_results ${i} uv`); - assertAlmostEqual(actualRow.pv, expectedRow.pv, 1, `voltage_results ${i} pv`); - assertAlmostEqual(actualRow.a50, expectedRow.a50, 5, `voltage_results ${i} a50`); - assertAlmostEqual(actualRow.a90, expectedRow.a90, 5, `voltage_results ${i} a90`); + assert.equal(actualRow.c, expectedRow.c, `voltage_results ${i} c`); + assertAlmostEqual(actualRow.uv, expectedRow.uv, 5, `voltage_results ${i} uv`); + assertAlmostEqual(actualRow.pv, expectedRow.pv, 1, `voltage_results ${i} pv`); + assertAlmostEqual(actualRow.a50, expectedRow.a50, 5, `voltage_results ${i} a50`); + assertAlmostEqual(actualRow.a90, expectedRow.a90, 5, `voltage_results ${i} a90`); - assert.equal(actualRow.da90.length, expectedRow.da90.length, `voltage_results ${i} da90 length should match`); - for (let j = 0; j < expectedRow.da90.length; j += 1) { - assertAlmostEqual(actualRow.da90[j], expectedRow.da90[j], 0.2, `voltage_results ${i} da90[${j}]`); + assert.equal(actualRow.da90.length, expectedRow.da90.length, `voltage_results ${i} da90 length should match`); + for (let j = 0; j < expectedRow.da90.length; j += 1) { + assertAlmostEqual(actualRow.da90[j], expectedRow.da90[j], 0.2, `voltage_results ${i} da90[${j}]`); + } } - } -}); + }, +); From cc1c248a9efd03311219ad41c06a255327708dc5 Mon Sep 17 00:00:00 2001 From: Kwabena Amponsah Date: Wed, 27 May 2026 13:18:21 +0100 Subject: [PATCH 13/13] Update test tolerances --- test/simulation.test.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/simulation.test.js b/test/simulation.test.js index 3de49e6..a21e094 100644 --- a/test/simulation.test.js +++ b/test/simulation.test.js @@ -246,14 +246,14 @@ test( const expectedRow = EXPECTED_VOLTAGE_RESULTS[i]; assert.equal(actualRow.c, expectedRow.c, `voltage_results ${i} c`); - assertAlmostEqual(actualRow.uv, expectedRow.uv, 5, `voltage_results ${i} uv`); - assertAlmostEqual(actualRow.pv, expectedRow.pv, 1, `voltage_results ${i} pv`); - assertAlmostEqual(actualRow.a50, expectedRow.a50, 5, `voltage_results ${i} a50`); - assertAlmostEqual(actualRow.a90, expectedRow.a90, 5, `voltage_results ${i} a90`); + assertAlmostEqual(actualRow.uv, expectedRow.uv, 1e-3, `voltage_results ${i} uv`); + assertAlmostEqual(actualRow.pv, expectedRow.pv, 1e-3, `voltage_results ${i} pv`); + assertAlmostEqual(actualRow.a50, expectedRow.a50, 1e-3, `voltage_results ${i} a50`); + assertAlmostEqual(actualRow.a90, expectedRow.a90, 1e-3, `voltage_results ${i} a90`); assert.equal(actualRow.da90.length, expectedRow.da90.length, `voltage_results ${i} da90 length should match`); for (let j = 0; j < expectedRow.da90.length; j += 1) { - assertAlmostEqual(actualRow.da90[j], expectedRow.da90[j], 0.2, `voltage_results ${i} da90[${j}]`); + assertAlmostEqual(actualRow.da90[j], expectedRow.da90[j], 1e-3, `voltage_results ${i} da90[${j}]`); } } },