fix: normalize ambience loudness for v0.2.5

This commit is contained in:
oz
2026-10-14 12:12:13 +00:00
parent 812540c743
commit 91aff66445
390 changed files with 97709 additions and 257 deletions
+180
View File
@@ -0,0 +1,180 @@
"""실제 승인/ZIP을 임시 사본에서 변조해 fail-closed 릴리스 계약을 검사한다."""
import copy
import json
from pathlib import Path
import shutil
import tempfile
import unittest
from scripts import release_approval as gate
class ReleaseApprovalTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name)
# 실제 증거에 쓰지 않는다. 자동 성공 fixture도 이 임시 사본에만 존재한다.
for name in (gate.APPROVAL, gate.ASR, gate.PRIOR_ASR, gate.BUNDLE,
*(f"public/ambience/{name}" for name in gate.SCOPE["files"])):
target = self.root / name
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(gate.ROOT / name, target)
self.approval = gate.read_json(self.root / gate.APPROVAL)
def write(self, path, value):
(self.root / path).write_text(json.dumps(value, ensure_ascii=False) + "\n")
def rejected(self):
with self.assertRaises((ValueError, KeyError, TypeError, OSError)):
gate.validate_release(self.root)
def synthetic_empty_screening(self):
"""OR의 자동 분기 검사용 fixture. 실제 보고서는 절대 수정하지 않는다."""
screening = gate.read_json(self.root / gate.ASR)
for row in screening["files"]:
row["segments"] = []
row["text"] = ""
row["info"]["duration_after_vad"] = 0
screening["requiredEmptyPassed"] = True
self.write(gate.ASR, screening)
def test_current_asr_failure_passes_only_through_exact_owner_approval(self):
result = gate.validate_release(self.root)
self.assertEqual(result["acceptancePath"], "exact-byte-owner-listening-approval")
self.assertFalse(result["automatedEmptyAsrPassed"])
self.assertTrue(result["humanListeningAcceptance"])
self.assertFalse(result["legalPrivacyClearance"])
(self.root / gate.APPROVAL).unlink()
self.rejected()
def test_automated_empty_criteria_can_pass_without_owner_approval(self):
self.synthetic_empty_screening()
(self.root / gate.APPROVAL).unlink()
result = gate.validate_release(self.root)
self.assertEqual(result["acceptancePath"], "automated-empty-asr")
self.assertFalse(result["humanListeningAcceptance"])
self.assertFalse(result["legalPrivacyClearance"])
def test_every_missing_approval_field_fails(self):
for key in self.approval:
with self.subTest(key=key):
changed = copy.deepcopy(self.approval)
del changed[key]
self.write(gate.APPROVAL, changed)
self.rejected()
def test_statement_owner_scope_timestamp_and_clearance_cannot_be_reinterpreted(self):
mutations = {
"owner": ["Neo", "oz", ""],
"statement": ["승인합니다", "승인합니다. 이대로 진행하세요 ", ""],
"timestampUtc": ["", "not-a-date", "2026-10-14T11:29:50Z"],
"timestampKst": ["", "2026-10-14T11:29:51+09:00"],
"scope": [{}, {**gate.SCOPE, "version": "0.2.3"}, {**gate.SCOPE, "files": ["market.wav"]}],
"productOwnerReleaseApproval": [False, "true", 1],
"humanListeningAcceptance": [False, "true", 1],
"legalPrivacyClearance": [True, "false", 0, None],
}
for key, values in mutations.items():
for value in values:
with self.subTest(key=key, value=value):
self.write(gate.APPROVAL, {**self.approval, key: value})
self.rejected()
def test_approval_hash_mismatches_and_incomplete_asset_scope_fail(self):
for field in ("reviewBundle", "manifest", "notice"):
with self.subTest(field=field):
changed = copy.deepcopy(self.approval)
changed[field]["sha256"] = "0" * 64
self.write(gate.APPROVAL, changed)
self.rejected()
for index in range(8):
with self.subTest(wav=index):
changed = copy.deepcopy(self.approval)
changed["wavs"][index]["sha256"] = "0" * 64
self.write(gate.APPROVAL, changed)
self.rejected()
for wavs in (self.approval["wavs"][:-1], self.approval["wavs"] + self.approval["wavs"][:1]):
self.write(gate.APPROVAL, {**self.approval, "wavs": wavs})
self.rejected()
def test_current_wavs_manifest_notice_and_bundle_are_bound_to_reviewed_bytes(self):
for name in (gate.BUNDLE, *(f"public/ambience/{name}" for name in gate.SCOPE["files"])):
with self.subTest(file=name):
path = self.root / name
original = path.read_bytes()
path.write_bytes(original + b" ")
self.rejected()
path.write_bytes(original)
def test_manifest_reformat_cannot_be_self_approved_by_updating_all_recorded_hashes(self):
path = self.root / gate.MANIFEST
path.write_bytes(path.read_bytes() + b"\n")
digest = gate.sha256(path.read_bytes())
self.approval["manifest"]["sha256"] = digest
self.write(gate.APPROVAL, self.approval)
screening = gate.read_json(self.root / gate.ASR)
screening["method"]["manifestSha256"] = digest
self.write(gate.ASR, screening)
self.rejected() # 전달 ZIP의 manifest 바이트는 여전히 다르다.
def test_wav_change_cannot_be_self_approved_by_updating_manifest_asr_and_approval(self):
path = self.root / "public/ambience/market.wav"
raw = bytearray(path.read_bytes())
raw[100] ^= 1
path.write_bytes(raw)
digest = gate.sha256(raw)
manifest = gate.read_json(self.root / gate.MANIFEST)
screening = gate.read_json(self.root / gate.ASR)
for rows in (manifest["files"], screening["files"], self.approval["wavs"]):
next(row for row in rows if row["environmentId"] == "market")["sha256"] = digest
self.write(gate.MANIFEST, manifest)
manifest_hash = gate.sha256((self.root / gate.MANIFEST).read_bytes())
screening["method"]["manifestSha256"] = manifest_hash
self.approval["manifest"]["sha256"] = manifest_hash
self.write(gate.ASR, screening)
self.write(gate.APPROVAL, self.approval)
self.rejected()
def test_different_bundle_hash_cannot_be_self_approved(self):
path = self.root / gate.BUNDLE
path.write_bytes(path.read_bytes() + b" ")
self.approval["reviewBundle"]["sha256"] = gate.sha256(path.read_bytes())
self.write(gate.APPROVAL, self.approval)
self.rejected()
def test_invalid_approval_is_not_ignored_even_when_automated_criteria_pass(self):
self.synthetic_empty_screening()
for key, value in (("legalPrivacyClearance", True), ("scope", {}), ("timestampUtc", "")):
self.write(gate.APPROVAL, {**self.approval, key: value})
self.rejected()
def test_nonempty_asr_cannot_be_relabeled_pass(self):
screening = gate.read_json(self.root / gate.ASR)
screening["requiredEmptyPassed"] = True
self.write(gate.ASR, screening)
self.rejected()
def test_stale_asr_settings_and_clearance_overclaims_fail_with_valid_owner_approval(self):
original = gate.read_json(self.root / gate.ASR)
for field, key, value in (("method", "manifestSha256", "0" * 64),
("method", "parameters", {}),
("limitations", "legalPrivacyClearance", True),
("limitations", "textIsVerifiedTranscript", True)):
changed = copy.deepcopy(original)
changed[field][key] = value
self.write(gate.ASR, changed)
self.rejected()
def test_unknown_clearance_claim_and_duplicate_json_keys_fail(self):
self.write(gate.APPROVAL, {**self.approval, "legalClearance": True})
self.rejected()
raw = json.dumps(self.approval)
(self.root / gate.APPROVAL).write_text(raw[:-1] + ', "legalPrivacyClearance": false}')
self.rejected()
(self.root / gate.APPROVAL).write_text('{"statement":')
self.rejected()
if __name__ == "__main__":
unittest.main()