| | |
| | | from __future__ import annotations |
| | | |
| | | import importlib.util |
| | | import io |
| | | import subprocess |
| | | import sys |
| | | import tempfile |
| | | import unittest |
| | | import json |
| | | import struct |
| | | import zipfile |
| | | from unittest import mock |
| | | from http import HTTPStatus |
| | | from urllib.parse import quote |
| | | from pathlib import Path |
| | | |
| | | |
| | |
| | | SPEC.loader.exec_module(MODULE) |
| | | |
| | | |
| | | def write_test_npz(path: Path, point_count: int) -> None: |
| | | """Create just enough valid NPY headers for the console's dependency-free validator.""" |
| | | def npy_header(shape: tuple[int, int]) -> bytes: |
| | | text = repr({"descr": "<f8", "fortran_order": False, "shape": shape}).encode("ascii") |
| | | padding = (-((10 + len(text) + 1) % 16)) % 16 |
| | | header = text + (b" " * padding) + b"\n" |
| | | return b"\x93NUMPY\x01\x00" + struct.pack("<H", len(header)) + header |
| | | |
| | | with zipfile.ZipFile(path, "w", compression=zipfile.ZIP_DEFLATED) as archive: |
| | | archive.writestr("xyz.npy", npy_header((point_count, 3))) |
| | | archive.writestr("las_rgb.npy", npy_header((point_count, 3))) |
| | | |
| | | |
| | | class WorkbenchConsoleHandlerTests(unittest.TestCase): |
| | | def test_run_deletion_plan_rejects_unknown_capability_and_path_traversal(self) -> None: |
| | | with tempfile.TemporaryDirectory() as temp_dir: |
| | | root = Path(temp_dir) |
| | | with self.assertRaises(MODULE.ApiError): |
| | | MODULE.run_deletion_plan(root, "not-a-capability", "run-1") |
| | | with self.assertRaises(MODULE.ApiError): |
| | | MODULE.run_deletion_plan(root, "02-semantic-mapping", "../run-1") |
| | | |
| | | def test_run_deletion_plan_protects_non_console_baseline(self) -> None: |
| | | with tempfile.TemporaryDirectory() as temp_dir: |
| | | root = Path(temp_dir) |
| | | artifact = root / "shared" / "outputs" / "02-semantic-mapping" / "validation-baseline" |
| | | artifact.mkdir(parents=True) |
| | | (artifact / "run_metadata.json").write_text(json.dumps({"capability": "02-semantic-mapping", "images": []}), encoding="utf-8") |
| | | plan = MODULE.run_deletion_plan(root, "02-semantic-mapping", "validation-baseline") |
| | | self.assertFalse(plan["removable"]) |
| | | self.assertEqual(plan["outputDirectories"], []) |
| | | |
| | | def test_console_run_deletion_removes_only_discovered_owned_directories(self) -> None: |
| | | with tempfile.TemporaryDirectory() as temp_dir: |
| | | root = Path(temp_dir) |
| | | run_id = "semantic-test-run" |
| | | output = root / "shared" / "outputs" / "02-semantic-mapping" / "runs" / run_id |
| | | raw = root / "shared" / "data" / "raw" / "02-semantic-mapping" / "runs" / run_id |
| | | processed = root / "shared" / "data" / "processed" / "02-semantic-mapping" / run_id |
| | | sibling = root / "shared" / "outputs" / "02-semantic-mapping" / "runs" / "keep-me" |
| | | external = root / "baseData" / "keep.jpg" |
| | | for directory in (output, raw, processed, sibling, external.parent): |
| | | directory.mkdir(parents=True, exist_ok=True) |
| | | (output / "run_metadata.json").write_text(json.dumps({"capability": "02-semantic-mapping", "images": [], "created_at": "2026-08-28T00:00:00Z"}), encoding="utf-8") |
| | | external.write_bytes(b"external") |
| | | plan = MODULE.run_deletion_plan(root, "02-semantic-mapping", run_id) |
| | | self.assertTrue(plan["removable"]) |
| | | self.assertEqual(plan["outputDirectories"], [f"shared/outputs/02-semantic-mapping/runs/{run_id}"]) |
| | | self.assertEqual(plan["rawDirectories"], [f"shared/data/raw/02-semantic-mapping/runs/{run_id}"]) |
| | | self.assertEqual(plan["processedDirectories"], [f"shared/data/processed/02-semantic-mapping/{run_id}"]) |
| | | removed = MODULE.delete_console_run(root, "02-semantic-mapping", run_id) |
| | | self.assertEqual(removed["runId"], run_id) |
| | | self.assertFalse(output.exists()) |
| | | self.assertFalse(raw.exists()) |
| | | self.assertFalse(processed.exists()) |
| | | self.assertTrue(sibling.is_dir()) |
| | | self.assertEqual(external.read_bytes(), b"external") |
| | | |
| | | def make_handler(self) -> MODULE.WorkbenchConsoleHandler: |
| | | handler = object.__new__(MODULE.WorkbenchConsoleHandler) |
| | | handler.directory = str(ROOT) |
| | |
| | | Path(handler.translate_path("/shared/data/raw/01-object-detection/sample.jpeg")), |
| | | ROOT / "shared" / "data" / "raw" / "01-object-detection" / "sample.jpeg", |
| | | ) |
| | | self.assertEqual( |
| | | Path(handler.translate_path("/shared/data/raw/02-semantic-mapping/sample.tif")), |
| | | ROOT / "shared" / "data" / "raw" / "02-semantic-mapping" / "sample.tif", |
| | | ) |
| | | self.assertEqual( |
| | | Path(handler.translate_path("/shared/data/raw/00-change-detection/sample.jpg")), |
| | | ROOT / "shared" / "data" / "raw" / "00-change-detection" / "sample.jpg", |
| | | ) |
| | | self.assertEqual( |
| | | Path(handler.translate_path("/shared/data/raw/09-anomaly-detection/sample.tif")), |
| | | ROOT / "shared" / "data" / "raw" / "09-anomaly-detection" / "sample.tif", |
| | | ) |
| | | self.assertEqual( |
| | | Path(handler.translate_path("/shared/data/processed/09-anomaly-detection/sample.tif")), |
| | | ROOT / ".console-forbidden", |
| | | ) |
| | | |
| | | def test_upload_name_is_sanitized_and_extension_is_allowlisted(self) -> None: |
| | | self.assertEqual(MODULE.safe_file_name("../../unsafe name.JPG", {".jpg"}), "unsafe_name.jpg") |
| | | self.assertEqual(MODULE.safe_file_name("../正常参考图.JPG", {".jpg"}), "正常参考图.jpg") |
| | | with self.assertRaises(MODULE.ApiError): |
| | | MODULE.safe_file_name("image.exe", {".jpg", ".png"}) |
| | | |
| | | def test_oversized_request_is_rejected_before_reading_body(self) -> None: |
| | | handler = self.make_handler() |
| | | handler.headers = {"Content-Length": str(MODULE.MAX_REQUEST_BYTES + 1), "Content-Type": "application/json"} |
| | | handler.rfile = io.BytesIO(b"") |
| | | with self.assertRaises(MODULE.ApiError): |
| | | handler.read_json_body() |
| | | |
| | | def test_large_tiff_upload_limits_allow_one_gibibyte_files(self) -> None: |
| | | self.assertEqual(MODULE.MAX_FILE_BYTES, 1024 * 1024 * 1024) |
| | | self.assertEqual(MODULE.MAX_REQUEST_BYTES, 3072 * 1024 * 1024) |
| | | |
| | | def test_photo_reconstruction_timeout_supports_long_cpu_runs(self) -> None: |
| | | self.assertEqual(MODULE.PHOTO_RECONSTRUCTION_TIMEOUT, 86_400) |
| | | |
| | | def test_binary_change_upload_preserves_original_bytes(self) -> None: |
| | | with tempfile.TemporaryDirectory() as temp_dir: |
| | | handler = object.__new__(MODULE.WorkbenchConsoleHandler) |
| | | handler.directory = temp_dir |
| | | handler.path = "/api/change-detection/uploads/0123456789abcdef0123456789abcdef?role=before" |
| | | handler.headers = {"Content-Length": "13", "X-Upload-Name": "../1.tif"} |
| | | handler.rfile = io.BytesIO(b"raw-tif-bytes") |
| | | result = handler.receive_change_upload("/api/change-detection/uploads/0123456789abcdef0123456789abcdef") |
| | | staged = Path(temp_dir) / "shared" / "data" / "raw" / "00-change-detection" / "uploads" / result["uploadId"] / "before.tif" |
| | | self.assertEqual(staged.read_bytes(), b"raw-tif-bytes") |
| | | self.assertEqual(result["name"], "1.tif") |
| | | |
| | | def test_binary_anomaly_upload_preserves_bytes_and_checksum(self) -> None: |
| | | with tempfile.TemporaryDirectory() as temp_dir: |
| | | handler = object.__new__(MODULE.WorkbenchConsoleHandler) |
| | | handler.directory = temp_dir |
| | | handler.path = "/api/anomaly-detection/uploads/1123456789abcdef0123456789abcdef?role=reference" |
| | | handler.headers = {"Content-Length": "15", "X-Upload-Name": "../normal.JPG"} |
| | | handler.rfile = io.BytesIO(b"reference-bytes") |
| | | result = handler.receive_anomaly_upload("/api/anomaly-detection/uploads/1123456789abcdef0123456789abcdef") |
| | | staged = Path(temp_dir) / "shared" / "data" / "raw" / "09-anomaly-detection" / "uploads" / result["uploadId"] / "reference.jpg" |
| | | self.assertEqual(staged.read_bytes(), b"reference-bytes") |
| | | self.assertEqual(result["sha256"], MODULE.file_sha256(staged)) |
| | | |
| | | def test_binary_photo_reconstruction_upload_preserves_jpeg_bytes(self) -> None: |
| | | with tempfile.TemporaryDirectory() as temp_dir: |
| | | handler = object.__new__(MODULE.WorkbenchConsoleHandler) |
| | | handler.directory = temp_dir |
| | | handler.path = "/api/3d-pointcloud/photo-uploads/4123456789abcdef0123456789abcdef?role=photo" |
| | | handler.headers = {"Content-Length": "10", "X-Upload-Name": "../flight.JPG"} |
| | | handler.rfile = io.BytesIO(b"jpeg-bytes") |
| | | result = handler.receive_photo_reconstruction_upload("/api/3d-pointcloud/photo-uploads/4123456789abcdef0123456789abcdef") |
| | | staged = Path(temp_dir) / "shared" / "data" / "raw" / "05-3d-pointcloud" / "uploads" / result["uploadId"] / "photo.jpg" |
| | | self.assertEqual(result["role"], "photo") |
| | | self.assertEqual(result["name"], "flight.jpg") |
| | | self.assertEqual(staged.read_bytes(), b"jpeg-bytes") |
| | | |
| | | def test_binary_pointcloud_upload_preserves_las_bytes(self) -> None: |
| | | with tempfile.TemporaryDirectory() as temp_dir: |
| | | handler = object.__new__(MODULE.WorkbenchConsoleHandler) |
| | | handler.directory = temp_dir |
| | | handler.path = "/api/3d-pointcloud/pointcloud-uploads/5123456789abcdef0123456789abcdef?role=pointcloud" |
| | | handler.headers = {"Content-Length": "9", "X-Upload-Name": "../corridor.LAS"} |
| | | handler.rfile = io.BytesIO(b"las-bytes") |
| | | result = handler.receive_pointcloud_upload("/api/3d-pointcloud/pointcloud-uploads/5123456789abcdef0123456789abcdef") |
| | | staged = Path(temp_dir) / "shared" / "data" / "raw" / "05-3d-pointcloud" / "uploads" / result["uploadId"] / "pointcloud.las" |
| | | self.assertEqual(result["role"], "pointcloud") |
| | | self.assertEqual(result["name"], "corridor.las") |
| | | self.assertEqual(staged.read_bytes(), b"las-bytes") |
| | | |
| | | def test_model_inference_rejects_empty_and_incomplete_las_uploads(self) -> None: |
| | | with tempfile.TemporaryDirectory() as temp_dir: |
| | | root = Path(temp_dir) |
| | | empty = root / "empty.ply" |
| | | empty.write_bytes(b"") |
| | | with self.assertRaisesRegex(MODULE.ApiError, "文件为空"): |
| | | MODULE.validate_pointcloud_model_input(empty) |
| | | |
| | | incomplete = root / "tile_000_000.las" |
| | | incomplete.write_bytes(b"las-bytes") |
| | | with self.assertRaisesRegex(MODULE.ApiError, "文件不完整"): |
| | | MODULE.validate_pointcloud_model_input(incomplete) |
| | | |
| | | empty_las = root / "empty.las" |
| | | header = bytearray(227) |
| | | header[:4] = b"LASF" |
| | | header[24:26] = bytes((1, 2)) |
| | | header[94:96] = (227).to_bytes(2, "little") |
| | | empty_las.write_bytes(header) |
| | | with self.assertRaisesRegex(MODULE.ApiError, "没有点记录"): |
| | | MODULE.validate_pointcloud_model_input(empty_las) |
| | | |
| | | def test_photo_reconstruction_request_requires_three_to_one_thousand_photos(self) -> None: |
| | | handler = self.make_handler() |
| | | with self.assertRaisesRegex(MODULE.ApiError, "at least three"): |
| | | handler.create_photo_reconstruction_run({"photos": []}) |
| | | with self.assertRaisesRegex(MODULE.ApiError, "at most"): |
| | | handler.create_photo_reconstruction_run({"photos": [{}] * (MODULE.MAX_PHOTO_RECONSTRUCTION_IMAGES_PER_RUN + 1)}) |
| | | with self.assertRaisesRegex(MODULE.ApiError, "true or false"): |
| | | handler.create_photo_reconstruction_run({"photos": [{}, {}, {}], "usePositionPriors": "yes"}) |
| | | |
| | | def test_photo_reconstruction_job_reads_progress_without_exposing_path(self) -> None: |
| | | with tempfile.TemporaryDirectory() as temp_dir: |
| | | progress_path = Path(temp_dir) / "progress.json" |
| | | progress_path.write_text(json.dumps({"percent": 68, "stage": "dense_fusion", "message": "running", "inputImages": 18, "estimate": True}), encoding="utf-8") |
| | | with MODULE.PHOTO_RECONSTRUCTION_JOBS_LOCK: |
| | | MODULE.PHOTO_RECONSTRUCTION_JOBS["progress-test"] = {"id": "progress-test", "runId": "run", "status": "running", "stage": "dense_mvs", "inputImages": 18, "progressPath": str(progress_path)} |
| | | try: |
| | | job = MODULE.photo_reconstruction_job("progress-test") |
| | | finally: |
| | | with MODULE.PHOTO_RECONSTRUCTION_JOBS_LOCK: |
| | | MODULE.PHOTO_RECONSTRUCTION_JOBS.pop("progress-test", None) |
| | | self.assertEqual(job["progress"]["percent"], 68) |
| | | self.assertNotIn("progressPath", job) |
| | | |
| | | def test_binary_anomaly_upload_decodes_chinese_file_name(self) -> None: |
| | | with tempfile.TemporaryDirectory() as temp_dir: |
| | | handler = object.__new__(MODULE.WorkbenchConsoleHandler) |
| | | handler.directory = temp_dir |
| | | handler.path = "/api/anomaly-detection/uploads/3123456789abcdef0123456789abcdef?role=input" |
| | | handler.headers = {"Content-Length": "11", "X-Upload-Name": quote("异常测试图.JPG", safe="")} |
| | | handler.rfile = io.BytesIO(b"image-bytes") |
| | | result = handler.receive_anomaly_upload("/api/anomaly-detection/uploads/3123456789abcdef0123456789abcdef") |
| | | staged = Path(temp_dir) / "shared" / "data" / "raw" / "09-anomaly-detection" / "uploads" / result["uploadId"] / "input.jpg" |
| | | self.assertEqual(result["name"], "异常测试图.jpg") |
| | | self.assertEqual(staged.read_bytes(), b"image-bytes") |
| | | |
| | | def test_interrupted_anomaly_upload_removes_partial_file(self) -> None: |
| | | with tempfile.TemporaryDirectory() as temp_dir: |
| | | handler = object.__new__(MODULE.WorkbenchConsoleHandler) |
| | | handler.directory = temp_dir |
| | | handler.path = "/api/anomaly-detection/uploads/2123456789abcdef0123456789abcdef?role=input" |
| | | handler.headers = {"Content-Length": "20", "X-Upload-Name": "target.tif"} |
| | | handler.rfile = io.BytesIO(b"short") |
| | | with self.assertRaisesRegex(MODULE.ApiError, "ended before"): |
| | | handler.receive_anomaly_upload("/api/anomaly-detection/uploads/2123456789abcdef0123456789abcdef") |
| | | staging = Path(temp_dir) / "shared" / "data" / "raw" / "09-anomaly-detection" / "uploads" / "2123456789abcdef0123456789abcdef" |
| | | self.assertFalse(any(staging.glob("*.part"))) |
| | | |
| | | def test_anomaly_parameters_are_bounded(self) -> None: |
| | | self.assertEqual(MODULE.validate_anomaly_parameters({}), (256, 128, 0.995, 42)) |
| | | with self.assertRaisesRegex(MODULE.ApiError, "Stride"): |
| | | MODULE.validate_anomaly_parameters({"tileSize": 128, "stride": 256}) |
| | | with self.assertRaisesRegex(MODULE.ApiError, "quantile"): |
| | | MODULE.validate_anomaly_parameters({"thresholdQuantile": 1.0}) |
| | | |
| | | def test_anomaly_run_discovery_requires_exposed_raw_sources(self) -> None: |
| | | with tempfile.TemporaryDirectory() as temp_dir: |
| | | root = Path(temp_dir) |
| | | artifact = root / "shared" / "outputs" / "09-anomaly-detection" / "runs" / "anomaly-test" |
| | | raw_input = root / "shared" / "data" / "raw" / "09-anomaly-detection" / "runs" / "anomaly-test" / "input" |
| | | raw_reference = raw_input.parent / "reference" |
| | | artifact.mkdir(parents=True) |
| | | raw_input.mkdir(parents=True) |
| | | raw_reference.mkdir(parents=True) |
| | | (artifact / "target.comparison.overlay.png").write_bytes(b"png") |
| | | metadata = {"capability": "09-anomaly-detection", "created_at": "2026-08-18", "display_name": "五参考图边界案例", "case_note": "保留真实漏检结果。", "raw_input_dir": raw_input.relative_to(root).as_posix(), "raw_reference_dir": raw_reference.relative_to(root).as_posix(), "images": [{"overlay_file": "target.comparison.overlay.png"}]} |
| | | (artifact / "run_metadata.json").write_text(json.dumps(metadata), encoding="utf-8") |
| | | runs = MODULE.anomaly_runs(root) |
| | | self.assertEqual(runs[0]["id"], "anomaly-test") |
| | | self.assertEqual(runs[0]["label"], "五参考图边界案例") |
| | | self.assertEqual(runs[0]["note"], "保留真实漏检结果。") |
| | | |
| | | def test_script_failure_becomes_a_useful_api_error(self) -> None: |
| | | handler = self.make_handler() |
| | | with self.assertRaisesRegex(MODULE.ApiError, "Processing failed"): |
| | | handler.run_command([sys.executable, "-c", "raise SystemExit(2)"], timeout=10) |
| | | |
| | | def test_pointcloud_execution_uses_fixed_gpu_environment_when_cuda_probe_succeeds(self) -> None: |
| | | with tempfile.TemporaryDirectory() as temp_dir: |
| | | root = Path(temp_dir) |
| | | gpu_python = root / ".venvs" / MODULE.POINTCLOUD_GPU_ENVIRONMENT / "Scripts" / "python.exe" |
| | | gpu_python.parent.mkdir(parents=True) |
| | | gpu_python.write_bytes(b"fixed-interpreter") |
| | | with mock.patch.object(MODULE.subprocess, "run", return_value=subprocess.CompletedProcess([], 0, '{"cuda": true, "torch": "2.11.0+cu128"}\n', "")): |
| | | execution = MODULE.pointcloud_execution_environment(root) |
| | | self.assertEqual(execution["device"], "cuda") |
| | | self.assertEqual(execution["environment"], MODULE.POINTCLOUD_GPU_ENVIRONMENT) |
| | | self.assertEqual(execution["torchVersion"], "2.11.0+cu128") |
| | | self.assertEqual(Path(execution["python"]), gpu_python) |
| | | |
| | | def test_pointcloud_execution_falls_back_to_fixed_cpu_environment(self) -> None: |
| | | with tempfile.TemporaryDirectory() as temp_dir: |
| | | root = Path(temp_dir) |
| | | cpu_python = root / ".venvs" / MODULE.POINTCLOUD_CPU_ENVIRONMENT / "Scripts" / "python.exe" |
| | | gpu_python = root / ".venvs" / MODULE.POINTCLOUD_GPU_ENVIRONMENT / "Scripts" / "python.exe" |
| | | cpu_python.parent.mkdir(parents=True) |
| | | gpu_python.parent.mkdir(parents=True) |
| | | cpu_python.write_bytes(b"fixed-cpu-interpreter") |
| | | gpu_python.write_bytes(b"fixed-gpu-interpreter") |
| | | with mock.patch.object(MODULE.subprocess, "run", return_value=subprocess.CompletedProcess([], 0, '{"cuda": false, "torch": "2.11.0+cu128"}\n', "")): |
| | | execution = MODULE.pointcloud_execution_environment(root) |
| | | self.assertEqual(execution["device"], "cpu") |
| | | self.assertEqual(execution["environment"], MODULE.POINTCLOUD_CPU_ENVIRONMENT) |
| | | with mock.patch.object(MODULE.subprocess, "run", return_value=subprocess.CompletedProcess([], 0, '{"cuda": false, "torch": "2.11.0+cu128"}\n', "")): |
| | | with self.assertRaisesRegex(MODULE.ApiError, "CUDA was requested"): |
| | | MODULE.pointcloud_execution_environment(root, "cuda") |
| | | |
| | | def test_explicit_cpu_execution_never_probes_cuda(self) -> None: |
| | | with tempfile.TemporaryDirectory() as temp_dir: |
| | | root = Path(temp_dir) |
| | | cpu_python = root / ".venvs" / MODULE.OBJECT_DETECTION_CPU_ENVIRONMENT / "Scripts" / "python.exe" |
| | | cpu_python.parent.mkdir(parents=True) |
| | | cpu_python.write_bytes(b"fixed-cpu-interpreter") |
| | | with mock.patch.object(MODULE.subprocess, "run") as probe: |
| | | execution = MODULE.object_detection_execution_environment(root, "cpu") |
| | | probe.assert_not_called() |
| | | self.assertEqual(execution["requestedDevice"], "cpu") |
| | | self.assertEqual(execution["device"], "cpu") |
| | | self.assertFalse(execution["fallbackUsed"]) |
| | | |
| | | def test_auto_cpu_fallback_records_the_probe_reason(self) -> None: |
| | | with tempfile.TemporaryDirectory() as temp_dir: |
| | | root = Path(temp_dir) |
| | | cpu_python = root / ".venvs" / MODULE.CHANGE_DETECTION_CPU_ENVIRONMENT / "Scripts" / "python.exe" |
| | | gpu_python = root / ".venvs" / MODULE.CHANGE_DETECTION_GPU_ENVIRONMENT / "Scripts" / "python.exe" |
| | | cpu_python.parent.mkdir(parents=True) |
| | | gpu_python.parent.mkdir(parents=True) |
| | | cpu_python.write_bytes(b"fixed-cpu-interpreter") |
| | | gpu_python.write_bytes(b"fixed-gpu-interpreter") |
| | | with mock.patch.object(MODULE.subprocess, "run", return_value=subprocess.CompletedProcess([], 0, '{"cuda": false, "torch": "2.11.0+cu128"}\n', "")): |
| | | execution = MODULE.change_detection_execution_environment(root, "auto") |
| | | self.assertTrue(execution["fallbackUsed"]) |
| | | self.assertIn("cannot use CUDA", execution["fallbackReason"]) |
| | | |
| | | def test_object_detection_execution_uses_fixed_gpu_environment_when_probe_succeeds(self) -> None: |
| | | with tempfile.TemporaryDirectory() as temp_dir: |
| | | root = Path(temp_dir) |
| | | gpu_python = root / ".venvs" / MODULE.OBJECT_DETECTION_GPU_ENVIRONMENT / "Scripts" / "python.exe" |
| | | gpu_python.parent.mkdir(parents=True) |
| | | gpu_python.write_bytes(b"fixed-interpreter") |
| | | with mock.patch.object(MODULE.subprocess, "run", return_value=subprocess.CompletedProcess([], 0, '{"cuda": true, "torch": "2.11.0+cu128"}\n', "")): |
| | | execution = MODULE.object_detection_execution_environment(root) |
| | | self.assertEqual(execution["device"], "cuda") |
| | | self.assertEqual(execution["environment"], MODULE.OBJECT_DETECTION_GPU_ENVIRONMENT) |
| | | |
| | | def test_run_discovery_uses_actual_device_in_case_note(self) -> None: |
| | | with tempfile.TemporaryDirectory() as temp_dir: |
| | | root = Path(temp_dir) |
| | | detection = root / "shared" / "outputs" / "01-object-detection" / "gpu-case" |
| | | raw = root / "shared" / "data" / "raw" / "01-object-detection" / "gpu-case" |
| | | detection.mkdir(parents=True) |
| | | raw.mkdir(parents=True) |
| | | (detection / "detections.json").write_text("{}", encoding="utf-8") |
| | | (detection / "run_metadata.json").write_text(json.dumps({"input_dir": str(raw), "created_at": "2026-08-25", "device": "cuda:0"}), encoding="utf-8") |
| | | self.assertIn("GPU", MODULE.detection_runs(root)[0]["note"]) |
| | | |
| | | change = root / "shared" / "outputs" / "00-change-detection" / "gpu-case" |
| | | change_raw = root / "shared" / "data" / "raw" / "00-change-detection" / "gpu-case" |
| | | before = change_raw / "before.jpg" |
| | | after = change_raw / "after.jpg" |
| | | change.mkdir(parents=True) |
| | | change_raw.mkdir(parents=True) |
| | | before.write_bytes(b"before") |
| | | after.write_bytes(b"after") |
| | | (change / "overlay.jpg").write_bytes(b"overlay") |
| | | (change / "changes.geojson").write_text("{}", encoding="utf-8") |
| | | (change / "run_metadata.json").write_text(json.dumps({"capability": "00-change-detection", "schema_version": 1, "created_at": "2026-08-25", "device": "cuda", "input_files": ["before.jpg", "after.jpg"], "raw_input_dir": change_raw.relative_to(root).as_posix(), "raw_before": before.relative_to(root).as_posix(), "raw_after": after.relative_to(root).as_posix(), "artifacts": {"overlay": "overlay.jpg", "vector": "changes.geojson"}}), encoding="utf-8") |
| | | self.assertIn("GPU", MODULE.change_runs(root)[0]["note"]) |
| | | |
| | | def test_object_detection_execution_falls_back_to_fixed_cpu_environment(self) -> None: |
| | | with tempfile.TemporaryDirectory() as temp_dir: |
| | | root = Path(temp_dir) |
| | | cpu_python = root / ".venvs" / MODULE.OBJECT_DETECTION_CPU_ENVIRONMENT / "Scripts" / "python.exe" |
| | | gpu_python = root / ".venvs" / MODULE.OBJECT_DETECTION_GPU_ENVIRONMENT / "Scripts" / "python.exe" |
| | | cpu_python.parent.mkdir(parents=True) |
| | | gpu_python.parent.mkdir(parents=True) |
| | | cpu_python.write_bytes(b"fixed-cpu-interpreter") |
| | | gpu_python.write_bytes(b"fixed-gpu-interpreter") |
| | | with mock.patch.object(MODULE.subprocess, "run", return_value=subprocess.CompletedProcess([], 0, '{"cuda": false, "torch": "2.11.0+cu128"}\n', "")): |
| | | execution = MODULE.object_detection_execution_environment(root) |
| | | self.assertEqual(execution["device"], "cpu") |
| | | self.assertEqual(execution["environment"], MODULE.OBJECT_DETECTION_CPU_ENVIRONMENT) |
| | | |
| | | def test_change_detection_execution_uses_fixed_gpu_environment_when_probe_succeeds(self) -> None: |
| | | with tempfile.TemporaryDirectory() as temp_dir: |
| | | root = Path(temp_dir) |
| | | gpu_python = root / ".venvs" / MODULE.CHANGE_DETECTION_GPU_ENVIRONMENT / "Scripts" / "python.exe" |
| | | gpu_python.parent.mkdir(parents=True) |
| | | gpu_python.write_bytes(b"fixed-interpreter") |
| | | with mock.patch.object(MODULE.subprocess, "run", return_value=subprocess.CompletedProcess([], 0, '{"cuda": true, "torch": "2.11.0+cu128"}\n', "")): |
| | | execution = MODULE.change_detection_execution_environment(root) |
| | | self.assertEqual(execution["device"], "cuda") |
| | | self.assertEqual(execution["environment"], MODULE.CHANGE_DETECTION_GPU_ENVIRONMENT) |
| | | |
| | | def test_change_detection_execution_falls_back_to_fixed_cpu_environment(self) -> None: |
| | | with tempfile.TemporaryDirectory() as temp_dir: |
| | | root = Path(temp_dir) |
| | | cpu_python = root / ".venvs" / MODULE.CHANGE_DETECTION_CPU_ENVIRONMENT / "Scripts" / "python.exe" |
| | | gpu_python = root / ".venvs" / MODULE.CHANGE_DETECTION_GPU_ENVIRONMENT / "Scripts" / "python.exe" |
| | | cpu_python.parent.mkdir(parents=True) |
| | | gpu_python.parent.mkdir(parents=True) |
| | | cpu_python.write_bytes(b"fixed-cpu-interpreter") |
| | | gpu_python.write_bytes(b"fixed-gpu-interpreter") |
| | | with mock.patch.object(MODULE.subprocess, "run", return_value=subprocess.CompletedProcess([], 0, '{"cuda": false, "torch": "2.11.0+cu128"}\n', "")): |
| | | execution = MODULE.change_detection_execution_environment(root) |
| | | self.assertEqual(execution["device"], "cpu") |
| | | self.assertEqual(execution["environment"], MODULE.CHANGE_DETECTION_CPU_ENVIRONMENT) |
| | | |
| | | def test_semantic_validation_run_is_discovered(self) -> None: |
| | | runs = MODULE.semantic_runs(ROOT) |
| | | self.assertTrue(any(item["id"] == "validation-20260817" for item in runs)) |
| | | |
| | | def test_spatial_measurement_run_is_discovered(self) -> None: |
| | | runs = MODULE.measurement_runs(ROOT) |
| | | validation = next(item for item in runs if item["id"] == "validation-normal-20260817-v4") |
| | | self.assertTrue(validation["artifactRoot"].startswith("shared/outputs/04-spatial-measurement/")) |
| | | |
| | | def test_pointcloud_validation_run_is_discovered(self) -> None: |
| | | runs = MODULE.pointcloud_runs(ROOT) |
| | | validation = next(item for item in runs if item["id"] == "validation-normal-20260821-v2") |
| | | self.assertTrue(validation["artifactRoot"].startswith("shared/outputs/05-3d-pointcloud/")) |
| | | |
| | | def test_multiview_annotation_source_requires_complete_ordered_contract(self) -> None: |
| | | with tempfile.TemporaryDirectory() as temp_dir: |
| | | root = Path(temp_dir) |
| | | artifact = root / "shared" / "outputs" / "05-3d-pointcloud" / "multiview-feature-valid" |
| | | artifact.mkdir(parents=True) |
| | | cloud = artifact / "multiview-annotation-source.ply" |
| | | cloud.write_text( |
| | | "ply\nformat ascii 1.0\nelement vertex 2\nproperty float x\nproperty float y\nproperty float z\nend_header\n0 0 0\n1 1 1\n", |
| | | encoding="ascii", |
| | | ) |
| | | dataset = artifact / "multiview-point-features.npz" |
| | | write_test_npz(dataset, 2) |
| | | (artifact / "run_metadata.json").write_text(json.dumps({ |
| | | "capability": "05-3d-pointcloud", |
| | | "artifacts": {"annotation_source": cloud.name, "feature_dataset": dataset.name}, |
| | | "annotation_source": { |
| | | "schema_version": 1, |
| | | "kind": "multiview_photo_feature_fusion", |
| | | "point_cloud": cloud.name, |
| | | "feature_dataset": dataset.name, |
| | | "point_count": 2, |
| | | "point_cloud_sha256": MODULE.file_sha256(cloud), |
| | | "feature_dataset_sha256": MODULE.file_sha256(dataset), |
| | | }, |
| | | }), encoding="utf-8") |
| | | |
| | | sources = MODULE.pointcloud_annotation_sources(root) |
| | | self.assertEqual(len(sources), 1) |
| | | self.assertEqual(sources[0]["pointCount"], 2) |
| | | self.assertIn("多视角照片特征融合", sources[0]["sourceKind"]) |
| | | |
| | | handler = object.__new__(MODULE.WorkbenchConsoleHandler) |
| | | handler.directory = str(root) |
| | | annotation = handler.create_pointcloud_annotation({"sourceId": sources[0]["id"], "labels": [[1, 15]]}) |
| | | self.assertEqual(annotation["labelCount"], 1) |
| | | execution = {"python": "fixed-python", "device": "cpu", "environment": "05-3d-pointcloud", "torchVersion": "2.13.0+cpu", "requestedDevice": "cpu", "fallbackUsed": False, "fallbackReason": None} |
| | | with mock.patch.object(MODULE, "pointcloud_execution_environment", return_value=execution), mock.patch.object(MODULE.threading, "Thread") as thread: |
| | | thread.return_value.start.return_value = None |
| | | job = handler.create_pointcloud_training_run({"annotationId": annotation["id"], "device": "cpu"}) |
| | | self.assertEqual(job["trainer"], "multiview_local_attention_baseline") |
| | | self.assertTrue(job["artifactRoot"] if "artifactRoot" in job else True) |
| | | self.assertEqual(thread.call_args.kwargs["args"][-1], "multiview_local_attention_baseline") |
| | | |
| | | dataset.unlink() |
| | | self.assertEqual(MODULE.pointcloud_annotation_sources(root), []) |
| | | |
| | | def test_pointcloud_request_requires_allowlisted_files(self) -> None: |
| | | handler = self.make_handler() |
| | | with self.assertRaisesRegex(MODULE.ApiError, "PLY, PCD, XYZ, LAS, or LAZ"): |
| | | handler.create_pointcloud_run({"pointClouds": []}) |
| | | with self.assertRaisesRegex(MODULE.ApiError, "Unsupported file type"): |
| | | handler.create_pointcloud_run({"pointClouds": [{"name": "unsafe.exe", "content": "eA=="}]}) |
| | | |
| | | def test_annotation_taxonomy_adds_custom_las_class_and_preserves_used_code(self) -> None: |
| | | with tempfile.TemporaryDirectory() as temp_dir: |
| | | root = Path(temp_dir) |
| | | handler = object.__new__(MODULE.WorkbenchConsoleHandler) |
| | | handler.directory = str(root) |
| | | defaults = MODULE.annotation_classes(root) |
| | | self.assertEqual({item["code"] for item in defaults}, {1, 2, 5, 6, 15, 16}) |
| | | created = handler.create_pointcloud_annotation_class({"key": "transformer", "label": "变压器", "color": [30, 144, 255]}) |
| | | self.assertEqual(created["code"], 17) |
| | | self.assertTrue(MODULE.annotation_classes_path(root).is_file()) |
| | | revision = root / "shared" / "outputs" / "05-3d-pointcloud" / "annotations" / "annotation-used" |
| | | revision.mkdir(parents=True) |
| | | (revision / "annotation.json").write_text(json.dumps({"schema_version": 1, "labels": [[0, 17]]}), encoding="utf-8") |
| | | with self.assertRaisesRegex(MODULE.ApiError, "used by a saved annotation"): |
| | | handler.delete_pointcloud_annotation_class(17) |
| | | (revision / "annotation.json").unlink() |
| | | self.assertEqual(handler.delete_pointcloud_annotation_class(17), 17) |
| | | self.assertNotIn(17, {item["code"] for item in MODULE.annotation_classes(root)}) |
| | | |
| | | def test_annotation_taxonomy_rejects_invalid_custom_metadata(self) -> None: |
| | | with tempfile.TemporaryDirectory() as temp_dir: |
| | | handler = object.__new__(MODULE.WorkbenchConsoleHandler) |
| | | handler.directory = temp_dir |
| | | with self.assertRaisesRegex(MODULE.ApiError, "lowercase English"): |
| | | handler.create_pointcloud_annotation_class({"key": "Transformer", "label": "变压器", "color": [30, 144, 255]}) |
| | | with self.assertRaisesRegex(MODULE.ApiError, "RGB"): |
| | | handler.create_pointcloud_annotation_class({"key": "transformer", "label": "变压器", "color": [999, 1, 1]}) |
| | | |
| | | def test_preview_only_annotation_source_is_discovered_without_geometry_artifacts(self) -> None: |
| | | with tempfile.TemporaryDirectory() as temp_dir: |
| | | root = Path(temp_dir) |
| | | run_id = "annotation-source-preview-only" |
| | | artifact = root / "shared" / "outputs" / "05-3d-pointcloud" / "runs" / run_id |
| | | artifact.mkdir(parents=True) |
| | | preview = artifact / "upload.annotation-source.ply" |
| | | preview.write_text( |
| | | "ply\nformat binary_little_endian 1.0\nelement vertex 2\nproperty float x\nproperty float y\nproperty float z\nproperty uchar red\nproperty uchar green\nproperty uchar blue\nend_header\n", |
| | | encoding="ascii", |
| | | ) |
| | | (artifact / "run_metadata.json").write_text(json.dumps({ |
| | | "capability": "05-3d-pointcloud", "annotation_source_job": True, |
| | | "annotation_source": {"schema_version": 1, "file": preview.name, "point_count": 2, "sha256": MODULE.file_sha256(preview)}, |
| | | "input": {"file": "upload.ply", "has_rgb": False}, |
| | | }), encoding="utf-8") |
| | | sources = MODULE.pointcloud_annotation_sources(root) |
| | | self.assertEqual(len(sources), 1) |
| | | self.assertEqual(sources[0]["id"], f"{run_id}:{preview.name}") |
| | | self.assertFalse(sources[0]["sourceHasRgb"]) |
| | | self.assertEqual(MODULE.pointcloud_annotation_source_deletion_plan(root, sources[0]["id"])["outputDirectories"], 1) |
| | | |
| | | def test_texture_baked_annotation_source_is_discovered_at_capability_root(self) -> None: |
| | | with tempfile.TemporaryDirectory() as temp_dir: |
| | | root = Path(temp_dir) |
| | | run_id = "texture-baked-preview" |
| | | artifact = root / "shared" / "outputs" / "05-3d-pointcloud" / run_id |
| | | artifact.mkdir(parents=True) |
| | | preview = artifact / "texture-baked-annotation-source.ply" |
| | | preview.write_text( |
| | | "ply\nformat binary_little_endian 1.0\nelement vertex 1\nproperty float x\nproperty float y\nproperty float z\nend_header\n", |
| | | encoding="ascii", |
| | | ) |
| | | (artifact / "run_metadata.json").write_text(json.dumps({ |
| | | "capability": "05-3d-pointcloud", "annotation_source_job": True, |
| | | "annotation_source": {"schema_version": 1, "file": preview.name, "point_count": 1, "sha256": MODULE.file_sha256(preview)}, |
| | | "input": {"file": "Model_0.zip", "has_rgb": True}, |
| | | }), encoding="utf-8") |
| | | sources = MODULE.pointcloud_annotation_sources(root) |
| | | self.assertEqual([item["id"] for item in sources], [f"{run_id}:{preview.name}"]) |
| | | self.assertTrue(sources[0]["sourceHasRgb"]) |
| | | |
| | | def test_annotation_source_removal_deletes_only_its_complete_local_chain(self) -> None: |
| | | with tempfile.TemporaryDirectory() as temp_dir: |
| | | root = Path(temp_dir) |
| | | run_id = "annotation-source-test" |
| | | artifact = root / "shared" / "outputs" / "05-3d-pointcloud" / "runs" / run_id |
| | | artifact.mkdir(parents=True) |
| | | annotation_cloud = artifact / "block.semantic-annotation-source.ply" |
| | | annotation_cloud.write_bytes(b"annotation-preview") |
| | | (artifact / "preview.png").write_bytes(b"preview") |
| | | (artifact / "vector.geojson").write_text('{"type":"FeatureCollection","features":[]}', encoding="utf-8") |
| | | (artifact / "run_metadata.json").write_text(json.dumps({"capability": "05-3d-pointcloud", "created_at": "2026-08-28T00:00:00+00:00", "annotation_source_job": True, "point_clouds": [{"file": "block.las", "preview_file": "preview.png", "vector_file": "vector.geojson", "semantic_annotation_source_point_cloud": annotation_cloud.name, "semantic_preview_points": 1}]}), encoding="utf-8") |
| | | source_id = f"{run_id}:{annotation_cloud.name}" |
| | | revision = root / "shared" / "outputs" / "05-3d-pointcloud" / "annotations" / "annotation-test" |
| | | revision.mkdir(parents=True) |
| | | annotation_path = revision / "annotation.json" |
| | | annotation_path.write_text(json.dumps({"schema_version": 1, "id": "annotation-test", "source_id": source_id, "labels": [[0, 5]]}), encoding="utf-8") |
| | | training = root / "shared" / "outputs" / "05-3d-pointcloud" / "training-runs" / "model-test" |
| | | training.mkdir(parents=True) |
| | | model = training / "model.pt" |
| | | model.write_bytes(b"weights") |
| | | (training / "metrics.json").write_text(json.dumps({"annotation": str(annotation_path)}), encoding="utf-8") |
| | | inference = root / "shared" / "outputs" / "05-3d-pointcloud" / "model-inference-runs" / "inference-test" |
| | | inference.mkdir(parents=True) |
| | | (inference / "run_metadata.json").write_text(json.dumps({"model": {"path": str(model)}}), encoding="utf-8") |
| | | raw = root / "shared" / "data" / "raw" / "05-3d-pointcloud" / "annotation-source-runs" / run_id |
| | | processed = root / "shared" / "data" / "processed" / "05-3d-pointcloud" / "annotation-source-runs" / run_id |
| | | raw.mkdir(parents=True); processed.mkdir(parents=True) |
| | | (raw / "block.las").write_bytes(b"raw"); (processed / "block.las").write_bytes(b"processed") |
| | | external = root / "baseData" / "block.las"; external.parent.mkdir(); external.write_bytes(b"must-remain") |
| | | |
| | | handler = object.__new__(MODULE.WorkbenchConsoleHandler) |
| | | handler.directory = str(root) |
| | | plan = MODULE.pointcloud_annotation_source_deletion_plan(root, source_id) |
| | | self.assertEqual((plan["rawDirectories"], plan["processedDirectories"], plan["annotationRevisions"], plan["trainingRuns"], plan["inferenceRuns"]), (1, 1, 1, 1, 1)) |
| | | result = handler.delete_pointcloud_annotation_source(source_id) |
| | | self.assertEqual(result["removed"]["trainingRuns"], 1) |
| | | self.assertFalse(artifact.exists()); self.assertFalse(raw.exists()); self.assertFalse(processed.exists()) |
| | | self.assertFalse(revision.exists()); self.assertFalse(training.exists()); self.assertFalse(inference.exists()) |
| | | self.assertEqual(external.read_bytes(), b"must-remain") |
| | | |
| | | def test_semantic_model_discovery_requires_complete_training_artifacts(self) -> None: |
| | | with tempfile.TemporaryDirectory() as temp_dir: |
| | | root = Path(temp_dir) |
| | | complete = root / "shared" / "outputs" / "05-3d-pointcloud" / "training-runs" / "semantic-good" |
| | | incomplete = complete.parent / "semantic-incomplete" |
| | | complete.mkdir(parents=True) |
| | | incomplete.mkdir(parents=True) |
| | | (complete / "model.pt").write_bytes(b"weights") |
| | | (complete / "metrics.json").write_text(json.dumps({"capability": "05-3d-pointcloud", "classification": "B", "created_at": "2026-08-24", "classes": {"5": {"key": "vegetation", "label": "Vegetation", "color": [1, 2, 3]}, "16": {"key": "power_line", "label": "Power line", "color": [4, 5, 6]}}, "test": {"report": {"vegetation": {"f1-score": 0.9}}}}), encoding="utf-8") |
| | | (incomplete / "model.pt").write_bytes(b"weights") |
| | | models = MODULE.pointcloud_semantic_models(root) |
| | | self.assertEqual([item["id"] for item in models], ["semantic-good"]) |
| | | self.assertEqual(models[0]["testF1"], {"vegetation": 0.9}) |
| | | |
| | | def test_model_inference_rejects_unsafe_or_unknown_model_id(self) -> None: |
| | | handler = self.make_handler() |
| | | with self.assertRaisesRegex(MODULE.ApiError, "Invalid trained model id"): |
| | | handler.create_pointcloud_model_inference_run({"modelId": "../model", "pointCloud": {}}) |
| | | with self.assertRaisesRegex(MODULE.ApiError, "unavailable or incomplete"): |
| | | handler.create_pointcloud_model_inference_run({"modelId": "does-not-exist", "pointCloud": {}}) |
| | | |
| | | def test_annotation_delete_removes_only_selected_revision(self) -> None: |
| | | with tempfile.TemporaryDirectory() as temp_dir: |
| | | root = Path(temp_dir) |
| | | annotation_id = "annotation-20260822-091458-b72099" |
| | | revision = root / "shared" / "outputs" / "05-3d-pointcloud" / "annotations" / annotation_id |
| | | revision.mkdir(parents=True) |
| | | (revision / "annotation.json").write_text(json.dumps({ |
| | | "schema_version": 1, |
| | | "id": annotation_id, |
| | | "source_id": "source:preview.ply", |
| | | "created_at": "2026-08-22T09:14:58+00:00", |
| | | "labels": [[1, 15]], |
| | | }), encoding="utf-8") |
| | | source_sentinel = root / "shared" / "data" / "raw" / "05-3d-pointcloud" / "part_01.las" |
| | | source_sentinel.parent.mkdir(parents=True) |
| | | source_sentinel.write_bytes(b"source-must-remain") |
| | | |
| | | handler = object.__new__(MODULE.WorkbenchConsoleHandler) |
| | | handler.directory = str(root) |
| | | handler.path = f"/api/3d-pointcloud/annotations/{annotation_id}" |
| | | responses: list[tuple[HTTPStatus, dict[str, object]]] = [] |
| | | handler.send_json = lambda status, body: responses.append((status, body)) |
| | | |
| | | handler.do_DELETE() |
| | | |
| | | self.assertEqual(responses, [(HTTPStatus.OK, {"deletedId": annotation_id})]) |
| | | self.assertFalse(revision.exists()) |
| | | self.assertEqual(source_sentinel.read_bytes(), b"source-must-remain") |
| | | |
| | | def test_risk_rule_validation_run_is_discovered(self) -> None: |
| | | runs = MODULE.risk_rule_runs(ROOT) |
| | | validation = next(item for item in runs if item["id"] == "validation-normal-20260820") |
| | | self.assertTrue(validation["artifactRoot"].startswith("shared/outputs/07-risk-rule-engine/")) |
| | | |
| | | def test_risk_rule_request_requires_fixed_three_file_contract(self) -> None: |
| | | handler = self.make_handler() |
| | | with self.assertRaisesRegex(MODULE.ApiError, "observations, zones and rules"): |
| | | handler.create_risk_rule_run({"files": {"observations": {}}}) |
| | | |
| | | def test_change_run_requires_both_allowlisted_images(self) -> None: |
| | | handler = self.make_handler() |
| | | with self.assertRaisesRegex(MODULE.ApiError, "name and Base64"): |
| | | handler.create_change_run({"files": {"before": {"name": "before.jpg", "content": "eA=="}}}) |
| | | |
| | | def test_change_threshold_is_validated(self) -> None: |
| | | handler = self.make_handler() |
| | | payload = {"files": {"before": {"name": "before.jpg", "content": "eA=="}, "after": {"name": "after.jpg", "content": "eA=="}}} |
| | | with self.assertRaisesRegex(MODULE.ApiError, "between 0.01 and 0.99"): |
| | | handler.create_change_run({**payload, "threshold": 1.0}) |
| | | with self.assertRaisesRegex(MODULE.ApiError, "must be a number"): |
| | | handler.create_change_run({**payload, "threshold": "0.5"}) |
| | | |
| | | def test_change_resolution_is_validated(self) -> None: |
| | | handler = self.make_handler() |
| | | payload = {"files": {"before": {"name": "before.jpg", "content": "eA=="}, "after": {"name": "after.jpg", "content": "eA=="}}} |
| | | with self.assertRaisesRegex(MODULE.ApiError, "between 512 and 4096"): |
| | | handler.create_change_run({**payload, "maxDimension": 256}) |
| | | with self.assertRaisesRegex(MODULE.ApiError, "must be an integer"): |
| | | handler.create_change_run({**payload, "maxDimension": "2048"}) |
| | | |
| | | def test_change_processing_mode_is_validated(self) -> None: |
| | | handler = self.make_handler() |
| | | payload = {"files": {"before": {"name": "before.jpg", "content": "eA=="}, "after": {"name": "after.jpg", "content": "eA=="}}} |
| | | with self.assertRaisesRegex(MODULE.ApiError, "auto, image, or geotiff"): |
| | | handler.create_change_run({**payload, "processingMode": "wrong"}) |
| | | |
| | | def test_change_validation_run_is_discovered_without_fake_crs(self) -> None: |
| | | runs = MODULE.change_runs(ROOT) |
| | | self.assertTrue(any(item["id"].startswith("validation-real-") for item in runs)) |
| | | |
| | | def test_semantic_task_catalog_only_enables_verified_baseline(self) -> None: |
| | | tasks = MODULE.semantic_tasks(ROOT) |
| | | self.assertEqual(len(tasks), 5) |
| | | selectable = [item["id"] for item in tasks if item.get("selectable") is True] |
| | | self.assertEqual(selectable, ["color_baseline"]) |
| | | handler = self.make_handler() |
| | | with self.assertRaisesRegex(MODULE.ApiError, "not runnable yet"): |
| | | handler.create_semantic_run({"taskId": "drainage_blockage", "images": []}) |
| | | |
| | | def test_blocks_repository_files_and_encoded_traversal(self) -> None: |
| | | handler = self.make_handler() |