将 Demo 录制收敛为三条独立链路,并重做联调台 UI。
移除 demo_orch 统一编排,改为 recording_demo 与 live/simulated 服务;客户端拆分为静态资源,以模式卡片与 chip 耗材覆盖三链路联调,并同步测试与文档。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -162,6 +162,7 @@ def integration_client(
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(main_module, "check_database", _noop)
|
||||
monkeypatch.setattr("app.api.check_database", _noop)
|
||||
|
||||
class _FakeEngine:
|
||||
async def dispose(self) -> None:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""FastAPI → 算法子进程调用链单元测试。
|
||||
|
||||
覆盖两条生产路径:
|
||||
1. ``POST /internal/demo/video-batch-surgery`` → ``VideoBatchRunner`` → ``subprocess.run``(reference bundle ``main.py``)
|
||||
1. ``POST /internal/demo/offline-batch`` → ``VideoBatchRunner`` → ``subprocess.run``(reference bundle ``main.py``)
|
||||
2. ``POST /client/surgeries/start`` → ``CameraSessionManager`` → ``asyncio.create_subprocess_exec``(``python -m app.algorithm_runner``)
|
||||
"""
|
||||
|
||||
@@ -22,7 +22,7 @@ from httpx import ASGITransport, AsyncClient
|
||||
from app.api import router as api_router
|
||||
from app.config import Settings
|
||||
from app.dependencies import build_container, get_surgery_pipeline, get_voice_terminal_hub
|
||||
from app.routers import demo_orch
|
||||
from app.routers import recording_demo
|
||||
from app.services.video.session_manager import CameraSessionManager
|
||||
from app.services.video_batch_runner import VideoBatchRunner, build_reference_command
|
||||
from tests.test_video_batch_runner import _complete_result_tsv_body, _write_minimal_reference_bundle
|
||||
@@ -65,7 +65,7 @@ def test_video_batch_endpoint_invokes_reference_bundle_subprocess(
|
||||
reference_bundle: Path,
|
||||
sqlite_session_factory,
|
||||
) -> None:
|
||||
monkeypatch.setattr(demo_orch.settings, "demo_orchestrator_enabled", True)
|
||||
monkeypatch.setattr(recording_demo.settings, "demo_orchestrator_enabled", True)
|
||||
monkeypatch.setattr(
|
||||
"app.services.video_batch_runner.resolve_reference_bundle_dir",
|
||||
lambda _override=None: reference_bundle.resolve(),
|
||||
@@ -75,7 +75,7 @@ def test_video_batch_endpoint_invokes_reference_bundle_subprocess(
|
||||
lambda _bundle: True,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
demo_orch,
|
||||
recording_demo,
|
||||
"VideoBatchRunner",
|
||||
lambda: VideoBatchRunner(
|
||||
bundle_dir=reference_bundle,
|
||||
@@ -89,14 +89,14 @@ def test_video_batch_endpoint_invokes_reference_bundle_subprocess(
|
||||
_fake_reference_subprocess_run(captured),
|
||||
)
|
||||
|
||||
container = build_container(demo_orch.settings, session_factory=sqlite_session_factory)
|
||||
container = build_container(recording_demo.settings, session_factory=sqlite_session_factory)
|
||||
app = FastAPI()
|
||||
app.include_router(demo_orch.router)
|
||||
app.include_router(recording_demo.router)
|
||||
app.dependency_overrides[get_surgery_pipeline] = lambda: container.surgery_pipeline
|
||||
|
||||
client = TestClient(app)
|
||||
res = client.post(
|
||||
"/internal/demo/video-batch-surgery",
|
||||
"/internal/demo/offline-batch",
|
||||
data={
|
||||
"surgery_id": "100001",
|
||||
"candidate_consumables_json": '["耗材1"]',
|
||||
@@ -198,7 +198,10 @@ async def test_start_surgery_endpoint_spawns_algorithm_runner_subprocess(
|
||||
async def _noop_voice_assign(*args: Any, **kwargs: Any) -> None:
|
||||
return None
|
||||
|
||||
monkeypatch.setattr("app.api.assign_voice_terminal_after_recording_started", _noop_voice_assign)
|
||||
monkeypatch.setattr(
|
||||
"app.services.recording_live.assign_voice_terminal_after_recording_started",
|
||||
_noop_voice_assign,
|
||||
)
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(api_router)
|
||||
|
||||
18
backend/tests/test_recording_modes_status.py
Normal file
18
backend/tests/test_recording_modes_status.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""GET /internal/demo/recording-modes-status 契约。"""
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.api import router as api_router
|
||||
|
||||
|
||||
def test_recording_modes_status_paths() -> None:
|
||||
app = FastAPI()
|
||||
app.include_router(api_router)
|
||||
client = TestClient(app)
|
||||
res = client.get("/internal/demo/recording-modes-status")
|
||||
assert res.status_code == 200
|
||||
body = res.json()
|
||||
assert body["simulated_start_path"] == "/internal/demo/simulated-start"
|
||||
assert body["offline_batch_path"] == "/internal/demo/offline-batch"
|
||||
assert "demo_recording_modes_enabled" in body
|
||||
@@ -16,7 +16,7 @@ from app.algorithm_runner import reference_bundle_runtime
|
||||
from app.api import router as api_router
|
||||
from app.dependencies import get_surgery_pipeline
|
||||
from app.domain.consumption import SurgeryConsumptionStored
|
||||
from app.routers import demo_orch
|
||||
from app.routers import recording_demo
|
||||
from app.schemas import SurgeryConsumptionDetail
|
||||
from app.services.video_batch_runner import (
|
||||
VideoBatchRunResult,
|
||||
@@ -558,7 +558,7 @@ def test_demo_video_batch_endpoint_writes_queryable_result(
|
||||
tmp_path: Path,
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
monkeypatch.setattr(demo_orch.settings, "demo_orchestrator_enabled", True)
|
||||
monkeypatch.setattr(recording_demo.settings, "demo_orchestrator_enabled", True)
|
||||
|
||||
detail = SurgeryConsumptionStored(
|
||||
item_id="P1",
|
||||
@@ -629,17 +629,17 @@ def test_demo_video_batch_endpoint_writes_queryable_result(
|
||||
for r in rows
|
||||
]
|
||||
|
||||
monkeypatch.setattr(demo_orch, "VideoBatchRunner", _FakeRunner)
|
||||
monkeypatch.setattr(recording_demo, "VideoBatchRunner", _FakeRunner)
|
||||
pipeline = _FakePipeline()
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(api_router)
|
||||
app.include_router(demo_orch.router)
|
||||
app.include_router(recording_demo.router)
|
||||
app.dependency_overrides[get_surgery_pipeline] = lambda: pipeline
|
||||
|
||||
client = TestClient(app)
|
||||
res = client.post(
|
||||
"/internal/demo/video-batch-surgery",
|
||||
"/internal/demo/offline-batch",
|
||||
data={"surgery_id": "100001", "candidate_consumables_json": '["耗材1"]'},
|
||||
files={"video1": ("case.mp4", b"video-bytes", "video/mp4")},
|
||||
)
|
||||
@@ -662,7 +662,7 @@ def test_demo_video_batch_endpoint_stages_vis_and_purges_cache_when_requested(
|
||||
tmp_path: Path,
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
monkeypatch.setattr(demo_orch.settings, "demo_orchestrator_enabled", True)
|
||||
monkeypatch.setattr(recording_demo.settings, "demo_orchestrator_enabled", True)
|
||||
|
||||
detail = SurgeryConsumptionStored(
|
||||
item_id="P1",
|
||||
@@ -708,14 +708,14 @@ def test_demo_video_batch_endpoint_stages_vis_and_purges_cache_when_requested(
|
||||
) -> None:
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(demo_orch, "VideoBatchRunner", _FakeRunner)
|
||||
monkeypatch.setattr(recording_demo, "VideoBatchRunner", _FakeRunner)
|
||||
app = FastAPI()
|
||||
app.include_router(demo_orch.router)
|
||||
app.include_router(recording_demo.router)
|
||||
app.dependency_overrides[get_surgery_pipeline] = lambda: _FakePipeline()
|
||||
|
||||
client = TestClient(app)
|
||||
res = client.post(
|
||||
"/internal/demo/video-batch-surgery",
|
||||
"/internal/demo/offline-batch",
|
||||
data={
|
||||
"surgery_id": "100001",
|
||||
"candidate_consumables_json": '["耗材1"]',
|
||||
@@ -725,7 +725,7 @@ def test_demo_video_batch_endpoint_stages_vis_and_purges_cache_when_requested(
|
||||
)
|
||||
assert res.status_code == 200, res.text
|
||||
body = res.json()
|
||||
assert body["visualization_url"] == "/internal/demo/video-batch-surgery/100001/visualization"
|
||||
assert body["visualization_url"] == "/internal/demo/offline-batch/100001/visualization"
|
||||
assert vis_calls == ["100001"]
|
||||
assert not (root_dir / "cache" / "100001").exists()
|
||||
pending_input = root_dir / "vis_pending" / "100001" / "input.mp4"
|
||||
|
||||
Reference in New Issue
Block a user