"""직행 업로드(`POST /api/projects/{id}/files`)가 청크 경로와 같은 보호막을 갖는지. 왜 있나(2026-09-08) — 창 넷이 DB·저장소를 함께 쓰는데 이 갈래만 ① 분석 중 차단(`is_analysis_running`) ② 같은 이름 옛 행 내리기(`supersede_previous_input_files`) 가 빠져 있었다. 빠지면 분석 2개가 같은 산출물 경로에서 부딪히고, 같은 파일이 두 줄로 활성으로 남아 어느 것으로 도는지 순서에 달린다. 파일 내용(소스 문자열)에 기대지 않는다 — **함수 객체를 실제로 호출**해 확인한다. """ from __future__ import annotations import asyncio from types import SimpleNamespace from typing import Any import pytest import B03_FileInput.B03_FileInput_Router as router_module class _Cursor: async def __aenter__(self) -> "_Cursor": return self async def __aexit__(self, *_: Any) -> None: return None async def execute(self, *_: Any, **__: Any) -> None: return None async def fetchone(self) -> None: return None class _Connection: def cursor(self, *_: Any, **__: Any) -> _Cursor: return _Cursor() class _Acquire: async def __aenter__(self) -> _Connection: return _Connection() async def __aexit__(self, *_: Any) -> None: return None class _Pool: def acquire(self) -> _Acquire: return _Acquire() class _Upload(SimpleNamespace): """UploadFile 흉내 — 라우터가 마지막에 `close()` 를 부른다.""" async def close(self) -> None: return None def _upload(filename: str) -> _Upload: return _Upload(filename=filename, size=10) _FULL_SET = ["a.las", "b.tif", "b.tfw", "b.prj", "route.csv"] def test_분석_중이면_직행_업로드를_409로_막는다(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(router_module, "get_db_pool", lambda: _Pool()) monkeypatch.setattr( router_module, "get_project_storage_relative_path", _async_return("1/3/proj") ) monkeypatch.setattr(router_module, "resolve_stored_project_path", lambda _p: ".") monkeypatch.setattr(router_module, "is_analysis_running", _async_return(True)) response = asyncio.run( router_module.upload_project_files( project_id=_PROJECT_ID, files=[_upload(name) for name in _FULL_SET], las_free=False, session={"role": "USER"}, ) ) assert response.status_code == 409 def test_직행_업로드가_같은_이름_옛_행을_내린다() -> None: """`supersede_previous_input_files` 가 이 모듈에 실제로 이어져 있는지.""" from B03_FileInput.B03_FileInput_Repository import ( supersede_previous_input_files as repository_function, ) assert router_module.supersede_previous_input_files is repository_function def test_세_갈래가_같은_보호막을_쓴다() -> None: """청크·임시배치·직행이 같은 두 함수를 쓰는지 — 한 갈래만 빠지는 사고를 막는다.""" import B03_FileInput.B03_FileInput_Router_Chunks as chunks import B03_FileInput.B03_FileInput_Router_Temp as temp for module in (router_module, chunks, temp): assert hasattr(module, "is_analysis_running"), module.__name__ assert hasattr(module, "supersede_previous_input_files"), module.__name__ # 확인 단계도 세 갈래가 다 들고 있어야 한다 — 하나만 빠지면 그 갈래가 500 으로 죽는다 # (2026-09-08 실제로 청크 갈래에서 `NameError` 로 겪었다). for module in (router_module, chunks, temp): assert hasattr(module, "OutputsWouldBeDiscarded"), module.__name__ assert hasattr(module, "_confirm_replace_response"), module.__name__ from uuid import UUID # noqa: E402 — 아래 상수에서만 쓴다 _PROJECT_ID = UUID("fa76c162-71c7-46e5-a95d-fb3930665a45") def _async_return(value: Any): async def _inner(*_: Any, **__: Any) -> Any: return value return _inner