- 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>
126 lines
8.7 KiB
Python
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)
|