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()