fix(studio-save): PROJECT_DECISION 의 칸을 계약에 맞추고 지울 수 있게 만든다
B-010 §4.1 에서 찾고 안 고친 것이다. 그때 안 고친 이유는 예산이 아니라 측정이었고, 이번에는 그 런에서 잰다. - 결정문→statement · 영향→consequences(OrderedText 배열) · 판단 이유→rationale - 근거→basis 를 뺐다. ProjectDecisionInput 에 그 칸이 없고 unevaluatedProperties: false 라 보내면 거절된다. 기록의 ## 근거 절은 그대로 둔다 - decisionStatus·decidedOn 을 frontmatter 에서 읽는다. enum 밖의 값은 지어내지 않고 거절한다 — 운영에 초안을 만들어 놓고 422 를 받는 것보다 낫다 그리고 절의 모양이 둘이었다. 사실·가정은 `- ` 목록이고 DECISION 의 영향은 빈 줄로 나뉜 문단이다. 목록만 읽어서 문단으로 쓴 절이 조용히 [] 가 되고 있었다 — minItems 가 없어 그대로 저장되고 내용만 사라진다. 목록을 먼저 보고 없으면 문단으로 나누며, 글이 있는데 항목이 0 개면 거절한다. QUESTION·REFERENCE 의 이미 잰 값은 안 바뀐다(목록이라 첫 갈래에서 끝난다). --harness-test 의 PROJECT_DECISION 거절은 우회하지 않고 값을 받게 했다. --project-id 는 사람이 Studio 목록에서 읽은 uuid 다 — 어댑터가 이름을 uuid 로 바꾸지 않는다. 없으면 여전히 거절한다. projectId 는 [string, "null"] 이고 계약이 저장 시점에는 강제하지 않으니 null 로도 만들어지고, 만들어지면 지울 수 없다. 되읽기가 그 값을 확인한다. python3 -m unittest discover -s scripts/tests — Ran 278 · OK (skipped=13) 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
0d588814cc
commit
f765a79d6f
@@ -733,6 +733,61 @@ class HarnessTestPlanTest(unittest.TestCase):
|
||||
{"id": "y4", "text": "하나", "order": 1}]}
|
||||
self.assertFalse(ss.compare_saved(sent, swapped)["same"])
|
||||
|
||||
def test_decision_uses_the_contract_field_names(self):
|
||||
"""`결정문`→`statement` · `영향`→`consequences`. 그리고 `근거`→`basis` 는
|
||||
계약에 없는 칸이라 `unevaluatedProperties: false` 에 거절된다 — 안 보낸다."""
|
||||
self.assertEqual({"결정문": "statement", "판단 이유": "rationale",
|
||||
"영향": "consequences"}, ss.FIELD_MAP["PROJECT_DECISION"])
|
||||
doc = ss._decision_shape(
|
||||
{"slug": "s", "statement": "이렇게 한다", "rationale": "왜냐하면",
|
||||
"consequences": "- 하나\n- 둘"},
|
||||
{"decisionStatus": "ADOPTED", "decidedOn": "2026-08-30"})
|
||||
self.assertEqual({"id", "text", "order"}, set(doc["consequences"][0]))
|
||||
self.assertEqual("ADOPTED", doc["decisionStatus"])
|
||||
self.assertEqual("2026-08-30", doc["decidedOn"])
|
||||
for gone in ("basis", "decision", "impact"):
|
||||
self.assertNotIn(gone, doc)
|
||||
|
||||
def test_decision_status_outside_the_enum_is_refused(self):
|
||||
"""enum 이 셋뿐이다. 지어내지 않고 여기서 막는다 —
|
||||
운영에 초안을 만들어 놓고 422 를 받는 것보다 낫다."""
|
||||
with self.assertRaises(ss.Refused):
|
||||
ss._decision_shape({"slug": "s"}, {"decisionStatus": "REJECTED"})
|
||||
self.assertIsNone(ss._decision_shape({"slug": "s"}, {})["decisionStatus"])
|
||||
self.assertIsNone(ss._decision_shape({"slug": "s"}, {})["decidedOn"])
|
||||
|
||||
def test_paragraph_sections_are_not_silently_empty(self):
|
||||
"""항목의 모양이 절마다 다르다. `- ` 목록만 읽으면 문단으로 쓴 절이 조용히
|
||||
`[]` 가 된다 — 스키마에 `minItems` 가 없어 그대로 저장되고 내용만 사라진다."""
|
||||
bullets = ss._ordered("s", "consequences", "- 하나\n- 둘")
|
||||
paras = ss._ordered("s", "consequences", "감수하는 것 : 가.\n\n얻는 것 : 나.")
|
||||
self.assertEqual(2, len(bullets))
|
||||
self.assertEqual(2, len(paras))
|
||||
self.assertEqual("얻는 것 : 나.", paras[1]["text"])
|
||||
# 절이 비어 있는 것은 정상이다 — 못 읽은 것과 다르다
|
||||
self.assertEqual([], ss._ordered("s", "consequences", ""))
|
||||
self.assertEqual([], ss._ordered("s", "consequences", None))
|
||||
|
||||
def test_a_decision_without_a_project_id_is_refused(self):
|
||||
"""프로젝트 id 없이 만들면 그 초안을 못 지운다. 우회하지 않는다."""
|
||||
doc = dict(SENT, kind="PROJECT_DECISION", slug="s")
|
||||
doc.pop("projectId", None)
|
||||
with self.assertRaises(ss.Refused) as cm:
|
||||
ss.plan_requests("docs/p/t/decision/x.md", doc, None, None, harness_test=True)
|
||||
self.assertIn("--project-id", str(cm.exception))
|
||||
|
||||
def test_a_decision_with_a_project_id_plans_a_deletable_draft(self):
|
||||
"""값이 있으면 정리 경로에 들어가고, 되읽기가 그 값을 확인한다 —
|
||||
되읽은 값이 다르면 지우려는 곳이 문서가 있는 곳이 아니다."""
|
||||
pid = "11111111-aaaa-bbbb-cccc-222222222222"
|
||||
doc = dict(SENT, kind="PROJECT_DECISION", slug="s", projectId=pid)
|
||||
steps = ss.plan_requests("docs/p/t/decision/x.md", doc, None, None,
|
||||
harness_test=True)
|
||||
cleanup = next(x for x in steps if x["op"] == "cleanup")
|
||||
self.assertIn(f"/projects/{pid}/decisions/", cleanup["path"])
|
||||
verify = next(x for x in steps if x["op"] == "verify")
|
||||
self.assertIn(pid, verify["expect"]["projectId"])
|
||||
|
||||
def test_a_harness_plan_still_has_no_publish_path(self):
|
||||
steps = ss.plan_requests("docs/p/t/case/x.md", dict(SENT), None, None,
|
||||
harness_test=True)
|
||||
|
||||
Reference in New Issue
Block a user