- 1-A 도면 GET · 저장 PUT 한도 60 s · 저장 중 미리받기 쉼 · 끊긴 저장 = 목록 confirmed 로 판정
- 2 CAD 자동백업 열쇠 = b07:{프로젝트}:{도면} · 도각 편집 = b07-frame:{프로젝트} · [초기화] 같은 열쇠
- 3-1 [초기화] 물음 글 = 프로젝트 도각 그대로 · [도각 편집] → [기본 도각 복원] 안내
- 3-2 풀린 장 저장본 json · 수량표 찌꺼기 걷음(_write_manifest 한 곳)
- 5-1 확정 풀기 · 배치 · 축척 = mark_stage(save)
- 7-3 start_stage 가 DEV_UNLOCK 표식 남김
- 6-a navigateDrawing · onNavigate · hasPrev · hasNext 걷음 · 6-b cad-host-save 토글 걷음 · 4-2 B08 안 쓰는 글 11 걷음
- 6-f 특성 판 색 끌기 = 되돌리기 한 칸(paintNowCommitLater 한 벌) · 6-g LAYMRG 사본 길
- 4-1 B09 [인쇄 미리보기] = 저장 뒤 이동 · 단계 그대로 · 6-e M02 카드 도움말 DWG 한 줄
- 시험 test_506_3_* 둘 · cad/undo-506-3 · test_298_2_b07_buttons 열쇠 맞춤
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
188 lines
6.4 KiB
Python
188 lines
6.4 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""506-3 가 · 나(서버) — B07 저장 길 · 풀린 장 찌꺼기.
|
|
|
|
못박는 것
|
|
1-A 도면 GET · 저장 PUT = `API_ANALYSIS_TIMEOUT_MS` · 저장 중 미리받기 쉼 · 끊긴 저장 = 목록 `confirmed` 로 판정.
|
|
2 CAD 자동백업 칸 = `b07:{프로젝트}:{도면}` · 도각 편집 = `b07-frame:{프로젝트}` · [초기화]가 같은 열쇠로 지움.
|
|
3-1 [초기화] 물음 글 = 프로젝트 도각은 그대로 · [도각 편집] → [기본 도각 복원] 안내.
|
|
3-2 풀린 장(`confirmed` 거짓) = 저장본 json · 수량표 걷음 · 도면번호 남김.
|
|
5-1 B07 쓰기 길 = `mark_stage(save)`(옛 `start_stage` 없음).
|
|
7-3 `start_stage` 가 `DEV_UNLOCK:` 표식을 남김.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
ROOT = Path(HERE).parents[1]
|
|
B07 = ROOT / "B07_DesignDetail"
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
|
|
def _read(name: str) -> str:
|
|
return (B07 / name).read_text(encoding="utf-8").replace("\r\n", "\n")
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def out():
|
|
proc = subprocess.run(
|
|
["node", os.path.join(HERE, "helper_506_3_b07_save.cjs")],
|
|
capture_output=True,
|
|
text=True,
|
|
encoding="utf-8",
|
|
timeout=60,
|
|
)
|
|
assert proc.returncode == 0, proc.stderr
|
|
return json.loads(proc.stdout)
|
|
|
|
|
|
def test_1a_한도():
|
|
api = _read("B07_DesignDetail_Api_Fetch.ts")
|
|
get = api[api.index("export function fetchDesignDrawing(") :]
|
|
get = get[: get.index("\n}\n")]
|
|
assert "API_ANALYSIS_TIMEOUT_MS" in get
|
|
put = api[api.index("export function confirmDesignDrawing(") :]
|
|
put = put[: put.index("\n}\n")]
|
|
assert "API_ANALYSIS_TIMEOUT_MS" in put
|
|
assert "timeoutMs: number = API_TIMEOUT_MS" in api
|
|
|
|
|
|
def test_1a_미리받기_쉼():
|
|
page = _read("B07_DesignDetail_UI_Page.ts")
|
|
assert "while (loadInFlight || isSaving())" in page
|
|
|
|
|
|
def test_1a_저장_판정(out):
|
|
assert out["normal"]["savingSeen"] is True
|
|
assert out["normal"]["savingAfter"] is False
|
|
assert out["normal"]["calls"] == [
|
|
"overlay+",
|
|
"confirm:cross_s1",
|
|
"overlay-",
|
|
"toast:success",
|
|
"saved",
|
|
]
|
|
# 끊김 + 서버 저장됨 = 저장했습니다
|
|
assert out["abortSaved"]["calls"] == [
|
|
"overlay+",
|
|
"confirm:cross_s1",
|
|
"list",
|
|
"overlay-",
|
|
"toast:success",
|
|
"saved",
|
|
]
|
|
# 끊김 + 서버 저장 안 됨 = 머묾
|
|
assert out["abortUnsaved"]["calls"] == [
|
|
"overlay+",
|
|
"confirm:cross_s1",
|
|
"list",
|
|
"toast:error",
|
|
"overlay-",
|
|
]
|
|
assert out["abortUnsaved"]["savingAfter"] is False
|
|
# 끊김 아닌 오류 = 목록 안 읽음
|
|
assert out["otherError"] == ["overlay+", "confirm:cross_s1", "toast:error", "overlay-"]
|
|
|
|
|
|
def test_2_자동백업_열쇠(out):
|
|
assert out["scope"] == "b07:p1:cross_s1"
|
|
assert "drop:OPEN_WEB_CAD__RECOVERY__b07:p1:cross_s1" in out["reset"]["calls"]
|
|
page = _read("B07_DesignDetail_UI_Page.ts")
|
|
assert "recoveryScopeOf(projectId, drawing.id)" in page
|
|
assert "{ guide, recoveryScope }" in page
|
|
assert "recoveryScope: `b07-frame:${projectId}`" in page
|
|
|
|
|
|
def test_3_1_초기화_글(out):
|
|
asked = out["reset"]["asked"]
|
|
assert "프로젝트 도각은 그대로" in asked
|
|
assert "[도각 편집] → [기본 도각 복원]" in asked
|
|
frame = (ROOT / "ui_template" / "cad_host" / "cad_host_frame_edit.ts").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
assert 'label: "기본 도각 복원"' in frame
|
|
|
|
|
|
def _project(tmp_path: Path) -> Path:
|
|
from B07_DesignDetail.B07_DesignDetail_Router_Support_Io import _design_root, _write_manifest
|
|
|
|
stage = _design_root(tmp_path)
|
|
(stage / "drawings").mkdir(parents=True)
|
|
for drawing_id in ("cross_s1", "plan_a_1"):
|
|
(stage / "drawings" / f"{drawing_id}.json").write_text("{}", encoding="utf-8")
|
|
_write_manifest(
|
|
tmp_path,
|
|
{
|
|
"drawings": {
|
|
"cross_s1": {
|
|
"kind": "cross",
|
|
"confirmed": True,
|
|
"number": 7,
|
|
"file": "drawings/cross_s1.json",
|
|
"quantity_table": {"a": 1},
|
|
"quantity_tables": {"1": {}},
|
|
},
|
|
"plan_a_1": {"kind": "plan", "confirmed": True, "file": "drawings/plan_a_1.json"},
|
|
}
|
|
},
|
|
)
|
|
return stage
|
|
|
|
|
|
def test_3_2_풂_찌꺼기(tmp_path):
|
|
from B07_DesignDetail.B07_DesignDetail_Router_Support import _invalidate_drawing
|
|
from B07_DesignDetail.B07_DesignDetail_Router_Support_Io import _read_manifest
|
|
|
|
stage = _project(tmp_path)
|
|
_invalidate_drawing(tmp_path, "cross_s1")
|
|
entry = _read_manifest(tmp_path)["drawings"]["cross_s1"]
|
|
assert entry == {"kind": "cross", "confirmed": False, "number": 7}
|
|
assert not (stage / "drawings" / "cross_s1.json").exists()
|
|
# 안 푼 장은 그대로
|
|
assert (stage / "drawings" / "plan_a_1.json").exists()
|
|
assert _read_manifest(tmp_path)["drawings"]["plan_a_1"]["file"] == "drawings/plan_a_1.json"
|
|
|
|
|
|
def test_3_2_초기화_찌꺼기(tmp_path):
|
|
from B07_DesignDetail.B07_DesignDetail_Router_Reset import reset_design_files
|
|
|
|
stage = _project(tmp_path)
|
|
assert reset_design_files(tmp_path)["released"] == 2
|
|
assert list((stage / "drawings").iterdir()) == []
|
|
|
|
|
|
def test_5_1_새_단계_규칙():
|
|
router = _read("B07_DesignDetail_Router.py")
|
|
scale = _read("B07_DesignDetail_Router_Support_Scale.py")
|
|
assert "start_stage" not in router
|
|
assert "start_stage" not in scale
|
|
assert router.count('await mark_stage(str(project_id), 4, "save", cursor)') == 3
|
|
assert scale.count('await mark_stage(str(project_id), 4, "save", cursor)') == 1
|
|
|
|
|
|
class _Cursor:
|
|
def __init__(self) -> None:
|
|
self.sql: list[str] = []
|
|
|
|
async def execute(self, sql: str, args: tuple = ()) -> None:
|
|
# pymysql 꼴 — 인자가 있으면 `%` 서식(`%%` = 글자 %)
|
|
self.sql.append(sql % tuple(repr(a) for a in args) if args else sql)
|
|
|
|
|
|
def test_7_3_잠금풀기_표식_남김():
|
|
from common_util.common_util_workflow_state import start_stage
|
|
|
|
cursor = _Cursor()
|
|
asyncio.run(start_stage(cursor, "p1", 4))
|
|
first = cursor.sql[0]
|
|
assert "message = CASE WHEN message LIKE 'DEV_UNLOCK:%' THEN message ELSE NULL END" in first
|
|
assert "message = NULL" not in first
|