[verified] Check trusted-template AC verification classification

This commit is contained in:
2026-10-12 16:28:55 +00:00
parent b687af03ec
commit c1f0034300
3 changed files with 253 additions and 1 deletions
@@ -61,6 +61,128 @@ def verify_fixture(before, after, review, r, paths, contract=None):
expected_pr_url="https://example.test/acme/repo/pulls/7")
class ACClassificationTests(unittest.TestCase):
TEMPLATE = ROOT / 'harness/docs/requirements/TEMPLATE.md'
def body(self):
return ('## 7. 인수 기준\n\n### REQ-001.01-AC01 — example\n\n'
'**판정 기준**\n\nexample\n\n**검증**\n\nexample\n\n'
'**검증 분류**\n\n- 수준: unit\n- 백엔드: 없음\n- 케이스: normal\n\n'
'**상태 불변조건**\n\n없음\n\n## 8. 오류 및 충돌 계약\n\n없음\n')
def check(self, body):
import hashlib
self.assertTrue(callable(getattr(mod, 'check_ac_classification', None)),
'trusted-template AC classification checker missing')
return mod.check_ac_classification(body, hashlib.sha256(self.TEMPLATE.read_bytes()).hexdigest())
def test_valid_canonical_ac_classification(self):
out = self.check(self.body())
self.assertEqual(out['ac_ids'], ['REQ-001.01-AC01'])
self.assertEqual(out['checked_fields'], ['수준', '백엔드', '케이스'])
self.assertTrue(out['read_only'])
def test_missing_empty_duplicate_and_placeholder_fields_rejected_per_ac(self):
valid = self.body()
self.check(valid)
for changed in (valid.replace('- 수준: unit\n', ''),
valid.replace('- 백엔드: 없음', '- 백엔드: '),
valid.replace('- 케이스: normal', '- 케이스: ${verification_cases}'),
valid.replace('- 수준: unit', '- 수준: unit\n- 수준: unit'),
valid.replace('**검증 분류**', '**검증**'),
valid.replace('**검증 분류**', '**검증 분류**\n\n**검증 분류**')):
with self.subTest(changed=changed):
with self.assertRaises(mod.ValidationError):
self.check(changed)
def test_other_ac_and_fenced_examples_cannot_supply_missing_field(self):
good = self.body().split('## 8.', 1)[0]
bad = good.replace('AC01', 'AC02').replace('- 수준: unit\n', '')
self.check(good + good.replace('AC01', 'AC02').split('\n\n', 1)[1] + '## 8. 오류 및 충돌 계약\n')
for extra in ('', '```markdown\n- 수준: unit\n```\n', '~~~\n- 수준: unit\n~~~\n'):
with self.subTest(extra=extra):
with self.assertRaises(mod.ValidationError):
self.check(good + bad.split('\n\n', 1)[1].replace('**상태 불변조건**', extra + '**상태 불변조건**') + '## 8. 오류 및 충돌 계약\n')
def test_ambiguous_hidden_sections_and_template_drift_fail_closed(self):
valid = self.body()
self.check(valid)
import hashlib
with self.assertRaises(mod.ValidationError):
mod.check_ac_classification(valid, '0' * 64)
for bad in ('<!--\n' + valid + '\n-->', valid + '\n```unclosed',
valid.replace('### REQ', '#### REQ'),
valid.replace('## 8. 오류 및 충돌 계약', '## 7. 인수 기준'),
valid.replace('- 수준: unit', ' - 수준: unit')):
with self.subTest(bad=bad):
with self.assertRaises(mod.ValidationError):
self.check(bad)
crlf = valid.replace('\n', '\r\n')
self.assertEqual(self.check(crlf)['document_sha256'], hashlib.sha256(crlf.encode()).hexdigest())
def test_cli_read_only_exact_bytes_digest_and_sanitized_failure(self):
import hashlib
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / 'req.md'
raw = self.body().replace('\n', '\r\n').encode()
path.write_bytes(raw)
os.chmod(path, 0o600)
before = (path.read_bytes(), path.stat().st_mode, path.stat().st_mtime_ns, path.stat().st_ino)
argv = [sys.executable, '-B', str(MODPATH), 'check-ac-classification',
'--document', str(path), '--expected-document-sha256', hashlib.sha256(raw).hexdigest(),
'--expected-template-sha256', hashlib.sha256(self.TEMPLATE.read_bytes()).hexdigest()]
actual = subprocess.run(argv, capture_output=True, text=True, timeout=10)
self.assertEqual(actual.returncode, 0, actual.stderr)
self.assertEqual(json.loads(actual.stdout)['document_sha256'], hashlib.sha256(raw).hexdigest())
self.assertEqual(before, (path.read_bytes(), path.stat().st_mode, path.stat().st_mtime_ns, path.stat().st_ino))
argv[-3] = '0' * 64
bad = subprocess.run(argv, capture_output=True, text=True, timeout=10)
self.assertEqual(bad.returncode, 2)
self.assertEqual(json.loads(bad.stderr), {'error': 'invalid input'})
self.assertEqual(bad.stdout, '')
def test_supported_scope_rejects_hidden_html_and_noncanonical_ac_heading(self):
valid = self.body()
self.check(valid)
for bad in ('<div>\n' + valid + '</div>',
valid.replace('### REQ-001.01-AC01 — example', '### REQ-001.01-AC01'),
valid.replace('**검증 분류**', '<span>**검증 분류**</span>')):
with self.subTest(bad=bad):
with self.assertRaises(mod.ValidationError):
self.check(bad)
def test_file_admission_rejects_symlink_fifo_invalid_utf8_and_oversize(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
valid = root / 'valid.md'
valid.write_text(self.body())
self.assertEqual(mod._classification_bytes(valid), valid.read_bytes())
alias = root / 'alias.md'
alias.symlink_to(valid)
fifo = root / 'fifo'
os.mkfifo(fifo)
invalid = root / 'invalid.md'
invalid.write_bytes(b'\xff')
large = root / 'large.md'
large.write_bytes(b'x' * 1048577)
for path in (alias, fifo, invalid, large, root):
with self.subTest(path=path.name):
with self.assertRaises(mod.ValidationError):
mod._classification_bytes(path)
def test_documentation_contract_scopes_checker_and_binds_digests(self):
text = (MODPATH.parent.parent / 'SKILL.md').read_text()
self.assertIn('### AC 검증 분류의 read-only 구조 검사', text)
section = text.split('### AC 검증 분류의 read-only 구조 검사', 1)[1].split('\n## ', 1)[0]
for rule in ('check-ac-classification', '--expected-document-sha256',
'--expected-template-sha256', '전체 문서 적합성', '의미',
'없음', '미확인', '코드 fence', '새 schema', '단일 행'):
self.assertIn(rule, section)
class SupportTests(unittest.TestCase):
def test_loads_json_rejects_duplicate_and_nonfinite(self):
with self.assertRaises(mod.ValidationError):