fix(514-4 추천 저장본 · 끌기 시험): 프로젝트 접근 = 표준 횡단면 「없음」과 config 기본값을 같은 뜻으로 두 꼴 다 견줌(지금 꼴 먼저 · 추천 · 초기 종단 · standard_forms) — B05 · B06 [저장]이 손대지 않은 기본값을 종단 정본에 실어 전처리(None) 저장본 키가 어긋나 다섯 갈래 409 · 고친 표준은 그대로 409 · Store 파일 그대로(엔진 지문 안 바뀜) · test_508_2 끌기 = 계정 보던 화면(user_ui_prefs) 시작 전 비움 · 끝나면 되돌림 · 둘째 도엽 올 때까지 기다림 · 새 시험 test_514_4

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-22 21:53:30 +09:00
co-authored by Claude Opus 5.5
parent f195a0c72c
commit 3736e601ee
4 changed files with 137 additions and 14 deletions
+21 -9
View File
@@ -157,15 +157,17 @@ async def compute_initial(
forget_pool_on_standard(pool_key, inputs["standard"])
stored = await asyncio.to_thread(read_stored, project_root, key)
if stored is None and not may_compute:
# 프로젝트 접근 — 엔진 코드 판이 달라도 같은 입력 저장본을 꺼냄(505-1 덧)
found = await asyncio.to_thread(
read_stored_any,
project_root,
pool_key,
lambda source: request_fingerprint(
route, terrain, streams, **inputs, source=source
)[0],
)
# 프로젝트 접근 — 엔진 코드 판이 달라도 같은 입력 저장본을 꺼냄(505-1 덧 · 514-4)
found = None
for form in standard_forms(inputs["standard"]):
found = found or await asyncio.to_thread(
read_stored_any,
project_root,
pool_key,
lambda source, form=form: request_fingerprint(
route, terrain, streams, **{**inputs, "standard": form}, source=source
)[0],
)
if found is None:
return JSONResponse(
status_code=409, content={"status": "error", "message": NOT_PREPARED}
@@ -249,6 +251,16 @@ async def project_standard(project_id: UUID) -> dict[str, Any] | None:
return stored_standard_cross_section({"data": data if isinstance(data, dict) else None})
def standard_forms(standard: dict[str, Any] | None) -> list[dict[str, Any] | None]:
"""같은 뜻 표준 횡단면 꼴(지금 꼴 먼저) — 없음(None) = config 기본값 · 키 · 지문은 둘을 가름 ·
[저장]은 손대지 않은 패널 기본값도 실어 보냄 → 프로젝트 접근은 두 꼴 저장본 다 꺼냄(514-4)."""
from config.config_system import STANDARD_CROSS_SECTION
if standard is None:
return [None, STANDARD_CROSS_SECTION]
return [standard, None] if standard == STANDARD_CROSS_SECTION else [standard]
async def precompute_defaults(project_id: UUID) -> None:
"""체인 끝 예약(PLAN 327-2 · 351-1) — 기본 다섯 알약 + 저장된 제약(initial_choice)의 다섯 ×
고를 수 있는 지형(509-5 · 설계 지형 먼저 · 수치지형도는 DB 모델 있을 때만).
@@ -49,6 +49,7 @@ async def initial_profile_of(
NOT_PREPARED,
balanced_profile_of,
project_standard,
standard_forms,
)
from B06_Section.B06_Section_Repository import (
get_latest_section_options,
@@ -113,10 +114,13 @@ async def initial_profile_of(
"road": await replan._road_settings(project_id),
}
mark = ip.fingerprint(parts)
# 프로젝트 접근(compute 거짓) = 코드 지문 안 견줌(505-1 덧)
stored = await asyncio.to_thread(
ip.read_initial, project_root, key, mark, None if compute else parts
)
# 프로젝트 접근(compute 거짓) = 코드 지문 안 견줌(505-1 덧) · 표준 두 꼴(514-4)
forms = standard_forms(parts["standard"])
stored = None
for form in [None] if compute else [{**parts, "standard": f} for f in forms]:
stored = stored or await asyncio.to_thread(
ip.read_initial, project_root, key, mark, form
)
if stored is not None:
return stored
if not compute:
+56 -1
View File
@@ -134,6 +134,45 @@ def _drop_logs() -> None:
asyncio.run(run())
# 계정 화면 취향(서버 user_ui_prefs) — 새 문맥이어도 남음 · 끌기 시험이 건드리는 것
VIEW_PREFS = (f"aislo:pref:routeedit-view:{PROJECT}", "aislo:pref:profile-collapsed")
def _prefs(email: str, values: dict[str, str | None] | None = None) -> dict[str, str | None]:
"""`VIEW_PREFS` 값 읽기 · `values` 있으면 그 키만 되씀(None = 지움 · 다른 키 그대로)."""
import asyncio
import concurrent.futures
from config.config_db import close_db_pool, init_db_pool
async def run():
pool = await init_db_pool()
try:
async with pool.acquire() as connection, connection.cursor() as cursor:
await cursor.execute("SELECT id FROM users WHERE email = %s", (email,))
user = (await cursor.fetchone())[0]
for key, value in (values or {}).items():
path = f'$."{key}"'
if value is None:
sql, args = "JSON_REMOVE(prefs, %s)", (path,)
else:
sql, args = "JSON_SET(prefs, %s, %s)", (path, value)
await cursor.execute(
f"UPDATE user_ui_prefs SET prefs = {sql} WHERE user_id = %s", (*args, user)
)
await connection.commit()
await cursor.execute("SELECT prefs FROM user_ui_prefs WHERE user_id = %s", (user,))
row = await cursor.fetchone()
prefs = json.loads(row[0]) if row else {}
return {key: prefs.get(key) for key in VIEW_PREFS}
finally:
await close_db_pool()
# 플레이라이트 고리 안 — asyncio.run 은 딴 실에서
with concurrent.futures.ThreadPoolExecutor(1) as pool:
return pool.submit(asyncio.run, run()).result()
def _board(corridor: str | None) -> None:
board = json.loads(BOARD.read_text("utf-8"))
board["sites"]["corridor_mesh"] = corridor or "both"
@@ -269,6 +308,17 @@ def test_노선_크게_끌어_놓기_등고선_다시_받음(ctx):
# 새 문맥 — 앞 시험 페이지가 남긴 보던 화면 · 보관함 없이(축척 · 받기가 늘 같게)
fresh_ctx = ctx.browser.new_context(viewport={"width": 1600, "height": 1000}, user_agent=MARK)
cred = json.loads((ROOT / "tmp" / "aislo_cred.json").read_text(encoding="utf-8"))
# 보던 화면은 계정(서버)에도 남음 — 지난 판 끌기 · 줄임이 쌓여 시작 화면이 매번 달랐음(514-4)
kept = _prefs(cred["email"])
_prefs(cred["email"], {VIEW_PREFS[0]: None})
try:
_drag_and_fetch(fresh_ctx, cred, got, on_response)
finally:
fresh_ctx.close()
_prefs(cred["email"], kept) # 계정 화면 취향 되돌림
def _drag_and_fetch(fresh_ctx, cred, got, on_response) -> None:
login = {k: cred[k] for k in ("email", "password")}
assert fresh_ctx.request.post(f"{BASE}/api/auth/login/request", data=login, timeout=60000).ok
page = _open(fresh_ctx, on_response)
@@ -309,6 +359,12 @@ def test_노선_크게_끌어_놓기_등고선_다시_받음(ctx):
drops += 1
page.wait_for_timeout(4000)
if any(g["bbox"] for g in got):
# 둘째 도엽(수 MB)은 늦게 옴 — 바쁜 서버(싱크 때 시험 묶음)면 4 초 안에 하나만(514-4)
for _ in range(60):
if sum(g["bbox"] for g in got) >= 2:
break
page.wait_for_timeout(500)
page.wait_for_timeout(1500) # 더 오는 것(다시 받기 두 번)도 잡히게
break
page.mouse.move(1500, 900)
page.mouse.down(button="middle")
@@ -326,7 +382,6 @@ def test_노선_크게_끌어_놓기_등고선_다시_받음(ctx):
page.screenshot(path=str(SHOTS / "sheet_follow_dark.png"))
page.evaluate("() => document.documentElement.setAttribute('data-theme', 'light')")
page.close()
fresh_ctx.close()
fresh = [g for g in after if g["bbox"]]
print("first", first, "fresh", fresh, "drops", drops, "status", status)
assert during == 0 # 끄는 동안 = 안 받음
@@ -0,0 +1,52 @@
"""PLAN 514-4 — 추천 저장본 접근 = 표준 횡단면 「없음」과 config 기본값을 같은 뜻으로.
./venv/Scripts/python.exe -m pytest resources/tester/test_514_4_recommend_standard_default.py -q
전처리 때 표준 없음(None)으로 셈 → B05 · B06 [저장]이 손대지 않은 패널 기본값을 실음 → 화면은
그 저장본을 꺼냄(409 아님 · 셈 0) · 반대 꼴도 · 사용자가 고친 표준은 옛 저장본을 안 끌어옴.
"""
from __future__ import annotations
import copy
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
TESTER = Path(__file__).resolve().parent
for path in (ROOT, TESTER):
if str(path) not in sys.path:
sys.path.insert(0, str(path))
from test_224_3_recommend_store import _ask, wired # noqa: E402, F401 — 가짜 세계 fixture
from B05_Profile.B05_Profile_Engine_RouteInitial_Store import DEFAULT_MODES # noqa: E402
from config.config_system import STANDARD_CROSS_SECTION # noqa: E402
def _chain_then_screen(wired, before, after) -> list[dict]: # noqa: F811
wired.standard = before
for mode in DEFAULT_MODES:
_ask(True, mode=mode)
count = wired.count
wired.standard = after
answers = [_ask(False, mode=mode) for mode in DEFAULT_MODES]
assert wired.count == count # 접근 = 셈 0
return answers
def test_전처리_없음_저장_뒤_기본값이면_꺼냄(wired) -> None: # noqa: F811
answers = _chain_then_screen(wired, None, copy.deepcopy(STANDARD_CROSS_SECTION))
assert all(isinstance(a, dict) and a["stored"] is True for a in answers), answers
def test_전처리_기본값_뒤_없음이어도_꺼냄(wired) -> None: # noqa: F811
answers = _chain_then_screen(wired, copy.deepcopy(STANDARD_CROSS_SECTION), None)
assert all(isinstance(a, dict) and a["stored"] is True for a in answers), answers
def test_고친_표준은_옛_저장본_안_끌어옴(wired) -> None: # noqa: F811
custom = copy.deepcopy(STANDARD_CROSS_SECTION)
custom["soil"]["road_width_m"] = 4.0
answers = _chain_then_screen(wired, None, custom)
assert all(getattr(a, "status_code", None) == 409 for a in answers), answers