Files
Aislo/resources/tester/test_b08_blocked_invariant.py
eomsangdonandClaude Opus 5 d7cb14f416 test(B08): tmp/tests 중 tester 에 없던 47 개를 resources/tester 로 옮김
tmp/ 가 창끼리 안 건너가는 것이 확정돼(랩탑이 시간 두고 두 번 확인) 시험·예외가
저절로 건너가도록 git 안으로 옮김. 사용자 확정.

- 내용은 하나도 안 고침 — 자리만 옮김. tmp/tests 는 남겨 둠.
- 같은 이름이 이미 있던 64 개는 랩탑 것을 그대로 두고 건너뜀.
- helper_b05_*.js 둘은 랩탑이 .cjs 로 이미 올린 것과 **줄바꿈만 다른 같은 내용**이라
  복사본을 도로 뺌(시험이 .cjs 를 부름).
- resources/tester/ 에서 전체 1176 통과 · 29 건너뜀 · 실패 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 17:15:57 +09:00

153 lines
6.5 KiB
Python

"""**코드가 없으면 반드시 막힘 표시가 붙는다** (2026-09-09 랩탑 메인 제보로 세운 불변식).
⚠⚠ 오늘 세운 규칙 「막혔다고 말하기 전에 `blocked_kind` 를 볼 것」의 **뒤집힌 얼굴**이다.
보는 쪽을 그렇게 고쳤으면 **다는 쪽도 빠짐없이 달아야** 한다. 안 그러면 받는 쪽이
「코드도 없고 막힘 표시도 없는 **멀쩡한 줄**」로 읽어 **금액이 조용히 빠진다.**
실제로 그랬다 — `측구터파기 · 굴삭기+브레카` 가 `work_item_code: null` · `blocked_kind: null`
이었고 화면에 아무 말도 안 떴다. 인계본의 `bill_flag_warnings`·`unmatched_work_items` 에는
**이미 실려 있었다** — 목록은 있는데 **줄에 표시가 없었던 것**이다. 줄 단위로 보는 쪽은
목록을 안 본다.
⚠ 예외 둘 — 이 둘만 코드 없이 서도 된다.
① **묶음 줄**(`composite_parts`) — 무엇으로 묶이는지 조각에 적혀 있다
② **내역에 안 서는 줄**(`in_bill: False`) — 합계·무대처럼 **막힌 게 아니라 안 세는 것**.
다만 그쪽도 **까닭은 있어야** 한다(`in_bill_reason` 또는 `blocked_kind`).
"""
from __future__ import annotations
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from B08_Quantity.B08_Quantity_Engine_Handoff import build_handoff # noqa: E402
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import build_table # noqa: E402
def 집계(item: str | None, group: str = "흙깎기") -> dict:
return {"rows": [{"group": group, "item": item, "unit": "㎥", "amount": 100.0}]}
def 인계(**kwargs) -> list[dict]:
return build_handoff(**kwargs)["work_items"]
def test_시공법을_못_고른_줄에_막힘_표시가_붙는다() -> None:
"""⚠ 실측 자리 — 「굴삭기+브레카」는 지반 갈래가 아니라 **시공법**이라 매핑이 안 맞았다."""
rows = 인계(summary_table=집계("리핑암"))
row = next(r for r in rows if r["name"] == "흙깎기")
assert row["work_item_code"] is None
assert row["blocked_kind"] == "input_missing" # 시공법을 고르면 풀린다
assert "시공법" in row["blocked_reason"]
def test_시공법을_고르면_코드가_붙고_표시가_사라진다() -> None:
# ⚠ 시공법 값은 **영문 키**다(`ripping`·`blasting`) — 한글을 넣으면 안 풀린다.
rows = 인계(summary_table=집계("리핑암"), ground_methods={"리핑암": "ripping"})
row = next(r for r in rows if r["name"] == "흙깎기")
assert row["work_item_code"] is not None
assert row["blocked_kind"] is None
def test_품셈을_못_이은_줄은_다른_갈래로_적힌다() -> None:
"""⚠ 「사용자가 고르면 풀림」과 「우리가 매핑을 못 이음」은 **다음에 할 일이 다르다**."""
rows = 인계(summary_table=집계(None, group="지장목제거"))
row = next(r for r in rows if r["name"] == "지장목제거")
assert row["work_item_code"] is None
assert row["blocked_kind"] == "unit_data_missing"
assert "못 이었습니다" in row["blocked_reason"]
def test_불변식_모든_줄에_대해_선다() -> None:
"""⚠ 한 자리를 고치는 것으로 끝내지 않는다 — **줄 빌더 여덟 곳 전부**를 건다."""
unit = build_table(
[
{
"structure_id": "s1",
"type_id": "masonry_wet",
"start_m": 0.0,
"end_m": 10.0,
"options": {"height_m": 2.5, "length_m": 10.0, "back_len_cm": 45},
},
{
"structure_id": "w1",
"type_id": "retaining_wall",
"start_m": 20.0,
"end_m": 30.0,
"options": {"height_m": 2.0, "length_m": 10.0, "form": "반중력식"},
},
]
)
haul = {
"rows": [
{"equipment": "dump_truck", "ground": "토사", "volume_m3": 50.0},
{"equipment": "free_haul", "ground": "토사", "volume_m3": 10.0},
{"equipment": "모르는장비", "ground": "토사", "volume_m3": 5.0},
]
}
rows = 인계(
summary_table=집계("리핑암"),
haul_table=haul,
unit_quantity_table=unit,
preparation_table={"rows": []},
)
나쁜 = [
r["name"]
for r in rows
if not r.get("work_item_code")
and not r.get("composite_parts")
and r.get("in_bill")
and not r.get("blocked_kind")
]
assert 나쁜 == [], f"코드도 막힘 표시도 없이 내역에 서는 줄: {나쁜}"
# 내역에 안 서는 줄도 **까닭 없이** 있으면 안 된다.
까닭없음 = [
r["name"]
for r in rows
if not r.get("in_bill")
and not r.get("blocked_kind")
and not str(r.get("in_bill_reason") or "").strip()
]
assert 까닭없음 == [], f"안 서는 까닭이 안 적힌 줄: {까닭없음}"
def test_무대는_막힌_것이_아니다() -> None:
"""⚠ 무대(소운반 20m 이내)는 **품에 포함**이라 안 세우는 것이지 막힌 것이 아니다.
여기에 막힘 표시를 달면 「만들어야 할 것」 목록에 올라 없는 일이 생긴다.
"""
rows = 인계(haul_table={"rows": [{"equipment": "free_haul", "ground": "토사", "volume_m3": 10.0}]})
row = next(r for r in rows if r["haul_equipment"] == "free_haul")
assert row["in_bill"] is False
assert row["blocked_kind"] is None
assert "품에 포함" in row["in_bill_reason"]
def test_물량_0_인_구조물은_내역에_안_선다() -> None:
"""⚠ 물넘이포장이 면적을 안 받아 **`0.0 ㎡` 로 내역에 서고 있었다**(2026-09-09 감사).
코드가 붙어 있어 **0 원 줄**이 만들어지고 화면에는 「값이 있는 줄」로 보인다 —
0 은 「없음」과 구별이 안 된다(오늘 표토제거에서 겪은 자리와 같은 결).
줄은 그대로 넘기되 **내역에서 빼고 까닭을 적는다.**
"""
unit = build_table(
[
{
"structure_id": "f1",
"type_id": "ford_pavement",
"start_m": 0.0,
"end_m": 10.0,
"options": {"thickness_cm": 20, "length_m": 10.0},
}
]
)
row = next(r for r in 인계(unit_quantity_table=unit) if r["name"] == "물넘이포장")
assert row["quantity"] == 0.0
assert row["in_bill"] is False
assert "물량이 0" in row["in_bill_reason"]
assert row["blocked_kind"] == "input_missing"