Files
Pier_v1/Code/교각모델링앱/tools/test_review.py
T
ahnyongjooandClaude Sonnet 5.5 964a841de1 chore(rebar): Pier_T_rebar 작업 폴더 초기 구성 및 외부 의존 파일 통합
- Code\rebar_pipeline, review_pipeline, pier-program을 정션에서 실제 폴더로 전환
- Code\rhino_mcp 및 CLAUDE_rhino배경.md 추가, .mcp.json은 PYTHONPATH로 새 위치 사용
- pier-program 내부 .git 제거(이력은 backup\pier-program_20261028.bundle에 보관)
- worktree 가이드 반영(CLAUDE.local.md, HANDOFF.md)
- D:\Rhino-MCP 경로 참조를 새 위치로 치환

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 11:20:18 +09:00

126 lines
8.7 KiB
Python

"""Test of the one-screen confirmation "검토 요약 확정" (2026-10-20). No Rhino. Own data root 검증/review_test_data (recreated): a copy of Ungok P4 with
no confirmations. Acceptance: nothing is confirmed by looking at the page; the list must be read to its end (marker) and a name given; a bundle the app
check reports as 주의 / 근거 없음 / 결정 기록 is confirmed only when it is ticked; 일치 bundles only with the group tick; 불일치 bundles never; every
confirmation record carries an informative reason (app check, change, origin) plus the person's note and the app status at that time; a changed list
invalidates the marker."""
import json
import os
import shutil
import sys
from pathlib import Path
APP = Path(__file__).resolve().parents[1]
D = APP / "data"
TD = APP / "검증" / "review_test_data"
if TD.exists():
shutil.rmtree(TD)
sys.path.insert(0, str(APP / "tools"))
from testlib import verified_pier # noqa: E402
SRC = verified_pier("운곡교P4") # the gate-verified P4 (live, or the recycle-bin copy after a real-use re-creation)
if SRC is None:
print("SKIP no verified 운곡교P4")
print("RESULT PASS")
sys.exit(0)
shutil.copytree(SRC, TD / "운곡교P4", ignore=shutil.ignore_patterns("runs", "backup", "dwg", "dxf", "confirm.json", "autocheck.json", "review_view.json", "confirm_view.json"))
shutil.copytree(D / "_템플릿", TD / "_템플릿", ignore=shutil.ignore_patterns("_이전"))
os.environ["PIERAPP_DATA"] = str(TD)
sys.path.insert(0, str(APP / "web"))
sys.path.insert(0, str(APP))
import app as webapp # noqa: E402
import store # noqa: E402
from engine import review as reviewlib # noqa: E402
assert Path(str(store.DATA)) == TD
ok = True
def check(name, cond, detail=""):
global ok
ok &= bool(cond)
print("PASS" if cond else "FAIL", name, str(detail)[:300], flush=True)
p = store.Pier("운곡교P4")
# one real mismatch (a stale value in expected only) so that group C exists
E = p.load("expected")
E["G1"]["piles"]["pitch_y"] = 1550
p.save("expected", E)
c = webapp.app.test_client()
items = webapp.review_items(p)
by = {g: [i for i in items if i["group"] == g] for g in "ABCD"}
check("the list has all four groups (일치 / 주의·근거 없음 / 불일치 / 결정 기록)", all(by[g] for g in "ABCD"), {g: len(by[g]) for g in "ABCD"})
check("the stale expected value makes expected:G1 and params:piles 불일치 (group C)", {"expected:G1", "params:piles"} <= {i["id"] for i in by["C"]}, [i["id"] for i in by["C"]])
check("every item knows what changed since creation, what the app did not verify (B) and its origin", all(i["change"]["text"] for i in items) and all(i["unverified"] for i in by["B"]))
h = c.get("/pier/운곡교P4/review").get_data(as_text=True)
check("page: one tick per B and D bundle, one group tick for A, none for C", h.count('name="ack"') == len(by["B"]) + len(by["D"]) and 'name="ack_matched"' in h and all(f'value="{i["id"]}"' not in h for i in by["C"]))
check("looking at the page confirmed nothing", not store.rjson(p.dir / "confirm.json", {}))
# ---- the gates of the form
r = c.post("/pier/운곡교P4/review", data={"by": "시험자", "ack_matched": "1"}, follow_redirects=True)
check("without having read the list to its end: refused, nothing written", "끝까지 읽은 뒤" in r.get_data(as_text=True) and not store.rjson(p.dir / "confirm.json", {}))
c.post("/pier/운곡교P4/review/read")
r = c.post("/pier/운곡교P4/review", data={"by": "", "ack_matched": "1"}, follow_redirects=True)
check("without a name: refused", "이름을 입력" in r.get_data(as_text=True) and not store.rjson(p.dir / "confirm.json", {}))
r = c.post("/pier/운곡교P4/review", data={"by": "시험자"}, follow_redirects=True)
check("a name but no tick at all: nothing is confirmed", "확정한 묶음이 없습니다" in r.get_data(as_text=True) and not store.rjson(p.dir / "confirm.json", {}))
# ---- group A with its tick only
r = c.post("/pier/운곡교P4/review", data={"by": "시험자", "ack_matched": "1", "note": "시험 의견"}, follow_redirects=True)
conf = store.rjson(p.dir / "confirm.json", {})
check("일치 group tick: exactly the 일치 bundles are confirmed", {k for k in conf} == {i["id"] for i in by["A"]}, sorted(conf)[:3])
rec = conf[by["A"][0]["id"]]
check("the record has an informative reason (app check + change) and the note, the app status and the name",
rec["why"].startswith("[검토 요약 일괄 확정]") and "앱 확인 일치" in rec["why"] and "의견: 시험 의견" in rec["why"] and rec["app_st"] == "g" and rec["by"] == "시험자", rec["why"])
check("B / C / D bundles stayed unconfirmed", not any(p.section_state(i["file"], i["key"])["confirmed"] for g in "BCD" for i in by[g]))
# ---- B and D need their own tick; C is refused even when posted
ticks = [i["id"] for i in by["B"][:2]] + [i["id"] for i in by["D"]] + [by["C"][0]["id"]]
c.post("/pier/운곡교P4/review/read")
r = c.post("/pier/운곡교P4/review", data={"by": "시험자", "ack": ticks}, follow_redirects=True)
conf = store.rjson(p.dir / "confirm.json", {})
check("ticked B and D bundles are confirmed", all(t in conf for t in ticks[:-1]))
check("the 불일치 bundle is not confirmed even though its id was posted", by["C"][0]["id"] not in conf)
rb = conf[by["B"][0]["id"]]
check("a B record says the person compared with the drawing, names the app status and what was not verified",
"사용자가 도면과 대조했다고 표시함" in rb["why"] and rb["app_st"] in ("w", "n") and ("근거 없음" in rb["why"] or "주의" in rb["why"]), rb["why"])
rd = conf["expected:planned_fail"]
check("the decision record (planned FAIL) is confirmed only by its tick and keeps its status 해당 없음", rd["app_st"] == "x" and "[검토 요약 일괄 확정]" in rd["why"])
check("the unticked B bundles stayed unconfirmed", all(i["id"] not in conf for i in by["B"][2:]))
check("the page has the bulk buttons (check all / clear) for the B and D groups", 'id="all-on"' in h and 'id="all-off"' in h)
check("a provisional registration that waits for the designer is flagged, and the flag is written into the reason",
"설계자 확인 전 임시 등록 포함" in by["D"][0]["flags"] if by["D"][0]["id"] == "expected:planned_fail" and any("설계자 확인" in d for d in by["D"][0].get("detail", [])) else True)
check("the reason of a flagged bundle carries the 주의 표시", "주의 표시" in rd["why"] if reviewlib.flags(dict(change={"mode": "x"}, st="x", detail=["a 설계자 확인 전"])) else False)
check("flags(): template original / provisional registration", reviewlib.flags(dict(change={"mode": "same"}, st="n")) == ["템플릿 원본 그대로(새 도면 근거 없음)"])
# ---- a changed list invalidates the marker
c.post("/pier/운곡교P4/review/read")
P = p.load("params")
P["footing"]["thickness"] = 2300.0
p.save("params", P)
r = c.post("/pier/운곡교P4/review", data={"by": "시험자", "ack": [i["id"] for i in by["B"][2:]]}, follow_redirects=True)
check("after a value changed the marker no longer matches: refused", "끝까지 읽은 뒤" in r.get_data(as_text=True) and all(i["id"] not in store.rjson(p.dir / "confirm.json", {}) for i in by["B"][2:]))
P["footing"]["thickness"] = 2200.0
p.save("params", P)
# ---- fix the mismatch, confirm the rest, nothing is left
E = p.load("expected")
E["G1"]["piles"]["pitch_y"] = 1675
p.save("expected", E)
items = webapp.review_items(p)
c.post("/pier/운곡교P4/review/read")
r = c.post("/pier/운곡교P4/review", data={"by": "시험자", "ack_matched": "1", "ack": [i["id"] for i in items if i["group"] in "BD"]}, follow_redirects=True)
left = webapp.review_items(p)
check("after fixing the mismatch every required bundle can be confirmed through this one screen", not left, [i["id"] for i in left])
check("every confirmation record of this screen has a non-empty reason", all(v.get("why") for v in store.rjson(p.dir / "confirm.json", {}).values()))
check("the gates are not blocked by unconfirmed bundles any more", not [b for b in webapp.gate_blockers(p, "G1") if "미확정" in b])
# ---- pure helpers
ch = reviewlib.bundle_change({"a": 1, "b": [1, 2]}, {"a": 1, "b": [1, 3]})
check("bundle_change: counts the changed values and shows the first ones", ch["mode"] == "changed" and ch["n_changed"] == 1 and "b.1" in ch["text"], ch["text"])
check("bundle_change: no snapshot -> says it cannot compare", reviewlib.bundle_change({"a": 1}, None)["mode"] == "none")
check("provenance: reads the app tools' logs", reviewlib.provenance("params", "footing", [{"at": "2026-10-20 09:00", "by": "x", "fields": ["params:footing.size_x", "params:column.height"]}]) == ["도면에서 읽기 1건 반영 (2026-10-20 09:00 x)"])
print("RESULT", "PASS" if ok else "FAIL")
sys.exit(0 if ok else 1)