[verified] Check trusted-template AC verification classification
This commit is contained in:
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user