更新v2版本适配skill

This commit is contained in:
ljz committed 2026-09-18 01:40:58 +08:00
1 parent 7611f13208
commit 2f3f7d00ec
58 files changed
+180419 -526

No files matched your search

+181
View File
@@ -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()