Files
Aislo/resources/tester/test_443_save_stops.py
T
eomsangdonandClaude Opus 5.5 cfdcf0aed7 fix(472-5 남음): 암선 · 암 경사 · B05 상단측 메모리 맵 걷음 · 노선 저장 뒤 옛 노선 칸 · hold 비움
- 암 경계 · 암 절토 경사 저장소(`B06_Section_UI_Page_Persist`) = 세션 칸이 주인 — 읽을 때마다 측점 열쇠 칸(`readCells`) · 고칠 때 지금 칸을 읽어 그 측점만 바꿔 씀(`putCell`) · 메모리 맵 · load · 갈아끼움 다시 읽기(keyedStores · reloadKeyedStores) 걷음 · ↶ 뒤 다음 조작이 되돌림을 덮던 흠(N5) 고침
- 저장 재료 = `offsets()` · `ratios()`(새 Map) · ↶ ↷ 가 rockb · cutslope 칸을 바꾸면 조정창과 같은 onRestore 로 카드 · 면적 다시
- B05 상단측 메모리 맵(uphillOverrides · load/saveUphillOverrides) 걷음 — 읽는 곳 없이 저장 뒤 비우기만 하던 사본 · 값 주인 = 세션 칸 `uphill` · 상단측 차례 = 기본 조각 그대로
- 노선 바뀐 [저장] 성공 뒤(페이지 다시 세운 뒤) 옛 노선 번호의 노선 범위 초안(`clearRouteDrafts`) · `route-draft-hold` 비움 · 넘김 규칙 그대로 · 같은 번호면 노선 칸 안 지움 · 실패면 그대로
- 시험 test_472_5r_rock_owner 7(옛 코드 빨강 5) · 옛 단언 고침(176_3 · 271 · 443 상단측 조각 · 344_3 저장소 load)
- 위키 암선 · 암 경사 · ↶ ↷ 규칙 · 노선 저장 뒤 정리 줄

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 15:13:30 +09:00

213 lines
7.7 KiB
Python

"""PLAN 443 — 저장 누락 ① 구조물 실패 · 상단측 재전송 · 확정 하위 실패(H6 · M3 · M6).
- H6 구조물 저장 실패(409 · 그 밖) = 던짐(저장 한 줄기가 구조물 차례에서 멈춤) · 초안 남음 · 줄 안 막힘.
- M3 상단측 차례 성공 = uphill 세션 비움(다음 [저장]이 다시 안 보냄) · 실패 = 세션 그대로.
- M6 B05 확정 하위(비정규 측점 횡단 · 상단측 병합) 실패 = 응답 failed_parts · 화면이 던짐.
Node 헬퍼 = `helper_443_save_stops.cjs`.
./venv/Scripts/python.exe -m pytest resources/tester/test_443_save_stops.py -q
"""
import asyncio
import json
import subprocess
from pathlib import Path
from uuid import uuid4
import pytest
from B05_Profile import B05_Profile_Router_Lifecycle as lifecycle
from B05_Profile.B05_Profile_Schema import RouteConfirmRequest
HERE = Path(__file__).parent
ROOT = HERE.parents[1]
@pytest.fixture(scope="module")
def out():
proc = subprocess.run(
["node", str(HERE / "helper_443_save_stops.cjs")],
capture_output=True,
text=True,
encoding="utf-8",
timeout=60,
)
assert proc.returncode == 0, proc.stderr
return json.loads(proc.stdout)
# ── H6 ───────────────────────────────────────────────────────────────────────
def test_구조물_409_던짐_초안_남음(out):
assert "먼저 저장" in out["conflict"]
assert out["pendingAfterConflict"] == [{"structure_id": "s1"}]
def test_구조물_실패_뒤_줄이_안_막힘(out):
assert out["boom"] == "서버 500" and out["pendingAfterBoom"] == [{"structure_id": "s1"}]
assert out["ok"] == "ok" and out["pendingAfterOk"] is None
assert out["puts"] == [3, 3, 3] # 세 번 다 나감
def test_구조물_차례가_토스트로_삼키지_않음():
page = (ROOT / "B05_Profile" / "B05_Profile_UI_Page_Structures.ts").read_text("utf-8")
body = page.split("async function persistStructures(")[1].split("\n }\n")[0]
assert "showToast(error" not in body and "return;\n" not in body.split("catch (error)")[1]
# ── M3 · M6 화면 ─────────────────────────────────────────────────────────────
def test_상단측_실패면_세션_그대로_성공이면_비움(out):
"""472-6 — 상단측 = 한 본문 `uphill` 칸 · 본문 = 세션 · 세션 비움 = 서버가 다 저장한 뒤 조각 정리만
(실패면 정리가 안 돎 = 세션 그대로 · 옛 창구 `flushUphillOverrides` 걷음)."""
assert out["exported"] == ["routePartsBody"] # 옛 창구 둘 걷음 · 본문 한 벌만
assert out["uphillAfterBody"] == {"40.000": "right"} and out["bodies"] == []
fetch = (ROOT / "B05_Profile" / "B05_Profile_Api_Fetch.ts").read_text("utf-8")
assert "uphill_overrides: uphillOverridesOf(projectId)," in fetch # 본문 = 세션
save = (ROOT / "A00_Common" / "design" / "design_save.ts").read_text("utf-8")
part = save[save.index('case "uphill":') :]
assert 'after: () => clearState("uphill", projectId),' in part[:500]
assert "for (const key of SAVE_PART_KEYS) await parts[key]?.after?.(" in save # 성공 뒤에만
def test_확정_하위_실패면_던짐(out):
"""472-6 — 하위 차례(비정규 측점 횡단 · 상단측 병합) 실패 = 서버 한 본문이 상단측 차례에서 실패 · 되돌림."""
server = (ROOT / "B06_Section" / "B06_Section_Router_DesignSave.py").read_text("utf-8")
assert 'if parts.get("failed_parts"):' in server
assert 'raise StepFailed("uphill", " · ".join(parts["failed_parts"]) + " 실패")' in server
def test_종단_보기_상단측_차례도_같은_창구():
actions = (ROOT / "B05_Profile" / "B05_Profile_UI_Page_Actions.ts").read_text("utf-8")
# 기본 조각 한 벌(463 · 472-6 · 종단 보기 몫 없음 · 472-5) — 상단측은 세션에서 읽음(메모리 사본 없음)
assert "uphill:" not in actions and "flushRouteParts" not in actions
fetch = (ROOT / "B05_Profile" / "B05_Profile_Api_Fetch.ts").read_text("utf-8")
assert "uphill_overrides: uphillOverridesOf(projectId)," in fetch
assert "confirmRoute(" not in actions
# ── M6 서버 ──────────────────────────────────────────────────────────────────
class _Cursor:
async def __aenter__(self):
return self
async def __aexit__(self, *exc):
return False
class _Connection:
def __init__(self):
self.committed = False
async def begin(self):
pass
async def commit(self):
self.committed = True
async def rollback(self):
pass
def cursor(self):
return _Cursor()
class _Pool:
def __init__(self, connection):
self.connection = connection
def acquire(self):
connection = self.connection
class _Ctx:
async def __aenter__(self):
return connection
async def __aexit__(self, *exc):
return False
return _Ctx()
def _patch(monkeypatch, connection, *, append, longitudinal, merge):
async def latest(*_a):
return {"id": 7, "status": "DRAFT"}
async def noop(*_a, **_k):
return None
async def storage(*_a):
return "p"
async def get_long(*_a):
return longitudinal
monkeypatch.setattr(lifecycle, "get_db_pool", lambda: _Pool(connection))
monkeypatch.setattr(lifecycle, "get_latest_route", latest)
monkeypatch.setattr(lifecycle, "confirm_route", noop)
monkeypatch.setattr(lifecycle, "complete_stage", noop)
monkeypatch.setattr(lifecycle, "_append_irregular_cross_sections", append)
monkeypatch.setattr(lifecycle, "get_project_storage_relative_path", storage)
monkeypatch.setattr(lifecycle, "get_longitudinal_section", get_long)
monkeypatch.setattr(lifecycle, "resolve_stored_project_path", lambda p: p)
monkeypatch.setattr(lifecycle, "_merge_uphill_overrides_into_longitudinal", merge)
monkeypatch.setattr(lifecycle, "sync_uphill_overrides_into_designs", noop)
def _boom(*_a, **_k):
raise RuntimeError("boom")
async def _aboom(*_a, **_k):
raise RuntimeError("boom")
_REQUEST = RouteConfirmRequest(
filter_key="csf",
method="tin",
surface_model_id=1,
uphill_overrides=[{"chainage_m": 40.0, "side": "right"}],
)
def test_확정_하위_실패를_응답에_실음(monkeypatch):
connection = _Connection()
_patch(
monkeypatch,
connection,
append=_aboom,
longitudinal={"longitudinal_file_path": "l.json"},
merge=_boom,
)
result = asyncio.run(lifecycle.confirm_latest_route(uuid4(), _REQUEST, False))
assert result.failed_parts == ["비정규 측점 횡단", "상단측 병합"]
assert connection.committed # 확정 자체는 진행(비치명)
def test_종단_없으면_상단측_병합_실패(monkeypatch):
async def ok(*_a, **_k):
return None
_patch(monkeypatch, _Connection(), append=ok, longitudinal=None, merge=lambda *a: None)
result = asyncio.run(lifecycle.confirm_latest_route(uuid4(), _REQUEST, False))
assert result.failed_parts == ["상단측 병합(종단 없음)"]
def test_다_되면_빈_목록(monkeypatch):
async def ok(*_a, **_k):
return None
_patch(
monkeypatch,
_Connection(),
append=ok,
longitudinal={"longitudinal_file_path": "l.json"},
merge=lambda *a: None,
)
result = asyncio.run(lifecycle.confirm_latest_route(uuid4(), _REQUEST, False))
assert result.failed_parts == []