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:
co-authored by
Claude Opus 5
parent
e1404f8a3d
commit
96d89ec6da
+34
-2
@@ -256,6 +256,20 @@ VERDICTS = ("PASS", "FAIL", "UNKNOWN")
|
||||
# 사람이 눈으로 가릴 수 있게 시험 초안에 붙이는 접두사
|
||||
HARNESS_PREFIX = "[HARNESS-TEST]"
|
||||
|
||||
# 종류마다 삭제 경로가 다르다. **서버가 종류를 조회 조건에 넣는다** —
|
||||
# 「그렇게 하지 않으면 Case 경로로 Reference 를 지울 수 있게 되기 때문이다」
|
||||
# (`DeleteDocumentDraftUseCase` · `tech-log-backend` @ `a000f87`).
|
||||
# 경로를 종류와 안 맞추면 `DOCUMENT_NOT_FOUND` 가 나고 **초안이 남는다.**
|
||||
DELETE_PATHS = {
|
||||
"CASE": "/api/v1/studio/cases/{id}",
|
||||
"REFERENCE": "/api/v1/studio/references/{id}",
|
||||
"CONCEPT": "/api/v1/studio/concepts/{id}",
|
||||
"SETUP": "/api/v1/studio/setups/{id}",
|
||||
"QUESTION": "/api/v1/studio/questions/{id}",
|
||||
# Decision 만 프로젝트 아래에 있다. 지우려면 프로젝트 id 도 있어야 한다
|
||||
"PROJECT_DECISION": "/api/v1/studio/projects/{projectId}/decisions/{id}",
|
||||
}
|
||||
|
||||
|
||||
def published_marks(record_path: str) -> list[str]:
|
||||
"""이 기록이 게시된 것으로 보이는 표시. 비면 저장 대상이다.
|
||||
@@ -500,16 +514,34 @@ def plan_requests(record_path: str, doc: dict, document_id: str | None,
|
||||
# 계약에 삭제 경로가 없다」고 적었는데 틀렸다 — `ManagementDocumentController.java:123`
|
||||
# 에 `DELETE /v1/studio/projects/{id}/decisions/{decisionId}` 가 있다. 스킬의 문장을
|
||||
# 확인 없이 옮겼다. **고른 이유는 그 기록이 이 배치가 만든 것이라 남의 것이 아니어서다.**
|
||||
kind = (doc.get("kind") or "").upper()
|
||||
template = DELETE_PATHS.get(kind)
|
||||
if template is None:
|
||||
raise Refused(
|
||||
f"이 종류의 삭제 경로를 모른다: {kind!r}. 지울 수 없는 것을 만들지 않는다\n"
|
||||
f" 아는 종류: {' · '.join(sorted(DELETE_PATHS))}")
|
||||
if "{projectId}" in template:
|
||||
raise Refused(
|
||||
f"{kind} 는 프로젝트 아래에 있어 지우려면 프로젝트 id 가 필요하다. "
|
||||
"이 어댑터는 그 값을 모른다 — 시험 초안으로 만들지 않는다")
|
||||
steps.append({
|
||||
"op": "cleanup", "method": "DELETE",
|
||||
"path": f"/api/v1/studio/cases/{document_id or '<생성된 id>'}",
|
||||
"path": template.format(id=document_id or "<생성된 id>"),
|
||||
"headers": {"X-CSRF-TOKEN": "<env STUDIO_CSRF_TOKEN>"},
|
||||
"cookies": {"TECHLOG_SESSION": "<env STUDIO_SESSION_COOKIE>"},
|
||||
"body": None,
|
||||
# **본문이 필수다.** `requestBody: required: true` 이고 컨트롤러가
|
||||
# `@RequestBody ExpectedVersionRequest` 를 받는다. 안 보내면 400 이고 초안이 남는다.
|
||||
# 값은 **`verify` 가 읽은 version** 이다 — 만들 때 version 이 아니다.
|
||||
# 만든 뒤 한 번 더 저장하므로 그 사이에 올라가 있다
|
||||
"body": {"expectedVersion": "<verify 가 읽은 document.version>"},
|
||||
"expect": {"status": 204,
|
||||
"onConflict": "409 DOCUMENT_PUBLISHED(공개된 기록은 삭제할 수 없습니다) "
|
||||
"또는 DOCUMENT_IN_USE(참조하는 곳이 있어 삭제할 수 없습니다) "
|
||||
"— 둘 다 사람이 화면에서 처리한다",
|
||||
"onNotFound": "404 DOCUMENT_NOT_FOUND — 경로의 종류가 문서의 종류와 "
|
||||
"다르면 이렇게 나오고 초안이 남는다",
|
||||
"csrfHeader": "X-CSRF-TOKEN 이다. X-XSRF-TOKEN 이면 403 이 난다",
|
||||
"then": "GET 으로 404 를 확인한다 — 지워졌다는 것은 그것이다",
|
||||
"why": "시험 초안을 남기지 않는다"},
|
||||
})
|
||||
_reject_forbidden_paths(steps)
|
||||
|
||||
@@ -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`).
|
||||
|
||||
Reference in New Issue
Block a user