feat(firmware): support rocking-horse workbench delivery
This commit is contained in:
@@ -1,12 +1,17 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import json
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
import urllib.request
|
||||
import zipfile
|
||||
from http.server import ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from cognitive_agent.application.firmware_workbench import FirmwareWorkbenchService
|
||||
from cognitive_agent.auth import AuthConfig, AuthRuntime
|
||||
@@ -75,8 +80,14 @@ class TestFirmwareWorkbenchApi:
|
||||
with urllib.request.urlopen(request, timeout=5) as response:
|
||||
return response.status, json.loads(response.read().decode())
|
||||
|
||||
def test_real_http_task_publish_detail_and_download_flow(self) -> None:
|
||||
instruction = "增加位置控制、座椅通风和座椅加热功能"
|
||||
@pytest.mark.parametrize("instruction,capabilities", [
|
||||
("增加位置控制、座椅通风和座椅加热功能", ["时间控制", "位置控制", "座椅通风", "座椅加热"]),
|
||||
("增加动态木马功能", ["时间控制", "位置控制", "动态木马"]),
|
||||
("增加动态木马、座椅通风和座椅加热功能", ["时间控制", "位置控制", "座椅通风", "座椅加热", "动态木马"]),
|
||||
])
|
||||
def test_real_http_task_publish_detail_and_download_flow(
|
||||
self, instruction: str, capabilities: list[str], monkeypatch
|
||||
) -> None:
|
||||
status, created = self.post(
|
||||
"/api/firmware-workbench/tasks", {"instruction": instruction}
|
||||
)
|
||||
@@ -135,6 +146,26 @@ class TestFirmwareWorkbenchApi:
|
||||
assert response.headers["Content-Type"] == "application/zip"
|
||||
assert "attachment" in response.headers["Content-Disposition"]
|
||||
assert len(package) == artifact["size_bytes"]
|
||||
assert artifact["capabilities"] == capabilities
|
||||
with zipfile.ZipFile(io.BytesIO(package)) as archive:
|
||||
assert json.loads(archive.read("manifest.json"))["capabilities"] == capabilities
|
||||
|
||||
frames = []
|
||||
|
||||
def publish(frame):
|
||||
frames.append(frame)
|
||||
return {"status": "published"}
|
||||
|
||||
monkeypatch.setattr(
|
||||
SeatDemoHandler, "service", SimpleNamespace(publish_firmware_notification=publish)
|
||||
)
|
||||
notify_status, notified = self.post(
|
||||
f"/api/firmware-workbench/artifacts/{artifact_id}/notify", {}
|
||||
)
|
||||
assert notify_status == 200
|
||||
assert notified["published_count"] == 3
|
||||
assert [frame["sequence"] for frame in frames] == [1, 2, 3]
|
||||
assert all(frame["firmware"]["release_id"] == release_id for frame in frames)
|
||||
|
||||
delete_request = urllib.request.Request(
|
||||
f"{self.base_url}/api/firmware-workbench/artifacts/{artifact_id}",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -58,8 +58,8 @@ def test_workbench_exposes_artifact_actions_and_always_starts_a_new_task() -> No
|
||||
"/firmware-workbench-entry.js?v=3",
|
||||
"/firmware-workbench.html",
|
||||
"/firmware-workbench-theme.js?v=1",
|
||||
"/firmware-workbench.css?v=7",
|
||||
"/firmware-workbench.js?v=4",
|
||||
"/firmware-workbench.css?v=8",
|
||||
"/firmware-workbench.js?v=5",
|
||||
):
|
||||
assert f'"{asset}"' in worker
|
||||
for runtime_file in (
|
||||
|
||||
Reference in New Issue
Block a user