fix(studio-save): CONCEPT·REFERENCE 의 칸이 계약과 달랐다
C 가 V-009 에서 운영에 직접 걸어 찾았다. QUESTION 에서 고친 것과 같은 결함족이다.
① CONCEPT — basisVersion 이 required 인데(studio-v1.yaml:979) 어댑터가 절만 봤다.
값은 frontmatter 에 있다. 상한 120자를 넘으면 조용히 자르지 않고 거절한다.
② REFERENCE — 셋이 한꺼번에 틀렸다(studio-v1.yaml:955-963).
이름: appliesWhen 이 아니라 applyWhen 이다 — nextValidation 과 같은 자리다
모양: rules 는 ReferenceRule 배열, applyWhen·exceptions·examples 는 OrderedText
배열이다. purpose 만 문자열이다
없는 칸: verifiedOn 이 required 인데 아예 없었다. 기록이 안 적었으면 null 로 둔다
③ 되읽기 비교가 항목 id 를 견주고 있었다. 보내는 것은 uuid5 이고 서버는 저장하면서
uuid4 를 새로 발급한다 — 그대로 두면 모든 QUESTION·REFERENCE 저장이 「차이 있음」이
된다. 칸 전체를 빼지 않고 id 만 뺐다. text·title·body·order 는 계속 대조하므로
항목이 빠지거나 순서가 바뀌는 것은 여전히 걸린다. 무엇을 왜 안 보는지는
itemIdsNotCompared 에 값으로 적는다.
python3 -m unittest discover -s scripts/tests — Ran 273 · 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
14137382fe
commit
0d588814cc
@@ -667,6 +667,72 @@ class HarnessTestPlanTest(unittest.TestCase):
|
||||
out = ss._question_shape({"slug": "s"}, {})
|
||||
self.assertIsNone(out["questionStatus"])
|
||||
|
||||
def test_concept_carries_basis_version_from_frontmatter(self):
|
||||
"""`basisVersion` 은 절이 아니라 frontmatter 에 있다. required 라 빠지면 422 다
|
||||
(C 의 `P-CONCEPT-01`·`02`)."""
|
||||
import tempfile, os
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
f = os.path.join(d, "c.md")
|
||||
open(f, "w", encoding="utf-8").write(
|
||||
"---\nkind: CONCEPT\ntitle: t\nslug: s\n"
|
||||
"basisVersion: Keycloak 26.7.0\n---\n\n## 본문\n\n가.\n")
|
||||
doc = ss.build_input(f)
|
||||
self.assertEqual("Keycloak 26.7.0", doc["basisVersion"])
|
||||
|
||||
def test_concept_refuses_an_overlong_basis_version(self):
|
||||
"""상한 120자. **조용히 자르지 않는다** — 무엇을 보고 쓴 글인지가 잘리면 모른다."""
|
||||
with self.assertRaises(ss.Refused):
|
||||
ss._concept_shape({}, {"basisVersion": "가" * 121})
|
||||
|
||||
def test_concept_keeps_the_field_even_when_empty(self):
|
||||
"""비워도 되지만 **빠지면 안 된다**."""
|
||||
self.assertEqual("", ss._concept_shape({}, {})["basisVersion"])
|
||||
|
||||
def test_reference_uses_the_contract_field_name(self):
|
||||
"""계약은 `applyWhen` 이다. 어댑터가 `appliesWhen` 을 보내고 있었다 —
|
||||
`nextVerification` → `nextValidation` 과 같은 자리다."""
|
||||
doc = ss._reference_shape({"slug": "s", "applyWhen": "- 하나"}, {})
|
||||
self.assertIn("applyWhen", doc)
|
||||
self.assertNotIn("appliesWhen", doc)
|
||||
self.assertEqual("하나", doc["applyWhen"][0]["text"])
|
||||
|
||||
def test_reference_shapes_are_arrays(self):
|
||||
"""`rules` 는 `ReferenceRule`, 나머지 셋은 `OrderedText` 다. `purpose` 만 문자열."""
|
||||
doc = ss._reference_shape(
|
||||
{"slug": "s", "purpose": "무엇을 위한 것인가", "rules": "### 1. 첫 규칙\n\n몸통이다.",
|
||||
"applyWhen": "- 하나", "exceptions": "", "examples": ""}, {})
|
||||
self.assertEqual({"id", "title", "body", "order"}, set(doc["rules"][0]))
|
||||
self.assertEqual("첫 규칙", doc["rules"][0]["title"])
|
||||
self.assertEqual({"id", "text", "order"}, set(doc["applyWhen"][0]))
|
||||
self.assertEqual([], doc["exceptions"]) # required — 비어도 [] 다
|
||||
self.assertEqual([], doc["examples"])
|
||||
self.assertIsInstance(doc["purpose"], str)
|
||||
|
||||
def test_reference_verified_on_is_not_invented(self):
|
||||
"""required 이고 `[string, "null"]` 이다. 기록이 안 적었으면 지어내지 않는다."""
|
||||
self.assertIsNone(ss._reference_shape({"slug": "s"}, {})["verifiedOn"])
|
||||
self.assertEqual("2026-08-30",
|
||||
ss._reference_shape({"slug": "s"},
|
||||
{"lastVerifiedOn": "2026-08-30"})["verifiedOn"])
|
||||
|
||||
def test_item_ids_are_not_compared_but_text_and_order_are(self):
|
||||
"""서버가 항목 `id` 를 새로 발급한다. 칸 전체를 빼면 항목이 빠진 것도 못 본다."""
|
||||
sent = {"facts": [{"id": "a5", "text": "하나", "order": 0},
|
||||
{"id": "b5", "text": "둘", "order": 1}]}
|
||||
same = {"facts": [{"id": "x4", "text": "하나", "order": 0},
|
||||
{"id": "y4", "text": "둘", "order": 1}]}
|
||||
self.assertTrue(ss.compare_saved(sent, same)["same"])
|
||||
# **못 보는 것을 값에 적는다** — 빼먹은 것이 아니다
|
||||
skipped = ss.compare_saved(sent, same)["itemIdsNotCompared"]
|
||||
self.assertEqual("facts", skipped[0]["field"])
|
||||
self.assertIn("order", skipped[0]["note"])
|
||||
# 항목이 빠지거나 순서가 바뀌면 여전히 걸린다
|
||||
dropped = {"facts": [{"id": "x4", "text": "하나", "order": 0}]}
|
||||
self.assertFalse(ss.compare_saved(sent, dropped)["same"])
|
||||
swapped = {"facts": [{"id": "x4", "text": "둘", "order": 0},
|
||||
{"id": "y4", "text": "하나", "order": 1}]}
|
||||
self.assertFalse(ss.compare_saved(sent, swapped)["same"])
|
||||
|
||||
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