"""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)