251 lines
14 KiB
Python
251 lines
14 KiB
Python
"""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()
|