feat: add seat firmware workbench and OTA delivery

This commit is contained in:
vee
2026-09-16 17:55:30 +08:00
parent 3eeca2ea75
commit ab8e43050c
70 changed files with 22594 additions and 316 deletions
+253
View File
@@ -0,0 +1,253 @@
from __future__ import annotations
import hashlib
import json
import threading
import time
import zipfile
from pathlib import Path
from cognitive_agent.application.firmware_workbench import (
FINAL_RELEASE_MESSAGE,
FirmwareWorkbenchService,
)
ROOT = Path(__file__).resolve().parents[2]
def wait_for_terminal(service: FirmwareWorkbenchService, task_id: str) -> dict[str, object]:
deadline = time.monotonic() + 5
while time.monotonic() < deadline:
task = service.task(task_id)
if task["status"] not in {"waiting", "running", "pending_confirmation"}:
return task
time.sleep(0.02)
raise AssertionError("firmware pipeline did not reach a terminal state")
def make_image(tmp_path: Path) -> Path:
image = tmp_path / "Seat_Controller_demo.hex"
image.write_bytes(b":020000040000FA\n" + b":100000000102030405060708090A0B0C0D0E0F1068\n" * 80)
return image
def test_pipeline_publishes_a_downloadable_ota_package(
tmp_path: Path, monkeypatch
) -> None:
monkeypatch.setenv(
"FIRMWARE_WORKBENCH_OTA_PUBLIC_BASE_URL",
"http://192.168.43.130:8000/api/firmware-workbench/releases",
)
image = make_image(tmp_path)
def build_runner(
_task_id: str, log: object, _cancel: threading.Event
) -> Path:
log("info", "IAR build: 0 errors, 0 warnings")
return image
service = FirmwareWorkbenchService(
ROOT,
artifact_root=tmp_path / "workbench",
ota_root=tmp_path / "ota",
build_runner=build_runner,
)
instruction = "增加支持座椅位置参数控制的能力,增加座椅通风功能,增加座椅加热功能"
first = service.start_task(instruction)
task = wait_for_terminal(service, str(first["task"]["id"]))
assert task["status"] == "success"
assert task["message"] == FINAL_RELEASE_MESSAGE
assert [stage["status"] for stage in task["stages"]] == ["success"] * 8
assert len(task["test_results"]) == 7
snapshot = service.snapshot()
assert snapshot["baselines"] == [
{
"id": "time_only",
"name": "时间控制基线",
"capabilities": ["时间控制"],
}
]
assert len(snapshot["artifacts"]) == 2
assert len(snapshot["releases"]) == 1
artifact = next(item for item in snapshot["artifacts"] if item["task_id"] == task["id"])
baseline = next(item for item in snapshot["artifacts"] if item.get("is_baseline"))
release = snapshot["releases"][0]
assert baseline["capabilities"] == ["时间控制"]
assert baseline["protected"] is True
assert artifact["capabilities"] == ["时间控制", "位置控制", "座椅通风", "座椅加热"]
assert artifact["status"] == "published"
assert release["download_url"] == (
f"http://192.168.43.130:8000/api/firmware-workbench/releases/"
f"{release['id']}/download"
)
assert service.latest_release()["download_url"] == release["download_url"]
latest_manifest = json.loads((tmp_path / "ota" / "latest.json").read_text("utf-8"))
assert latest_manifest["download_url"] == release["download_url"]
package, filename = service.resolve_release_download(str(release["id"]))
assert filename == artifact["firmware_filename"]
assert hashlib.sha256(package.read_bytes()).hexdigest() == artifact["sha256"]
with zipfile.ZipFile(package) as archive:
assert {
"Seat_Controller_demo.hex",
"manifest.json",
"requirements.json",
"validation.json",
"feature_profile.json",
} <= set(archive.namelist())
assert (tmp_path / "ota" / "latest.json").is_file()
assert service.publish_artifact(str(artifact["id"]))["id"] == release["id"]
service.close()
monkeypatch.setenv(
"FIRMWARE_WORKBENCH_OTA_PUBLIC_BASE_URL",
"http://192.168.43.131:8000/api/firmware-workbench/releases",
)
migrated = FirmwareWorkbenchService(
ROOT,
artifact_root=tmp_path / "workbench",
ota_root=tmp_path / "ota",
build_runner=build_runner,
)
migrated_release = migrated.latest_release()
assert migrated_release["download_url"].startswith("http://192.168.43.131:8000/")
migrated_latest = json.loads((tmp_path / "ota" / "latest.json").read_text("utf-8"))
assert migrated_latest["download_url"] == migrated_release["download_url"]
migrated.close()
def test_repeated_instruction_always_creates_and_executes_a_new_task(tmp_path: Path) -> None:
image = make_image(tmp_path)
def build_runner(
_task_id: str, log: object, _cancel: threading.Event
) -> Path:
log("info", "IAR build: 0 errors, 0 warnings")
return image
service = FirmwareWorkbenchService(
ROOT,
artifact_root=tmp_path / "workbench",
ota_root=tmp_path / "ota",
build_runner=build_runner,
)
first = service.start_task("增加座椅加热功能。")
first_task = wait_for_terminal(service, str(first["task"]["id"]))
second = service.start_task("增加座椅加热功能。")
assert first["task"]["id"] != second["task"]["id"]
assert "duplicate" not in first
assert "duplicate" not in second
second_task = wait_for_terminal(service, str(second["task"]["id"]))
assert first_task["status"] == "success"
assert second_task["status"] == "success"
assert first_task["target_capabilities"] == ["时间控制", "座椅加热"]
assert second_task["target_capabilities"] == ["时间控制", "座椅加热"]
service.close()
def test_non_baseline_artifact_can_be_deleted_but_baseline_is_protected(tmp_path: Path) -> None:
image = make_image(tmp_path)
def build_runner(
_task_id: str, log: object, _cancel: threading.Event
) -> Path:
log("info", "IAR build: 0 errors, 0 warnings")
return image
service = FirmwareWorkbenchService(
ROOT,
artifact_root=tmp_path / "workbench",
ota_root=tmp_path / "ota",
build_runner=build_runner,
)
started = service.start_task("增加座椅通风功能")
task = wait_for_terminal(service, str(started["task"]["id"]))
package_path, _ = service.resolve_artifact_download(str(task["artifact_id"]))
release_path, _ = service.resolve_release_download(str(task["release_id"]))
result = service.delete_artifact(str(task["artifact_id"]))
assert result["deleted"] is True
assert not package_path.exists()
assert not release_path.exists()
snapshot = service.snapshot()
assert [artifact["id"] for artifact in snapshot["artifacts"]] == [
"fw-art-time-only-baseline"
]
assert snapshot["releases"] == []
try:
service.delete_artifact("fw-art-time-only-baseline")
except Exception as exc: # noqa: BLE001 - public error type is asserted below
assert getattr(exc, "code", None) == "baseline_protected"
else:
raise AssertionError("time-only baseline must not be deletable")
service.close()
def test_position_instruction_merges_with_time_only_baseline(tmp_path: Path) -> None:
image = make_image(tmp_path)
def build_runner(
_task_id: str, log: object, _cancel: threading.Event
) -> Path:
log("info", "IAR build: profile position=1 ventilation=0 heating=0")
return image
service = FirmwareWorkbenchService(
ROOT,
artifact_root=tmp_path / "workbench",
ota_root=tmp_path / "ota",
build_runner=build_runner,
)
started = service.start_task("增加支持座椅位置参数控制的能力。", "time_only")
task = wait_for_terminal(service, str(started["task"]["id"]))
assert task["status"] == "success"
assert task["baseline_capabilities"] == ["时间控制"]
assert task["requested_capabilities"] == ["位置控制"]
assert task["target_capabilities"] == ["时间控制", "位置控制"]
artifact = service.snapshot()["artifacts"][0]
assert artifact["capabilities"] == ["时间控制", "位置控制"]
assert artifact["capability_diff"]["added"] == ["位置控制"]
assert "vent" not in artifact["firmware_filename"]
assert "heat" not in artifact["firmware_filename"]
service.close()
def test_running_pipeline_can_be_canceled_and_retried(tmp_path: Path) -> None:
image = make_image(tmp_path)
allow_build = threading.Event()
def build_runner(
_task_id: str, log: object, cancel: threading.Event
) -> Path:
log("info", "IAR build started")
while not allow_build.is_set():
if cancel.wait(0.01):
raise InterruptedError
return image
service = FirmwareWorkbenchService(
ROOT,
artifact_root=tmp_path / "workbench",
ota_root=tmp_path / "ota",
build_runner=build_runner,
)
started = service.start_task("增加座椅通风功能")
task_id = str(started["task"]["id"])
deadline = time.monotonic() + 3
while service.task(task_id)["current_stage"] != "build" and time.monotonic() < deadline:
time.sleep(0.01)
service.cancel_task(task_id)
canceled = wait_for_terminal(service, task_id)
assert canceled["status"] == "canceled"
assert any(stage["status"] == "canceled" for stage in canceled["stages"])
allow_build.set()
retried = service.retry_task(task_id)
assert retried["id"] == task_id
completed = wait_for_terminal(service, task_id)
assert completed["status"] == "success"
service.close()