feat(firmware): support rocking-horse workbench delivery

This commit is contained in:
Miayui
2026-09-22 11:15:13 +08:00
parent ba5dfcd6da
commit 4d99702acd
13 changed files with 250 additions and 29 deletions
+88
View File
@@ -7,10 +7,14 @@ import time
import zipfile
from pathlib import Path
import pytest
from cognitive_agent.application.firmware_workbench import (
FINAL_RELEASE_MESSAGE,
FirmwareWorkbenchError,
FirmwareWorkbenchService,
)
from cognitive_agent.infrastructure.can.gateway import CanGatewayError
ROOT = Path(__file__).resolve().parents[2]
@@ -213,6 +217,8 @@ def test_position_instruction_merges_with_time_only_baseline(tmp_path: Path) ->
assert artifact["capability_diff"]["added"] == ["位置控制"]
assert "vent" not in artifact["firmware_filename"]
assert "heat" not in artifact["firmware_filename"]
profile_path = tmp_path / "workbench" / "tasks" / task["id"] / "feature_profile.json"
assert json.loads(profile_path.read_text("utf-8"))["rocking_horse"] is False
service.close()
@@ -251,3 +257,85 @@ def test_running_pipeline_can_be_canceled_and_retried(tmp_path: Path) -> None:
completed = wait_for_terminal(service, task_id)
assert completed["status"] == "success"
service.close()
@pytest.mark.parametrize("instruction", [
"增加动态木马功能",
"增加木马功能",
"Add rocking_horse",
"Add Rocking Horse",
"Add rocking-horse",
])
def test_rocking_horse_package_and_notification(tmp_path: Path, instruction: str) -> None:
image = make_image(tmp_path)
profile = {
"position_control": True, "ventilation": False,
"heating": False, "rocking_horse": True,
}
def build_runner(task_id: str, log: object, cancel: threading.Event) -> Path:
workspace = tmp_path / "workbench" / "tasks" / task_id
assert json.loads((workspace / "feature_profile.json").read_text("utf-8")) == profile
return image
service = FirmwareWorkbenchService(
ROOT, artifact_root=tmp_path / "workbench", ota_root=tmp_path / "ota",
build_runner=build_runner,
)
try:
started = service.start_task(instruction)
task = wait_for_terminal(service, started["task"]["id"])
assert task["status"] == "success", task.get("failure_reason")
assert [stage["status"] for stage in task["stages"]] == ["success"] * 8
assert task["target_capabilities"] == ["时间控制", "位置控制", "动态木马"]
assert any(item["name"] == "动态木马控制与停止保护" for item in task["test_results"])
artifact = service.artifact(task["artifact_id"])
assert artifact["capability_diff"]["added"] == ["位置控制", "动态木马"]
assert "rocking-horse" in artifact["firmware_filename"]
package, _ = service.resolve_release_download(task["release_id"])
with zipfile.ZipFile(package) as archive:
assert json.loads(archive.read("manifest.json"))["capabilities"] == task["target_capabilities"]
assert json.loads(archive.read("feature_profile.json")) == profile
assert json.loads(archive.read("requirements.json"))["feature_profile"] == profile
frames = []
def publish(frame):
frames.append(frame)
return {"status": "published"}
result = service.notify_artifact(artifact["id"], publish)
assert result["published_count"] == 3
assert [frame["sequence"] for frame in frames] == [1, 2, 3]
assert len({frame["message_id"] for frame in frames}) == 1
assert all(frame["firmware"]["artifact_name"] == artifact["firmware_filename"] for frame in frames)
assert all(frame["firmware"]["sha256"] == artifact["sha256"] for frame in frames)
assert all(frame["auto_upgrade"] is False for frame in frames)
def unavailable(frame):
raise CanGatewayError("broker unavailable")
with pytest.raises(FirmwareWorkbenchError, match="0/3") as error:
service.notify_artifact(artifact["id"], unavailable)
assert error.value.code == "notification_publish_failed"
assert service.artifact(artifact["id"])["status"] == "published"
finally:
service.close()
def test_rocking_horse_build_failure_does_not_publish(tmp_path: Path) -> None:
def fail_build(task_id: str, log: object, cancel: threading.Event) -> Path:
raise RuntimeError("IAR build failed")
service = FirmwareWorkbenchService(
ROOT, artifact_root=tmp_path / "workbench", ota_root=tmp_path / "ota",
build_runner=fail_build,
)
try:
started = service.start_task("增加动态木马功能")
task = wait_for_terminal(service, started["task"]["id"])
assert task["status"] == "failed"
assert task["failure_reason"] == "IAR build failed"
assert service.snapshot()["releases"] == []
assert all(item.get("is_baseline") for item in service.snapshot()["artifacts"])
finally:
service.close()