diff --git a/A00_Common/design/design_pipeline_page.ts b/A00_Common/design/design_pipeline_page.ts index cf56a56db..5aacf0107 100644 --- a/A00_Common/design/design_pipeline_page.ts +++ b/A00_Common/design/design_pipeline_page.ts @@ -375,8 +375,10 @@ function createPagePipeline( const finishDrop = async (record: PipelineRunRecord): Promise => { // 뒤 차례(B06 cross 등)에 밀림 · 초안은 그대로 버려짐 — 마지막 차례 끝에서 되돌림(B05 와 같게) const pushed = record.outcome === "dropped" && record.generation !== pipe.generation; - if (pushed && dropPending && !pipe.input("route")) - return (lastRun as Promise).then(finishDrop); + if (pushed && dropPending && !pipe.input("route")) { + const next = await (lastRun as Promise); + if (next !== record) return finishDrop(next); // 같은 기록 = 끝난 차례 · 되받으면 끝없는 고리(298-3) + } dropPending = false; const routeId = pageSaved.canonRouteId(); const stations = pipe.output("ground")?.stations ?? []; diff --git a/resources/tester/helper_298_3_finishdrop_loop.cjs b/resources/tester/helper_298_3_finishdrop_loop.cjs new file mode 100644 index 000000000..85348a63c --- /dev/null +++ b/resources/tester/helper_298_3_finishdrop_loop.cjs @@ -0,0 +1,63 @@ +/* 298-3 노선 [저장] 뒤 렌더러 멈춤 단위검증 헬퍼 — `design_pipeline_page.ts` 의 `finishDrop` 본문을 + * 그대로 떼어 트랜스파일 · 둘레 칸은 가짜. 밀린 버림 기록(dropped · 옛 차례)을 넘기고 lastRun 이 같은 기록을 + * 되돌려 줄 때 finishDrop 이 몇 번 불리는지 · 끝나는지. 결과 JSON 을 stdout 으로. + */ +const fs = require("fs"); +const path = require("path"); +const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); + +const ROOT = path.join(__dirname, "..", ".."); +const code = fs.readFileSync( + path.join(ROOT, "A00_Common", "design", "design_pipeline_page.ts"), + "utf8", +); +const begin = code.indexOf("const finishDrop = async"); +const end = code.indexOf("\n };", begin) + 5; +if (begin < 0 || end < begin) throw new Error("finishDrop 못 찾음"); +const body = code + .slice(begin, end) + .replace("const finishDrop = async", "const finishDropInner = async"); + +const harness = ` +let calls = 0; +const finishDrop = (record: any): Promise => { + calls += 1; + if (calls > 50) return Promise.reject(new Error("loop")); + return finishDropInner(record); +}; +${body} +return { finishDrop, calls: () => calls }; +`; +const js = ts + .transpileModule(`(function () {${harness}})`, { + compilerOptions: { target: ts.ScriptTarget.ES2022 }, + }) + .outputText.replace(/^"use strict";/, ""); + +const record = { outcome: "dropped", generation: 3 }; +const pipe = { generation: 5, input: () => null, output: () => null }; +let released = 0; +const scope = { + pipe, + dropPending: true, + lastRun: Promise.resolve(record), // 이미 끝난 같은 차례 기록 + pageSaved: { canonRouteId: () => 7, adoptCanon() {} }, + held: null, + options: {}, + heldSectionDetail: () => null, + stationDiff: () => null, + adoptAfterDrop() {}, + publisher: { shown: async () => undefined }, + releaseWhenQuiet: async () => void (released += 1), +}; +const names = Object.keys(scope); +const make = new Function( + "scope", + `let { ${names.join(", ")} } = scope;\nreturn ${js.trim().replace(/;$/, "")}();`, +); +const { finishDrop, calls } = make(scope); + +finishDrop(record).then( + (why) => console.log(JSON.stringify({ calls: calls(), why, released, record })), + (error) => console.log(JSON.stringify({ calls: calls(), error: String(error), released })), +); diff --git a/resources/tester/test_298_3_finishdrop_loop.py b/resources/tester/test_298_3_finishdrop_loop.py new file mode 100644 index 000000000..2e7981dce --- /dev/null +++ b/resources/tester/test_298_3_finishdrop_loop.py @@ -0,0 +1,25 @@ +"""298-3 노선 [저장] 뒤 렌더러 멈춤 — 밀린 버림 기록을 lastRun 이 같은 기록으로 되돌려 줘도 finishDrop 은 한 번. + +./venv/Scripts/python.exe -m pytest resources/tester/test_298_3_finishdrop_loop.py -q +""" + +from __future__ import annotations + +import json +import subprocess +from pathlib import Path + +HELPER = Path(__file__).resolve().parent / "helper_298_3_finishdrop_loop.cjs" + + +def test_같은_기록_되받기_고리_없음() -> None: + run = subprocess.run( # noqa: S603 — 고정 실행 파일 + ["node", str(HELPER)], capture_output=True, text=True, encoding="utf-8", timeout=30 + ) + assert run.returncode == 0, run.stderr + out = json.loads(run.stdout) + assert "error" not in out, out + assert out["calls"] == 1 + assert out["why"] == "dropped" # 밀린 기록 그대로 · 되돌림 없음 + assert out["released"] == 1 + assert out["record"]["restored"] is False