Merge remote-tracking branch 'origin/dev' into sub_laptop_8
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
import asyncio
|
||||
|
||||
from B01_Dashboard import B01_Dashboard_Knowledge_Chat as chat
|
||||
|
||||
|
||||
def test_chat_searches_all_knowledge_markdown_except_excluded_folders(monkeypatch, tmp_path):
|
||||
public = tmp_path / "original" / "법률"
|
||||
guide = tmp_path / "technical_info"
|
||||
private = tmp_path / "original" / "실무문서"
|
||||
stmate = tmp_path / "original" / "원가계산" / "STmate"
|
||||
dasan = tmp_path / "original" / "원가계산" / "다산소프트"
|
||||
public.mkdir(parents=True)
|
||||
guide.mkdir()
|
||||
private.mkdir()
|
||||
stmate.mkdir(parents=True)
|
||||
dasan.mkdir()
|
||||
(public / "현행.md").write_text("# 임도 기준\n" + "임도 설치 기준 내용 " * 10, encoding="utf-8")
|
||||
(guide / "_검토.md").write_text("# 임도 기준\n기술정보 안내 " * 10, encoding="utf-8")
|
||||
(private / "사례.md").write_text("# 임도 기준\n비공개 사례 " * 10, encoding="utf-8")
|
||||
(stmate / "자료.md").write_text("# 임도 기준\nSTmate 자료 " * 10, encoding="utf-8")
|
||||
(dasan / "자료.md").write_text("# 임도 기준\n다산소프트 자료 " * 10, encoding="utf-8")
|
||||
monkeypatch.setattr(chat, "KNOWLEDGE", tmp_path)
|
||||
monkeypatch.setenv("GEMINI_API_KEY", "test-key")
|
||||
chat._sections.cache_clear()
|
||||
sent = {}
|
||||
|
||||
class Response:
|
||||
def raise_for_status(self):
|
||||
pass
|
||||
|
||||
def json(self):
|
||||
return {"candidates": [{"content": {"parts": [{"text": "설명 [1]"}]}}]}
|
||||
|
||||
class Client:
|
||||
def __init__(self, timeout):
|
||||
pass
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *_args):
|
||||
pass
|
||||
|
||||
async def post(self, url, *, headers, json):
|
||||
sent["prompt"] = json["contents"][0]["parts"][0]["text"]
|
||||
return Response()
|
||||
|
||||
monkeypatch.setattr(chat.httpx, "AsyncClient", Client)
|
||||
result = asyncio.run(chat.ask_knowledge_chat(chat.ChatRequest(question="임도 기준"), {}))
|
||||
assert result["answer"] == "설명 [1]"
|
||||
paths = {source["path"] for source in result["sources"]}
|
||||
assert paths == {"original/법률/현행.md", "technical_info/_검토.md"}
|
||||
assert "실무문서" not in sent["prompt"]
|
||||
assert "비공개 사례" not in sent["prompt"]
|
||||
assert "STmate" not in sent["prompt"]
|
||||
assert "다산소프트" not in sent["prompt"]
|
||||
chat._sections.cache_clear()
|
||||
@@ -0,0 +1,98 @@
|
||||
"""횡단 벽 형상 한 벌 — 관 유입·유출 흙막이 · 추가 흙막이 · 사면안정 흙막이 · 옹벽.
|
||||
|
||||
규칙(10-6 사용자):
|
||||
① 네 벽 모두 공용 `buildRevetWallGeometry` 로 선다 — 형태는 안 무늬만 바꿈(외곽 같음).
|
||||
② 형태별 높이 한도(메 2.0 · 찰 3.0 · 콘크리트 5.0) · 원지반 맞춤 자동 자르기 없음.
|
||||
③ 벽 윗면은 노면 위로 올라오지 않음(넘으면 노면에서 멈춤).
|
||||
|
||||
서버 면적은 같은 TS 를 Node 로 돌리므로(`B06_Section_Server_Calc_Node.ts`) 이 함수가 곧 서버 기하.
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
B06 = PROJECT_ROOT / "B06_Section"
|
||||
TSC = PROJECT_ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc"
|
||||
|
||||
_RUNNER = """
|
||||
import { writeFileSync } from "node:fs";
|
||||
import { buildRevetWallGeometry } from "./B06_Section_UI_Cross_Wall.js";
|
||||
import { revetWallSpec, REVET_FORMS } from "./B06_Section_UI_Cross_Culvert_Const.js";
|
||||
const anchor = { offset: 5, elevation: 100 };
|
||||
const outline = (form, role, extra = {}) =>
|
||||
buildRevetWallGeometry({ anchor, outward: 1, height: 2.5, material: "dry", role, form, ...extra })
|
||||
.points.map((p) => [p.offset, p.elevation]);
|
||||
writeFileSync(process.argv[2], JSON.stringify({
|
||||
forms: REVET_FORMS.map((form) => outline(form, "outlet")),
|
||||
roles: ["inlet", "outlet", "extra"].map((role) => outline("돌쌓기(메)", role)),
|
||||
capped: buildRevetWallGeometry({
|
||||
anchor, outward: 1, height: 5, material: "dry", role: "outlet", roadElevation: 103,
|
||||
}).topJoint.elevation,
|
||||
under: buildRevetWallGeometry({
|
||||
anchor, outward: 1, height: 2, material: "dry", role: "outlet", roadElevation: 103,
|
||||
}).topJoint.elevation,
|
||||
dry: revetWallSpec({ revet_form: "돌쌓기(메)" }, { h: 3.0 }, true, 0.3).pureHeight,
|
||||
concrete: revetWallSpec({ revet_form: "콘크리트" }, { h: 7.5 }, true, 0.3).pureHeight,
|
||||
}));
|
||||
"""
|
||||
|
||||
|
||||
@pytest.mark.skipif(not TSC.is_file(), reason="프론트엔드 의존성 미설치")
|
||||
def test_벽_외곽_한_벌_높이_입력_그대로_노면_멈춤(tmp_path: Path) -> None:
|
||||
out = tmp_path / "js"
|
||||
# 형(type) import 가 끌어오는 페이지 파일은 별칭을 못 풀어 오류를 내지만 산출물은 나옴.
|
||||
subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
[
|
||||
"node",
|
||||
str(TSC),
|
||||
str(B06 / "B06_Section_UI_Cross_Wall.ts"),
|
||||
"--outDir",
|
||||
str(out),
|
||||
"--module",
|
||||
"esnext",
|
||||
"--target",
|
||||
"es2022",
|
||||
"--moduleResolution",
|
||||
"bundler",
|
||||
"--skipLibCheck",
|
||||
"--ignoreConfig",
|
||||
],
|
||||
cwd=str(PROJECT_ROOT),
|
||||
capture_output=True,
|
||||
)
|
||||
for emitted in out.rglob("*.js"):
|
||||
emitted.write_text(
|
||||
re.sub(r'(from\s+"\./[^"]+)"', r'\1.js"', emitted.read_text(encoding="utf-8")),
|
||||
encoding="utf-8",
|
||||
)
|
||||
base = next(path.parent for path in out.rglob("B06_Section_UI_Cross_Wall.js"))
|
||||
(base / "runner.mjs").write_text(_RUNNER, encoding="utf-8")
|
||||
result = tmp_path / "out.json"
|
||||
subprocess.run( # noqa: S603
|
||||
["node", str(base / "runner.mjs"), str(result)], check=True, capture_output=True
|
||||
)
|
||||
got = json.loads(result.read_text(encoding="utf-8"))
|
||||
|
||||
assert all(outline == got["forms"][0] for outline in got["forms"]), "형태마다 외곽이 다름"
|
||||
assert all(outline == got["roles"][0] for outline in got["roles"]), "벽 갈래마다 외곽이 다름"
|
||||
assert got["capped"] == pytest.approx(103.0), "노면 위로 올라옴"
|
||||
assert got["under"] == pytest.approx(102.0), "노면 아래 벽이 잘림"
|
||||
assert got["dry"] == pytest.approx(3.0), "돌쌓기(메) 3.0 이 잘림"
|
||||
assert got["concrete"] == pytest.approx(7.5), "콘크리트 7.5 가 잘림"
|
||||
|
||||
|
||||
def test_네_벽이_공용_함수로_서고_형태_한도가_없다() -> None:
|
||||
for name in ("Culvert_Geom", "Culvert_Extra", "Revetment"):
|
||||
source = (B06 / f"B06_Section_UI_Cross_{name}.ts").read_text(encoding="utf-8")
|
||||
assert "buildRevetWallGeometry(" in source, f"{name} 가 공용 벽 함수를 안 씀"
|
||||
for path in B06.glob("*.ts"):
|
||||
source = path.read_text(encoding="utf-8")
|
||||
assert "revetHeightLimit" not in source, f"{path.name} 에 형태 높이 한도가 남음"
|
||||
assert "materialLimit" not in source, f"{path.name} 에 재질 높이 한도가 남음"
|
||||
extra = (B06 / "B06_Section_UI_Cross_Culvert_Extra.ts").read_text(encoding="utf-8")
|
||||
assert "fitted" not in extra, "추가 흙막이가 원지반에 맞춰 늘어남"
|
||||
@@ -220,8 +220,12 @@ def test_저장된_표준단면이_브라우저까지_간다():
|
||||
# 값으로 재는 확인은 화면 실측에 있음(보조 창 b269ea34 81.77m — 단 0 → 1 에서 성토
|
||||
# 10.9027 → 10.6743㎡). 계획서에 그 수치가 남아 있다.
|
||||
def test_다단_성토부선이_면적_트림에_들어간다():
|
||||
source = (PROJECT_ROOT / "B06_Section" / "B06_Section_UI_Cross_Culvert_Geom.ts").read_text(
|
||||
encoding="utf-8"
|
||||
# 다단 · 묻힌 구간 거르기는 `_Culvert_Tiers` 로 나뉘어 있어 두 파일을 함께 읽음.
|
||||
source = "".join(
|
||||
(PROJECT_ROOT / "B06_Section" / f"B06_Section_UI_Cross_Culvert_{name}.ts").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
for name in ("Geom", "Tiers")
|
||||
)
|
||||
assert "const extendTrimSlope = (" in source, "트림 확장 자리가 사라짐"
|
||||
# 유출측·집수정 계류측 **둘 다** — 한쪽만 넣으면 그쪽 단만 물량에 잡힌다.
|
||||
|
||||
Reference in New Issue
Block a user