feat(studio-save): 검토로 보낸다는 것은 저장이 막힌다는 뜻이어야 한다
시도 1 이 「8건이 PASS 로 나가지 않는다」를 세웠는데 코드 3건에 대해서만 성립했다. 경고는 실렸는데 그것을 읽고 막는 코드가 없었다. 검토로 라우팅한다는 것이 실제로는 경고를 붙여서 통과시키는 것이었다. 막지 않으면 라우팅이 아니라 주석이다. 경고가 있으면 항목마다 판정을 받고 전부 PASS 일 때만 저장이 나간다. 판정 파일이 없으면 거절한다 — 검토를 안 받은 것과 검토가 통과시킨 것은 같은 결과일 수 없다. 판정이 안 붙은 경고가 있어도 거절한다 — 빠뜨린 것과 통과시킨 것은 다르다. FAIL 과 UNKNOWN 은 둘 다 막되 문구가 갈린다. 근거가 모자라 판정을 못 한 것과 근거를 읽고 틀렸다고 본 것은 다음에 할 일이 다르다. 판정이 어느 경고에 붙은 것인지 정하려고 경고에 키를 붙였고, 그 키를 읽는 코드를 같은 변경에 넣었다. 내용이 바뀌면 키도 바뀌어 옛 판정이 다른 경고에 붙지 않는다. 경고가 없으면 판정 파일 없이 그대로 나간다. 정상은 이 게이트에 안 걸린다 — 채택 편집 100쌍에서 91쌍이 판정 없이 나가고 9쌍이 판정을 받아야 한다. 판정을 받아야 한다는 것은 막힌다는 뜻이 아니다. 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
845ee89054
commit
20c8535169
@@ -304,3 +304,89 @@ class CliTest(unittest.TestCase):
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
|
||||
class ReviewGateTest(unittest.TestCase):
|
||||
"""검토로 보낸다는 것은 저장이 막힌다는 뜻이어야 한다.
|
||||
|
||||
막지 않으면 라우팅이 아니라 주석이다. `quality-policy@1` §6 — 「UNKNOWN 은 통과가 아니다」.
|
||||
이 게이트가 없던 동안 검토로 보낸 다섯 건이 그대로 저장됐다.
|
||||
"""
|
||||
|
||||
WARN = [{"id": "유보 감소", "detail": "가능성 2회 → 1회", "key": "aaaa1111"},
|
||||
{"id": "그림이 검토 뒤에 바뀌었다", "detail": "fd5f → 3ba4", "key": "bbbb2222"}]
|
||||
|
||||
def setUp(self):
|
||||
self.dir = tempfile.mkdtemp()
|
||||
self.pkg = os.path.join(self.dir, "pkg.json")
|
||||
json.dump({"target": {"sha256": "x"}}, open(self.pkg, "w", encoding="utf-8"))
|
||||
self.book = os.path.join(self.dir, "v.json")
|
||||
|
||||
def _write(self, verdicts, sha=None, keys=None):
|
||||
json.dump({"schemaVersion": 1, "reviewer": "사람",
|
||||
"packageSha256": sha if sha is not None else ss._sha256_file(self.pkg),
|
||||
"verdicts": [{"key": k, "verdict": v, "why": "왜"}
|
||||
for k, v in zip(keys or [w["key"] for w in self.WARN],
|
||||
verdicts)]},
|
||||
open(self.book, "w", encoding="utf-8"), ensure_ascii=False)
|
||||
|
||||
def test_no_warnings_needs_no_verdict_file(self):
|
||||
"""대조군. 정상은 이 게이트에 안 걸린다 — 불필요한 차단을 만들지 않는다."""
|
||||
self.assertEqual([], ss._review_gate([], None, self.pkg))
|
||||
|
||||
def test_warnings_without_a_verdict_file_are_refused(self):
|
||||
"""검토를 안 받은 것과 검토가 통과시킨 것은 같은 결과일 수 없다."""
|
||||
with self.assertRaises(ss.Refused) as e:
|
||||
ss._review_gate(self.WARN, None, self.pkg)
|
||||
self.assertIn("검토를 안 받은 것", str(e.exception))
|
||||
|
||||
def test_all_pass_lets_the_save_through(self):
|
||||
self._write(["PASS", "PASS"])
|
||||
cleared = ss._review_gate(self.WARN, self.book, self.pkg)
|
||||
self.assertEqual(2, len(cleared))
|
||||
self.assertEqual({"PASS"}, {c["verdict"] for c in cleared})
|
||||
|
||||
def test_fail_and_unknown_both_block_with_different_words(self):
|
||||
"""둘 다 막되 문구가 갈린다 — 근거가 모자란 것과 틀렸다고 본 것은 다르다."""
|
||||
self._write(["PASS", "FAIL"])
|
||||
with self.assertRaises(ss.Refused) as fail:
|
||||
ss._review_gate(self.WARN, self.book, self.pkg)
|
||||
self._write(["PASS", "UNKNOWN"])
|
||||
with self.assertRaises(ss.Refused) as unknown:
|
||||
ss._review_gate(self.WARN, self.book, self.pkg)
|
||||
self.assertIn("틀렸다고 봤다", str(fail.exception))
|
||||
self.assertIn("근거가 모자라", str(unknown.exception))
|
||||
self.assertNotEqual(str(fail.exception), str(unknown.exception))
|
||||
|
||||
def test_a_warning_with_no_verdict_is_refused(self):
|
||||
"""빠뜨린 것과 통과시킨 것은 다르다."""
|
||||
self._write(["PASS"], keys=[self.WARN[0]["key"]])
|
||||
with self.assertRaises(ss.Refused) as e:
|
||||
ss._review_gate(self.WARN, self.book, self.pkg)
|
||||
self.assertIn("판정이 안 붙은", str(e.exception))
|
||||
|
||||
def test_a_verdict_for_another_package_is_refused(self):
|
||||
"""묶음이 바뀌면 그 판정을 이 묶음에 쓸 수 없다."""
|
||||
self._write(["PASS", "PASS"], sha="0" * 64)
|
||||
with self.assertRaises(ss.Refused) as e:
|
||||
ss._review_gate(self.WARN, self.book, self.pkg)
|
||||
self.assertIn("다른 묶음", str(e.exception))
|
||||
|
||||
def test_a_missing_verdict_file_is_refused(self):
|
||||
with self.assertRaises(ss.Refused):
|
||||
ss._review_gate(self.WARN, os.path.join(self.dir, "nope.json"), self.pkg)
|
||||
|
||||
def test_an_unknown_verdict_word_is_refused(self):
|
||||
self._write(["PASS", "MAYBE"])
|
||||
with self.assertRaises(ss.Refused):
|
||||
ss._review_gate(self.WARN, self.book, self.pkg)
|
||||
|
||||
def test_the_key_changes_when_the_warning_changes(self):
|
||||
"""판정이 다른 경고에 옛 판정으로 붙지 않아야 한다."""
|
||||
import hashlib
|
||||
def key(entry):
|
||||
seed = "|".join(str(entry.get(k, "")) for k in ("id", "detail", "asset"))
|
||||
return hashlib.sha256(seed.encode("utf-8")).hexdigest()[:12]
|
||||
a = {"id": "유보 감소", "detail": "가능성 2회 → 1회"}
|
||||
b = {"id": "유보 감소", "detail": "가능성 3회 → 1회"}
|
||||
self.assertNotEqual(key(a), key(b))
|
||||
|
||||
Reference in New Issue
Block a user