将 Demo 录制收敛为三条独立链路,并重做联调台 UI。

移除 demo_orch 统一编排,改为 recording_demo 与 live/simulated 服务;客户端拆分为静态资源,以模式卡片与 chip 耗材覆盖三链路联调,并同步测试与文档。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Kevin
2026-05-21 16:50:23 +08:00
parent 09885b4184
commit 153c91f8ff
16 changed files with 2030 additions and 1364 deletions

View File

@@ -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:

View File

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

View 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

View File

@@ -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"