更新v2版本适配skill
This commit is contained in:
1 parent
7611f13208
commit
2f3f7d00ec
58 files changed
+180419
-526
No files matched your search
@@ -0,0 +1,181 @@
|
||||
"""Skill authoring tests use the actual catalog, XML validator and compiler."""
|
||||
from copy import deepcopy
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SCRIPT = ROOT / "skills/system-simulation/scripts/project_builder.py"
|
||||
SPEC = importlib.util.spec_from_file_location("skill_project_builder", SCRIPT)
|
||||
builder = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(builder)
|
||||
PLAN_PATH = ROOT / "skills/system-simulation/assets/closed-tank.plan.json"
|
||||
|
||||
|
||||
class ProjectBuilderTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.plan = json.loads(PLAN_PATH.read_text(encoding="utf-8"))
|
||||
|
||||
def build(self):
|
||||
return builder.build_project(self.plan)[0]
|
||||
|
||||
def test_current_catalog_v2_and_si_defaults(self):
|
||||
original = deepcopy(self.plan)
|
||||
project, defaults = builder.build_project(self.plan)
|
||||
report, xml = builder.validate_project(project)
|
||||
self.assertEqual(self.plan, original)
|
||||
self.assertEqual(project["projectSchemaVersion"], 2)
|
||||
self.assertEqual(report["componentCount"], 4)
|
||||
self.assertEqual(report["compiledComponentCount"], 3)
|
||||
self.assertEqual(report["checks"]["simulation"], "notRun")
|
||||
self.assertEqual(report["checks"]["browserImport"], "notRun")
|
||||
self.assertIn("cvol", defaults["tank"])
|
||||
values = builder.semantic_signature(xml)["components"]["tank"]["parameters"]
|
||||
self.assertEqual(values["p0"], 250000)
|
||||
self.assertEqual(values["T0"], 293.15)
|
||||
self.assertEqual(values["cvol"], 0.057)
|
||||
self.assertEqual(project["nodes"][1]["data"]["parameters"]["p0"], 2.5)
|
||||
project["nodes"][1]["position"]["x"] = 999
|
||||
project["simulation"]["t_stop"] = 99
|
||||
self.assertEqual(self.plan, original)
|
||||
|
||||
def test_signal_snapshot_omits_null_and_keeps_registered_version(self):
|
||||
self.plan["components"] = [{"id": "step", "modelType": "amesim_step0"}]
|
||||
self.plan["connections"] = []
|
||||
project = self.build()
|
||||
data = project["nodes"][0]["data"]
|
||||
self.assertNotIn("positiveFlowDirection", data["ports"][0])
|
||||
from app.simulation.registry import get_component_model_spec
|
||||
self.assertEqual(data["modelVersion"], get_component_model_spec("amesim_step0").model_version)
|
||||
with self.assertRaisesRegex(builder.InputError, "expected one connection"):
|
||||
builder.validate_project(project)
|
||||
|
||||
def test_unknown_or_unit_only_parameters_are_rejected(self):
|
||||
for field, value in (("parameters", {"typoPressure": 2}),
|
||||
("parameterUnits", {"cvol": "L"})):
|
||||
with self.subTest(field=field):
|
||||
plan = deepcopy(self.plan)
|
||||
plan["components"][1][field] = value
|
||||
with self.assertRaises(builder.InputError):
|
||||
builder.build_project(plan)
|
||||
|
||||
def test_invalid_physical_values_units_and_discrete_choices_fail(self):
|
||||
for parameters, units in (({"p0": -1}, {}), ({"p0": 2}, {"p0": "kg"}),
|
||||
({"gi": "=1"}, {})):
|
||||
with self.subTest(parameters=parameters, units=units):
|
||||
project = self.build()
|
||||
project["nodes"][1]["data"]["parameters"].update(parameters)
|
||||
project["nodes"][1]["data"]["parameterUnits"].update(units)
|
||||
with self.assertRaises(builder.InputError):
|
||||
builder.validate_project(project)
|
||||
|
||||
def test_missing_medium_reference_is_rejected(self):
|
||||
self.plan["components"][1]["parameters"]["gi"] = 2
|
||||
with self.assertRaises(builder.InputError):
|
||||
builder.validate_project(self.build())
|
||||
|
||||
def test_duplicate_ids_unconnected_and_invalid_endpoints_fail(self):
|
||||
cases = [lambda p: p["nodes"][1].update(id="air"),
|
||||
lambda p: p["edges"].pop(),
|
||||
lambda p: p["edges"][0].update(sourceHandle="port_99"),
|
||||
lambda p: p["edges"][0].update(source="missing")]
|
||||
for modify in cases:
|
||||
with self.subTest(modify=modify):
|
||||
project = self.build()
|
||||
modify(project)
|
||||
with self.assertRaises(builder.InputError):
|
||||
builder.validate_project(project)
|
||||
|
||||
def test_browser_specific_fields_are_checked_before_backend_defaults(self):
|
||||
cases = [lambda p: p["edges"][0]["data"].pop("isContactEdge"),
|
||||
lambda p: p["nodes"][1]["data"]["ports"][0].update(positiveFlowDirection=None),
|
||||
lambda p: p["nodes"][1].update(type="component"),
|
||||
lambda p: p["nodes"][1]["position"].update(x=float("nan")),
|
||||
lambda p: p["nodes"][1]["data"].update(mirrored="false")]
|
||||
for modify in cases:
|
||||
with self.subTest(modify=modify):
|
||||
project = self.build()
|
||||
modify(project)
|
||||
with self.assertRaises(builder.InputError):
|
||||
builder.validate_project(project)
|
||||
|
||||
def test_v1_si_and_v2_selected_units_have_equal_execution_inputs(self):
|
||||
current = self.build()
|
||||
legacy = deepcopy(current)
|
||||
legacy["projectSchemaVersion"] = 1
|
||||
legacy["nodes"][1]["data"]["parameters"].update(p0=250000, T0=293.15)
|
||||
before = builder.semantic_signature(builder.validate_project(legacy)[1])
|
||||
after = builder.semantic_signature(builder.validate_project(current)[1])
|
||||
self.assertEqual(before, after)
|
||||
# Relabelling v1 as v2 without conversion is a physical change.
|
||||
legacy["projectSchemaVersion"] = 2
|
||||
self.assertNotEqual(before, builder.semantic_signature(builder.validate_project(legacy)[1]))
|
||||
|
||||
def test_old_versions_warn_without_mutating_source(self):
|
||||
project = self.build()
|
||||
project["nodes"][1]["data"]["modelVersion"] = "0.0.0"
|
||||
original = deepcopy(project)
|
||||
report, _ = builder.validate_project(project)
|
||||
self.assertEqual(report["warnings"][0]["code"], "COMPONENT_MODEL_VERSION_WARNING")
|
||||
self.assertEqual(project, original)
|
||||
|
||||
def test_dynamic_ports_keep_full_snapshot_but_only_check_active_ports(self):
|
||||
self.plan["components"] = [
|
||||
{"id": "junction", "modelType": "amesim_lmechn1", "parameters": {"v1": 1}},
|
||||
{"id": "mass", "modelType": "amesim_mecmas21"},
|
||||
]
|
||||
self.plan["connections"] = [
|
||||
{"id": f"e{i}", "source": {"component": "junction", "port": f"port_{i}"},
|
||||
"target": {"component": "mass", "port": f"port_{i}"}} for i in (1, 2)
|
||||
]
|
||||
project = self.build()
|
||||
self.assertEqual(len(project["nodes"][0]["data"]["ports"]), 21)
|
||||
report, _ = builder.validate_project(project)
|
||||
self.assertEqual(report["checks"]["activePorts"], "passed")
|
||||
self.assertEqual(report["checks"]["networkCompile"], "passed")
|
||||
|
||||
def test_compare_ignores_layout_but_detects_changed_physics(self):
|
||||
project = self.build()
|
||||
before = builder.semantic_signature(builder.validate_project(project)[1])
|
||||
project["nodes"][1]["position"]["x"] = 900
|
||||
project["nodes"][1]["data"]["label"] = "Different label"
|
||||
self.assertEqual(before, builder.semantic_signature(builder.validate_project(project)[1]))
|
||||
project["nodes"][1]["data"]["parameters"]["p0"] = 3
|
||||
self.assertNotEqual(before, builder.semantic_signature(builder.validate_project(project)[1]))
|
||||
|
||||
def test_cli_writes_only_valid_new_files_and_compare_detects_changes(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
output = Path(directory) / "project.json"
|
||||
with patch.object(builder, "emit_json") as emit:
|
||||
self.assertEqual(builder.main(["build", str(PLAN_PATH), "--output", str(output)]), 0)
|
||||
self.assertEqual(emit.call_args.args[0]["checks"]["xmlV3"], "passed")
|
||||
data = output.read_bytes()
|
||||
self.assertEqual(builder.main(["build", str(PLAN_PATH), "--output", str(output)]), 2)
|
||||
self.assertEqual(output.read_bytes(), data)
|
||||
self.assertEqual(builder.main(["compare", str(output), str(output)]), 0)
|
||||
changed = json.loads(data)
|
||||
changed["nodes"][1]["data"]["parameters"]["p0"] = 3
|
||||
other = Path(directory) / "changed.json"
|
||||
other.write_text(json.dumps(changed), encoding="utf-8")
|
||||
self.assertEqual(builder.main(["compare", str(output), str(other)]), 2)
|
||||
invalid = deepcopy(self.plan)
|
||||
invalid["connections"] = []
|
||||
source = Path(directory) / "invalid-plan.json"
|
||||
source.write_text(json.dumps(invalid), encoding="utf-8")
|
||||
rejected = Path(directory) / "must-not-exist.json"
|
||||
self.assertEqual(builder.main(["build", str(source), "--output", str(rejected)]), 2)
|
||||
self.assertFalse(rejected.exists())
|
||||
|
||||
def test_catalog_filter_exposes_real_details_without_experimental_models(self):
|
||||
with patch.object(builder, "emit_json") as emit:
|
||||
self.assertEqual(builder.main(["catalog", "--model", "amesim_pnch023"]), 0)
|
||||
self.assertEqual(emit.call_args.args[0]["models"][0]["modelType"], "amesim_pnch023")
|
||||
self.assertIn("parameters", emit.call_args.args[0]["models"][0])
|
||||
self.assertEqual(builder.main(["catalog", "--model", "not-a-model"]), 2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user