diff --git a/vcell-client/src/main/resources/bioModelsNetInfo.xml b/vcell-client/src/main/resources/bioModelsNetInfo.xml
index 5321e97859..55c424482e 100644
--- a/vcell-client/src/main/resources/bioModelsNetInfo.xml
+++ b/vcell-client/src/main/resources/bioModelsNetInfo.xml
@@ -14,7 +14,7 @@
-
+
@@ -174,7 +174,7 @@
-
+
@@ -255,7 +255,7 @@
-
+
@@ -301,7 +301,7 @@
-
+
@@ -339,7 +339,7 @@
-
+
@@ -456,7 +456,7 @@
-
+
@@ -498,7 +498,7 @@
-
+
@@ -519,7 +519,7 @@
-
+
@@ -533,10 +533,10 @@
-
-
-
-
+
+
+
+
@@ -545,8 +545,8 @@
-
-
+
+
@@ -561,7 +561,7 @@
-
+
@@ -576,7 +576,7 @@
-
+
@@ -588,17 +588,17 @@
-
+
-
-
-
+
+
+
-
+
-
+
@@ -612,7 +612,7 @@
-
+
@@ -627,11 +627,11 @@
-
+
-
+
@@ -702,12 +702,12 @@
-
+
-
+
@@ -736,7 +736,7 @@
-
+
@@ -761,7 +761,7 @@
-
+
@@ -783,7 +783,7 @@
-
+
@@ -791,7 +791,7 @@
-
+
@@ -801,7 +801,7 @@
-
+
@@ -811,7 +811,7 @@
-
+
@@ -819,17 +819,17 @@
-
+
-
-
-
+
+
+
-
+
@@ -846,7 +846,7 @@
-
+
@@ -864,12 +864,12 @@
-
+
-
+
@@ -896,7 +896,7 @@
-
+
@@ -905,7 +905,7 @@
-
+
@@ -922,7 +922,7 @@
-
+
@@ -949,7 +949,7 @@
-
+
@@ -962,11 +962,11 @@
-
-
-
+
+
+
-
+
@@ -985,7 +985,7 @@
-
+
@@ -1000,7 +1000,7 @@
-
+
@@ -1016,7 +1016,7 @@
-
+
@@ -1039,9 +1039,9 @@
-
+
-
+
@@ -1053,9 +1053,9 @@
-
-
-
-
+
+
+
+
diff --git a/vcell-client/src/test/java/cbit/vcell/client/desktop/biomodel/BioModelsNetInfoTest.java b/vcell-client/src/test/java/cbit/vcell/client/desktop/biomodel/BioModelsNetInfoTest.java
new file mode 100644
index 0000000000..74ad1fbca9
--- /dev/null
+++ b/vcell-client/src/test/java/cbit/vcell/client/desktop/biomodel/BioModelsNetInfoTest.java
@@ -0,0 +1,169 @@
+package cbit.vcell.client.desktop.biomodel;
+
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+/**
+ * Keeps {@code bioModelsNetInfo.xml} -- the list behind the desktop's BMDB tab -- honest about
+ * which BioModels Database models VCell can actually open.
+ *
+ *
The tab marks each model supported or not; unsupported ones get a warning icon reading "model
+ * not compatible with vCell", so that a user is not surprised by an import that fails. That list was
+ * hand-maintained and had drifted: 66 models were being flagged as incompatible that VCell imports
+ * perfectly well, and one was flagged compatible that does not import.
+ *
+ *
The authority for "can VCell open this" is {@code test_cases.ndjson}, which the BMDB nightly
+ * executes against the real collection. This test derives the {@code Supported} attribute from it
+ * and fails if the checked-in XML disagrees, so the list cannot silently go stale again as the
+ * importer improves.
+ *
+ *
To accept a legitimate change, regenerate and commit:
+ *
+ *
+ * mvn test -pl vcell-client -Dtest=BioModelsNetInfoTest -Dvcell.updateBioModelsNetInfo=true
+ *
+ *
+ * The nightly records execution -- import and simulation -- while this list is only
+ * about whether the model opens. So the mapping keys on the failure type: a model that imports and
+ * then fails in the solver is still perfectly loadable and stays supported. See
+ * {@link #IMPORT_BLOCKING_FAILURES}.
+ */
+@Tag("Fast")
+public class BioModelsNetInfoTest {
+
+ private static final Path XML = Paths.get("src/main/resources/bioModelsNetInfo.xml");
+ private static final Path NDJSON = Paths.get("../vcell-cli/src/main/resources/test_cases.ndjson");
+ private static final String UPDATE_PROPERTY = "vcell.updateBioModelsNetInfo";
+
+ /**
+ * Failure types that mean the model never became a BioModel. Everything else -- solver failures,
+ * divide-by-zero, the SEDML-level outcomes -- happens after a successful import, so the model
+ * still opens in the desktop and should not be flagged incompatible.
+ */
+ private static final Set IMPORT_BLOCKING_FAILURES = new HashSet<>(List.of(
+ "SBML_IMPORT_FAILURE",
+ "SBML_XML_NODE_FAILURE",
+ "UNSUPPORTED_NON_INT_STOCH",
+ "UNSUPPORTED_NON_NUMERIC_STOCH",
+ "UNSUPPORTED_NON_CONSTANT_COMPARTMENTS",
+ "UNSUPPORTED_DELAY_SBML",
+ "MATH_GENERATION_FAILURE"));
+
+ private static final Pattern ID_ATTR = Pattern.compile("ID=\"([^\"]+)\"");
+ private static final Pattern SUPPORTED_ATTR = Pattern.compile("Supported=\"(true|false)\"");
+
+ @Test
+ public void supportedFlagsMatchTheNightlyResults() throws IOException {
+ assertTrue(Files.exists(XML), XML + " not found (run from the vcell-client module directory)");
+ if(!Files.exists(NDJSON)){
+ // vcell-cli is a sibling module, not a dependency; if the checkout is partial there is
+ // nothing to compare against and this check simply does not apply.
+ System.out.println("skipping: " + NDJSON + " not present");
+ return;
+ }
+
+ Map importsOk = readNightlyImportResults();
+ // Split keeping the terminators: this file is CRLF, and rewriting it with the platform
+ // separator would turn a handful of attribute edits into a whole-file diff.
+ String original = new String(Files.readAllBytes(XML), StandardCharsets.UTF_8);
+ List lines = splitKeepingLineEndings(original);
+ List updated = new ArrayList<>(lines.size());
+ List changes = new ArrayList<>();
+
+ for(String line : lines){
+ Matcher id = ID_ATTR.matcher(line);
+ Matcher supported = SUPPORTED_ATTR.matcher(line);
+ if(!id.find() || !supported.find()){
+ updated.add(line);
+ continue;
+ }
+ Boolean expected = importsOk.get(id.group(1));
+ if(expected == null){
+ updated.add(line); // no nightly evidence: leave the curated value alone
+ continue;
+ }
+ boolean current = Boolean.parseBoolean(supported.group(1));
+ if(current == expected){
+ updated.add(line);
+ continue;
+ }
+ changes.add(id.group(1) + ": Supported " + current + " -> " + expected);
+ updated.add(supported.replaceFirst("Supported=\"" + expected + "\""));
+ }
+
+ if(changes.isEmpty()){
+ return;
+ }
+ if(Boolean.getBoolean(UPDATE_PROPERTY)){
+ Files.write(XML, String.join("", updated).getBytes(StandardCharsets.UTF_8));
+ System.out.println("updated " + XML + " (" + changes.size() + " models):");
+ changes.forEach(c -> System.out.println(" " + c));
+ return;
+ }
+ fail(changes.size() + " model(s) in " + XML.getFileName() + " disagree with the BMDB nightly"
+ + " results in test_cases.ndjson:\n " + String.join("\n ", changes)
+ + "\n\nRegenerate with: mvn test -pl vcell-client -Dtest=" + getClass().getSimpleName()
+ + " -D" + UPDATE_PROPERTY + "=true");
+ }
+
+ /** Splits on line boundaries but keeps the terminators, so the file can be rewritten byte-for-byte. */
+ private static List splitKeepingLineEndings(String text){
+ List lines = new ArrayList<>();
+ Matcher m = Pattern.compile("[^\\r\\n]*(\\r\\n|\\r|\\n|$)").matcher(text);
+ int end = 0;
+ while(m.find() && m.start() < text.length()){
+ lines.add(m.group());
+ end = m.end();
+ }
+ if(end < text.length()){
+ lines.add(text.substring(end));
+ }
+ return lines;
+ }
+
+ /** BioModels id -> whether the nightly shows VCell importing it. */
+ private static Map readNightlyImportResults() throws IOException {
+ Map result = new HashMap<>();
+ for(String line : Files.readAllLines(NDJSON, StandardCharsets.UTF_8)){
+ String trimmed = line.trim();
+ if(trimmed.isEmpty() || !trimmed.contains("\"SYSBIO_BIOMD\"")){
+ continue;
+ }
+ String id = jsonString(trimmed, "file_path");
+ String status = jsonString(trimmed, "known_status");
+ if(id == null || status == null || "SKIP".equals(status)){
+ continue; // SKIP carries no evidence either way
+ }
+ id = id.replace(".omex", "");
+ String failureType = jsonString(trimmed, "known_failure_type");
+ result.put(id, "PASS".equals(status) || !IMPORT_BLOCKING_FAILURES.contains(failureType));
+ }
+ return result;
+ }
+
+ /**
+ * Reads one string field. Deliberately not a JSON parser: vcell-client has no JSON dependency,
+ * and these fields are flat strings written by the same tool every night.
+ */
+ private static String jsonString(String json, String field){
+ Matcher m = Pattern.compile("\"" + field + "\"\\s*:\\s*\"([^\"]*)\"").matcher(json);
+ return m.find() ? m.group(1) : null;
+ }
+}