fix(studio-save): 항목 id 를 다시 매기는지는 종류마다 다르다

A 가 B-011 운영 실행에서 봤다. PROJECT_DECISION 은 보낸 uuid5 여섯이 글자 그대로
돌아온다. QUESTION 은 서버가 uuid4 로 다시 발급한다(C · V-009 열 번째). 둘 다
OrderedText 배열인데 서버가 다르게 다룬다.

한 벌로 묶어 빼면 DECISION 에서 볼 수 있는 것을 안 보게 된다 — 서버가 언젠가
DECISION 도 재발급하기 시작해도 아무도 모른다. 그래서 종류로 갈랐다.

  QUESTION          다시 매긴다 → id 를 뺀다 (쟀다)
  PROJECT_DECISION  다시 안 매긴다 → 그대로 본다 (쟀다)
  그 밖              안 쟀다 → 빼지 않고 그대로 보고, 안 쟀다는 것을 값에 적는다

안 잰 채로 빼면 「안 봐도 되는 것」으로 굳는다. 「못 보는 것」과 「안 봐도 되는 것」은
다르다 — itemIdBehaviourUnmeasured 가 어느 칸을 왜 그대로 견줬는지 적는다.

python3 -m unittest discover -s scripts/tests — Ran 286 · 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:
DongHyeonka
2026-09-11 11:19:55 +09:00
co-authored by Claude Opus 5
parent 325b6008ab
commit 1be1c8ea6d
2 changed files with 72 additions and 14 deletions
+43 -7
View File
@@ -717,20 +717,21 @@ class HarnessTestPlanTest(unittest.TestCase):
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}]}
K = {"kind": "QUESTION"} # 종류마다 다르다 — 아래 갈래 시험을 본다
sent = {**K, "facts": [{"id": "a5", "text": "하나", "order": 0},
{"id": "b5", "text": "", "order": 1}]}
same = {**K, "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}]}
dropped = {**K, "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}]}
swapped = {**K, "facts": [{"id": "x4", "text": "", "order": 0},
{"id": "y4", "text": "하나", "order": 1}]}
self.assertFalse(ss.compare_saved(sent, swapped)["same"])
def test_decision_uses_the_contract_field_names(self):
@@ -866,6 +867,41 @@ class HarnessTestPlanTest(unittest.TestCase):
# 글이 있으면 반드시 무언가를 읽는다 — 조용히 비지 않는다
self.assertTrue(ss._question_options("제목 한 줄만 있다"))
def test_item_id_handling_is_split_by_kind(self):
"""둘 다 `OrderedText` 배열인데 서버가 다르게 다룬다.
QUESTION 은 `uuid4` 로 다시 매기고(C · V-009 열 번째), **PROJECT_DECISION 은 보낸
`uuid5` 를 그대로 쓴다**(A · B-011 운영 실행). 한 벌로 묶어 빼면 DECISION 에서
**볼 수 있는 것을 안 보게 된다** — 서버가 언젠가 재발급을 시작해도 아무도 모른다.
"""
def arr(ids):
return [{"id": i, "text": f"t{n}", "order": n} for n, i in enumerate(ids)]
q = {"kind": "QUESTION", "facts": arr(["a5", "b5"])}
self.assertTrue(ss.compare_saved(q, {"kind": "QUESTION",
"facts": arr(["x4", "y4"])})["same"])
d = {"kind": "PROJECT_DECISION", "consequences": arr(["a5", "b5"])}
self.assertTrue(ss.compare_saved(d, dict(d))["same"])
# **DECISION 에서 id 가 바뀌면 보여야 한다** — 그것이 이 갈래의 값이다
moved = ss.compare_saved(d, {"kind": "PROJECT_DECISION",
"consequences": arr(["x4", "y4"])})
self.assertFalse(moved["same"])
self.assertEqual([], moved["itemIdsNotCompared"])
def test_an_unmeasured_kind_is_compared_not_skipped(self):
"""안 잰 종류는 뺀 것이 아니라 그대로 본다. 안 잰 채로 빼면
「안 봐도 되는 것」으로 굳는다 — 무엇을 왜 그대로 견줬는지 값에 적는다."""
ref = {"kind": "REFERENCE",
"rules": [{"id": "a5", "title": "t", "body": "b", "order": 0}]}
r = ss.compare_saved(ref, dict(ref))
self.assertNotIn("REFERENCE", ss.ITEM_ID_REISSUED)
self.assertEqual([], r["itemIdsNotCompared"])
self.assertEqual(["rules"], r["itemIdBehaviourUnmeasured"]["fields"])
self.assertIn("아직 안 쟀다", r["itemIdBehaviourUnmeasured"]["note"])
# 잰 종류에는 그 칸이 안 붙는다
q = {"kind": "QUESTION", "facts": [{"id": "a", "text": "t", "order": 0}]}
self.assertNotIn("itemIdBehaviourUnmeasured", ss.compare_saved(q, dict(q)))
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)