From aae43f4f99be8737dc962d2b98eac628e5ee1b4e Mon Sep 17 00:00:00 2001 From: DongHyeonka Date: Thu, 10 Sep 2026 19:19:01 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EC=9A=B0=EC=97=B0=ED=9E=88=20=EC=A7=80?= =?UTF-8?q?=EC=BC=9C=EC=A7=80=EB=8D=98=20=EB=A9=B1=EB=93=B1=EC=84=B1?= =?UTF-8?q?=EC=9D=84=20=ED=9A=8C=EA=B7=80=EB=A1=9C=20=EB=AA=BB=EB=B0=95?= =?UTF-8?q?=EA=B3=A0,=20=ED=8B=80=EB=A6=B0=20=EB=AC=B8=EC=9E=A5=EC=9D=98?= =?UTF-8?q?=20=EC=B6=9C=EC=B2=98=EB=A5=BC=20=EA=B3=A0=EC=B9=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 저장 멱등 키가 필드 순서에 안 흔들리는 것은 sort_keys=True 덕이고, 그것은 JSON 을 안정적으로 만들려고 넣은 것이지 멱등성을 노린 것이 아니다. 다시 직렬화한 재시도가 새 저장으로 잡히지 않게 지키고 있었는데 그 성질을 명시한 시험이 없었다. sort_keys 를 빼도 아무 시험이 안 깨졌다. 의도하지 않은 성질에 기대는 코드는 회귀로 못박지 않으면 다음 사람이 지운다. 대조군도 함께 넣었다. 순서에 안 흔들린다고 내용에도 안 흔들리면 그건 다른 결함이다. 그리고 「Decision 은 계약에 삭제 경로가 없다」의 출처가 스킬이었다. 내가 코드 주석과 보고서로 옮긴 문장이 거기서 왔다. 다섯 종류 전부의 삭제 경로를 세어 표로 바꾸고 확인한 리비전(tech-log-backend @ a000f87)을 함께 적었다 — 서버를 서술할 때 리비전이 없으면 서버가 바뀌어도 아무도 모른다. 지워지지 않는 것은 따로 있다. 게시한 적이 있으면 DOCUMENT_PUBLISHED, 관계로 참조되면 DOCUMENT_IN_USE 다. 그것이 원래 말하려던 것이다. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk --- .../publishing-tech-log-to-studio/SKILL.md | 22 ++++++++++-- scripts/tests/test_studio_save.py | 34 +++++++++++++++++++ 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/.agents/skills/publishing-tech-log-to-studio/SKILL.md b/.agents/skills/publishing-tech-log-to-studio/SKILL.md index bedbdf6..7774ebb 100644 --- a/.agents/skills/publishing-tech-log-to-studio/SKILL.md +++ b/.agents/skills/publishing-tech-log-to-studio/SKILL.md @@ -128,9 +128,25 @@ python3 scripts/verify-tech-log-tree.py <프로젝트> ## 시험용 초안을 남기지 않는다 -확인하려고 만든 작업본은 지운다. 다만 **Decision 은 계약에 삭제 경로가 없다** — 확인용으로 -만들지 않는 편이 낫다. 관계로 참조된 문서도 지워지지 않는다(`DOCUMENT_IN_USE`). 참조하는 쪽의 -관계를 먼저 끊는다. +확인하려고 만든 작업본은 지운다. **다섯 종류 전부 삭제 경로가 있다** — +`tech-log-backend` @ `a000f87` 의 `ManagementDocumentController` 에서 센 것이다. + +| 종류 | 경로 | +|---|---| +| Case | `DELETE /v1/studio/cases/{id}` (`:72`) | +| Reference | `DELETE /v1/studio/references/{id}` (`:81`) | +| Concept | `DELETE /v1/studio/concepts/{id}` (`:95`) | +| Question | `DELETE /v1/studio/questions/{id}` (`:114`) | +| Decision | `DELETE /v1/studio/projects/{id}/decisions/{decisionId}` (`:123`) | + +**전에 이 자리에 「Decision 은 계약에 삭제 경로가 없다」고 적혀 있었고 그것은 틀렸다.** +확인 없이 적힌 문장이 옮겨 다녔다 — 이 배치에서 그 문장을 코드 주석과 보고서로 다시 옮긴 +일이 있었다. **서버를 서술할 때는 확인한 리비전을 함께 적는다.** 리비전이 없으면 서버가 +바뀌어도 아무도 모른다. + +**지워지지 않는 것은 따로 있다.** 게시한 적이 있으면 `DOCUMENT_PUBLISHED` 로 409 가 나고 +(「공개된 기록은 삭제할 수 없습니다」), 관계로 참조된 문서는 `DOCUMENT_IN_USE` 로 막힌다 +(「이 기록을 참조하는 곳이 있어 삭제할 수 없습니다」). 참조하는 쪽의 관계를 먼저 끊는다. ## 실패했을 때 diff --git a/scripts/tests/test_studio_save.py b/scripts/tests/test_studio_save.py index 0ed5f51..30f0b52 100644 --- a/scripts/tests/test_studio_save.py +++ b/scripts/tests/test_studio_save.py @@ -533,3 +533,37 @@ class HarnessTestPlanTest(unittest.TestCase): for s in steps: self.assertNotIn("/publish", s["path"]) self.assertNotIn("/unpublish", s["path"]) + + +class KeyStabilityTest(unittest.TestCase): + """저장 멱등 키가 **필드 순서에 안 흔들린다.** + + `idempotency_key` 가 `json.dumps(..., sort_keys=True)` 로 본문을 직렬화한다. + **그 효과를 노리고 넣은 것이 아니라** JSON 을 안정적으로 만들려고 넣은 것이 멱등성을 + 지켰다 — 다시 직렬화한 재시도(같은 내용, 다른 필드 순서)가 새 저장으로 잡히지 않는다. + + **의도하지 않은 성질에 기대는 코드는 회귀로 못박지 않으면 다음 사람이 지운다.** + `sort_keys` 를 빼면 이 시험이 깨진다. + """ + + REL = "docs/p/tech-log-studio/t/case/x.md" + + def test_field_order_does_not_change_the_save_key(self): + one = {"expectedVersion": 3, + "document": {"kind": "CASE", "title": "가", "slug": "x"}} + two = {"document": {"slug": "x", "title": "가", "kind": "CASE"}, + "expectedVersion": 3} + self.assertEqual(ss.idempotency_key("save", self.REL, one), + ss.idempotency_key("save", self.REL, two)) + + def test_content_still_changes_the_save_key(self): + """대조군. 순서에 안 흔들린다고 내용에도 안 흔들리면 그건 다른 결함이다.""" + one = {"expectedVersion": 3, "document": {"title": "가"}} + two = {"expectedVersion": 3, "document": {"title": "나"}} + self.assertNotEqual(ss.idempotency_key("save", self.REL, one), + ss.idempotency_key("save", self.REL, two)) + + def test_the_create_key_does_not_depend_on_a_payload(self): + """만들기 키는 경로만으로 만든다. 재시도가 문서를 둘 만들지 않는다.""" + self.assertEqual(ss.idempotency_key("create", self.REL), + ss.idempotency_key("create", self.REL))