"""Documentation contracts and local synthetic controls; not agent/live execution.""" from pathlib import Path import re import unittest HARNESS = Path(__file__).resolve().parents[2] DISCOVERY = HARNESS / 'agents/spec/skills/gitea-discovery-review/SKILL.md' IMPLEMENTATION = HARNESS / 'agents/spec/skills/gitea-implementation-spec-review/SKILL.md' README = HARNESS / 'docs/discovery/README.md' class PriorityReviewContracts(unittest.TestCase): def test_issue_body_provider_revision_documentation_contract(self): access = (HARNESS / 'operations/gitea-access.md').read_text() contract = access.split('### Issue body update의 provider-derived readback', 1)[-1] for required in ('matching provider contract', 'content_version', '정확히 1', 'baseline과 readback', '정수', 'boolean', 'pre-write drift', '자동 재write하지 않는다', 'updated_at', '보호 필드', 'uncertain/partial', '새 runtime checker'): self.assertIn(required, contract) authoring = (HARNESS / 'agents/spec/skills/gitea-discovery-authoring/SKILL.md').read_text() for text in (DISCOVERY.read_text(), authoring): self.assertIn('Issue body update의 provider-derived readback', text) self.assertIn('content_version', text) self.assertIn('자동 재write하지 않는다', text) self.assertIn('post-comment snapshot을 body update 직전 baseline', authoring) def test_scope_classification_and_optional_nonblocking(self): text = README.read_text() for value in ('핵심 요청:', '기존 호환성:', '선택적 확장:', '명시적으로 채택되지 않은 선택적 확장은', 'blocking OQ로 승격하지 않는다'): self.assertIn(value, text) def test_all_oq_options_preserve_mandatory_contracts(self): text = README.read_text() oq_rules = text.split('## OQ(${open_questions}) 작성 방법', 1)[1].split('```markdown', 1)[0] for value in ('추천안을 포함한 모든 선택지', '원본 요청의 필수 범위, 확정된 결정과 기존 호환성 불변조건을 보존', '미검증 전제를 확정된 사실로 취급하지 않는다', '요구 철회나 기존 계약 변경', '일반 구현 대안과 구분하고 변경 대상과 영향을 명시'): with self.subTest(rule=value): self.assertIn(value, oq_rules) def test_compatibility_user_decisions_and_existing_oq_preserved(self): text = DISCOVERY.read_text() for value in ('기존 보안·호환성 제약은 선택적으로 낮추지', '미선택 OQ를 임의 삭제·해결하지', '선택을 보존하고 사용자 판단', '사용자 선택이 필요한 결정 공백과 구분'): self.assertIn(value, text) def test_experiment_verdict_boundary_and_parent_check(self): text = DISCOVERY.read_text() for value in ('시험한 방식의 실패:', '소스로 확인된 제약:', '요구 충족 불가능:', '미검증 또는 판단 유보', '시험하지 않은 대안·확인 한계', '원본 실험·공식 소스에 직접 대조', '일부 prototype 실패만으로'): self.assertIn(value, text) def test_document_delegate_inputs_and_fail_closed_result_adoption(self): text = IMPLEMENTATION.read_text() for value in ('byte parity', '실제 내용을 delegate context로 제공', '대상 저장소 README/TEMPLATE를 작성 정책으로 대신 사용하지', '산출물별 의무표:', 'pinned baseline 문서 및 전체 AC', '이전 finding의 승인 근거와 최소 보완 요청', '입력이 부족하면 main이 보완하거나 직접 검토', 'delegate의 PASS를 최종 판정으로 채택하지'): self.assertIn(value, text) def test_structure_semantics_and_preserved_contract_not_conflated(self): text = IMPLEMENTATION.read_text() for value in ('구조 적합성(', '의미 적합성(', 'exact section·line', '구조 통과만으로 의미를 승인', '반복 기재 부재만으로 의미 누락', '제품 저장소의 새 산출물이나 result schema 변경을 요구하지'): self.assertIn(value, text) def test_named_test_documentation_contract_requires_call_assertion_cross_check(self): text = IMPLEMENTATION.read_text() heading = '### Named test 근거와 재작업 보고' self.assertIn(heading, text) section = text.split(heading, 1)[1].split('\n## ', 1)[0] for rule in ('exact reviewed head', '실제 호출 인수와 assertions', '검증하는 경우와 검증하지 않는 경우', '정적 확인과 실제 실행', '테스트 이름이나 PASS 수치만으로', '확인 한계'): with self.subTest(rule=rule): self.assertIn(rule, section) def test_rework_report_documentation_contract_requires_categories_and_counting_rules(self): text = IMPLEMENTATION.read_text() section = text.split('### Named test 근거와 재작업 보고', 1)[1].split('\n## ', 1)[0] for rule in ('해소:', '잔여:', '신규:', '문서 보완:', '해당 없음', '같은 원인의 구현·문서 finding', '중복 집계하지 않는다', '승인 계약 위반', 'blocker로 승격하지 않는다', 'result schema나 renderer', '자동 assertion 해석기'): with self.subTest(rule=rule): self.assertIn(rule, section) def test_writer_family_documentation_contract_requires_bounded_path_evidence(self): text = IMPLEMENTATION.read_text() heading = '### Writer-family criterion closure checklist' self.assertIn(heading, text) section = text.split(heading, 1)[1].split('\n## ', 1)[0] for rule in ('기존 승인 계약', 'monetary/current-cache', 'create/update/import', '실제 존재', '정규화', 'unknown과 zero', 'mutation/no-write', 'downstream', 'exact reviewed head', '`evidence`·`reason`', 'family 전체', '비적용', '잔여 finding', '새 금액 정책', '모든 입력 조합', '전체 suite 재실행', '자동 runtime checker', '제품 실행 금지', 'legacy financial-authority'): with self.subTest(rule=rule): self.assertIn(rule, section) def test_changed_readback_helper_documentation_contract_is_conditional_and_precreate(self): text = IMPLEMENTATION.read_text() heading = '### 비정본·변경된 readback helper 호출 계약' self.assertIn(heading, text, 'conditional readback invocation guidance is missing') section = text.split(heading, 1)[1].split('\n## ', 1)[0] for rule in ('비정본', '변경된', 'native create 이전', '정본 adapter를 변경 없이', '중복 signature 검사를 강제하지', 'inspect.signature', '.bind(', '실행하지', 'partial', '인증', 'pagination completeness', 'read-only', '새 runtime gate', 'source revision'): with self.subTest(rule=rule): self.assertIn(rule, section) step = text.split('17. 호출 전에', 1)[1].split('\n18.', 1)[0] self.assertIn('비정본·변경된 readback helper 호출 계약', step) self.assertLess(step.index('비정본·변경된 readback helper 호출 계약'), step.index('Native review create를')) def test_discovery_checker_documentation_contract_preserves_bytes_and_bounded_transformations(self): text = DISCOVERY.read_text() heading = '### 임시·변경된 Discovery checker 준비' self.assertIn(heading, text, 'bounded Discovery checker preparation guidance is missing') section = text.split(heading, 1)[1].split('\n## ', 1)[0] for rule in ('임시·변경된', 'LF/CRLF', 'splitlines', '원본 bytes', 'payload', 'hash', 'flags=', 'section·paragraph', 'OQ ID', '정확 문구', 'prefix', '일괄 면제하지', 'replay', '외부 write 없이', '사용자 선택', '공통 semantic validator', '부분 완료', 'synthetic'): with self.subTest(rule=rule): self.assertIn(rule, section) step = text.split('11. trusted workflow directory', 1)[1].split('\n12.', 1)[0] self.assertIn('임시·변경된 Discovery checker 준비', step) def test_local_links_resolve_and_procedures_reference_new_rules(self): for path in (DISCOVERY, IMPLEMENTATION, README): text = path.read_text() for link in re.findall(r'\]\(([^)]+)\)', text): if not link.startswith(('http:', 'https:', '#')): self.assertTrue((path.parent / link.split('#')[0]).exists(), link) self.assertIn('8. 범위 분류·blocking 판단과 기술적 결론의 경계를 먼저 적용', DISCOVERY.read_text()) self.assertIn('11. 문서 검토 입력과 구조·의미 분리 절차', IMPLEMENTATION.read_text()) class SyntheticDiscoveryCheckerControls(unittest.TestCase): """Small local primitive examples, not a shared Discovery validator.""" def test_synthetic_logical_headings_accept_crlf_without_rewriting_bytes(self): import hashlib lf = b'## Scope\nbody\n## Questions\nnone\n' crlf = lf.replace(b'\n', b'\r\n') original_digest = hashlib.sha256(crlf).digest() self.assertIsNone(re.search(r'^## Scope$', crlf.decode(), flags=re.MULTILINE)) expected = ['## Scope', '## Questions'] for raw in (lf, crlf): headings = [line for line in raw.decode().splitlines() if line.startswith('## ')] self.assertEqual(headings, expected) for malformed in (lf.replace(b'## Scope', b'## Scopes'), lf + b'## Scope\n', lf.replace(b'## Questions\n', b'')): headings = [line for line in malformed.decode().splitlines() if line.startswith('## ')] self.assertNotEqual(headings, expected) self.assertEqual(hashlib.sha256(crlf).digest(), original_digest) self.assertEqual(crlf, b'## Scope\r\nbody\r\n## Questions\r\nnone\r\n') def test_synthetic_exact_paragraph_exception_does_not_hide_other_oq_residue(self): old = 'OQ-1: legacy introduction' replacement = old + '; explicit approved reselection' paragraphs = {'decision': replacement, 'question': 'OQ-2: unrelated question'} self.assertIn(old, paragraphs['decision']) # Naive global substring false positive. self.assertEqual(paragraphs['decision'], replacement) residue = {key: value for key, value in paragraphs.items() if old in value and not (key == 'decision' and value == replacement)} self.assertEqual(residue, {}) paragraphs['question'] = old + '; unresolved reference in OQ-2' residue = {key: value for key, value in paragraphs.items() if old in value and not (key == 'decision' and value == replacement)} self.assertEqual(set(residue), {'question'}) self.assertEqual(paragraphs['decision'], replacement) def test_synthetic_self_containing_exact_replacement_replays_without_duplication(self): old = b'Legacy introduction' replacement = old + b'; approved detail' naive_replay = replacement.replace(old, replacement) self.assertNotEqual(naive_replay, replacement) # Approved exact paragraph fixture only; no general normalization policy. def fixture_transform(paragraph): if paragraph == replacement: return paragraph if paragraph == old: return replacement raise ValueError('ambiguous paragraph fixture') first = fixture_transform(old) self.assertEqual(first, replacement) self.assertEqual(fixture_transform(first), first) with self.assertRaisesRegex(ValueError, 'ambiguous paragraph fixture'): fixture_transform(old + b'; unrelated edit') class SyntheticReadbackInvocationTests(unittest.TestCase): """Local Python binding/collector controls, not a production pre-create gate.""" def test_synthetic_signature_binding_rejects_extra_argument_without_callback_execution(self): import inspect calls = [] def pages(page, *, endpoint='reviews'): calls.append((page, endpoint)) signature = inspect.signature(pages) bound = signature.bind(1, endpoint='reviews') self.assertEqual(bound.arguments, {'page': 1, 'endpoint': 'reviews'}) self.assertEqual(calls, []) with self.assertRaises(TypeError): signature.bind(1, 50) with self.assertRaises(TypeError): signature.bind(1, page_size=50) self.assertEqual(calls, []) def test_actual_collector_calls_synthetic_callback_with_one_page_argument(self): import importlib.util import inspect path = HARNESS / 'agents/spec/skills/gitea-implementation-spec-review/scripts/review_support.py' spec = importlib.util.spec_from_file_location('readback_invocation_support', path) assert spec is not None and spec.loader is not None module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) calls = [] def pages(page): calls.append(page) return {'items': [{'id': page}], 'total_count': 2, 'has_more': page < 2} inspect.signature(pages).bind(1) self.assertEqual(calls, []) self.assertEqual(module.collect_pages(pages, 'id', page_size=1), [{'id': 1}, {'id': 2}]) self.assertEqual(calls, [1, 2]) if __name__ == '__main__': unittest.main()