From 0ddf524bf0d62e4930321c4a3a0eae433e634098 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Fri, 21 Aug 2026 19:36:03 -0700 Subject: [PATCH] fix: inherit the workflow's application version and build on toggle relaxation [release] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit toggleRelaxation() looked the relaxation subworkflow up from standata by application name only, then stamped the template's own hardcoded application onto the inserted subworkflow — espresso 6.3/GNU, vasp 5.4.4/GNU. The version and build already in use by the workflow were read for the lookup and then discarded, so Version and Build rendered blank whenever the deployment's registry no longer offered the template's hardcoded pair: ave builds those option lists from ApplicationRegistry filtered by name and version, and a value absent from the list shows as nothing selected. Stamp the workflow's application into the subworkflow config, at the subworkflow level and on every execution unit, and construct from that. The constructor then re-resolves each unit's executable, flavor and input templates against the inherited version and serializes the result. Read it before inserting: addSubworkflow(_, true) unshifts, so subworkflowInstances[0] is a different subworkflow afterwards. Deliberately not Subworkflow.setApplication(), despite that being the path the manual Version dropdown uses. It updates unit instances without re-serializing `units`, so the inherited version is lost through the JSON round-trip web-app's store performs; it replaces the model with the application default, swapping the vasp relaxation template's method.subtype from paw to us; and it never reaches setDefaultInput(), so stale input templates survive a version change. The setter's serialization gap still affects the manual dropdown and needs its own ticket. getStandataRelaxationSubworkflow() no longer returns the execution unit's hardcoded application. The caller stamps the inherited one, and the other caller matches on systemName, so the field was both dead and misleading. Also correct workflows/default.ts, whose application was a hand-written stub disagreeing with the registry in three fields: build "6.3" (a version string where a build name belongs), shortName "espresso" and summary "Quantum Espresso". Nothing resolved them before — executable lookup keys on name and version — but inheriting build "6.3" stamped a non-registry build onto the relaxation subworkflow, and ave's Build options for espresso 6.3 are ["GNU"], so the control rendered blank. That is this ticket's own symptom on /bank/workflows/new, which builds from defaultConfig. With the stub corrected the pre-existing assertion in Workflow.test.ts passes unchanged. Verified across every application the registry offers, asserting after a toJSON() round-trip rather than on live instances: espresso (4 builds) and vasp inherit at the subworkflow and unit level, both controls render a pair the registry offers, executable/flavor resolve, and each keeps its template's method; the seven applications with no relaxation template still toggle cleanly. SOF-7884 --- src/js/Workflow.ts | 21 ++++++---- src/js/workflows/default.ts | 6 +-- tests/js/Workflow.test.ts | 82 +++++++++++++++++++++++++++++++++++++ 3 files changed, 99 insertions(+), 10 deletions(-) diff --git a/src/js/Workflow.ts b/src/js/Workflow.ts index 855d23b3..70cbc578 100644 --- a/src/js/Workflow.ts +++ b/src/js/Workflow.ts @@ -347,7 +347,17 @@ class Workflow extends InMemoryEntity implements W } else { const vcRelax = this.getStandataRelaxationSubworkflow(); if (vcRelax) { - this.addSubworkflow(new Subworkflow(vcRelax), true); + const application = structuredClone(this.subworkflowInstances[0].application); + this.addSubworkflow( + new Subworkflow({ + ...vcRelax, + application, + units: vcRelax.units.map((unit) => + unit.type === UnitType.execution ? { ...unit, application } : unit, + ), + }), + true, + ); } } } @@ -370,15 +380,12 @@ class Workflow extends InMemoryEntity implements W return undefined; } - const executionUnit = subworkflow.units.find((unit) => unit.type === UnitType.execution); - if (!executionUnit) { + const hasExecutionUnit = subworkflow.units.some((unit) => unit.type === UnitType.execution); + if (!hasExecutionUnit) { throw new Error("Relaxation subworkflow is missing an execution unit"); } - return { - ...subworkflow, - application: executionUnit.application, - }; + return subworkflow; } private getRelaxationSubworkflow() { diff --git a/src/js/workflows/default.ts b/src/js/workflows/default.ts index 7b739599..0b01235a 100644 --- a/src/js/workflows/default.ts +++ b/src/js/workflows/default.ts @@ -8,9 +8,9 @@ const defaultWorkflowConfig: WorkflowSchema = { _id: "c6e9dbbee8929de01f4e76ee", application: { name: "espresso", - shortName: "espresso", - summary: "Quantum Espresso", - build: "6.3", + shortName: "qe", + summary: "Quantum ESPRESSO", + build: "GNU", version: "6.3", }, model: { diff --git a/tests/js/Workflow.test.ts b/tests/js/Workflow.test.ts index cb540c44..fdc6a39c 100644 --- a/tests/js/Workflow.test.ts +++ b/tests/js/Workflow.test.ts @@ -22,10 +22,12 @@ import type { WorkflowRenderContext } from "src/js/Workflow"; import { Subworkflow, UnitFactory, Workflow } from "../../src/js"; import { UnitType } from "../../src/js/enums"; +import type { ExecutionUnit } from "../../src/js/units"; import { repairWorkflow } from "../../src/js/utils/repair"; import type { WorkflowEntity } from "../../src/js/Workflow"; import type { WorkflowSchema } from "../../src/js/workflows/types"; import workflowHashes from "../fixtures/workflow_hashes.json"; +import { assertNotNull } from "./assertNotNull"; function invalidExecutionUnit(flowchartId: string) { return { @@ -195,6 +197,86 @@ describe("Workflow", () => { }); }); + it("inherits the existing subworkflow's application version and build", () => { + const config = structuredClone(Workflow.defaultConfig); + Object.assign(config.subworkflows[0].application, { build: "Intel", version: "7.5" }); + + const workflow = new Workflow(config); + workflow.toggleRelaxation(); + + const rehydrated = new Workflow(structuredClone(workflow.toJSON())); + const relaxation = assertNotNull( + rehydrated.subworkflowInstances.find( + (subworkflow) => subworkflow.systemName === "espresso-variable-cell-relaxation", + ), + ); + + expect(relaxation.application).to.include({ build: "Intel", version: "7.5" }); + + const executionUnit = assertNotNull( + relaxation.unitsInstances.find((unit) => unit.type === UnitType.execution), + ) as ExecutionUnit; + + expect(executionUnit.application).to.include({ build: "Intel", version: "7.5" }); + expect(executionUnit.executable?.name).to.equal("pw.x"); + expect(executionUnit.flavor?.name).to.equal("pw_vc-relax"); + }); + + it("inherits a version and build the registry actually offers", () => { + const offered = (application: { name: string; version: string; build: string }) => + new ApplicationRegistry() + .getApplications() + .some( + (candidate) => + candidate.name === application.name && + candidate.version === application.version && + candidate.build === application.build, + ); + + const workflow = new Workflow(structuredClone(Workflow.defaultConfig)); + workflow.toggleRelaxation(); + + const rehydrated = new Workflow(structuredClone(workflow.toJSON())); + const relaxation = assertNotNull( + rehydrated.subworkflowInstances.find( + (subworkflow) => subworkflow.systemName === "espresso-variable-cell-relaxation", + ), + ); + + const executionUnit = assertNotNull( + relaxation.unitsInstances.find((unit) => unit.type === UnitType.execution), + ) as ExecutionUnit; + + expect(offered(relaxation.application), "subworkflow application").to.equal(true); + expect(offered(executionUnit.application), "unit application").to.equal(true); + }); + + it("does not reset the model to the application default when inheriting", () => { + const config = structuredClone(Workflow.defaultConfig); + Object.assign(config.subworkflows[0].application, { + name: "vasp", + shortName: "vasp", + summary: "Vienna Ab-initio Simulation Package", + build: "GNU", + version: "5.4.4", + }); + + const workflow = new Workflow(config); + workflow.toggleRelaxation(); + + const rehydrated = new Workflow(structuredClone(workflow.toJSON())); + const relaxation = assertNotNull( + rehydrated.subworkflowInstances.find( + (subworkflow) => subworkflow.systemName === "vasp-variable-cell-relaxation", + ), + ); + + expect(relaxation.model.method).to.include({ + type: "pseudopotential", + subtype: "paw", + }); + }); + it("removes the added relaxation subworkflow on the next toggle", () => { const workflow = new Workflow(structuredClone(Workflow.defaultConfig)); const initialSubworkflowCount = workflow.subworkflowInstances.length;