fix(run-ledger): 파일은 안 깨지는데 두 세션의 기록이 섞였다

이어받은 단계에 앞 세션이 관문을 써도 그대로 들어갔다. 원자성 문제가 아니다 — 파일은
온전한 채로 두 세션의 기록이 섞인다. 그리고 관문마다 누가 적었는지가 없어서 나중에
원장을 읽어도 가릴 수 없었다.

begin 이 세대를 올리고 주인을 적는다. 낮은 세대나 다른 주인의 쓰기는 거절한다.
bin/task.py 의 attempt 와 같은 자리다.

gate 와 end 도 --session 을 받아 적는다. 관문마다 session·generation·at 이 남고
end 는 finishedBy 를 남긴다.

주인이 없는 단계는 그대로 받는다. 세션을 안 쓰는 단일 세션 사용이 깨지지 않는다.
회귀에 그 대조를 넣었다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
This commit is contained in:
DongHyeonka
2026-09-10 13:33:10 +09:00
co-authored by Claude Opus 5
parent 8ca822dc4d
commit 96d7fbc57a
2 changed files with 87 additions and 6 deletions
+44
View File
@@ -140,6 +140,50 @@ class LedgerTest(unittest.TestCase):
_cli("rider", self.led, "--id", "R12", "--why", "x", "--seconds", "60")
self.assertEqual("S3", self._read()["riders"][0]["duringStage"])
# ── 두 세션의 기록이 섞이지 않는다 ────────────────────────────────
def test_a_superseded_session_cannot_write(self):
"""파일이 안 깨지는 것과 두 세션의 기록이 안 섞이는 것은 다른 일이다.
`plan/02` B-005 — 「이전 작업자의 종료를 확인하고 재배정한 시도 번호와 맞는
결과만 받는다.」 `bin/task.py` 의 attempt 와 같은 자리다.
"""
_cli("begin", self.led, "--stage", "S3", "--session", "sess-1")
_cli("begin", self.led, "--stage", "S3", "--session", "sess-2")
for args in (("gate", self.led, "--stage", "S3", "--cmd", "stale", "--exit", "0"),
("end", self.led, "--stage", "S3", "--status", "DONE", "--echo", "x")):
with self.subTest(cmd=args[0]):
p = _cli(*args, "--session", "sess-1")
self.assertEqual(2, p.returncode)
self.assertIn("sess-2", p.stderr)
def test_a_stale_generation_is_refused(self):
_cli("begin", self.led, "--stage", "S3", "--session", "sess-1")
_cli("begin", self.led, "--stage", "S3", "--session", "sess-2")
p = _cli("gate", self.led, "--stage", "S3", "--cmd", "x", "--exit", "0",
"--session", "sess-2", "--generation", "1")
self.assertEqual(2, p.returncode)
def test_an_anonymous_write_to_an_owned_stage_is_refused(self):
"""누가 썼는지 안 밝히면 나중에 원장을 읽을 수 없다."""
_cli("begin", self.led, "--stage", "S3", "--session", "sess-1")
p = _cli("gate", self.led, "--stage", "S3", "--cmd", "x", "--exit", "0")
self.assertEqual(2, p.returncode)
def test_the_current_owner_writes_and_is_recorded(self):
"""대조군. 지금 주인의 쓰기는 들어가고, 누가 어느 세대에 썼는지 남는다."""
_cli("begin", self.led, "--stage", "S3", "--session", "sess-1")
p = _cli("gate", self.led, "--stage", "S3", "--cmd", "ok", "--exit", "0",
"--session", "sess-1", "--generation", "1")
self.assertEqual(0, p.returncode, p.stderr)
gate = self._stage("S3")["gates"][0]
self.assertEqual(("sess-1", 1), (gate["session"], gate["generation"]))
def test_an_unowned_stage_still_accepts_writes(self):
"""주인이 없으면 (세션을 안 쓰는 단일 세션 사용) 그대로 쓴다. 기존 쓰임을 안 깬다."""
_cli("begin", self.led, "--stage", "S3")
self.assertEqual(0, _cli("gate", self.led, "--stage", "S3",
"--cmd", "x", "--exit", "0").returncode)
# ── 옛 원장을 깨지 않는다 ─────────────────────────────────────────
def test_the_added_fields_do_not_break_the_verifier(self):
"""이 도구가 더한 칸이 있어도 검사기가 그대로 읽어야 한다."""