- 공통 제약 기준값 확정(9-29 사용자 결정) — 후보 표 임시값 그대로 기본값, 화면에서 프로젝트마다 바꿀 수 있음 - 로직 단추 옆 제약 네 칸(비우면 placeholder 로 기본값) · 채운 칸만 POST /route/initial 몸통 criteria 로 실어 보냄 - [확인]의 initial.criteria 에도 실어 성공하면 initial_choice.json 에 겹친 칸만 남김(빈 칸은 기본값이라 안 담김) - 다시 열 때 current_initial.criteria 로 그 칸만 복원 · 캐시가 서버 갈래·비중·제약과 다르면 버림(sameBase) - 서버: generate_initial_route/_SearchGrid 가 stream_offset_m·grade_smooth_m 겹침을 받아 탐색·비교값에 반영 - 응답 criteria.pending 은 이제 늘 빈 목록 — 화면의 「후보 · 사용자 확정 대기」 글 삭제 시험: test_route_initial(+2) · test_b05_routeedit_initial(criteria 겹침·확정 표시 삭제) · test_b05_routeedit_initial_roundtrip(+1, 사본에서 계류 이격 30m 겹침 왕복) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015sCeZz8z8YnbovsKgn7HK7
145 lines
6.2 KiB
Python
145 lines
6.2 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""계획노선 편집 창 — **초기노선 갈래**(계획서 20) 셈 회귀검증.
|
|
|
|
비중 슬라이더 셋은 합 100 을 지키고(하나를 옮기면 나머지가 비율대로), 서버 답은 노드 목록 꼴과
|
|
`/route/plan` 꼴(노드+곡선) 둘 다 편집값 한 벌로 펴야 한다. 기준값 확정(2026-09-29) 뒤로는
|
|
비교값 · 기준값 글에 「후보 · 사용자 확정 대기」 꼬리표가 안 붙는다. 화면 제약 칸에 사용자가
|
|
겹쳐 넣은 값(`criteriaOverrides`)만 캐시·서버 `current_initial` 과 오가고, 그 겹침이 서버 것과
|
|
다르면 캐시를 버린다(`sameBase`). 마지막 초기값은 결과 통 프로젝트 칸.
|
|
|
|
Node 헬퍼(`helper_b05_routeedit_initial.cjs`)가 TS 를 트랜스파일해 돌리고, 여기서 값을 판정한다.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import subprocess
|
|
|
|
import pytest
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def results():
|
|
proc = subprocess.run(
|
|
["node", os.path.join(HERE, "helper_b05_routeedit_initial.cjs")],
|
|
capture_output=True,
|
|
text=True,
|
|
encoding="utf-8",
|
|
timeout=60,
|
|
)
|
|
assert proc.returncode == 0, proc.stderr
|
|
return json.loads(proc.stdout)
|
|
|
|
|
|
def test_비중은_늘_합_100_정수(results):
|
|
for weights in results["moves"]:
|
|
assert list(weights) == ["expected", "earthwork", "grade"]
|
|
assert sum(weights.values()) == 100
|
|
assert all(isinstance(v, int) and 0 <= v <= 100 for v in weights.values())
|
|
|
|
|
|
def test_비중은_나머지를_지금_비율대로_나눔(results):
|
|
moves = results["moves"]
|
|
assert moves[0] == {"expected": 70, "earthwork": 15, "grade": 15}
|
|
assert moves[1] == {"expected": 40, "earthwork": 40, "grade": 20}
|
|
# 나머지가 모두 0 이면 반씩.
|
|
assert moves[2] == {"expected": 60, "earthwork": 20, "grade": 20}
|
|
assert moves[3] == {"expected": 0, "earthwork": 0, "grade": 100}
|
|
assert moves[5]["grade"] == 0
|
|
|
|
|
|
def test_노드_목록_꼴을_편집값으로(results):
|
|
snap = results["plain"]["snapshot"]
|
|
assert snap["planned"] == [[0, 0], [50, 10], [100, 0]]
|
|
assert snap["curveOn"] == [True, True, False]
|
|
assert snap["curveRadius"] == [None, 30, None]
|
|
assert snap["curveLock"] == [None, None, None]
|
|
assert snap["apexLock"] == [False, False, False]
|
|
assert snap["picked"] == -1
|
|
|
|
|
|
def test_노선계획_꼴도_같은_규칙으로(results):
|
|
shape = results["planShape"]
|
|
snap = shape["snapshot"]
|
|
assert snap["planned"] == [[0, 0], [40, 30], [90, 30]]
|
|
assert snap["curveOn"] == [False, True, False]
|
|
assert snap["curveRadius"] == [None, 25, None]
|
|
assert shape["weights"] == {"expected": 60, "earthwork": 20, "grade": 20}
|
|
assert shape["metrics"] is None and shape["criteria"] is None
|
|
|
|
|
|
def test_노드가_모자라면_싣지_않음(results):
|
|
assert "모자랍니다" in results["tooShort"]
|
|
|
|
|
|
def test_비교값과_기준값_글(results):
|
|
labels = results["labels"]
|
|
assert labels["follow"] == "예상노선 추종"
|
|
assert labels["weighted"] == "비중 계산 60/20/20"
|
|
assert labels["metrics"] == (
|
|
"연장 1,234.6m · 기울기 최대 12.3% · 평균 6.1% · 절토 1,500㎥ · 성토 900㎥"
|
|
" · 기준 초과 2구간 · 이격 3.3m"
|
|
)
|
|
assert "로직을 누르면" in labels["metricsNone"]
|
|
# 기준값 확정(2026-09-29) — 「후보 · 사용자 확정 대기」 꼬리표가 더는 안 붙는다.
|
|
assert labels["criteria"] == (
|
|
"공통 제약 종단기울기 ≤ 14.0% · 최소 R 12m · 계류 이격 ≥ 5.0m · 기울기 잴 지반 5m 고름"
|
|
)
|
|
assert "확정 대기" not in labels["criteria"]
|
|
assert labels["criteriaFallback"] == "공통 제약 종단기울기 ≤ — · 최소 R 15m · 계류 이격 ≥ —"
|
|
assert "확정 대기" not in labels["criteriaFallback"]
|
|
|
|
|
|
def test_제약_못_지킨_구간과_상한_풂(results):
|
|
labels = results["labels"]
|
|
assert labels["violations"] == (
|
|
"제약 못 지킨 구간 3곳: No.9+0.0 29.1%(지형상 불가능)"
|
|
" · No.20+0.0 R 9.5m(곡선 둘 자리 모자람) · No.30+0.0 계류 32.0m(계류 버퍼 안)"
|
|
)
|
|
assert labels["violationsNone"] == "제약 못 지킨 구간 없음 — 모두 지킴"
|
|
assert labels["violationsOld"] == "" # 옛 답(목록 칸 없음)은 줄을 숨긴다
|
|
assert labels["violationsMany"].startswith(
|
|
"제약 못 지킨 구간 7곳: No.0+0.0 15.0%(곡선 맞춤 뒤 지반 차)"
|
|
)
|
|
assert labels["violationsMany"].endswith(" 외 2곳")
|
|
assert labels["relaxed"] == "지형상 14.0% 로 못 이어져 상한을 23.5% 로 풂"
|
|
assert labels["notRelaxed"] == ""
|
|
|
|
|
|
def test_서버가_적어_둔_현재_초기값(results):
|
|
bases = results["bases"]
|
|
assert bases["follow"] == {"logic": "follow", "weights": None, "criteria": None}
|
|
assert bases["weighted"] == {
|
|
"logic": "weighted",
|
|
"weights": {"expected": 70, "earthwork": 15, "grade": 15},
|
|
"criteria": {"stream_offset_m": 50},
|
|
}
|
|
assert bases["unknown"] == {"logic": "follow", "weights": None, "criteria": None}
|
|
assert bases["none"] == {"logic": "follow", "weights": None, "criteria": None}
|
|
same = results["sameAsServer"]
|
|
assert same == {
|
|
"earthVsFollow": False,
|
|
"earthVsEarth": True,
|
|
# 같은 갈래라도 넣은 제약 겹침(계류 이격 50m)이 서버 기억(안 겹침)과 다르면 캐시를 버린다.
|
|
"earthVsEarthDiffCriteria": False,
|
|
"weightedSame": True,
|
|
"weightedOther": False,
|
|
}
|
|
|
|
|
|
def test_캐시는_결과_통_프로젝트_칸(results):
|
|
assert results["keys"] == [
|
|
"aislo:result:routeedit-initial:p-1",
|
|
"aislo:result:routeedit-initial:p-3",
|
|
]
|
|
assert results["readBack"]["logic"] == "earthwork"
|
|
assert results["readBack"]["snapshot"]["planned"] == [[0, 0], [50, 10], [100, 0]]
|
|
# 화면 칸에 아무것도 안 겹쳤으면 `criteriaOverrides` 는 null.
|
|
assert results["readBack"]["criteriaOverrides"] is None
|
|
# 계류 이격만 겹쳐 물었으면 그 한 칸만 남는다(나머지는 기본값이라 안 담김).
|
|
assert results["readBackOverrides"]["criteriaOverrides"] == {"stream_offset_m": 50}
|
|
assert results["other"] is None
|
|
assert results["badLogic"] is None
|
|
assert results["cleared"] is None
|