Files
Aislo/resources/tester/test_b06_culvert_edit_queue.py
T
eomsangdonandClaude Opus 5 11161659f5 feat(b06): 종단 그래프 우클릭으로 구조물·계곡 통과 시설을 넣고 빼게 함
B05 와 B06 은 한 페이지인데 구조물을 넣고 빼는 자리가 B05 뿐이라 화면을 오가야 했음.
종단 그래프 렌더러·알약 레인·좌측 배치 폼은 이미 공용이었으므로, 빠져 있던 조작층만
B05 와 같은 부품(mountStructureMenu)으로 B06 에도 붙임.

관 추가·삭제는 이동과 같은 예약 경로를 씀 — 세션에 쌓고 [저장]·[확정]에서 정본
(pipe_points.json)으로 나가며, 세부 배수유역 재분할은 그때 서버가 처리함. 넣었다 바로
빼거나 뺐다 다시 넣으면 예약만 걷어 내 정본이 흔들리지 않음.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fe1QWPTfw11PaKh2LjwXdR
2026-09-12 15:50:02 +09:00

136 lines
5.0 KiB
Python

"""B06 종단에서 넣고 뺀 **계곡 통과 시설**이 세션에 제대로 쌓이는지(2026-09-12 사용자 지시).
B05·B06 은 한 페이지이므로 관 추가·삭제도 양쪽에서 되어야 한다. 다만 정본에 바로 쓰지
않고 **세션에 예약**했다가 [저장]·[확정]에서 한 번에 내보낸다(CLAUDE.md 5장). 넣었다 바로
빼면 예약만 걷어 내야 하고, 뺐다 다시 넣어도 마찬가지다 — 그러지 않으면 저장 때 있지도
않은 관을 지우거나 같은 자리에 둘이 선다.
실제 화면 코드를 그대로 컴파일해 Node 로 돌린다(다단 하한 시험과 같은 방식).
"""
import json
import subprocess
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
TSC = PROJECT_ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc"
MODULE = PROJECT_ROOT / "B06_Section" / "B06_Section_Api_Culvert_Options.ts"
_RUNNER = """
const { writeFileSync } = require("node:fs");
const Module = require("node:module");
// 화면 코드가 번들러 별칭(`@config/…`)을 쓴다 — Node 에는 없으니 빈 껍데기로 돌려준다.
// 이 시험이 보는 것은 세션 예약 로직뿐이라 값은 안 쓴다.
const load = Module._load;
Module._load = function (request, parent, isMain) {
if (request.startsWith("@config/") || request.startsWith("@util/") || request.startsWith("@ui/"))
return new Proxy({}, { get: () => () => undefined });
return load.call(this, request, parent, isMain);
};
// 세션 저장소 흉내 — 화면 코드는 window.sessionStorage 만 쓴다.
const store = new Map();
global.window = {
sessionStorage: {
getItem: (key) => (store.has(key) ? store.get(key) : null),
setItem: (key, value) => store.set(key, String(value)),
removeItem: (key) => store.delete(key),
},
};
const { createCulvertOptionWriter } = require(process.argv[3]);
const EDIT_KEY = "edit";
const writer = createCulvertOptionWriter(
() => "p1",
() => "opt",
undefined,
() => "move",
() => EDIT_KEY,
);
const read = () => JSON.parse(store.get(EDIT_KEY) ?? '{"adds":[],"removes":[]}');
const steps = {};
writer.queueAdd(120, "pipe");
writer.queueAdd(140);
steps.afterTwoAdds = read();
// 방금 넣은 자리를 다시 빼면 **추가 예약만** 사라진다(정본에는 아직 없다).
writer.queueRemove(140);
steps.afterUndoAdd = read();
// 정본에 있던 관을 빼면 삭제 예약으로 남는다.
writer.queueRemove(200);
steps.afterRemove = read();
// 뺀 자리를 다시 넣으면 삭제 예약만 걷어 낸다(새로 만들지 않는다).
writer.queueAdd(200);
steps.afterUndoRemove = read();
// 같은 자리를 두 번 넣어도 하나만 남는다.
writer.queueAdd(120);
steps.afterDuplicateAdd = read();
writeFileSync(process.argv[2], JSON.stringify(steps));
"""
def _run(tmp_path: Path) -> dict:
out = tmp_path / "out"
subprocess.run( # noqa: S603 — 고정 실행 파일
[
"node",
str(TSC),
"--ignoreConfig",
"--target",
"es2022",
"--module",
"commonjs",
"--skipLibCheck",
"--outDir",
str(out),
str(MODULE),
],
cwd=str(PROJECT_ROOT),
check=False,
capture_output=True,
)
compiled = next(out.rglob("B06_Section_Api_Culvert_Options.js"), None)
assert compiled is not None, "화면 코드가 JS 로 안 나옴 — tsc 실패"
(out / "runner.cjs").write_text(_RUNNER, encoding="utf-8")
result = tmp_path / "result.json"
subprocess.run( # noqa: S603 — 고정 실행 파일
["node", str(out / "runner.cjs"), str(result), str(compiled)],
cwd=str(PROJECT_ROOT),
check=True,
capture_output=True,
)
return json.loads(result.read_text(encoding="utf-8"))
def test_추가와_삭제_예약이_서로를_걷어_낸다(tmp_path: Path) -> None:
steps = _run(tmp_path)
# 넣은 둘이 그대로 쌓인다.
assert [entry["chainage_m"] for entry in steps["afterTwoAdds"]["adds"]] == [120.0, 140.0]
assert steps["afterTwoAdds"]["adds"][0]["facility"] == "pipe"
assert steps["afterTwoAdds"]["removes"] == []
# 방금 넣은 자리를 빼면 추가 예약만 사라지고 삭제 예약은 안 생긴다.
assert [entry["chainage_m"] for entry in steps["afterUndoAdd"]["adds"]] == [120.0]
assert steps["afterUndoAdd"]["removes"] == []
# 정본에 있던 관은 삭제 예약으로 남는다.
assert steps["afterRemove"]["removes"] == ["200.00"]
# 뺀 자리를 다시 넣으면 삭제 예약만 걷힌다 — 새 관을 만들지 않는다.
assert steps["afterUndoRemove"]["removes"] == []
assert [entry["chainage_m"] for entry in steps["afterUndoRemove"]["adds"]] == [120.0]
# 같은 자리를 두 번 넣어도 하나만 남는다.
assert [entry["chainage_m"] for entry in steps["afterDuplicateAdd"]["adds"]] == [120.0]