fix(studio-save): 정리 단계가 본문 없이 나가고 종류를 안 봤다

DELETE 가 본문을 요구한다. requestBody: required: true 이고 컨트롤러가 @RequestBody
ExpectedVersionRequest 를 받는다. 본문 없이 보내면 400 이고 초안이 남는다. expectedVersion
은 verify 가 읽은 값이어야 한다 — 만든 뒤 한 번 더 저장하므로 만들 때 version 이 아니다.

경로가 종류를 안 보고 무조건 cases 로 나갔다. 서버는 종류를 조회 조건에 넣고 그 이유를
코드에 적어 두었다 — 그렇게 하지 않으면 Case 경로로 Reference 를 지울 수 있게 되기
때문이다. 경로가 종류와 어긋나면 DOCUMENT_NOT_FOUND 가 나고 초안이 남는다.

시험 초안을 CASE 로 쓴 판단은 맞았지만 코드가 그 판단에 기대고 있었다. 종류별 경로를
표로 만들고, Decision 처럼 프로젝트 id 가 필요한 것과 모르는 종류는 거절한다 — 조용히
받으면 못 지우는 초안이 운영에 남는다.

CSRF 헤더 이름과 404 의 뜻도 계획에 적었다. X-XSRF-TOKEN 이면 403 이다.

이 고침은 계약에 맞춘 것이지 걸어 본 것이 아니다. 계획대로 보내 본 사람은 아직 없다.

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-11 00:36:29 +09:00
co-authored by Claude Opus 5
parent e1404f8a3d
commit 96d89ec6da
2 changed files with 83 additions and 2 deletions
+49
View File
@@ -502,6 +502,55 @@ class HarnessTestPlanTest(unittest.TestCase):
harness_test=True)
self.assertIn("cleanup", [s["op"] for s in harness])
def test_the_cleanup_path_follows_the_kind(self):
"""서버가 종류를 조회 조건에 넣는다.
「그렇게 하지 않으면 Case 경로로 Reference 를 지울 수 있게 되기 때문이다」
(`DeleteDocumentDraftUseCase`). 경로를 종류와 안 맞추면 `DOCUMENT_NOT_FOUND` 가
나고 **초안이 남는다.** 시험 초안을 CASE 로만 쓰면 이 결함이 안 드러난다.
"""
for kind, want in (("CASE", "/cases/"), ("CONCEPT", "/concepts/"),
("REFERENCE", "/references/"), ("QUESTION", "/questions/"),
("SETUP", "/setups/")):
with self.subTest(kind=kind):
steps = ss.plan_requests("docs/p/t/case/x.md", {**SENT, "kind": kind},
None, None, harness_test=True)
cleanup = next(s for s in steps if s["op"] == "cleanup")
self.assertIn(want, cleanup["path"])
def test_the_cleanup_body_is_not_empty(self):
"""`requestBody: required: true` 다. 안 보내면 400 이고 초안이 남는다.
값은 **`verify` 가 읽은 version** 이다 — 만든 뒤 한 번 더 저장하므로 만들 때
version 이 아니다.
"""
steps = ss.plan_requests("docs/p/t/case/x.md", dict(SENT), None, None,
harness_test=True)
cleanup = next(s for s in steps if s["op"] == "cleanup")
self.assertIsNotNone(cleanup["body"])
self.assertIn("expectedVersion", cleanup["body"])
self.assertIn("verify", str(cleanup["body"]["expectedVersion"]))
def test_a_kind_we_cannot_delete_is_refused(self):
"""지울 수 없는 것을 만들지 않는다.
Decision 은 프로젝트 아래에 있어 프로젝트 id 가 있어야 지운다. 이 어댑터는
그 값을 모른다 — 조용히 받으면 못 지우는 초안이 운영에 남는다.
"""
for kind in ("PROJECT_DECISION", "MYSTERY"):
with self.subTest(kind=kind):
with self.assertRaises(ss.Refused):
ss.plan_requests("docs/p/t/case/x.md", {**SENT, "kind": kind},
None, None, harness_test=True)
def test_the_cleanup_names_the_csrf_header_and_the_not_found_shape(self):
"""`X-XSRF-TOKEN` 이면 403 이 난다. 그리고 404 는 「종류가 어긋났다」의 모양이다."""
steps = ss.plan_requests("docs/p/t/case/x.md", dict(SENT), None, None,
harness_test=True)
e = next(s for s in steps if s["op"] == "cleanup")["expect"]
self.assertIn("X-CSRF-TOKEN", e["csrfHeader"])
self.assertIn("DOCUMENT_NOT_FOUND", e["onNotFound"])
def test_the_cleanup_path_is_the_case_delete_endpoint(self):
"""`DELETE /api/v1/studio/cases/{id}` 는 실재한다
(`ManagementDocumentController.java:72`).